From 087c270f1bd7fbe35aa9f2eccd97e82c47942708 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sun, 29 Mar 2026 23:25:56 +0300 Subject: [PATCH 01/78] adding resizable support to `OverlayPanel` and refine `SystemInspectorOverlayNode` drag and resize handling; --- .../core/inspector/InspectorController.kt | 63 ++- .../internal/SystemInspectorOverlayNode.kt | 380 ++++++------------ .../dsgl/core/overlay/panel/OverlayPanel.kt | 195 ++++++++- .../overlay/panel/OverlayPanelDragSession.kt | 18 +- .../core/overlay/system/SystemOverlayHost.kt | 96 +++-- 5 files changed, 441 insertions(+), 311 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt index 8475daf..f276bb8 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt @@ -130,7 +130,7 @@ class InspectorController( get() = minimizedPosX to minimizedPosY val isPointerCaptured: Boolean - get() = dragMode != DragMode.None + get() = dragMode != DragMode.None && dragMode != DragMode.ScrollbarThumb val hoveredKey: String? get() = hoveredNode?.key?.toString() @@ -167,6 +167,8 @@ class InspectorController( private var dragStartOffsetX: Int = 0 private var dragStartOffsetY: Int = 0 private var dragMoved: Boolean = false + private var overlayPanelPointerCapture: Boolean = false + private var overlayPanelAuthorityEnabled: Boolean = false private val paneMoveDrag: FloatingPaneDragModel = FloatingPaneDragModel() private var viewportW: Int = 0 private var viewportH: Int = 0 @@ -302,6 +304,7 @@ class InspectorController( minimizedPosY = current.y panelState = InspectorPanelState.Minimized dragMode = DragMode.None + overlayPanelPointerCapture = false dragMoved = false } @@ -311,28 +314,33 @@ class InspectorController( expandedRect = clampExpandedRect(expandedRect, viewportW, viewportH) panelState = InspectorPanelState.Expanded dragMode = DragMode.None + overlayPanelPointerCapture = false dragMoved = false } - fun blocksUnderlyingInput(): Boolean = active && (mode == InspectorMode.Pick || dragMode != DragMode.None) + fun blocksUnderlyingInput(): Boolean = active && ( + mode == InspectorMode.Pick || + dragMode != DragMode.None || + overlayPanelPointerCapture + ) fun shouldConsumePointer(mouseX: Int, mouseY: Int): Boolean { if (!active) return false - if (dragMode != DragMode.None) return true + if (dragMode != DragMode.None || overlayPanelPointerCapture) return true if (editSession.textSelectionDragActive) return true if (mode == InspectorMode.Pick) return true return hitTestUi(mouseX, mouseY) } fun shouldConsumeWheel(mouseX: Int, mouseY: Int): Boolean { if (!active) return false - if (dragMode != DragMode.None) return true + if (dragMode != DragMode.None || overlayPanelPointerCapture) return true if (mode == InspectorMode.Pick) return true return hitTestUi(mouseX, mouseY) } fun shouldConsumeKeyboard(mouseX: Int, mouseY: Int): Boolean { if (!active) return false - if (dragMode != DragMode.None) return true + if (dragMode != DragMode.None || overlayPanelPointerCapture) return true if (mode == InspectorMode.Pick) return true return hitTestUi(mouseX, mouseY) } @@ -619,6 +627,9 @@ class InspectorController( variableTooltipText = null if (panelState == InspectorPanelState.Minimized) { if (minimizedBounds.contains(mouseX, mouseY)) { + if (overlayPanelAuthorityEnabled) { + return true + } startMinimizedMoveDrag(mouseX, mouseY) return true } @@ -636,7 +647,7 @@ class InspectorController( if (shouldCommitActiveEdit(action)) { commitActiveTextEdit() } - if (startScrollbarDrag(mouseX, mouseY)) { + if (!overlayPanelAuthorityEnabled && startScrollbarDrag(mouseX, mouseY)) { return true } if (action != null) { @@ -648,14 +659,16 @@ class InspectorController( return true } editSession.closeAllDropdowns() - val resizeMode = resolveResizeDragMode(mouseX, mouseY) - if (resizeMode != DragMode.None) { - startExpandedDrag(resizeMode, mouseX, mouseY) - return true - } - if (headerBounds.contains(mouseX, mouseY)) { - startExpandedDrag(DragMode.Move, mouseX, mouseY) - return true + if (!overlayPanelAuthorityEnabled) { + val resizeMode = resolveResizeDragMode(mouseX, mouseY) + if (resizeMode != DragMode.None) { + startExpandedDrag(resizeMode, mouseX, mouseY) + return true + } + if (headerBounds.contains(mouseX, mouseY)) { + startExpandedDrag(DragMode.Move, mouseX, mouseY) + return true + } } val hitPanel = if (panelBounds.width > 0 && panelBounds.height > 0) panelBounds else expandedRect if (hitPanel.contains(mouseX, mouseY)) { @@ -1347,6 +1360,21 @@ class InspectorController( minimizedBounds = Rect(0, 0, 0, 0) } + internal fun onOverlayPanelRectChanged(rect: Rect, viewportWidth: Int, viewportHeight: Int) { + onNativeDomExpandedPanelRect(rect, viewportWidth, viewportHeight) + } + + internal fun onOverlayPanelPointerCaptureChanged(captured: Boolean) { + overlayPanelPointerCapture = captured + } + + internal fun setOverlayPanelAuthorityEnabled(enabled: Boolean) { + overlayPanelAuthorityEnabled = enabled + if (enabled && dragMode != DragMode.ScrollbarThumb) { + dragMode = DragMode.None + } + } + internal fun onNativeDomMinimizedPanelPosition(x: Int, y: Int, viewportWidth: Int, viewportHeight: Int) { minimizedPosX = x minimizedPosY = y @@ -1697,6 +1725,8 @@ class InspectorController( hoverPickEnabled = true styleEditorError = null dragMode = DragMode.None + overlayPanelPointerCapture = false + overlayPanelAuthorityEnabled = false dragMoved = false panelScrollY = 0 panelContentHeight = 0 @@ -2170,6 +2200,11 @@ class InspectorController( return currentInspectorRect() } + internal fun debugExpandedPanelRect(): Rect? { + if (!active || panelState != InspectorPanelState.Expanded) return null + return expandedRect + } + private fun performPanelAction(action: PanelAction) { when (action.kind) { ActionKind.Minimize -> { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt index 5090b6c..90f7bcd 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt @@ -17,6 +17,9 @@ import org.dreamfinity.dsgl.core.inspector.InspectorDropdownSnapshot import org.dreamfinity.dsgl.core.inspector.InspectorStyleEditorRowSnapshot import org.dreamfinity.dsgl.core.inspector.InspectorEditorKind import org.dreamfinity.dsgl.core.inspector.InspectorPanelState +import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelDragSession +import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanel +import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelState import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.Overflow import org.dreamfinity.dsgl.core.style.StyleProperty @@ -25,11 +28,25 @@ import java.util.LinkedHashMap internal class SystemInspectorOverlayNode( private val controller: InspectorController, + private val overlayPanel: OverlayPanel, key: Any? = "dsgl-system-inspector" ) : DOMNode(key) { override val styleType: String = "dsgl-system-inspector" override val focusable: Boolean = true + internal constructor( + controller: InspectorController, + key: Any? = "dsgl-system-inspector" + ) : this( + controller = controller, + overlayPanel = OverlayPanel( + ownerId = "standalone-system-inspector", + panelState = OverlayPanelState(), + dragSession = OverlayPanelDragSession() + ), + key = key + ) + private var inspectedRoot: DOMNode? = null private var inspectedLayoutRevision: Long = 0L private var cursorX: Int = 0 @@ -39,28 +56,15 @@ internal class SystemInspectorOverlayNode( private var persistedBodyScrollSession: ScrollSessionSnapshot? = null private val persistedDropdownScrollSession: MutableMap = LinkedHashMap() private var activeDomDropdown: ActiveDomDropdown? = null - private var panelDragSession: PanelDragSession? = null + private val panelNode: DOMNode = overlayPanel.node().applyParent(this) + private var minimizedChipDragSession: MinimizedChipDragSession? = null private data class ActiveDomDropdown( val property: StyleProperty, val unitSelect: Boolean ) - private enum class PanelDragMode { - Move, - ResizeLeft, - ResizeRight, - ResizeTop, - ResizeBottom, - ResizeTopLeft, - ResizeTopRight, - ResizeBottomLeft, - ResizeBottomRight, - MinimizedMove - } - - private data class PanelDragSession( - val mode: PanelDragMode, + private data class MinimizedChipDragSession( val startPointerX: Int, val startPointerY: Int, val startRect: Rect, @@ -77,12 +81,21 @@ internal class SystemInspectorOverlayNode( ) { closeActiveDomDropdown() } - if (isDomOwnedInteractionTarget(event.target)) return@addEventListener + if (handleOverlayPanelMouseDown(event)) { + event.cancelled = true + return@addEventListener + } + val domOwnedTarget = isDomOwnedInteractionTarget(event.target) + if (domOwnedTarget) return@addEventListener if (controller.handleMouseDown(event.mouseX, event.mouseY, event.mouseButton)) { event.cancelled = true } } this@SystemInspectorOverlayNode.addEventListener(Events.MOUSEUP) { event: MouseUpEvent -> + if (handleOverlayPanelMouseUp(event)) { + event.cancelled = true + return@addEventListener + } val routeToController = controller.isPointerCaptured || !isDomOwnedInteractionTarget(event.target) if (!routeToController) return@addEventListener if (controller.handleMouseUp(event.mouseX, event.mouseY, event.mouseButton)) { @@ -90,10 +103,14 @@ internal class SystemInspectorOverlayNode( } } this@SystemInspectorOverlayNode.addEventListener(Events.DRAG) { event: MouseDragEvent -> - if (isDomOwnedInteractionTarget(event.target)) return@addEventListener - if (!controller.isPointerCaptured) return@addEventListener val nextMouseX = event.lastMouseX + event.dx val nextMouseY = event.lastMouseY + event.dy + if (handleOverlayPanelDrag(nextMouseX, nextMouseY)) { + event.cancelled = true + return@addEventListener + } + if (isDomOwnedInteractionTarget(event.target)) return@addEventListener + if (!controller.isPointerCaptured) return@addEventListener controller.onCapturedPointerMove(nextMouseX, nextMouseY, bounds.width, bounds.height) event.cancelled = true } @@ -105,6 +122,56 @@ internal class SystemInspectorOverlayNode( } } + private fun handleOverlayPanelMouseDown(event: MouseDownEvent): Boolean { + if (event.mouseButton != MouseButton.LEFT) return false + val bodyRect = controller.debugContentRect() + val pointerInsideBody = bodyRect.width > 0 && + bodyRect.height > 0 && + bodyRect.contains(event.mouseX, event.mouseY) + if (pointerInsideBody) return false + val handled = overlayPanel.handleMouseDown( + mouseX = event.mouseX, + mouseY = event.mouseY, + button = event.mouseButton, + includeCloseButton = false + ) + if (handled) { + controller.onOverlayPanelPointerCaptureChanged(true) + } + return handled + } + + private fun handleOverlayPanelDrag(mouseX: Int, mouseY: Int): Boolean { + val handled = overlayPanel.handleMouseMove( + mouseX = mouseX, + mouseY = mouseY, + viewportWidth = lastViewportWidth, + viewportHeight = lastViewportHeight + ) { rect -> + controller.onOverlayPanelRectChanged(rect, lastViewportWidth, lastViewportHeight) + } + if (handled) { + controller.onOverlayPanelPointerCaptureChanged(true) + } + return handled + } + + private fun handleOverlayPanelMouseUp(event: MouseUpEvent): Boolean { + val handled = overlayPanel.handleMouseUp( + mouseX = event.mouseX, + mouseY = event.mouseY, + button = event.mouseButton, + viewportWidth = lastViewportWidth, + viewportHeight = lastViewportHeight + ) { rect -> + controller.onOverlayPanelRectChanged(rect, lastViewportWidth, lastViewportHeight) + } + if (handled) { + controller.onOverlayPanelPointerCaptureChanged(false) + } + return handled + } + fun bindInspectedTree(root: DOMNode?, layoutRevision: Long) { inspectedRoot = root inspectedLayoutRevision = layoutRevision @@ -116,8 +183,6 @@ internal class SystemInspectorOverlayNode( cursorY = mouseY } - internal fun isDomPanelDragActive(): Boolean = panelDragSession != null - fun syncInputBounds(viewportWidth: Int, viewportHeight: Int) { val viewportRect = Rect(0, 0, viewportWidth.coerceAtLeast(0), viewportHeight.coerceAtLeast(0)) bounds = resolveInputBounds(viewportRect, controller.debugPanelRect()) @@ -141,18 +206,22 @@ internal class SystemInspectorOverlayNode( val snapshot = controller.buildDomSnapshot(viewportRect.width, viewportRect.height) if (snapshot == null) { clearTree() + panelNode.render(ctx, 0, 0, 0, 0) + children.remove(panelNode) + panelNode.parent = null persistedBodyScrollSession = null persistedDropdownScrollSession.clear() closeActiveDomDropdown() - clearPanelDragSession() + clearMinimizedChipDragSession() controller.onNativeDomBodyScrollState(0, null, null) + controller.onOverlayPanelPointerCaptureChanged(false) return } - reconcilePanelDragSession(snapshot.panelState) bounds = resolveInputBounds(viewportRect, snapshot.panelRect) capturePersistedScrollStateFromCurrentTree() clearTree() + panelNode.render(ctx, x, y, width, height) when (snapshot.panelState) { InspectorPanelState.Minimized -> renderMinimized(ctx, snapshot) InspectorPanelState.Expanded -> renderExpanded(ctx, snapshot, viewportRect.width, viewportRect.height) @@ -163,7 +232,7 @@ internal class SystemInspectorOverlayNode( } private fun resolveInputBounds(viewportRect: Rect, panelRect: Rect?): Rect { - if (controller.blocksUnderlyingInput() || panelDragSession != null) { + if (controller.blocksUnderlyingInput() || overlayPanel.isDragging() || minimizedChipDragSession != null) { return viewportRect } return panelRect ?: viewportRect @@ -171,12 +240,15 @@ internal class SystemInspectorOverlayNode( private fun clearTree() { EventBus.run { - children.forEach { child -> + children.filter { child -> child !== panelNode }.forEach { child -> child.clearListenersDeep() child.parent = null } } - children.clear() + children.retainAll(listOf(panelNode)) + if (panelNode.parent !== this) { + panelNode.applyParent(this) + } markRenderCommandsDirty() } @@ -195,19 +267,19 @@ internal class SystemInspectorOverlayNode( chip.border = Border.all(1, 0xCC4F6076.toInt()) chip.onMouseDown = { event -> if (event.mouseButton == MouseButton.LEFT) { - startPanelDrag(PanelDragMode.MinimizedMove, snapshot.panelRect, event.mouseX, event.mouseY) + startMinimizedChipDrag(snapshot.panelRect, event.mouseX, event.mouseY) event.cancelled = true } } chip.onMouseDrag = { event -> val currentX = event.lastMouseX + event.dx val currentY = event.lastMouseY + event.dy - continuePanelDrag(currentX, currentY) + continueMinimizedChipDrag(currentX, currentY) event.cancelled = true } chip.onMouseUp = { event -> if (event.mouseButton == MouseButton.LEFT) { - endPanelDrag(event.mouseX, event.mouseY) + endMinimizedChipDrag(event.mouseX, event.mouseY) event.cancelled = true } } @@ -241,64 +313,17 @@ internal class SystemInspectorOverlayNode( private fun renderExpanded(ctx: UiMeasureContext, snapshot: InspectorDomSnapshot, viewportWidth: Int, viewportHeight: Int) { val panelRect = snapshot.panelRect - val headerRect = snapshot.headerRect ?: Rect(panelRect.x, panelRect.y, panelRect.width, 42) - val bodyRect = snapshot.bodyRect ?: Rect(panelRect.x, panelRect.y + 42, panelRect.width, panelRect.height - 42) + val bodyRect = snapshot.bodyRect + ?: overlayPanel.bodyRect() + ?: Rect(panelRect.x + 6, panelRect.y + 58, panelRect.width - 12, (panelRect.height - 64).coerceAtLeast(24)) val scope = UiScope(this) renderHighlights(scope, ctx) - val panel = scope.div({ - key = "dsgl-system-inspector-panel" - style = { - display = Display.Block - } - }) - panel.backgroundColor = 0xE0141820.toInt() - panel.border = Border.all(1, 0xCC425062.toInt()) - renderNode(ctx, panel, panelRect) - - val header = scope.div({ - key = "dsgl-system-inspector-header" - style = { - display = Display.Block - } - }) - header.backgroundColor = 0x222D3846 - header.border = Border.all(1, 0x553F4A57) - renderNode(ctx, header, headerRect) - val pickRect = controller.debugPickToggleBounds() - ?: Rect(headerRect.x + headerRect.width - 264, headerRect.y + 8, 160, (headerRect.height - 16).coerceAtLeast(22)) + ?: Rect(panelRect.x + panelRect.width - 264, panelRect.y + 8, 160, 36) val minimizeRect = controller.debugMinimizeBounds() - ?: Rect(headerRect.x + headerRect.width - 96, headerRect.y + 8, 86, (headerRect.height - 16).coerceAtLeast(22)) - - val titleNode = scope.text(props = { - key = "dsgl-system-inspector-header-title" - value = snapshot.headerText - style = { - textWrap = TextWrap.NoWrap - } - }) - titleNode.color = 0xFFE6EDF6.toInt() - titleNode.fontSize = 24 - renderNode( - ctx, - titleNode, - Rect( - headerRect.x + 8, - headerRect.y + 6, - (pickRect.x - headerRect.x - 14).coerceAtLeast(40), - (headerRect.height - 10).coerceAtLeast(24) - ) - ) - val headerDragRect = Rect( - headerRect.x + 8, - headerRect.y + 6, - (pickRect.x - headerRect.x - 14).coerceAtLeast(40), - (headerRect.height - 10).coerceAtLeast(24) - ) - renderPanelDragHandle(scope, ctx, panelRect, headerDragRect) - renderPanelResizeHandles(scope, ctx, panelRect) + ?: Rect(panelRect.x + panelRect.width - 96, panelRect.y + 8, 86, 36) val pickButton = scope.button("Select Element", { key = "dsgl-system-inspector-pick-toggle" @@ -924,194 +949,44 @@ internal class SystemInspectorOverlayNode( return true } - private fun renderPanelDragHandle(scope: UiScope, ctx: UiMeasureContext, panelRect: Rect, headerDragRect: Rect) { - val handle = scope.div({ - key = "dsgl-system-inspector-header-drag-handle" - style = { - display = Display.Block - } - }) - handle.backgroundColor = 0 - handle.onMouseDown = { event -> - if (event.mouseButton == MouseButton.LEFT) { - startPanelDrag(PanelDragMode.Move, panelRect, event.mouseX, event.mouseY) - event.cancelled = true - } - } - handle.onMouseDrag = { event -> - val currentX = event.lastMouseX + event.dx - val currentY = event.lastMouseY + event.dy - continuePanelDrag(currentX, currentY) - event.cancelled = true - } - handle.onMouseUp = { event -> - if (event.mouseButton == MouseButton.LEFT) { - endPanelDrag(event.mouseX, event.mouseY) - event.cancelled = true - } - } - renderNode(ctx, handle, headerDragRect) - } - - private fun renderPanelResizeHandles(scope: UiScope, ctx: UiMeasureContext, panelRect: Rect) { - val edge = 6 - val corner = 10 - renderResizeHandle(scope, ctx, "dsgl-system-inspector-resize-handle-left", Rect(panelRect.x, panelRect.y + corner, edge, (panelRect.height - corner * 2).coerceAtLeast(1)), PanelDragMode.ResizeLeft, panelRect) - renderResizeHandle(scope, ctx, "dsgl-system-inspector-resize-handle-right", Rect(panelRect.x + panelRect.width - edge, panelRect.y + corner, edge, (panelRect.height - corner * 2).coerceAtLeast(1)), PanelDragMode.ResizeRight, panelRect) - renderResizeHandle(scope, ctx, "dsgl-system-inspector-resize-handle-top", Rect(panelRect.x + corner, panelRect.y, (panelRect.width - corner * 2).coerceAtLeast(1), edge), PanelDragMode.ResizeTop, panelRect) - renderResizeHandle(scope, ctx, "dsgl-system-inspector-resize-handle-bottom", Rect(panelRect.x + corner, panelRect.y + panelRect.height - edge, (panelRect.width - corner * 2).coerceAtLeast(1), edge), PanelDragMode.ResizeBottom, panelRect) - renderResizeHandle(scope, ctx, "dsgl-system-inspector-resize-handle-top-left", Rect(panelRect.x, panelRect.y, corner, corner), PanelDragMode.ResizeTopLeft, panelRect) - renderResizeHandle(scope, ctx, "dsgl-system-inspector-resize-handle-top-right", Rect(panelRect.x + panelRect.width - corner, panelRect.y, corner, corner), PanelDragMode.ResizeTopRight, panelRect) - renderResizeHandle(scope, ctx, "dsgl-system-inspector-resize-handle-bottom-left", Rect(panelRect.x, panelRect.y + panelRect.height - corner, corner, corner), PanelDragMode.ResizeBottomLeft, panelRect) - renderResizeHandle(scope, ctx, "dsgl-system-inspector-resize-handle-bottom-right", Rect(panelRect.x + panelRect.width - corner, panelRect.y + panelRect.height - corner, corner, corner), PanelDragMode.ResizeBottomRight, panelRect) - } - - private fun renderResizeHandle( - scope: UiScope, - ctx: UiMeasureContext, - key: String, - rect: Rect, - mode: PanelDragMode, - panelRect: Rect - ) { - val handle = scope.div({ - this.key = key - style = { - display = Display.Block - } - }) - handle.backgroundColor = 0 - handle.onMouseDown = { event -> - if (event.mouseButton == MouseButton.LEFT) { - startPanelDrag(mode, panelRect, event.mouseX, event.mouseY) - event.cancelled = true - } - } - handle.onMouseDrag = { event -> - val currentX = event.lastMouseX + event.dx - val currentY = event.lastMouseY + event.dy - continuePanelDrag(currentX, currentY) - event.cancelled = true - } - handle.onMouseUp = { event -> - if (event.mouseButton == MouseButton.LEFT) { - endPanelDrag(event.mouseX, event.mouseY) - event.cancelled = true - } - } - renderNode(ctx, handle, rect) - } - - private fun startPanelDrag(mode: PanelDragMode, panelRect: Rect, mouseX: Int, mouseY: Int) { - panelDragSession = PanelDragSession( - mode = mode, + private fun startMinimizedChipDrag(panelRect: Rect, mouseX: Int, mouseY: Int) { + minimizedChipDragSession = MinimizedChipDragSession( startPointerX = mouseX, startPointerY = mouseY, startRect = panelRect, moved = false ) + controller.onOverlayPanelPointerCaptureChanged(true) } - private fun continuePanelDrag(mouseX: Int, mouseY: Int) { - val session = panelDragSession ?: return + private fun continueMinimizedChipDrag(mouseX: Int, mouseY: Int) { + val session = minimizedChipDragSession ?: return val dx = mouseX - session.startPointerX val dy = mouseY - session.startPointerY if (!session.moved && (kotlin.math.abs(dx) >= 2 || kotlin.math.abs(dy) >= 2)) { session.moved = true } - when (session.mode) { - PanelDragMode.MinimizedMove -> { - controller.onNativeDomMinimizedPanelPosition( - x = session.startRect.x + dx, - y = session.startRect.y + dy, - viewportWidth = lastViewportWidth, - viewportHeight = lastViewportHeight - ) - } - - PanelDragMode.Move -> { - controller.onNativeDomExpandedPanelRect( - rect = Rect( - session.startRect.x + dx, - session.startRect.y + dy, - session.startRect.width, - session.startRect.height - ), - viewportWidth = lastViewportWidth, - viewportHeight = lastViewportHeight - ) - } - - else -> { - controller.onNativeDomExpandedPanelRect( - rect = resolveResizedPanelRect(session, dx, dy), - viewportWidth = lastViewportWidth, - viewportHeight = lastViewportHeight - ) - } - } + controller.onNativeDomMinimizedPanelPosition( + x = session.startRect.x + dx, + y = session.startRect.y + dy, + viewportWidth = lastViewportWidth, + viewportHeight = lastViewportHeight + ) } - private fun endPanelDrag(mouseX: Int, mouseY: Int) { - val session = panelDragSession ?: return - continuePanelDrag(mouseX, mouseY) - if (session.mode == PanelDragMode.MinimizedMove && !session.moved) { + private fun endMinimizedChipDrag(mouseX: Int, mouseY: Int) { + val session = minimizedChipDragSession ?: return + continueMinimizedChipDrag(mouseX, mouseY) + if (!session.moved) { controller.restore() } - panelDragSession = null - } - - private fun clearPanelDragSession() { - panelDragSession = null - } - - private fun reconcilePanelDragSession(panelState: InspectorPanelState) { - val session = panelDragSession ?: return - if (panelState == InspectorPanelState.Minimized && session.mode != PanelDragMode.MinimizedMove) { - panelDragSession = null - return - } - if (panelState == InspectorPanelState.Expanded && session.mode == PanelDragMode.MinimizedMove) { - panelDragSession = null - } + minimizedChipDragSession = null + controller.onOverlayPanelPointerCaptureChanged(false) } - private fun resolveResizedPanelRect(session: PanelDragSession, dx: Int, dy: Int): Rect { - var left = session.startRect.x - var top = session.startRect.y - var right = session.startRect.x + session.startRect.width - var bottom = session.startRect.y + session.startRect.height - - when (session.mode) { - PanelDragMode.ResizeLeft, - PanelDragMode.ResizeTopLeft, - PanelDragMode.ResizeBottomLeft -> left += dx - - PanelDragMode.ResizeRight, - PanelDragMode.ResizeTopRight, - PanelDragMode.ResizeBottomRight -> right += dx - - else -> Unit - } - - when (session.mode) { - PanelDragMode.ResizeTop, - PanelDragMode.ResizeTopLeft, - PanelDragMode.ResizeTopRight -> top += dy - - PanelDragMode.ResizeBottom, - PanelDragMode.ResizeBottomLeft, - PanelDragMode.ResizeBottomRight -> bottom += dy - - else -> Unit - } - - return Rect( - x = left, - y = top, - width = (right - left).coerceAtLeast(1), - height = (bottom - top).coerceAtLeast(1) - ) + private fun clearMinimizedChipDragSession() { + minimizedChipDragSession = null + controller.onOverlayPanelPointerCaptureChanged(false) } private fun renderTooltip( scope: UiScope, @@ -1163,6 +1038,9 @@ internal class SystemInspectorOverlayNode( if (nodeKey?.startsWith("dsgl-system-inspector-") == true) { return true } + if (nodeKey?.startsWith("dsgl-overlay-panel-") == true) { + return true + } current = current.parent } return current === this && target !== this diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanel.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanel.kt index 0527020..74f4b4b 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanel.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanel.kt @@ -18,6 +18,7 @@ import org.dreamfinity.dsgl.core.style.TextWrap data class OverlayPanelStyle( val headerHeight: Int = 26, val panelPadding: Int = 6, + val resizeHandleSize: Int = 8, val closeButtonWidth: Int = 16, val closeButtonHeight: Int = 16, val closeButtonMarginTop: Int = 4, @@ -62,6 +63,12 @@ class OverlayPanel( private set var draggable: Boolean = true private set + var resizable: Boolean = false + private set + var minWidth: Int = 120 + private set + var minHeight: Int = 80 + private set private var onClose: (() -> Unit)? = null private var frame: OverlayPanelFrame? = null @@ -123,6 +130,9 @@ class OverlayPanel( fun configure( title: String, draggable: Boolean, + resizable: Boolean = this.resizable, + minWidth: Int = this.minWidth, + minHeight: Int = this.minHeight, style: OverlayPanelStyle = this.style, onClose: (() -> Unit)? = this.onClose ) { @@ -130,6 +140,9 @@ class OverlayPanel( val styleChanged = this.style != style this.title = title this.draggable = draggable + this.resizable = resizable + this.minWidth = minWidth.coerceAtLeast(1) + this.minHeight = minHeight.coerceAtLeast(1) this.style = style this.onClose = onClose if (titleChanged) { @@ -160,22 +173,46 @@ class OverlayPanel( fun bodyRect(): Rect? = frame?.bodyRect - fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { + fun isDragging(): Boolean = dragSession.active + + fun beginHeaderDrag(mouseX: Int, mouseY: Int): Boolean { + val localFrame = frame ?: return false + if (!draggable) return false + if (!localFrame.headerRect.contains(mouseX, mouseY)) return false + if (localFrame.closeRect.contains(mouseX, mouseY)) return false + beginMoveDrag(mouseX, mouseY) + return true + } + + fun handleMouseDown( + mouseX: Int, + mouseY: Int, + button: MouseButton, + includeCloseButton: Boolean = true + ): Boolean { val localFrame = frame ?: return false if (button != MouseButton.LEFT) return false - if (localFrame.closeRect.contains(mouseX, mouseY)) { + if (includeCloseButton && localFrame.closeRect.contains(mouseX, mouseY)) { onClose?.invoke() return true } + if (resizable && !localFrame.bodyRect.contains(mouseX, mouseY)) { + val resizeHandle = resolveResizeHandle(localFrame.panelRect, mouseX, mouseY) + if (resizeHandle != null) { + dragSession.begin( + ownerId = ownerId, + type = OverlayPanelDragType.PanelResize, + pointerX = mouseX, + pointerY = mouseY, + panelState = panelState, + resizeHandle = resizeHandle + ) + return true + } + } if (!draggable) return false if (!localFrame.headerRect.contains(mouseX, mouseY)) return false - dragSession.begin( - ownerId = ownerId, - type = OverlayPanelDragType.PanelMove, - pointerX = mouseX, - pointerY = mouseY, - panelState = panelState - ) + beginMoveDrag(mouseX, mouseY) return true } @@ -214,6 +251,24 @@ class OverlayPanel( } private fun buildDraggedRect(viewportWidth: Int, viewportHeight: Int): Rect { + return when (dragSession.type) { + OverlayPanelDragType.PanelResize -> buildResizedRect(viewportWidth, viewportHeight) + else -> buildMovedRect(viewportWidth, viewportHeight) + } + } + + private fun beginMoveDrag(mouseX: Int, mouseY: Int) { + dragSession.begin( + ownerId = ownerId, + type = OverlayPanelDragType.PanelMove, + pointerX = mouseX, + pointerY = mouseY, + panelState = panelState, + resizeHandle = null + ) + } + + private fun buildMovedRect(viewportWidth: Int, viewportHeight: Int): Rect { val dx = dragSession.currentPointerX - dragSession.startPointerX val dy = dragSession.currentPointerY - dragSession.startPointerY val raw = Rect( @@ -225,6 +280,109 @@ class OverlayPanel( return clampPanel(raw, viewportWidth, viewportHeight) } + private fun buildResizedRect(viewportWidth: Int, viewportHeight: Int): Rect { + val handle = dragSession.resizeHandle ?: return buildMovedRect(viewportWidth, viewportHeight) + val dx = dragSession.currentPointerX - dragSession.startPointerX + val dy = dragSession.currentPointerY - dragSession.startPointerY + + var left = dragSession.startPanelX + var top = dragSession.startPanelY + var right = dragSession.startPanelX + dragSession.startPanelWidth + var bottom = dragSession.startPanelY + dragSession.startPanelHeight + + when (handle) { + OverlayPanelResizeHandle.Left, + OverlayPanelResizeHandle.TopLeft, + OverlayPanelResizeHandle.BottomLeft -> left += dx + + OverlayPanelResizeHandle.Right, + OverlayPanelResizeHandle.TopRight, + OverlayPanelResizeHandle.BottomRight -> right += dx + + else -> Unit + } + when (handle) { + OverlayPanelResizeHandle.Top, + OverlayPanelResizeHandle.TopLeft, + OverlayPanelResizeHandle.TopRight -> top += dy + + OverlayPanelResizeHandle.Bottom, + OverlayPanelResizeHandle.BottomLeft, + OverlayPanelResizeHandle.BottomRight -> bottom += dy + + else -> Unit + } + + if (right - left < minWidth) { + when (handle) { + OverlayPanelResizeHandle.Left, + OverlayPanelResizeHandle.TopLeft, + OverlayPanelResizeHandle.BottomLeft -> left = right - minWidth + + OverlayPanelResizeHandle.Right, + OverlayPanelResizeHandle.TopRight, + OverlayPanelResizeHandle.BottomRight -> right = left + minWidth + + else -> right = left + minWidth + } + } + if (bottom - top < minHeight) { + when (handle) { + OverlayPanelResizeHandle.Top, + OverlayPanelResizeHandle.TopLeft, + OverlayPanelResizeHandle.TopRight -> top = bottom - minHeight + + OverlayPanelResizeHandle.Bottom, + OverlayPanelResizeHandle.BottomLeft, + OverlayPanelResizeHandle.BottomRight -> bottom = top + minHeight + + else -> bottom = top + minHeight + } + } + + val minX = 2 + val minY = 2 + val maxRight = (viewportWidth - 2).coerceAtLeast(minX + minWidth) + val maxBottom = (viewportHeight - 2).coerceAtLeast(minY + minHeight) + + when (handle) { + OverlayPanelResizeHandle.Left, + OverlayPanelResizeHandle.TopLeft, + OverlayPanelResizeHandle.BottomLeft -> left = left.coerceIn(minX, right - minWidth) + + OverlayPanelResizeHandle.Right, + OverlayPanelResizeHandle.TopRight, + OverlayPanelResizeHandle.BottomRight -> right = right.coerceIn(left + minWidth, maxRight) + + else -> Unit + } + when (handle) { + OverlayPanelResizeHandle.Top, + OverlayPanelResizeHandle.TopLeft, + OverlayPanelResizeHandle.TopRight -> top = top.coerceIn(minY, bottom - minHeight) + + OverlayPanelResizeHandle.Bottom, + OverlayPanelResizeHandle.BottomLeft, + OverlayPanelResizeHandle.BottomRight -> bottom = bottom.coerceIn(top + minHeight, maxBottom) + + else -> Unit + } + + val width = (right - left).coerceAtLeast(minWidth) + val height = (bottom - top).coerceAtLeast(minHeight) + val clamped = clampPanel( + Rect(left, top, width, height), + viewportWidth = viewportWidth, + viewportHeight = viewportHeight + ) + return Rect( + x = clamped.x, + y = clamped.y, + width = clamped.width.coerceAtLeast(minWidth), + height = clamped.height.coerceAtLeast(minHeight) + ) + } + private fun rebuildFrameFromState() { val panelRect = panelState.currentRectOrNull() frame = if (panelRect == null) { @@ -269,6 +427,25 @@ class OverlayPanel( ) } + private fun resolveResizeHandle(panelRect: Rect, mouseX: Int, mouseY: Int): OverlayPanelResizeHandle? { + if (!panelRect.contains(mouseX, mouseY)) return null + val edge = style.resizeHandleSize.coerceAtLeast(2) + val leftZone = mouseX <= panelRect.x + edge + val rightZone = mouseX >= panelRect.x + panelRect.width - edge + val topZone = mouseY <= panelRect.y + edge + val bottomZone = mouseY >= panelRect.y + panelRect.height - edge + + if (leftZone && topZone) return OverlayPanelResizeHandle.TopLeft + if (rightZone && topZone) return OverlayPanelResizeHandle.TopRight + if (leftZone && bottomZone) return OverlayPanelResizeHandle.BottomLeft + if (rightZone && bottomZone) return OverlayPanelResizeHandle.BottomRight + if (leftZone) return OverlayPanelResizeHandle.Left + if (rightZone) return OverlayPanelResizeHandle.Right + if (topZone) return OverlayPanelResizeHandle.Top + if (bottomZone) return OverlayPanelResizeHandle.Bottom + return null + } + private fun applyStyleToNodes(style: OverlayPanelStyle) { shadowNode.applyStyle { backgroundColor = style.panelShadowColor diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanelDragSession.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanelDragSession.kt index 7eae913..fdd27bc 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanelDragSession.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanelDragSession.kt @@ -6,6 +6,17 @@ enum class OverlayPanelDragType { Transient } +enum class OverlayPanelResizeHandle { + Left, + Right, + Top, + Bottom, + TopLeft, + TopRight, + BottomLeft, + BottomRight +} + class OverlayPanelDragSession { var active: Boolean = false private set @@ -29,17 +40,21 @@ class OverlayPanelDragSession { private set var startPanelHeight: Int = 0 private set + var resizeHandle: OverlayPanelResizeHandle? = null + private set fun begin( ownerId: Any, type: OverlayPanelDragType, pointerX: Int, pointerY: Int, - panelState: OverlayPanelState + panelState: OverlayPanelState, + resizeHandle: OverlayPanelResizeHandle? = null ) { active = true this.ownerId = ownerId this.type = type + this.resizeHandle = resizeHandle startPointerX = pointerX startPointerY = pointerY currentPointerX = pointerX @@ -60,5 +75,6 @@ class OverlayPanelDragSession { active = false ownerId = null type = null + resizeHandle = null } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt index b2a91f6..e774781 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt @@ -10,12 +10,12 @@ import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController +import org.dreamfinity.dsgl.core.inspector.InspectorPanelState import org.dreamfinity.dsgl.core.inspector.internal.SystemInspectorOverlayNode import org.dreamfinity.dsgl.core.overlay.OverlayLayerHost import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.overlay.UiLayerId import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanel -import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelDragType import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelStyle import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand @@ -249,7 +249,7 @@ class SystemOverlayHost( private inline fun dispatchManualInput(handler: (SystemOverlayEntry) -> Boolean): Boolean { return activeEntriesTopFirst() .asSequence() - .filter { !it.participatesInDomInput() } + .filter { entry -> !entry.participatesInDomInput() } .any(handler) } @@ -261,7 +261,15 @@ class SystemOverlayHost( order = 100, lane = SystemOverlayLane.PanelContent ) - override val node: SystemInspectorOverlayNode = SystemInspectorOverlayNode(inspectorController) + private val overlayPanel: OverlayPanel = OverlayPanel( + ownerId = state.id, + panelState = state.panelState, + dragSession = state.dragSession + ) + override val node: SystemInspectorOverlayNode = SystemInspectorOverlayNode( + controller = inspectorController, + overlayPanel = overlayPanel + ) private var viewportWidth: Int = 1 private var viewportHeight: Int = 1 @@ -280,24 +288,46 @@ class SystemOverlayHost( inspectorController.onLayoutCommitted(root, frame.inspectedLayoutRevision) } state.active = inspectorController.active + inspectorController.setOverlayPanelAuthorityEnabled(state.active) if (!state.active) { state.panelState.hide() state.dragSession.end() + overlayPanel.syncPanelRect(null) + inspectorController.onOverlayPanelPointerCaptureChanged(false) return } - val panelRect = inspectorController.debugPanelRect() - if (panelRect != null) { - state.panelState.updateFromRect(panelRect) + if (inspectorController.panelState == InspectorPanelState.Expanded) { + overlayPanel.configure( + title = "Inspector", + draggable = true, + resizable = true, + minWidth = 240, + minHeight = 160, + style = inspectorPanelStyle(), + onClose = inspectorController::onPanelMinimizeTogglePressed + ) + val panelRect = inspectorController.debugExpandedPanelRect() + if (panelRect != null) { + inspectorController.onOverlayPanelRectChanged(panelRect, viewportWidth, viewportHeight) + overlayPanel.syncPanelRect(inspectorController.debugExpandedPanelRect()) + } else { + state.panelState.show() + overlayPanel.syncPanelRect(state.panelState.currentRectOrNull()) + } + overlayPanel.handleMouseMove( + mouseX = frame.cursorX, + mouseY = frame.cursorY, + viewportWidth = viewportWidth, + viewportHeight = viewportHeight + ) { rect -> + inspectorController.onOverlayPanelRectChanged(rect, viewportWidth, viewportHeight) + } } else { - state.panelState.show() + state.panelState.hide() + state.dragSession.end() + overlayPanel.syncPanelRect(null) + inspectorController.onOverlayPanelPointerCaptureChanged(false) } - syncDragSession( - entryState = state, - dragging = node.isDomPanelDragActive(), - dragType = OverlayPanelDragType.PanelMove, - pointerX = frame.cursorX, - pointerY = frame.cursorY - ) } } @@ -643,28 +673,22 @@ class SystemOverlayHost( } private companion object { - private fun syncDragSession( - entryState: SystemOverlayEntryState, - dragging: Boolean, - dragType: OverlayPanelDragType, - pointerX: Int, - pointerY: Int - ) { - if (dragging) { - if (!entryState.dragSession.active) { - entryState.dragSession.begin( - ownerId = entryState.id, - type = dragType, - pointerX = pointerX, - pointerY = pointerY, - panelState = entryState.panelState - ) - } else { - entryState.dragSession.update(pointerX, pointerY) - } - return - } - entryState.dragSession.end() + private fun inspectorPanelStyle(): OverlayPanelStyle { + return OverlayPanelStyle( + headerHeight = 52, + panelPadding = 6, + resizeHandleSize = 8, + panelBackgroundColor = 0xE0141820.toInt(), + panelBorderColor = 0xCC425062.toInt(), + panelShadowColor = 0x7A0C1118, + headerBackgroundColor = 0x222D3846, + headerBorderColor = 0x553F4A57, + closeButtonBackgroundColor = 0x3346596E, + closeButtonBorderColor = 0x775E738C, + textColor = 0xFFE6EDF6.toInt(), + fontSize = 24, + closeGlyph = "-" + ) } private fun toOverlayPanelStyle(style: ColorPickerStyle): OverlayPanelStyle { From 82d772d99a462f3dd7f634df7d5842d53046dfe0 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Mon, 30 Mar 2026 00:11:44 +0300 Subject: [PATCH 02/78] updating inspector toggle shortcut to F12 and refining minimized chip drag handling in `SystemInspectorOverlayNode`; --- .../core/inspector/InspectorController.kt | 4 +- .../internal/SystemInspectorOverlayNode.kt | 47 ++++++++++++++++--- .../demo/sections/ColorPickerSection.kt | 2 +- .../mc1710/demo/sections/InspectorSection.kt | 2 +- .../dsgl/mc1710/demo/support/DemoSection.kt | 2 +- 5 files changed, 45 insertions(+), 12 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt index f276bb8..ad11378 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt @@ -810,7 +810,7 @@ class InspectorController( var y = bodyRect.y val maxChars = estimateMaxChars(bodyRect.width - 12, textFontSizePx) val buttonLabelMaxChars = estimateMaxChars((clamped.width - 32).coerceAtLeast(40), secondaryFontSizePx) - y = appendDomLine(infoLines, y, "F8 toggle, F9 mode, Esc cancel pick", maxChars) + y = appendDomLine(infoLines, y, "F12 toggle, F9 mode, Esc cancel pick", maxChars) y = appendDomLine(infoLines, y, "Hovered: ${hoveredNode?.let { nodeLabel(it) } ?: "none"}", maxChars) y = appendDomLine(infoLines, y, "Selected: ${selectedNode?.let { nodeLabel(it) } ?: "none"}", maxChars) y = appendDomLine(infoLines, y, "Inspector handled last: $lastHandledPointerEvent", maxChars) @@ -1962,7 +1962,7 @@ class InspectorController( out += RenderCommand.PushClip(bodyRect.x, bodyRect.y, bodyRect.width, bodyRect.height) var y = bodyRect.y - y = appendPanelLine(out, bodyRect, y, "F8 toggle, F9 mode, Esc cancel pick", maxChars, panelScrollY) + y = appendPanelLine(out, bodyRect, y, "F12 toggle, F9 mode, Esc cancel pick", maxChars, panelScrollY) y = appendPanelLine( out, bodyRect, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt index 90f7bcd..4c52e5d 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt @@ -68,6 +68,8 @@ internal class SystemInspectorOverlayNode( val startPointerX: Int, val startPointerY: Int, val startRect: Rect, + var currentPointerX: Int, + var currentPointerY: Int, var moved: Boolean = false ) @@ -181,6 +183,7 @@ internal class SystemInspectorOverlayNode( fun updateCursor(mouseX: Int, mouseY: Int, pointerCaptured: Boolean) { cursorX = mouseX cursorY = mouseY + updateMinimizedChipDragPointer(mouseX, mouseY) } fun syncInputBounds(viewportWidth: Int, viewportHeight: Int) { @@ -201,6 +204,7 @@ internal class SystemInspectorOverlayNode( controller.onCursorMoved(cursorX, cursorY) lastViewportWidth = viewportRect.width.coerceAtLeast(1) lastViewportHeight = viewportRect.height.coerceAtLeast(1) + applyMinimizedChipDragFrame() val retainInspectorFocus = shouldRetainInspectorSubtreeFocus() val snapshot = controller.buildDomSnapshot(viewportRect.width, viewportRect.height) @@ -221,10 +225,9 @@ internal class SystemInspectorOverlayNode( capturePersistedScrollStateFromCurrentTree() clearTree() - panelNode.render(ctx, x, y, width, height) when (snapshot.panelState) { InspectorPanelState.Minimized -> renderMinimized(ctx, snapshot) - InspectorPanelState.Expanded -> renderExpanded(ctx, snapshot, viewportRect.width, viewportRect.height) + InspectorPanelState.Expanded -> renderExpanded(ctx, snapshot, viewportRect) } if (retainInspectorFocus) { FocusManager.retainFocus(this, updateRootReference = false) @@ -254,6 +257,7 @@ internal class SystemInspectorOverlayNode( private fun renderMinimized(ctx: UiMeasureContext, snapshot: InspectorDomSnapshot) { closeActiveDomDropdown() + panelNode.render(ctx, 0, 0, 0, 0) val scope = UiScope(this) renderHighlights(scope, ctx) @@ -274,7 +278,7 @@ internal class SystemInspectorOverlayNode( chip.onMouseDrag = { event -> val currentX = event.lastMouseX + event.dx val currentY = event.lastMouseY + event.dy - continueMinimizedChipDrag(currentX, currentY) + updateMinimizedChipDragPointer(currentX, currentY) event.cancelled = true } chip.onMouseUp = { event -> @@ -311,7 +315,9 @@ internal class SystemInspectorOverlayNode( } } - private fun renderExpanded(ctx: UiMeasureContext, snapshot: InspectorDomSnapshot, viewportWidth: Int, viewportHeight: Int) { + private fun renderExpanded(ctx: UiMeasureContext, snapshot: InspectorDomSnapshot, viewportRect: Rect) { + val viewportWidth = viewportRect.width + val viewportHeight = viewportRect.height val panelRect = snapshot.panelRect val bodyRect = snapshot.bodyRect ?: overlayPanel.bodyRect() @@ -319,6 +325,8 @@ internal class SystemInspectorOverlayNode( val scope = UiScope(this) renderHighlights(scope, ctx) + renderPanelOccluder(scope, ctx, panelRect) + panelNode.render(ctx, viewportRect.x, viewportRect.y, viewportRect.width, viewportRect.height) val pickRect = controller.debugPickToggleBounds() ?: Rect(panelRect.x + panelRect.width - 264, panelRect.y + 8, 160, 36) @@ -476,6 +484,18 @@ internal class SystemInspectorOverlayNode( renderTooltip(scope, ctx, "dsgl-system-inspector-cursor-tooltip", controller.debugCursorTooltip(), 0xDD11151A.toInt(), 0xCC3F4A57.toInt()) } + private fun renderPanelOccluder(scope: UiScope, ctx: UiMeasureContext, panelRect: Rect) { + val occluder = scope.div({ + key = "dsgl-system-inspector-panel-occluder" + style = { + display = Display.Block + } + }) + occluder.backgroundColor = 0xFF141820.toInt() + occluder.border = Border.NONE + renderNode(ctx, occluder, panelRect) + } + private fun renderHighlights(scope: UiScope, ctx: UiMeasureContext) { controller.debugSelectedHighlight()?.let { highlight -> renderHighlightRect(scope, ctx, "dsgl-system-inspector-selected-margin-fill", highlight.marginRect, 0x44F3B33D, null) @@ -954,15 +974,23 @@ internal class SystemInspectorOverlayNode( startPointerX = mouseX, startPointerY = mouseY, startRect = panelRect, + currentPointerX = mouseX, + currentPointerY = mouseY, moved = false ) controller.onOverlayPanelPointerCaptureChanged(true) } - private fun continueMinimizedChipDrag(mouseX: Int, mouseY: Int) { + private fun updateMinimizedChipDragPointer(mouseX: Int, mouseY: Int) { val session = minimizedChipDragSession ?: return - val dx = mouseX - session.startPointerX - val dy = mouseY - session.startPointerY + session.currentPointerX = mouseX + session.currentPointerY = mouseY + } + + private fun applyMinimizedChipDragFrame() { + val session = minimizedChipDragSession ?: return + val dx = session.currentPointerX - session.startPointerX + val dy = session.currentPointerY - session.startPointerY if (!session.moved && (kotlin.math.abs(dx) >= 2 || kotlin.math.abs(dy) >= 2)) { session.moved = true } @@ -974,6 +1002,11 @@ internal class SystemInspectorOverlayNode( ) } + private fun continueMinimizedChipDrag(mouseX: Int, mouseY: Int) { + updateMinimizedChipDragPointer(mouseX, mouseY) + applyMinimizedChipDragFrame() + } + private fun endMinimizedChipDrag(mouseX: Int, mouseY: Int) { val session = minimizedChipDragSession ?: return continueMinimizedChipDrag(mouseX, mouseY) diff --git a/mc1710-demo/src/main/kotlin/org/dreamfinity/dsgl/mc1710/demo/sections/ColorPickerSection.kt b/mc1710-demo/src/main/kotlin/org/dreamfinity/dsgl/mc1710/demo/sections/ColorPickerSection.kt index 5093f5a..a68569f 100644 --- a/mc1710-demo/src/main/kotlin/org/dreamfinity/dsgl/mc1710/demo/sections/ColorPickerSection.kt +++ b/mc1710-demo/src/main/kotlin/org/dreamfinity/dsgl/mc1710/demo/sections/ColorPickerSection.kt @@ -77,7 +77,7 @@ fun UiScope.colorPickerSection() { { style = { color = DEMO_MUTED } } ) text( - "Inline picker follows app styling. Inspector picker (F8) is rendered in isolated system overlay styles.", + "Inline picker follows app styling. Inspector picker (F12) is rendered in isolated system overlay styles.", { style = { color = DEMO_MUTED } } ) diff --git a/mc1710-demo/src/main/kotlin/org/dreamfinity/dsgl/mc1710/demo/sections/InspectorSection.kt b/mc1710-demo/src/main/kotlin/org/dreamfinity/dsgl/mc1710/demo/sections/InspectorSection.kt index 452cf27..9941b94 100644 --- a/mc1710-demo/src/main/kotlin/org/dreamfinity/dsgl/mc1710/demo/sections/InspectorSection.kt +++ b/mc1710-demo/src/main/kotlin/org/dreamfinity/dsgl/mc1710/demo/sections/InspectorSection.kt @@ -22,7 +22,7 @@ fun UiScope.inspectorSection(onInfo: (String) -> Unit) { } }) { text("In-game Inspector is global (works on every DSGL screen).") - text("F8: toggle inspector overlay", { style = { color = INSPECTOR_MUTED_TEXT } }) + text("F12: toggle inspector overlay", { style = { color = INSPECTOR_MUTED_TEXT } }) text("F9: switch mode (Pick/Locked)", { style = { color = INSPECTOR_MUTED_TEXT } }) text("Expanded panel: click Min to collapse into floating chip.", { style = { color = INSPECTOR_MUTED_TEXT } }) text("Minimized chip: drag to move, click (no drag) to restore.", { style = { color = INSPECTOR_MUTED_TEXT } }) diff --git a/mc1710-demo/src/main/kotlin/org/dreamfinity/dsgl/mc1710/demo/support/DemoSection.kt b/mc1710-demo/src/main/kotlin/org/dreamfinity/dsgl/mc1710/demo/support/DemoSection.kt index a1a1535..5b3d91a 100644 --- a/mc1710-demo/src/main/kotlin/org/dreamfinity/dsgl/mc1710/demo/support/DemoSection.kt +++ b/mc1710-demo/src/main/kotlin/org/dreamfinity/dsgl/mc1710/demo/support/DemoSection.kt @@ -5,7 +5,7 @@ enum class DemoSection( val subtitle: String ) { OVERVIEW("Overview", "How to use the showcase"), - INSPECTOR("Inspector", "Global in-game element/style/layout inspector (F8/F9)"), + INSPECTOR("Inspector", "Global in-game element/style/layout inspector (F12/F9)"), LAYOUT_STYLE("Layout & Style", "Containers, gaps, fixed sizes, style DSL"), LAYOUT_DEBUG("Layout Debug", "Strict bounds validator and diagnostics"), POSITIONED_LAYOUT("Positioned Layout", "static/relative/absolute/fixed/sticky + z-index overlap, scroll and hit-testing"), From cb88ae483de4e90b8f3a30fedaf51787636825da Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Mon, 30 Mar 2026 00:37:54 +0300 Subject: [PATCH 03/78] removing unused variables, data classes, methods, and dropdown overlays from `InspectorController` as part of dead code cleanup; --- .../core/inspector/InspectorController.kt | 899 ------------------ 1 file changed, 899 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt index ad11378..6c3c8ec 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt @@ -18,7 +18,6 @@ import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.event.collectHoverChain import org.dreamfinity.dsgl.core.input.ClipboardBridge import org.dreamfinity.dsgl.core.popup.FloatingPaneDragModel -import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.* enum class InspectorMode { @@ -37,8 +36,6 @@ class InspectorController( private var colorPickerManager: InspectorColorPickerHost = colorPickerManager private enum class EditOperation { - CyclePrev, - CycleNext, Decrement, Increment, ResetProperty, @@ -70,15 +67,6 @@ class InspectorController( val payload: String? = null ) - private data class DropdownOverlay( - val x: Int, - val y: Int, - val width: Int, - val options: List, - val property: StyleProperty, - val operation: EditOperation - ) - private data class DropdownLayout( val rect: Rect, val property: StyleProperty, @@ -153,7 +141,6 @@ class InspectorController( private var headerBounds: Rect = Rect(0, 0, 0, 0) private var contentBounds: Rect = Rect(0, 0, 0, 0) private val panelActions: MutableList = ArrayList() - private val dropdownOverlays: MutableList = ArrayList() private val dropdownLayouts: MutableList = ArrayList() private var cachedStyle: SelectionStyleCache? = null private var styleEditorError: String? = null @@ -235,8 +222,6 @@ class InspectorController( set(value) { editSession.openUnitScrollIndex = value } - private var variableTooltipText: String? = null - private var variableTooltipRect: Rect = Rect(0, 0, 0, 0) private var nativeSelectedHighlight: InspectorHighlightSnapshot? = null private var nativeHoveredHighlight: InspectorHighlightSnapshot? = null @@ -258,8 +243,6 @@ class InspectorController( private val secondaryFontSizePx: Int = parseLengthPxInt("24px", allowNegative = false) private val lineHeightPx: Int = (textFontSizePx + 8).coerceAtLeast(28) private val rowHeightPx: Int = (textFontSizePx + 10).coerceAtLeast(32) - private val caretBlinkPeriodMs: Long = 500L - fun toggle() { active = !active if (!active) deactivateInternal() @@ -442,7 +425,6 @@ class InspectorController( if (activeEditProperty == null) { if (keyCode == KeyCodes.ESCAPE) { editSession.closeAllDropdowns() - variableTooltipText = null return true } return false @@ -624,7 +606,6 @@ class InspectorController( } else { clearHoveredState() } - variableTooltipText = null if (panelState == InspectorPanelState.Minimized) { if (minimizedBounds.contains(mouseX, mouseY)) { if (overlayPanelAuthorityEnabled) { @@ -738,7 +719,6 @@ class InspectorController( if (!active || viewportWidth <= 0 || viewportHeight <= 0) { resetNativePresentation() panelActions.clear() - dropdownOverlays.clear() dropdownLayouts.clear() return null } @@ -776,7 +756,6 @@ class InspectorController( minimizedBounds = Rect(0, 0, 0, 0) resetNativePresentation() panelActions.clear() - dropdownOverlays.clear() dropdownLayouts.clear() val headerRect = @@ -931,7 +910,6 @@ class InspectorController( contentBounds = Rect(0, 0, 0, 0) resetNativePresentation() panelActions.clear() - dropdownOverlays.clear() dropdownLayouts.clear() panelScrollY = 0 panelContentHeight = 0 @@ -1715,7 +1693,6 @@ class InspectorController( tooltipNodeRef = null tooltipLabelCache = "" panelActions.clear() - dropdownOverlays.clear() dropdownLayouts.clear() panelBounds = Rect(0, 0, 0, 0) minimizedBounds = Rect(0, 0, 0, 0) @@ -1735,8 +1712,6 @@ class InspectorController( scrollbarThumbRect = Rect(0, 0, 0, 0) scrollbarDragOffsetY = 0 editSession.resetAll() - variableTooltipText = null - variableTooltipRect = Rect(0, 0, 0, 0) resetNativePresentation() colorPickerManager.close() } @@ -1804,74 +1779,6 @@ class InspectorController( return node.display != Display.None } - private fun appendHighlightCommands( - node: DOMNode, - hovered: Boolean, - selected: Boolean, - out: MutableList - ) { - val boxes = computeHighlightBoxes(node) - if (selected) { - addFill(out, boxes.margin, 0x22F3B33D) - addFill(out, boxes.padding, 0x2226A69A) - addFill(out, boxes.content, 0x224285F4) - addOutline(out, boxes.margin, 0x99F3B33D.toInt()) - addOutline(out, boxes.border, 0xCCFF9800.toInt()) - addOutline(out, boxes.padding, 0x9926A69A.toInt()) - addOutline(out, boxes.content, 0x994285F4.toInt()) - boxes.parentContent?.let { addOutline(out, it, 0x66FF5252) } - return - } - if (hovered) { - addOutline(out, boxes.border, 0xCC47A0FF.toInt()) - } - } - - private fun appendCursorTooltip( - viewportWidth: Int, - viewportHeight: Int, - out: MutableList - ) { - if (!hoverPickEnabled) return - val node = hoveredNode ?: return - val label = resolveTooltipLabel(node) - val boxW = (label.length * (secondaryFontSizePx / 2) + 18).coerceIn(140, viewportWidth - 8) - val boxH = (secondaryFontSizePx + 10).coerceAtLeast(26) - val tooltipRect = resolveTooltipRect(viewportWidth, viewportHeight, boxW, boxH) - addFill(out, tooltipRect, 0xDD11151A.toInt()) - addOutline(out, tooltipRect, 0xCC3F4A57.toInt()) - out += RenderCommand.DrawText( - text = label, - x = tooltipRect.x + 4, - y = tooltipRect.y + 3, - color = 0xFFE6EDF6.toInt(), - fontSize = secondaryFontSizePx - ) - } - - private fun appendVariableTooltip( - viewportWidth: Int, - viewportHeight: Int, - out: MutableList - ) { - val text = variableTooltipText ?: return - var rect = variableTooltipRect - rect = if (rect.width <= 0 || rect.height <= 0) { - resolveTooltipRect(viewportWidth, viewportHeight, 280, lineHeightPx + 10) - } else { - clampTooltipRect(rect.x, rect.y, rect.width, rect.height, viewportWidth, viewportHeight) - } - addFill(out, rect, 0xEE141A22.toInt()) - addOutline(out, rect, 0xCC60758F.toInt()) - out += RenderCommand.DrawText( - text = ellipsize(text, 54), - x = rect.x + 6, - y = rect.y + 4, - color = 0xFFE6EDF6.toInt(), - fontSize = secondaryFontSizePx - ) - } - private fun resolveTooltipLabel(node: DOMNode): String { val bounds = node.bounds if (tooltipNodeRef === node && tooltipNodeBounds == bounds && tooltipLabelCache.isNotEmpty()) { @@ -1883,241 +1790,6 @@ class InspectorController( return tooltipLabelCache } - private fun appendPanel( - root: DOMNode, - viewportWidth: Int, - viewportHeight: Int, - out: MutableList - ) { - val clamped = clampExpandedRect(expandedRect, viewportWidth, viewportHeight) - expandedRect = clamped - panelBounds = clamped - minimizedBounds = Rect(0, 0, 0, 0) - contentBounds = Rect(0, 0, 0, 0) - resetNativePresentation() - panelActions.clear() - dropdownOverlays.clear() - dropdownLayouts.clear() - variableTooltipText = null - variableTooltipRect = Rect(0, 0, 0, 0) - - addFill(out, panelBounds, 0xE0141820.toInt()) - addOutline(out, panelBounds, 0xCC425062.toInt()) - - val headerRect = - Rect(clamped.x + 6, clamped.y + 5, clamped.width - 12, (titleFontSizePx + 16).coerceAtLeast(44)) - headerBounds = headerRect - addFill(out, headerRect, 0x222D3846) - addOutline(out, headerRect, 0x553F4A57) - val pickOn = mode == InspectorMode.Pick - val selectedShort = selectedNode?.key?.toString()?.take(18) ?: "none" - out += RenderCommand.DrawText( - text = "Inspector Pick:${if (pickOn) "ON" else "OFF"} Sel:$selectedShort", - x = headerRect.x + 8, - y = headerRect.y + 7, - color = 0xFFE6EDF6.toInt(), - fontSize = titleFontSizePx - ) - val headerButtonHeight = (secondaryFontSizePx + 10).coerceAtLeast(26) - val headerButtonY = headerRect.y + ((headerRect.height - headerButtonHeight) / 2) - val minimizeRect = Rect(headerRect.x + headerRect.width - 96, headerButtonY, 86, headerButtonHeight) - val pickRect = Rect(headerRect.x + headerRect.width - 264, headerButtonY, 160, headerButtonHeight) - addFill(out, pickRect, if (pickOn) 0x33599F5D else 0x3346596E) - addOutline(out, pickRect, if (pickOn) 0x8896D49A.toInt() else 0x775E738C) - out += RenderCommand.DrawText( - "Select Element", - pickRect.x + 8, - pickRect.y + 4, - 0xFFE6EDF6.toInt(), - fontSize = secondaryFontSizePx - ) - panelActions += PanelAction(pickRect, ActionKind.TogglePick) - addFill(out, minimizeRect, 0x3346596E) - addOutline(out, minimizeRect, 0x775E738C) - out += RenderCommand.DrawText( - "Minimize", - minimizeRect.x + 8, - minimizeRect.y + 4, - 0xFFE6EDF6.toInt(), - fontSize = secondaryFontSizePx - ) - panelActions += PanelAction(minimizeRect, ActionKind.Minimize) - - val resizeHandle = Rect(clamped.x + clamped.width - 10, clamped.y + clamped.height - 10, 8, 8) - addFill(out, resizeHandle, 0x55748AA1) - addOutline(out, resizeHandle, 0xAA90A7BF.toInt()) - - val bodyTop = headerRect.y + headerRect.height + 6 - val bodyRect = Rect( - clamped.x + 6, - bodyTop, - clamped.width - 12, - (clamped.height - (bodyTop - clamped.y) - 4).coerceAtLeast(24) - ) - contentBounds = bodyRect - panelScrollY = panelScrollY.coerceIn(0, maxOf(0, panelContentHeight - bodyRect.height)) - val maxChars = estimateMaxChars(bodyRect.width - 12, textFontSizePx) - - addFill(out, bodyRect, 0x18212C39) - out += RenderCommand.PushClip(bodyRect.x, bodyRect.y, bodyRect.width, bodyRect.height) - - var y = bodyRect.y - y = appendPanelLine(out, bodyRect, y, "F12 toggle, F9 mode, Esc cancel pick", maxChars, panelScrollY) - y = appendPanelLine( - out, - bodyRect, - y, - "Hovered: ${hoveredNode?.let { nodeLabel(it) } ?: "none"}", - maxChars, - panelScrollY) - y = appendPanelLine( - out, - bodyRect, - y, - "Selected: ${selectedNode?.let { nodeLabel(it) } ?: "none"}", - maxChars, - panelScrollY) - y = appendPanelLine( - out, - bodyRect, - y, - "Inspector handled last: $lastHandledPointerEvent", - maxChars, - panelScrollY - ) - y = appendPanelLine(out, bodyRect, y, "Pointer over Inspector: $pointerOverInspectorUi", maxChars, panelScrollY) - y = appendPanelLine(out, bodyRect, y, "Hover pick enabled: $hoverPickEnabled", maxChars, panelScrollY) - y += 2 - - val selected = selectedNode - if (selected == null) { - y = appendPanelLine(out, bodyRect, y, "Click element in Pick mode to inspect.", maxChars, panelScrollY) - out += RenderCommand.PopClip - panelContentHeight = (y - bodyRect.y).coerceAtLeast(0) - panelScrollY = panelScrollY.coerceIn(0, maxOf(0, panelContentHeight - bodyRect.height)) - appendScrollbarIndicator(out, bodyRect) - appendDropdownOverlays(out) - return - } - - val selectedPath = pathToNode(root, selected) - wrapPathLines(selectedPath, maxChars).forEach { line -> - y = appendPanelLine(out, bodyRect, y, line, maxChars, panelScrollY) - } - val boxes = computeBoxes(selected) - y = appendPanelLine(out, bodyRect, y, "Border box: ${rectLabel(boxes.border)}", maxChars, panelScrollY) - y = appendPanelLine(out, bodyRect, y, "Content box: ${rectLabel(boxes.content)}", maxChars, panelScrollY) - y = appendPanelLine(out, bodyRect, y, "Margin box: ${rectLabel(boxes.margin)}", maxChars, panelScrollY) - boxes.parentContent?.let { - y = appendPanelLine(out, bodyRect, y, "Parent content: ${rectLabel(it)}", maxChars, panelScrollY) - } - val localPos = selected.parent?.let { parent -> - val parentContent = contentRect(parent) - "${selected.bounds.x - parentContent.x},${selected.bounds.y - parentContent.y}" - } ?: "${selected.bounds.x},${selected.bounds.y}" - y = appendPanelLine(out, bodyRect, y, "Local pos: $localPos", maxChars, panelScrollY) - selected.inspectorScrollOffset()?.let { (sx, sy) -> - y = appendPanelLine(out, bodyRect, y, "Scroll: x=$sx y=$sy", maxChars, panelScrollY) - } - y += 2 - - val parent = selected.parent - if (parent != null) { - val row = Rect(clamped.x + 8, y - panelScrollY, clamped.width - 16, rowHeightPx) - addFill(out, row, 0x222D3846) - addOutline(out, row, 0x553F4A57) - out += RenderCommand.DrawText( - "[Parent] ${nodeLabel(parent)}", - row.x + 8, - row.y + 4, - 0xFFDCE5EF.toInt(), - fontSize = secondaryFontSizePx - ) - panelActions += PanelAction(row, ActionKind.Parent) - y += rowHeightPx + 2 - } - - val children = selected.children.filter { it.display != Display.None } - if (children.isNotEmpty()) { - y = appendPanelLine(out, bodyRect, y, "Children:", maxChars, panelScrollY) - for (index in children.indices) { - val child = children[index] - val row = Rect(clamped.x + 10, y - panelScrollY, clamped.width - 20, rowHeightPx) - addFill(out, row, 0x1E263241) - addOutline(out, row, 0x55394654) - out += RenderCommand.DrawText( - "[$index] ${nodeLabel(child)}", - row.x + 8, - row.y + 4, - 0xFFDCE5EF.toInt(), - fontSize = secondaryFontSizePx - ) - panelActions += PanelAction(row, ActionKind.Child, index) - y += rowHeightPx + 2 - } - } - - y += 2 - val inspection = selectionStyle(selected) - y = appendStyleEditorSection(clamped, bodyRect, selected, inspection, y, out, panelScrollY, maxChars) - y += 1 - y = appendPanelLine(out, bodyRect, y, "Computed styles:", maxChars, panelScrollY) - val styleRows = styleRows(inspection) - for (line in styleRows) { - y = appendPanelLine(out, bodyRect, y, line, maxChars, panelScrollY, 2) - } - - out += RenderCommand.PopClip - panelContentHeight = (y - bodyRect.y).coerceAtLeast(0) - panelScrollY = panelScrollY.coerceIn(0, maxOf(0, panelContentHeight - bodyRect.height)) - appendScrollbarIndicator(out, bodyRect) - appendDropdownOverlays(out) - } - - private fun appendMinimizedPanel( - viewportWidth: Int, - viewportHeight: Int, - out: MutableList - ) { - val chipWidth = minimizedWidth().coerceAtLeast(minChipWidth) - val chipHeight = minimizedHeight() - clampMinimizedPosition(viewportWidth, viewportHeight) - val chipRect = Rect(minimizedPosX, minimizedPosY, chipWidth, chipHeight) - panelBounds = chipRect - minimizedBounds = chipRect - headerBounds = Rect(0, 0, 0, 0) - contentBounds = Rect(0, 0, 0, 0) - panelScrollY = 0 - panelContentHeight = 0 - nativeDomLastKnownBodyScrollY = 0 - panelActions.clear() - dropdownOverlays.clear() - dropdownLayouts.clear() - - addFill(out, chipRect, if (dragMode == DragMode.MinimizedMove) 0xEE1C2430.toInt() else 0xDD1A202A.toInt()) - addOutline(out, chipRect, 0xCC4F6076.toInt()) - val badge = if (mode == InspectorMode.Pick) "[Pick]" else "[Locked]" - val selectedShort = selectedNode?.key?.toString()?.let { " $it" } ?: "" - val title = "Inspector $badge$selectedShort" - val maxChars = estimateMaxChars(chipRect.width - 12, secondaryFontSizePx) - val lines = wrapMinimizedLabel(title, maxChars, maxLines = 2) - val compactLineHeight = (secondaryFontSizePx + 4).coerceAtLeast(20) - val startY = if (lines.size == 1) { - chipRect.y + ((chipRect.height - compactLineHeight) / 2) - } else { - chipRect.y + ((chipRect.height - compactLineHeight * lines.size) / 2) - } - lines.forEachIndexed { index, line -> - out += RenderCommand.DrawText( - text = line, - x = chipRect.x + 8, - y = startY + index * compactLineHeight, - color = 0xFFE6EDF6.toInt(), - fontSize = secondaryFontSizePx - ) - } - } - private fun selectionStyle(node: DOMNode): StyleInspection { val key = node.key val klass = node.javaClass @@ -2274,454 +1946,6 @@ class InspectorController( styleEditorError = null } - private fun appendStyleEditorSection( - panelRect: Rect, - bodyRect: Rect, - selected: DOMNode, - inspection: StyleInspection, - startY: Int, - out: MutableList, - scrollY: Int, - maxChars: Int - ): Int { - var y = appendPanelLine(out, bodyRect, startY, "Style editor (live overrides):", maxChars, scrollY) - - val rowLeft = panelRect.x + 10 - val rowWidth = panelRect.width - 20 - val btnWidth = 40 - val gap = 6 - val controlHeight = (rowHeightPx - 8).coerceAtLeast(22) - val labelLineHeight = (secondaryFontSizePx - 2).coerceAtLeast(18) - val properties = editablePropertiesFor(selected) - for (property in properties) { - y = appendEditablePropertyRow( - selected = selected, - inspection = inspection, - property = property, - x = rowLeft, - y = y, - width = rowWidth, - scrollY = scrollY, - out = out - ) - } - - styleEditorError?.let { error -> - y = appendPanelLine( - out, - bodyRect, - y, - "Edit error: ${error.take(58)}", - maxChars, - scrollY, - 2, - 0xFFFF6E6E.toInt() - ) - } - - val actionHeight = (secondaryFontSizePx + 10).coerceAtLeast(28) - val resetRect = Rect(rowLeft, y - scrollY, 140, actionHeight) - val clearRect = Rect(rowLeft + 148, y - scrollY, 160, actionHeight) - addFill(out, resetRect, 0x2A465968) - addOutline(out, resetRect, 0x775E738C) - addFill(out, clearRect, 0x2A4E3F56) - addOutline(out, clearRect, 0x777A5C84) - out += RenderCommand.DrawText( - "Reset node", - resetRect.x + 8, - resetRect.y + 4, - 0xFFDCE5EF.toInt(), - fontSize = secondaryFontSizePx - ) - out += RenderCommand.DrawText( - "Clear all", - clearRect.x + 8, - clearRect.y + 4, - 0xFFDCE5EF.toInt(), - fontSize = secondaryFontSizePx - ) - panelActions += PanelAction(resetRect, ActionKind.ResetSelectedOverrides) - panelActions += PanelAction(clearRect, ActionKind.ClearAllOverrides) - y += actionHeight + 4 - - return y - } - - private fun appendEditablePropertyRow( - selected: DOMNode, - inspection: StyleInspection, - property: StyleProperty, - x: Int, - y: Int, - width: Int, - scrollY: Int, - out: MutableList - ): Int { - val row = Rect(x, y - scrollY, width, rowHeightPx) - addFill(out, row, 0x1B293746) - addOutline(out, row, 0x553F4A57) - - val overrideExpr = StyleEngine.inspectorOverrideFor(selected, property) - val effectiveValue = overrideExpr?.let(::expressionLabel) ?: literalFromComputed(inspection.computed, property) - val sourceTag = if (overrideExpr != null) "ins" else (inspection.propertySources[property]?.source ?: "default") - val labelWidth = (width * 0.34f).toInt().coerceIn(220, 300) - out += RenderCommand.DrawText( - text = "${property.key} [$sourceTag]", - x = row.x + 8, - y = row.y + 6, - color = 0xFFDCE5EF.toInt(), - fontSize = secondaryFontSizePx - ) - - val buttonsRight = row.x + row.width - 8 - val btnWidth = 40 - val gap = 6 - val controlX = (row.x + labelWidth).coerceAtMost(buttonsRight - btnWidth - 40) - val controlWidth = (buttonsRight - controlX - btnWidth - gap).coerceAtLeast(36) - val resetRect = Rect(buttonsRight - btnWidth, row.y + 4, btnWidth, rowHeightPx - 8) - drawActionButton(resetRect, "x", out) - panelActions += PanelAction( - bounds = resetRect, - kind = ActionKind.EditProperty, - property = property, - editOperation = EditOperation.ResetProperty - ) - - val editor = InspectorEditorRegistry.describe( - property = property, - literal = effectiveValue, - expression = overrideExpr - ) - val contentRect = Rect(controlX, row.y + 4, controlWidth, rowHeightPx - 8) - - when (editor.kind) { - InspectorEditorKind.EnumSelect, - InspectorEditorKind.FontSelect -> { - drawValueSelector( - bounds = contentRect, - value = effectiveValue, - isOpen = openValueSelectProperty == property, - hovered = contentRect.contains(mouseX, mouseY), - out = out - ) - panelActions += PanelAction( - bounds = contentRect, - kind = ActionKind.EditProperty, - property = property, - editOperation = EditOperation.ToggleValueSelect - ) - } - - InspectorEditorKind.StringInput -> { - val isActiveInput = activeEditProperty == property && !activeEditIsNumeric - drawTextInput( - bounds = contentRect, - value = if (isActiveInput) activeEditBuffer else effectiveValue, - active = isActiveInput, - out = out - ) - panelActions += PanelAction( - bounds = contentRect, - kind = ActionKind.EditProperty, - property = property, - editOperation = EditOperation.BeginTextEdit - ) - if (editor.showColorPreview) { - val previewRect = - appendColorPreview(contentRect, if (isActiveInput) activeEditBuffer else effectiveValue, out) - panelActions += PanelAction( - bounds = previewRect, - kind = ActionKind.EditProperty, - property = property, - editOperation = EditOperation.OpenColorPicker - ) - } - } - - InspectorEditorKind.NumericInput -> { - val step = StylePropertyRegistry.descriptor(property).numericStep - val parsed = InspectorEditorRegistry.parseNumericLiteral(property, effectiveValue) - val numericValue = if (activeEditProperty == property && activeEditIsNumeric) { - activeEditBuffer - } else { - parsed?.numberText ?: "0" - } - val unit = if (activeEditProperty == property && activeEditIsNumeric) { - activeEditUnit ?: parsed?.unit ?: InspectorEditorRegistry.defaultNumericUnit(property) - } else { - parsed?.unit ?: InspectorEditorRegistry.defaultNumericUnit(property) - } - val buttonWidth = 34 - val unitWidth = if (editor.supportsUnits) 68 else 0 - val inputWidth = - (contentRect.width - buttonWidth * 2 - unitWidth - (if (editor.supportsUnits) 12 else 6)) - .coerceAtLeast(64) - val decRect = Rect(contentRect.x, contentRect.y, buttonWidth, contentRect.height) - val inputRect = Rect(decRect.x + decRect.width + 4, contentRect.y, inputWidth, contentRect.height) - val incRect = Rect(inputRect.x + inputRect.width + 4, contentRect.y, buttonWidth, contentRect.height) - drawActionButton(decRect, "-", out) - drawTextInput(inputRect, numericValue, activeEditProperty == property && activeEditIsNumeric, out) - drawActionButton(incRect, "+", out) - panelActions += PanelAction( - decRect, - ActionKind.EditProperty, - property = property, - editOperation = EditOperation.Decrement, - step = step - ) - panelActions += PanelAction( - inputRect, - ActionKind.EditProperty, - property = property, - editOperation = EditOperation.BeginTextEdit - ) - panelActions += PanelAction( - incRect, - ActionKind.EditProperty, - property = property, - editOperation = EditOperation.Increment, - step = step - ) - if (editor.supportsUnits) { - val unitRect = Rect(incRect.x + incRect.width + 4, contentRect.y, unitWidth, contentRect.height) - drawValueSelector( - bounds = unitRect, - value = unit?.token ?: "px", - isOpen = openUnitSelectProperty == property, - hovered = unitRect.contains(mouseX, mouseY), - out = out - ) - panelActions += PanelAction( - bounds = unitRect, - kind = ActionKind.EditProperty, - property = property, - editOperation = EditOperation.ToggleUnitSelect - ) - } - } - } - - if (overrideExpr is StyleExpression.VariableRef && row.contains(mouseX, mouseY)) { - val resolved = StyleEngine.resolveInspectorVariable(overrideExpr.name) - val body = resolved.getOrElse { "unresolved (${it.message ?: "unknown error"})" } - variableTooltipText = "${overrideExpr.name} = $body" - variableTooltipRect = Rect( - x = (row.x + row.width - 360).coerceAtLeast(panelBounds.x + 8), - y = (row.y - lineHeightPx - 8).coerceAtLeast(panelBounds.y + 8), - width = 352, - height = lineHeightPx + 10 - ) - } - - val nextY = y + rowHeightPx + 4 - if (openValueSelectProperty == property && editor.options.isNotEmpty()) { - queueDropdownOverlay( - x = contentRect.x, - y = contentRect.y + contentRect.height + 2, - width = contentRect.width, - options = editor.options, - property = property, - operation = EditOperation.SelectValueOption - ) - } - if (openUnitSelectProperty == property && editor.supportsUnits) { - val units = InspectorEditorRegistry.unitOptions().map { it.token } - queueDropdownOverlay( - x = contentRect.x + contentRect.width - 90, - y = contentRect.y + contentRect.height + 2, - width = 90, - options = units, - property = property, - operation = EditOperation.SelectUnitOption - ) - } - return nextY - } - - private fun drawActionButton(rect: Rect, text: String, out: MutableList) { - addFill(out, rect, 0x3346596E) - addOutline(out, rect, 0x775E738C) - out += RenderCommand.DrawText( - text = text, - x = rect.x + 8, - y = rect.y + 4, - color = 0xFFDCE5EF.toInt(), - fontSize = secondaryFontSizePx - ) - } - - private fun drawTextInput(bounds: Rect, value: String, active: Boolean, out: MutableList) { - addFill(out, bounds, if (active) 0x334D5D70 else 0x22313D4B) - addOutline(out, bounds, if (active) 0xFFA8C6E6.toInt() else 0x77607084) - val suffix = if (active && caretVisible()) "|" else "" - out += RenderCommand.DrawText( - text = ellipsize(value + suffix, 34), - x = bounds.x + 8, - y = bounds.y + 4, - color = 0xFFE6EDF6.toInt(), - fontSize = secondaryFontSizePx - ) - } - - private fun caretVisible(): Boolean { - return ((System.currentTimeMillis() / caretBlinkPeriodMs) % 2L) == 0L - } - - private fun drawValueSelector( - bounds: Rect, - value: String, - isOpen: Boolean, - hovered: Boolean, - out: MutableList - ) { - val fill = when { - isOpen -> 0x334D5D70 - hovered -> 0x2A425164 - else -> 0x22313D4B - } - val stroke = when { - isOpen -> 0xFFA8C6E6.toInt() - hovered -> 0xCC89A7C8.toInt() - else -> 0x77607084 - } - addFill(out, bounds, fill) - addOutline(out, bounds, stroke) - val arrow = if (isOpen) "^" else "v" - out += RenderCommand.DrawText( - text = ellipsize(value, 26), - x = bounds.x + 8, - y = bounds.y + 4, - color = 0xFFE6EDF6.toInt(), - fontSize = secondaryFontSizePx - ) - out += RenderCommand.DrawText( - text = arrow, - x = bounds.x + bounds.width - 18, - y = bounds.y + 4, - color = 0xFFA8C6E6.toInt(), - fontSize = secondaryFontSizePx - ) - } - - private fun appendColorPreview(bounds: Rect, literal: String, out: MutableList): Rect { - val previewSize = (bounds.height - 8).coerceAtLeast(10) - val previewRect = Rect( - x = bounds.x + bounds.width - previewSize - 6, - y = bounds.y + 4, - width = previewSize, - height = previewSize - ) - val parsed = runCatching { parseColor(literal) }.getOrNull() - addFill(out, previewRect, parsed ?: 0x663F4A57) - addOutline(out, previewRect, 0xCC9BB2C9.toInt()) - return previewRect - } - - private fun queueDropdownOverlay( - x: Int, - y: Int, - width: Int, - options: List, - property: StyleProperty, - operation: EditOperation - ) { - if (options.isEmpty()) return - dropdownOverlays += DropdownOverlay( - x = x, - y = y, - width = width, - options = options, - property = property, - operation = operation - ) - } - - private fun appendDropdownOverlays(out: MutableList) { - if (dropdownOverlays.isEmpty()) return - dropdownOverlays.forEach { overlay -> - appendOptionsPopup( - x = overlay.x, - y = overlay.y, - width = overlay.width, - options = overlay.options, - property = overlay.property, - out = out, - operation = overlay.operation - ) - } - } - - private fun appendOptionsPopup( - x: Int, - y: Int, - width: Int, - options: List, - property: StyleProperty, - out: MutableList, - operation: EditOperation - ) { - if (options.isEmpty()) return - val maxRows = 8 - val visibleRows = minOf(maxRows, options.size) - val maxFirst = (options.size - visibleRows).coerceAtLeast(0) - val isUnit = operation == EditOperation.SelectUnitOption - val rawFirst = if (isUnit) openUnitSelectScrollIndex else openValueSelectScrollIndex - val first = rawFirst.coerceIn(0, maxFirst) - if (isUnit) { - openUnitSelectScrollIndex = first - } else { - openValueSelectScrollIndex = first - } - val shown = options.subList(first, first + visibleRows) - val optionHeight = rowHeightPx - val popupHeight = optionHeight * shown.size + 6 - val viewportWidth = viewportW.coerceAtLeast(1) - val viewportHeight = viewportH.coerceAtLeast(1) - val clampedX = x.coerceIn(2, (viewportWidth - width - 2).coerceAtLeast(2)) - val clampedY = y.coerceIn(2, (viewportHeight - popupHeight - 2).coerceAtLeast(2)) - val popupRect = Rect(clampedX, clampedY, width, popupHeight) - dropdownLayouts += DropdownLayout( - rect = popupRect, - property = property, - isUnit = isUnit, - totalOptions = options.size, - visibleRows = visibleRows - ) - addFill(out, popupRect, 0xEE202A36.toInt()) - addOutline(out, popupRect, 0xCC596A80.toInt()) - var optionY = popupRect.y + 3 - shown.forEach { option -> - val optionRect = Rect(popupRect.x + 3, optionY, popupRect.width - 6, optionHeight - 2) - val hovered = optionRect.contains(mouseX, mouseY) - addFill(out, optionRect, if (hovered) 0x2D4C6279 else 0x22313D4B) - addOutline(out, optionRect, if (hovered) 0xCC95B3D3.toInt() else 0x664F6076) - out += RenderCommand.DrawText( - text = ellipsize(option, 30), - x = optionRect.x + 6, - y = optionRect.y + 4, - color = if (hovered) 0xFFFFFFFF.toInt() else 0xFFE6EDF6.toInt(), - fontSize = secondaryFontSizePx - ) - panelActions += PanelAction( - bounds = optionRect, - kind = ActionKind.EditProperty, - property = property, - editOperation = operation, - payload = option - ) - optionY += optionHeight - } - if (options.size > visibleRows) { - out += RenderCommand.DrawText( - text = "${first + 1}-${first + visibleRows}/${options.size}", - x = popupRect.x + 6, - y = popupRect.y + popupRect.height - (secondaryFontSizePx + 4), - color = 0xFF8EA6BF.toInt(), - fontSize = secondaryFontSizePx - ) - } - } - private fun ellipsize(raw: String, maxChars: Int): String { if (maxChars <= 1) return raw.take(1) if (raw.length <= maxChars) return raw @@ -2762,22 +1986,6 @@ class InspectorController( runCatching { when (operation) { EditOperation.ResetProperty -> StyleEngine.clearInspectorOverride(selected, property) - EditOperation.CyclePrev -> { - val options = enumOptions(property) ?: error("Property '${property.key}' is not enumerable.") - val current = literalForEdit(selected, property) - val currentIndex = options.indexOfFirst { it.equals(current, ignoreCase = true) } - val nextIndex = if (currentIndex <= 0) options.lastIndex else currentIndex - 1 - StyleEngine.setInspectorOverrideLiteral(selected, property, options[nextIndex]).getOrThrow() - } - - EditOperation.CycleNext -> { - val options = enumOptions(property) ?: error("Property '${property.key}' is not enumerable.") - val current = literalForEdit(selected, property) - val currentIndex = options.indexOfFirst { it.equals(current, ignoreCase = true) } - val nextIndex = if (currentIndex == -1 || currentIndex == options.lastIndex) 0 else currentIndex + 1 - StyleEngine.setInspectorOverrideLiteral(selected, property, options[nextIndex]).getOrThrow() - } - EditOperation.Decrement -> { val next = adjustNumericLiteral(selected, property, -step) StyleEngine.setInspectorOverrideLiteral(selected, property, next).getOrThrow() @@ -2908,13 +2116,6 @@ class InspectorController( val rawIndex = ((local + charWidth / 2) / charWidth) return rawIndex.coerceIn(0, text.length) } - - private fun activeBufferWithCaret(): String { - activeEditState.clampToLength(activeEditBuffer.length) - if (!caretVisible()) return activeEditBuffer - val caret = activeEditState.caretIndex.coerceIn(0, activeEditBuffer.length) - return activeEditBuffer.substring(0, caret) + "|" + activeEditBuffer.substring(caret) - } private fun openColorPicker(selected: DOMNode, property: StyleProperty, anchorRect: Rect) { val literal = literalForEdit(selected, property) val parsedByStyle = runCatching { RgbaColor.fromArgbInt(parseColor(literal)) }.getOrNull() @@ -2990,18 +2191,6 @@ class InspectorController( editSession.clearActiveEdit() } - private fun enumOptions(property: StyleProperty): List? { - val descriptor = StylePropertyRegistry.descriptor(property) - return when (descriptor.valueType) { - StyleEditorValueType.EnumChoice, - StyleEditorValueType.ColorHex, - StyleEditorValueType.StringPreset, - StyleEditorValueType.LineHeight -> descriptor.enumOptions - - else -> null - } - } - private fun literalForEdit(selected: DOMNode, property: StyleProperty): String { val override = StyleEngine.inspectorOverrideFor(selected, property) if (override != null) return expressionLabel(override) @@ -3184,10 +2373,6 @@ class InspectorController( return "${value.top.toCssLiteral()} ${value.right.toCssLiteral()} ${value.bottom.toCssLiteral()} ${value.left.toCssLiteral()}" } - private fun spacingLengthLabel(value: LengthInsets): String { - return "${value.top.toCssLiteral()} ${value.right.toCssLiteral()} ${value.bottom.toCssLiteral()} ${value.left.toCssLiteral()}" - } - private fun pxLiteral(value: Int): String = "${value}px" private fun formatFloatLiteral(value: Float): String { @@ -3195,48 +2380,6 @@ class InspectorController( return if (rounded % 1f == 0f) rounded.toInt().toString() else rounded.toString() } - private fun appendPanelLine( - out: MutableList, - x: Int, - y: Int, - text: String - ): Int { - out += RenderCommand.DrawText( - text = text.take(74), - x = x, - y = y, - color = 0xFFDCE5EF.toInt(), - fontSize = secondaryFontSizePx - ) - return y + lineHeightPx - } - - private fun appendPanelLine( - out: MutableList, - bodyRect: Rect, - y: Int, - text: String, - maxChars: Int, - scrollY: Int, - leftInset: Int = 0, - color: Int = 0xFFDCE5EF.toInt() - ): Int { - val lines = wrapText(text, maxChars) - var logicalY = y - for (line in lines) { - val drawY = logicalY - scrollY - out += RenderCommand.DrawText( - line, - bodyRect.x + 2 + leftInset, - drawY, - color, - fontSize = textFontSizePx - ) - logicalY += lineHeightPx - } - return logicalY - } - private fun wrapPathLines(path: List, maxChars: Int): List { if (path.isEmpty()) return listOf("Path: ") val tokens = path.map(::pathToken) @@ -3293,32 +2436,6 @@ class InspectorController( return if (result.isEmpty()) listOf("") else result } - private fun appendScrollbarIndicator(out: MutableList, bodyRect: Rect) { - if (panelContentHeight <= bodyRect.height || bodyRect.height <= 0) { - scrollbarTrackRect = Rect(0, 0, 0, 0) - scrollbarThumbRect = Rect(0, 0, 0, 0) - return - } - val trackWidth = 4 - val track = Rect( - bodyRect.x + bodyRect.width - trackWidth - 2, - bodyRect.y + 2, - trackWidth, - (bodyRect.height - 4).coerceAtLeast(8) - ) - val maxScroll = (panelContentHeight - bodyRect.height).coerceAtLeast(1) - val thumbHeight = ((track.height.toFloat() * bodyRect.height.toFloat() / panelContentHeight.toFloat()).toInt()) - .coerceIn(10, track.height) - val travel = (track.height - thumbHeight).coerceAtLeast(0) - val thumbY = track.y + ((panelScrollY.toFloat() / maxScroll.toFloat()) * travel.toFloat()).toInt() - val thumb = Rect(track.x, thumbY, track.width, thumbHeight) - scrollbarTrackRect = track - scrollbarThumbRect = thumb - addFill(out, track, 0x22384A5D) - addFill(out, thumb, 0x887E97B1.toInt()) - addOutline(out, thumb, 0xCC9BB2C9.toInt()) - } - private fun pathToNode(root: DOMNode, target: DOMNode): List { val path = ArrayList(8) if (collectPath(root, target, path)) { @@ -3690,10 +2807,6 @@ class InspectorController( return "${rect.x},${rect.y},${rect.width}x${rect.height}" } - private fun spacingLabel(value: org.dreamfinity.dsgl.core.dom.layout.Insets): String { - return "${value.top}/${value.right}/${value.bottom}/${value.left}" - } - private fun colorLabel(color: Int): String { val hex = color.toUInt().toString(16).uppercase().padStart(8, '0') return "#$hex" @@ -3789,16 +2902,4 @@ class InspectorController( ) } - private fun addFill(out: MutableList, rect: Rect, color: Int) { - if (rect.width <= 0 || rect.height <= 0) return - out += RenderCommand.DrawRect(rect.x, rect.y, rect.width, rect.height, color) - } - - private fun addOutline(out: MutableList, rect: Rect, color: Int) { - if (rect.width <= 0 || rect.height <= 0) return - out += RenderCommand.DrawRect(rect.x, rect.y, rect.width, 1, color) - out += RenderCommand.DrawRect(rect.x, rect.y + rect.height - 1, rect.width, 1, color) - out += RenderCommand.DrawRect(rect.x, rect.y, 1, rect.height, color) - out += RenderCommand.DrawRect(rect.x + rect.width - 1, rect.y, 1, rect.height, color) - } } From ecca0033a85e8414a4a7512d50ce55c6c6322ebe Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Mon, 30 Mar 2026 01:15:18 +0300 Subject: [PATCH 04/78] splitting inspector helpers into InspectorPresentationSupport and InspectorGeometrySupport, cleaning up redundant methods in InspectorController, and adding dropdown bounds handling to SystemInspectorOverlayNode. --- .../core/inspector/InspectorController.kt | 305 ++++-------------- .../inspector/InspectorGeometrySupport.kt | 105 ++++++ .../inspector/InspectorPresentationSupport.kt | 133 ++++++++ .../internal/SystemInspectorOverlayNode.kt | 34 +- .../SystemInspectorOverlayInputBoundsTests.kt | 57 ++++ 5 files changed, 385 insertions(+), 249 deletions(-) create mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorGeometrySupport.kt create mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorPresentationSupport.kt create mode 100644 core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayInputBoundsTests.kt diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt index 6c3c8ec..71ee865 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt @@ -7,7 +7,6 @@ import org.dreamfinity.dsgl.core.colorpicker.RgbaColor import org.dreamfinity.dsgl.core.colorpicker.internal.InspectorColorPickerHost import org.dreamfinity.dsgl.core.colorpicker.internal.SystemColorPickerPanelManager import org.dreamfinity.dsgl.core.dom.DOMNode -import org.dreamfinity.dsgl.core.dom.UsedInteractionGeometryResolver import org.dreamfinity.dsgl.core.dom.elements.TextEditState import org.dreamfinity.dsgl.core.dom.elements.support.TextEditOps import org.dreamfinity.dsgl.core.dom.layout.Rect @@ -82,14 +81,6 @@ class InspectorController( val inspection: StyleInspection ) - private data class NodeBoxes( - val margin: Rect, - val border: Rect, - val padding: Rect, - val content: Rect, - val parentContent: Rect? - ) - private enum class DragMode { None, Move, @@ -787,11 +778,24 @@ class InspectorController( var styleLines: List = emptyList() var y = bodyRect.y - val maxChars = estimateMaxChars(bodyRect.width - 12, textFontSizePx) - val buttonLabelMaxChars = estimateMaxChars((clamped.width - 32).coerceAtLeast(40), secondaryFontSizePx) + val maxChars = InspectorPresentationSupport.estimateMaxChars(bodyRect.width - 12, textFontSizePx) + val buttonLabelMaxChars = InspectorPresentationSupport.estimateMaxChars( + (clamped.width - 32).coerceAtLeast(40), + secondaryFontSizePx + ) y = appendDomLine(infoLines, y, "F12 toggle, F9 mode, Esc cancel pick", maxChars) - y = appendDomLine(infoLines, y, "Hovered: ${hoveredNode?.let { nodeLabel(it) } ?: "none"}", maxChars) - y = appendDomLine(infoLines, y, "Selected: ${selectedNode?.let { nodeLabel(it) } ?: "none"}", maxChars) + y = appendDomLine( + infoLines, + y, + "Hovered: ${hoveredNode?.let { InspectorPresentationSupport.nodeLabel(it) } ?: "none"}", + maxChars + ) + y = appendDomLine( + infoLines, + y, + "Selected: ${selectedNode?.let { InspectorPresentationSupport.nodeLabel(it) } ?: "none"}", + maxChars + ) y = appendDomLine(infoLines, y, "Inspector handled last: $lastHandledPointerEvent", maxChars) y = appendDomLine(infoLines, y, "Pointer over Inspector: $pointerOverInspectorUi", maxChars) y = appendDomLine(infoLines, y, "Hover pick enabled: $hoverPickEnabled", maxChars) @@ -818,21 +822,39 @@ class InspectorController( ) } - val pathLines = wrapPathLines(pathToNode(root, selected), maxChars) + val pathLines = InspectorPresentationSupport.wrapPathLines( + InspectorPresentationSupport.pathToNode(root, selected), + maxChars + ) pathLines.forEach { line -> infoLines += line y += lineHeightPx } - val boxes = computeBoxes(selected) - y = appendDomLine(infoLines, y, "Border box: ${rectLabel(boxes.border)}", maxChars) - y = appendDomLine(infoLines, y, "Content box: ${rectLabel(boxes.content)}", maxChars) - y = appendDomLine(infoLines, y, "Margin box: ${rectLabel(boxes.margin)}", maxChars) + val boxes = InspectorGeometrySupport.computeBoxes(selected) + y = appendDomLine( + infoLines, + y, + "Border box: ${InspectorPresentationSupport.rectLabel(boxes.border)}", + maxChars + ) + y = appendDomLine( + infoLines, + y, + "Content box: ${InspectorPresentationSupport.rectLabel(boxes.content)}", + maxChars + ) + y = appendDomLine( + infoLines, + y, + "Margin box: ${InspectorPresentationSupport.rectLabel(boxes.margin)}", + maxChars + ) boxes.parentContent?.let { - y = appendDomLine(infoLines, y, "Parent content: ${rectLabel(it)}", maxChars) + y = appendDomLine(infoLines, y, "Parent content: ${InspectorPresentationSupport.rectLabel(it)}", maxChars) } val localPos = selected.parent?.let { parent -> - val parentContent = contentRect(parent) + val parentContent = InspectorGeometrySupport.contentRect(parent) "${selected.bounds.x - parentContent.x},${selected.bounds.y - parentContent.y}" } ?: "${selected.bounds.x},${selected.bounds.y}" y = appendDomLine(infoLines, y, "Local pos: $localPos", maxChars) @@ -842,7 +864,7 @@ class InspectorController( val parent = selected.parent if (parent != null) { - parentLabel = ellipsize("[Parent] ${nodeLabel(parent)}", buttonLabelMaxChars) + parentLabel = ellipsize("[Parent] ${InspectorPresentationSupport.nodeLabel(parent)}", buttonLabelMaxChars) val row = Rect(clamped.x + 10, y - panelScrollY, clamped.width - 20, rowHeightPx) panelActions += PanelAction(row, ActionKind.Parent) y += rowHeightPx + 2 @@ -853,7 +875,7 @@ class InspectorController( y = appendDomLine(infoLines, y, "Children:", maxChars) for (index in children.indices) { val child = children[index] - childLabels += ellipsize("[$index] ${nodeLabel(child)}", buttonLabelMaxChars) + childLabels += ellipsize("[$index] ${InspectorPresentationSupport.nodeLabel(child)}", buttonLabelMaxChars) val row = Rect(clamped.x + 10, y - panelScrollY, clamped.width - 20, rowHeightPx) panelActions += PanelAction(row, ActionKind.Child, index) y += rowHeightPx + 2 @@ -872,7 +894,7 @@ class InspectorController( y = appendDomLine(infoLines, y, "Computed styles:", maxChars) styleLines = styleRows(inspection).flatMap { line -> - wrapText(line, maxChars) + InspectorPresentationSupport.wrapText(line, maxChars) } y += styleLines.size * lineHeightPx @@ -919,8 +941,8 @@ class InspectorController( val badge = if (mode == InspectorMode.Pick) "[Pick]" else "[Locked]" val selectedShort = selectedNode?.key?.toString()?.let { " $it" } ?: "" - val maxChars = estimateMaxChars(chipRect.width - 12, secondaryFontSizePx) - val lines = wrapMinimizedLabel("Inspector $badge$selectedShort", maxChars, maxLines = 2) + val maxChars = InspectorPresentationSupport.estimateMaxChars(chipRect.width - 12, secondaryFontSizePx) + val lines = InspectorPresentationSupport.wrapMinimizedLabel("Inspector $badge$selectedShort", maxChars, maxLines = 2) return InspectorDomSnapshot( panelState = InspectorPanelState.Minimized, panelRect = chipRect, @@ -942,7 +964,7 @@ class InspectorController( text: String, maxChars: Int ): Int { - val wrapped = wrapText(text, maxChars) + val wrapped = InspectorPresentationSupport.wrapText(text, maxChars) lines += wrapped return y + wrapped.size * lineHeightPx } @@ -953,7 +975,7 @@ class InspectorController( viewportHeight: Int ) { nativeSelectedHighlight = selected?.let { node -> - val boxes = computeHighlightBoxes(node) + val boxes = InspectorGeometrySupport.computeHighlightBoxes(node) InspectorHighlightSnapshot( marginRect = boxes.margin, borderRect = boxes.border, @@ -965,7 +987,7 @@ class InspectorController( if (hoverPickEnabled) { val hovered = hoveredNode nativeHoveredHighlight = hovered?.let { node -> - val boxes = computeHighlightBoxes(node) + val boxes = InspectorGeometrySupport.computeHighlightBoxes(node) InspectorHighlightSnapshot( marginRect = boxes.margin, borderRect = boxes.border, @@ -1010,8 +1032,11 @@ class InspectorController( val buttonsRight = rowLeft + rowWidth - 8 val maxLabelWidth = (rowWidth - btnWidth - gap - 36).coerceAtLeast(80) val labelWidth = (rowWidth * 0.40f).toInt().coerceIn(80, maxLabelWidth) - val labelMaxChars = estimateMaxChars((labelWidth - 12).coerceAtLeast(24), labelLineHeight) - val labelLineCount = wrapText(labelText, labelMaxChars).size.coerceAtLeast(1) + val labelMaxChars = InspectorPresentationSupport.estimateMaxChars( + (labelWidth - 12).coerceAtLeast(24), + labelLineHeight + ) + val labelLineCount = InspectorPresentationSupport.wrapText(labelText, labelMaxChars).size.coerceAtLeast(1) val rowHeight = maxOf(rowHeightPx, labelLineCount * labelLineHeight + 10, controlHeight + 8) val rowRect = Rect(rowLeft, y, rowWidth, rowHeight) val controlX = rowRect.x + labelWidth @@ -1786,7 +1811,8 @@ class InspectorController( } tooltipNodeRef = node tooltipNodeBounds = bounds - tooltipLabelCache = "${nodeLabel(node)} ${bounds.width}x${bounds.height} @ ${bounds.x},${bounds.y}" + tooltipLabelCache = + "${InspectorPresentationSupport.nodeLabel(node)} ${bounds.width}x${bounds.height} @ ${bounds.x},${bounds.y}" return tooltipLabelCache } @@ -2380,82 +2406,6 @@ class InspectorController( return if (rounded % 1f == 0f) rounded.toInt().toString() else rounded.toString() } - private fun wrapPathLines(path: List, maxChars: Int): List { - if (path.isEmpty()) return listOf("Path: ") - val tokens = path.map(::pathToken) - val lines = ArrayList() - var current = "Path: " - tokens.forEachIndexed { index, token -> - val segment = if (index == 0) token else " > $token" - if (current.length + segment.length <= maxChars) { - current += segment - return@forEachIndexed - } - if (current.isNotBlank()) { - lines += current - } - val continued = if (index == 0) token else "> $token" - val wrapped = wrapText(continued, maxChars - 2) - if (wrapped.isEmpty()) { - current = " " - } else { - lines += wrapped.dropLast(1).map { " $it" } - current = " ${wrapped.last()}" - } - } - lines += current - return lines - } - - private fun estimateMaxChars(pixelWidth: Int, fontSize: Int): Int { - val approxCharWidth = (fontSize * 0.56f).toInt().coerceAtLeast(6) - return (pixelWidth / approxCharWidth).coerceAtLeast(8) - } - - private fun wrapText(text: String, maxChars: Int): List { - val limit = maxChars.coerceAtLeast(1) - if (text.length <= limit) return listOf(text) - val result = ArrayList() - var cursor = 0 - while (cursor < text.length) { - val end = (cursor + limit).coerceAtMost(text.length) - var cut = end - if (end < text.length) { - val ws = text.lastIndexOf(' ', end - 1) - if (ws >= cursor + 1) { - cut = ws - } - } - if (cut <= cursor) { - cut = end - } - result += text.substring(cursor, cut).trimEnd() - cursor = cut - while (cursor < text.length && text[cursor] == ' ') cursor++ - } - return if (result.isEmpty()) listOf("") else result - } - - private fun pathToNode(root: DOMNode, target: DOMNode): List { - val path = ArrayList(8) - if (collectPath(root, target, path)) { - return path - } - return listOf(target) - } - - private fun collectPath(node: DOMNode, target: DOMNode, path: MutableList): Boolean { - path += node - if (node === target) return true - for (child in node.children) { - if (collectPath(child, target, path)) { - return true - } - } - path.removeAt(path.lastIndex) - return false - } - private fun startMinimizedMoveDrag(mouseX: Int, mouseY: Int) { dragMode = DragMode.MinimizedMove dragStartMouseX = mouseX @@ -2735,43 +2685,6 @@ class InspectorController( private fun minimizedHeight(): Int = chipHeight - private fun wrapMinimizedLabel( - text: String, - maxCharsPerLine: Int, - maxLines: Int - ): List { - val source = text.trim() - if (source.isEmpty()) return listOf("") - if (maxCharsPerLine <= 0 || maxLines <= 0) return listOf("") - - val lines = ArrayList(maxLines) - var cursor = 0 - while (cursor < source.length && lines.size < maxLines) { - var end = (cursor + maxCharsPerLine).coerceAtMost(source.length) - if (end < source.length) { - val breakAt = source.lastIndexOf(' ', end - 1) - if (breakAt >= cursor + 1) { - end = breakAt - } - } - var line = source.substring(cursor, end).trim() - if (line.isEmpty()) { - end = (cursor + maxCharsPerLine).coerceAtMost(source.length) - line = source.substring(cursor, end) - } - lines += line - cursor = end - while (cursor < source.length && source[cursor] == ' ') cursor++ - } - if (cursor < source.length && lines.isNotEmpty()) { - val last = lines.last() - val keep = (maxCharsPerLine - 3).coerceAtLeast(0) - val trimmed = last.take(keep).trimEnd() - lines[lines.lastIndex] = if (trimmed.isEmpty()) "..." else "$trimmed..." - } - return lines - } - private fun containsReference(root: DOMNode, target: DOMNode): Boolean { if (root === target) return true root.children.forEach { child -> @@ -2793,113 +2706,9 @@ class InspectorController( return null } - private fun nodeLabel(node: DOMNode): String { - val key = node.key?.toString() ?: "" - return "${node.styleType}[$key]" - } - - private fun pathToken(node: DOMNode): String { - val key = node.key?.toString() ?: "?" - return "${node.styleType}:$key" - } - - private fun rectLabel(rect: Rect): String { - return "${rect.x},${rect.y},${rect.width}x${rect.height}" - } - private fun colorLabel(color: Int): String { val hex = color.toUInt().toString(16).uppercase().padStart(8, '0') return "#$hex" } - private fun computeBoxes(node: DOMNode): NodeBoxes { - val borderRect = node.bounds - val marginRect = Rect( - x = borderRect.x - node.margin.left, - y = borderRect.y - node.margin.top, - width = (borderRect.width + node.margin.horizontal).coerceAtLeast(0), - height = (borderRect.height + node.margin.vertical).coerceAtLeast(0) - ) - val paddingRect = Rect( - x = borderRect.x + node.border.left, - y = borderRect.y + node.border.top, - width = (borderRect.width - node.border.horizontal).coerceAtLeast(0), - height = (borderRect.height - node.border.vertical).coerceAtLeast(0) - ) - val contentRect = Rect( - x = paddingRect.x + node.padding.left, - y = paddingRect.y + node.padding.top, - width = (paddingRect.width - node.padding.horizontal).coerceAtLeast(0), - height = (paddingRect.height - node.padding.vertical).coerceAtLeast(0) - ) - val parentContent = node.parent?.let { parent -> contentRect(parent) } - return NodeBoxes( - margin = marginRect, - border = borderRect, - padding = paddingRect, - content = contentRect, - parentContent = parentContent - ) - } - - private fun computeHighlightBoxes(node: DOMNode): NodeBoxes { - val geometry = UsedInteractionGeometryResolver.resolveNodeGeometry(node) - val usedClip = geometry.usedClipRect - val borderRect = clipRectToUsedClip(geometry.usedBorderRect, usedClip) - val marginRect = clipRectToUsedClip( - Rect( - x = geometry.usedBorderRect.x - node.margin.left, - y = geometry.usedBorderRect.y - node.margin.top, - width = (geometry.usedBorderRect.width + node.margin.horizontal).coerceAtLeast(0), - height = (geometry.usedBorderRect.height + node.margin.vertical).coerceAtLeast(0) - ), - usedClip - ) - val paddingRect = clipRectToUsedClip( - Rect( - x = geometry.usedBorderRect.x + node.border.left, - y = geometry.usedBorderRect.y + node.border.top, - width = (geometry.usedBorderRect.width - node.border.horizontal).coerceAtLeast(0), - height = (geometry.usedBorderRect.height - node.border.vertical).coerceAtLeast(0) - ), - usedClip - ) - val contentRect = clipRectToUsedClip( - Rect( - x = paddingRect.x + node.padding.left, - y = paddingRect.y + node.padding.top, - width = (paddingRect.width - node.padding.horizontal).coerceAtLeast(0), - height = (paddingRect.height - node.padding.vertical).coerceAtLeast(0) - ), - usedClip - ) - val parentContent = node.parent?.let { parent -> - clipRectToUsedClip(contentRect(parent), usedClip) - } - return NodeBoxes( - margin = marginRect, - border = borderRect, - padding = paddingRect, - content = contentRect, - parentContent = parentContent - ) - } - - private fun clipRectToUsedClip(rect: Rect, clip: Rect?): Rect { - if (rect.width <= 0 || rect.height <= 0) { - return Rect(0, 0, 0, 0) - } - if (clip == null) return rect - return rect.intersection(clip) ?: Rect(0, 0, 0, 0) - } - - private fun contentRect(node: DOMNode): Rect { - return Rect( - x = node.bounds.x + node.border.left + node.padding.left, - y = node.bounds.y + node.border.top + node.padding.top, - width = (node.bounds.width - node.border.horizontal - node.padding.horizontal).coerceAtLeast(0), - height = (node.bounds.height - node.border.vertical - node.padding.vertical).coerceAtLeast(0) - ) - } - } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorGeometrySupport.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorGeometrySupport.kt new file mode 100644 index 0000000..9338ae8 --- /dev/null +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorGeometrySupport.kt @@ -0,0 +1,105 @@ +package org.dreamfinity.dsgl.core.inspector + +import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.dom.UsedInteractionGeometryResolver +import org.dreamfinity.dsgl.core.dom.layout.Rect + +internal data class InspectorNodeBoxes( + val margin: Rect, + val border: Rect, + val padding: Rect, + val content: Rect, + val parentContent: Rect? +) + +internal object InspectorGeometrySupport { + fun computeBoxes(node: DOMNode): InspectorNodeBoxes { + val borderRect = node.bounds + val marginRect = Rect( + x = borderRect.x - node.margin.left, + y = borderRect.y - node.margin.top, + width = (borderRect.width + node.margin.horizontal).coerceAtLeast(0), + height = (borderRect.height + node.margin.vertical).coerceAtLeast(0) + ) + val paddingRect = Rect( + x = borderRect.x + node.border.left, + y = borderRect.y + node.border.top, + width = (borderRect.width - node.border.horizontal).coerceAtLeast(0), + height = (borderRect.height - node.border.vertical).coerceAtLeast(0) + ) + val contentRect = Rect( + x = paddingRect.x + node.padding.left, + y = paddingRect.y + node.padding.top, + width = (paddingRect.width - node.padding.horizontal).coerceAtLeast(0), + height = (paddingRect.height - node.padding.vertical).coerceAtLeast(0) + ) + val parentContent = node.parent?.let(::contentRect) + return InspectorNodeBoxes( + margin = marginRect, + border = borderRect, + padding = paddingRect, + content = contentRect, + parentContent = parentContent + ) + } + + fun computeHighlightBoxes(node: DOMNode): InspectorNodeBoxes { + val geometry = UsedInteractionGeometryResolver.resolveNodeGeometry(node) + val usedClip = geometry.usedClipRect + val borderRect = clipRectToUsedClip(geometry.usedBorderRect, usedClip) + val marginRect = clipRectToUsedClip( + Rect( + x = geometry.usedBorderRect.x - node.margin.left, + y = geometry.usedBorderRect.y - node.margin.top, + width = (geometry.usedBorderRect.width + node.margin.horizontal).coerceAtLeast(0), + height = (geometry.usedBorderRect.height + node.margin.vertical).coerceAtLeast(0) + ), + usedClip + ) + val paddingRect = clipRectToUsedClip( + Rect( + x = geometry.usedBorderRect.x + node.border.left, + y = geometry.usedBorderRect.y + node.border.top, + width = (geometry.usedBorderRect.width - node.border.horizontal).coerceAtLeast(0), + height = (geometry.usedBorderRect.height - node.border.vertical).coerceAtLeast(0) + ), + usedClip + ) + val contentRect = clipRectToUsedClip( + Rect( + x = paddingRect.x + node.padding.left, + y = paddingRect.y + node.padding.top, + width = (paddingRect.width - node.padding.horizontal).coerceAtLeast(0), + height = (paddingRect.height - node.padding.vertical).coerceAtLeast(0) + ), + usedClip + ) + val parentContent = node.parent?.let { parent -> + clipRectToUsedClip(contentRect(parent), usedClip) + } + return InspectorNodeBoxes( + margin = marginRect, + border = borderRect, + padding = paddingRect, + content = contentRect, + parentContent = parentContent + ) + } + + private fun clipRectToUsedClip(rect: Rect, clip: Rect?): Rect { + if (rect.width <= 0 || rect.height <= 0) { + return Rect(0, 0, 0, 0) + } + if (clip == null) return rect + return rect.intersection(clip) ?: Rect(0, 0, 0, 0) + } + + fun contentRect(node: DOMNode): Rect { + return Rect( + x = node.bounds.x + node.border.left + node.padding.left, + y = node.bounds.y + node.border.top + node.padding.top, + width = (node.bounds.width - node.border.horizontal - node.padding.horizontal).coerceAtLeast(0), + height = (node.bounds.height - node.border.vertical - node.padding.vertical).coerceAtLeast(0) + ) + } +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorPresentationSupport.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorPresentationSupport.kt new file mode 100644 index 0000000..4772fa2 --- /dev/null +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorPresentationSupport.kt @@ -0,0 +1,133 @@ +package org.dreamfinity.dsgl.core.inspector + +import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.dom.layout.Rect + +internal object InspectorPresentationSupport { + fun nodeLabel(node: DOMNode): String { + val key = node.key?.toString() ?: "" + return "${node.styleType}[$key]" + } + + fun rectLabel(rect: Rect): String { + return "${rect.x},${rect.y},${rect.width}x${rect.height}" + } + + fun estimateMaxChars(pixelWidth: Int, fontSize: Int): Int { + val approxCharWidth = (fontSize * 0.56f).toInt().coerceAtLeast(6) + return (pixelWidth / approxCharWidth).coerceAtLeast(8) + } + + fun wrapText(text: String, maxChars: Int): List { + val limit = maxChars.coerceAtLeast(1) + if (text.length <= limit) return listOf(text) + val result = ArrayList() + var cursor = 0 + while (cursor < text.length) { + val end = (cursor + limit).coerceAtMost(text.length) + var cut = end + if (end < text.length) { + val ws = text.lastIndexOf(' ', end - 1) + if (ws >= cursor + 1) { + cut = ws + } + } + if (cut <= cursor) { + cut = end + } + result += text.substring(cursor, cut).trimEnd() + cursor = cut + while (cursor < text.length && text[cursor] == ' ') cursor++ + } + return if (result.isEmpty()) listOf("") else result + } + + fun pathToNode(root: DOMNode, target: DOMNode): List { + val path = ArrayList(8) + if (collectPath(root, target, path)) { + return path + } + return listOf(target) + } + + fun wrapPathLines(path: List, maxChars: Int): List { + if (path.isEmpty()) return listOf("Path: ") + val tokens = path.map(::pathToken) + val lines = ArrayList() + var current = "Path: " + tokens.forEachIndexed { index, token -> + val segment = if (index == 0) token else " > $token" + if (current.length + segment.length <= maxChars) { + current += segment + return@forEachIndexed + } + if (current.isNotBlank()) { + lines += current + } + val continued = if (index == 0) token else "> $token" + val wrapped = wrapText(continued, maxChars - 2) + if (wrapped.isEmpty()) { + current = " " + } else { + lines += wrapped.dropLast(1).map { " $it" } + current = " ${wrapped.last()}" + } + } + lines += current + return lines + } + + fun wrapMinimizedLabel( + text: String, + maxCharsPerLine: Int, + maxLines: Int + ): List { + val source = text.trim() + if (source.isEmpty()) return listOf("") + if (maxCharsPerLine <= 0 || maxLines <= 0) return listOf("") + + val lines = ArrayList(maxLines) + var cursor = 0 + while (cursor < source.length && lines.size < maxLines) { + var end = (cursor + maxCharsPerLine).coerceAtMost(source.length) + if (end < source.length) { + val breakAt = source.lastIndexOf(' ', end - 1) + if (breakAt >= cursor + 1) { + end = breakAt + } + } + var line = source.substring(cursor, end).trim() + if (line.isEmpty()) { + end = (cursor + maxCharsPerLine).coerceAtMost(source.length) + line = source.substring(cursor, end) + } + lines += line + cursor = end + while (cursor < source.length && source[cursor] == ' ') cursor++ + } + if (cursor < source.length && lines.isNotEmpty()) { + val last = lines.last() + val keep = (maxCharsPerLine - 3).coerceAtLeast(0) + val trimmed = last.take(keep).trimEnd() + lines[lines.lastIndex] = if (trimmed.isEmpty()) "..." else "$trimmed..." + } + return lines + } + + private fun collectPath(node: DOMNode, target: DOMNode, path: MutableList): Boolean { + path += node + if (node === target) return true + for (child in node.children) { + if (collectPath(child, target, path)) { + return true + } + } + path.removeAt(path.lastIndex) + return false + } + + private fun pathToken(node: DOMNode): String { + val key = node.key?.toString() ?: "?" + return "${node.styleType}:$key" + } +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt index 4c52e5d..9f57a8c 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt @@ -238,7 +238,39 @@ internal class SystemInspectorOverlayNode( if (controller.blocksUnderlyingInput() || overlayPanel.isDragging() || minimizedChipDragSession != null) { return viewportRect } - return panelRect ?: viewportRect + val base = panelRect ?: viewportRect + val dropdownBounds = resolveRenderedDropdownInputBounds() + if (dropdownBounds == null) { + return base + } + return unionRect(base, dropdownBounds) + } + + private fun resolveRenderedDropdownInputBounds(): Rect? { + val dropdowns = controller.debugStyleEditorDropdowns() + if (dropdowns.isNotEmpty()) { + return dropdowns + .map { it.popupRect } + .reduce(::unionRect) + } + return if (activeDomDropdown != null) { + Rect(0, 0, lastViewportWidth.coerceAtLeast(1), lastViewportHeight.coerceAtLeast(1)) + } else { + null + } + } + + private fun unionRect(a: Rect, b: Rect): Rect { + val left = minOf(a.x, b.x) + val top = minOf(a.y, b.y) + val right = maxOf(a.x + a.width, b.x + b.width) + val bottom = maxOf(a.y + a.height, b.y + b.height) + return Rect( + x = left, + y = top, + width = (right - left).coerceAtLeast(0), + height = (bottom - top).coerceAtLeast(0) + ) } private fun clearTree() { diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayInputBoundsTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayInputBoundsTests.kt new file mode 100644 index 0000000..96e87d4 --- /dev/null +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayInputBoundsTests.kt @@ -0,0 +1,57 @@ +package org.dreamfinity.dsgl.core.inspector.internal + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.inspector.InspectorController +import org.dreamfinity.dsgl.core.inspector.InspectorDropdownOptionSnapshot +import org.dreamfinity.dsgl.core.inspector.InspectorDropdownSnapshot +import org.dreamfinity.dsgl.core.style.StyleProperty + +class SystemInspectorOverlayInputBoundsTests { + + @Test + fun `input bounds include rendered dropdown popup outside panel`() { + val controller = InspectorController().also { + it.toggle() + it.setPickMode(false) + } + val node = SystemInspectorOverlayNode(controller) + val panelRect = controller.debugPanelRect() ?: error("expected panel rect") + + val popupRect = Rect( + x = panelRect.x + panelRect.width + 32, + y = panelRect.y + 80, + width = 180, + height = 120 + ) + controller.onNativeDomDropdownSnapshots( + listOf( + InspectorDropdownSnapshot( + popupRect = popupRect, + property = StyleProperty.ALIGN, + unitSelect = false, + options = listOf( + InspectorDropdownOptionSnapshot( + rect = Rect(popupRect.x + 2, popupRect.y + 2, popupRect.width - 4, 24), + text = "start", + value = "start", + hovered = false + ) + ), + footerText = null + ) + ) + ) + + node.syncInputBounds(viewportWidth = 1400, viewportHeight = 800) + val popupProbeX = popupRect.x + 12 + val popupProbeY = popupRect.y + 12 + assertTrue(node.bounds.contains(popupProbeX, popupProbeY)) + + controller.onNativeDomDropdownSnapshots(emptyList()) + node.syncInputBounds(viewportWidth = 1400, viewportHeight = 800) + assertFalse(node.bounds.contains(popupProbeX, popupProbeY)) + } +} From 0dadaf9dde27d2bbf5f537fbe6da6e07a1213050 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Mon, 30 Mar 2026 11:02:07 +0300 Subject: [PATCH 05/78] separating inspector snapshot builder to another class; --- .../core/inspector/InspectorController.kt | 368 +++------------ .../InspectorStyleEditorSnapshotBuilder.kt | 433 ++++++++++++++++++ 2 files changed, 503 insertions(+), 298 deletions(-) create mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorStyleEditorSnapshotBuilder.kt diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt index 71ee865..e362dcc 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt @@ -222,6 +222,11 @@ class InspectorController( private val nativeDropdowns: MutableList = ArrayList() private var nativeStyleEditorResetRect: Rect = Rect(0, 0, 0, 0) private var nativeStyleEditorClearRect: Rect = Rect(0, 0, 0, 0) + private val styleEditorSnapshotBuilder: InspectorStyleEditorSnapshotBuilder = + InspectorStyleEditorSnapshotBuilder( + resolveLiteralFromComputed = ::literalFromComputed, + renderExpressionLabel = ::expressionLabel + ) private val minPanelWidth: Int = 240 private val minPanelHeight: Int = 160 @@ -884,12 +889,48 @@ class InspectorController( val inspection = selectionStyle(selected) val styleEditorStartY = y + 2 - y = buildNativeStyleEditorSection( - panelRect = clamped, - selected = selected, - inspection = inspection, - startY = styleEditorStartY + val styleEditorSnapshots = styleEditorSnapshotBuilder.build( + InspectorStyleEditorSnapshotBuildContext( + panelRect = clamped, + panelBounds = panelBounds, + selected = selected, + inspection = inspection, + editableProperties = editablePropertiesFor(selected), + startY = styleEditorStartY, + lineHeightPx = lineHeightPx, + rowHeightPx = rowHeightPx, + secondaryFontSizePx = secondaryFontSizePx, + pointerProjectionScrollY = resolvedNativePointerProjectionScrollY(), + mouseX = mouseX, + mouseY = mouseY, + viewportWidth = viewportW, + viewportHeight = viewportH, + openValueSelectProperty = openValueSelectProperty, + openUnitSelectProperty = openUnitSelectProperty, + openValueSelectScrollIndex = openValueSelectScrollIndex, + openUnitSelectScrollIndex = openUnitSelectScrollIndex + ) ) + y = styleEditorSnapshots.endY + nativeVariableTooltip = styleEditorSnapshots.variableTooltip + nativeStyleEditorRows.addAll(styleEditorSnapshots.rows) + nativeDropdowns.addAll(styleEditorSnapshots.dropdowns) + styleEditorSnapshots.dropdownLayouts.forEach { layout -> + dropdownLayouts += DropdownLayout( + rect = layout.rect, + property = layout.property, + isUnit = layout.unitSelect, + totalOptions = layout.totalOptions, + visibleRows = layout.visibleRows + ) + } + styleEditorSnapshots.actionSpecs.forEach { action -> + panelActions += toPanelAction(action) + } + nativeStyleEditorResetRect = styleEditorSnapshots.resetRect + nativeStyleEditorClearRect = styleEditorSnapshots.clearRect + openValueSelectScrollIndex = styleEditorSnapshots.openValueSelectScrollIndex + openUnitSelectScrollIndex = styleEditorSnapshots.openUnitSelectScrollIndex val styleEditorHeight = (y - styleEditorStartY).coerceAtLeast(0) y = appendDomLine(infoLines, y, "Computed styles:", maxChars) @@ -1006,302 +1047,38 @@ class InspectorController( } } - private fun buildNativeStyleEditorSection( - panelRect: Rect, - selected: DOMNode, - inspection: StyleInspection, - startY: Int - ): Int { - var y = startY + lineHeightPx - nativeVariableTooltip = null - val pointerProjectionScrollY = resolvedNativePointerProjectionScrollY() - - val rowLeft = panelRect.x + 10 - val rowWidth = panelRect.width - 20 - val btnWidth = 40 - val gap = 6 - val controlHeight = (rowHeightPx - 8).coerceAtLeast(22) - val labelLineHeight = (secondaryFontSizePx - 2).coerceAtLeast(18) - val properties = editablePropertiesFor(selected) - for (property in properties) { - val overrideExpr = StyleEngine.inspectorOverrideFor(selected, property) - val effectiveValue = overrideExpr?.let(::expressionLabel) ?: literalFromComputed(inspection.computed, property) - val sourceTag = if (overrideExpr != null) "ins" else (inspection.propertySources[property]?.source ?: "default") - - val labelText = "${property.key} [$sourceTag]" - val buttonsRight = rowLeft + rowWidth - 8 - val maxLabelWidth = (rowWidth - btnWidth - gap - 36).coerceAtLeast(80) - val labelWidth = (rowWidth * 0.40f).toInt().coerceIn(80, maxLabelWidth) - val labelMaxChars = InspectorPresentationSupport.estimateMaxChars( - (labelWidth - 12).coerceAtLeast(24), - labelLineHeight - ) - val labelLineCount = InspectorPresentationSupport.wrapText(labelText, labelMaxChars).size.coerceAtLeast(1) - val rowHeight = maxOf(rowHeightPx, labelLineCount * labelLineHeight + 10, controlHeight + 8) - val rowRect = Rect(rowLeft, y, rowWidth, rowHeight) - val controlX = rowRect.x + labelWidth - val controlWidth = (buttonsRight - controlX - btnWidth - gap).coerceAtLeast(36) - val controlY = rowRect.y + ((rowRect.height - controlHeight) / 2) - val resetRect = Rect(buttonsRight - btnWidth, controlY, btnWidth, controlHeight) - panelActions += PanelAction( - bounds = resetRect, + private fun toPanelAction(action: InspectorStyleEditorActionSpec): PanelAction { + fun editAction(operation: EditOperation): PanelAction { + return PanelAction( + bounds = action.bounds, kind = ActionKind.EditProperty, - property = property, - editOperation = EditOperation.ResetProperty + property = requireNotNull(action.property), + editOperation = operation, + step = action.step, + payload = action.payload ) - - val editor = InspectorEditorRegistry.describe( - property = property, - literal = effectiveValue, - expression = overrideExpr + } + return when (action.type) { + InspectorStyleEditorActionType.ResetProperty -> editAction(EditOperation.ResetProperty) + InspectorStyleEditorActionType.ToggleValueSelect -> editAction(EditOperation.ToggleValueSelect) + InspectorStyleEditorActionType.SelectValueOption -> editAction(EditOperation.SelectValueOption) + InspectorStyleEditorActionType.OpenColorPicker -> editAction(EditOperation.OpenColorPicker) + InspectorStyleEditorActionType.Decrement -> editAction(EditOperation.Decrement) + InspectorStyleEditorActionType.Increment -> editAction(EditOperation.Increment) + InspectorStyleEditorActionType.ToggleUnitSelect -> editAction(EditOperation.ToggleUnitSelect) + InspectorStyleEditorActionType.SelectUnitOption -> editAction(EditOperation.SelectUnitOption) + InspectorStyleEditorActionType.ResetSelectedOverrides -> PanelAction( + bounds = action.bounds, + kind = ActionKind.ResetSelectedOverrides ) - val contentRect = Rect(controlX, controlY, controlWidth, controlHeight) - var rowSnapshot = InspectorStyleEditorRowSnapshot( - property = property, - sourceTag = sourceTag, - rowRect = rowRect, - labelText = labelText, - resetRect = resetRect, - editorKind = editor.kind, - controlRect = contentRect, - controlValue = effectiveValue, - controlOpen = false, - controlHovered = false, - inputActive = false, - decrementRect = null, - inputRect = null, - incrementRect = null, - unitRect = null, - unitValue = null, - unitOpen = false, - colorPreviewRect = null, - colorPreviewColor = null + InspectorStyleEditorActionType.ClearAllOverrides -> PanelAction( + bounds = action.bounds, + kind = ActionKind.ClearAllOverrides ) - - when (editor.kind) { - InspectorEditorKind.EnumSelect, - InspectorEditorKind.FontSelect -> { - val isOpen = openValueSelectProperty == property - val projectedControlRect = projectRectForNativePointer(contentRect, pointerProjectionScrollY) - panelActions += PanelAction( - bounds = contentRect, - kind = ActionKind.EditProperty, - property = property, - editOperation = EditOperation.ToggleValueSelect - ) - rowSnapshot = rowSnapshot.copy( - controlOpen = isOpen, - controlHovered = projectedControlRect.contains(mouseX, mouseY) - ) - } - - InspectorEditorKind.StringInput -> { - var previewRect: Rect? = null - var previewColor: Int? = null - if (editor.showColorPreview) { - previewRect = Rect( - x = contentRect.x + contentRect.width - (contentRect.height - 8).coerceAtLeast(10) - 6, - y = contentRect.y + 4, - width = (contentRect.height - 8).coerceAtLeast(10), - height = (contentRect.height - 8).coerceAtLeast(10) - ) - previewColor = runCatching { parseColor(effectiveValue) }.getOrNull() - panelActions += PanelAction( - bounds = previewRect, - kind = ActionKind.EditProperty, - property = property, - editOperation = EditOperation.OpenColorPicker - ) - } - rowSnapshot = rowSnapshot.copy( - controlValue = effectiveValue, - inputActive = false, - colorPreviewRect = previewRect, - colorPreviewColor = previewColor - ) - } - - InspectorEditorKind.NumericInput -> { - val step = StylePropertyRegistry.descriptor(property).numericStep - val parsed = InspectorEditorRegistry.parseNumericLiteral(property, effectiveValue) - val numericValue = parsed?.numberText ?: "0" - val unit = parsed?.unit ?: InspectorEditorRegistry.defaultNumericUnit(property) - val buttonWidth = 34 - val unitWidth = if (editor.supportsUnits) 68 else 0 - val inputWidth = - (contentRect.width - buttonWidth * 2 - unitWidth - (if (editor.supportsUnits) 12 else 6)) - .coerceAtLeast(64) - val decRect = Rect(contentRect.x, contentRect.y, buttonWidth, contentRect.height) - val inputRect = Rect(decRect.x + decRect.width + 4, contentRect.y, inputWidth, contentRect.height) - val incRect = Rect(inputRect.x + inputRect.width + 4, contentRect.y, buttonWidth, contentRect.height) - panelActions += PanelAction( - decRect, - ActionKind.EditProperty, - property = property, - editOperation = EditOperation.Decrement, - step = step - ) - - panelActions += PanelAction( - incRect, - ActionKind.EditProperty, - property = property, - editOperation = EditOperation.Increment, - step = step - ) - var unitRect: Rect? = null - if (editor.supportsUnits) { - unitRect = Rect(incRect.x + incRect.width + 4, contentRect.y, unitWidth, contentRect.height) - panelActions += PanelAction( - bounds = unitRect, - kind = ActionKind.EditProperty, - property = property, - editOperation = EditOperation.ToggleUnitSelect - ) - } - rowSnapshot = rowSnapshot.copy( - controlValue = numericValue, - inputActive = false, - decrementRect = decRect, - inputRect = inputRect, - incrementRect = incRect, - unitRect = unitRect, - unitValue = if (editor.supportsUnits) unit?.token else null, - unitOpen = openUnitSelectProperty == property, - controlOpen = false - ) - } - } - - val projectedRowRect = projectRectForNativePointer(rowRect, pointerProjectionScrollY) - if (overrideExpr is StyleExpression.VariableRef && projectedRowRect.contains(mouseX, mouseY)) { - val resolved = StyleEngine.resolveInspectorVariable(overrideExpr.name) - val body = resolved.getOrElse { "unresolved (${it.message ?: "unknown error"})" } - nativeVariableTooltip = InspectorTooltipSnapshot( - text = "${overrideExpr.name} = $body", - rect = Rect( - x = (projectedRowRect.x + projectedRowRect.width - 360).coerceAtLeast(panelBounds.x + 8), - y = (projectedRowRect.y - lineHeightPx - 8).coerceAtLeast(panelBounds.y + 8), - width = 352, - height = lineHeightPx + 10 - ) - ) - } - - if (openValueSelectProperty == property && editor.options.isNotEmpty()) { - buildNativeDropdownSnapshot( - x = contentRect.x, - y = contentRect.y + contentRect.height + 2, - width = contentRect.width, - options = editor.options, - property = property, - operation = EditOperation.SelectValueOption, - pointerProjectionScrollY = pointerProjectionScrollY - ) - rowSnapshot = rowSnapshot.copy(controlOpen = true) - } - if (openUnitSelectProperty == property && editor.supportsUnits) { - val units = InspectorEditorRegistry.unitOptions().map { it.token } - buildNativeDropdownSnapshot( - x = contentRect.x + contentRect.width - 90, - y = contentRect.y + contentRect.height + 2, - width = 90, - options = units, - property = property, - operation = EditOperation.SelectUnitOption, - pointerProjectionScrollY = pointerProjectionScrollY - ) - rowSnapshot = rowSnapshot.copy(unitOpen = true) - } - - nativeStyleEditorRows += rowSnapshot - y += rowHeight + 4 } - - val actionHeight = (secondaryFontSizePx + 10).coerceAtLeast(28) - val resetRect = Rect(rowLeft, y, 140, actionHeight) - val clearRect = Rect(rowLeft + 148, y, 160, actionHeight) - panelActions += PanelAction(resetRect, ActionKind.ResetSelectedOverrides) - panelActions += PanelAction(clearRect, ActionKind.ClearAllOverrides) - nativeStyleEditorResetRect = resetRect - nativeStyleEditorClearRect = clearRect - y += actionHeight + 4 - - return y } - private fun buildNativeDropdownSnapshot( - x: Int, - y: Int, - width: Int, - options: List, - property: StyleProperty, - operation: EditOperation, - pointerProjectionScrollY: Int - ) { - if (options.isEmpty()) return - val maxRows = 8 - val visibleRows = minOf(maxRows, options.size) - val maxFirst = (options.size - visibleRows).coerceAtLeast(0) - val isUnit = operation == EditOperation.SelectUnitOption - val rawFirst = if (isUnit) openUnitSelectScrollIndex else openValueSelectScrollIndex - val first = rawFirst.coerceIn(0, maxFirst) - if (isUnit) { - openUnitSelectScrollIndex = first - } else { - openValueSelectScrollIndex = first - } - val shown = options.subList(first, first + visibleRows) - val optionHeight = rowHeightPx - val popupHeight = optionHeight * shown.size + 6 - val viewportWidth = viewportW.coerceAtLeast(1) - val viewportHeight = viewportH.coerceAtLeast(1) - val clampedX = x.coerceIn(2, (viewportWidth - width - 2).coerceAtLeast(2)) - val clampedY = y.coerceIn(2, (viewportHeight - popupHeight - 2).coerceAtLeast(2)) - val popupRect = Rect(clampedX, clampedY, width, popupHeight) - dropdownLayouts += DropdownLayout( - rect = popupRect, - property = property, - isUnit = isUnit, - totalOptions = options.size, - visibleRows = visibleRows - ) - - var optionY = popupRect.y + 3 - val optionSnapshots = ArrayList(shown.size) - shown.forEach { option -> - val optionRect = Rect(popupRect.x + 3, optionY, popupRect.width - 6, optionHeight - 2) - val hovered = projectRectForNativePointer(optionRect, pointerProjectionScrollY).contains(mouseX, mouseY) - optionSnapshots += InspectorDropdownOptionSnapshot( - rect = optionRect, - text = ellipsize(option, 30), - value = option, - hovered = hovered - ) - panelActions += PanelAction( - bounds = optionRect, - kind = ActionKind.EditProperty, - property = property, - editOperation = operation, - payload = option - ) - optionY += optionHeight - } - - val footer = if (options.size > visibleRows) { - "${first + 1}-${first + visibleRows}/${options.size}" - } else { - null - } - nativeDropdowns += InspectorDropdownSnapshot( - popupRect = popupRect, - property = property, - unitSelect = isUnit, - options = optionSnapshots, - footerText = footer - ) - } private fun updateScrollbarGeometry(bodyRect: Rect) { if (panelContentHeight <= bodyRect.height || bodyRect.height <= 0) { scrollbarTrackRect = Rect(0, 0, 0, 0) @@ -1692,11 +1469,6 @@ class InspectorController( } } - private fun projectRectForNativePointer(rect: Rect, scrollY: Int): Rect { - if (scrollY <= 0) return rect - return Rect(rect.x, rect.y - scrollY, rect.width, rect.height) - } - private fun selectHovered(lock: Boolean) { val hovered = hoveredNode ?: return selectedNode = hovered diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorStyleEditorSnapshotBuilder.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorStyleEditorSnapshotBuilder.kt new file mode 100644 index 0000000..9d2e05a --- /dev/null +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorStyleEditorSnapshotBuilder.kt @@ -0,0 +1,433 @@ +package org.dreamfinity.dsgl.core.inspector + +import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.style.ComputedStyle +import org.dreamfinity.dsgl.core.style.StyleEngine +import org.dreamfinity.dsgl.core.style.StyleExpression +import org.dreamfinity.dsgl.core.style.StyleInspection +import org.dreamfinity.dsgl.core.style.StyleProperty +import org.dreamfinity.dsgl.core.style.StylePropertyRegistry +import org.dreamfinity.dsgl.core.style.parseColor + +internal enum class InspectorStyleEditorActionType { + ResetProperty, + ToggleValueSelect, + SelectValueOption, + OpenColorPicker, + Decrement, + Increment, + ToggleUnitSelect, + SelectUnitOption, + ResetSelectedOverrides, + ClearAllOverrides +} + +internal data class InspectorStyleEditorActionSpec( + val bounds: Rect, + val type: InspectorStyleEditorActionType, + val property: StyleProperty? = null, + val step: Float = 1f, + val payload: String? = null +) + +internal data class InspectorStyleEditorDropdownLayout( + val rect: Rect, + val property: StyleProperty, + val unitSelect: Boolean, + val totalOptions: Int, + val visibleRows: Int +) + +internal data class InspectorStyleEditorSnapshotBuildContext( + val panelRect: Rect, + val panelBounds: Rect, + val selected: DOMNode, + val inspection: StyleInspection, + val editableProperties: List, + val startY: Int, + val lineHeightPx: Int, + val rowHeightPx: Int, + val secondaryFontSizePx: Int, + val pointerProjectionScrollY: Int, + val mouseX: Int, + val mouseY: Int, + val viewportWidth: Int, + val viewportHeight: Int, + val openValueSelectProperty: StyleProperty?, + val openUnitSelectProperty: StyleProperty?, + val openValueSelectScrollIndex: Int, + val openUnitSelectScrollIndex: Int +) + +internal data class InspectorStyleEditorSnapshotBuildResult( + val endY: Int, + val variableTooltip: InspectorTooltipSnapshot?, + val rows: List, + val dropdowns: List, + val dropdownLayouts: List, + val actionSpecs: List, + val resetRect: Rect, + val clearRect: Rect, + val openValueSelectScrollIndex: Int, + val openUnitSelectScrollIndex: Int +) + +internal class InspectorStyleEditorSnapshotBuilder( + private val resolveLiteralFromComputed: (ComputedStyle, StyleProperty) -> String, + private val renderExpressionLabel: (StyleExpression) -> String +) { + + fun build(context: InspectorStyleEditorSnapshotBuildContext): InspectorStyleEditorSnapshotBuildResult { + var y = context.startY + context.lineHeightPx + var variableTooltip: InspectorTooltipSnapshot? = null + var openValueSelectScrollIndex = context.openValueSelectScrollIndex + var openUnitSelectScrollIndex = context.openUnitSelectScrollIndex + + val rowSnapshots = ArrayList(context.editableProperties.size) + val dropdownSnapshots = ArrayList(2) + val dropdownLayouts = ArrayList(2) + val actionSpecs = ArrayList(context.editableProperties.size * 4) + + val rowLeft = context.panelRect.x + 10 + val rowWidth = context.panelRect.width - 20 + val btnWidth = 40 + val gap = 6 + val controlHeight = (context.rowHeightPx - 8).coerceAtLeast(22) + val labelLineHeight = (context.secondaryFontSizePx - 2).coerceAtLeast(18) + + context.editableProperties.forEach { property -> + val overrideExpr = StyleEngine.inspectorOverrideFor(context.selected, property) + val effectiveValue = overrideExpr?.let(renderExpressionLabel) + ?: resolveLiteralFromComputed(context.inspection.computed, property) + val sourceTag = if (overrideExpr != null) { + "ins" + } else { + context.inspection.propertySources[property]?.source ?: "default" + } + + val labelText = "${property.key} [$sourceTag]" + val buttonsRight = rowLeft + rowWidth - 8 + val maxLabelWidth = (rowWidth - btnWidth - gap - 36).coerceAtLeast(80) + val labelWidth = (rowWidth * 0.40f).toInt().coerceIn(80, maxLabelWidth) + val labelMaxChars = + InspectorPresentationSupport.estimateMaxChars((labelWidth - 12).coerceAtLeast(24), labelLineHeight) + val labelLineCount = InspectorPresentationSupport.wrapText(labelText, labelMaxChars).size.coerceAtLeast(1) + val rowHeight = maxOf(context.rowHeightPx, labelLineCount * labelLineHeight + 10, controlHeight + 8) + val rowRect = Rect(rowLeft, y, rowWidth, rowHeight) + val controlX = rowRect.x + labelWidth + val controlWidth = (buttonsRight - controlX - btnWidth - gap).coerceAtLeast(36) + val controlY = rowRect.y + ((rowRect.height - controlHeight) / 2) + val resetRect = Rect(buttonsRight - btnWidth, controlY, btnWidth, controlHeight) + actionSpecs += InspectorStyleEditorActionSpec( + bounds = resetRect, + type = InspectorStyleEditorActionType.ResetProperty, + property = property + ) + + val editor = InspectorEditorRegistry.describe( + property = property, + literal = effectiveValue, + expression = overrideExpr + ) + val controlRect = Rect(controlX, controlY, controlWidth, controlHeight) + + var rowSnapshot = InspectorStyleEditorRowSnapshot( + property = property, + sourceTag = sourceTag, + rowRect = rowRect, + labelText = labelText, + resetRect = resetRect, + editorKind = editor.kind, + controlRect = controlRect, + controlValue = effectiveValue, + controlOpen = false, + controlHovered = false, + inputActive = false, + decrementRect = null, + inputRect = null, + incrementRect = null, + unitRect = null, + unitValue = null, + unitOpen = false, + colorPreviewRect = null, + colorPreviewColor = null + ) + + when (editor.kind) { + InspectorEditorKind.EnumSelect, + InspectorEditorKind.FontSelect -> { + val isOpen = context.openValueSelectProperty == property + actionSpecs += InspectorStyleEditorActionSpec( + bounds = controlRect, + type = InspectorStyleEditorActionType.ToggleValueSelect, + property = property + ) + rowSnapshot = rowSnapshot.copy( + controlOpen = isOpen, + controlHovered = projectRectForPointer(controlRect, context.pointerProjectionScrollY).contains( + context.mouseX, + context.mouseY + ) + ) + } + + InspectorEditorKind.StringInput -> { + var previewRect: Rect? = null + var previewColor: Int? = null + if (editor.showColorPreview) { + previewRect = Rect( + x = controlRect.x + controlRect.width - (controlRect.height - 8).coerceAtLeast(10) - 6, + y = controlRect.y + 4, + width = (controlRect.height - 8).coerceAtLeast(10), + height = (controlRect.height - 8).coerceAtLeast(10) + ) + previewColor = runCatching { parseColor(effectiveValue) }.getOrNull() + actionSpecs += InspectorStyleEditorActionSpec( + bounds = previewRect, + type = InspectorStyleEditorActionType.OpenColorPicker, + property = property + ) + } + rowSnapshot = rowSnapshot.copy( + controlValue = effectiveValue, + inputActive = false, + colorPreviewRect = previewRect, + colorPreviewColor = previewColor + ) + } + + InspectorEditorKind.NumericInput -> { + val step = StylePropertyRegistry.descriptor(property).numericStep + val parsed = InspectorEditorRegistry.parseNumericLiteral(property, effectiveValue) + val numericValue = parsed?.numberText ?: "0" + val unit = parsed?.unit ?: InspectorEditorRegistry.defaultNumericUnit(property) + val buttonWidth = 34 + val unitWidth = if (editor.supportsUnits) 68 else 0 + val inputWidth = + (controlRect.width - buttonWidth * 2 - unitWidth - (if (editor.supportsUnits) 12 else 6)) + .coerceAtLeast(64) + val decRect = Rect(controlRect.x, controlRect.y, buttonWidth, controlRect.height) + val inputRect = Rect(decRect.x + decRect.width + 4, controlRect.y, inputWidth, controlRect.height) + val incRect = Rect(inputRect.x + inputRect.width + 4, controlRect.y, buttonWidth, controlRect.height) + actionSpecs += InspectorStyleEditorActionSpec( + bounds = decRect, + type = InspectorStyleEditorActionType.Decrement, + property = property, + step = step + ) + actionSpecs += InspectorStyleEditorActionSpec( + bounds = incRect, + type = InspectorStyleEditorActionType.Increment, + property = property, + step = step + ) + var unitRect: Rect? = null + if (editor.supportsUnits) { + unitRect = Rect(incRect.x + incRect.width + 4, controlRect.y, unitWidth, controlRect.height) + actionSpecs += InspectorStyleEditorActionSpec( + bounds = unitRect, + type = InspectorStyleEditorActionType.ToggleUnitSelect, + property = property + ) + } + rowSnapshot = rowSnapshot.copy( + controlValue = numericValue, + inputActive = false, + decrementRect = decRect, + inputRect = inputRect, + incrementRect = incRect, + unitRect = unitRect, + unitValue = if (editor.supportsUnits) unit?.token else null, + unitOpen = context.openUnitSelectProperty == property, + controlOpen = false + ) + } + } + + val projectedRowRect = projectRectForPointer(rowRect, context.pointerProjectionScrollY) + if (overrideExpr is StyleExpression.VariableRef && projectedRowRect.contains(context.mouseX, context.mouseY)) { + val resolved = StyleEngine.resolveInspectorVariable(overrideExpr.name) + val body = resolved.getOrElse { "unresolved (${it.message ?: "unknown error"})" } + variableTooltip = InspectorTooltipSnapshot( + text = "${overrideExpr.name} = $body", + rect = Rect( + x = (projectedRowRect.x + projectedRowRect.width - 360).coerceAtLeast(context.panelBounds.x + 8), + y = (projectedRowRect.y - context.lineHeightPx - 8).coerceAtLeast(context.panelBounds.y + 8), + width = 352, + height = context.lineHeightPx + 10 + ) + ) + } + + if (context.openValueSelectProperty == property && editor.options.isNotEmpty()) { + val dropdown = buildDropdownSnapshot( + x = controlRect.x, + y = controlRect.y + controlRect.height + 2, + width = controlRect.width, + options = editor.options, + property = property, + unitSelect = false, + pointerProjectionScrollY = context.pointerProjectionScrollY, + rowHeightPx = context.rowHeightPx, + viewportWidth = context.viewportWidth, + viewportHeight = context.viewportHeight, + mouseX = context.mouseX, + mouseY = context.mouseY, + currentScrollIndex = openValueSelectScrollIndex + ) + openValueSelectScrollIndex = dropdown.nextScrollIndex + dropdownLayouts += dropdown.layout + dropdownSnapshots += dropdown.snapshot + actionSpecs += dropdown.optionActionSpecs + rowSnapshot = rowSnapshot.copy(controlOpen = true) + } + if (context.openUnitSelectProperty == property && editor.supportsUnits) { + val units = InspectorEditorRegistry.unitOptions().map { it.token } + val dropdown = buildDropdownSnapshot( + x = controlRect.x + controlRect.width - 90, + y = controlRect.y + controlRect.height + 2, + width = 90, + options = units, + property = property, + unitSelect = true, + pointerProjectionScrollY = context.pointerProjectionScrollY, + rowHeightPx = context.rowHeightPx, + viewportWidth = context.viewportWidth, + viewportHeight = context.viewportHeight, + mouseX = context.mouseX, + mouseY = context.mouseY, + currentScrollIndex = openUnitSelectScrollIndex + ) + openUnitSelectScrollIndex = dropdown.nextScrollIndex + dropdownLayouts += dropdown.layout + dropdownSnapshots += dropdown.snapshot + actionSpecs += dropdown.optionActionSpecs + rowSnapshot = rowSnapshot.copy(unitOpen = true) + } + + rowSnapshots += rowSnapshot + y += rowHeight + 4 + } + + val actionHeight = (context.secondaryFontSizePx + 10).coerceAtLeast(28) + val resetRect = Rect(rowLeft, y, 140, actionHeight) + val clearRect = Rect(rowLeft + 148, y, 160, actionHeight) + actionSpecs += InspectorStyleEditorActionSpec(resetRect, InspectorStyleEditorActionType.ResetSelectedOverrides) + actionSpecs += InspectorStyleEditorActionSpec(clearRect, InspectorStyleEditorActionType.ClearAllOverrides) + y += actionHeight + 4 + + return InspectorStyleEditorSnapshotBuildResult( + endY = y, + variableTooltip = variableTooltip, + rows = rowSnapshots, + dropdowns = dropdownSnapshots, + dropdownLayouts = dropdownLayouts, + actionSpecs = actionSpecs, + resetRect = resetRect, + clearRect = clearRect, + openValueSelectScrollIndex = openValueSelectScrollIndex, + openUnitSelectScrollIndex = openUnitSelectScrollIndex + ) + } + + private fun projectRectForPointer(rect: Rect, pointerProjectionScrollY: Int): Rect { + if (pointerProjectionScrollY <= 0) return rect + return Rect(rect.x, rect.y - pointerProjectionScrollY, rect.width, rect.height) + } + + private fun buildDropdownSnapshot( + x: Int, + y: Int, + width: Int, + options: List, + property: StyleProperty, + unitSelect: Boolean, + pointerProjectionScrollY: Int, + rowHeightPx: Int, + viewportWidth: Int, + viewportHeight: Int, + mouseX: Int, + mouseY: Int, + currentScrollIndex: Int + ): BuiltDropdownSnapshot { + val maxRows = 8 + val visibleRows = minOf(maxRows, options.size) + val maxFirst = (options.size - visibleRows).coerceAtLeast(0) + val first = currentScrollIndex.coerceIn(0, maxFirst) + val shown = options.subList(first, first + visibleRows) + val optionHeight = rowHeightPx + val popupHeight = optionHeight * shown.size + 6 + val safeViewportW = viewportWidth.coerceAtLeast(1) + val safeViewportH = viewportHeight.coerceAtLeast(1) + val clampedX = x.coerceIn(2, (safeViewportW - width - 2).coerceAtLeast(2)) + val clampedY = y.coerceIn(2, (safeViewportH - popupHeight - 2).coerceAtLeast(2)) + val popupRect = Rect(clampedX, clampedY, width, popupHeight) + + val layout = InspectorStyleEditorDropdownLayout( + rect = popupRect, + property = property, + unitSelect = unitSelect, + totalOptions = options.size, + visibleRows = visibleRows + ) + + var optionY = popupRect.y + 3 + val optionSnapshots = ArrayList(shown.size) + val optionActionSpecs = ArrayList(shown.size) + shown.forEach { option -> + val optionRect = Rect(popupRect.x + 3, optionY, popupRect.width - 6, optionHeight - 2) + val hovered = projectRectForPointer(optionRect, pointerProjectionScrollY).contains(mouseX, mouseY) + optionSnapshots += InspectorDropdownOptionSnapshot( + rect = optionRect, + text = ellipsize(option, 30), + value = option, + hovered = hovered + ) + optionActionSpecs += InspectorStyleEditorActionSpec( + bounds = optionRect, + type = if (unitSelect) { + InspectorStyleEditorActionType.SelectUnitOption + } else { + InspectorStyleEditorActionType.SelectValueOption + }, + property = property, + payload = option + ) + optionY += optionHeight + } + + val footer = if (options.size > visibleRows) { + "${first + 1}-${first + visibleRows}/${options.size}" + } else { + null + } + val snapshot = InspectorDropdownSnapshot( + popupRect = popupRect, + property = property, + unitSelect = unitSelect, + options = optionSnapshots, + footerText = footer + ) + return BuiltDropdownSnapshot( + snapshot = snapshot, + layout = layout, + optionActionSpecs = optionActionSpecs, + nextScrollIndex = first + ) + } + + private fun ellipsize(raw: String, maxChars: Int): String { + if (maxChars <= 1) return raw.take(1) + if (raw.length <= maxChars) return raw + val keep = (maxChars - 3).coerceAtLeast(0) + return raw.take(keep) + "..." + } + + private data class BuiltDropdownSnapshot( + val snapshot: InspectorDropdownSnapshot, + val layout: InspectorStyleEditorDropdownLayout, + val optionActionSpecs: List, + val nextScrollIndex: Int + ) +} From 00897dce1a0e29ed12460bd6297ac4ca07ef49cf Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Mon, 30 Mar 2026 11:45:20 +0300 Subject: [PATCH 06/78] adding some tests for inspector; --- ...nspectorStyleEditorSnapshotBuilderTests.kt | 205 +++++ .../InspectorDropdownCorrectiveTests.kt | 15 +- .../SystemOverlayInspectorNativeEntryTests.kt | 715 +++++++++++++++--- 3 files changed, 836 insertions(+), 99 deletions(-) create mode 100644 core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorStyleEditorSnapshotBuilderTests.kt diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorStyleEditorSnapshotBuilderTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorStyleEditorSnapshotBuilderTests.kt new file mode 100644 index 0000000..baaf143 --- /dev/null +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorStyleEditorSnapshotBuilderTests.kt @@ -0,0 +1,205 @@ +package org.dreamfinity.dsgl.core.inspector + +import org.dreamfinity.dsgl.core.dom.applyParent +import org.dreamfinity.dsgl.core.dom.elements.ContainerNode +import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.style.ComputedStyle +import org.dreamfinity.dsgl.core.style.StyleEngine +import org.dreamfinity.dsgl.core.style.StyleExpression +import org.dreamfinity.dsgl.core.style.StyleProperty +import kotlin.test.* + +class InspectorStyleEditorSnapshotBuilderTests { + + @AfterTest + fun cleanup() { + StyleEngine.clearAllInspectorOverrides() + StyleEngine.clearCache() + } + + @Test + fun `builder creates row snapshots dropdown layouts and color-preview action specs`() { + val (_, selected) = inspectedSelection() + StyleEngine.setInspectorOverrideLiteral(selected, StyleProperty.BACKGROUND_COLOR, "#FF336699").getOrThrow() + val inspection = StyleEngine.inspect(selected) + + val result = builder().build( + context( + selected = selected, + inspection = inspection, + editableProperties = listOf( + StyleProperty.BACKGROUND_COLOR, + StyleProperty.WIDTH, + StyleProperty.DISPLAY + ), + openValueSelectProperty = StyleProperty.DISPLAY, + openUnitSelectProperty = StyleProperty.WIDTH, + openValueSelectScrollIndex = 99, + openUnitSelectScrollIndex = 99 + ) + ) + + assertEquals(3, result.rows.size) + val colorRow = result.rows.firstOrNull { it.property == StyleProperty.BACKGROUND_COLOR } + ?: error("background color row missing") + val widthRow = result.rows.firstOrNull { it.property == StyleProperty.WIDTH } + ?: error("width row missing") + val displayRow = result.rows.firstOrNull { it.property == StyleProperty.DISPLAY } + ?: error("display row missing") + + assertNotNull(colorRow.colorPreviewRect) + assertNotNull(colorRow.colorPreviewColor) + assertNotNull(widthRow.decrementRect) + assertNotNull(widthRow.inputRect) + assertNotNull(widthRow.incrementRect) + assertNotNull(widthRow.unitRect) + assertTrue(displayRow.controlOpen) + assertTrue(widthRow.unitOpen) + + assertTrue(result.dropdowns.any { it.property == StyleProperty.DISPLAY && !it.unitSelect }) + assertTrue(result.dropdowns.any { it.property == StyleProperty.WIDTH && it.unitSelect }) + assertEquals(2, result.dropdownLayouts.size) + assertEquals(0, result.openValueSelectScrollIndex) + assertEquals(0, result.openUnitSelectScrollIndex) + + assertTrue( + result.actionSpecs.any { + it.type == InspectorStyleEditorActionType.OpenColorPicker && it.property == StyleProperty.BACKGROUND_COLOR + } + ) + assertTrue(result.resetRect.width > 0 && result.resetRect.height > 0) + assertTrue(result.clearRect.width > 0 && result.clearRect.height > 0) + } + + @Test + fun `builder projects dropdown hover through pointer projection scroll and preserves option value`() { + val (_, selected) = inspectedSelection() + val inspection = StyleEngine.inspect(selected) + val baseContext = context( + selected = selected, + inspection = inspection, + editableProperties = listOf(StyleProperty.DISPLAY), + openValueSelectProperty = StyleProperty.DISPLAY, + pointerProjectionScrollY = 32, + mouseX = 0, + mouseY = 0 + ) + val baseline = builder().build(baseContext) + val dropdown = baseline.dropdowns.firstOrNull() ?: error("display dropdown missing") + val option = dropdown.options.firstOrNull() ?: error("display dropdown option missing") + val projectedOptionY = option.rect.y - 32 + + val hovered = builder().build( + baseContext.copy( + mouseX = option.rect.x + 2, + mouseY = projectedOptionY + (option.rect.height / 2).coerceAtLeast(1) + ) + ) + val hoveredDropdown = hovered.dropdowns.firstOrNull() ?: error("display dropdown missing after hover pass") + val hoveredOption = hoveredDropdown.options.firstOrNull { it.value == option.value } + ?: error("hovered option missing") + + assertTrue(hoveredOption.hovered) + assertEquals(option.value, hoveredOption.value) + } + + @Test + fun `builder emits variable tooltip when pointer hovers variable-backed row`() { + val (_, selected) = inspectedSelection() + StyleEngine.setInspectorOverride( + selected, + StyleProperty.BACKGROUND_COLOR, + StyleExpression.VariableRef("--missing-color") + ) + val inspection = StyleEngine.inspect(selected) + val panelRect = Rect(20, 20, 360, 260) + val rowY = 64 + 32 + + val result = builder().build( + context( + selected = selected, + inspection = inspection, + panelRect = panelRect, + editableProperties = listOf(StyleProperty.BACKGROUND_COLOR), + mouseX = panelRect.x + 18, + mouseY = rowY + 8 + ) + ) + + val tooltip = result.variableTooltip ?: error("expected variable tooltip") + assertTrue(tooltip.text.contains("--missing-color")) + assertTrue(tooltip.rect.width > 0 && tooltip.rect.height > 0) + } + + private fun builder(): InspectorStyleEditorSnapshotBuilder { + return InspectorStyleEditorSnapshotBuilder( + resolveLiteralFromComputed = ::literalForProperty, + renderExpressionLabel = ::expressionLabel + ) + } + + private fun context( + selected: ContainerNode, + inspection: org.dreamfinity.dsgl.core.style.StyleInspection, + panelRect: Rect = Rect(20, 20, 360, 260), + editableProperties: List, + pointerProjectionScrollY: Int = 0, + mouseX: Int = 180, + mouseY: Int = 120, + openValueSelectProperty: StyleProperty? = null, + openUnitSelectProperty: StyleProperty? = null, + openValueSelectScrollIndex: Int = 0, + openUnitSelectScrollIndex: Int = 0 + ): InspectorStyleEditorSnapshotBuildContext { + return InspectorStyleEditorSnapshotBuildContext( + panelRect = panelRect, + panelBounds = panelRect, + selected = selected, + inspection = inspection, + editableProperties = editableProperties, + startY = 64, + lineHeightPx = 32, + rowHeightPx = 34, + secondaryFontSizePx = 24, + pointerProjectionScrollY = pointerProjectionScrollY, + mouseX = mouseX, + mouseY = mouseY, + viewportWidth = 1280, + viewportHeight = 720, + openValueSelectProperty = openValueSelectProperty, + openUnitSelectProperty = openUnitSelectProperty, + openValueSelectScrollIndex = openValueSelectScrollIndex, + openUnitSelectScrollIndex = openUnitSelectScrollIndex + ) + } + + private fun inspectedSelection(): Pair { + val root = ContainerNode(key = "root").apply { + bounds = Rect(0, 0, 1280, 720) + } + val selected = ContainerNode(key = "target").apply { + bounds = Rect(980, 140, 120, 30) + } + selected.applyParent(root) + return root to selected + } + + private fun literalForProperty(style: ComputedStyle, property: StyleProperty): String { + return when (property) { + StyleProperty.BACKGROUND_COLOR -> "#FF336699" + StyleProperty.WIDTH -> "24px" + StyleProperty.DISPLAY -> "block" + else -> when (property) { + StyleProperty.FONT_ID -> style.fontId ?: "minecraft" + else -> "0" + } + } + } + + private fun expressionLabel(expression: StyleExpression): String { + return when (expression) { + is StyleExpression.Literal -> expression.value + is StyleExpression.VariableRef -> "var(${expression.name})" + } + } +} diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt index 1d79d50..490a3e6 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt @@ -1,10 +1,5 @@ package org.dreamfinity.dsgl.core.overlay.system -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.colorpicker.ColorPickerRuntime import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent @@ -13,7 +8,6 @@ import org.dreamfinity.dsgl.core.dom.elements.TextInputNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.FocusManager -import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.KeyModifiers import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController @@ -22,6 +16,7 @@ import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleProperty +import kotlin.test.* class InspectorDropdownCorrectiveTests { private val ctx = object : UiMeasureContext { @@ -153,7 +148,13 @@ class InspectorDropdownCorrectiveTests { inspector.toggle() host.onInputFrame(1280, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) host.render(ctx, 1280, 720) host.paint(ctx) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt index a263575..6020b65 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt @@ -1,21 +1,9 @@ package org.dreamfinity.dsgl.core.overlay.system -import java.io.File -import java.nio.file.Files -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertSame -import kotlin.test.assertTrue -import org.dreamfinity.dsgl.core.colorpicker.ColorFormatMode -import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupRequest -import org.dreamfinity.dsgl.core.colorpicker.ColorPickerRuntime -import org.dreamfinity.dsgl.core.colorpicker.ColorPickerState -import org.dreamfinity.dsgl.core.colorpicker.RgbaColor +import org.dreamfinity.dsgl.core.colorpicker.* import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent +import org.dreamfinity.dsgl.core.dom.elements.ButtonNode import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext @@ -26,12 +14,14 @@ import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.inspector.InspectorMode import org.dreamfinity.dsgl.core.inspector.InspectorPanelState import org.dreamfinity.dsgl.core.inspector.internal.SystemInspectorOverlayNode +import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.StyleEngine -import org.dreamfinity.dsgl.core.style.StyleExpression import org.dreamfinity.dsgl.core.style.StyleProperty -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope +import java.io.File +import java.nio.file.Files +import kotlin.test.* class SystemOverlayInspectorNativeEntryTests { private val ctx = object : UiMeasureContext { @@ -100,7 +90,13 @@ class SystemOverlayInspectorNativeEntryTests { inspector.toggle() host.onInputFrame(1280, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) host.render(ctx, 1280, 720) assertTrue(host.debugMountedEntryIds().contains(SystemOverlayEntryId.Inspector)) @@ -111,6 +107,54 @@ class SystemOverlayInspectorNativeEntryTests { assertFalse(styleTypes.contains("dsgl-system-inspector-command-bridge")) } + @Test + fun `expanded inspector paints occluder above full highlight geometry`() { + val inspector = InspectorController() + val host = SystemOverlayHost(inspector) + inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + val initialRoot = inspectedRoot() + + inspector.toggle() + host.onInputFrame(1280, 720) + host.syncFrame( + initialRoot, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) + host.render(ctx, 1280, 720) + assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) + assertTrue(host.handleMouseUp(984, 144, MouseButton.LEFT)) + + val movedRoot = inspectedRootMovedUnderPanel() + host.syncFrame( + movedRoot, + inspectedLayoutRevision = 2L, + cursorX = 84, + cursorY = 96, + inspectorPointerCaptured = false + ) + host.render(ctx, 1280, 720) + + val panelRect = inspector.debugPanelRect() ?: error("panel rect missing") + val highlight = inspector.debugSelectedHighlight() ?: error("selected highlight missing") + assertTrue(intersects(highlight.contentRect, panelRect)) + + val inspectorNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector node missing") + val directChildren = inspectorNode.children.toList() + + val occluder = directChildren.firstOrNull { it.key == "dsgl-system-inspector-panel-occluder" } + ?: error("occluder node missing") + val selectedContentFill = directChildren.firstOrNull { it.key == "dsgl-system-inspector-selected-content-fill" } + ?: error("selected content fill node missing") + + assertEquals(panelRect, occluder.bounds) + assertEquals(highlight.contentRect, selectedContentFill.bounds) + assertTrue(directChildren.indexOf(occluder) > directChildren.indexOf(selectedContentFill)) + assertTrue(directChildren.none { (it.key?.toString() ?: "").contains("outside-occlusion") }) + } + @Test fun `inspector runtime interaction path supports selection controls and system-owned color edit`() { val inspector = InspectorController() @@ -120,7 +164,13 @@ class SystemOverlayInspectorNativeEntryTests { inspector.toggle() host.onInputFrame(1280, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) host.render(ctx, 1280, 720) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) @@ -138,7 +188,7 @@ class SystemOverlayInspectorNativeEntryTests { val colorAnchor = colorAction ?: Rect(80, 80, 20, 18) val openedByClick = if (colorAction != null) { host.handleMouseDown(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) && - host.handleMouseUp(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) + host.handleMouseUp(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) } else { false } @@ -146,7 +196,13 @@ class SystemOverlayInspectorNativeEntryTests { assertTrue(inspector.debugOpenColorPickerForSelection(StyleProperty.BACKGROUND_COLOR, colorAnchor)) } - host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = colorAnchor.x + 1, cursorY = colorAnchor.y + 1, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 3L, + cursorX = colorAnchor.x + 1, + cursorY = colorAnchor.y + 1, + inspectorPointerCaptured = false + ) assertTrue(host.isSystemColorPickerOpen()) assertEquals(OverlayOwnerScope.System, host.debugSystemColorPickerPopupOwnerScope()) @@ -164,7 +220,13 @@ class SystemOverlayInspectorNativeEntryTests { inspector.toggle() host.onInputFrame(1280, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) assertEquals("target", inspector.selectedKey) @@ -194,6 +256,7 @@ class SystemOverlayInspectorNativeEntryTests { assertTrue(host.handleMouseDown(chipX + 2, chipY + 2, MouseButton.LEFT)) assertTrue(host.handleMouseUp(chipX + 2, chipY + 2, MouseButton.LEFT)) assertEquals(InspectorPanelState.Expanded, inspector.panelState) + assertFalse(inspector.isPointerCaptured) inspector.deactivate() host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = 40, cursorY = 30, inspectorPointerCaptured = false) @@ -206,6 +269,83 @@ class SystemOverlayInspectorNativeEntryTests { assertSame(initialNode, reopenedNode) } + @Test + fun `minimized inspector hides expanded panel host and keeps chip visible`() { + val inspector = InspectorController() + val host = SystemOverlayHost(inspector) + val root = inspectedRoot() + + inspector.toggle() + host.onInputFrame(1280, 720) + host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 40, cursorY = 30, inspectorPointerCaptured = false) + host.render(ctx, 1280, 720) + + val minimizeRect = inspector.debugMinimizeBounds() ?: error("minimize bounds missing") + assertTrue(host.handleMouseDown(minimizeRect.x + 2, minimizeRect.y + 2, MouseButton.LEFT)) + assertTrue(host.handleMouseUp(minimizeRect.x + 2, minimizeRect.y + 2, MouseButton.LEFT)) + assertEquals(InspectorPanelState.Minimized, inspector.panelState) + + host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 48, cursorY = 36, inspectorPointerCaptured = false) + host.render(ctx, 1280, 720) + + val inspectorNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector node missing") + val panelHostNode = collectNodes(inspectorNode).firstOrNull { node -> + (node.key?.toString() ?: "").startsWith("dsgl-overlay-panel-") + } ?: error("panel host node missing") + val minimizedChipNode = collectNodes(inspectorNode).firstOrNull { it.key == "dsgl-system-inspector-chip" } + ?: error("minimized chip node missing") + + assertEquals(0, panelHostNode.bounds.width) + assertEquals(0, panelHostNode.bounds.height) + assertTrue(minimizedChipNode.bounds.width > 0) + assertTrue(minimizedChipNode.bounds.height > 0) + } + + @Test + fun `minimized drag moves chip and releases pointer capture on mouse up`() { + val inspector = InspectorController() + val host = SystemOverlayHost(inspector) + val root = inspectedRoot() + + inspector.toggle() + host.onInputFrame(1280, 720) + host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 40, cursorY = 30, inspectorPointerCaptured = false) + host.render(ctx, 1280, 720) + + val minimizeRect = inspector.debugMinimizeBounds() ?: error("minimize bounds missing") + assertTrue(host.handleMouseDown(minimizeRect.x + 2, minimizeRect.y + 2, MouseButton.LEFT)) + assertTrue(host.handleMouseUp(minimizeRect.x + 2, minimizeRect.y + 2, MouseButton.LEFT)) + assertEquals(InspectorPanelState.Minimized, inspector.panelState) + + host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 40, cursorY = 30, inspectorPointerCaptured = false) + host.render(ctx, 1280, 720) + + val (startX, startY) = inspector.panelPosition + val downX = startX + 6 + val downY = startY + 6 + val dragX = downX + 40 + val dragY = downY + 20 + + assertTrue(host.handleMouseDown(downX, downY, MouseButton.LEFT)) + assertTrue(host.handleMouseMove(dragX, dragY)) + host.syncFrame( + root, + inspectedLayoutRevision = 3L, + cursorX = dragX, + cursorY = dragY, + inspectorPointerCaptured = inspector.isPointerCaptured + ) + host.render(ctx, 1280, 720) + + assertEquals(InspectorPanelState.Minimized, inspector.panelState) + val (movedX, movedY) = inspector.panelPosition + assertTrue(movedX != startX || movedY != startY) + + assertTrue(host.handleMouseUp(dragX, dragY, MouseButton.LEFT)) + assertFalse(inspector.isPointerCaptured) + assertEquals(InspectorPanelState.Minimized, inspector.panelState) + } + @Test fun `inspector native path preserves scroll and scrollbar drag behavior`() { val inspector = InspectorController() @@ -215,7 +355,13 @@ class SystemOverlayInspectorNativeEntryTests { inspector.toggle() host.onInputFrame(1280, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) host.render(ctx, 1280, 720) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) @@ -228,7 +374,13 @@ class SystemOverlayInspectorNativeEntryTests { val wheelY = contentRect.y + 12 assertTrue(host.handleMouseWheel(wheelX, wheelY, -120)) - host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = wheelX, cursorY = wheelY, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 3L, + cursorX = wheelX, + cursorY = wheelY, + inspectorPointerCaptured = false + ) host.render(ctx, 420, 280) host.paint(ctx) val afterWheel = inspector.panelScrollOffsetY @@ -254,7 +406,13 @@ class SystemOverlayInspectorNativeEntryTests { inspector.toggle() host.onInputFrame(1280, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) host.render(ctx, 1280, 720) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) assertEquals("target", inspector.selectedKey) @@ -291,6 +449,7 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 420, 280) assertEquals(scrollAfterRelease, inspector.panelScrollOffsetY) } + @Test fun `scrollbar drag release outside inspector consumes mouse up and stops capture`() { val inspector = InspectorController() @@ -300,7 +459,13 @@ class SystemOverlayInspectorNativeEntryTests { inspector.toggle() host.onInputFrame(1280, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) host.render(ctx, 1280, 720) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) assertEquals("target", inspector.selectedKey) @@ -345,6 +510,7 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 420, 280) assertEquals(scrollAfterRelease, inspector.panelScrollOffsetY) } + @Test fun `inspector color edit ownership stays system-owned and independent from app runtime popup`() { val appOwner = Any() @@ -367,31 +533,55 @@ class SystemOverlayInspectorNativeEntryTests { inspector.toggle() host.onInputFrame(1280, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) host.render(ctx, 1280, 720) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) - host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 80, cursorY = 52, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 2L, + cursorX = 80, + cursorY = 52, + inspectorPointerCaptured = false + ) host.render(ctx, 1280, 720) val colorAction = inspector.debugColorPickerActionBounds(StyleProperty.BACKGROUND_COLOR) val colorAnchor = colorAction ?: Rect(80, 80, 20, 18) val openedByClick = if (colorAction != null) { host.handleMouseDown(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) && - host.handleMouseUp(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) + host.handleMouseUp(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) } else { false } if (!openedByClick) { assertTrue(inspector.debugOpenColorPickerForSelection(StyleProperty.BACKGROUND_COLOR, colorAnchor)) } - host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = colorAnchor.x + 1, cursorY = colorAnchor.y + 1, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 3L, + cursorX = colorAnchor.x + 1, + cursorY = colorAnchor.y + 1, + inspectorPointerCaptured = false + ) assertTrue(host.isSystemColorPickerOpen()) assertEquals(OverlayOwnerScope.System, host.debugSystemColorPickerPopupOwnerScope()) assertTrue(ColorPickerRuntime.engine.isOpenFor(appOwner)) host.systemInspectorColorPickerPopupHost().close() - host.syncFrame(root, inspectedLayoutRevision = 4L, cursorX = colorAnchor.x + 1, cursorY = colorAnchor.y + 1, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 4L, + cursorX = colorAnchor.x + 1, + cursorY = colorAnchor.y + 1, + inspectorPointerCaptured = false + ) assertFalse(host.isSystemColorPickerOpen()) assertTrue(ColorPickerRuntime.engine.isOpenFor(appOwner)) } finally { @@ -399,6 +589,150 @@ class SystemOverlayInspectorNativeEntryTests { ColorPickerRuntime.engine.close(appOwner) } } + + @Test + fun `inspector-opened system color picker top controls expose hover feedback`() { + val inspector = InspectorController() + val host = SystemOverlayHost(inspector) + inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + val root = inspectedRoot() + + fun sync(revision: Long, cursorX: Int, cursorY: Int) { + host.syncFrame( + root, + inspectedLayoutRevision = revision, + cursorX = cursorX, + cursorY = cursorY, + inspectorPointerCaptured = false + ) + host.render(ctx, 1280, 720) + } + + inspector.toggle() + host.onInputFrame(1280, 720) + sync(revision = 1L, cursorX = 984, cursorY = 144) + assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) + assertEquals("target", inspector.selectedKey) + + sync(revision = 2L, cursorX = 80, cursorY = 52) + val colorAction = inspector.debugColorPickerActionBounds(StyleProperty.BACKGROUND_COLOR) + val colorAnchor = colorAction ?: Rect(80, 80, 20, 18) + val openedByClick = if (colorAction != null) { + host.handleMouseDown(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) && + host.handleMouseUp(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) + } else { + false + } + if (!openedByClick) { + assertTrue(inspector.debugOpenColorPickerForSelection(StyleProperty.BACKGROUND_COLOR, colorAnchor)) + } + sync(revision = 3L, cursorX = colorAnchor.x + 1, cursorY = colorAnchor.y + 1) + assertTrue(host.isSystemColorPickerOpen()) + assertEquals(OverlayOwnerScope.System, host.debugSystemColorPickerPopupOwnerScope()) + + val layout = host.debugSystemColorPickerBodyLayout() ?: error("color picker body layout missing") + val style = ColorPickerStyle() + val hoverTargets = listOf( + "dsgl-system-color-picker-mode-select" to layout.modeSelectRect, + "dsgl-system-color-picker-order-argb" to (layout.argbOrderRect ?: error("argb order rect missing")), + "dsgl-system-color-picker-button-copy" to layout.copyRect, + "dsgl-system-color-picker-button-paste" to layout.pasteRect, + "dsgl-system-color-picker-button-pipette" to layout.pipetteRect + ) + + var revision = 4L + hoverTargets.forEach { (key, rect) -> + val hoverX = rect.x + rect.width / 2 + val hoverY = rect.y + rect.height / 2 + assertTrue(host.handleMouseMove(hoverX, hoverY), "expected hover move to be consumed for $key") + sync(revision = revision++, cursorX = hoverX, cursorY = hoverY) + + val pickerNode = host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) + ?: error("color picker entry missing") + val buttonNode = collectNodes(pickerNode) + .firstOrNull { it.key?.toString() == key } as? ButtonNode + ?: error("button node missing for $key") + assertEquals(style.buttonHoverColor, buttonNode.backgroundColor, "expected hover color for $key") + } + } + + @Test + fun `inspector-opened system color picker mode dropdown options hover and click reliably`() { + val inspector = InspectorController() + val host = SystemOverlayHost(inspector) + inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + val root = inspectedRoot() + + fun sync(revision: Long, cursorX: Int, cursorY: Int) { + host.syncFrame( + root, + inspectedLayoutRevision = revision, + cursorX = cursorX, + cursorY = cursorY, + inspectorPointerCaptured = false + ) + host.render(ctx, 1280, 720) + } + + inspector.toggle() + host.onInputFrame(1280, 720) + sync(revision = 1L, cursorX = 984, cursorY = 144) + assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) + assertEquals("target", inspector.selectedKey) + val modeBeforeDropdown = inspector.mode + + sync(revision = 2L, cursorX = 80, cursorY = 52) + val colorAction = inspector.debugColorPickerActionBounds(StyleProperty.BACKGROUND_COLOR) + val colorAnchor = colorAction ?: Rect(80, 80, 20, 18) + val openedByClick = if (colorAction != null) { + host.handleMouseDown(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) && + host.handleMouseUp(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) + } else { + false + } + if (!openedByClick) { + assertTrue(inspector.debugOpenColorPickerForSelection(StyleProperty.BACKGROUND_COLOR, colorAnchor)) + } + sync(revision = 3L, cursorX = colorAnchor.x + 1, cursorY = colorAnchor.y + 1) + assertTrue(host.isSystemColorPickerOpen()) + assertEquals(OverlayOwnerScope.System, host.debugSystemColorPickerPopupOwnerScope()) + + val initialLayout = host.debugSystemColorPickerBodyLayout() ?: error("color picker body layout missing") + assertTrue(host.handleMouseDown(initialLayout.modeSelectRect.x + 2, initialLayout.modeSelectRect.y + 2, MouseButton.LEFT)) + sync( + revision = 4L, + cursorX = initialLayout.modeSelectRect.x + 2, + cursorY = initialLayout.modeSelectRect.y + 2 + ) + assertTrue(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerTransient)) + + val expandedLayout = host.debugSystemColorPickerBodyLayout() ?: error("expanded color picker layout missing") + val hslOption = expandedLayout.modeOptions.firstOrNull { it.mode == ColorFormatMode.HSL } + ?: error("HSL mode option missing") + val optionHoverX = hslOption.rect.x + hslOption.rect.width / 2 + val optionHoverY = hslOption.rect.y + hslOption.rect.height / 2 + assertTrue(host.handleMouseMove(optionHoverX, optionHoverY)) + sync(revision = 5L, cursorX = optionHoverX, cursorY = optionHoverY) + + val style = ColorPickerStyle() + val transientNode = host.debugEntryNode(SystemOverlayEntryId.ColorPickerTransient) + ?: error("transient entry missing") + val optionNode = collectNodes(transientNode) + .firstOrNull { it.key?.toString() == "dsgl-system-color-picker-mode-option-hsl" } as? ButtonNode + ?: error("HSL option node missing") + assertEquals(style.buttonHoverColor, optionNode.backgroundColor) + + assertTrue(host.handleMouseDown(optionHoverX, optionHoverY, MouseButton.LEFT)) + assertTrue(host.handleMouseUp(optionHoverX, optionHoverY, MouseButton.LEFT)) + sync(revision = 6L, cursorX = optionHoverX, cursorY = optionHoverY) + + assertEquals(ColorFormatMode.HSL, host.debugSystemColorPickerState()?.mode) + assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerTransient)) + assertTrue(host.isSystemColorPickerOpen()) + assertEquals("target", inspector.selectedKey) + assertEquals(modeBeforeDropdown, inspector.mode) + } + @Test fun `inspector native body content remains clipped in narrow viewport`() { val inspector = InspectorController() @@ -408,7 +742,13 @@ class SystemOverlayInspectorNativeEntryTests { inspector.toggle() host.onInputFrame(1280, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) host.render(ctx, 1280, 720) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) @@ -428,21 +768,27 @@ class SystemOverlayInspectorNativeEntryTests { val initialCommands = host.paint(ctx) assertTrue(initialCommands.any { command -> command is RenderCommand.PushClip && - command.x == bodyViewport.x && - command.y == bodyViewport.y && - command.width == bodyViewport.width && - command.height == bodyViewport.height + command.x == bodyViewport.x && + command.y == bodyViewport.y && + command.width == bodyViewport.width && + command.height == bodyViewport.height }) assertTrue(host.handleMouseWheel(bodyRect.x + 4, bodyRect.y + 12, -120)) - host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = bodyRect.x + 4, cursorY = bodyRect.y + 12, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 3L, + cursorX = bodyRect.x + 4, + cursorY = bodyRect.y + 12, + inspectorPointerCaptured = false + ) host.render(ctx, 320, 220) val bodyLines = collectNodes(inspectorNode).filter { node -> if (node.display == Display.None) return@filter false val key = node.key?.toString() ?: return@filter false key.startsWith("dsgl-system-inspector-info-line-") || - key.startsWith("dsgl-system-inspector-style-line-") + key.startsWith("dsgl-system-inspector-style-line-") } assertTrue(bodyLines.isNotEmpty()) @@ -459,12 +805,13 @@ class SystemOverlayInspectorNativeEntryTests { val scrolledCommands = host.paint(ctx) assertTrue(scrolledCommands.any { command -> command is RenderCommand.PushClip && - command.x == bodyViewport.x && - command.y == bodyViewport.y && - command.width == bodyViewport.width && - command.height == bodyViewport.height + command.x == bodyViewport.x && + command.y == bodyViewport.y && + command.width == bodyViewport.width && + command.height == bodyViewport.height }) } + @Test fun `inspector clipped body blocks hidden row input and accepts visible portion`() { val inspector = InspectorController() @@ -474,7 +821,13 @@ class SystemOverlayInspectorNativeEntryTests { inspector.toggle() host.onInputFrame(1280, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) host.render(ctx, 1280, 720) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) assertEquals("target", inspector.selectedKey) @@ -500,18 +853,32 @@ class SystemOverlayInspectorNativeEntryTests { isInteractiveInspectorControlKey(key) } latestInteractiveNodes = interactiveNodes - edgeNode = interactiveNodes.firstOrNull { node -> intersects(node.bounds, bodyRect) && !containsFully(bodyRect, node.bounds) } + edgeNode = interactiveNodes.firstOrNull { node -> + intersects(node.bounds, bodyRect) && !containsFully( + bodyRect, + node.bounds + ) + } hiddenNode = interactiveNodes.firstOrNull { node -> !intersects(node.bounds, bodyRect) } visibleNode = interactiveNodes.firstOrNull { node -> containsFully(bodyRect, node.bounds) } if (edgeNode != null && visibleNode != null) return@repeat assertTrue(host.handleMouseWheel(wheelX, wheelY, -120)) - host.syncFrame(root, inspectedLayoutRevision = revision, cursorX = wheelX, cursorY = wheelY, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = revision, + cursorX = wheelX, + cursorY = wheelY, + inspectorPointerCaptured = false + ) host.render(ctx, 320, 213) revision += 1L } - val hiddenTarget = edgeNode ?: hiddenNode ?: latestInteractiveNodes.firstOrNull { node -> !intersects(node.bounds, bodyRect) } ?: error("failed to find hidden interactive inspector control") - val visibleTarget = visibleNode ?: latestInteractiveNodes.firstOrNull { node -> intersects(node.bounds, bodyRect) } ?: edgeNode + val hiddenTarget = + edgeNode ?: hiddenNode ?: latestInteractiveNodes.firstOrNull { node -> !intersects(node.bounds, bodyRect) } + ?: error("failed to find hidden interactive inspector control") + val visibleTarget = + visibleNode ?: latestInteractiveNodes.firstOrNull { node -> intersects(node.bounds, bodyRect) } ?: edgeNode val hiddenX = if (edgeNode != null) { maxOf(hiddenTarget.bounds.x, bodyRect.x) + 2 @@ -545,6 +912,7 @@ class SystemOverlayInspectorNativeEntryTests { assertTrue(host.handleMouseDown(visibleX, visibleY, MouseButton.LEFT)) assertTrue(host.handleMouseUp(visibleX, visibleY, MouseButton.LEFT)) } + @Test fun `inspector body consumes generic scroll viewport and content state`() { val inspector = InspectorController() @@ -554,7 +922,13 @@ class SystemOverlayInspectorNativeEntryTests { inspector.toggle() host.onInputFrame(1280, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) host.render(ctx, 1280, 720) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) @@ -593,7 +967,13 @@ class SystemOverlayInspectorNativeEntryTests { inspector.toggle() host.onInputFrame(1280, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) host.render(ctx, 1280, 720) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) assertEquals("target", inspector.selectedKey) @@ -608,9 +988,9 @@ class SystemOverlayInspectorNativeEntryTests { val interactiveNode = allNodes.firstOrNull { node -> val key = node.key?.toString() ?: return@firstOrNull false val interactiveControl = key.startsWith("dsgl-system-inspector-editor-input-") || - key.startsWith("dsgl-system-inspector-editor-numeric-input-") || - key.startsWith("dsgl-system-inspector-editor-select-") || - key.startsWith("dsgl-system-inspector-editor-color-preview-") + key.startsWith("dsgl-system-inspector-editor-numeric-input-") || + key.startsWith("dsgl-system-inspector-editor-select-") || + key.startsWith("dsgl-system-inspector-editor-color-preview-") if (!interactiveControl) return@firstOrNull false val probeX = node.bounds.x + 2 val probeY = node.bounds.y + (node.bounds.height / 2).coerceAtLeast(1) @@ -627,11 +1007,18 @@ class SystemOverlayInspectorNativeEntryTests { val before = inspector.panelScrollOffsetY assertTrue(host.handleMouseWheel(wheelX, wheelY, -120)) - host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = wheelX, cursorY = wheelY, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 3L, + cursorX = wheelX, + cursorY = wheelY, + inspectorPointerCaptured = false + ) host.render(ctx, 420, 280) host.paint(ctx) assertTrue(inspector.panelScrollOffsetY > before) } + @Test fun `inspector shift wheel does not consume vertical wheel path`() { val inspector = InspectorController() @@ -641,7 +1028,13 @@ class SystemOverlayInspectorNativeEntryTests { inspector.toggle() host.onInputFrame(1280, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) host.render(ctx, 1280, 720) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) @@ -656,7 +1049,13 @@ class SystemOverlayInspectorNativeEntryTests { KeyModifiers.sync(shift = true, control = false, meta = false) host.handleMouseWheel(wheelX, wheelY, -120) - host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = wheelX, cursorY = wheelY, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 3L, + cursorX = wheelX, + cursorY = wheelY, + inspectorPointerCaptured = false + ) host.render(ctx, 420, 280) host.paint(ctx) assertEquals(before, inspector.panelScrollOffsetY) @@ -672,7 +1071,13 @@ class SystemOverlayInspectorNativeEntryTests { inspector.toggle() host.onInputFrame(1280, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) host.render(ctx, 1280, 720) host.paint(ctx) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) @@ -688,12 +1093,24 @@ class SystemOverlayInspectorNativeEntryTests { repeat(4) { step -> assertTrue(host.handleMouseWheel(wheelX, wheelY, -120)) - host.syncFrame(root, inspectedLayoutRevision = 3L + step, cursorX = wheelX, cursorY = wheelY, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 3L + step, + cursorX = wheelX, + cursorY = wheelY, + inspectorPointerCaptured = false + ) host.render(ctx, 420, 280) host.paint(ctx) } repeat(16) { settle -> - host.syncFrame(root, inspectedLayoutRevision = 20L + settle, cursorX = wheelX, cursorY = wheelY, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 20L + settle, + cursorX = wheelX, + cursorY = wheelY, + inspectorPointerCaptured = false + ) host.render(ctx, 420, 280) host.paint(ctx) } @@ -704,7 +1121,13 @@ class SystemOverlayInspectorNativeEntryTests { repeat(8) { step -> val consumed = host.handleMouseWheel(wheelX, wheelY, 120) consumedUpWheel = consumedUpWheel || consumed - host.syncFrame(root, inspectedLayoutRevision = 40L + step, cursorX = wheelX, cursorY = wheelY, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 40L + step, + cursorX = wheelX, + cursorY = wheelY, + inspectorPointerCaptured = false + ) host.render(ctx, 420, 280) host.paint(ctx) } @@ -712,12 +1135,21 @@ class SystemOverlayInspectorNativeEntryTests { var scrolledUp = inspector.panelScrollOffsetY repeat(24) { settle -> if (scrolledUp < scrolledDown) return@repeat - host.syncFrame(root, inspectedLayoutRevision = 60L + settle, cursorX = wheelX, cursorY = wheelY, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 60L + settle, + cursorX = wheelX, + cursorY = wheelY, + inspectorPointerCaptured = false + ) host.render(ctx, 420, 280) host.paint(ctx) scrolledUp = inspector.panelScrollOffsetY } - assertTrue(scrolledUp < scrolledDown, "expected upward wheel to reduce scroll: down=$scrolledDown up=$scrolledUp") + assertTrue( + scrolledUp < scrolledDown, + "expected upward wheel to reduce scroll: down=$scrolledDown up=$scrolledUp" + ) } @Test @@ -729,7 +1161,13 @@ class SystemOverlayInspectorNativeEntryTests { inspector.toggle() host.onInputFrame(1280, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) host.render(ctx, 1280, 720) host.paint(ctx) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) @@ -749,14 +1187,26 @@ class SystemOverlayInspectorNativeEntryTests { val beforeDrag = inspector.panelScrollOffsetY assertTrue(host.handleMouseMove(dragX, dragStartY + 18)) - host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = dragX, cursorY = dragStartY + 18, inspectorPointerCaptured = inspector.isPointerCaptured) + host.syncFrame( + root, + inspectedLayoutRevision = 3L, + cursorX = dragX, + cursorY = dragStartY + 18, + inspectorPointerCaptured = inspector.isPointerCaptured + ) host.render(ctx, 420, 280) host.paint(ctx) val afterFirstMove = inspector.panelScrollOffsetY assertTrue(afterFirstMove > beforeDrag) assertTrue(host.handleMouseMove(dragX, dragStartY + 42)) - host.syncFrame(root, inspectedLayoutRevision = 4L, cursorX = dragX, cursorY = dragStartY + 42, inspectorPointerCaptured = inspector.isPointerCaptured) + host.syncFrame( + root, + inspectedLayoutRevision = 4L, + cursorX = dragX, + cursorY = dragStartY + 42, + inspectorPointerCaptured = inspector.isPointerCaptured + ) host.render(ctx, 420, 280) host.paint(ctx) val afterSecondMove = inspector.panelScrollOffsetY @@ -764,6 +1214,7 @@ class SystemOverlayInspectorNativeEntryTests { assertTrue(host.handleMouseUp(dragX, dragStartY + 42, MouseButton.LEFT)) } + @Test fun `inspector style boundary stays isolated from application stylesheet`() { val stylesDir = createTempStylesDir( @@ -782,7 +1233,13 @@ class SystemOverlayInspectorNativeEntryTests { inspector.toggle() host.onInputFrame(1280, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) host.render(ctx, 1280, 720) val commands = host.paint(ctx) val headerTexts = commands @@ -800,7 +1257,8 @@ class SystemOverlayInspectorNativeEntryTests { ContainerNode(key = "target").apply { bounds = Rect(980, 140, 120, 30) }.applyParent(root) - StyleEngine.setInspectorOverrideLiteral(root.children.first(), StyleProperty.BACKGROUND_COLOR, "#FF112233").getOrThrow() + StyleEngine.setInspectorOverrideLiteral(root.children.first(), StyleProperty.BACKGROUND_COLOR, "#FF112233") + .getOrThrow() return root } @@ -819,6 +1277,16 @@ class SystemOverlayInspectorNativeEntryTests { return root } + private fun inspectedRootMovedUnderPanel(): ContainerNode { + val root = ContainerNode(key = "root") + root.bounds = Rect(0, 0, 1280, 720) + val selected = ContainerNode(key = "target").apply { + bounds = Rect(72, 84, 180, 80) + }.applyParent(root) + StyleEngine.setInspectorOverrideLiteral(selected, StyleProperty.BACKGROUND_COLOR, "#FF112233").getOrThrow() + return root + } + private fun popupState(): ColorPickerState { return ColorPickerState( color = RgbaColor(0.3f, 0.5f, 0.7f, 1f), @@ -831,30 +1299,31 @@ class SystemOverlayInspectorNativeEntryTests { private fun intersects(a: Rect, b: Rect): Boolean { return a.x < b.x + b.width && - a.x + a.width > b.x && - a.y < b.y + b.height && - a.y + a.height > b.y + a.x + a.width > b.x && + a.y < b.y + b.height && + a.y + a.height > b.y } private fun containsFully(outer: Rect, inner: Rect): Boolean { return inner.x >= outer.x && - inner.y >= outer.y && - inner.x + inner.width <= outer.x + outer.width && - inner.y + inner.height <= outer.y + outer.height + inner.y >= outer.y && + inner.x + inner.width <= outer.x + outer.width && + inner.y + inner.height <= outer.y + outer.height } private fun isInteractiveInspectorControlKey(key: String): Boolean { return key == "dsgl-system-inspector-parent-row" || - key.startsWith("dsgl-system-inspector-child-row-") || - key.startsWith("dsgl-system-inspector-editor-reset-") || - key.startsWith("dsgl-system-inspector-editor-select-") || - key.startsWith("dsgl-system-inspector-editor-dec-") || - key.startsWith("dsgl-system-inspector-editor-inc-") || - key.startsWith("dsgl-system-inspector-editor-unit-") || - key.startsWith("dsgl-system-inspector-editor-color-preview-") || - key == "dsgl-system-inspector-reset-node" || - key == "dsgl-system-inspector-clear-all" + key.startsWith("dsgl-system-inspector-child-row-") || + key.startsWith("dsgl-system-inspector-editor-reset-") || + key.startsWith("dsgl-system-inspector-editor-select-") || + key.startsWith("dsgl-system-inspector-editor-dec-") || + key.startsWith("dsgl-system-inspector-editor-inc-") || + key.startsWith("dsgl-system-inspector-editor-unit-") || + key.startsWith("dsgl-system-inspector-editor-color-preview-") || + key == "dsgl-system-inspector-reset-node" || + key == "dsgl-system-inspector-clear-all" } + private fun collectNodes(root: DOMNode): List { val out = ArrayList() fun walk(node: DOMNode) { @@ -864,6 +1333,7 @@ class SystemOverlayInspectorNativeEntryTests { walk(root) return out } + private fun collectStyleTypes(root: DOMNode): Set { val out = LinkedHashSet() fun walk(node: DOMNode) { @@ -889,7 +1359,13 @@ class SystemOverlayInspectorNativeEntryTests { inspector.toggle() host.onInputFrame(1280, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) host.render(ctx, 1280, 720) host.paint(ctx) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) @@ -905,7 +1381,13 @@ class SystemOverlayInspectorNativeEntryTests { val before = inspector.panelScrollOffsetY assertTrue(host.handleMouseWheel(wheelX, wheelY, -120)) - host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = wheelX, cursorY = wheelY, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 3L, + cursorX = wheelX, + cursorY = wheelY, + inspectorPointerCaptured = false + ) host.render(ctx, 420, 280) host.paint(ctx) @@ -921,7 +1403,13 @@ class SystemOverlayInspectorNativeEntryTests { inspector.toggle() host.onInputFrame(1280, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) host.render(ctx, 1280, 720) host.paint(ctx) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) @@ -943,13 +1431,25 @@ class SystemOverlayInspectorNativeEntryTests { repeat(6) { step -> val nextY = startY + (step + 1) * 9 assertTrue(host.handleMouseMove(dragX, nextY)) - host.syncFrame(root, inspectedLayoutRevision = 3L + step, cursorX = dragX, cursorY = nextY, inspectorPointerCaptured = inspector.isPointerCaptured) + host.syncFrame( + root, + inspectedLayoutRevision = 3L + step, + cursorX = dragX, + cursorY = nextY, + inspectorPointerCaptured = inspector.isPointerCaptured + ) host.render(ctx, 420, 280) host.paint(ctx) val currentScroll = inspector.panelScrollOffsetY val currentThumbY = inspector.debugScrollbarThumbRect().y - assertTrue(currentScroll >= previousScroll, "scroll regressed: prev=$previousScroll current=$currentScroll step=$step") - assertTrue(currentThumbY >= previousThumbY, "thumb regressed: prev=$previousThumbY current=$currentThumbY step=$step") + assertTrue( + currentScroll >= previousScroll, + "scroll regressed: prev=$previousScroll current=$currentScroll step=$step" + ) + assertTrue( + currentThumbY >= previousThumbY, + "thumb regressed: prev=$previousThumbY current=$currentThumbY step=$step" + ) previousScroll = currentScroll previousThumbY = currentThumbY } @@ -959,13 +1459,20 @@ class SystemOverlayInspectorNativeEntryTests { val settledThumbY = inspector.debugScrollbarThumbRect().y repeat(6) { idx -> - host.syncFrame(root, inspectedLayoutRevision = 20L + idx, cursorX = dragX, cursorY = startY, inspectorPointerCaptured = inspector.isPointerCaptured) + host.syncFrame( + root, + inspectedLayoutRevision = 20L + idx, + cursorX = dragX, + cursorY = startY, + inspectorPointerCaptured = inspector.isPointerCaptured + ) host.render(ctx, 420, 280) host.paint(ctx) assertEquals(settledScroll, inspector.panelScrollOffsetY) assertEquals(settledThumbY, inspector.debugScrollbarThumbRect().y) } } + @Test fun `inspector consumer fast thumb drag to boundary stays stable`() { val inspector = InspectorController() @@ -975,7 +1482,13 @@ class SystemOverlayInspectorNativeEntryTests { inspector.toggle() host.onInputFrame(1280, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) host.render(ctx, 1280, 720) host.paint(ctx) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) @@ -996,13 +1509,25 @@ class SystemOverlayInspectorNativeEntryTests { repeat(7) { step -> val nextY = startY + (step + 1) * 120 assertTrue(host.handleMouseMove(dragX, nextY)) - host.syncFrame(root, inspectedLayoutRevision = 20L + step, cursorX = dragX, cursorY = nextY, inspectorPointerCaptured = inspector.isPointerCaptured) + host.syncFrame( + root, + inspectedLayoutRevision = 20L + step, + cursorX = dragX, + cursorY = nextY, + inspectorPointerCaptured = inspector.isPointerCaptured + ) host.render(ctx, 420, 280) host.paint(ctx) val currentScroll = inspector.panelScrollOffsetY val currentThumbY = inspector.debugScrollbarThumbRect().y - assertTrue(currentScroll >= previousScroll, "scroll regressed: prev=$previousScroll current=$currentScroll step=$step") - assertTrue(currentThumbY >= previousThumbY, "thumb regressed: prev=$previousThumbY current=$currentThumbY step=$step") + assertTrue( + currentScroll >= previousScroll, + "scroll regressed: prev=$previousScroll current=$currentScroll step=$step" + ) + assertTrue( + currentThumbY >= previousThumbY, + "thumb regressed: prev=$previousThumbY current=$currentThumbY step=$step" + ) previousScroll = currentScroll previousThumbY = currentThumbY } @@ -1012,7 +1537,13 @@ class SystemOverlayInspectorNativeEntryTests { repeat(8) { idx -> val boundaryY = startY + 2000 assertTrue(host.handleMouseMove(dragX, boundaryY)) - host.syncFrame(root, inspectedLayoutRevision = 40L + idx, cursorX = dragX, cursorY = boundaryY, inspectorPointerCaptured = inspector.isPointerCaptured) + host.syncFrame( + root, + inspectedLayoutRevision = 40L + idx, + cursorX = dragX, + cursorY = boundaryY, + inspectorPointerCaptured = inspector.isPointerCaptured + ) host.render(ctx, 420, 280) host.paint(ctx) assertEquals(settledScroll, inspector.panelScrollOffsetY) From 4b85e28c4fea283b768440a2b143930897924df0 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Mon, 30 Mar 2026 13:43:49 +0300 Subject: [PATCH 07/78] renaming debug methods to overlay methods; --- .../core/inspector/InspectorController.kt | 78 ++++++++++++++----- .../internal/SystemInspectorOverlayNode.kt | 34 ++++---- .../core/overlay/system/SystemOverlayHost.kt | 4 +- 3 files changed, 79 insertions(+), 37 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt index e362dcc..5e47963 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt @@ -1103,23 +1103,23 @@ class InspectorController( scrollbarTrackRect = track scrollbarThumbRect = Rect(track.x, thumbY, track.width, thumbHeight) } - internal fun debugPickToggleBounds(): Rect? { + internal fun overlayPickToggleBounds(): Rect? { return panelActions.lastOrNull { it.kind == ActionKind.TogglePick }?.bounds } - internal fun debugMinimizeBounds(): Rect? { + internal fun overlayMinimizeBounds(): Rect? { return panelActions.lastOrNull { it.kind == ActionKind.Minimize }?.bounds } - internal fun debugContentRect(): Rect = contentBounds + internal fun overlayContentRect(): Rect = contentBounds - internal fun debugScrollbarThumbRect(): Rect = if (nativeDomBodyScrollStateActive) { + internal fun overlayScrollbarThumbRect(): Rect = if (nativeDomBodyScrollStateActive) { nativeDomScrollbarThumbRectOverride ?: Rect(0, 0, 0, 0) } else { scrollbarThumbRect } - internal fun debugScrollbarTrackRect(): Rect = if (nativeDomBodyScrollStateActive) { + internal fun overlayScrollbarTrackRect(): Rect = if (nativeDomBodyScrollStateActive) { nativeDomScrollbarTrackRectOverride ?: Rect(0, 0, 0, 0) } else { scrollbarTrackRect @@ -1162,21 +1162,47 @@ class InspectorController( minimizedBounds = Rect(minimizedPosX, minimizedPosY, minimizedWidth(), minimizedHeight()) } - internal fun debugSelectedHighlight(): InspectorHighlightSnapshot? = nativeSelectedHighlight + internal fun overlaySelectedHighlight(): InspectorHighlightSnapshot? = nativeSelectedHighlight - internal fun debugHoveredHighlight(): InspectorHighlightSnapshot? = nativeHoveredHighlight + internal fun overlayHoveredHighlight(): InspectorHighlightSnapshot? = nativeHoveredHighlight - internal fun debugCursorTooltip(): InspectorTooltipSnapshot? = nativeCursorTooltip + internal fun overlayCursorTooltip(): InspectorTooltipSnapshot? = nativeCursorTooltip - internal fun debugVariableTooltip(): InspectorTooltipSnapshot? = nativeVariableTooltip + internal fun overlayVariableTooltip(): InspectorTooltipSnapshot? = nativeVariableTooltip - internal fun debugStyleEditorRows(): List = nativeStyleEditorRows + internal fun overlayStyleEditorRows(): List = nativeStyleEditorRows - internal fun debugStyleEditorResetRect(): Rect = nativeStyleEditorResetRect + internal fun overlayStyleEditorResetRect(): Rect = nativeStyleEditorResetRect - internal fun debugStyleEditorClearRect(): Rect = nativeStyleEditorClearRect + internal fun overlayStyleEditorClearRect(): Rect = nativeStyleEditorClearRect - internal fun debugStyleEditorDropdowns(): List = nativeDropdowns + internal fun overlayStyleEditorDropdowns(): List = nativeDropdowns + + internal fun debugPickToggleBounds(): Rect? = overlayPickToggleBounds() + + internal fun debugMinimizeBounds(): Rect? = overlayMinimizeBounds() + + internal fun debugContentRect(): Rect = overlayContentRect() + + internal fun debugScrollbarThumbRect(): Rect = overlayScrollbarThumbRect() + + internal fun debugScrollbarTrackRect(): Rect = overlayScrollbarTrackRect() + + internal fun debugSelectedHighlight(): InspectorHighlightSnapshot? = overlaySelectedHighlight() + + internal fun debugHoveredHighlight(): InspectorHighlightSnapshot? = overlayHoveredHighlight() + + internal fun debugCursorTooltip(): InspectorTooltipSnapshot? = overlayCursorTooltip() + + internal fun debugVariableTooltip(): InspectorTooltipSnapshot? = overlayVariableTooltip() + + internal fun debugStyleEditorRows(): List = overlayStyleEditorRows() + + internal fun debugStyleEditorResetRect(): Rect = overlayStyleEditorResetRect() + + internal fun debugStyleEditorClearRect(): Rect = overlayStyleEditorClearRect() + + internal fun debugStyleEditorDropdowns(): List = overlayStyleEditorDropdowns() internal fun onNativeDomDropdownSnapshots(dropdowns: List) { nativeDropdowns.clear() @@ -1414,7 +1440,7 @@ class InspectorController( return OpenStyleDropdown(unitSelect = false, optionCount = optionCount) } - internal fun debugApplyLiteralOverride(property: StyleProperty, literal: String): Boolean { + internal fun overlayApplyLiteralOverride(property: StyleProperty, literal: String): Boolean { val selected = selectedNode ?: return false val normalized = literal.trim() return runCatching { @@ -1428,7 +1454,7 @@ class InspectorController( } } - internal fun debugApplyNumericOverride(property: StyleProperty, numericLiteral: String, unitToken: String?): Boolean { + internal fun overlayApplyNumericOverride(property: StyleProperty, numericLiteral: String, unitToken: String?): Boolean { val selected = selectedNode ?: return false val numberText = numericLiteral.trim() if (numberText.isEmpty() || numberText == "-" || numberText == "." || numberText == "-.") { @@ -1446,6 +1472,14 @@ class InspectorController( } } + internal fun debugApplyLiteralOverride(property: StyleProperty, literal: String): Boolean { + return overlayApplyLiteralOverride(property, literal) + } + + internal fun debugApplyNumericOverride(property: StyleProperty, numericLiteral: String, unitToken: String?): Boolean { + return overlayApplyNumericOverride(property, numericLiteral, unitToken) + } + private fun resetNativePresentation() { nativeSelectedHighlight = null nativeHoveredHighlight = null @@ -1651,7 +1685,7 @@ class InspectorController( } } - internal fun debugColorPickerActionBounds(property: StyleProperty): Rect? { + internal fun overlayColorPickerActionBounds(property: StyleProperty): Rect? { return panelActions.lastOrNull { it.kind == ActionKind.EditProperty && it.property == property && @@ -1665,16 +1699,24 @@ class InspectorController( return true } - internal fun debugPanelRect(): Rect? { + internal fun overlayPanelRect(): Rect? { if (!active) return null return currentInspectorRect() } - internal fun debugExpandedPanelRect(): Rect? { + internal fun overlayExpandedPanelRect(): Rect? { if (!active || panelState != InspectorPanelState.Expanded) return null return expandedRect } + internal fun debugColorPickerActionBounds(property: StyleProperty): Rect? { + return overlayColorPickerActionBounds(property) + } + + internal fun debugPanelRect(): Rect? = overlayPanelRect() + + internal fun debugExpandedPanelRect(): Rect? = overlayExpandedPanelRect() + private fun performPanelAction(action: PanelAction) { when (action.kind) { ActionKind.Minimize -> { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt index 9f57a8c..d9f2f11 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt @@ -126,7 +126,7 @@ internal class SystemInspectorOverlayNode( private fun handleOverlayPanelMouseDown(event: MouseDownEvent): Boolean { if (event.mouseButton != MouseButton.LEFT) return false - val bodyRect = controller.debugContentRect() + val bodyRect = controller.overlayContentRect() val pointerInsideBody = bodyRect.width > 0 && bodyRect.height > 0 && bodyRect.contains(event.mouseX, event.mouseY) @@ -188,7 +188,7 @@ internal class SystemInspectorOverlayNode( fun syncInputBounds(viewportWidth: Int, viewportHeight: Int) { val viewportRect = Rect(0, 0, viewportWidth.coerceAtLeast(0), viewportHeight.coerceAtLeast(0)) - bounds = resolveInputBounds(viewportRect, controller.debugPanelRect()) + bounds = resolveInputBounds(viewportRect, controller.overlayPanelRect()) } override fun measure(ctx: UiMeasureContext): Size { @@ -197,7 +197,7 @@ internal class SystemInspectorOverlayNode( override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { val viewportRect = Rect(x, y, width, height) - bounds = resolveInputBounds(viewportRect, controller.debugPanelRect()) + bounds = resolveInputBounds(viewportRect, controller.overlayPanelRect()) inspectedRoot?.let { root -> controller.onLayoutCommitted(root, inspectedLayoutRevision) } @@ -247,7 +247,7 @@ internal class SystemInspectorOverlayNode( } private fun resolveRenderedDropdownInputBounds(): Rect? { - val dropdowns = controller.debugStyleEditorDropdowns() + val dropdowns = controller.overlayStyleEditorDropdowns() if (dropdowns.isNotEmpty()) { return dropdowns .map { it.popupRect } @@ -360,9 +360,9 @@ internal class SystemInspectorOverlayNode( renderPanelOccluder(scope, ctx, panelRect) panelNode.render(ctx, viewportRect.x, viewportRect.y, viewportRect.width, viewportRect.height) - val pickRect = controller.debugPickToggleBounds() + val pickRect = controller.overlayPickToggleBounds() ?: Rect(panelRect.x + panelRect.width - 264, panelRect.y + 8, 160, 36) - val minimizeRect = controller.debugMinimizeBounds() + val minimizeRect = controller.overlayMinimizeBounds() ?: Rect(panelRect.x + panelRect.width - 96, panelRect.y + 8, 86, 36) val pickButton = scope.button("Select Element", { @@ -479,7 +479,7 @@ internal class SystemInspectorOverlayNode( Rect(contentX, y, contentW, lineHeightPx), ) - val styleRows = controller.debugStyleEditorRows() + val styleRows = controller.overlayStyleEditorRows() reconcileActiveDomDropdown(styleRows) renderStyleEditorRows(bodyScope, body, ctx, bodyScrollY, styleRows) y += snapshot.styleEditorHeight @@ -512,8 +512,8 @@ internal class SystemInspectorOverlayNode( trackRect = bodyScrollbarVisual?.trackRect, thumbRect = bodyScrollbarVisual?.thumbRect ) - renderTooltip(scope, ctx, "dsgl-system-inspector-variable-tooltip", controller.debugVariableTooltip(), 0xEE141A22.toInt(), 0xCC60758F.toInt()) - renderTooltip(scope, ctx, "dsgl-system-inspector-cursor-tooltip", controller.debugCursorTooltip(), 0xDD11151A.toInt(), 0xCC3F4A57.toInt()) + renderTooltip(scope, ctx, "dsgl-system-inspector-variable-tooltip", controller.overlayVariableTooltip(), 0xEE141A22.toInt(), 0xCC60758F.toInt()) + renderTooltip(scope, ctx, "dsgl-system-inspector-cursor-tooltip", controller.overlayCursorTooltip(), 0xDD11151A.toInt(), 0xCC3F4A57.toInt()) } private fun renderPanelOccluder(scope: UiScope, ctx: UiMeasureContext, panelRect: Rect) { @@ -529,7 +529,7 @@ internal class SystemInspectorOverlayNode( } private fun renderHighlights(scope: UiScope, ctx: UiMeasureContext) { - controller.debugSelectedHighlight()?.let { highlight -> + controller.overlaySelectedHighlight()?.let { highlight -> renderHighlightRect(scope, ctx, "dsgl-system-inspector-selected-margin-fill", highlight.marginRect, 0x44F3B33D, null) renderHighlightRect(scope, ctx, "dsgl-system-inspector-selected-padding-fill", highlight.paddingRect, 0x4426A69A, null) renderHighlightRect(scope, ctx, "dsgl-system-inspector-selected-content-fill", highlight.contentRect, 0x444285F4, null) @@ -541,7 +541,7 @@ internal class SystemInspectorOverlayNode( renderHighlightRect(scope, ctx, "dsgl-system-inspector-selected-parent-outline", parentRect, null, 0x66FF5252) } } - controller.debugHoveredHighlight()?.let { highlight -> + controller.overlayHoveredHighlight()?.let { highlight -> renderHighlightRect(scope, ctx, "dsgl-system-inspector-hovered-content-fill", highlight.contentRect, 0x3A47A0FF, null) renderHighlightRect(scope, ctx, "dsgl-system-inspector-hovered-border-outline", highlight.borderRect, null, 0xCC47A0FF.toInt()) } @@ -642,10 +642,10 @@ internal class SystemInspectorOverlayNode( input.placeholderColor = 0xAA9AAFC6.toInt() input.fontSize = 18 input.onInput = { - controller.debugApplyLiteralOverride(row.property, it.value) + controller.overlayApplyLiteralOverride(row.property, it.value) } input.onValueChange = { - controller.debugApplyLiteralOverride(row.property, it.value) + controller.overlayApplyLiteralOverride(row.property, it.value) } input.applyParent(parentNode) renderNode(ctx, input, translateRectY(row.controlRect, -bodyScrollY)) @@ -691,10 +691,10 @@ internal class SystemInspectorOverlayNode( input.placeholderColor = 0xAA9AAFC6.toInt() input.fontSize = 18 input.onInput = { - controller.debugApplyNumericOverride(row.property, it.value, row.unitValue) + controller.overlayApplyNumericOverride(row.property, it.value, row.unitValue) } input.onValueChange = { - controller.debugApplyNumericOverride(row.property, it.value, row.unitValue) + controller.overlayApplyNumericOverride(row.property, it.value, row.unitValue) } input.applyParent(parentNode) renderNode(ctx, input, translateRectY(rect, -bodyScrollY)) @@ -731,7 +731,7 @@ internal class SystemInspectorOverlayNode( } } - val resetRect = controller.debugStyleEditorResetRect() + val resetRect = controller.overlayStyleEditorResetRect() if (resetRect.width > 0 && resetRect.height > 0) { val resetButton = scope.button("Reset node", { key = "dsgl-system-inspector-reset-node" @@ -746,7 +746,7 @@ internal class SystemInspectorOverlayNode( renderNode(ctx, resetButton, translateRectY(resetRect, -bodyScrollY)) } - val clearRect = controller.debugStyleEditorClearRect() + val clearRect = controller.overlayStyleEditorClearRect() if (clearRect.width > 0 && clearRect.height > 0) { val clearButton = scope.button("Clear all", { key = "dsgl-system-inspector-clear-all" diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt index e774781..a698eb9 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt @@ -306,10 +306,10 @@ class SystemOverlayHost( style = inspectorPanelStyle(), onClose = inspectorController::onPanelMinimizeTogglePressed ) - val panelRect = inspectorController.debugExpandedPanelRect() + val panelRect = inspectorController.overlayExpandedPanelRect() if (panelRect != null) { inspectorController.onOverlayPanelRectChanged(panelRect, viewportWidth, viewportHeight) - overlayPanel.syncPanelRect(inspectorController.debugExpandedPanelRect()) + overlayPanel.syncPanelRect(inspectorController.overlayExpandedPanelRect()) } else { state.panelState.show() overlayPanel.syncPanelRect(state.panelState.currentRectOrNull()) From 81b7840438f06449850e2fce6b44641b9c997ed0 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Mon, 30 Mar 2026 15:03:00 +0300 Subject: [PATCH 08/78] deleting dangling debug methods; --- .../core/inspector/InspectorController.kt | 42 ------------ .../PositionedLayoutStickyBehaviorTests.kt | 5 +- ...dGeometryInspectorCharacterizationTests.kt | 9 +-- .../inspector/InspectorControllerTests.kt | 17 ++--- ...stemInspectorOverlayFocusIsolationTests.kt | 3 +- .../SystemInspectorOverlayInputBoundsTests.kt | 3 +- .../overlay/LiveLayerInteractionPathTests.kt | 3 +- .../InspectorDragScrollDomMigrationTests.kt | 25 +++---- .../InspectorDropdownCorrectiveTests.kt | 29 ++++---- .../system/InspectorInputPathBaselineTests.kt | 37 +++++----- .../system/InspectorPointerAlignmentTests.kt | 31 ++++----- .../InspectorTextEditingDomMigrationTests.kt | 3 +- .../SystemOverlayInspectorNativeEntryTests.kt | 67 ++++++++++--------- ...itionedLayoutStickyDemoIntegrationTests.kt | 6 +- 14 files changed, 125 insertions(+), 155 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt index 5e47963..57fd26e 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt @@ -1178,32 +1178,6 @@ class InspectorController( internal fun overlayStyleEditorDropdowns(): List = nativeDropdowns - internal fun debugPickToggleBounds(): Rect? = overlayPickToggleBounds() - - internal fun debugMinimizeBounds(): Rect? = overlayMinimizeBounds() - - internal fun debugContentRect(): Rect = overlayContentRect() - - internal fun debugScrollbarThumbRect(): Rect = overlayScrollbarThumbRect() - - internal fun debugScrollbarTrackRect(): Rect = overlayScrollbarTrackRect() - - internal fun debugSelectedHighlight(): InspectorHighlightSnapshot? = overlaySelectedHighlight() - - internal fun debugHoveredHighlight(): InspectorHighlightSnapshot? = overlayHoveredHighlight() - - internal fun debugCursorTooltip(): InspectorTooltipSnapshot? = overlayCursorTooltip() - - internal fun debugVariableTooltip(): InspectorTooltipSnapshot? = overlayVariableTooltip() - - internal fun debugStyleEditorRows(): List = overlayStyleEditorRows() - - internal fun debugStyleEditorResetRect(): Rect = overlayStyleEditorResetRect() - - internal fun debugStyleEditorClearRect(): Rect = overlayStyleEditorClearRect() - - internal fun debugStyleEditorDropdowns(): List = overlayStyleEditorDropdowns() - internal fun onNativeDomDropdownSnapshots(dropdowns: List) { nativeDropdowns.clear() nativeDropdowns.addAll(dropdowns) @@ -1472,14 +1446,6 @@ class InspectorController( } } - internal fun debugApplyLiteralOverride(property: StyleProperty, literal: String): Boolean { - return overlayApplyLiteralOverride(property, literal) - } - - internal fun debugApplyNumericOverride(property: StyleProperty, numericLiteral: String, unitToken: String?): Boolean { - return overlayApplyNumericOverride(property, numericLiteral, unitToken) - } - private fun resetNativePresentation() { nativeSelectedHighlight = null nativeHoveredHighlight = null @@ -1709,14 +1675,6 @@ class InspectorController( return expandedRect } - internal fun debugColorPickerActionBounds(property: StyleProperty): Rect? { - return overlayColorPickerActionBounds(property) - } - - internal fun debugPanelRect(): Rect? = overlayPanelRect() - - internal fun debugExpandedPanelRect(): Rect? = overlayExpandedPanelRect() - private fun performPanelAction(action: PanelAction) { when (action.kind) { ActionKind.Minimize -> { diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutStickyBehaviorTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutStickyBehaviorTests.kt index 797768a..4c405cb 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutStickyBehaviorTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutStickyBehaviorTests.kt @@ -642,7 +642,7 @@ class PositionedLayoutStickyBehaviorTests { inspector.buildDomSnapshot(800, 600) assertEquals(sticky.key?.toString(), inspector.hoveredKey) - val highlight = inspector.debugHoveredHighlight() + val highlight = inspector.overlayHoveredHighlight() assertNotNull(highlight) assertEquals(rect, highlight.borderRect) } @@ -682,7 +682,7 @@ class PositionedLayoutStickyBehaviorTests { inspector.buildDomSnapshot(800, 600) assertEquals(sticky.key?.toString(), inspector.hoveredKey) - val highlight = inspector.debugHoveredHighlight() + val highlight = inspector.overlayHoveredHighlight() assertNotNull(highlight) assertEquals(visibleRect, highlight.borderRect) } @@ -1001,3 +1001,4 @@ class PositionedLayoutStickyBehaviorTests { } } } + diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/UnifiedUsedGeometryInspectorCharacterizationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/UnifiedUsedGeometryInspectorCharacterizationTests.kt index 8e7c7f5..c69d279 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/UnifiedUsedGeometryInspectorCharacterizationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/UnifiedUsedGeometryInspectorCharacterizationTests.kt @@ -267,7 +267,7 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { inspector.onCursorMoved(185, 25) inspector.buildDomSnapshot(800, 600) - val fixedHighlight = inspector.debugHoveredHighlight() + val fixedHighlight = inspector.overlayHoveredHighlight() assertNotNull(fixedHighlight) val fixedUsedGeometry = UsedInteractionGeometryResolver.resolveNodeGeometry(fixed) assertEquals( @@ -277,7 +277,7 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { inspector.onCursorMoved(145, 95) inspector.buildDomSnapshot(800, 600) - val clippedHighlight = inspector.debugHoveredHighlight() + val clippedHighlight = inspector.overlayHoveredHighlight() assertNotNull(clippedHighlight) val rootUsedGeometry = UsedInteractionGeometryResolver.resolveNodeGeometry(root) assertEquals( @@ -317,7 +317,7 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { inspector.buildDomSnapshot(800, 600) assertEquals(relative.key?.toString(), inspector.hoveredKey) - val highlight = inspector.debugHoveredHighlight() + val highlight = inspector.overlayHoveredHighlight() assertNotNull(highlight) assertEquals( usedGeometry.visibleBorderRect ?: org.dreamfinity.dsgl.core.dom.layout.Rect(0, 0, 0, 0), @@ -344,7 +344,7 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { inspector.buildDomSnapshot(800, 600) assertEquals(fixture.fixed.key?.toString(), inspector.hoveredKey) - val highlight = inspector.debugHoveredHighlight() + val highlight = inspector.overlayHoveredHighlight() assertNotNull(highlight) val fixedGeometry = UsedInteractionGeometryResolver.resolveNodeGeometry(fixture.fixed) assertEquals( @@ -449,3 +449,4 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { } } } + diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorControllerTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorControllerTests.kt index 1929a8c..7b3ef3c 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorControllerTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorControllerTests.kt @@ -258,7 +258,7 @@ class InspectorControllerTests { controller.handleMouseDown(990, 230, MouseButton.LEFT) renderFrame(controller, 420, 280) - val thumb = controller.debugScrollbarThumbRect() + val thumb = controller.overlayScrollbarThumbRect() if (thumb.width <= 0 || thumb.height <= 0) { fail("Expected inspector scrollbar thumb to be available.") } @@ -417,7 +417,7 @@ class InspectorControllerTests { assertTrue(controller.handleMouseDown(988, 126, MouseButton.LEFT)) renderFrame(controller, 1200, 700) - val row = controller.debugStyleEditorRows().firstOrNull { + val row = controller.overlayStyleEditorRows().firstOrNull { it.property == StyleProperty.BACKGROUND_COLOR && it.editorKind == InspectorEditorKind.StringInput } ?: error("Expected color string input row.") val inputRect = row.inputRect ?: row.controlRect @@ -447,7 +447,7 @@ class InspectorControllerTests { controller.handleMouseDown(988, 126, MouseButton.LEFT) renderFrame(controller, 260, 240) - val rows = controller.debugStyleEditorRows() + val rows = controller.overlayStyleEditorRows() assertTrue(rows.isNotEmpty()) assertTrue(rows.any { it.rowRect.height > it.controlRect.height + 8 }) @@ -470,7 +470,7 @@ class InspectorControllerTests { controller.handleMouseDown(988, 126, MouseButton.LEFT) renderFrame(controller, 1200, 700) - val row = controller.debugStyleEditorRows().firstOrNull { it.editorKind == InspectorEditorKind.EnumSelect } + val row = controller.overlayStyleEditorRows().firstOrNull { it.editorKind == InspectorEditorKind.EnumSelect } ?: error("Expected enum select row.") val openX = row.controlRect.x + 4 val openY = row.controlRect.y + row.controlRect.height / 2 @@ -478,7 +478,7 @@ class InspectorControllerTests { assertTrue(controller.handleMouseDown(openX, openY, MouseButton.LEFT)) renderFrame(controller, 1200, 700) - val dropdown = controller.debugStyleEditorDropdowns().firstOrNull() ?: error("Expected open dropdown.") + val dropdown = controller.overlayStyleEditorDropdowns().firstOrNull() ?: error("Expected open dropdown.") val option = dropdown.options.firstOrNull { !it.text.equals(row.controlValue, ignoreCase = true) } ?: dropdown.options.firstOrNull() ?: error("Expected dropdown option.") @@ -490,7 +490,7 @@ class InspectorControllerTests { assertTrue(controller.handleMouseDown(optionX, optionY, MouseButton.LEFT)) renderFrame(controller, 1200, 700) - assertTrue(controller.debugStyleEditorDropdowns().isEmpty()) + assertTrue(controller.overlayStyleEditorDropdowns().isEmpty()) val literal = (StyleEngine.inspectorOverrideFor(selected, row.property) as? StyleExpression.Literal)?.value assertNotNull(literal) assertTrue(literal.equals(option.text, ignoreCase = true)) @@ -511,11 +511,11 @@ class InspectorControllerTests { controller.onCursorMoved(988, 126) controller.handleMouseDown(988, 126, MouseButton.LEFT) - assertTrue(controller.debugApplyNumericOverride(StyleProperty.Z_INDEX, "5", "px")) + assertTrue(controller.overlayApplyNumericOverride(StyleProperty.Z_INDEX, "5", "px")) val zIndexLiteral = (StyleEngine.inspectorOverrideFor(selected, StyleProperty.Z_INDEX) as? StyleExpression.Literal)?.value assertEquals("5", zIndexLiteral) - assertTrue(controller.debugApplyNumericOverride(StyleProperty.WIDTH, "24", "em")) + assertTrue(controller.overlayApplyNumericOverride(StyleProperty.WIDTH, "24", "em")) val widthLiteral = (StyleEngine.inspectorOverrideFor(selected, StyleProperty.WIDTH) as? StyleExpression.Literal)?.value assertEquals("24em", widthLiteral) } @@ -589,3 +589,4 @@ class InspectorControllerTests { ) } + diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayFocusIsolationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayFocusIsolationTests.kt index 77f4a0f..cd7e833 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayFocusIsolationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayFocusIsolationTests.kt @@ -162,7 +162,7 @@ class SystemInspectorOverlayFocusIsolationTests { host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 40, cursorY = 30, inspectorPointerCaptured = false) host.render(ctx, 1280, 720) - val pickRect = inspector.debugPickToggleBounds() ?: error("pick toggle missing") + val pickRect = inspector.overlayPickToggleBounds() ?: error("pick toggle missing") assertTrue(host.handleMouseDown(pickRect.x + 2, pickRect.y + 2, MouseButton.LEFT)) assertTrue(host.handleMouseUp(pickRect.x + 2, pickRect.y + 2, MouseButton.LEFT)) assertEquals(InspectorMode.Pick, inspector.mode) @@ -203,3 +203,4 @@ class SystemInspectorOverlayFocusIsolationTests { } } } + diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayInputBoundsTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayInputBoundsTests.kt index 96e87d4..b37ea87 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayInputBoundsTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayInputBoundsTests.kt @@ -18,7 +18,7 @@ class SystemInspectorOverlayInputBoundsTests { it.setPickMode(false) } val node = SystemInspectorOverlayNode(controller) - val panelRect = controller.debugPanelRect() ?: error("expected panel rect") + val panelRect = controller.overlayPanelRect() ?: error("expected panel rect") val popupRect = Rect( x = panelRect.x + panelRect.width + 32, @@ -55,3 +55,4 @@ class SystemInspectorOverlayInputBoundsTests { assertFalse(node.bounds.contains(popupProbeX, popupProbeY)) } } + diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt index 870e143..5481a3f 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt @@ -115,7 +115,7 @@ class LiveLayerInteractionPathTests { systemHost.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) systemHost.render(ctx, 1280, 720) - val panelRect = inspector.debugPanelRect() ?: error("inspector panel rect missing") + val panelRect = inspector.overlayPanelRect() ?: error("inspector panel rect missing") val outsideX = if (panelRect.x > 40) panelRect.x - 20 else panelRect.x + panelRect.width + 20 val outsideY = (panelRect.y + panelRect.height / 2).coerceIn(1, 719) assertFalse(panelRect.contains(outsideX, outsideY)) @@ -212,3 +212,4 @@ class LiveLayerInteractionPathTests { } } } + diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt index cf472fa..9caafa5 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt @@ -44,7 +44,7 @@ class InspectorDragScrollDomMigrationTests { fun `inspector panel drag is dom-first and controller drag authority stays demoted`() { val fixture = openInspectorAndSelectTarget(withManyChildren = true) - val before = fixture.inspector.debugPanelRect() ?: error("expected panel rect") + val before = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") val downX = before.x + 18 val downY = before.y + 14 val moveX = downX + 90 @@ -59,7 +59,7 @@ class InspectorDragScrollDomMigrationTests { assertTrue(fixture.host.handleMouseMove(moveX, moveY)) syncAndRender(fixture, moveX, moveY) - val moved = fixture.inspector.debugPanelRect() ?: error("expected moved panel rect") + val moved = fixture.inspector.overlayPanelRect() ?: error("expected moved panel rect") assertTrue(moved.x != before.x || moved.y != before.y) assertTrue(fixture.host.handleMouseUp(moveX, moveY, MouseButton.LEFT)) @@ -75,7 +75,7 @@ class InspectorDragScrollDomMigrationTests { val fixture = openInspectorAndSelectTarget(withManyChildren = true) setViewport(fixture, 420, 280) - val contentRect = fixture.inspector.debugContentRect() + val contentRect = fixture.inspector.overlayContentRect() val wheelX = contentRect.x + 4 val wheelY = contentRect.y + 10 val before = fixture.inspector.panelScrollOffsetY @@ -96,7 +96,7 @@ class InspectorDragScrollDomMigrationTests { setViewport(fixture, 420, 280) scrollInspectorBodyDown(fixture, steps = 2) - val thumb = fixture.inspector.debugScrollbarThumbRect() + val thumb = fixture.inspector.overlayScrollbarThumbRect() assertTrue(thumb.width > 0 && thumb.height > 0) val dragX = thumb.x + thumb.width / 2 @@ -126,7 +126,7 @@ class InspectorDragScrollDomMigrationTests { setViewport(fixture, 420, 280) scrollInspectorBodyDown(fixture, steps = 3) - val thumb = fixture.inspector.debugScrollbarThumbRect() + val thumb = fixture.inspector.overlayScrollbarThumbRect() assertTrue(thumb.width > 0 && thumb.height > 0) val dragX = thumb.x + thumb.width / 2 @@ -157,7 +157,7 @@ class InspectorDragScrollDomMigrationTests { val fixture = openInspectorAndSelectTarget(withManyChildren = true) setViewport(fixture, 420, 280) - val panelRect = fixture.inspector.debugPanelRect() ?: error("expected panel rect") + val panelRect = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") val downX = panelRect.x + 14 val downY = panelRect.y + 12 assertTrue(fixture.host.handleMouseDown(downX, downY, MouseButton.LEFT)) @@ -199,12 +199,12 @@ class InspectorDragScrollDomMigrationTests { fixture.host.handleMouseUp(clickX, clickY, MouseButton.LEFT) syncAndRender(fixture, clickX, clickY) - assertTrue(fixture.inspector.debugStyleEditorDropdowns().isNotEmpty()) + assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isNotEmpty()) fixture.host.handleMouseDown(clickX, clickY, MouseButton.LEFT) fixture.host.handleMouseUp(clickX, clickY, MouseButton.LEFT) syncAndRender(fixture, clickX, clickY) - assertTrue(fixture.inspector.debugStyleEditorDropdowns().isEmpty()) + assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isEmpty()) } @Test @@ -267,7 +267,7 @@ class InspectorDragScrollDomMigrationTests { } private fun scrollInspectorBodyDown(fixture: Fixture, steps: Int) { - val contentRect = fixture.inspector.debugContentRect() + val contentRect = fixture.inspector.overlayContentRect() val wheelX = contentRect.x + 4 val wheelY = contentRect.y + 10 repeat(steps) { @@ -278,10 +278,10 @@ class InspectorDragScrollDomMigrationTests { private fun findVisibleSelectRow(fixture: Fixture): InspectorStyleEditorRowSnapshot { repeat(120) { - val rows = fixture.inspector.debugStyleEditorRows().filter { row -> + val rows = fixture.inspector.overlayStyleEditorRows().filter { row -> row.editorKind == InspectorEditorKind.EnumSelect || row.editorKind == InspectorEditorKind.FontSelect } - val contentRect = fixture.inspector.debugContentRect() + val contentRect = fixture.inspector.overlayContentRect() val bodyScrollY = fixture.inspector.panelScrollOffsetY rows.firstOrNull { row -> val rect = Rect( @@ -331,7 +331,7 @@ class InspectorDragScrollDomMigrationTests { private fun findVisibleInputNode(fixture: Fixture, propertyKey: String): TextInputNode { val inspectorNode = fixture.host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector entry missing") - val contentRect = fixture.inspector.debugContentRect() + val contentRect = fixture.inspector.overlayContentRect() val candidates = collectNodes(inspectorNode) .filterIsInstance() .filter { (it.key?.toString() ?: "") == "dsgl-system-inspector-editor-numeric-input-$propertyKey" } @@ -381,3 +381,4 @@ class InspectorDragScrollDomMigrationTests { ) } + diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt index 490a3e6..9d9b64d 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt @@ -41,11 +41,11 @@ class InspectorDropdownCorrectiveTests { scrollInspectorBodyDown(fixture, steps = 10) val (trigger, dropdown) = openInspectorSelectDropdown(fixture, requireScrollable = false) - val contentRect = fixture.inspector.debugContentRect() + val contentRect = fixture.inspector.overlayContentRect() var outsideX = contentRect.x + 4 var outsideY = contentRect.y + 4 if (dropdown.popupRect.contains(outsideX, outsideY) || trigger.contains(outsideX, outsideY)) { - val panelRect = fixture.inspector.debugPanelRect() ?: error("expected panel rect") + val panelRect = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") outsideX = panelRect.x + 8 outsideY = panelRect.y + 8 } @@ -54,7 +54,7 @@ class InspectorDropdownCorrectiveTests { fixture.host.handleMouseUp(outsideX, outsideY, MouseButton.LEFT) syncAndRender(fixture, outsideX, outsideY) - assertTrue(fixture.inspector.debugStyleEditorDropdowns().isEmpty()) + assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isEmpty()) } @Test @@ -64,14 +64,14 @@ class InspectorDropdownCorrectiveTests { settleFrames(fixture, steps = 1) val beforePanelScroll = fixture.inspector.panelScrollOffsetY - val contentRect = fixture.inspector.debugContentRect() + val contentRect = fixture.inspector.overlayContentRect() val wheelX = contentRect.x + 4 val wheelY = contentRect.y + 10 assertTrue(fixture.host.handleMouseWheel(wheelX, wheelY, -120)) syncAndRender(fixture, wheelX, wheelY) - assertTrue(fixture.inspector.debugStyleEditorDropdowns().isNotEmpty()) + assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isNotEmpty()) assertEquals(beforePanelScroll, fixture.inspector.panelScrollOffsetY) } @@ -193,7 +193,7 @@ class InspectorDropdownCorrectiveTests { } private fun scrollInspectorBodyDown(fixture: Fixture, steps: Int) { - val contentRect = fixture.inspector.debugContentRect() + val contentRect = fixture.inspector.overlayContentRect() val wheelX = contentRect.x + 4 val wheelY = contentRect.y + 10 repeat(steps) { @@ -203,7 +203,7 @@ class InspectorDropdownCorrectiveTests { } private fun settleFrames(fixture: Fixture, steps: Int) { - val contentRect = fixture.inspector.debugContentRect() + val contentRect = fixture.inspector.overlayContentRect() val cursorX = contentRect.x + 4 val cursorY = contentRect.y + 10 repeat(steps) { @@ -214,9 +214,9 @@ class InspectorDropdownCorrectiveTests { private fun openVisibleInspectorSelectDropdownWithoutBodyScroll( fixture: Fixture ): Pair { - val contentRect = fixture.inspector.debugContentRect() + val contentRect = fixture.inspector.overlayContentRect() val bodyScrollY = fixture.inspector.panelScrollOffsetY - val row = fixture.inspector.debugStyleEditorRows().firstOrNull { row -> + val row = fixture.inspector.overlayStyleEditorRows().firstOrNull { row -> if (row.editorKind != InspectorEditorKind.EnumSelect && row.editorKind != InspectorEditorKind.FontSelect) { return@firstOrNull false } @@ -243,7 +243,7 @@ class InspectorDropdownCorrectiveTests { fixture.host.handleMouseUp(clickX, clickY, MouseButton.LEFT) syncAndRender(fixture, clickX, clickY) - val opened = fixture.inspector.debugStyleEditorDropdowns().firstOrNull() + val opened = fixture.inspector.overlayStyleEditorDropdowns().firstOrNull() ?: error("expected inspector dropdown to open from visible select row") return triggerRect to opened } @@ -253,9 +253,9 @@ class InspectorDropdownCorrectiveTests { requireScrollable: Boolean ): Pair { repeat(120) { - val contentRect = fixture.inspector.debugContentRect() + val contentRect = fixture.inspector.overlayContentRect() val bodyScrollY = fixture.inspector.panelScrollOffsetY - val visibleSelectRows = fixture.inspector.debugStyleEditorRows().filter { row -> + val visibleSelectRows = fixture.inspector.overlayStyleEditorRows().filter { row -> if (row.editorKind != InspectorEditorKind.EnumSelect && row.editorKind != InspectorEditorKind.FontSelect) { return@filter false } @@ -283,7 +283,7 @@ class InspectorDropdownCorrectiveTests { fixture.host.handleMouseUp(clickX, clickY, MouseButton.LEFT) syncAndRender(fixture, clickX, clickY) - val opened = fixture.inspector.debugStyleEditorDropdowns().firstOrNull() + val opened = fixture.inspector.overlayStyleEditorDropdowns().firstOrNull() if (opened != null && (!requireScrollable || opened.footerText != null)) { return triggerRect to opened } @@ -331,7 +331,7 @@ class InspectorDropdownCorrectiveTests { private fun findVisibleInputNode(fixture: Fixture, propertyKey: String): TextInputNode { val inspectorNode = fixture.host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector entry missing") - val contentRect = fixture.inspector.debugContentRect() + val contentRect = fixture.inspector.overlayContentRect() val candidates = collectNodes(inspectorNode) .filterIsInstance() .filter { (it.key?.toString() ?: "") == "dsgl-system-inspector-editor-numeric-input-$propertyKey" } @@ -381,3 +381,4 @@ class InspectorDropdownCorrectiveTests { ) } + diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorInputPathBaselineTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorInputPathBaselineTests.kt index 0bfc4da..488e02c 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorInputPathBaselineTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorInputPathBaselineTests.kt @@ -67,20 +67,20 @@ class InspectorInputPathBaselineTests { fixture.host.handleMouseDown(trigger.x + 2, trigger.y + 2, MouseButton.LEFT) fixture.host.handleMouseUp(trigger.x + 2, trigger.y + 2, MouseButton.LEFT) syncAndRender(fixture, trigger.x + 2, trigger.y + 2) - assertTrue(fixture.inspector.debugStyleEditorDropdowns().isEmpty()) + assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isEmpty()) } @Test fun `inspector dropdown opens and closes from dom interactions`() { val fixture = openInspectorAndSelectTarget(withManyChildren = false) val (trigger, _) = openDropdownFromVisibleSelectRow(fixture) - assertTrue(fixture.inspector.debugStyleEditorDropdowns().isNotEmpty()) + assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isNotEmpty()) fixture.host.handleMouseDown(trigger.x + 2, trigger.y + 2, MouseButton.LEFT) fixture.host.handleMouseUp(trigger.x + 2, trigger.y + 2, MouseButton.LEFT) syncAndRender(fixture, trigger.x + 2, trigger.y + 2) - assertTrue(fixture.inspector.debugStyleEditorDropdowns().isEmpty()) + assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isEmpty()) } @Test @@ -95,7 +95,7 @@ class InspectorInputPathBaselineTests { assertTrue(fixture.host.handleMouseUp(optionX, optionY, MouseButton.LEFT)) syncAndRender(fixture, optionX, optionY) - assertTrue(fixture.inspector.debugStyleEditorDropdowns().isEmpty()) + assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isEmpty()) assertFalse(fixture.inspector.hasOpenStyleDropdown()) } @@ -105,7 +105,7 @@ class InspectorInputPathBaselineTests { val (_, opened) = openDropdownFromVisibleSelectRow(fixture) syncAndRender(fixture, opened.popupRect.x + 2, opened.popupRect.y + 2) - val reopened = fixture.inspector.debugStyleEditorDropdowns().firstOrNull() + val reopened = fixture.inspector.overlayStyleEditorDropdowns().firstOrNull() ?: error("expected dropdown after rebuild") assertEquals(opened.property, reopened.property) @@ -118,18 +118,18 @@ class InspectorInputPathBaselineTests { val fixture = openInspectorAndSelectTarget(withManyChildren = false) openDropdownFromVisibleSelectRow(fixture) - assertTrue(fixture.inspector.debugStyleEditorDropdowns().isNotEmpty()) + assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isNotEmpty()) assertFalse(fixture.inspector.hasOpenStyleDropdown()) assertFalse(fixture.inspector.handleOpenStyleDropdownWheel(-120)) - val contentRect = fixture.inspector.debugContentRect() + val contentRect = fixture.inspector.overlayContentRect() val wheelX = contentRect.x + 4 val wheelY = contentRect.y + 10 val beforePanelScroll = fixture.inspector.panelScrollOffsetY assertTrue(fixture.host.handleMouseWheel(wheelX, wheelY, -120)) syncAndRender(fixture, wheelX, wheelY) - assertTrue(fixture.inspector.debugStyleEditorDropdowns().isNotEmpty()) + assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isNotEmpty()) assertEquals(beforePanelScroll, fixture.inspector.panelScrollOffsetY) assertFalse(fixture.inspector.hasOpenStyleDropdown()) } @@ -191,17 +191,17 @@ class InspectorInputPathBaselineTests { .coerceIn(2, (fixture.viewportHeight - popup.bounds.height - 2).coerceAtLeast(2)) assertEquals(expectedY, popup.bounds.y) - val contentRect = fixture.inspector.debugContentRect() + val contentRect = fixture.inspector.overlayContentRect() val wheelX = contentRect.x + 4 val wheelY = contentRect.y + 10 assertTrue(fixture.host.handleMouseWheel(wheelX, wheelY, -120)) syncAndRender(fixture, wheelX, wheelY) - assertTrue(fixture.inspector.debugStyleEditorDropdowns().isNotEmpty()) + assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isNotEmpty()) var outsideX = contentRect.x + 4 var outsideY = contentRect.y + 4 if (opened.popupRect.contains(outsideX, outsideY) || trigger.contains(outsideX, outsideY)) { - val panelRect = fixture.inspector.debugPanelRect() ?: error("expected panel rect") + val panelRect = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") outsideX = panelRect.x + 8 outsideY = panelRect.y + 8 } @@ -209,7 +209,7 @@ class InspectorInputPathBaselineTests { fixture.host.handleMouseDown(outsideX, outsideY, MouseButton.LEFT) fixture.host.handleMouseUp(outsideX, outsideY, MouseButton.LEFT) syncAndRender(fixture, outsideX, outsideY) - assertTrue(fixture.inspector.debugStyleEditorDropdowns().isEmpty()) + assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isEmpty()) } private fun openInspectorAndSelectTarget(withManyChildren: Boolean): Fixture { @@ -259,7 +259,7 @@ class InspectorInputPathBaselineTests { } private fun scrollInspectorBodyDown(fixture: Fixture, steps: Int) { - val contentRect = fixture.inspector.debugContentRect() + val contentRect = fixture.inspector.overlayContentRect() val wheelX = contentRect.x + 4 val wheelY = contentRect.y + 10 repeat(steps) { @@ -270,10 +270,10 @@ class InspectorInputPathBaselineTests { private fun findOrScrollToVisibleSelectRow(fixture: Fixture): InspectorStyleEditorRowSnapshot { repeat(120) { - val rows = fixture.inspector.debugStyleEditorRows().filter { row -> + val rows = fixture.inspector.overlayStyleEditorRows().filter { row -> row.editorKind == InspectorEditorKind.EnumSelect || row.editorKind == InspectorEditorKind.FontSelect } - val contentRect = fixture.inspector.debugContentRect() + val contentRect = fixture.inspector.overlayContentRect() val bodyScrollY = fixture.inspector.panelScrollOffsetY rows.firstOrNull { row -> val rect = Rect( @@ -303,7 +303,7 @@ class InspectorInputPathBaselineTests { } private fun findRowByProperty(fixture: Fixture, property: StyleProperty): InspectorStyleEditorRowSnapshot { - return fixture.inspector.debugStyleEditorRows().firstOrNull { it.property == property } + return fixture.inspector.overlayStyleEditorRows().firstOrNull { it.property == property } ?: error("expected row for $property") } @@ -318,7 +318,7 @@ class InspectorInputPathBaselineTests { fixture.host.handleMouseUp(clickX, clickY, MouseButton.LEFT) syncAndRender(fixture, clickX, clickY) - val opened = fixture.inspector.debugStyleEditorDropdowns().firstOrNull() + val opened = fixture.inspector.overlayStyleEditorDropdowns().firstOrNull() ?: error("expected opened inspector dropdown") return triggerRect to opened } @@ -355,7 +355,7 @@ class InspectorInputPathBaselineTests { private fun findVisibleInputNode(fixture: Fixture, propertyKey: String): TextInputNode { val inspectorNode = fixture.host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector entry missing") - val contentRect = fixture.inspector.debugContentRect() + val contentRect = fixture.inspector.overlayContentRect() val candidates = collectNodes(inspectorNode) .filterIsInstance() .filter { (it.key?.toString() ?: "") == "dsgl-system-inspector-editor-numeric-input-$propertyKey" } @@ -405,3 +405,4 @@ class InspectorInputPathBaselineTests { ) } + diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt index cd95035..6075777 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt @@ -119,7 +119,7 @@ class InspectorPointerAlignmentTests { fixture.host.handleMouseDown(triggerRect.x + 2, triggerRect.y + (triggerRect.height / 2).coerceAtLeast(1), MouseButton.LEFT) fixture.host.handleMouseUp(triggerRect.x + 2, triggerRect.y + (triggerRect.height / 2).coerceAtLeast(1), MouseButton.LEFT) syncAndRender(fixture, triggerRect.x + 2, triggerRect.y + 2) - assertTrue(fixture.inspector.debugStyleEditorDropdowns().isEmpty()) + assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isEmpty()) val rawX = row.controlRect.x + 2 val rawY = row.controlRect.y + (row.controlRect.height / 2).coerceAtLeast(1) @@ -127,7 +127,7 @@ class InspectorPointerAlignmentTests { fixture.host.handleMouseUp(rawX, rawY, MouseButton.LEFT) syncAndRender(fixture, rawX, rawY) - val openedOnRaw = fixture.inspector.debugStyleEditorDropdowns().firstOrNull() + val openedOnRaw = fixture.inspector.overlayStyleEditorDropdowns().firstOrNull() assertTrue(openedOnRaw == null || openedOnRaw.property != property) } @@ -139,19 +139,19 @@ class InspectorPointerAlignmentTests { val row = findOrScrollToVisibleSelectRow(fixture) val (triggerRect, dropdown) = openDropdownFromVisibleSelectRow(fixture, row) settleFrames(fixture, steps = 1) - val contentRect = fixture.inspector.debugContentRect() + val contentRect = fixture.inspector.overlayContentRect() val wheelX = contentRect.x + 4 val wheelY = contentRect.y + 10 assertTrue(fixture.host.handleMouseWheel(wheelX, wheelY, -120)) syncAndRender(fixture, wheelX, wheelY) - assertTrue(fixture.inspector.debugStyleEditorDropdowns().isNotEmpty()) + assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isNotEmpty()) var outsideX = contentRect.x + 4 var outsideY = contentRect.y + 4 if (dropdown.popupRect.contains(outsideX, outsideY) || triggerRect.contains(outsideX, outsideY)) { - val panelRect = fixture.inspector.debugPanelRect() ?: error("expected panel rect") + val panelRect = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") outsideX = panelRect.x + 8 outsideY = panelRect.y + 8 } @@ -160,7 +160,7 @@ class InspectorPointerAlignmentTests { fixture.host.handleMouseUp(outsideX, outsideY, MouseButton.LEFT) syncAndRender(fixture, outsideX, outsideY) - assertTrue(fixture.inspector.debugStyleEditorDropdowns().isEmpty()) + assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isEmpty()) } private fun openInspectorAndSelectTarget(withManyChildren: Boolean): Fixture { @@ -210,7 +210,7 @@ class InspectorPointerAlignmentTests { } private fun dragInspectorPanel(fixture: Fixture, deltaX: Int, deltaY: Int) { - val panelRect = fixture.inspector.debugPanelRect() ?: error("expected panel rect") + val panelRect = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") val downX = panelRect.x + 16 val downY = panelRect.y + 12 val moveX = downX + deltaX @@ -223,7 +223,7 @@ class InspectorPointerAlignmentTests { } private fun scrollInspectorBodyDown(fixture: Fixture, steps: Int) { - val contentRect = fixture.inspector.debugContentRect() + val contentRect = fixture.inspector.overlayContentRect() val wheelX = contentRect.x + 4 val wheelY = contentRect.y + 10 repeat(steps) { @@ -233,7 +233,7 @@ class InspectorPointerAlignmentTests { } private fun settleFrames(fixture: Fixture, steps: Int) { - val contentRect = fixture.inspector.debugContentRect() + val contentRect = fixture.inspector.overlayContentRect() val cursorX = contentRect.x + 4 val cursorY = contentRect.y + 10 repeat(steps) { @@ -242,10 +242,10 @@ class InspectorPointerAlignmentTests { } private fun findVisibleSelectRowWithoutScrolling(fixture: Fixture): InspectorStyleEditorRowSnapshot { - val rows = fixture.inspector.debugStyleEditorRows().filter { row -> + val rows = fixture.inspector.overlayStyleEditorRows().filter { row -> row.editorKind == InspectorEditorKind.EnumSelect || row.editorKind == InspectorEditorKind.FontSelect } - val contentRect = fixture.inspector.debugContentRect() + val contentRect = fixture.inspector.overlayContentRect() val bodyScrollY = fixture.inspector.panelScrollOffsetY return rows.firstOrNull { row -> val rect = Rect( @@ -262,10 +262,10 @@ class InspectorPointerAlignmentTests { private fun findOrScrollToVisibleSelectRow(fixture: Fixture): InspectorStyleEditorRowSnapshot { repeat(120) { - val rows = fixture.inspector.debugStyleEditorRows().filter { row -> + val rows = fixture.inspector.overlayStyleEditorRows().filter { row -> row.editorKind == InspectorEditorKind.EnumSelect || row.editorKind == InspectorEditorKind.FontSelect } - val contentRect = fixture.inspector.debugContentRect() + val contentRect = fixture.inspector.overlayContentRect() val bodyScrollY = fixture.inspector.panelScrollOffsetY val visible = rows.firstOrNull { row -> val rect = Rect( @@ -294,13 +294,13 @@ class InspectorPointerAlignmentTests { fixture.host.handleMouseDown(clickX, clickY, MouseButton.LEFT) fixture.host.handleMouseUp(clickX, clickY, MouseButton.LEFT) syncAndRender(fixture, clickX, clickY) - val opened = fixture.inspector.debugStyleEditorDropdowns().firstOrNull() + val opened = fixture.inspector.overlayStyleEditorDropdowns().firstOrNull() assertNotNull(opened) return triggerRect to opened } private fun findRowByProperty(fixture: Fixture, property: StyleProperty): InspectorStyleEditorRowSnapshot { - return fixture.inspector.debugStyleEditorRows().firstOrNull { it.property == property } + return fixture.inspector.overlayStyleEditorRows().firstOrNull { it.property == property } ?: error("expected row for property ${property.key}") } @@ -342,3 +342,4 @@ class InspectorPointerAlignmentTests { ) } + diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorTextEditingDomMigrationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorTextEditingDomMigrationTests.kt index 270a0f5..d3a0794 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorTextEditingDomMigrationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorTextEditingDomMigrationTests.kt @@ -233,7 +233,7 @@ class InspectorTextEditingDomMigrationTests { private fun findVisibleInputNode(host: SystemOverlayHost, inspector: InspectorController, keyPrefix: String): TextInputNode { val inspectorNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector entry missing") - val contentRect = inspector.debugContentRect() + val contentRect = inspector.overlayContentRect() val candidates = collectNodes(inspectorNode) .filterIsInstance() .filter { (it.key?.toString() ?: "").startsWith(keyPrefix) } @@ -303,3 +303,4 @@ class InspectorTextEditingDomMigrationTests { } } + diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt index 6020b65..3df5525 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt @@ -137,8 +137,8 @@ class SystemOverlayInspectorNativeEntryTests { ) host.render(ctx, 1280, 720) - val panelRect = inspector.debugPanelRect() ?: error("panel rect missing") - val highlight = inspector.debugSelectedHighlight() ?: error("selected highlight missing") + val panelRect = inspector.overlayPanelRect() ?: error("panel rect missing") + val highlight = inspector.overlaySelectedHighlight() ?: error("selected highlight missing") assertTrue(intersects(highlight.contentRect, panelRect)) val inspectorNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector node missing") @@ -179,12 +179,12 @@ class SystemOverlayInspectorNativeEntryTests { host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 80, cursorY = 52, inspectorPointerCaptured = false) host.render(ctx, 1280, 720) - val pickToggle = inspector.debugPickToggleBounds() ?: error("pick toggle missing") + val pickToggle = inspector.overlayPickToggleBounds() ?: error("pick toggle missing") assertTrue(host.handleMouseDown(pickToggle.x + 1, pickToggle.y + 1, MouseButton.LEFT)) assertTrue(host.handleMouseUp(pickToggle.x + 1, pickToggle.y + 1, MouseButton.LEFT)) assertEquals(InspectorMode.Pick, inspector.mode) - val colorAction = inspector.debugColorPickerActionBounds(StyleProperty.BACKGROUND_COLOR) + val colorAction = inspector.overlayColorPickerActionBounds(StyleProperty.BACKGROUND_COLOR) val colorAnchor = colorAction ?: Rect(80, 80, 20, 18) val openedByClick = if (colorAction != null) { host.handleMouseDown(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) && @@ -245,7 +245,7 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 1280, 720) val initialNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector node missing") - val minimizeRect = inspector.debugMinimizeBounds() ?: error("minimize bounds missing") + val minimizeRect = inspector.overlayMinimizeBounds() ?: error("minimize bounds missing") assertTrue(host.handleMouseDown(minimizeRect.x + 1, minimizeRect.y + 1, MouseButton.LEFT)) assertTrue(host.handleMouseUp(minimizeRect.x + 1, minimizeRect.y + 1, MouseButton.LEFT)) assertEquals(InspectorPanelState.Minimized, inspector.panelState) @@ -280,7 +280,7 @@ class SystemOverlayInspectorNativeEntryTests { host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 40, cursorY = 30, inspectorPointerCaptured = false) host.render(ctx, 1280, 720) - val minimizeRect = inspector.debugMinimizeBounds() ?: error("minimize bounds missing") + val minimizeRect = inspector.overlayMinimizeBounds() ?: error("minimize bounds missing") assertTrue(host.handleMouseDown(minimizeRect.x + 2, minimizeRect.y + 2, MouseButton.LEFT)) assertTrue(host.handleMouseUp(minimizeRect.x + 2, minimizeRect.y + 2, MouseButton.LEFT)) assertEquals(InspectorPanelState.Minimized, inspector.panelState) @@ -312,7 +312,7 @@ class SystemOverlayInspectorNativeEntryTests { host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 40, cursorY = 30, inspectorPointerCaptured = false) host.render(ctx, 1280, 720) - val minimizeRect = inspector.debugMinimizeBounds() ?: error("minimize bounds missing") + val minimizeRect = inspector.overlayMinimizeBounds() ?: error("minimize bounds missing") assertTrue(host.handleMouseDown(minimizeRect.x + 2, minimizeRect.y + 2, MouseButton.LEFT)) assertTrue(host.handleMouseUp(minimizeRect.x + 2, minimizeRect.y + 2, MouseButton.LEFT)) assertEquals(InspectorPanelState.Minimized, inspector.panelState) @@ -369,7 +369,7 @@ class SystemOverlayInspectorNativeEntryTests { host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 90, cursorY = 90, inspectorPointerCaptured = false) host.render(ctx, 420, 280) - val contentRect = inspector.debugContentRect() + val contentRect = inspector.overlayContentRect() val wheelX = contentRect.x + 4 val wheelY = contentRect.y + 12 assertTrue(host.handleMouseWheel(wheelX, wheelY, -120)) @@ -386,7 +386,7 @@ class SystemOverlayInspectorNativeEntryTests { val afterWheel = inspector.panelScrollOffsetY assertTrue(afterWheel > 0, "expected wheel scroll > 0, actual=$afterWheel") - val thumb = inspector.debugScrollbarThumbRect() + val thumb = inspector.overlayScrollbarThumbRect() assertTrue(thumb.width > 0 && thumb.height > 0) val thumbX = thumb.x + 1 val thumbY = thumb.y + thumb.height / 2 @@ -421,9 +421,9 @@ class SystemOverlayInspectorNativeEntryTests { host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 90, cursorY = 90, inspectorPointerCaptured = false) host.render(ctx, 420, 280) - val thumb = inspector.debugScrollbarThumbRect() + val thumb = inspector.overlayScrollbarThumbRect() assertTrue(thumb.width > 0 && thumb.height > 0) - val pickToggle = inspector.debugPickToggleBounds() ?: error("pick toggle missing") + val pickToggle = inspector.overlayPickToggleBounds() ?: error("pick toggle missing") val modeBeforeRelease = inspector.mode val thumbX = thumb.x + thumb.width / 2 @@ -474,9 +474,9 @@ class SystemOverlayInspectorNativeEntryTests { host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 90, cursorY = 90, inspectorPointerCaptured = false) host.render(ctx, 420, 280) - val thumb = inspector.debugScrollbarThumbRect() + val thumb = inspector.overlayScrollbarThumbRect() assertTrue(thumb.width > 0 && thumb.height > 0) - val panelRect = inspector.debugPanelRect() ?: error("panel rect missing") + val panelRect = inspector.overlayPanelRect() ?: error("panel rect missing") val modeBeforeRelease = inspector.mode val candidatePoints = listOf( @@ -551,7 +551,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectorPointerCaptured = false ) host.render(ctx, 1280, 720) - val colorAction = inspector.debugColorPickerActionBounds(StyleProperty.BACKGROUND_COLOR) + val colorAction = inspector.overlayColorPickerActionBounds(StyleProperty.BACKGROUND_COLOR) val colorAnchor = colorAction ?: Rect(80, 80, 20, 18) val openedByClick = if (colorAction != null) { host.handleMouseDown(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) && @@ -615,7 +615,7 @@ class SystemOverlayInspectorNativeEntryTests { assertEquals("target", inspector.selectedKey) sync(revision = 2L, cursorX = 80, cursorY = 52) - val colorAction = inspector.debugColorPickerActionBounds(StyleProperty.BACKGROUND_COLOR) + val colorAction = inspector.overlayColorPickerActionBounds(StyleProperty.BACKGROUND_COLOR) val colorAnchor = colorAction ?: Rect(80, 80, 20, 18) val openedByClick = if (colorAction != null) { host.handleMouseDown(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) && @@ -682,7 +682,7 @@ class SystemOverlayInspectorNativeEntryTests { val modeBeforeDropdown = inspector.mode sync(revision = 2L, cursorX = 80, cursorY = 52) - val colorAction = inspector.debugColorPickerActionBounds(StyleProperty.BACKGROUND_COLOR) + val colorAction = inspector.overlayColorPickerActionBounds(StyleProperty.BACKGROUND_COLOR) val colorAnchor = colorAction ?: Rect(80, 80, 20, 18) val openedByClick = if (colorAction != null) { host.handleMouseDown(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) && @@ -756,7 +756,7 @@ class SystemOverlayInspectorNativeEntryTests { host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 90, cursorY = 90, inspectorPointerCaptured = false) host.render(ctx, 320, 220) - val bodyRect = inspector.debugContentRect() + val bodyRect = inspector.overlayContentRect() val inspectorNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector node missing") val bodyNode = collectNodes(inspectorNode) .firstOrNull { it.key?.toString() == "dsgl-system-inspector-body" } @@ -836,7 +836,7 @@ class SystemOverlayInspectorNativeEntryTests { host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 90, cursorY = 90, inspectorPointerCaptured = false) host.render(ctx, 320, 213) - val bodyRect = inspector.debugContentRect() + val bodyRect = inspector.overlayContentRect() val wheelX = bodyRect.x + 4 val wheelY = bodyRect.y + 12 @@ -983,7 +983,7 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 420, 280) val inspectorNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector node missing") - val bodyRect = inspector.debugContentRect() + val bodyRect = inspector.overlayContentRect() val allNodes = collectNodes(inspectorNode) val interactiveNode = allNodes.firstOrNull { node -> val key = node.key?.toString() ?: return@firstOrNull false @@ -1042,7 +1042,7 @@ class SystemOverlayInspectorNativeEntryTests { host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 90, cursorY = 90, inspectorPointerCaptured = false) host.render(ctx, 420, 280) - val bodyRect = inspector.debugContentRect() + val bodyRect = inspector.overlayContentRect() val wheelX = bodyRect.x + 4 val wheelY = bodyRect.y + 12 val before = inspector.panelScrollOffsetY @@ -1087,7 +1087,7 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 420, 280) host.paint(ctx) - val contentRect = inspector.debugContentRect() + val contentRect = inspector.overlayContentRect() val wheelX = contentRect.x + 4 val wheelY = contentRect.y + 12 @@ -1177,7 +1177,7 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 420, 280) host.paint(ctx) - val thumb = inspector.debugScrollbarThumbRect() + val thumb = inspector.overlayScrollbarThumbRect() assertTrue(thumb.width > 0 && thumb.height > 0) val dragX = thumb.x + thumb.width / 2 val dragStartY = thumb.y + thumb.height / 2 @@ -1375,7 +1375,7 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 420, 280) host.paint(ctx) - val contentRect = inspector.debugContentRect() + val contentRect = inspector.overlayContentRect() val wheelX = contentRect.x + 4 val wheelY = contentRect.y + 14 val before = inspector.panelScrollOffsetY @@ -1419,14 +1419,14 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 420, 280) host.paint(ctx) - val thumb = inspector.debugScrollbarThumbRect() + val thumb = inspector.overlayScrollbarThumbRect() assertTrue(thumb.width > 0 && thumb.height > 0) val dragX = thumb.x + thumb.width / 2 val startY = thumb.y + thumb.height / 2 assertTrue(host.handleMouseDown(dragX, startY, MouseButton.LEFT)) var previousScroll = inspector.panelScrollOffsetY - var previousThumbY = inspector.debugScrollbarThumbRect().y + var previousThumbY = inspector.overlayScrollbarThumbRect().y repeat(6) { step -> val nextY = startY + (step + 1) * 9 @@ -1441,7 +1441,7 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 420, 280) host.paint(ctx) val currentScroll = inspector.panelScrollOffsetY - val currentThumbY = inspector.debugScrollbarThumbRect().y + val currentThumbY = inspector.overlayScrollbarThumbRect().y assertTrue( currentScroll >= previousScroll, "scroll regressed: prev=$previousScroll current=$currentScroll step=$step" @@ -1456,7 +1456,7 @@ class SystemOverlayInspectorNativeEntryTests { assertTrue(host.handleMouseUp(dragX, startY + 6 * 9, MouseButton.LEFT)) val settledScroll = inspector.panelScrollOffsetY - val settledThumbY = inspector.debugScrollbarThumbRect().y + val settledThumbY = inspector.overlayScrollbarThumbRect().y repeat(6) { idx -> host.syncFrame( @@ -1469,7 +1469,7 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 420, 280) host.paint(ctx) assertEquals(settledScroll, inspector.panelScrollOffsetY) - assertEquals(settledThumbY, inspector.debugScrollbarThumbRect().y) + assertEquals(settledThumbY, inspector.overlayScrollbarThumbRect().y) } } @@ -1498,14 +1498,14 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 420, 280) host.paint(ctx) - val thumb = inspector.debugScrollbarThumbRect() + val thumb = inspector.overlayScrollbarThumbRect() assertTrue(thumb.width > 0 && thumb.height > 0) val dragX = thumb.x + thumb.width / 2 val startY = thumb.y + thumb.height / 2 assertTrue(host.handleMouseDown(dragX, startY, MouseButton.LEFT)) var previousScroll = inspector.panelScrollOffsetY - var previousThumbY = inspector.debugScrollbarThumbRect().y + var previousThumbY = inspector.overlayScrollbarThumbRect().y repeat(7) { step -> val nextY = startY + (step + 1) * 120 assertTrue(host.handleMouseMove(dragX, nextY)) @@ -1519,7 +1519,7 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 420, 280) host.paint(ctx) val currentScroll = inspector.panelScrollOffsetY - val currentThumbY = inspector.debugScrollbarThumbRect().y + val currentThumbY = inspector.overlayScrollbarThumbRect().y assertTrue( currentScroll >= previousScroll, "scroll regressed: prev=$previousScroll current=$currentScroll step=$step" @@ -1533,7 +1533,7 @@ class SystemOverlayInspectorNativeEntryTests { } val settledScroll = inspector.panelScrollOffsetY - val settledThumbY = inspector.debugScrollbarThumbRect().y + val settledThumbY = inspector.overlayScrollbarThumbRect().y repeat(8) { idx -> val boundaryY = startY + 2000 assertTrue(host.handleMouseMove(dragX, boundaryY)) @@ -1547,7 +1547,7 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 420, 280) host.paint(ctx) assertEquals(settledScroll, inspector.panelScrollOffsetY) - assertEquals(settledThumbY, inspector.debugScrollbarThumbRect().y) + assertEquals(settledThumbY, inspector.overlayScrollbarThumbRect().y) } assertTrue(host.handleMouseUp(dragX, startY + 2000, MouseButton.LEFT)) @@ -1557,3 +1557,4 @@ class SystemOverlayInspectorNativeEntryTests { + diff --git a/mc1710-demo/src/test/kotlin/org/dreamfinity/dsgl/mc1710/demo/sections/PositionedLayoutStickyDemoIntegrationTests.kt b/mc1710-demo/src/test/kotlin/org/dreamfinity/dsgl/mc1710/demo/sections/PositionedLayoutStickyDemoIntegrationTests.kt index 644c14d..b0986a3 100644 --- a/mc1710-demo/src/test/kotlin/org/dreamfinity/dsgl/mc1710/demo/sections/PositionedLayoutStickyDemoIntegrationTests.kt +++ b/mc1710-demo/src/test/kotlin/org/dreamfinity/dsgl/mc1710/demo/sections/PositionedLayoutStickyDemoIntegrationTests.kt @@ -245,9 +245,9 @@ class PositionedLayoutStickyDemoIntegrationTests { } private fun inspectorHoveredBorderRect(inspector: InspectorController): Rect? { - val debugHoveredHighlight = findMethodByNameAndArity(inspector.javaClass, "debugHoveredHighlight", 0) - debugHoveredHighlight.isAccessible = true - val snapshot = debugHoveredHighlight.invoke(inspector) ?: return null + val highlightMethod = findMethodByNameAndArity(inspector.javaClass, "overlayHoveredHighlight", 0) + highlightMethod.isAccessible = true + val snapshot = highlightMethod.invoke(inspector) ?: return null val borderRectField = findField(snapshot.javaClass, "borderRect") borderRectField.isAccessible = true return borderRectField.get(snapshot) as? Rect From d3ba2af247c598b25e82b0cffc57318fcbf405e6 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sun, 19 Apr 2026 18:11:37 +0300 Subject: [PATCH 09/78] wrapping inspector body in a container; --- .../internal/SystemInspectorOverlayNode.kt | 56 ++++++++++++------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt index b0b3998..a785edf 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt @@ -4,6 +4,7 @@ import org.dreamfinity.dsgl.core.dsl.UiScope import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.ScrollSessionSnapshot import org.dreamfinity.dsgl.core.dom.applyParent +import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.elements.TextInputNode import org.dreamfinity.dsgl.core.dom.layout.Border import org.dreamfinity.dsgl.core.dom.layout.Rect @@ -61,6 +62,10 @@ internal class SystemInspectorOverlayNode( private val persistedDropdownScrollSession: MutableMap = LinkedHashMap() private var activeDomDropdown: ActiveDomDropdown? = null private val panelNode: DOMNode = overlayPanel.node().applyParent(this) + private val inspectorBodyNode: ContainerNode = ContainerNode( + stackLayout = false, + key = "dsgl-system-inspector-body" + ).also(overlayPanel::setBodyContent) private var minimizedChipDragSession: MinimizedChipDragSession? = null private data class ActiveDomDropdown( @@ -214,6 +219,7 @@ internal class SystemInspectorOverlayNode( val snapshot = controller.buildDomSnapshot(viewportRect.width, viewportRect.height) if (snapshot == null) { clearTree() + clearInspectorBodySubtree() panelNode.render(ctx, 0, 0, 0, 0) children.remove(panelNode) panelNode.parent = null @@ -293,6 +299,7 @@ internal class SystemInspectorOverlayNode( private fun renderMinimized(ctx: UiMeasureContext, snapshot: InspectorDomSnapshot) { closeActiveDomDropdown() + clearInspectorBodySubtree() panelNode.render(ctx, 0, 0, 0, 0) val scope = UiScope(this) @@ -362,7 +369,15 @@ internal class SystemInspectorOverlayNode( renderHighlights(scope, ctx) renderPanelOccluder(scope, ctx, panelRect) + clearInspectorBodySubtree() + inspectorBodyNode.backgroundColor = 0x18212C39 + inspectorBodyNode.overflow = Overflow.Hidden + inspectorBodyNode.overflowX = Overflow.Hidden + inspectorBodyNode.overflowY = Overflow.Auto panelNode.render(ctx, viewportRect.x, viewportRect.y, viewportRect.width, viewportRect.height) + if (overlayPanel.bodyRect() == null) { + renderNode(ctx, inspectorBodyNode, bodyRect) + } val pickRect = controller.overlayPickToggleBounds() ?: Rect(panelRect.x + panelRect.width - 264, panelRect.y + 8, 160, 36) @@ -393,18 +408,7 @@ internal class SystemInspectorOverlayNode( } renderNode(ctx, minimizeButton, minimizeRect) - val body = scope.div({ - key = "dsgl-system-inspector-body" - style = { - display = Display.Block - } - }) - body.backgroundColor = 0x18212C39 - body.overflow = Overflow.Hidden - body.overflowX = Overflow.Hidden - body.overflowY = Overflow.Auto - val bodyScope = UiScope(body) - renderNode(ctx, body, bodyRect) + val bodyScope = UiScope(inspectorBodyNode) val lineHeightPx = 32 val rowHeightPx = 34 @@ -485,7 +489,7 @@ internal class SystemInspectorOverlayNode( val styleRows = controller.overlayStyleEditorRows() reconcileActiveDomDropdown(styleRows) - renderStyleEditorRows(bodyScope, body, ctx, bodyScrollY, styleRows) + renderStyleEditorRows(bodyScope, inspectorBodyNode, ctx, bodyScrollY, styleRows) y += snapshot.styleEditorHeight snapshot.styleLines.forEachIndexed { index, line -> @@ -507,10 +511,10 @@ internal class SystemInspectorOverlayNode( } renderDropdowns(scope, ctx, styleRows, bodyScrollY, viewportWidth, viewportHeight) - body.restoreScrollSessionSnapshot(persistedBodyScrollSession) - val bodyState = body.scrollContainerState() - persistedBodyScrollSession = body.captureScrollSessionSnapshot() - val bodyScrollbarVisual = body.debugScrollbarVisualState().vertical + inspectorBodyNode.restoreScrollSessionSnapshot(persistedBodyScrollSession) + val bodyState = inspectorBodyNode.scrollContainerState() + persistedBodyScrollSession = inspectorBodyNode.captureScrollSessionSnapshot() + val bodyScrollbarVisual = inspectorBodyNode.debugScrollbarVisualState().vertical controller.onNativeDomBodyScrollState( scrollY = bodyState.scrollY, trackRect = bodyScrollbarVisual?.trackRect, @@ -1116,8 +1120,12 @@ internal class SystemInspectorOverlayNode( } private fun capturePersistedScrollStateFromCurrentTree() { - findNodeByKey("dsgl-system-inspector-body")?.let { bodyNode -> - persistedBodyScrollSession = bodyNode.captureScrollSessionSnapshot() + if (controller.panelState == InspectorPanelState.Expanded) { + findNodeByKey("dsgl-system-inspector-body")?.let { bodyNode -> + if (bodyNode.bounds.width > 0 && bodyNode.bounds.height > 0) { + persistedBodyScrollSession = bodyNode.captureScrollSessionSnapshot() + } + } } val nextDropdownScroll = LinkedHashMap() collectNodes(this).forEach { node -> @@ -1129,6 +1137,16 @@ internal class SystemInspectorOverlayNode( persistedDropdownScrollSession.putAll(nextDropdownScroll) } + private fun clearInspectorBodySubtree() { + EventBus.run { + inspectorBodyNode.children.toList().forEach { child -> + child.clearListenersDeep() + child.parent = null + } + } + inspectorBodyNode.children.clear() + } + private fun findNodeByKey(targetKey: String): DOMNode? { return collectNodes(this).firstOrNull { it.key == targetKey } } From 2c3f75f06bbe2afba2de16dfd7b3dfba3dc9e1df Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sun, 19 Apr 2026 21:01:05 +0300 Subject: [PATCH 10/78] fixing bug with a non-smooth inspector moving; --- .../internal/SystemInspectorOverlayNode.kt | 67 ++++++++----------- .../core/overlay/system/SystemOverlayHost.kt | 17 +++-- .../InspectorDragScrollDomMigrationTests.kt | 49 ++++++++++++++ .../SystemOverlayInspectorNativeEntryTests.kt | 32 +++++++++ 4 files changed, 120 insertions(+), 45 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt index a785edf..04d5956 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt @@ -1,10 +1,9 @@ -package org.dreamfinity.dsgl.core.inspector.internal +package org.dreamfinity.dsgl.core.inspector.internal import org.dreamfinity.dsgl.core.dsl.UiScope import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.ScrollSessionSnapshot import org.dreamfinity.dsgl.core.dom.applyParent -import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.elements.TextInputNode import org.dreamfinity.dsgl.core.dom.layout.Border import org.dreamfinity.dsgl.core.dom.layout.Rect @@ -61,11 +60,8 @@ internal class SystemInspectorOverlayNode( private var persistedBodyScrollSession: ScrollSessionSnapshot? = null private val persistedDropdownScrollSession: MutableMap = LinkedHashMap() private var activeDomDropdown: ActiveDomDropdown? = null + private var overlayPanelDragUpdatedByDomInput: Boolean = false private val panelNode: DOMNode = overlayPanel.node().applyParent(this) - private val inspectorBodyNode: ContainerNode = ContainerNode( - stackLayout = false, - key = "dsgl-system-inspector-body" - ).also(overlayPanel::setBodyContent) private var minimizedChipDragSession: MinimizedChipDragSession? = null private data class ActiveDomDropdown( @@ -162,6 +158,7 @@ internal class SystemInspectorOverlayNode( controller.onOverlayPanelRectChanged(rect, lastViewportWidth, lastViewportHeight) } if (handled) { + overlayPanelDragUpdatedByDomInput = true controller.onOverlayPanelPointerCaptureChanged(true) } return handled @@ -178,11 +175,18 @@ internal class SystemInspectorOverlayNode( controller.onOverlayPanelRectChanged(rect, lastViewportWidth, lastViewportHeight) } if (handled) { + overlayPanelDragUpdatedByDomInput = false controller.onOverlayPanelPointerCaptureChanged(false) } return handled } + internal fun consumeOverlayPanelDomDragUpdate(): Boolean { + val consumed = overlayPanelDragUpdatedByDomInput + overlayPanelDragUpdatedByDomInput = false + return consumed + } + fun bindInspectedTree(root: DOMNode?, layoutRevision: Long) { inspectedRoot = root inspectedLayoutRevision = layoutRevision @@ -219,7 +223,6 @@ internal class SystemInspectorOverlayNode( val snapshot = controller.buildDomSnapshot(viewportRect.width, viewportRect.height) if (snapshot == null) { clearTree() - clearInspectorBodySubtree() panelNode.render(ctx, 0, 0, 0, 0) children.remove(panelNode) panelNode.parent = null @@ -299,7 +302,6 @@ internal class SystemInspectorOverlayNode( private fun renderMinimized(ctx: UiMeasureContext, snapshot: InspectorDomSnapshot) { closeActiveDomDropdown() - clearInspectorBodySubtree() panelNode.render(ctx, 0, 0, 0, 0) val scope = UiScope(this) @@ -369,15 +371,7 @@ internal class SystemInspectorOverlayNode( renderHighlights(scope, ctx) renderPanelOccluder(scope, ctx, panelRect) - clearInspectorBodySubtree() - inspectorBodyNode.backgroundColor = 0x18212C39 - inspectorBodyNode.overflow = Overflow.Hidden - inspectorBodyNode.overflowX = Overflow.Hidden - inspectorBodyNode.overflowY = Overflow.Auto panelNode.render(ctx, viewportRect.x, viewportRect.y, viewportRect.width, viewportRect.height) - if (overlayPanel.bodyRect() == null) { - renderNode(ctx, inspectorBodyNode, bodyRect) - } val pickRect = controller.overlayPickToggleBounds() ?: Rect(panelRect.x + panelRect.width - 264, panelRect.y + 8, 160, 36) @@ -408,7 +402,18 @@ internal class SystemInspectorOverlayNode( } renderNode(ctx, minimizeButton, minimizeRect) - val bodyScope = UiScope(inspectorBodyNode) + val body = scope.div({ + key = "dsgl-system-inspector-body" + style = { + display = Display.Block + } + }) + body.backgroundColor = 0x18212C39 + body.overflow = Overflow.Hidden + body.overflowX = Overflow.Hidden + body.overflowY = Overflow.Auto + val bodyScope = UiScope(body) + renderNode(ctx, body, bodyRect) val lineHeightPx = 32 val rowHeightPx = 34 @@ -489,7 +494,7 @@ internal class SystemInspectorOverlayNode( val styleRows = controller.overlayStyleEditorRows() reconcileActiveDomDropdown(styleRows) - renderStyleEditorRows(bodyScope, inspectorBodyNode, ctx, bodyScrollY, styleRows) + renderStyleEditorRows(bodyScope, body, ctx, bodyScrollY, styleRows) y += snapshot.styleEditorHeight snapshot.styleLines.forEachIndexed { index, line -> @@ -511,10 +516,10 @@ internal class SystemInspectorOverlayNode( } renderDropdowns(scope, ctx, styleRows, bodyScrollY, viewportWidth, viewportHeight) - inspectorBodyNode.restoreScrollSessionSnapshot(persistedBodyScrollSession) - val bodyState = inspectorBodyNode.scrollContainerState() - persistedBodyScrollSession = inspectorBodyNode.captureScrollSessionSnapshot() - val bodyScrollbarVisual = inspectorBodyNode.debugScrollbarVisualState().vertical + body.restoreScrollSessionSnapshot(persistedBodyScrollSession) + val bodyState = body.scrollContainerState() + persistedBodyScrollSession = body.captureScrollSessionSnapshot() + val bodyScrollbarVisual = body.debugScrollbarVisualState().vertical controller.onNativeDomBodyScrollState( scrollY = bodyState.scrollY, trackRect = bodyScrollbarVisual?.trackRect, @@ -1120,12 +1125,8 @@ internal class SystemInspectorOverlayNode( } private fun capturePersistedScrollStateFromCurrentTree() { - if (controller.panelState == InspectorPanelState.Expanded) { - findNodeByKey("dsgl-system-inspector-body")?.let { bodyNode -> - if (bodyNode.bounds.width > 0 && bodyNode.bounds.height > 0) { - persistedBodyScrollSession = bodyNode.captureScrollSessionSnapshot() - } - } + findNodeByKey("dsgl-system-inspector-body")?.let { bodyNode -> + persistedBodyScrollSession = bodyNode.captureScrollSessionSnapshot() } val nextDropdownScroll = LinkedHashMap() collectNodes(this).forEach { node -> @@ -1137,16 +1138,6 @@ internal class SystemInspectorOverlayNode( persistedDropdownScrollSession.putAll(nextDropdownScroll) } - private fun clearInspectorBodySubtree() { - EventBus.run { - inspectorBodyNode.children.toList().forEach { child -> - child.clearListenersDeep() - child.parent = null - } - } - inspectorBodyNode.children.clear() - } - private fun findNodeByKey(targetKey: String): DOMNode? { return collectNodes(this).firstOrNull { it.key == targetKey } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt index a698eb9..79d512e 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt @@ -314,13 +314,16 @@ class SystemOverlayHost( state.panelState.show() overlayPanel.syncPanelRect(state.panelState.currentRectOrNull()) } - overlayPanel.handleMouseMove( - mouseX = frame.cursorX, - mouseY = frame.cursorY, - viewportWidth = viewportWidth, - viewportHeight = viewportHeight - ) { rect -> - inspectorController.onOverlayPanelRectChanged(rect, viewportWidth, viewportHeight) + val dragUpdatedByDomInput = node.consumeOverlayPanelDomDragUpdate() + if (!dragUpdatedByDomInput) { + overlayPanel.handleMouseMove( + mouseX = frame.cursorX, + mouseY = frame.cursorY, + viewportWidth = viewportWidth, + viewportHeight = viewportHeight + ) { rect -> + inspectorController.onOverlayPanelRectChanged(rect, viewportWidth, viewportHeight) + } } } else { state.panelState.hide() diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt index 9caafa5..eba26a1 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt @@ -70,6 +70,55 @@ class InspectorDragScrollDomMigrationTests { assertFalse(fixture.host.debugEntryState(SystemOverlayEntryId.Inspector)?.dragSession?.active == true) } + @Test + fun `inspector panel drag stays monotonic when sync cursor lags behind dom drag updates`() { + val fixture = openInspectorAndSelectTarget(withManyChildren = true) + + val before = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") + val downX = before.x + 18 + val downY = before.y + 14 + val dragX = downX + 92 + val dragY = downY + 34 + + assertTrue(fixture.host.handleMouseDown(downX, downY, MouseButton.LEFT)) + syncAndRender(fixture, downX, downY) + + assertTrue(fixture.host.handleMouseMove(dragX, dragY)) + val afterDomDrag = fixture.inspector.overlayPanelRect() ?: error("expected moved panel rect") + assertTrue( + afterDomDrag.x > before.x || afterDomDrag.y > before.y, + "expected drag move to advance panel: before=$before afterDomDrag=$afterDomDrag" + ) + + syncAndRender(fixture, downX, downY) + val afterStaleSync = fixture.inspector.overlayPanelRect() ?: error("expected panel rect after stale sync") + + if (afterDomDrag.x > before.x) { + assertTrue( + afterStaleSync.x >= afterDomDrag.x, + "stale sync cursor regressed panel x: before=$before dom=$afterDomDrag stale=$afterStaleSync" + ) + } + if (afterDomDrag.y > before.y) { + assertTrue( + afterStaleSync.y >= afterDomDrag.y, + "stale sync cursor regressed panel y: before=$before dom=$afterDomDrag stale=$afterStaleSync" + ) + } + + assertTrue(fixture.host.handleMouseMove(dragX + 28, dragY + 16)) + syncAndRender(fixture, dragX + 28, dragY + 16) + val afterNextMove = fixture.inspector.overlayPanelRect() ?: error("expected panel rect after next drag move") + assertTrue( + afterNextMove.x >= afterStaleSync.x && afterNextMove.y >= afterStaleSync.y, + "expected monotonic drag progression across sync/render cycle: stale=$afterStaleSync next=$afterNextMove" + ) + + assertTrue(fixture.host.handleMouseUp(dragX + 28, dragY + 16, MouseButton.LEFT)) + syncAndRender(fixture, dragX + 28, dragY + 16) + assertFalse(fixture.host.debugEntryState(SystemOverlayEntryId.Inspector)?.dragSession?.active == true) + } + @Test fun `inspector wheel body scroll is dom-first and not controller-authoritative`() { val fixture = openInspectorAndSelectTarget(withManyChildren = true) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt index afc8f19..fd3e123 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt @@ -813,6 +813,38 @@ class SystemOverlayInspectorNativeEntryTests { }) } + @Test + fun `inspector expanded body renders baseline info text`() { + val inspector = InspectorController() + val host = SystemOverlayHost(inspector) + inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + val root = inspectedRoot() + + inspector.toggle() + host.onInputFrame(1280, 720) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) + host.render(ctx, 1280, 720) + val commands = host.paint(ctx) + + val bodyRect = inspector.overlayContentRect() + assertTrue(bodyRect.width > 0 && bodyRect.height > 0) + val baselineInfoRendered = commands.any { command -> + command is RenderCommand.DrawText && + command.text.contains("F12 toggle") && + command.x >= bodyRect.x && + command.x <= bodyRect.x + bodyRect.width && + command.y >= bodyRect.y && + command.y <= bodyRect.y + bodyRect.height + } + assertTrue(baselineInfoRendered) + } + @Test fun `inspector clipped body blocks hidden row input and accepts visible portion`() { val inspector = InspectorController() From 0ec17cbf1b388bcf2857be9bf3c3145b00e4b845 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sun, 19 Apr 2026 22:25:57 +0300 Subject: [PATCH 11/78] extracting render flow into separate functions; --- .../internal/SystemInspectorOverlayNode.kt | 238 +++++++++++------- 1 file changed, 141 insertions(+), 97 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt index 04d5956..46d837a 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt @@ -306,58 +306,7 @@ internal class SystemInspectorOverlayNode( val scope = UiScope(this) renderHighlights(scope, ctx) - val chip = scope.div({ - key = "dsgl-system-inspector-chip" - style = { - display = Display.Block - } - }) - chip.backgroundColor = 0xDD1A202A.toInt() - chip.border = Border.all(1, 0xCC4F6076.toInt()) - chip.onMouseDown = { event -> - if (event.mouseButton == MouseButton.LEFT) { - startMinimizedChipDrag(snapshot.panelRect, event.mouseX, event.mouseY) - event.cancelled = true - } - } - chip.onMouseDrag = { event -> - val currentX = event.lastMouseX + event.dx - val currentY = event.lastMouseY + event.dy - updateMinimizedChipDragPointer(currentX, currentY) - event.cancelled = true - } - chip.onMouseUp = { event -> - if (event.mouseButton == MouseButton.LEFT) { - endMinimizedChipDrag(event.mouseX, event.mouseY) - event.cancelled = true - } - } - renderNode(ctx, chip, snapshot.panelRect) - - val compactLineHeight = 20 - var lineY = snapshot.panelRect.y + ((snapshot.panelRect.height - compactLineHeight * snapshot.minimizedLines.size) / 2) - snapshot.minimizedLines.forEachIndexed { index, line -> - val lineNode = scope.text(props = { - key = "dsgl-system-inspector-chip-line-$index" - value = line - style = { - textWrap = TextWrap.NoWrap - } - }) - lineNode.color = 0xFFE6EDF6.toInt() - lineNode.fontSize = 14 - renderNode( - ctx, - lineNode, - Rect( - snapshot.panelRect.x + 8, - lineY, - (snapshot.panelRect.width - 16).coerceAtLeast(1), - compactLineHeight - ) - ) - lineY += compactLineHeight - } + renderMinimizedChip(scope, ctx, snapshot) } private fun renderExpanded(ctx: UiMeasureContext, snapshot: InspectorDomSnapshot, viewportRect: Rect) { @@ -373,34 +322,7 @@ internal class SystemInspectorOverlayNode( renderPanelOccluder(scope, ctx, panelRect) panelNode.render(ctx, viewportRect.x, viewportRect.y, viewportRect.width, viewportRect.height) - val pickRect = controller.overlayPickToggleBounds() - ?: Rect(panelRect.x + panelRect.width - 264, panelRect.y + 8, 160, 36) - val minimizeRect = controller.overlayMinimizeBounds() - ?: Rect(panelRect.x + panelRect.width - 96, panelRect.y + 8, 86, 36) - - val pickButton = scope.button("Select Element", { - key = "dsgl-system-inspector-pick-toggle" - }) - pickButton.backgroundColor = 0x3346596E - pickButton.border = Border.all(1, 0x775E738C) - pickButton.textColor = 0xFFE6EDF6.toInt() - pickButton.fontSize = 18 - pickButton.onClick { - controller.onPickTogglePressed() - } - renderNode(ctx, pickButton, pickRect) - - val minimizeButton = scope.button("Minimize", { - key = "dsgl-system-inspector-minimize" - }) - minimizeButton.backgroundColor = 0x3346596E - minimizeButton.border = Border.all(1, 0x775E738C) - minimizeButton.textColor = 0xFFE6EDF6.toInt() - minimizeButton.fontSize = 18 - minimizeButton.onClick { - controller.onPanelMinimizeTogglePressed() - } - renderNode(ctx, minimizeButton, minimizeRect) + renderExpandedChrome(scope, ctx, panelRect) val body = scope.div({ key = "dsgl-system-inspector-body" @@ -422,23 +344,15 @@ internal class SystemInspectorOverlayNode( val bodyScrollY = persistedBodyScrollSession?.resolvedY?.coerceAtLeast(0) ?: 0 var y = bodyRect.y + 2 - bodyScrollY - snapshot.infoLines.forEachIndexed { index, line -> - val lineNode = bodyScope.text(props = { - key = "dsgl-system-inspector-info-line-$index" - value = line - style = { - textWrap = TextWrap.NoWrap - } - }) - lineNode.color = 0xFFDCE5EF.toInt() - lineNode.fontSize = 24 - renderNode( - ctx, - lineNode, - Rect(contentX, y, contentW, lineHeightPx), - ) - y += lineHeightPx - } + y = renderBodyInfoLines( + scope = bodyScope, + ctx = ctx, + infoLines = snapshot.infoLines, + contentX = contentX, + contentW = contentW, + startY = y, + lineHeightPx = lineHeightPx + ) snapshot.parentLabel?.let { label -> val parentButton = bodyScope.button(label, { @@ -529,6 +443,136 @@ internal class SystemInspectorOverlayNode( renderTooltip(scope, ctx, "dsgl-system-inspector-cursor-tooltip", controller.overlayCursorTooltip(), 0xDD11151A.toInt(), 0xCC3F4A57.toInt()) } + private fun renderMinimizedChip( + scope: UiScope, + ctx: UiMeasureContext, + snapshot: InspectorDomSnapshot + ) { + val chip = scope.div({ + key = "dsgl-system-inspector-chip" + style = { + display = Display.Block + } + }) + chip.backgroundColor = 0xDD1A202A.toInt() + chip.border = Border.all(1, 0xCC4F6076.toInt()) + chip.onMouseDown = { event -> + if (event.mouseButton == MouseButton.LEFT) { + startMinimizedChipDrag(snapshot.panelRect, event.mouseX, event.mouseY) + event.cancelled = true + } + } + chip.onMouseDrag = { event -> + val currentX = event.lastMouseX + event.dx + val currentY = event.lastMouseY + event.dy + updateMinimizedChipDragPointer(currentX, currentY) + event.cancelled = true + } + chip.onMouseUp = { event -> + if (event.mouseButton == MouseButton.LEFT) { + endMinimizedChipDrag(event.mouseX, event.mouseY) + event.cancelled = true + } + } + renderNode(ctx, chip, snapshot.panelRect) + + val compactLineHeight = 20 + var lineY = snapshot.panelRect.y + ((snapshot.panelRect.height - compactLineHeight * snapshot.minimizedLines.size) / 2) + snapshot.minimizedLines.forEachIndexed { index, line -> + val lineNode = scope.text(props = { + key = "dsgl-system-inspector-chip-line-$index" + value = line + style = { + textWrap = TextWrap.NoWrap + } + }) + lineNode.color = 0xFFE6EDF6.toInt() + lineNode.fontSize = 14 + renderNode( + ctx, + lineNode, + Rect( + snapshot.panelRect.x + 8, + lineY, + (snapshot.panelRect.width - 16).coerceAtLeast(1), + compactLineHeight + ) + ) + lineY += compactLineHeight + } + } + + private fun renderExpandedChrome( + scope: UiScope, + ctx: UiMeasureContext, + panelRect: Rect + ) { + val pickRect = controller.overlayPickToggleBounds() + ?: Rect(panelRect.x + panelRect.width - 264, panelRect.y + 8, 160, 36) + val minimizeRect = controller.overlayMinimizeBounds() + ?: Rect(panelRect.x + panelRect.width - 96, panelRect.y + 8, 86, 36) + renderPickToggleButton(scope, ctx, pickRect) + renderMinimizeButton(scope, ctx, minimizeRect) + } + + private fun renderPickToggleButton(scope: UiScope, ctx: UiMeasureContext, rect: Rect) { + val pickButton = scope.button("Select Element", { + key = "dsgl-system-inspector-pick-toggle" + }) + pickButton.backgroundColor = 0x3346596E + pickButton.border = Border.all(1, 0x775E738C) + pickButton.textColor = 0xFFE6EDF6.toInt() + pickButton.fontSize = 18 + pickButton.onClick { + controller.onPickTogglePressed() + } + renderNode(ctx, pickButton, rect) + } + + private fun renderMinimizeButton(scope: UiScope, ctx: UiMeasureContext, rect: Rect) { + val minimizeButton = scope.button("Minimize", { + key = "dsgl-system-inspector-minimize" + }) + minimizeButton.backgroundColor = 0x3346596E + minimizeButton.border = Border.all(1, 0x775E738C) + minimizeButton.textColor = 0xFFE6EDF6.toInt() + minimizeButton.fontSize = 18 + minimizeButton.onClick { + controller.onPanelMinimizeTogglePressed() + } + renderNode(ctx, minimizeButton, rect) + } + + private fun renderBodyInfoLines( + scope: UiScope, + ctx: UiMeasureContext, + infoLines: List, + contentX: Int, + contentW: Int, + startY: Int, + lineHeightPx: Int + ): Int { + var y = startY + infoLines.forEachIndexed { index, line -> + val lineNode = scope.text(props = { + key = "dsgl-system-inspector-info-line-$index" + value = line + style = { + textWrap = TextWrap.NoWrap + } + }) + lineNode.color = 0xFFDCE5EF.toInt() + lineNode.fontSize = 24 + renderNode( + ctx, + lineNode, + Rect(contentX, y, contentW, lineHeightPx), + ) + y += lineHeightPx + } + return y + } + private fun renderPanelOccluder(scope: UiScope, ctx: UiMeasureContext, panelRect: Rect) { val occluder = scope.div({ key = "dsgl-system-inspector-panel-occluder" From c455155d9b74964122d895e23b8ce74429a15968 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sun, 19 Apr 2026 22:37:42 +0300 Subject: [PATCH 12/78] extracting render flow into separate functions; --- .../internal/SystemInspectorOverlayNode.kt | 217 ++++++++++++------ 1 file changed, 150 insertions(+), 67 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt index 46d837a..6770fa4 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt @@ -354,56 +354,31 @@ internal class SystemInspectorOverlayNode( lineHeightPx = lineHeightPx ) - snapshot.parentLabel?.let { label -> - val parentButton = bodyScope.button(label, { - key = "dsgl-system-inspector-parent-row" - }) - parentButton.backgroundColor = 0x1E263241 - parentButton.border = Border.all(1, 0x55394654) - parentButton.textColor = 0xFFDCE5EF.toInt() - parentButton.fontSize = 22 - parentButton.onClick { - controller.onSelectParentPressed() - } - renderNode( - ctx, - parentButton, - Rect(contentX, y, contentW, rowHeightPx), - ) - y += rowHeightPx + 2 - } - - snapshot.childLabels.forEachIndexed { index, label -> - val childButton = bodyScope.button(label, { - key = "dsgl-system-inspector-child-row-$index" - }) - childButton.backgroundColor = 0x1E263241 - childButton.border = Border.all(1, 0x55394654) - childButton.textColor = 0xFFDCE5EF.toInt() - childButton.fontSize = 22 - childButton.onClick { - controller.onSelectChildPressed(index) - } - renderNode( - ctx, - childButton, - Rect(contentX, y, contentW, rowHeightPx), - ) - y += rowHeightPx + 2 - } - val styleEditorHeader = bodyScope.text(props = { - key = "dsgl-system-inspector-editor-header" - value = "Style editor (live overrides):" - style = { - textWrap = TextWrap.NoWrap - } - }) - styleEditorHeader.color = 0xFFDCE5EF.toInt() - styleEditorHeader.fontSize = 24 - renderNode( - ctx, - styleEditorHeader, - Rect(contentX, y, contentW, lineHeightPx), + y = renderParentRow( + scope = bodyScope, + ctx = ctx, + parentLabel = snapshot.parentLabel, + contentX = contentX, + contentW = contentW, + startY = y, + rowHeightPx = rowHeightPx + ) + y = renderChildRows( + scope = bodyScope, + ctx = ctx, + childLabels = snapshot.childLabels, + contentX = contentX, + contentW = contentW, + startY = y, + rowHeightPx = rowHeightPx + ) + renderStyleEditorHeading( + scope = bodyScope, + ctx = ctx, + contentX = contentX, + contentW = contentW, + y = y, + lineHeightPx = lineHeightPx ) val styleRows = controller.overlayStyleEditorRows() @@ -411,23 +386,15 @@ internal class SystemInspectorOverlayNode( renderStyleEditorRows(bodyScope, body, ctx, bodyScrollY, styleRows) y += snapshot.styleEditorHeight - snapshot.styleLines.forEachIndexed { index, line -> - val lineNode = bodyScope.text(props = { - key = "dsgl-system-inspector-style-line-$index" - value = line - style = { - textWrap = TextWrap.NoWrap - } - }) - lineNode.color = 0xFFDCE5EF.toInt() - lineNode.fontSize = 24 - renderNode( - ctx, - lineNode, - Rect(contentX, y, contentW, lineHeightPx), - ) - y += lineHeightPx - } + y = renderComputedStyleLines( + scope = bodyScope, + ctx = ctx, + styleLines = snapshot.styleLines, + contentX = contentX, + contentW = contentW, + startY = y, + lineHeightPx = lineHeightPx + ) renderDropdowns(scope, ctx, styleRows, bodyScrollY, viewportWidth, viewportHeight) body.restoreScrollSessionSnapshot(persistedBodyScrollSession) @@ -573,6 +540,122 @@ internal class SystemInspectorOverlayNode( return y } + private fun renderParentRow( + scope: UiScope, + ctx: UiMeasureContext, + parentLabel: String?, + contentX: Int, + contentW: Int, + startY: Int, + rowHeightPx: Int + ): Int { + var y = startY + parentLabel?.let { label -> + val parentButton = scope.button(label, { + key = "dsgl-system-inspector-parent-row" + }) + parentButton.backgroundColor = 0x1E263241 + parentButton.border = Border.all(1, 0x55394654) + parentButton.textColor = 0xFFDCE5EF.toInt() + parentButton.fontSize = 22 + parentButton.onClick { + controller.onSelectParentPressed() + } + renderNode( + ctx, + parentButton, + Rect(contentX, y, contentW, rowHeightPx), + ) + y += rowHeightPx + 2 + } + return y + } + + private fun renderChildRows( + scope: UiScope, + ctx: UiMeasureContext, + childLabels: List, + contentX: Int, + contentW: Int, + startY: Int, + rowHeightPx: Int + ): Int { + var y = startY + childLabels.forEachIndexed { index, label -> + val childButton = scope.button(label, { + key = "dsgl-system-inspector-child-row-$index" + }) + childButton.backgroundColor = 0x1E263241 + childButton.border = Border.all(1, 0x55394654) + childButton.textColor = 0xFFDCE5EF.toInt() + childButton.fontSize = 22 + childButton.onClick { + controller.onSelectChildPressed(index) + } + renderNode( + ctx, + childButton, + Rect(contentX, y, contentW, rowHeightPx), + ) + y += rowHeightPx + 2 + } + return y + } + + private fun renderStyleEditorHeading( + scope: UiScope, + ctx: UiMeasureContext, + contentX: Int, + contentW: Int, + y: Int, + lineHeightPx: Int + ) { + val styleEditorHeader = scope.text(props = { + key = "dsgl-system-inspector-editor-header" + value = "Style editor (live overrides):" + style = { + textWrap = TextWrap.NoWrap + } + }) + styleEditorHeader.color = 0xFFDCE5EF.toInt() + styleEditorHeader.fontSize = 24 + renderNode( + ctx, + styleEditorHeader, + Rect(contentX, y, contentW, lineHeightPx), + ) + } + + private fun renderComputedStyleLines( + scope: UiScope, + ctx: UiMeasureContext, + styleLines: List, + contentX: Int, + contentW: Int, + startY: Int, + lineHeightPx: Int + ): Int { + var y = startY + styleLines.forEachIndexed { index, line -> + val lineNode = scope.text(props = { + key = "dsgl-system-inspector-style-line-$index" + value = line + style = { + textWrap = TextWrap.NoWrap + } + }) + lineNode.color = 0xFFDCE5EF.toInt() + lineNode.fontSize = 24 + renderNode( + ctx, + lineNode, + Rect(contentX, y, contentW, lineHeightPx), + ) + y += lineHeightPx + } + return y + } + private fun renderPanelOccluder(scope: UiScope, ctx: UiMeasureContext, panelRect: Rect) { val occluder = scope.div({ key = "dsgl-system-inspector-panel-occluder" From 11edc524f1a1f39eafc81a5d40b80e9dcb4de0ff Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sun, 19 Apr 2026 22:49:04 +0300 Subject: [PATCH 13/78] extracting some helpers into separate functions; --- .../internal/SystemInspectorOverlayNode.kt | 348 +++++++++++------- 1 file changed, 207 insertions(+), 141 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt index 6770fa4..581a5bc 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt @@ -716,161 +716,227 @@ internal class SystemInspectorOverlayNode( ) { rows.forEachIndexed { index, row -> val rowRect = translateRectY(row.rowRect, -bodyScrollY) - val rowNode = scope.div({ - key = "dsgl-system-inspector-editor-row-$index" - style = { - display = Display.Block - } - }) - rowNode.backgroundColor = 0x1B293746 - rowNode.border = Border.all(1, 0x553F4A57) - renderNode(ctx, rowNode, rowRect) - - val labelNode = scope.text(props = { - key = "dsgl-system-inspector-editor-label-$index" - value = row.labelText - style = { - textWrap = TextWrap.Wrap - } - }) - labelNode.color = 0xFFDCE5EF.toInt() - labelNode.fontSize = 18 - renderNode( - ctx, - labelNode, - Rect(rowRect.x + 8, rowRect.y + 5, (row.controlRect.x - row.rowRect.x - 14).coerceAtLeast(40), rowRect.height - 10), - ) - - val resetButton = scope.button("x", { - key = "dsgl-system-inspector-editor-reset-$index" - }) - resetButton.backgroundColor = 0x3346596E - resetButton.border = Border.all(1, 0x775E738C) - resetButton.textColor = 0xFFDCE5EF.toInt() - resetButton.fontSize = 18 - resetButton.onClick { - controller.onResetPropertyPressed(row.property) - } - renderNode(ctx, resetButton, translateRectY(row.resetRect, -bodyScrollY)) + renderStyleEditorRowContainer(scope, ctx, rowRect, index) + renderStyleEditorRowLabel(scope, ctx, rowRect, row, index) + renderStyleEditorRowResetButton(scope, ctx, bodyScrollY, row, index) when (row.editorKind) { InspectorEditorKind.EnumSelect, InspectorEditorKind.FontSelect -> { - val valueOpen = isDomDropdownOpen(row.property, unitSelect = false) - val selector = scope.button(row.controlValue, { - key = "dsgl-system-inspector-editor-select-$index" - }) - selector.backgroundColor = if (valueOpen) 0x334D5D70 else if (row.controlHovered) 0x2A425164 else 0x22313D4B - selector.border = Border.all(1, if (valueOpen) 0xFFA8C6E6.toInt() else 0x77607084) - selector.textColor = 0xFFE6EDF6.toInt() - selector.fontSize = 18 - selector.onClick { - toggleDomDropdown(row.property, unitSelect = false) - } - renderNode(ctx, selector, translateRectY(row.controlRect, -bodyScrollY)) + renderStyleEditorSelectButton(scope, ctx, bodyScrollY, row, index) } InspectorEditorKind.StringInput -> { - val input = TextInputNode( - text = row.controlValue.replace("|", ""), - key = "dsgl-system-inspector-editor-input-${row.property.key}" - ) - input.backgroundColor = if (row.inputActive) 0x334D5D70 else 0x22313D4B - input.focusedBackgroundColor = input.backgroundColor - input.border = Border.all(1, if (row.inputActive) 0xFFA8C6E6.toInt() else 0x77607084) - input.textColor = 0xFFE6EDF6.toInt() - input.placeholderColor = 0xAA9AAFC6.toInt() - input.fontSize = 18 - input.onInput = { - controller.overlayApplyLiteralOverride(row.property, it.value) - } - input.onValueChange = { - controller.overlayApplyLiteralOverride(row.property, it.value) - } - input.applyParent(parentNode) - renderNode(ctx, input, translateRectY(row.controlRect, -bodyScrollY)) - - row.colorPreviewRect?.let { previewRect -> - val shiftedPreviewRect = translateRectY(previewRect, -bodyScrollY) - val preview = scope.button("", { - key = "dsgl-system-inspector-editor-color-preview-$index" - }) - preview.backgroundColor = row.colorPreviewColor ?: 0x663F4A57 - preview.border = Border.all(1, 0xCC9BB2C9.toInt()) - preview.onClick { - controller.onOpenColorPickerPressed(row.property, shiftedPreviewRect) - } - renderNode(ctx, preview, shiftedPreviewRect) - } + renderStyleEditorStringInput(scope, parentNode, ctx, bodyScrollY, row) + renderStyleEditorColorPreview(scope, ctx, bodyScrollY, row, index) } InspectorEditorKind.NumericInput -> { - row.decrementRect?.let { rect -> - val dec = scope.button("-", { - key = "dsgl-system-inspector-editor-dec-$index" - }) - dec.backgroundColor = 0x3346596E - dec.border = Border.all(1, 0x775E738C) - dec.textColor = 0xFFDCE5EF.toInt() - dec.fontSize = 18 - dec.onClick { - controller.onNumericDecrementPressed(row.property) - } - renderNode(ctx, dec, translateRectY(rect, -bodyScrollY)) - } - row.inputRect?.let { rect -> - val input = TextInputNode( - text = row.controlValue.replace("|", ""), - key = "dsgl-system-inspector-editor-numeric-input-${row.property.key}" - ) - input.allowedChars = "-0123456789." - input.backgroundColor = if (row.inputActive) 0x334D5D70 else 0x22313D4B - input.focusedBackgroundColor = input.backgroundColor - input.border = Border.all(1, if (row.inputActive) 0xFFA8C6E6.toInt() else 0x77607084) - input.textColor = 0xFFE6EDF6.toInt() - input.placeholderColor = 0xAA9AAFC6.toInt() - input.fontSize = 18 - input.onInput = { - controller.overlayApplyNumericOverride(row.property, it.value, row.unitValue) - } - input.onValueChange = { - controller.overlayApplyNumericOverride(row.property, it.value, row.unitValue) - } - input.applyParent(parentNode) - renderNode(ctx, input, translateRectY(rect, -bodyScrollY)) - } - - row.incrementRect?.let { rect -> - val inc = scope.button("+", { - key = "dsgl-system-inspector-editor-inc-$index" - }) - inc.backgroundColor = 0x3346596E - inc.border = Border.all(1, 0x775E738C) - inc.textColor = 0xFFDCE5EF.toInt() - inc.fontSize = 18 - inc.onClick { - controller.onNumericIncrementPressed(row.property) - } - renderNode(ctx, inc, translateRectY(rect, -bodyScrollY)) - } - row.unitRect?.let { rect -> - val unitOpen = isDomDropdownOpen(row.property, unitSelect = true) - val unit = scope.button(row.unitValue ?: "px", { - key = "dsgl-system-inspector-editor-unit-$index" - }) - unit.backgroundColor = if (unitOpen) 0x334D5D70 else 0x22313D4B - unit.border = Border.all(1, if (unitOpen) 0xFFA8C6E6.toInt() else 0x77607084) - unit.textColor = 0xFFE6EDF6.toInt() - unit.fontSize = 18 - unit.onClick { - toggleDomDropdown(row.property, unitSelect = true) - } - renderNode(ctx, unit, translateRectY(rect, -bodyScrollY)) - } + renderStyleEditorNumericControls(scope, parentNode, ctx, bodyScrollY, row, index) } } } + renderStyleEditorFooterActions(scope, ctx, bodyScrollY) + } + + private fun renderStyleEditorRowContainer(scope: UiScope, ctx: UiMeasureContext, rowRect: Rect, index: Int) { + val rowNode = scope.div({ + key = "dsgl-system-inspector-editor-row-$index" + style = { + display = Display.Block + } + }) + rowNode.backgroundColor = 0x1B293746 + rowNode.border = Border.all(1, 0x553F4A57) + renderNode(ctx, rowNode, rowRect) + } + + private fun renderStyleEditorRowLabel( + scope: UiScope, + ctx: UiMeasureContext, + rowRect: Rect, + row: InspectorStyleEditorRowSnapshot, + index: Int + ) { + val labelNode = scope.text(props = { + key = "dsgl-system-inspector-editor-label-$index" + value = row.labelText + style = { + textWrap = TextWrap.Wrap + } + }) + labelNode.color = 0xFFDCE5EF.toInt() + labelNode.fontSize = 18 + renderNode( + ctx, + labelNode, + Rect(rowRect.x + 8, rowRect.y + 5, (row.controlRect.x - row.rowRect.x - 14).coerceAtLeast(40), rowRect.height - 10), + ) + } + + private fun renderStyleEditorRowResetButton( + scope: UiScope, + ctx: UiMeasureContext, + bodyScrollY: Int, + row: InspectorStyleEditorRowSnapshot, + index: Int + ) { + val resetButton = scope.button("x", { + key = "dsgl-system-inspector-editor-reset-$index" + }) + resetButton.backgroundColor = 0x3346596E + resetButton.border = Border.all(1, 0x775E738C) + resetButton.textColor = 0xFFDCE5EF.toInt() + resetButton.fontSize = 18 + resetButton.onClick { + controller.onResetPropertyPressed(row.property) + } + renderNode(ctx, resetButton, translateRectY(row.resetRect, -bodyScrollY)) + } + + private fun renderStyleEditorSelectButton( + scope: UiScope, + ctx: UiMeasureContext, + bodyScrollY: Int, + row: InspectorStyleEditorRowSnapshot, + index: Int + ) { + val valueOpen = isDomDropdownOpen(row.property, unitSelect = false) + val selector = scope.button(row.controlValue, { + key = "dsgl-system-inspector-editor-select-$index" + }) + selector.backgroundColor = if (valueOpen) 0x334D5D70 else if (row.controlHovered) 0x2A425164 else 0x22313D4B + selector.border = Border.all(1, if (valueOpen) 0xFFA8C6E6.toInt() else 0x77607084) + selector.textColor = 0xFFE6EDF6.toInt() + selector.fontSize = 18 + selector.onClick { + toggleDomDropdown(row.property, unitSelect = false) + } + renderNode(ctx, selector, translateRectY(row.controlRect, -bodyScrollY)) + } + + private fun renderStyleEditorStringInput( + scope: UiScope, + parentNode: DOMNode, + ctx: UiMeasureContext, + bodyScrollY: Int, + row: InspectorStyleEditorRowSnapshot + ) { + val input = TextInputNode( + text = row.controlValue.replace("|", ""), + key = "dsgl-system-inspector-editor-input-${row.property.key}" + ) + input.backgroundColor = if (row.inputActive) 0x334D5D70 else 0x22313D4B + input.focusedBackgroundColor = input.backgroundColor + input.border = Border.all(1, if (row.inputActive) 0xFFA8C6E6.toInt() else 0x77607084) + input.textColor = 0xFFE6EDF6.toInt() + input.placeholderColor = 0xAA9AAFC6.toInt() + input.fontSize = 18 + input.onInput = { + controller.overlayApplyLiteralOverride(row.property, it.value) + } + input.onValueChange = { + controller.overlayApplyLiteralOverride(row.property, it.value) + } + input.applyParent(parentNode) + renderNode(ctx, input, translateRectY(row.controlRect, -bodyScrollY)) + } + + private fun renderStyleEditorColorPreview( + scope: UiScope, + ctx: UiMeasureContext, + bodyScrollY: Int, + row: InspectorStyleEditorRowSnapshot, + index: Int + ) { + row.colorPreviewRect?.let { previewRect -> + val shiftedPreviewRect = translateRectY(previewRect, -bodyScrollY) + val preview = scope.button("", { + key = "dsgl-system-inspector-editor-color-preview-$index" + }) + preview.backgroundColor = row.colorPreviewColor ?: 0x663F4A57 + preview.border = Border.all(1, 0xCC9BB2C9.toInt()) + preview.onClick { + controller.onOpenColorPickerPressed(row.property, shiftedPreviewRect) + } + renderNode(ctx, preview, shiftedPreviewRect) + } + } + + private fun renderStyleEditorNumericControls( + scope: UiScope, + parentNode: DOMNode, + ctx: UiMeasureContext, + bodyScrollY: Int, + row: InspectorStyleEditorRowSnapshot, + index: Int + ) { + row.decrementRect?.let { rect -> + val dec = scope.button("-", { + key = "dsgl-system-inspector-editor-dec-$index" + }) + dec.backgroundColor = 0x3346596E + dec.border = Border.all(1, 0x775E738C) + dec.textColor = 0xFFDCE5EF.toInt() + dec.fontSize = 18 + dec.onClick { + controller.onNumericDecrementPressed(row.property) + } + renderNode(ctx, dec, translateRectY(rect, -bodyScrollY)) + } + row.inputRect?.let { rect -> + val input = TextInputNode( + text = row.controlValue.replace("|", ""), + key = "dsgl-system-inspector-editor-numeric-input-${row.property.key}" + ) + input.allowedChars = "-0123456789." + input.backgroundColor = if (row.inputActive) 0x334D5D70 else 0x22313D4B + input.focusedBackgroundColor = input.backgroundColor + input.border = Border.all(1, if (row.inputActive) 0xFFA8C6E6.toInt() else 0x77607084) + input.textColor = 0xFFE6EDF6.toInt() + input.placeholderColor = 0xAA9AAFC6.toInt() + input.fontSize = 18 + input.onInput = { + controller.overlayApplyNumericOverride(row.property, it.value, row.unitValue) + } + input.onValueChange = { + controller.overlayApplyNumericOverride(row.property, it.value, row.unitValue) + } + input.applyParent(parentNode) + renderNode(ctx, input, translateRectY(rect, -bodyScrollY)) + } + + row.incrementRect?.let { rect -> + val inc = scope.button("+", { + key = "dsgl-system-inspector-editor-inc-$index" + }) + inc.backgroundColor = 0x3346596E + inc.border = Border.all(1, 0x775E738C) + inc.textColor = 0xFFDCE5EF.toInt() + inc.fontSize = 18 + inc.onClick { + controller.onNumericIncrementPressed(row.property) + } + renderNode(ctx, inc, translateRectY(rect, -bodyScrollY)) + } + row.unitRect?.let { rect -> + val unitOpen = isDomDropdownOpen(row.property, unitSelect = true) + val unit = scope.button(row.unitValue ?: "px", { + key = "dsgl-system-inspector-editor-unit-$index" + }) + unit.backgroundColor = if (unitOpen) 0x334D5D70 else 0x22313D4B + unit.border = Border.all(1, if (unitOpen) 0xFFA8C6E6.toInt() else 0x77607084) + unit.textColor = 0xFFE6EDF6.toInt() + unit.fontSize = 18 + unit.onClick { + toggleDomDropdown(row.property, unitSelect = true) + } + renderNode(ctx, unit, translateRectY(rect, -bodyScrollY)) + } + } + + private fun renderStyleEditorFooterActions(scope: UiScope, ctx: UiMeasureContext, bodyScrollY: Int) { val resetRect = controller.overlayStyleEditorResetRect() if (resetRect.width > 0 && resetRect.height > 0) { val resetButton = scope.button("Reset node", { From 5e5a9c4abdd25a8b7be748c0af68029d51653316 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sun, 19 Apr 2026 23:26:39 +0300 Subject: [PATCH 14/78] extracting popups into separate functions; --- .../internal/SystemInspectorOverlayNode.kt | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt index 581a5bc..f65108d 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt @@ -985,6 +985,22 @@ internal class SystemInspectorOverlayNode( val dropdownKey = dropdownScrollKey(dropdown.property, dropdown.unitSelect) val persistedDropdownSession = persistedDropdownScrollSession[dropdownKey] val persistedDropdownY = persistedDropdownSession?.resolvedY?.coerceAtLeast(0) ?: 0 + val popup = renderDropdownPopupContainer(scope, ctx, dropdownKey, dropdown.popupRect) + renderDropdownOptionButtons(scope, ctx, dropdown, dropdownKey, persistedDropdownY) + renderDropdownFooter(scope, ctx, dropdown, dropdownKey, persistedDropdownY) + + popup.restoreScrollSessionSnapshot(persistedDropdownSession) + popup.scrollContainerState() + persistedDropdownScrollSession[dropdownKey] = popup.captureScrollSessionSnapshot() + controller.onNativeDomDropdownSnapshots(listOf(dropdown)) + } + + private fun renderDropdownPopupContainer( + scope: UiScope, + ctx: UiMeasureContext, + dropdownKey: String, + popupRect: Rect + ): DOMNode { val popup = scope.div({ key = dropdownKey style = { @@ -994,8 +1010,17 @@ internal class SystemInspectorOverlayNode( popup.backgroundColor = 0xEE202A36.toInt() popup.border = Border.all(1, 0xCC596A80.toInt()) popup.overflowY = Overflow.Auto - renderNode(ctx, popup, dropdown.popupRect) + renderNode(ctx, popup, popupRect) + return popup + } + private fun renderDropdownOptionButtons( + scope: UiScope, + ctx: UiMeasureContext, + dropdown: InspectorDropdownSnapshot, + dropdownKey: String, + persistedDropdownY: Int + ) { dropdown.options.forEachIndexed { optionIndex, option -> val optionRect = Rect( option.rect.x, @@ -1021,7 +1046,15 @@ internal class SystemInspectorOverlayNode( } renderNode(ctx, button, optionRect) } + } + private fun renderDropdownFooter( + scope: UiScope, + ctx: UiMeasureContext, + dropdown: InspectorDropdownSnapshot, + dropdownKey: String, + persistedDropdownY: Int + ) { dropdown.footerText?.let { footer -> val footerNode = scope.text(props = { key = "$dropdownKey-footer" @@ -1043,11 +1076,6 @@ internal class SystemInspectorOverlayNode( ) ) } - - popup.restoreScrollSessionSnapshot(persistedDropdownSession) - popup.scrollContainerState() - persistedDropdownScrollSession[dropdownKey] = popup.captureScrollSessionSnapshot() - controller.onNativeDomDropdownSnapshots(listOf(dropdown)) } private fun resolveDomDropdownSnapshot( From fcd7782ed8b16c5196ead2919e25220cfba613bb Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Mon, 20 Apr 2026 20:13:45 +0300 Subject: [PATCH 15/78] split popup owner logic into app and system overlays; --- .../dsgl/mcForge1710/DsglScreenHost.kt | 61 ++++++++++++++---- .../dsgl/core/dom/elements/SelectNode.kt | 13 ++-- .../org/dreamfinity/dsgl/core/dsl/InputDsl.kt | 5 +- .../dsgl/core/select/SelectHost.kt | 4 +- .../dsgl/core/select/SelectRuntime.kt | 42 ++++++++++++- .../core/dom/SelectNodeOwnerScopeTests.kt | 63 +++++++++++++++++++ .../SelectRuntimeOwnershipBridgeTests.kt | 60 ++++++++++++++++++ 7 files changed, 227 insertions(+), 21 deletions(-) create mode 100644 core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerScopeTests.kt create mode 100644 core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntimeOwnershipBridgeTests.kt diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index f1384cc..6ed2f2b 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -91,6 +91,7 @@ abstract class DsglScreenHost( private val composedCommandsBuffer: MutableList = ArrayList(512) private val stagingCommandsBuffer: MutableList = ArrayList(512) private val applicationOverlayCommandsBuffer: MutableList = ArrayList(256) + private val systemOverlayCommandsBuffer: MutableList = ArrayList(256) private var activeTarget: DOMNode? = null private var lastFrameNanos: Long = 0L private val inspector: InspectorController = InspectorController() @@ -234,7 +235,8 @@ abstract class DsglScreenHost( tracePhase("style.end") } ContextMenuRuntime.engine.onFrame(adapter, lastWidth, lastHeight, 1f) - SelectRuntime.engine.onFrame(adapter, lastWidth, lastHeight, 1f) + SelectRuntime.applicationEngine.onFrame(adapter, lastWidth, lastHeight, 1f) + SelectRuntime.systemEngine.onFrame(adapter, lastWidth, lastHeight, 1f) ColorPickerRuntime.engine.onFrame(lastWidth, lastHeight) ColorPickerRuntime.engine.onCursorPosition(dsglMouseX, dsglMouseY) refreshActiveColorSamplerOwner(tree.root) @@ -273,6 +275,11 @@ abstract class DsglScreenHost( emptyList() } } + systemOverlayCommandsBuffer.clear() + systemOverlayCommandsBuffer.addAll(systemOverlayCommands) + if (systemOverlayRenderEnabled) { + SelectRuntime.systemEngine.appendOverlayCommands(adapter, lastWidth, lastHeight, systemOverlayCommandsBuffer) + } val debugOverlayCommands = runCatching { debugOverlayHost.render(lastWidth, lastHeight) debugOverlayHost.paint(adapter) @@ -280,13 +287,14 @@ abstract class DsglScreenHost( emptyList() } val contextMenuBlocks = appOverlayInputEnabled && !inspectorBlocks && ContextMenuRuntime.engine.isOpen() - val selectBlocks = appOverlayInputEnabled && !inspectorBlocks && SelectRuntime.engine.isOpen() + val selectBlocks = appOverlayInputEnabled && !inspectorBlocks && SelectRuntime.applicationEngine.isOpen() + val systemSelectBlocks = systemOverlayInputEnabled && SelectRuntime.systemEngine.isOpen() val inlineSamplerOwnsSession = activeColorSamplerOwner is ActiveColorSamplerOwner.Inline val colorPickerBlocks = !inspectorBlocks && ( (systemOverlayInputEnabled && systemOverlayHost.isSystemColorPickerOpen()) || (appOverlayInputEnabled && ColorPickerRuntime.engine.isOpen() && !inlineSamplerOwnsSession) ) - if (!inspectorBlocks && !contextMenuBlocks && !selectBlocks && !colorPickerBlocks) { + if (!inspectorBlocks && !contextMenuBlocks && !selectBlocks && !systemSelectBlocks && !colorPickerBlocks) { DndRuntime.engine.onMouseMove(tree.root, dsglMouseX, dsglMouseY) } DndRuntime.engine.onFrame(tree.root, dtSeconds) @@ -294,7 +302,7 @@ abstract class DsglScreenHost( val prevY = if (lastMoveY == Int.MIN_VALUE) dsglMouseY else lastMoveY val dx = dsglMouseX - prevX val dy = dsglMouseY - prevY - if (inspectorBlocks || contextMenuBlocks || selectBlocks || colorPickerBlocks) { + if (inspectorBlocks || contextMenuBlocks || selectBlocks || systemSelectBlocks || colorPickerBlocks) { clearHoverChainStates() hoverTarget = null } else { @@ -322,7 +330,12 @@ abstract class DsglScreenHost( lastHeight, applicationOverlayCommandsBuffer ) - SelectRuntime.engine.appendOverlayCommands(adapter, lastWidth, lastHeight, applicationOverlayCommandsBuffer) + SelectRuntime.applicationEngine.appendOverlayCommands( + adapter, + lastWidth, + lastHeight, + applicationOverlayCommandsBuffer + ) ContextMenuRuntime.engine.appendOverlayCommands( adapter, lastWidth, @@ -335,7 +348,7 @@ abstract class DsglScreenHost( OverlayLayerContracts.composePaintCommands( applicationRoot = commands, applicationOverlay = applicationOverlayCommandsBuffer, - systemOverlay = systemOverlayCommands, + systemOverlay = systemOverlayCommandsBuffer, debug = debugOverlayCommands, out = stagingCommandsBuffer, shouldRenderLayer = OverlayLayerDebugState::isRenderEnabled @@ -372,7 +385,7 @@ abstract class DsglScreenHost( FocusManager.clearFocus() DndRuntime.engine.cancelActiveDrag() ColorPickerRuntime.engine.closeAll() - SelectRuntime.engine.closeAll() + SelectRuntime.host.closeAll() ContextMenuRuntime.engine.closeAll() clearActiveTarget() flushPendingCleanup() @@ -620,7 +633,13 @@ abstract class DsglScreenHost( viewportHeight = lastHeight, viewportScale = 1f ) - SelectRuntime.engine.onFrame( + SelectRuntime.applicationEngine.onFrame( + measureContext = adapter, + viewportWidth = lastWidth, + viewportHeight = lastHeight, + viewportScale = 1f + ) + SelectRuntime.systemEngine.onFrame( measureContext = adapter, viewportWidth = lastWidth, viewportHeight = lastHeight, @@ -764,6 +783,9 @@ abstract class DsglScreenHost( if (systemOverlayHost.handleKeyDown(keyCode, keyChar)) { return true } + if (SelectRuntime.systemEngine.handleKeyDown(keyCode, keyChar)) { + return true + } val keyboardBlocked = inspector.active && ( inspector.shouldConsumeKeyboard(inspectorMouseX, inspectorMouseY) || inspector.mode == InspectorMode.Locked @@ -782,7 +804,7 @@ abstract class DsglScreenHost( if (applicationOverlayHost.handleKeyDown(keyCode, keyChar)) { return true } - if (SelectRuntime.engine.handleKeyDown(keyCode, keyChar)) { + if (SelectRuntime.applicationEngine.handleKeyDown(keyCode, keyChar)) { return true } if (ContextMenuRuntime.engine.handleKeyDown(keyCode)) { @@ -881,8 +903,21 @@ abstract class DsglScreenHost( if (consumedBySystemOverlay) { return true } + val consumedBySystemSelect = if (buttonPressed) { + SelectRuntime.systemEngine.handleMouseDown(mouseX, mouseY, mappedButton) + } else { + SelectRuntime.systemEngine.handleMouseUp(mouseX, mouseY, mappedButton) + } + if (consumedBySystemSelect) { + return true + } } else if (mouseButton == -1 && systemOverlayHost.handleMouseMove(mouseX, mouseY)) { return true + } else if (mouseButton == -1 && SelectRuntime.systemEngine.handleMouseMove(mouseX, mouseY)) { + return true + } + if (dWheel != 0 && SelectRuntime.systemEngine.handleMouseWheel(mouseX, mouseY, dWheel)) { + return true } val inspectorConsumesPointer = inspector.shouldConsumePointer(mouseX, mouseY) @@ -940,7 +975,7 @@ abstract class DsglScreenHost( if (dWheel != 0 && ContextMenuRuntime.engine.handleMouseWheel(mouseX, mouseY, dWheel)) { return true } - if (dWheel != 0 && SelectRuntime.engine.handleMouseWheel(mouseX, mouseY, dWheel)) { + if (dWheel != 0 && SelectRuntime.applicationEngine.handleMouseWheel(mouseX, mouseY, dWheel)) { return true } if (mouseButton != -1 && mappedButton != null) { @@ -953,9 +988,9 @@ abstract class DsglScreenHost( return true } val consumedBySelect = if (buttonPressed) { - SelectRuntime.engine.handleMouseDown(mouseX, mouseY, mappedButton) + SelectRuntime.applicationEngine.handleMouseDown(mouseX, mouseY, mappedButton) } else { - SelectRuntime.engine.handleMouseUp(mouseX, mouseY, mappedButton) + SelectRuntime.applicationEngine.handleMouseUp(mouseX, mouseY, mappedButton) } if (consumedBySelect) { return true @@ -965,7 +1000,7 @@ abstract class DsglScreenHost( if (mouseButton == -1 && ContextMenuRuntime.engine.handleMouseMove(mouseX, mouseY)) { return true } - if (mouseButton == -1 && SelectRuntime.engine.handleMouseMove(mouseX, mouseY)) { + if (mouseButton == -1 && SelectRuntime.applicationEngine.handleMouseMove(mouseX, mouseY)) { return true } return false diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt index 5359536..13f75bd 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt @@ -7,6 +7,7 @@ import org.dreamfinity.dsgl.core.dom.layout.Insets import org.dreamfinity.dsgl.core.dom.layout.Size import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.* +import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.select.SelectEntry import org.dreamfinity.dsgl.core.select.SelectModel @@ -19,6 +20,7 @@ class SelectNode( value: String? = null, defaultValue: String? = null, closeOnSelect: Boolean = true, + ownerScope: OverlayOwnerScope = OverlayOwnerScope.Application, key: Any? = null ) : DOMNode(key) { override val styleType: String = "select" @@ -56,6 +58,7 @@ class SelectNode( markRenderCommandsDirty() } var closeOnSelect: Boolean = closeOnSelect + var ownerScope: OverlayOwnerScope = ownerScope var textColor: Int = DsglColors.TEXT var placeholderColor: Int = 0xFF8A8A8A.toInt() var backgroundColor: Int = 0xFF2E2E33.toInt() @@ -94,13 +97,13 @@ class SelectNode( KeyCodes.DOWN -> { openPopup() - SelectRuntime.engine.moveHighlight(ownerToken, 1) + SelectRuntime.engineFor(ownerScope).moveHighlight(ownerToken, 1) event.cancelled = true } KeyCodes.UP -> { openPopup() - SelectRuntime.engine.moveHighlight(ownerToken, -1) + SelectRuntime.engineFor(ownerScope).moveHighlight(ownerToken, -1) event.cancelled = true } } @@ -211,6 +214,7 @@ class SelectNode( controlledValue = template.controlledValue defaultValue = template.defaultValue closeOnSelect = template.closeOnSelect + ownerScope = template.ownerScope textColor = template.textColor placeholderColor = template.placeholderColor backgroundColor = template.backgroundColor @@ -248,7 +252,7 @@ class SelectNode( val open = SelectRuntime.host.isOpenFor(ownerToken) setOpenState(open) if (open) { - SelectRuntime.engine.sync(openRequest()) + SelectRuntime.engineFor(ownerScope).sync(openRequest()) } } @@ -265,7 +269,8 @@ class SelectNode( onSelect = { selected -> applySelection(selected) }, onClose = { setOpenState(false) }, fontId = fontId, - fontSize = fontSize + fontSize = fontSize, + ownerScope = ownerScope ) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/InputDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/InputDsl.kt index 00ceb0e..2b8e807 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/InputDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/InputDsl.kt @@ -3,6 +3,7 @@ package org.dreamfinity.dsgl.core.dsl import org.dreamfinity.dsgl.core.dom.elements.* import org.dreamfinity.dsgl.core.hooks.ref.ElementHandle import org.dreamfinity.dsgl.core.hooks.ref.RefTarget +import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.select.SelectModelBuilder import org.dreamfinity.dsgl.core.select.selectModel import java.time.Instant @@ -15,6 +16,7 @@ open class TextAreaProps(var placeholder: String = "") : TextProps() open class SelectProps : ComponentProps() { var closeOnSelect: Boolean = true var defaultValue: String? = null + var ownerScope: OverlayOwnerScope = OverlayOwnerScope.Application var value: String? get() = valueInternal @@ -161,6 +163,7 @@ fun UiScope.select( value = if (controlled) props.controlledValue() else null, defaultValue = props.defaultValue, closeOnSelect = props.closeOnSelect, + ownerScope = props.ownerScope, key = props.key ).apply { applyStyle(this, props.style) @@ -195,4 +198,4 @@ fun UiScope.toggle( applyRef(this, ref) add(this) } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectHost.kt index 5c2a009..1678b93 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectHost.kt @@ -1,6 +1,7 @@ package org.dreamfinity.dsgl.core.select import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope interface SelectHost { fun open(request: SelectOpenRequest) @@ -20,7 +21,8 @@ data class SelectOpenRequest( val onSelect: ((String) -> Unit)? = null, val onClose: (() -> Unit)? = null, val fontId: String? = null, - val fontSize: Int? = null + val fontSize: Int? = null, + val ownerScope: OverlayOwnerScope = OverlayOwnerScope.Application, ) fun interface SelectClock { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntime.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntime.kt index 693ae42..0fce91d 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntime.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntime.kt @@ -1,6 +1,44 @@ package org.dreamfinity.dsgl.core.select +import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope + object SelectRuntime { - val engine: SelectEngine = SelectEngine() - val host: SelectHost = engine + val applicationEngine: SelectEngine = SelectEngine() + val systemEngine: SelectEngine = SelectEngine() + val engine: SelectEngine = applicationEngine + val host: SelectHost = RoutedSelectHost() + + fun engineFor(ownerScope: OverlayOwnerScope): SelectEngine { + return when (ownerScope) { + OverlayOwnerScope.Application -> applicationEngine + OverlayOwnerScope.System -> systemEngine + } + } + + private class RoutedSelectHost : SelectHost { + override fun open(request: SelectOpenRequest) { + val target = engineFor(request.ownerScope) + val other = if (target === applicationEngine) systemEngine else applicationEngine + other.close(request.owner) + target.open(request) + } + + override fun close(owner: Any) { + applicationEngine.close(owner) + systemEngine.close(owner) + } + + override fun closeAll() { + applicationEngine.closeAll() + systemEngine.closeAll() + } + + override fun isOpenFor(owner: Any): Boolean { + return applicationEngine.isOpenFor(owner) || systemEngine.isOpenFor(owner) + } + + override fun isOpen(): Boolean { + return applicationEngine.isOpen() || systemEngine.isOpen() + } + } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerScopeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerScopeTests.kt new file mode 100644 index 0000000..575f231 --- /dev/null +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerScopeTests.kt @@ -0,0 +1,63 @@ +package org.dreamfinity.dsgl.core.dom + +import org.dreamfinity.dsgl.core.DomTree +import org.dreamfinity.dsgl.core.dom.elements.ContainerNode +import org.dreamfinity.dsgl.core.dom.elements.SelectNode +import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope +import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter +import org.dreamfinity.dsgl.core.render.RenderCommand +import org.dreamfinity.dsgl.core.select.SelectRuntime +import org.dreamfinity.dsgl.core.select.selectModel +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class SelectNodeOwnerScopeTests { + private val ctx = object : UiMeasureContext { + override val fontHeight: Int = 9 + override fun measureText(text: String): Int = text.length * 6 + override fun paint(commands: List) = Unit + } + + @AfterTest + fun cleanup() { + SelectRuntime.host.closeAll() + } + + @Test + fun `system owner scope routes select popup to system engine`() { + val root = ContainerNode(key = "root") + root.bounds = Rect(0, 0, 300, 200) + val ownerKey = "system-select-owner" + val select = SelectNode( + model = selectModel(id = "system.select.model") { + option("a", "Alpha") + option("b", "Beta") + }, + ownerScope = OverlayOwnerScope.System, + key = ownerKey + ).apply { + width = 120 + height = 20 + bounds = Rect(20, 20, 120, 20) + } + select.applyParent(root) + + val tree = DomTree(root) + tree.render(ctx, 300, 200) + tree.paint(ctx) + val router = LayerDomInputRouter { root } + val clickX = select.bounds.x + (select.bounds.width / 2).coerceAtLeast(1) + val clickY = select.bounds.y + (select.bounds.height / 2).coerceAtLeast(1) + + assertTrue(router.handleMouseDown(clickX, clickY, MouseButton.LEFT)) + assertTrue(router.handleMouseUp(clickX, clickY, MouseButton.LEFT)) + + assertFalse(SelectRuntime.applicationEngine.isOpenFor(ownerKey)) + assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) + } +} diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntimeOwnershipBridgeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntimeOwnershipBridgeTests.kt new file mode 100644 index 0000000..b727d9b --- /dev/null +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntimeOwnershipBridgeTests.kt @@ -0,0 +1,60 @@ +package org.dreamfinity.dsgl.core.select + +import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class SelectRuntimeOwnershipBridgeTests { + @AfterTest + fun cleanup() { + SelectRuntime.host.closeAll() + } + + @Test + fun `application-scoped request opens application engine only`() { + val owner = Any() + SelectRuntime.host.open(request(owner, OverlayOwnerScope.Application)) + + assertTrue(SelectRuntime.applicationEngine.isOpenFor(owner)) + assertFalse(SelectRuntime.systemEngine.isOpenFor(owner)) + assertTrue(SelectRuntime.host.isOpenFor(owner)) + } + + @Test + fun `system-scoped request opens system engine only`() { + val owner = Any() + SelectRuntime.host.open(request(owner, OverlayOwnerScope.System)) + + assertFalse(SelectRuntime.applicationEngine.isOpenFor(owner)) + assertTrue(SelectRuntime.systemEngine.isOpenFor(owner)) + assertTrue(SelectRuntime.host.isOpenFor(owner)) + } + + @Test + fun `opening same owner in another scope switches engine ownership`() { + val owner = Any() + SelectRuntime.host.open(request(owner, OverlayOwnerScope.Application)) + assertTrue(SelectRuntime.applicationEngine.isOpenFor(owner)) + assertFalse(SelectRuntime.systemEngine.isOpenFor(owner)) + + SelectRuntime.host.open(request(owner, OverlayOwnerScope.System)) + assertFalse(SelectRuntime.applicationEngine.isOpenFor(owner)) + assertTrue(SelectRuntime.systemEngine.isOpenFor(owner)) + } + + private fun request(owner: Any, scope: OverlayOwnerScope): SelectOpenRequest { + return SelectOpenRequest( + owner = owner, + modelToken = 1L, + entries = listOf(SelectEntry.Option("a", labelProvider = { "Alpha" })), + selectedId = "a", + anchorRect = Rect(10, 10, 100, 20), + closeOnSelect = true, + ownerScope = scope + ) + } +} + From ab07ed4f78b52742456a1965ea2ef3c2a62543af Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Mon, 20 Apr 2026 23:30:05 +0300 Subject: [PATCH 16/78] replacing manual dropdown with input select; --- .../dsgl/mcForge1710/DsglScreenHost.kt | 35 ++-- .../internal/SystemInspectorOverlayNode.kt | 69 +++++-- .../InspectorDragScrollDomMigrationTests.kt | 24 ++- .../InspectorDropdownCorrectiveTests.kt | 131 ++++++++----- .../system/InspectorInputPathBaselineTests.kt | 181 ++++++++++-------- .../system/InspectorPointerAlignmentTests.kt | 115 +++++++---- 6 files changed, 341 insertions(+), 214 deletions(-) diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index 6ed2f2b..3b81d60 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -278,7 +278,12 @@ abstract class DsglScreenHost( systemOverlayCommandsBuffer.clear() systemOverlayCommandsBuffer.addAll(systemOverlayCommands) if (systemOverlayRenderEnabled) { - SelectRuntime.systemEngine.appendOverlayCommands(adapter, lastWidth, lastHeight, systemOverlayCommandsBuffer) + SelectRuntime.systemEngine.appendOverlayCommands( + adapter, + lastWidth, + lastHeight, + systemOverlayCommandsBuffer + ) } val debugOverlayCommands = runCatching { debugOverlayHost.render(lastWidth, lastHeight) @@ -780,10 +785,10 @@ abstract class DsglScreenHost( inspectorMouseX: Int, inspectorMouseY: Int ): Boolean { - if (systemOverlayHost.handleKeyDown(keyCode, keyChar)) { + if (SelectRuntime.systemEngine.handleKeyDown(keyCode, keyChar)) { return true } - if (SelectRuntime.systemEngine.handleKeyDown(keyCode, keyChar)) { + if (systemOverlayHost.handleKeyDown(keyCode, keyChar)) { return true } val keyboardBlocked = inspector.active && ( @@ -891,18 +896,13 @@ abstract class DsglScreenHost( mappedButton: MouseButton?, buttonPressed: Boolean ): Boolean { + if (dWheel != 0 && SelectRuntime.systemEngine.handleMouseWheel(mouseX, mouseY, dWheel)) { + return true + } if (dWheel != 0 && systemOverlayHost.handleMouseWheel(mouseX, mouseY, dWheel)) { return true } if (mouseButton != -1 && mappedButton != null) { - val consumedBySystemOverlay = if (buttonPressed) { - systemOverlayHost.handleMouseDown(mouseX, mouseY, mappedButton) - } else { - systemOverlayHost.handleMouseUp(mouseX, mouseY, mappedButton) - } - if (consumedBySystemOverlay) { - return true - } val consumedBySystemSelect = if (buttonPressed) { SelectRuntime.systemEngine.handleMouseDown(mouseX, mouseY, mappedButton) } else { @@ -911,12 +911,17 @@ abstract class DsglScreenHost( if (consumedBySystemSelect) { return true } - } else if (mouseButton == -1 && systemOverlayHost.handleMouseMove(mouseX, mouseY)) { - return true + val consumedBySystemOverlay = if (buttonPressed) { + systemOverlayHost.handleMouseDown(mouseX, mouseY, mappedButton) + } else { + systemOverlayHost.handleMouseUp(mouseX, mouseY, mappedButton) + } + if (consumedBySystemOverlay) { + return true + } } else if (mouseButton == -1 && SelectRuntime.systemEngine.handleMouseMove(mouseX, mouseY)) { return true - } - if (dWheel != 0 && SelectRuntime.systemEngine.handleMouseWheel(mouseX, mouseY, dWheel)) { + } else if (mouseButton == -1 && systemOverlayHost.handleMouseMove(mouseX, mouseY)) { return true } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt index f65108d..80581a1 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt @@ -11,6 +11,7 @@ import org.dreamfinity.dsgl.core.dom.layout.Size import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.dsl.button import org.dreamfinity.dsgl.core.dsl.div +import org.dreamfinity.dsgl.core.dsl.select import org.dreamfinity.dsgl.core.dsl.text import org.dreamfinity.dsgl.core.event.* import org.dreamfinity.dsgl.core.inspector.InspectorController @@ -21,9 +22,11 @@ import org.dreamfinity.dsgl.core.inspector.InspectorStyleEditorRowSnapshot import org.dreamfinity.dsgl.core.inspector.InspectorEditorKind import org.dreamfinity.dsgl.core.inspector.InspectorPanelState import org.dreamfinity.dsgl.core.inspector.InspectorTooltipSnapshot +import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelDragSession import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanel import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelState +import org.dreamfinity.dsgl.core.select.SelectRuntime import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.Overflow import org.dreamfinity.dsgl.core.style.StyleProperty @@ -802,16 +805,14 @@ internal class SystemInspectorOverlayNode( row: InspectorStyleEditorRowSnapshot, index: Int ) { - val valueOpen = isDomDropdownOpen(row.property, unitSelect = false) - val selector = scope.button(row.controlValue, { - key = "dsgl-system-inspector-editor-select-$index" - }) - selector.backgroundColor = if (valueOpen) 0x334D5D70 else if (row.controlHovered) 0x2A425164 else 0x22313D4B - selector.border = Border.all(1, if (valueOpen) 0xFFA8C6E6.toInt() else 0x77607084) - selector.textColor = 0xFFE6EDF6.toInt() - selector.fontSize = 18 - selector.onClick { - toggleDomDropdown(row.property, unitSelect = false) + val selector = buildSystemOwnedSelectControl( + scope = scope, + key = "dsgl-system-inspector-editor-select-$index", + selectedValue = row.controlValue, + options = controller.resolveDropdownOptionsForProperty(row.property, unitSelect = false), + hovered = row.controlHovered + ) { selected -> + controller.onSelectValueOptionPressed(row.property, selected) } renderNode(ctx, selector, translateRectY(row.controlRect, -bodyScrollY)) } @@ -921,21 +922,49 @@ internal class SystemInspectorOverlayNode( renderNode(ctx, inc, translateRectY(rect, -bodyScrollY)) } row.unitRect?.let { rect -> - val unitOpen = isDomDropdownOpen(row.property, unitSelect = true) - val unit = scope.button(row.unitValue ?: "px", { - key = "dsgl-system-inspector-editor-unit-$index" - }) - unit.backgroundColor = if (unitOpen) 0x334D5D70 else 0x22313D4B - unit.border = Border.all(1, if (unitOpen) 0xFFA8C6E6.toInt() else 0x77607084) - unit.textColor = 0xFFE6EDF6.toInt() - unit.fontSize = 18 - unit.onClick { - toggleDomDropdown(row.property, unitSelect = true) + val unitValue = row.unitValue ?: "px" + val unit = buildSystemOwnedSelectControl( + scope = scope, + key = "dsgl-system-inspector-editor-unit-$index", + selectedValue = unitValue, + options = controller.resolveDropdownOptionsForProperty(row.property, unitSelect = true), + hovered = false + ) { selected -> + controller.onSelectUnitOptionPressed(row.property, selected) } renderNode(ctx, unit, translateRectY(rect, -bodyScrollY)) } } + private fun buildSystemOwnedSelectControl( + scope: UiScope, + key: String, + selectedValue: String, + options: List, + hovered: Boolean, + onSelected: (String) -> Unit + ): DOMNode { + val open = SelectRuntime.host.isOpenFor(key) + val selectNode = scope.select( + props = { + this.key = key + ownerScope = OverlayOwnerScope.System + value = selectedValue + onInput = { onSelected(it.value) } + } + ) { + options.forEach { option -> + option(id = option, label = option) + } + } + selectNode.backgroundColor = if (open) 0x334D5D70 else if (hovered) 0x2A425164 else 0x22313D4B + selectNode.border = Border.all(1, if (open) 0xFFA8C6E6.toInt() else 0x77607084) + selectNode.textColor = 0xFFE6EDF6.toInt() + selectNode.fontSize = 18 + selectNode.placeholderColor = 0xFFE6EDF6.toInt() + return selectNode + } + private fun renderStyleEditorFooterActions(scope: UiScope, ctx: UiMeasureContext, bodyScrollY: Int) { val resetRect = controller.overlayStyleEditorResetRect() if (resetRect.width > 0 && resetRect.height > 0) { diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt index eba26a1..b12fc87 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt @@ -1,11 +1,5 @@ package org.dreamfinity.dsgl.core.overlay.system -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.colorpicker.ColorPickerRuntime import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent @@ -21,8 +15,10 @@ import org.dreamfinity.dsgl.core.inspector.InspectorEditorKind import org.dreamfinity.dsgl.core.inspector.InspectorStyleEditorRowSnapshot import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.render.RenderCommand +import org.dreamfinity.dsgl.core.select.SelectRuntime import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleProperty +import kotlin.test.* class InspectorDragScrollDomMigrationTests { private val ctx = object : UiMeasureContext { @@ -36,6 +32,7 @@ class InspectorDragScrollDomMigrationTests { FocusManager.clearFocus() KeyModifiers.sync(shift = false, control = false, meta = false) ColorPickerRuntime.engine.closeAll() + SelectRuntime.host.closeAll() StyleEngine.clearAllInspectorOverrides() StyleEngine.clearCache() } @@ -240,6 +237,9 @@ class InspectorDragScrollDomMigrationTests { fun `dropdown migration remains intact after drag-scroll migration`() { val fixture = openInspectorAndSelectTarget(withManyChildren = false) val row = findVisibleSelectRow(fixture) + val rowIndex = fixture.inspector.overlayStyleEditorRows().indexOfFirst { it.property == row.property } + .takeIf { it >= 0 } ?: error("expected style row index for ${row.property.key}") + val ownerKey = "dsgl-system-inspector-editor-select-$rowIndex" val trigger = visibleControlRect(fixture, row) val clickX = trigger.x + 2 val clickY = trigger.y + (trigger.height / 2).coerceAtLeast(1) @@ -248,12 +248,12 @@ class InspectorDragScrollDomMigrationTests { fixture.host.handleMouseUp(clickX, clickY, MouseButton.LEFT) syncAndRender(fixture, clickX, clickY) - assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isNotEmpty()) + assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) fixture.host.handleMouseDown(clickX, clickY, MouseButton.LEFT) fixture.host.handleMouseUp(clickX, clickY, MouseButton.LEFT) syncAndRender(fixture, clickX, clickY) - assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isEmpty()) + assertFalse(SelectRuntime.systemEngine.isOpenFor(ownerKey)) } @Test @@ -277,7 +277,13 @@ class InspectorDragScrollDomMigrationTests { inspector.toggle() host.onInputFrame(1280, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) host.render(ctx, 1280, 720) host.paint(ctx) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt index aecc420..a012a2f 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt @@ -11,10 +11,10 @@ import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.event.KeyModifiers import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController -import org.dreamfinity.dsgl.core.inspector.InspectorDropdownSnapshot import org.dreamfinity.dsgl.core.inspector.InspectorEditorKind import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.render.RenderCommand +import org.dreamfinity.dsgl.core.select.SelectRuntime import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleProperty import kotlin.test.* @@ -31,6 +31,7 @@ class InspectorDropdownCorrectiveTests { FocusManager.clearFocus() KeyModifiers.sync(shift = false, control = false, meta = false) ColorPickerRuntime.engine.closeAll() + SelectRuntime.host.closeAll() StyleEngine.clearAllInspectorOverrides() StyleEngine.clearCache() } @@ -41,38 +42,36 @@ class InspectorDropdownCorrectiveTests { setViewport(fixture, 420, 280) scrollInspectorBodyDown(fixture, steps = 10) - val (trigger, dropdown) = openInspectorSelectDropdown(fixture, requireScrollable = false) - val contentRect = fixture.inspector.overlayContentRect() - var outsideX = contentRect.x + 4 - var outsideY = contentRect.y + 4 - if (dropdown.popupRect.contains(outsideX, outsideY) || trigger.contains(outsideX, outsideY)) { - val panelRect = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") - outsideX = panelRect.x + 8 - outsideY = panelRect.y + 8 - } + val (trigger, ownerKey) = openInspectorSelectDropdown(fixture, requireScrollable = false) + val dropdown = selectPanelRect(ownerKey, fixture) + val panelRect = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") + val outsideX = (panelRect.x - 12).coerceAtLeast(1) + val outsideY = (panelRect.y - 12).coerceAtLeast(1) + assertFalse(dropdown.contains(outsideX, outsideY)) + assertFalse(trigger.contains(outsideX, outsideY)) - assertTrue(fixture.host.handleMouseDown(outsideX, outsideY, MouseButton.LEFT)) - fixture.host.handleMouseUp(outsideX, outsideY, MouseButton.LEFT) + assertTrue(dispatchSystemMouseDown(fixture, outsideX, outsideY)) + dispatchSystemMouseUp(fixture, outsideX, outsideY) syncAndRender(fixture, outsideX, outsideY) - assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isEmpty()) + waitForSystemSelectClosed(fixture, ownerKey, outsideX, outsideY) } @Test fun `wheel is routed to active dropdown before inspector body`() { val fixture = openInspectorAndSelectTarget(withManyChildren = false) - openVisibleInspectorSelectDropdownWithoutBodyScroll(fixture) + val (_, ownerKey) = openVisibleInspectorSelectDropdownWithoutBodyScroll(fixture) settleFrames(fixture, steps = 1) val beforePanelScroll = fixture.inspector.panelScrollOffsetY - val contentRect = fixture.inspector.overlayContentRect() - val wheelX = contentRect.x + 4 - val wheelY = contentRect.y + 10 + val popup = selectPanelRect(ownerKey, fixture) + val wheelX = popup.x + (popup.width / 2).coerceAtLeast(1) + val wheelY = popup.y + (popup.height / 2).coerceAtLeast(1) - assertTrue(fixture.host.handleMouseWheel(wheelX, wheelY, -120)) + assertTrue(dispatchSystemMouseWheel(fixture, wheelX, wheelY, -120)) syncAndRender(fixture, wheelX, wheelY) - assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isNotEmpty()) + assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) assertEquals(beforePanelScroll, fixture.inspector.panelScrollOffsetY) } @@ -82,12 +81,10 @@ class InspectorDropdownCorrectiveTests { setViewport(fixture, 420, 280) scrollInspectorBodyDown(fixture, steps = 12) - val (trigger, _) = openInspectorSelectDropdown(fixture, requireScrollable = false) - val popup = findDropdownPopupNode(fixture) - - val expectedY = (trigger.y + trigger.height + 2) - .coerceIn(2, (fixture.viewportHeight - popup.bounds.height - 2).coerceAtLeast(2)) - assertEquals(expectedY, popup.bounds.y) + val (trigger, ownerKey) = openInspectorSelectDropdown(fixture, requireScrollable = false) + val popup = selectPanelRect(ownerKey, fixture) + assertTrue(popup.y >= 0) + assertTrue(popup.y + popup.height <= fixture.viewportHeight) } @Test @@ -96,12 +93,12 @@ class InspectorDropdownCorrectiveTests { setViewport(fixture, 420, 280) scrollInspectorBodyDown(fixture, steps = 12) - val (trigger, _) = openInspectorSelectDropdown(fixture, requireScrollable = false) - val popup = findDropdownPopupNode(fixture) + val (trigger, ownerKey) = openInspectorSelectDropdown(fixture, requireScrollable = false) + val popup = selectPanelRect(ownerKey, fixture) val expectedX = trigger.x - .coerceIn(2, (fixture.viewportWidth - popup.bounds.width - 2).coerceAtLeast(2)) - assertEquals(expectedX, popup.bounds.x) + .coerceIn(2, (fixture.viewportWidth - popup.width - 2).coerceAtLeast(2)) + assertEquals(expectedX, popup.x) } @Test @@ -214,7 +211,7 @@ class InspectorDropdownCorrectiveTests { private fun openVisibleInspectorSelectDropdownWithoutBodyScroll( fixture: Fixture - ): Pair { + ): Pair { val contentRect = fixture.inspector.overlayContentRect() val bodyScrollY = fixture.inspector.panelScrollOffsetY val row = fixture.inspector.overlayStyleEditorRows().firstOrNull { row -> @@ -238,21 +235,23 @@ class InspectorDropdownCorrectiveTests { row.controlRect.width, row.controlRect.height ) + val rowIndex = fixture.inspector.overlayStyleEditorRows().indexOfFirst { it.property == row.property } + .takeIf { it >= 0 } ?: error("expected style row index for ${row.property.key}") + val ownerKey = "dsgl-system-inspector-editor-select-$rowIndex" val clickX = triggerRect.x + 2 val clickY = triggerRect.y + (triggerRect.height / 2).coerceAtLeast(1) - fixture.host.handleMouseDown(clickX, clickY, MouseButton.LEFT) - fixture.host.handleMouseUp(clickX, clickY, MouseButton.LEFT) + dispatchSystemMouseDown(fixture, clickX, clickY) + dispatchSystemMouseUp(fixture, clickX, clickY) syncAndRender(fixture, clickX, clickY) - val opened = fixture.inspector.overlayStyleEditorDropdowns().firstOrNull() - ?: error("expected inspector dropdown to open from visible select row") - return triggerRect to opened + assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) + return triggerRect to ownerKey } private fun openInspectorSelectDropdown( fixture: Fixture, requireScrollable: Boolean - ): Pair { + ): Pair { repeat(120) { val contentRect = fixture.inspector.overlayContentRect() val bodyScrollY = fixture.inspector.panelScrollOffsetY @@ -278,19 +277,25 @@ class InspectorDropdownCorrectiveTests { row.controlRect.width, row.controlRect.height ) + val rowIndex = fixture.inspector.overlayStyleEditorRows().indexOfFirst { it.property == row.property } + .takeIf { it >= 0 } ?: return@forEach + val ownerKey = "dsgl-system-inspector-editor-select-$rowIndex" val clickX = triggerRect.x + 2 val clickY = triggerRect.y + (triggerRect.height / 2).coerceAtLeast(1) - fixture.host.handleMouseDown(clickX, clickY, MouseButton.LEFT) - fixture.host.handleMouseUp(clickX, clickY, MouseButton.LEFT) + dispatchSystemMouseDown(fixture, clickX, clickY) + dispatchSystemMouseUp(fixture, clickX, clickY) syncAndRender(fixture, clickX, clickY) - val opened = fixture.inspector.overlayStyleEditorDropdowns().firstOrNull() - if (opened != null && (!requireScrollable || opened.footerText != null)) { - return triggerRect to opened + val opened = SelectRuntime.systemEngine.isOpenFor(ownerKey) + if (opened) { + val popup = selectPanelRect(ownerKey, fixture) + if (!requireScrollable || popup.height > triggerRect.height + 24) { + return triggerRect to ownerKey + } } - if (opened != null) { - fixture.host.handleMouseDown(clickX, clickY, MouseButton.LEFT) - fixture.host.handleMouseUp(clickX, clickY, MouseButton.LEFT) + if (opened) { + dispatchSystemMouseDown(fixture, clickX, clickY) + dispatchSystemMouseUp(fixture, clickX, clickY) syncAndRender(fixture, clickX, clickY) } } @@ -300,15 +305,35 @@ class InspectorDropdownCorrectiveTests { error("expected inspector select dropdown to open") } - private fun findDropdownPopupNode(fixture: Fixture): DOMNode { - val inspectorNode = fixture.host.debugEntryNode(SystemOverlayEntryId.Inspector) - ?: error("inspector entry missing") - return collectNodes(inspectorNode).firstOrNull { node -> - val key = node.key?.toString() ?: return@firstOrNull false - key.startsWith("dsgl-system-inspector-dropdown-") && - !key.contains("-option-") && - !key.endsWith("-footer") - } ?: error("expected dropdown popup node") + private fun selectPanelRect(ownerKey: String, fixture: Fixture): Rect { + SelectRuntime.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) + return SelectRuntime.systemEngine.debugPanelRect(ownerKey) + ?: error("expected system select popup for owner=$ownerKey") + } + + private fun dispatchSystemMouseDown(fixture: Fixture, x: Int, y: Int): Boolean { + return SelectRuntime.systemEngine.handleMouseDown(x, y, MouseButton.LEFT) || + fixture.host.handleMouseDown(x, y, MouseButton.LEFT) + } + + private fun dispatchSystemMouseUp(fixture: Fixture, x: Int, y: Int): Boolean { + return SelectRuntime.systemEngine.handleMouseUp(x, y, MouseButton.LEFT) || + fixture.host.handleMouseUp(x, y, MouseButton.LEFT) + } + + private fun dispatchSystemMouseWheel(fixture: Fixture, x: Int, y: Int, delta: Int): Boolean { + return SelectRuntime.systemEngine.handleMouseWheel(x, y, delta) || + fixture.host.handleMouseWheel(x, y, delta) + } + + private fun waitForSystemSelectClosed(fixture: Fixture, ownerKey: String, cursorX: Int, cursorY: Int) { + repeat(30) { + if (!SelectRuntime.systemEngine.isOpenFor(ownerKey)) return + Thread.sleep(5) + syncAndRender(fixture, cursorX, cursorY) + SelectRuntime.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) + } + assertFalse(SelectRuntime.systemEngine.isOpenFor(ownerKey)) } private fun focusInputByClick(fixture: Fixture, input: TextInputNode): Pair { diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorInputPathBaselineTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorInputPathBaselineTests.kt index 488e02c..86d509a 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorInputPathBaselineTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorInputPathBaselineTests.kt @@ -1,11 +1,5 @@ package org.dreamfinity.dsgl.core.overlay.system -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.colorpicker.ColorPickerRuntime import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent @@ -17,13 +11,14 @@ import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.event.KeyModifiers import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController -import org.dreamfinity.dsgl.core.inspector.InspectorDropdownSnapshot import org.dreamfinity.dsgl.core.inspector.InspectorEditorKind import org.dreamfinity.dsgl.core.inspector.InspectorStyleEditorRowSnapshot import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.render.RenderCommand +import org.dreamfinity.dsgl.core.select.SelectRuntime import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleProperty +import kotlin.test.* class InspectorInputPathBaselineTests { private val ctx = object : UiMeasureContext { @@ -37,6 +32,7 @@ class InspectorInputPathBaselineTests { FocusManager.clearFocus() KeyModifiers.sync(shift = false, control = false, meta = false) ColorPickerRuntime.engine.closeAll() + SelectRuntime.host.closeAll() StyleEngine.clearAllInspectorOverrides() StyleEngine.clearCache() } @@ -44,9 +40,10 @@ class InspectorInputPathBaselineTests { @Test fun `inspector dropdown live path is dom first`() { val fixture = openInspectorAndSelectTarget(withManyChildren = false) - val (trigger, opened) = openDropdownFromVisibleSelectRow(fixture) + val (trigger, ownerKey) = openDropdownFromVisibleSelectRow(fixture) - assertTrue(opened.options.isNotEmpty()) + assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) + assertNotNull(selectPanelRect(ownerKey, fixture)) assertFalse(fixture.inspector.hasOpenStyleDropdown()) assertFalse(fixture.inspector.closeOpenStyleDropdowns()) assertFalse(fixture.inspector.handleOpenStyleDropdownWheel(-120)) @@ -56,80 +53,77 @@ class InspectorInputPathBaselineTests { val routeProbe = inspectorNode.javaClass.getDeclaredMethod("isDomOwnedInteractionTarget", DOMNode::class.java) routeProbe.isAccessible = true - val dropdownPopupNode = findDropdownPopupNode(fixture) - val dropdownOptionNode = collectNodes(inspectorNode).firstOrNull { - (it.key?.toString() ?: "").contains("-option-") - } ?: error("expected dropdown option node") + val triggerNode = collectNodes(inspectorNode).firstOrNull { it.key?.toString() == ownerKey } + ?: error("expected select trigger node") + assertTrue(routeProbe.invoke(inspectorNode, triggerNode) as Boolean) - assertTrue(routeProbe.invoke(inspectorNode, dropdownPopupNode) as Boolean) - assertTrue(routeProbe.invoke(inspectorNode, dropdownOptionNode) as Boolean) - - fixture.host.handleMouseDown(trigger.x + 2, trigger.y + 2, MouseButton.LEFT) - fixture.host.handleMouseUp(trigger.x + 2, trigger.y + 2, MouseButton.LEFT) - syncAndRender(fixture, trigger.x + 2, trigger.y + 2) - assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isEmpty()) + val triggerCenterX = triggerNode.bounds.x + (triggerNode.bounds.width / 2).coerceAtLeast(1) + val triggerCenterY = triggerNode.bounds.y + (triggerNode.bounds.height / 2).coerceAtLeast(1) + dispatchSystemMouseDown(fixture, triggerCenterX, triggerCenterY) + dispatchSystemMouseUp(fixture, triggerCenterX, triggerCenterY) + syncAndRender(fixture, triggerCenterX, triggerCenterY) + waitForSystemSelectClosed(fixture, ownerKey, triggerCenterX, triggerCenterY) } @Test fun `inspector dropdown opens and closes from dom interactions`() { val fixture = openInspectorAndSelectTarget(withManyChildren = false) - val (trigger, _) = openDropdownFromVisibleSelectRow(fixture) - assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isNotEmpty()) + val (trigger, ownerKey) = openDropdownFromVisibleSelectRow(fixture) + assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) - fixture.host.handleMouseDown(trigger.x + 2, trigger.y + 2, MouseButton.LEFT) - fixture.host.handleMouseUp(trigger.x + 2, trigger.y + 2, MouseButton.LEFT) + dispatchSystemMouseDown(fixture, trigger.x + 2, trigger.y + 2) + dispatchSystemMouseUp(fixture, trigger.x + 2, trigger.y + 2) syncAndRender(fixture, trigger.x + 2, trigger.y + 2) - assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isEmpty()) + waitForSystemSelectClosed(fixture, ownerKey, trigger.x + 2, trigger.y + 2) } @Test fun `inspector dropdown option selection is consumed without click through`() { val fixture = openInspectorAndSelectTarget(withManyChildren = false) - val (_, opened) = openDropdownFromVisibleSelectRow(fixture) - val option = opened.options.firstOrNull() ?: error("expected dropdown option") - val optionX = option.rect.x + 2 - val optionY = option.rect.y + (option.rect.height / 2).coerceAtLeast(1) - - assertTrue(fixture.host.handleMouseDown(optionX, optionY, MouseButton.LEFT)) - assertTrue(fixture.host.handleMouseUp(optionX, optionY, MouseButton.LEFT)) + val (_, ownerKey) = openDropdownFromVisibleSelectRow(fixture) + val panel = selectPanelRect(ownerKey, fixture) + val optionX = panel.x + 6 + val optionY = panel.y + 10 + + SelectRuntime.systemEngine.handleMouseMove(optionX, optionY) + assertTrue(SelectRuntime.systemEngine.handleMouseDown(optionX, optionY, MouseButton.LEFT)) + assertTrue(SelectRuntime.systemEngine.handleMouseUp(optionX, optionY, MouseButton.LEFT)) syncAndRender(fixture, optionX, optionY) - assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isEmpty()) + waitForSystemSelectClosed(fixture, ownerKey, optionX, optionY) assertFalse(fixture.inspector.hasOpenStyleDropdown()) } @Test fun `inspector dropdown continuity survives rebuild`() { val fixture = openInspectorAndSelectTarget(withManyChildren = false) - val (_, opened) = openDropdownFromVisibleSelectRow(fixture) + val (_, ownerKey) = openDropdownFromVisibleSelectRow(fixture) - syncAndRender(fixture, opened.popupRect.x + 2, opened.popupRect.y + 2) - val reopened = fixture.inspector.overlayStyleEditorDropdowns().firstOrNull() - ?: error("expected dropdown after rebuild") - - assertEquals(opened.property, reopened.property) - assertEquals(opened.unitSelect, reopened.unitSelect) + val panel = selectPanelRect(ownerKey, fixture) + syncAndRender(fixture, panel.x + 2, panel.y + 2) + assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) assertFalse(fixture.inspector.hasOpenStyleDropdown()) } @Test fun `controller dropdown authority stays demoted while dom dropdown is active`() { val fixture = openInspectorAndSelectTarget(withManyChildren = false) - openDropdownFromVisibleSelectRow(fixture) + val (_, ownerKey) = openDropdownFromVisibleSelectRow(fixture) - assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isNotEmpty()) + assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) assertFalse(fixture.inspector.hasOpenStyleDropdown()) assertFalse(fixture.inspector.handleOpenStyleDropdownWheel(-120)) val contentRect = fixture.inspector.overlayContentRect() - val wheelX = contentRect.x + 4 - val wheelY = contentRect.y + 10 + val popup = selectPanelRect(ownerKey, fixture) + val wheelX = popup.x + (popup.width / 2).coerceAtLeast(1) + val wheelY = popup.y + (popup.height / 2).coerceAtLeast(1) val beforePanelScroll = fixture.inspector.panelScrollOffsetY - assertTrue(fixture.host.handleMouseWheel(wheelX, wheelY, -120)) + assertTrue(dispatchSystemMouseWheel(fixture, wheelX, wheelY, -120)) syncAndRender(fixture, wheelX, wheelY) - assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isNotEmpty()) + assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) assertEquals(beforePanelScroll, fixture.inspector.panelScrollOffsetY) assertFalse(fixture.inspector.hasOpenStyleDropdown()) } @@ -185,31 +179,28 @@ class InspectorInputPathBaselineTests { syncAndRender(fixture, visibleX, visibleY) assertTrue(findRowByProperty(fixture, row.property).controlHovered) - val (trigger, opened) = openDropdownFromVisibleSelectRow(fixture, row) - val popup = findDropdownPopupNode(fixture) - val expectedY = (trigger.y + trigger.height + 2) - .coerceIn(2, (fixture.viewportHeight - popup.bounds.height - 2).coerceAtLeast(2)) - assertEquals(expectedY, popup.bounds.y) + val (trigger, ownerKey) = openDropdownFromVisibleSelectRow(fixture, row) + val popup = selectPanelRect(ownerKey, fixture) + assertTrue(popup.y >= 0) + assertTrue(popup.y + popup.height <= fixture.viewportHeight) val contentRect = fixture.inspector.overlayContentRect() val wheelX = contentRect.x + 4 val wheelY = contentRect.y + 10 - assertTrue(fixture.host.handleMouseWheel(wheelX, wheelY, -120)) + assertTrue(dispatchSystemMouseWheel(fixture, wheelX, wheelY, -120)) syncAndRender(fixture, wheelX, wheelY) - assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isNotEmpty()) - - var outsideX = contentRect.x + 4 - var outsideY = contentRect.y + 4 - if (opened.popupRect.contains(outsideX, outsideY) || trigger.contains(outsideX, outsideY)) { - val panelRect = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") - outsideX = panelRect.x + 8 - outsideY = panelRect.y + 8 - } + assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) + + val panelRect = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") + val outsideX = (panelRect.x - 12).coerceAtLeast(1) + val outsideY = (panelRect.y - 12).coerceAtLeast(1) + assertFalse(popup.contains(outsideX, outsideY)) + assertFalse(trigger.contains(outsideX, outsideY)) - fixture.host.handleMouseDown(outsideX, outsideY, MouseButton.LEFT) - fixture.host.handleMouseUp(outsideX, outsideY, MouseButton.LEFT) + dispatchSystemMouseDown(fixture, outsideX, outsideY) + dispatchSystemMouseUp(fixture, outsideX, outsideY) syncAndRender(fixture, outsideX, outsideY) - assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isEmpty()) + waitForSystemSelectClosed(fixture, ownerKey, outsideX, outsideY) } private fun openInspectorAndSelectTarget(withManyChildren: Boolean): Fixture { @@ -220,7 +211,13 @@ class InspectorInputPathBaselineTests { inspector.toggle() host.onInputFrame(1280, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) host.render(ctx, 1280, 720) host.paint(ctx) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) @@ -263,7 +260,7 @@ class InspectorInputPathBaselineTests { val wheelX = contentRect.x + 4 val wheelY = contentRect.y + 10 repeat(steps) { - fixture.host.handleMouseWheel(wheelX, wheelY, -120) + dispatchSystemMouseWheel(fixture, wheelX, wheelY, -120) syncAndRender(fixture, wheelX, wheelY) } } @@ -310,28 +307,50 @@ class InspectorInputPathBaselineTests { private fun openDropdownFromVisibleSelectRow( fixture: Fixture, row: InspectorStyleEditorRowSnapshot = findOrScrollToVisibleSelectRow(fixture) - ): Pair { + ): Pair { val triggerRect = visibleControlRect(fixture, row) + val rowIndex = fixture.inspector.overlayStyleEditorRows().indexOfFirst { it.property == row.property } + .takeIf { it >= 0 } ?: error("expected style row index for ${row.property.key}") + val ownerKey = "dsgl-system-inspector-editor-select-$rowIndex" val clickX = triggerRect.x + 2 val clickY = triggerRect.y + (triggerRect.height / 2).coerceAtLeast(1) - fixture.host.handleMouseDown(clickX, clickY, MouseButton.LEFT) - fixture.host.handleMouseUp(clickX, clickY, MouseButton.LEFT) + dispatchSystemMouseDown(fixture, clickX, clickY) + dispatchSystemMouseUp(fixture, clickX, clickY) syncAndRender(fixture, clickX, clickY) - val opened = fixture.inspector.overlayStyleEditorDropdowns().firstOrNull() - ?: error("expected opened inspector dropdown") - return triggerRect to opened + assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) + return triggerRect to ownerKey } - private fun findDropdownPopupNode(fixture: Fixture): DOMNode { - val inspectorNode = fixture.host.debugEntryNode(SystemOverlayEntryId.Inspector) - ?: error("inspector entry missing") - return collectNodes(inspectorNode).firstOrNull { node -> - val key = node.key?.toString() ?: return@firstOrNull false - key.startsWith("dsgl-system-inspector-dropdown-") && - !key.contains("-option-") && - !key.endsWith("-footer") - } ?: error("expected dropdown popup node") + private fun selectPanelRect(ownerKey: String, fixture: Fixture): Rect { + SelectRuntime.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) + return SelectRuntime.systemEngine.debugPanelRect(ownerKey) + ?: error("expected system select popup for owner=$ownerKey") + } + + private fun dispatchSystemMouseDown(fixture: Fixture, x: Int, y: Int): Boolean { + return SelectRuntime.systemEngine.handleMouseDown(x, y, MouseButton.LEFT) || + fixture.host.handleMouseDown(x, y, MouseButton.LEFT) + } + + private fun dispatchSystemMouseUp(fixture: Fixture, x: Int, y: Int): Boolean { + return SelectRuntime.systemEngine.handleMouseUp(x, y, MouseButton.LEFT) || + fixture.host.handleMouseUp(x, y, MouseButton.LEFT) + } + + private fun dispatchSystemMouseWheel(fixture: Fixture, x: Int, y: Int, delta: Int): Boolean { + return SelectRuntime.systemEngine.handleMouseWheel(x, y, delta) || + fixture.host.handleMouseWheel(x, y, delta) + } + + private fun waitForSystemSelectClosed(fixture: Fixture, ownerKey: String, cursorX: Int, cursorY: Int) { + repeat(30) { + if (!SelectRuntime.systemEngine.isOpenFor(ownerKey)) return + Thread.sleep(5) + syncAndRender(fixture, cursorX, cursorY) + SelectRuntime.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) + } + assertFalse(SelectRuntime.systemEngine.isOpenFor(ownerKey)) } private fun focusInputByClick(fixture: Fixture, input: TextInputNode): Pair { diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt index 6075777..ff01977 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt @@ -1,11 +1,5 @@ package org.dreamfinity.dsgl.core.overlay.system -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.colorpicker.ColorPickerRuntime import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode @@ -15,12 +9,16 @@ import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.event.KeyModifiers import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController -import org.dreamfinity.dsgl.core.inspector.InspectorDropdownSnapshot import org.dreamfinity.dsgl.core.inspector.InspectorEditorKind import org.dreamfinity.dsgl.core.inspector.InspectorStyleEditorRowSnapshot import org.dreamfinity.dsgl.core.render.RenderCommand +import org.dreamfinity.dsgl.core.select.SelectRuntime import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleProperty +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue class InspectorPointerAlignmentTests { private val ctx = object : UiMeasureContext { @@ -34,6 +32,7 @@ class InspectorPointerAlignmentTests { FocusManager.clearFocus() KeyModifiers.sync(shift = false, control = false, meta = false) ColorPickerRuntime.engine.closeAll() + SelectRuntime.host.closeAll() StyleEngine.clearAllInspectorOverrides() StyleEngine.clearCache() } @@ -112,14 +111,25 @@ class InspectorPointerAlignmentTests { assertTrue(fixture.inspector.panelScrollOffsetY > 0) val row = findOrScrollToVisibleSelectRow(fixture) + val rowIndex = fixture.inspector.overlayStyleEditorRows().indexOfFirst { it.property == row.property } + .takeIf { it >= 0 } ?: error("expected style row index for ${row.property.key}") + val ownerKey = "dsgl-system-inspector-editor-select-$rowIndex" val property = row.property - val (triggerRect, openedOnVisible) = openDropdownFromVisibleSelectRow(fixture, row) - assertEquals(property, openedOnVisible.property) + val triggerRect = openDropdownFromVisibleSelectRow(fixture, row) + assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) - fixture.host.handleMouseDown(triggerRect.x + 2, triggerRect.y + (triggerRect.height / 2).coerceAtLeast(1), MouseButton.LEFT) - fixture.host.handleMouseUp(triggerRect.x + 2, triggerRect.y + (triggerRect.height / 2).coerceAtLeast(1), MouseButton.LEFT) + fixture.host.handleMouseDown( + triggerRect.x + 2, + triggerRect.y + (triggerRect.height / 2).coerceAtLeast(1), + MouseButton.LEFT + ) + fixture.host.handleMouseUp( + triggerRect.x + 2, + triggerRect.y + (triggerRect.height / 2).coerceAtLeast(1), + MouseButton.LEFT + ) syncAndRender(fixture, triggerRect.x + 2, triggerRect.y + 2) - assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isEmpty()) + waitForSystemSelectClosed(fixture, ownerKey, triggerRect.x + 2, triggerRect.y + 2) val rawX = row.controlRect.x + 2 val rawY = row.controlRect.y + (row.controlRect.height / 2).coerceAtLeast(1) @@ -127,8 +137,7 @@ class InspectorPointerAlignmentTests { fixture.host.handleMouseUp(rawX, rawY, MouseButton.LEFT) syncAndRender(fixture, rawX, rawY) - val openedOnRaw = fixture.inspector.overlayStyleEditorDropdowns().firstOrNull() - assertTrue(openedOnRaw == null || openedOnRaw.property != property) + waitForSystemSelectClosed(fixture, ownerKey, rawX, rawY) } @Test @@ -137,30 +146,29 @@ class InspectorPointerAlignmentTests { setViewport(fixture, 420, 280) val row = findOrScrollToVisibleSelectRow(fixture) - val (triggerRect, dropdown) = openDropdownFromVisibleSelectRow(fixture, row) + val rowIndex = fixture.inspector.overlayStyleEditorRows().indexOfFirst { it.property == row.property } + .takeIf { it >= 0 } ?: error("expected style row index for ${row.property.key}") + val ownerKey = "dsgl-system-inspector-editor-select-$rowIndex" + val triggerRect = openDropdownFromVisibleSelectRow(fixture, row) + val dropdown = selectPanelRect(ownerKey, fixture) settleFrames(fixture, steps = 1) - val contentRect = fixture.inspector.overlayContentRect() - val wheelX = contentRect.x + 4 - val wheelY = contentRect.y + 10 + val wheelX = dropdown.x + (dropdown.width / 2).coerceAtLeast(1) + val wheelY = dropdown.y + (dropdown.height / 2).coerceAtLeast(1) - assertTrue(fixture.host.handleMouseWheel(wheelX, wheelY, -120)) + assertTrue(dispatchSystemMouseWheel(fixture, wheelX, wheelY, -120)) syncAndRender(fixture, wheelX, wheelY) - assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isNotEmpty()) + assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) - var outsideX = contentRect.x + 4 - var outsideY = contentRect.y + 4 - if (dropdown.popupRect.contains(outsideX, outsideY) || triggerRect.contains(outsideX, outsideY)) { - val panelRect = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") - outsideX = panelRect.x + 8 - outsideY = panelRect.y + 8 - } + val panelRect = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") + val outsideX = (panelRect.x - 12).coerceAtLeast(1) + val outsideY = (panelRect.y - 12).coerceAtLeast(1) + assertFalse(dropdown.contains(outsideX, outsideY)) + assertFalse(triggerRect.contains(outsideX, outsideY)) - fixture.host.handleMouseDown(outsideX, outsideY, MouseButton.LEFT) - fixture.host.handleMouseUp(outsideX, outsideY, MouseButton.LEFT) + assertTrue(dispatchSystemMouseDown(fixture, outsideX, outsideY)) + assertTrue(dispatchSystemMouseUp(fixture, outsideX, outsideY)) syncAndRender(fixture, outsideX, outsideY) - - assertTrue(fixture.inspector.overlayStyleEditorDropdowns().isEmpty()) } private fun openInspectorAndSelectTarget(withManyChildren: Boolean): Fixture { @@ -171,7 +179,13 @@ class InspectorPointerAlignmentTests { inspector.toggle() host.onInputFrame(1280, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false + ) host.render(ctx, 1280, 720) host.paint(ctx) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) @@ -287,16 +301,45 @@ class InspectorPointerAlignmentTests { private fun openDropdownFromVisibleSelectRow( fixture: Fixture, row: InspectorStyleEditorRowSnapshot - ): Pair { + ): Rect { val triggerRect = visibleControlRect(fixture, row) val clickX = triggerRect.x + 2 val clickY = triggerRect.y + (triggerRect.height / 2).coerceAtLeast(1) fixture.host.handleMouseDown(clickX, clickY, MouseButton.LEFT) fixture.host.handleMouseUp(clickX, clickY, MouseButton.LEFT) syncAndRender(fixture, clickX, clickY) - val opened = fixture.inspector.overlayStyleEditorDropdowns().firstOrNull() - assertNotNull(opened) - return triggerRect to opened + return triggerRect + } + + private fun selectPanelRect(ownerKey: String, fixture: Fixture): Rect { + SelectRuntime.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) + return SelectRuntime.systemEngine.debugPanelRect(ownerKey) + ?: error("expected system select popup for owner=$ownerKey") + } + + private fun dispatchSystemMouseDown(fixture: Fixture, x: Int, y: Int): Boolean { + return SelectRuntime.systemEngine.handleMouseDown(x, y, MouseButton.LEFT) || + fixture.host.handleMouseDown(x, y, MouseButton.LEFT) + } + + private fun dispatchSystemMouseUp(fixture: Fixture, x: Int, y: Int): Boolean { + return SelectRuntime.systemEngine.handleMouseUp(x, y, MouseButton.LEFT) || + fixture.host.handleMouseUp(x, y, MouseButton.LEFT) + } + + private fun dispatchSystemMouseWheel(fixture: Fixture, x: Int, y: Int, delta: Int): Boolean { + return SelectRuntime.systemEngine.handleMouseWheel(x, y, delta) || + fixture.host.handleMouseWheel(x, y, delta) + } + + private fun waitForSystemSelectClosed(fixture: Fixture, ownerKey: String, cursorX: Int, cursorY: Int) { + repeat(30) { + if (!SelectRuntime.systemEngine.isOpenFor(ownerKey)) return + Thread.sleep(5) + syncAndRender(fixture, cursorX, cursorY) + SelectRuntime.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) + } + assertFalse(SelectRuntime.systemEngine.isOpenFor(ownerKey)) } private fun findRowByProperty(fixture: Fixture, property: StyleProperty): InspectorStyleEditorRowSnapshot { From 00617f627c1c07cc09d6e017d19b2a56090b8b65 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Tue, 21 Apr 2026 00:05:25 +0300 Subject: [PATCH 17/78] small clean-up; --- .../internal/SystemInspectorOverlayNode.kt | 458 +++++------------- 1 file changed, 115 insertions(+), 343 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt index 80581a1..51077ab 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt @@ -1,6 +1,5 @@ package org.dreamfinity.dsgl.core.inspector.internal -import org.dreamfinity.dsgl.core.dsl.UiScope import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.ScrollSessionSnapshot import org.dreamfinity.dsgl.core.dom.applyParent @@ -9,29 +8,17 @@ import org.dreamfinity.dsgl.core.dom.layout.Border import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.Size import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext -import org.dreamfinity.dsgl.core.dsl.button -import org.dreamfinity.dsgl.core.dsl.div -import org.dreamfinity.dsgl.core.dsl.select -import org.dreamfinity.dsgl.core.dsl.text +import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.* -import org.dreamfinity.dsgl.core.inspector.InspectorController -import org.dreamfinity.dsgl.core.inspector.InspectorDomSnapshot -import org.dreamfinity.dsgl.core.inspector.InspectorDropdownOptionSnapshot -import org.dreamfinity.dsgl.core.inspector.InspectorDropdownSnapshot -import org.dreamfinity.dsgl.core.inspector.InspectorStyleEditorRowSnapshot -import org.dreamfinity.dsgl.core.inspector.InspectorEditorKind -import org.dreamfinity.dsgl.core.inspector.InspectorPanelState -import org.dreamfinity.dsgl.core.inspector.InspectorTooltipSnapshot +import org.dreamfinity.dsgl.core.inspector.* import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope -import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelDragSession import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanel +import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelDragSession import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelState import org.dreamfinity.dsgl.core.select.SelectRuntime import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.Overflow -import org.dreamfinity.dsgl.core.style.StyleProperty import org.dreamfinity.dsgl.core.style.TextWrap -import java.util.LinkedHashMap internal class SystemInspectorOverlayNode( private val controller: InspectorController, @@ -61,17 +48,10 @@ internal class SystemInspectorOverlayNode( private var lastViewportWidth: Int = 1 private var lastViewportHeight: Int = 1 private var persistedBodyScrollSession: ScrollSessionSnapshot? = null - private val persistedDropdownScrollSession: MutableMap = LinkedHashMap() - private var activeDomDropdown: ActiveDomDropdown? = null private var overlayPanelDragUpdatedByDomInput: Boolean = false private val panelNode: DOMNode = overlayPanel.node().applyParent(this) private var minimizedChipDragSession: MinimizedChipDragSession? = null - private data class ActiveDomDropdown( - val property: StyleProperty, - val unitSelect: Boolean - ) - private data class MinimizedChipDragSession( val startPointerX: Int, val startPointerY: Int, @@ -84,13 +64,6 @@ internal class SystemInspectorOverlayNode( init { EventBus.run { this@SystemInspectorOverlayNode.addEventListener(Events.MOUSEDOWN) { event: MouseDownEvent -> - if ( - event.mouseButton == MouseButton.LEFT && - activeDomDropdown != null && - shouldDismissOpenDropdownOnPointerDown(event.target) - ) { - closeActiveDomDropdown() - } if (handleOverlayPanelMouseDown(event)) { event.cancelled = true return@addEventListener @@ -124,11 +97,6 @@ internal class SystemInspectorOverlayNode( controller.onCapturedPointerMove(nextMouseX, nextMouseY, bounds.width, bounds.height) event.cancelled = true } - this@SystemInspectorOverlayNode.addEventListener(Events.WHEEL) { event: MouseWheelEvent -> - if (handleActiveDomDropdownWheel(event.dWheel)) { - event.cancelled = true - } - } } } @@ -136,8 +104,8 @@ internal class SystemInspectorOverlayNode( if (event.mouseButton != MouseButton.LEFT) return false val bodyRect = controller.overlayContentRect() val pointerInsideBody = bodyRect.width > 0 && - bodyRect.height > 0 && - bodyRect.contains(event.mouseX, event.mouseY) + bodyRect.height > 0 && + bodyRect.contains(event.mouseX, event.mouseY) if (pointerInsideBody) return false val handled = overlayPanel.handleMouseDown( mouseX = event.mouseX, @@ -230,8 +198,7 @@ internal class SystemInspectorOverlayNode( children.remove(panelNode) panelNode.parent = null persistedBodyScrollSession = null - persistedDropdownScrollSession.clear() - closeActiveDomDropdown() + controller.onNativeDomDropdownSnapshots(emptyList()) clearMinimizedChipDragSession() controller.onNativeDomBodyScrollState(0, null, null) controller.onOverlayPanelPointerCaptureChanged(false) @@ -269,11 +236,7 @@ internal class SystemInspectorOverlayNode( .map { it.popupRect } .reduce(::unionRect) } - return if (activeDomDropdown != null) { - Rect(0, 0, lastViewportWidth.coerceAtLeast(1), lastViewportHeight.coerceAtLeast(1)) - } else { - null - } + return null } private fun unionRect(a: Rect, b: Rect): Rect { @@ -304,7 +267,7 @@ internal class SystemInspectorOverlayNode( } private fun renderMinimized(ctx: UiMeasureContext, snapshot: InspectorDomSnapshot) { - closeActiveDomDropdown() + controller.onNativeDomDropdownSnapshots(emptyList()) panelNode.render(ctx, 0, 0, 0, 0) val scope = UiScope(this) @@ -313,8 +276,6 @@ internal class SystemInspectorOverlayNode( } private fun renderExpanded(ctx: UiMeasureContext, snapshot: InspectorDomSnapshot, viewportRect: Rect) { - val viewportWidth = viewportRect.width - val viewportHeight = viewportRect.height val panelRect = snapshot.panelRect val bodyRect = snapshot.bodyRect ?: overlayPanel.bodyRect() @@ -385,7 +346,6 @@ internal class SystemInspectorOverlayNode( ) val styleRows = controller.overlayStyleEditorRows() - reconcileActiveDomDropdown(styleRows) renderStyleEditorRows(bodyScope, body, ctx, bodyScrollY, styleRows) y += snapshot.styleEditorHeight @@ -398,8 +358,7 @@ internal class SystemInspectorOverlayNode( startY = y, lineHeightPx = lineHeightPx ) - - renderDropdowns(scope, ctx, styleRows, bodyScrollY, viewportWidth, viewportHeight) + controller.onNativeDomDropdownSnapshots(emptyList()) body.restoreScrollSessionSnapshot(persistedBodyScrollSession) val bodyState = body.scrollContainerState() persistedBodyScrollSession = body.captureScrollSessionSnapshot() @@ -409,8 +368,22 @@ internal class SystemInspectorOverlayNode( trackRect = bodyScrollbarVisual?.trackRect, thumbRect = bodyScrollbarVisual?.thumbRect ) - renderTooltip(scope, ctx, "dsgl-system-inspector-variable-tooltip", controller.overlayVariableTooltip(), 0xEE141A22.toInt(), 0xCC60758F.toInt()) - renderTooltip(scope, ctx, "dsgl-system-inspector-cursor-tooltip", controller.overlayCursorTooltip(), 0xDD11151A.toInt(), 0xCC3F4A57.toInt()) + renderTooltip( + scope, + ctx, + "dsgl-system-inspector-variable-tooltip", + controller.overlayVariableTooltip(), + 0xEE141A22.toInt(), + 0xCC60758F.toInt() + ) + renderTooltip( + scope, + ctx, + "dsgl-system-inspector-cursor-tooltip", + controller.overlayCursorTooltip(), + 0xDD11151A.toInt(), + 0xCC3F4A57.toInt() + ) } private fun renderMinimizedChip( @@ -447,7 +420,8 @@ internal class SystemInspectorOverlayNode( renderNode(ctx, chip, snapshot.panelRect) val compactLineHeight = 20 - var lineY = snapshot.panelRect.y + ((snapshot.panelRect.height - compactLineHeight * snapshot.minimizedLines.size) / 2) + var lineY = + snapshot.panelRect.y + ((snapshot.panelRect.height - compactLineHeight * snapshot.minimizedLines.size) / 2) snapshot.minimizedLines.forEachIndexed { index, line -> val lineNode = scope.text(props = { key = "dsgl-system-inspector-chip-line-$index" @@ -673,20 +647,90 @@ internal class SystemInspectorOverlayNode( private fun renderHighlights(scope: UiScope, ctx: UiMeasureContext) { controller.overlaySelectedHighlight()?.let { highlight -> - renderHighlightRect(scope, ctx, "dsgl-system-inspector-selected-margin-fill", highlight.marginRect, 0x44F3B33D, null) - renderHighlightRect(scope, ctx, "dsgl-system-inspector-selected-padding-fill", highlight.paddingRect, 0x4426A69A, null) - renderHighlightRect(scope, ctx, "dsgl-system-inspector-selected-content-fill", highlight.contentRect, 0x444285F4, null) - renderHighlightRect(scope, ctx, "dsgl-system-inspector-selected-margin-outline", highlight.marginRect, null, 0x99F3B33D.toInt()) - renderHighlightRect(scope, ctx, "dsgl-system-inspector-selected-border-outline", highlight.borderRect, null, 0xCCFF9800.toInt()) - renderHighlightRect(scope, ctx, "dsgl-system-inspector-selected-padding-outline", highlight.paddingRect, null, 0x9926A69A.toInt()) - renderHighlightRect(scope, ctx, "dsgl-system-inspector-selected-content-outline", highlight.contentRect, null, 0x994285F4.toInt()) + renderHighlightRect( + scope, + ctx, + "dsgl-system-inspector-selected-margin-fill", + highlight.marginRect, + 0x44F3B33D, + null + ) + renderHighlightRect( + scope, + ctx, + "dsgl-system-inspector-selected-padding-fill", + highlight.paddingRect, + 0x4426A69A, + null + ) + renderHighlightRect( + scope, + ctx, + "dsgl-system-inspector-selected-content-fill", + highlight.contentRect, + 0x444285F4, + null + ) + renderHighlightRect( + scope, + ctx, + "dsgl-system-inspector-selected-margin-outline", + highlight.marginRect, + null, + 0x99F3B33D.toInt() + ) + renderHighlightRect( + scope, + ctx, + "dsgl-system-inspector-selected-border-outline", + highlight.borderRect, + null, + 0xCCFF9800.toInt() + ) + renderHighlightRect( + scope, + ctx, + "dsgl-system-inspector-selected-padding-outline", + highlight.paddingRect, + null, + 0x9926A69A.toInt() + ) + renderHighlightRect( + scope, + ctx, + "dsgl-system-inspector-selected-content-outline", + highlight.contentRect, + null, + 0x994285F4.toInt() + ) highlight.parentContentRect?.let { parentRect -> - renderHighlightRect(scope, ctx, "dsgl-system-inspector-selected-parent-outline", parentRect, null, 0x66FF5252) + renderHighlightRect( + scope, + ctx, + "dsgl-system-inspector-selected-parent-outline", + parentRect, + null, + 0x66FF5252 + ) } } controller.overlayHoveredHighlight()?.let { highlight -> - renderHighlightRect(scope, ctx, "dsgl-system-inspector-hovered-content-fill", highlight.contentRect, 0x3A47A0FF, null) - renderHighlightRect(scope, ctx, "dsgl-system-inspector-hovered-border-outline", highlight.borderRect, null, 0xCC47A0FF.toInt()) + renderHighlightRect( + scope, + ctx, + "dsgl-system-inspector-hovered-content-fill", + highlight.contentRect, + 0x3A47A0FF, + null + ) + renderHighlightRect( + scope, + ctx, + "dsgl-system-inspector-hovered-border-outline", + highlight.borderRect, + null, + 0xCC47A0FF.toInt() + ) } } @@ -774,7 +818,12 @@ internal class SystemInspectorOverlayNode( renderNode( ctx, labelNode, - Rect(rowRect.x + 8, rowRect.y + 5, (row.controlRect.x - row.rowRect.x - 14).coerceAtLeast(40), rowRect.height - 10), + Rect( + rowRect.x + 8, + rowRect.y + 5, + (row.controlRect.x - row.rowRect.x - 14).coerceAtLeast(40), + rowRect.height - 10 + ), ) } @@ -997,273 +1046,6 @@ internal class SystemInspectorOverlayNode( } } - private fun renderDropdowns( - scope: UiScope, - ctx: UiMeasureContext, - rows: List, - bodyScrollY: Int, - viewportWidth: Int, - viewportHeight: Int - ) { - val dropdown = resolveDomDropdownSnapshot(rows, bodyScrollY, viewportWidth, viewportHeight) - if (dropdown == null) { - controller.onNativeDomDropdownSnapshots(emptyList()) - return - } - - val dropdownKey = dropdownScrollKey(dropdown.property, dropdown.unitSelect) - val persistedDropdownSession = persistedDropdownScrollSession[dropdownKey] - val persistedDropdownY = persistedDropdownSession?.resolvedY?.coerceAtLeast(0) ?: 0 - val popup = renderDropdownPopupContainer(scope, ctx, dropdownKey, dropdown.popupRect) - renderDropdownOptionButtons(scope, ctx, dropdown, dropdownKey, persistedDropdownY) - renderDropdownFooter(scope, ctx, dropdown, dropdownKey, persistedDropdownY) - - popup.restoreScrollSessionSnapshot(persistedDropdownSession) - popup.scrollContainerState() - persistedDropdownScrollSession[dropdownKey] = popup.captureScrollSessionSnapshot() - controller.onNativeDomDropdownSnapshots(listOf(dropdown)) - } - - private fun renderDropdownPopupContainer( - scope: UiScope, - ctx: UiMeasureContext, - dropdownKey: String, - popupRect: Rect - ): DOMNode { - val popup = scope.div({ - key = dropdownKey - style = { - display = Display.Block - } - }) - popup.backgroundColor = 0xEE202A36.toInt() - popup.border = Border.all(1, 0xCC596A80.toInt()) - popup.overflowY = Overflow.Auto - renderNode(ctx, popup, popupRect) - return popup - } - - private fun renderDropdownOptionButtons( - scope: UiScope, - ctx: UiMeasureContext, - dropdown: InspectorDropdownSnapshot, - dropdownKey: String, - persistedDropdownY: Int - ) { - dropdown.options.forEachIndexed { optionIndex, option -> - val optionRect = Rect( - option.rect.x, - option.rect.y - persistedDropdownY, - option.rect.width, - option.rect.height - ) - val hovered = optionRect.contains(cursorX, cursorY) - val button = scope.button(option.text, { - key = "$dropdownKey-option-$optionIndex" - }) - button.backgroundColor = if (hovered) 0x2D4C6279 else 0x22313D4B - button.border = Border.all(1, if (hovered) 0xCC95B3D3.toInt() else 0x664F6076) - button.textColor = if (hovered) 0xFFFFFFFF.toInt() else 0xFFE6EDF6.toInt() - button.fontSize = 18 - button.onClick { - if (dropdown.unitSelect) { - controller.onSelectUnitOptionPressed(dropdown.property, option.value) - } else { - controller.onSelectValueOptionPressed(dropdown.property, option.value) - } - closeActiveDomDropdown() - } - renderNode(ctx, button, optionRect) - } - } - - private fun renderDropdownFooter( - scope: UiScope, - ctx: UiMeasureContext, - dropdown: InspectorDropdownSnapshot, - dropdownKey: String, - persistedDropdownY: Int - ) { - dropdown.footerText?.let { footer -> - val footerNode = scope.text(props = { - key = "$dropdownKey-footer" - value = footer - style = { - textWrap = TextWrap.NoWrap - } - }) - footerNode.color = 0xFF8EA6BF.toInt() - footerNode.fontSize = 18 - renderNode( - ctx, - footerNode, - Rect( - dropdown.popupRect.x + 6, - dropdown.popupRect.y + dropdown.popupRect.height - 22 - persistedDropdownY, - (dropdown.popupRect.width - 12).coerceAtLeast(20), - 20 - ) - ) - } - } - - private fun resolveDomDropdownSnapshot( - rows: List, - bodyScrollY: Int, - viewportWidth: Int, - viewportHeight: Int - ): InspectorDropdownSnapshot? { - val activeDropdown = activeDomDropdown ?: return null - val row = rows.firstOrNull { it.property == activeDropdown.property } ?: run { - closeActiveDomDropdown() - return null - } - if (activeDropdown.unitSelect && row.unitRect == null) { - closeActiveDomDropdown() - return null - } - - val options = controller.resolveDropdownOptionsForProperty(activeDropdown.property, activeDropdown.unitSelect) - if (options.isEmpty()) { - closeActiveDomDropdown() - return null - } - - val triggerRect = if (activeDropdown.unitSelect) { - row.unitRect ?: row.controlRect - } else { - row.controlRect - } - val visibleTriggerRect = translateRectY(triggerRect, -bodyScrollY) - - val maxChars = options.maxOfOrNull { it.length } ?: 0 - val estimatedTextWidth = (maxChars * 8 + 22).coerceAtLeast(120) - val popupWidth = maxOf(triggerRect.width, estimatedTextWidth).coerceAtLeast(120) - val optionHeight = 24 - val maxVisibleRows = 8 - val visibleRows = minOf(maxVisibleRows, options.size) - val footerText = if (options.size > visibleRows) "Scroll for more" else null - val footerHeight = if (footerText != null) 22 else 0 - val popupHeight = (visibleRows * optionHeight + footerHeight + 4).coerceAtLeast(optionHeight + 4) - - val rawX = if (activeDropdown.unitSelect) { - visibleTriggerRect.x + visibleTriggerRect.width - popupWidth - } else { - visibleTriggerRect.x - } - val rawY = visibleTriggerRect.y + visibleTriggerRect.height + 2 - val clampedX = rawX.coerceIn(2, (viewportWidth - popupWidth - 2).coerceAtLeast(2)) - val clampedY = rawY.coerceIn(2, (viewportHeight - popupHeight - 2).coerceAtLeast(2)) - val popupRect = Rect(clampedX, clampedY, popupWidth, popupHeight) - - val optionWidth = (popupRect.width - 4).coerceAtLeast(20) - val optionSnapshots = options.mapIndexed { index, option -> - InspectorDropdownOptionSnapshot( - rect = Rect( - popupRect.x + 2, - popupRect.y + 2 + index * optionHeight, - optionWidth, - optionHeight - ), - text = option, - value = option, - hovered = false - ) - } - - return InspectorDropdownSnapshot( - popupRect = popupRect, - property = activeDropdown.property, - unitSelect = activeDropdown.unitSelect, - options = optionSnapshots, - footerText = footerText - ) - } - - private fun handleActiveDomDropdownWheel(delta: Int): Boolean { - if (delta == 0 || KeyModifiers.shiftDown) return false - val activeDropdown = activeDomDropdown ?: return false - val dropdownKey = dropdownScrollKey(activeDropdown.property, activeDropdown.unitSelect) - val current = persistedDropdownScrollSession[dropdownKey] - val baseResolved = current?.resolvedY?.coerceAtLeast(0) ?: 0 - val steps = (kotlin.math.abs(delta) / 120).coerceAtLeast(1) - val amount = steps * 18 - val nextResolved = if (delta < 0) { - baseResolved + amount - } else { - (baseResolved - amount).coerceAtLeast(0) - } - val nextTarget = if (delta < 0) { - (current?.targetY?.coerceAtLeast(0) ?: baseResolved) + amount - } else { - ((current?.targetY?.coerceAtLeast(0) ?: baseResolved) - amount).coerceAtLeast(0) - } - persistedDropdownScrollSession[dropdownKey] = ScrollSessionSnapshot( - targetX = current?.targetX?.coerceAtLeast(0) ?: 0, - targetY = nextTarget, - displayedX = current?.displayedX?.takeIf { it.isFinite() }?.coerceAtLeast(0.0) ?: 0.0, - displayedY = nextResolved.toDouble(), - resolvedX = current?.resolvedX?.coerceAtLeast(0) ?: 0, - resolvedY = nextResolved, - dragSession = current?.dragSession - ) - return true - } - - private fun reconcileActiveDomDropdown(rows: List) { - val activeDropdown = activeDomDropdown ?: return - val row = rows.firstOrNull { it.property == activeDropdown.property } ?: run { - closeActiveDomDropdown() - return - } - if (activeDropdown.unitSelect && row.unitRect == null) { - closeActiveDomDropdown() - return - } - val options = controller.resolveDropdownOptionsForProperty(activeDropdown.property, activeDropdown.unitSelect) - if (options.isEmpty()) { - closeActiveDomDropdown() - } - } - - private fun isDomDropdownOpen(property: StyleProperty, unitSelect: Boolean): Boolean { - val activeDropdown = activeDomDropdown ?: return false - return activeDropdown.property == property && activeDropdown.unitSelect == unitSelect - } - - private fun toggleDomDropdown(property: StyleProperty, unitSelect: Boolean) { - val activeDropdown = activeDomDropdown - if (activeDropdown != null && activeDropdown.property == property && activeDropdown.unitSelect == unitSelect) { - closeActiveDomDropdown() - return - } - activeDomDropdown = ActiveDomDropdown(property = property, unitSelect = unitSelect) - } - - private fun closeActiveDomDropdown() { - if (activeDomDropdown == null) return - activeDomDropdown = null - controller.onNativeDomDropdownSnapshots(emptyList()) - } - - private fun shouldDismissOpenDropdownOnPointerDown(target: DOMNode?): Boolean { - var current = target - while (current != null && current !== this) { - val key = current.key?.toString() ?: "" - if (key.startsWith("dsgl-system-inspector-dropdown-")) { - return false - } - if ( - key.startsWith("dsgl-system-inspector-editor-select-") || - key.startsWith("dsgl-system-inspector-editor-unit-") - ) { - return false - } - current = current.parent - } - return true - } - private fun startMinimizedChipDrag(panelRect: Rect, mouseX: Int, mouseY: Int) { minimizedChipDragSession = MinimizedChipDragSession( startPointerX = mouseX, @@ -1316,6 +1098,7 @@ internal class SystemInspectorOverlayNode( minimizedChipDragSession = null controller.onOverlayPanelPointerCaptureChanged(false) } + private fun renderTooltip( scope: UiScope, ctx: UiMeasureContext, @@ -1378,14 +1161,6 @@ internal class SystemInspectorOverlayNode( findNodeByKey("dsgl-system-inspector-body")?.let { bodyNode -> persistedBodyScrollSession = bodyNode.captureScrollSessionSnapshot() } - val nextDropdownScroll = LinkedHashMap() - collectNodes(this).forEach { node -> - val nodeKey = node.key?.toString() ?: return@forEach - if (!nodeKey.startsWith("dsgl-system-inspector-dropdown-")) return@forEach - nextDropdownScroll[nodeKey] = node.captureScrollSessionSnapshot() - } - persistedDropdownScrollSession.clear() - persistedDropdownScrollSession.putAll(nextDropdownScroll) } private fun findNodeByKey(targetKey: String): DOMNode? { @@ -1402,10 +1177,6 @@ internal class SystemInspectorOverlayNode( return out } - private fun dropdownScrollKey(property: StyleProperty, unitSelect: Boolean): String { - return "dsgl-system-inspector-dropdown-${property.key}-${if (unitSelect) "unit" else "value"}" - } - private fun shouldRetainInspectorSubtreeFocus(): Boolean { val focused = FocusManager.focusedNode() ?: return false return isSameOrAncestor(this, focused) @@ -1423,6 +1194,7 @@ internal class SystemInspectorOverlayNode( private fun translateRectY(rect: Rect, deltaY: Int): Rect { return Rect(rect.x, rect.y + deltaY, rect.width, rect.height) } + private fun renderNode( ctx: UiMeasureContext, node: DOMNode, From 0f26ce7276a4b38ffa527a0ee7387738a7bd860e Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Tue, 21 Apr 2026 01:22:21 +0300 Subject: [PATCH 18/78] fixing inspector color picker issue; --- .../SystemColorPickerPopupBodyNode.kt | 393 +++++++++++++++--- .../org/dreamfinity/dsgl/core/dom/DOMNode.kt | 4 + .../SystemOverlayColorPickerEntryTests.kt | 76 ++++ 3 files changed, 421 insertions(+), 52 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt index 4fa91c2..7b1bcd4 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt @@ -114,10 +114,56 @@ internal class SystemColorPickerPopupBodyNode( val activeInputBuffer = controller.viewActiveInputBuffer() val inputValues = controller.viewInputValues() val recentColors = controller.viewRecentColors() + val definitionsByKey = controller.viewInputDefinitions().associate { it.first to it.second } + + renderTopControls( + ctx = ctx, + controller = controller, + layout = layout, + style = style, + state = state, + hueDeg = hueDeg, + hoverX = hoverX, + hoverY = hoverY, + modeDropdownOpen = modeDropdownOpen + ) + renderInputRows( + ctx = ctx, + controller = controller, + layout = layout, + style = style, + hoverX = hoverX, + hoverY = hoverY, + nowMs = nowMs, + activeInputKey = activeInputKey, + activeInputBuffer = activeInputBuffer, + inputValues = inputValues, + definitionsByKey = definitionsByKey + ) + renderRecentSwatchGrid( + ctx = ctx, + layout = layout, + style = style, + hoverX = hoverX, + hoverY = hoverY, + recentColors = recentColors + ) + } - modeSelectButton.text = if (modeDropdownOpen) "${state.mode.name} ^" else "${state.mode.name} v" - applyButtonVisual( + private fun renderTopControls( + ctx: UiMeasureContext, + controller: ColorPickerController, + layout: ColorPickerLayout, + style: ColorPickerStyle, + state: ColorPickerState, + hueDeg: Float, + hoverX: Int, + hoverY: Int, + modeDropdownOpen: Boolean + ) { + syncPickerButtonVisual( button = modeSelectButton, + text = if (modeDropdownOpen) "${state.mode.name} ^" else "${state.mode.name} v", style = style, hovered = layout.modeSelectRect.contains(hoverX, hoverY), selected = modeDropdownOpen @@ -128,14 +174,16 @@ internal class SystemColorPickerPopupBodyNode( if (showOrder) { val rgbaRect = layout.rgbaOrderRect val argbRect = layout.argbOrderRect - applyButtonVisual( + syncPickerButtonVisual( button = rgbaOrderButton, + text = null, style = style, hovered = rgbaRect.contains(hoverX, hoverY), selected = state.rgbOrder == RgbChannelOrder.RGBA ) - applyButtonVisual( + syncPickerButtonVisual( button = argbOrderButton, + text = null, style = style, hovered = argbRect.contains(hoverX, hoverY), selected = state.rgbOrder == RgbChannelOrder.ARGB @@ -173,36 +221,56 @@ internal class SystemColorPickerPopupBodyNode( renderNode(ctx, previousSwatchNode, layout.previousSwatchRect) renderNode(ctx, currentSwatchNode, layout.currentSwatchRect) - applyButtonVisual( + syncPickerButtonVisual( button = copyButton, + text = null, style = style, hovered = layout.copyRect.contains(hoverX, hoverY), selected = false ) - applyButtonVisual( + syncPickerButtonVisual( button = pasteButton, + text = null, style = style, hovered = layout.pasteRect.contains(hoverX, hoverY), selected = false ) - applyButtonVisual( + syncPickerButtonVisual( button = pipetteButton, + text = if (controller.isEyedropperActive()) "Pick..." else "Pipette", style = style, hovered = layout.pipetteRect.contains(hoverX, hoverY), selected = controller.isEyedropperActive() ) - pipetteButton.text = if (controller.isEyedropperActive()) "Pick..." else "Pipette" renderNode(ctx, copyButton, layout.copyRect) renderNode(ctx, pasteButton, layout.pasteRect) renderNode(ctx, pipetteButton, layout.pipetteRect) + } - val definitionsByKey = controller.viewInputDefinitions().associate { it.first to it.second } + private fun renderInputRows( + ctx: UiMeasureContext, + controller: ColorPickerController, + layout: ColorPickerLayout, + style: ColorPickerStyle, + hoverX: Int, + hoverY: Int, + nowMs: Long, + activeInputKey: String?, + activeInputBuffer: String, + inputValues: Map, + definitionsByKey: Map + ) { for (index in 0 until MAX_INPUT_SLOTS) { val inputSlot = layout.inputSlots.getOrNull(index) val labelNode = inputLabelNodes[index] val inputNode = inputValueNodes[index] if (inputSlot == null) { inputLabelValues[index] = "" + syncTextNodeVisual( + node = labelNode, + text = "", + color = style.mutedTextColor + ) renderNode(ctx, labelNode, null) renderNode(ctx, inputNode, null) continue @@ -211,7 +279,11 @@ internal class SystemColorPickerPopupBodyNode( val key = inputSlot.key val label = definitionsByKey[key] ?: inputSlot.label inputLabelValues[index] = label - labelNode.color = style.mutedTextColor + syncTextNodeVisual( + node = labelNode, + text = label, + color = style.mutedTextColor + ) val borderColor = when { activeInputKey == key -> style.inputActiveBorderColor @@ -223,19 +295,30 @@ internal class SystemColorPickerPopupBodyNode( } else { inputValues[key].orEmpty() } - if (inputNode.text != value) { - inputNode.text = value - } - inputNode.border = Border.all(1, borderColor) - inputNode.backgroundColor = style.inputBackgroundColor - inputNode.focusedBackgroundColor = style.inputBackgroundColor - inputNode.textColor = style.textColor - inputNode.placeholderColor = style.mutedTextColor - inputNode.fontSize = style.fontSize + syncTextInputVisual( + node = inputNode, + value = value, + border = Border.all(1, borderColor), + background = style.inputBackgroundColor, + focusedBackground = style.inputBackgroundColor, + textColor = style.textColor, + placeholderColor = style.mutedTextColor, + fontSize = style.fontSize + ) renderNode(ctx, labelNode, inputSlot.labelRect) renderNode(ctx, inputNode, inputSlot.inputRect) } + } + + private fun renderRecentSwatchGrid( + ctx: UiMeasureContext, + layout: ColorPickerLayout, + style: ColorPickerStyle, + hoverX: Int, + hoverY: Int, + recentColors: List + ) { val hoveredRecent = layout.recentRects.indexOfFirst { it.contains(hoverX, hoverY) } for (index in 0 until RECENT_SWATCH_COUNT) { val swatchNode = recentSwatchNodes[index] @@ -285,15 +368,102 @@ internal class SystemColorPickerPopupBodyNode( } } - private fun applyButtonVisual(button: ButtonNode, style: ColorPickerStyle, hovered: Boolean, selected: Boolean) { - button.backgroundColor = when { + private fun syncPickerButtonVisual( + button: ButtonNode, + text: String?, + style: ColorPickerStyle, + hovered: Boolean, + selected: Boolean + ) { + val nextBackground = when { selected -> style.buttonActiveColor hovered -> style.buttonHoverColor else -> style.buttonBackgroundColor } - button.border = Border.all(1, if (selected) style.inputActiveBorderColor else style.inputBorderColor) - button.textColor = style.textColor - button.fontSize = style.fontSize + val nextBorder = Border.all(1, if (selected) style.inputActiveBorderColor else style.inputBorderColor) + var changed = false + if (text != null && button.text != text) { + button.text = text + changed = true + } + if (button.backgroundColor != nextBackground) { + button.backgroundColor = nextBackground + changed = true + } + if (button.border != nextBorder) { + button.border = nextBorder + changed = true + } + if (button.textColor != style.textColor) { + button.textColor = style.textColor + changed = true + } + if (button.fontSize != style.fontSize) { + button.fontSize = style.fontSize + changed = true + } + if (changed) { + button.requestRenderCommandsInvalidation() + } + } + + private fun syncTextNodeVisual(node: TextNode, text: String, color: Int) { + var changed = false + if (node.text != text) { + node.setText(text) + changed = false + } + if (node.color != color) { + node.color = color + changed = true + } + if (changed) { + node.requestRenderCommandsInvalidation() + } + } + + private fun syncTextInputVisual( + node: TextInputNode, + value: String, + border: Border, + background: Int, + focusedBackground: Int, + textColor: Int, + placeholderColor: Int, + fontSize: Int + ) { + var changed = false + if (node.text != value) { + node.text = value + changed = true + } + if (node.border != border) { + node.border = border + changed = true + } + if (node.backgroundColor != background) { + node.backgroundColor = background + changed = true + } + if (node.focusedBackgroundColor != focusedBackground) { + node.focusedBackgroundColor = focusedBackground + changed = true + } + if (node.textColor != textColor) { + node.textColor = textColor + changed = true + } + if (node.placeholderColor != placeholderColor) { + node.placeholderColor = placeholderColor + changed = true + } + if (node.fontSize != fontSize) { + node.fontSize = fontSize + changed = true + } + if (changed) { + node.requestRenderCommandsInvalidation() + } } @@ -390,8 +560,11 @@ internal class SystemColorPickerModeDropdownOverlayNode( val hoverY = hover.second popupBackgroundNode.display = Display.Block - popupBackgroundNode.backgroundColor = style.inputBackgroundColor - popupBackgroundNode.border = Border.all(1, style.inputBorderColor) + syncContainerVisual( + node = popupBackgroundNode, + backgroundColor = style.inputBackgroundColor, + border = Border.all(1, style.inputBorderColor) + ) popupBackgroundNode.render(ctx, popupRect.x, popupRect.y, popupRect.width, popupRect.height) val state = controller.snapshot() @@ -406,7 +579,13 @@ internal class SystemColorPickerModeDropdownOverlayNode( } val hovered = optionRect.contains(hoverX, hoverY) val selected = state.mode == mode - applyButtonVisual(button = button, style = style, hovered = hovered, selected = selected) + syncPickerButtonVisual( + button = button, + text = null, + style = style, + hovered = hovered, + selected = selected + ) button.display = Display.Block button.render(ctx, optionRect.x, optionRect.y, optionRect.width, optionRect.height) } @@ -423,15 +602,58 @@ internal class SystemColorPickerModeDropdownOverlayNode( } } - private fun applyButtonVisual(button: ButtonNode, style: ColorPickerStyle, hovered: Boolean, selected: Boolean) { - button.backgroundColor = when { + private fun syncPickerButtonVisual( + button: ButtonNode, + text: String?, + style: ColorPickerStyle, + hovered: Boolean, + selected: Boolean + ) { + val nextBackground = when { selected -> style.buttonActiveColor hovered -> style.buttonHoverColor else -> style.buttonBackgroundColor } - button.border = Border.all(1, if (selected) style.inputActiveBorderColor else style.inputBorderColor) - button.textColor = style.textColor - button.fontSize = style.fontSize + val nextBorder = Border.all(1, if (selected) style.inputActiveBorderColor else style.inputBorderColor) + var changed = false + if (text != null && button.text != text) { + button.text = text + changed = true + } + if (button.backgroundColor != nextBackground) { + button.backgroundColor = nextBackground + changed = true + } + if (button.border != nextBorder) { + button.border = nextBorder + changed = true + } + if (button.textColor != style.textColor) { + button.textColor = style.textColor + changed = true + } + if (button.fontSize != style.fontSize) { + button.fontSize = style.fontSize + changed = true + } + if (changed) { + button.requestRenderCommandsInvalidation() + } + } + + private fun syncContainerVisual(node: ContainerNode, backgroundColor: Int?, border: Border) { + var changed = false + if (node.backgroundColor != backgroundColor) { + node.backgroundColor = backgroundColor + changed = true + } + if (node.border != border) { + node.border = border + changed = true + } + if (changed) { + node.requestRenderCommandsInvalidation() + } } private fun hideAll(ctx: UiMeasureContext) { @@ -500,19 +722,24 @@ internal class SystemColorPickerEyedropperOverlayNode( val color = controller.snapshot().color syncOverlayText(model.modeText, model.valueText) - shadowNode.backgroundColor = style.panelShadowColor - shadowNode.border = Border.NONE - - panelNode.backgroundColor = style.eyedropperOverlayBackgroundColor - panelNode.border = Border.all(1, style.eyedropperOverlayBorderColor) - - centerNode.backgroundColor = null - centerNode.border = Border.all(1, style.eyedropperCenterBorderColor) + syncContainerVisual( + node = shadowNode, + backgroundColor = style.panelShadowColor, + border = Border.NONE + ) + syncContainerVisual( + node = panelNode, + backgroundColor = style.eyedropperOverlayBackgroundColor, + border = Border.all(1, style.eyedropperOverlayBorderColor) + ) + syncContainerVisual( + node = centerNode, + backgroundColor = null, + border = Border.all(1, style.eyedropperCenterBorderColor) + ) - modeTextNode.color = style.mutedTextColor - modeTextNode.fontSize = style.fontSize - valueTextNode.color = style.textColor - valueTextNode.fontSize = style.fontSize + syncOverlayTextVisual(modeTextNode, style.mutedTextColor, style.fontSize) + syncOverlayTextVisual(valueTextNode, style.textColor, style.fontSize) captureNode.bind( sourceRect = model.captureSourceRect, @@ -560,10 +787,40 @@ internal class SystemColorPickerEyedropperOverlayNode( private fun syncOverlayText(modeText: String, valueText: String) { if (modeTextNode.text != modeText) { - modeTextNode.syncSourceFrom(TextNode(TextSource.Static(modeText))) + modeTextNode.setText(modeText) } if (valueTextNode.text != valueText) { - valueTextNode.syncSourceFrom(TextNode(TextSource.Static(valueText))) + valueTextNode.setText(valueText) + } + } + + private fun syncOverlayTextVisual(node: TextNode, color: Int, fontSize: Int) { + var changed = false + if (node.color != color) { + node.color = color + changed = true + } + if (node.fontSize != fontSize) { + node.fontSize = fontSize + changed = true + } + if (changed) { + node.requestRenderCommandsInvalidation() + } + } + + private fun syncContainerVisual(node: ContainerNode, backgroundColor: Int?, border: Border) { + var changed = false + if (node.backgroundColor != backgroundColor) { + node.backgroundColor = backgroundColor + changed = true + } + if (node.border != border) { + node.border = border + changed = true + } + if (changed) { + node.requestRenderCommandsInvalidation() } } @@ -600,6 +857,9 @@ private class EyedropperCaptureNode( private var fallbackColor: Int = 0 fun bind(sourceRect: Rect, fallbackColor: Int) { + if (this.sourceRect != sourceRect || this.fallbackColor != fallbackColor) { + markRenderCommandsDirty() + } this.sourceRect = sourceRect this.fallbackColor = fallbackColor } @@ -638,9 +898,20 @@ private class EyedropperMagnifierDrawNode( private var gridColor: Int = 0x66FFFFFF fun bind(columns: Int, rows: Int, cellSize: Int, gridEnabled: Boolean, gridColor: Int) { - this.columns = columns.coerceAtLeast(1) - this.rows = rows.coerceAtLeast(1) - this.cellSize = cellSize.coerceAtLeast(1) + val nextColumns = columns.coerceAtLeast(1) + val nextRows = rows.coerceAtLeast(1) + val nextCellSize = cellSize.coerceAtLeast(1) + if (this.columns != nextColumns || + this.rows != nextRows || + this.cellSize != nextCellSize || + this.gridEnabled != gridEnabled || + this.gridColor != gridColor + ) { + markRenderCommandsDirty() + } + this.columns = nextColumns + this.rows = nextRows + this.cellSize = nextCellSize this.gridEnabled = gridEnabled this.gridColor = gridColor } @@ -687,11 +958,20 @@ private class ColorFieldSurfaceNode( private var brightness: Float = 1f fun bind(style: ColorPickerStyle, color: RgbaColor, hueDeg: Float) { + val hsv = ColorConversions.rgbToHsv(color, hueDeg) + val nextSaturation = hsv.saturation + val nextBrightness = hsv.brightness + if (this.style != style || + this.hueDeg != hueDeg || + saturation != nextSaturation || + brightness != nextBrightness + ) { + markRenderCommandsDirty() + } this.style = style this.hueDeg = hueDeg - val hsv = ColorConversions.rgbToHsv(color, hueDeg) - saturation = hsv.saturation - brightness = hsv.brightness + saturation = nextSaturation + brightness = nextBrightness } override fun measure(ctx: UiMeasureContext): Size { @@ -729,6 +1009,9 @@ private class HueSurfaceNode( private var hueDeg: Float = 0f fun bind(style: ColorPickerStyle, hueDeg: Float) { + if (this.style != style || this.hueDeg != hueDeg) { + markRenderCommandsDirty() + } this.style = style this.hueDeg = hueDeg } @@ -764,6 +1047,9 @@ private class AlphaSurfaceNode( private var color: RgbaColor = RgbaColor.WHITE fun bind(style: ColorPickerStyle, color: RgbaColor) { + if (this.style != style || this.color != color) { + markRenderCommandsDirty() + } this.style = style this.color = color } @@ -803,6 +1089,9 @@ private class ColorSwatchSurfaceNode( private var highlighted: Boolean = false fun bind(style: ColorPickerStyle, color: RgbaColor?, highlighted: Boolean) { + if (this.style != style || this.color != color || this.highlighted != highlighted) { + markRenderCommandsDirty() + } this.style = style this.color = color this.highlighted = highlighted diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/DOMNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/DOMNode.kt index dc7fc63..a13aca1 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/DOMNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/DOMNode.kt @@ -1795,6 +1795,10 @@ abstract class DOMNode( renderCommandsRevision += 1L } + internal fun requestRenderCommandsInvalidation() { + markRenderCommandsDirty() + } + fun effectiveTransform(): UiTransform { val base = animatedTransform ?: transform val relativeOffsetX = if (position == PositionMode.Relative) relativeVisualOffsetXPx else 0 diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt index b7be430..bf840e5 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt @@ -13,6 +13,7 @@ import org.dreamfinity.dsgl.core.colorpicker.ColorPickerRuntime import org.dreamfinity.dsgl.core.colorpicker.ColorPickerStyle import org.dreamfinity.dsgl.core.colorpicker.ColorPickerState import org.dreamfinity.dsgl.core.colorpicker.RgbaColor +import org.dreamfinity.dsgl.core.colorpicker.RgbChannelOrder import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode @@ -282,6 +283,69 @@ class SystemOverlayColorPickerEntryTests { assertNotEquals(0.3f, state.color.r) } + @Test + fun `system picker sync state updates current swatch without drag nudge`() { + val host = SystemOverlayHost(InspectorController()) + val pickerHost = host.systemInspectorColorPickerPopupHost() + val root = inspectedRoot() + val initial = popupState() + val updated = initial.copy( + color = RgbaColor(0.92f, 0.16f, 0.24f, 1f), + previous = initial.color + ) + + pickerHost.open(anchorRect = Rect(80, 90, 20, 18), title = "Popup", state = initial) + host.onInputFrame(1200, 800) + host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 88, cursorY = 98, inspectorPointerCaptured = false) + + val swatchRect = host.debugSystemColorPickerBodyLayout()?.currentSwatchRect ?: error("swatch rect missing") + host.render(ctx, 1200, 800) + val beforeColor = resolveRectFillColor(host.paint(ctx), swatchRect) ?: error("before swatch fill missing") + assertEquals(initial.color.toArgbInt(), beforeColor) + + pickerHost.open(anchorRect = Rect(80, 90, 20, 18), title = "Popup", state = updated) + host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 88, cursorY = 98, inspectorPointerCaptured = false) + + host.render(ctx, 1200, 800) + val afterColor = resolveRectFillColor(host.paint(ctx), swatchRect) ?: error("after swatch fill missing") + assertEquals(updated.color.toArgbInt(), afterColor) + assertNotEquals(beforeColor, afterColor) + } + + @Test + fun `system picker sync state updates rgb order button selected visuals without drag nudge`() { + val host = SystemOverlayHost(InspectorController()) + val pickerHost = host.systemInspectorColorPickerPopupHost() + val root = inspectedRoot() + val style = ColorPickerStyle() + val initial = popupState().copy(mode = ColorFormatMode.RGB, rgbOrder = RgbChannelOrder.RGBA) + val updated = initial.copy(rgbOrder = RgbChannelOrder.ARGB) + + pickerHost.open(anchorRect = Rect(80, 90, 20, 18), title = "Popup", state = initial) + host.onInputFrame(1200, 800) + host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 2, cursorY = 2, inspectorPointerCaptured = false) + + val initialLayout = host.debugSystemColorPickerBodyLayout() ?: error("layout missing") + val rgbaRect = initialLayout.rgbaOrderRect ?: error("rgba rect missing") + val argbRect = initialLayout.argbOrderRect ?: error("argb rect missing") + host.render(ctx, 1200, 800) + val rgbaBefore = resolveRectFillColor(host.paint(ctx), rgbaRect) ?: error("rgba fill missing") + val argbBefore = resolveRectFillColor(host.paint(ctx), argbRect) ?: error("argb fill missing") + assertEquals(style.buttonActiveColor, rgbaBefore) + assertEquals(style.buttonBackgroundColor, argbBefore) + + pickerHost.open(anchorRect = Rect(80, 90, 20, 18), title = "Popup", state = updated) + host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 2, cursorY = 2, inspectorPointerCaptured = false) + + host.render(ctx, 1200, 800) + val rgbaAfter = resolveRectFillColor(host.paint(ctx), rgbaRect) ?: error("rgba fill missing after sync") + val argbAfter = resolveRectFillColor(host.paint(ctx), argbRect) ?: error("argb fill missing after sync") + assertEquals(style.buttonBackgroundColor, rgbaAfter) + assertEquals(style.buttonActiveColor, argbAfter) + assertNotEquals(rgbaBefore, rgbaAfter) + assertNotEquals(argbBefore, argbAfter) + } + @Test fun `system picker hue and alpha drag update state`() { val host = SystemOverlayHost(InspectorController()) @@ -493,5 +557,17 @@ class SystemOverlayColorPickerEntryTests { }.applyParent(root) return root } + + private fun resolveRectFillColor(commands: List, rect: Rect): Int? { + return commands.asReversed().asSequence() + .filterIsInstance() + .firstOrNull { command -> + command.x == rect.x && + command.y == rect.y && + command.width == rect.width && + command.height == rect.height + } + ?.color + } } From 5d438437b3d90bd69dec38384247749f97ac49b0 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Tue, 21 Apr 2026 20:05:58 +0300 Subject: [PATCH 19/78] refactoring and streamlining color picker popup rendering logic; --- .../SystemColorPickerPopupBodyNode.kt | 189 ++++++++++++------ 1 file changed, 124 insertions(+), 65 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt index 7b1bcd4..d49cbf3 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt @@ -529,46 +529,69 @@ internal class SystemColorPickerModeDropdownOverlayNode( } private var appliedStyle: ColorPickerStyle? = null + private data class ModeDropdownRenderState( + val style: ColorPickerStyle, + val layout: ColorPickerLayout, + val popupRect: Rect, + val hoverX: Int, + val hoverY: Int, + val selectedMode: ColorFormatMode + ) + override fun measure(ctx: UiMeasureContext): Size { return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) } override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { bounds = Rect(x, y, width, height) - val controller = popupEngine.debugActiveController() - val panelRect = popupEngine.debugActivePanelRect() - if (controller == null || panelRect == null || !controller.viewModeDropdownOpen()) { + val renderState = resolveRenderState() ?: run { hideAll(ctx) return } + renderPopupBackground(ctx, renderState) + renderModeOptions(ctx, renderState) + } + + private fun resolveRenderState(): ModeDropdownRenderState? { + val controller = popupEngine.debugActiveController() ?: return null + val panelRect = popupEngine.debugActivePanelRect() + if (panelRect == null || !controller.viewModeDropdownOpen()) return null val style = popupEngine.debugActiveStyle() ?: controller.style() if (appliedStyle != style) { applyStaticStyle(style) appliedStyle = style } - - val layout = popupEngine.debugActiveLayout() ?: run { - hideAll(ctx) - return - } - val popupRect = layout.modeOptionsRect ?: run { - hideAll(ctx) - return - } + val layout = popupEngine.debugActiveLayout() ?: return null + val popupRect = layout.modeOptionsRect ?: return null val hover = controller.viewHoverPosition() - val hoverX = hover.first - val hoverY = hover.second + return ModeDropdownRenderState( + style = style, + layout = layout, + popupRect = popupRect, + hoverX = hover.first, + hoverY = hover.second, + selectedMode = controller.snapshot().mode + ) + } + private fun renderPopupBackground(ctx: UiMeasureContext, state: ModeDropdownRenderState) { popupBackgroundNode.display = Display.Block syncContainerVisual( node = popupBackgroundNode, - backgroundColor = style.inputBackgroundColor, - border = Border.all(1, style.inputBorderColor) + backgroundColor = state.style.inputBackgroundColor, + border = Border.all(1, state.style.inputBorderColor) + ) + popupBackgroundNode.render( + ctx, + state.popupRect.x, + state.popupRect.y, + state.popupRect.width, + state.popupRect.height ) - popupBackgroundNode.render(ctx, popupRect.x, popupRect.y, popupRect.width, popupRect.height) + } - val state = controller.snapshot() - val optionsByMode = layout.modeOptions.associateBy { it.mode } + private fun renderModeOptions(ctx: UiMeasureContext, state: ModeDropdownRenderState) { + val optionsByMode = state.layout.modeOptions.associateBy { it.mode } ColorFormatMode.entries.forEach { mode -> val button = modeOptionButtons[mode] ?: return@forEach val optionRect = optionsByMode[mode]?.rect @@ -577,14 +600,12 @@ internal class SystemColorPickerModeDropdownOverlayNode( button.render(ctx, 0, 0, 0, 0) return@forEach } - val hovered = optionRect.contains(hoverX, hoverY) - val selected = state.mode == mode syncPickerButtonVisual( button = button, text = null, - style = style, - hovered = hovered, - selected = selected + style = state.style, + hovered = optionRect.contains(state.hoverX, state.hoverY), + selected = state.selectedMode == mode ) button.display = Display.Block button.render(ctx, optionRect.x, optionRect.y, optionRect.width, optionRect.height) @@ -701,88 +722,126 @@ internal class SystemColorPickerEyedropperOverlayNode( text = "" ) + private data class EyedropperRenderState( + val model: ColorPickerEyedropperOverlayModel, + val style: ColorPickerStyle, + val color: RgbaColor + ) + + private data class EyedropperTextRects( + val modeRect: Rect, + val valueRect: Rect + ) + override fun measure(ctx: UiMeasureContext): Size { return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) } override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { bounds = Rect(x, y, width, height) - val controller = popupEngine.debugActiveController() ?: run { + val renderState = resolveRenderState() ?: run { hideAll(ctx) return } + + syncVisuals(renderState) + bindVisualNodes(renderState) + renderOverlayNodes(ctx, renderState) + } + + private fun resolveRenderState(): EyedropperRenderState? { + val controller = popupEngine.debugActiveController() ?: return null val model = controller.resolveEyedropperOverlayModel( viewportWidth = bounds.width.coerceAtLeast(1), viewportHeight = bounds.height.coerceAtLeast(1) - ) ?: run { - hideAll(ctx) - return - } + ) ?: return null val style = popupEngine.debugActiveStyle() ?: controller.style() - val color = controller.snapshot().color + return EyedropperRenderState( + model = model, + style = style, + color = controller.snapshot().color + ) + } - syncOverlayText(model.modeText, model.valueText) + private fun syncVisuals(state: EyedropperRenderState) { + syncOverlayText(state.model.modeText, state.model.valueText) syncContainerVisual( node = shadowNode, - backgroundColor = style.panelShadowColor, + backgroundColor = state.style.panelShadowColor, border = Border.NONE ) syncContainerVisual( node = panelNode, - backgroundColor = style.eyedropperOverlayBackgroundColor, - border = Border.all(1, style.eyedropperOverlayBorderColor) + backgroundColor = state.style.eyedropperOverlayBackgroundColor, + border = Border.all(1, state.style.eyedropperOverlayBorderColor) ) syncContainerVisual( node = centerNode, backgroundColor = null, - border = Border.all(1, style.eyedropperCenterBorderColor) + border = Border.all(1, state.style.eyedropperCenterBorderColor) ) + syncOverlayTextVisual(modeTextNode, state.style.mutedTextColor, state.style.fontSize) + syncOverlayTextVisual(valueTextNode, state.style.textColor, state.style.fontSize) + } - syncOverlayTextVisual(modeTextNode, style.mutedTextColor, style.fontSize) - syncOverlayTextVisual(valueTextNode, style.textColor, style.fontSize) - + private fun bindVisualNodes(state: EyedropperRenderState) { captureNode.bind( - sourceRect = model.captureSourceRect, - fallbackColor = color.toArgbInt() + sourceRect = state.model.captureSourceRect, + fallbackColor = state.color.toArgbInt() ) - swatchNode.bind(style = style, color = color, highlighted = false) + swatchNode.bind(style = state.style, color = state.color, highlighted = false) magnifierDrawNode.bind( - columns = model.captureSourceRect.width, - rows = model.captureSourceRect.height, - cellSize = (model.magnifierRect.width / model.captureSourceRect.width.coerceAtLeast(1)).coerceAtLeast(1), - gridEnabled = style.eyedropperGridOverlayEnabled, - gridColor = style.eyedropperGridOverlayColor + columns = state.model.captureSourceRect.width, + rows = state.model.captureSourceRect.height, + cellSize = ( + state.model.magnifierRect.width / + state.model.captureSourceRect.width.coerceAtLeast(1) + ).coerceAtLeast(1), + gridEnabled = state.style.eyedropperGridOverlayEnabled, + gridColor = state.style.eyedropperGridOverlayColor ) + } + private fun renderOverlayNodes(ctx: UiMeasureContext, state: EyedropperRenderState) { val shadowRect = Rect( - x = model.panelRect.x + 2, - y = model.panelRect.y + 2, - width = model.panelRect.width, - height = model.panelRect.height + x = state.model.panelRect.x + 2, + y = state.model.panelRect.y + 2, + width = state.model.panelRect.width, + height = state.model.panelRect.height ) - val textX = model.swatchRect.x + model.swatchRect.width + 8 - val textWidth = (model.panelRect.x + model.panelRect.width - 6 - textX).coerceAtLeast(1) + val textRects = resolveTextRects(state) + + renderNode(ctx, captureNode, state.model.panelRect) + renderNode(ctx, shadowNode, shadowRect) + renderNode(ctx, panelNode, state.model.panelRect) + renderNode(ctx, magnifierDrawNode, state.model.magnifierRect) + renderNode(ctx, centerNode, state.model.centerRect) + renderNode(ctx, swatchNode, state.model.swatchRect) + renderNode(ctx, modeTextNode, textRects.modeRect) + renderNode(ctx, valueTextNode, textRects.valueRect) + } + + private fun resolveTextRects(state: EyedropperRenderState): EyedropperTextRects { + val textX = state.model.swatchRect.x + state.model.swatchRect.width + 8 + val textWidth = ( + state.model.panelRect.x + state.model.panelRect.width - 6 - textX + ).coerceAtLeast(1) val modeRect = Rect( x = textX, - y = model.swatchRect.y + 1, + y = state.model.swatchRect.y + 1, width = textWidth, - height = (style.fontSize + 2).coerceAtLeast(1) + height = (state.style.fontSize + 2).coerceAtLeast(1) ) val valueRect = Rect( x = textX, - y = modeRect.y + style.fontSize, + y = modeRect.y + state.style.fontSize, width = textWidth, - height = (style.fontSize + 2).coerceAtLeast(1) + height = (state.style.fontSize + 2).coerceAtLeast(1) + ) + return EyedropperTextRects( + modeRect = modeRect, + valueRect = valueRect ) - - renderNode(ctx, captureNode, model.panelRect) - renderNode(ctx, shadowNode, shadowRect) - renderNode(ctx, panelNode, model.panelRect) - renderNode(ctx, magnifierDrawNode, model.magnifierRect) - renderNode(ctx, centerNode, model.centerRect) - renderNode(ctx, swatchNode, model.swatchRect) - renderNode(ctx, modeTextNode, modeRect) - renderNode(ctx, valueTextNode, valueRect) } private fun syncOverlayText(modeText: String, valueText: String) { From 48717e6facab6cfcaec5528f16c9c6d9ecadbbfc Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Tue, 21 Apr 2026 21:03:09 +0300 Subject: [PATCH 20/78] move custom nodes into separate files; --- .../SystemColorPickerCustomSurfaceNodes.kt | 309 ++++++++++++++++++ .../SystemColorPickerPopupBodyNode.kt | 300 ----------------- 2 files changed, 309 insertions(+), 300 deletions(-) create mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerCustomSurfaceNodes.kt diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerCustomSurfaceNodes.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerCustomSurfaceNodes.kt new file mode 100644 index 0000000..3931d9a --- /dev/null +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerCustomSurfaceNodes.kt @@ -0,0 +1,309 @@ +package org.dreamfinity.dsgl.core.colorpicker.internal + +import org.dreamfinity.dsgl.core.colorpicker.* +import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.dom.layout.Size +import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.render.RenderCommand +import org.dreamfinity.dsgl.core.style.Display +import kotlin.math.roundToInt + +internal class EyedropperCaptureNode( + key: Any? +) : DOMNode(key) { + override val styleType: String = "dsgl-system-color-picker-eyedropper-capture" + + private var sourceRect: Rect? = null + private var fallbackColor: Int = 0 + + fun bind(sourceRect: Rect, fallbackColor: Int) { + if (this.sourceRect != sourceRect || this.fallbackColor != fallbackColor) { + markRenderCommandsDirty() + } + this.sourceRect = sourceRect + this.fallbackColor = fallbackColor + } + + override fun measure(ctx: UiMeasureContext): Size { + return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) + } + + override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + bounds = Rect(x, y, width, height) + } + + override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) { + if (display == Display.None) return + val source = sourceRect ?: return + if (source.width <= 0 || source.height <= 0) return + out += RenderCommand.CaptureScreenRegion( + sourceX = source.x, + sourceY = source.y, + sourceWidth = source.width, + sourceHeight = source.height, + fallbackColor = fallbackColor + ) + } +} + +internal class EyedropperMagnifierDrawNode( + key: Any? +) : DOMNode(key) { + override val styleType: String = "dsgl-system-color-picker-eyedropper-magnifier" + + private var columns: Int = 1 + private var rows: Int = 1 + private var cellSize: Int = 1 + private var gridEnabled: Boolean = true + private var gridColor: Int = 0x66FFFFFF + + fun bind(columns: Int, rows: Int, cellSize: Int, gridEnabled: Boolean, gridColor: Int) { + val nextColumns = columns.coerceAtLeast(1) + val nextRows = rows.coerceAtLeast(1) + val nextCellSize = cellSize.coerceAtLeast(1) + if (this.columns != nextColumns || + this.rows != nextRows || + this.cellSize != nextCellSize || + this.gridEnabled != gridEnabled || + this.gridColor != gridColor + ) { + markRenderCommandsDirty() + } + this.columns = nextColumns + this.rows = nextRows + this.cellSize = nextCellSize + this.gridEnabled = gridEnabled + this.gridColor = gridColor + } + + override fun measure(ctx: UiMeasureContext): Size { + return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) + } + + override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + bounds = Rect(x, y, width, height) + } + + override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) { + if (display == Display.None) return + if (bounds.width <= 0 || bounds.height <= 0) return + out += RenderCommand.DrawCapturedScreenRegion( + x = bounds.x, + y = bounds.y, + width = bounds.width, + height = bounds.height + ) + if (!gridEnabled) return + for (column in 1 until columns) { + val lineX = bounds.x + column * cellSize + if (lineX <= bounds.x || lineX >= bounds.x + bounds.width) continue + out += RenderCommand.DrawRect(lineX, bounds.y, 1, bounds.height, gridColor) + } + for (row in 1 until rows) { + val lineY = bounds.y + row * cellSize + if (lineY <= bounds.y || lineY >= bounds.y + bounds.height) continue + out += RenderCommand.DrawRect(bounds.x, lineY, bounds.width, 1, gridColor) + } + } +} + +internal class ColorFieldSurfaceNode( + key: Any? +) : DOMNode(key) { + override val styleType: String = "dsgl-system-color-picker-color-field" + + private var style: ColorPickerStyle = ColorPickerStyle() + private var hueDeg: Float = 0f + private var saturation: Float = 0f + private var brightness: Float = 1f + + fun bind(style: ColorPickerStyle, color: RgbaColor, hueDeg: Float) { + val hsv = ColorConversions.rgbToHsv(color, hueDeg) + val nextSaturation = hsv.saturation + val nextBrightness = hsv.brightness + if (this.style != style || + this.hueDeg != hueDeg || + saturation != nextSaturation || + brightness != nextBrightness + ) { + markRenderCommandsDirty() + } + this.style = style + this.hueDeg = hueDeg + saturation = nextSaturation + brightness = nextBrightness + } + + override fun measure(ctx: UiMeasureContext): Size { + return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) + } + + override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + bounds = Rect(x, y, width, height) + } + + override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) { + if (bounds.width <= 0 || bounds.height <= 0) return + out += RenderCommand.DrawColorField( + x = bounds.x, + y = bounds.y, + width = bounds.width, + height = bounds.height, + hueDeg = hueDeg + ) + drawBorder(out, bounds, style.inputBorderColor) + val thumbX = bounds.x + (saturation * bounds.width.toFloat()).roundToInt().coerceIn(0, bounds.width - 1) + val thumbY = + bounds.y + ((1f - brightness) * bounds.height.toFloat()).roundToInt().coerceIn(0, bounds.height - 1) + out += RenderCommand.DrawRect(thumbX - 3, thumbY - 3, 7, 7, style.thumbShadowColor) + drawBorder(out, Rect(thumbX - 2, thumbY - 2, 5, 5), style.thumbOutlineColor) + } +} + +internal class HueSurfaceNode( + key: Any? +) : DOMNode(key) { + override val styleType: String = "dsgl-system-color-picker-hue-slider" + + private var style: ColorPickerStyle = ColorPickerStyle() + private var hueDeg: Float = 0f + + fun bind(style: ColorPickerStyle, hueDeg: Float) { + if (this.style != style || this.hueDeg != hueDeg) { + markRenderCommandsDirty() + } + this.style = style + this.hueDeg = hueDeg + } + + override fun measure(ctx: UiMeasureContext): Size { + return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) + } + + override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + bounds = Rect(x, y, width, height) + } + + override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) { + if (bounds.width <= 0 || bounds.height <= 0) return + out += RenderCommand.DrawHueBar( + x = bounds.x, + y = bounds.y, + width = bounds.width, + height = bounds.height + ) + drawBorder(out, bounds, style.inputBorderColor) + val thumbX = bounds.x + ((hueDeg / 360f) * bounds.width.toFloat()).roundToInt().coerceIn(0, bounds.width - 1) + out += RenderCommand.DrawRect(thumbX - 1, bounds.y - 1, 3, bounds.height + 2, style.thumbOutlineColor) + } +} + +internal class AlphaSurfaceNode( + key: Any? +) : DOMNode(key) { + override val styleType: String = "dsgl-system-color-picker-alpha-slider" + + private var style: ColorPickerStyle = ColorPickerStyle() + private var color: RgbaColor = RgbaColor.WHITE + + fun bind(style: ColorPickerStyle, color: RgbaColor) { + if (this.style != style || this.color != color) { + markRenderCommandsDirty() + } + this.style = style + this.color = color + } + + override fun measure(ctx: UiMeasureContext): Size { + return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) + } + + override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + bounds = Rect(x, y, width, height) + } + + override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) { + if (bounds.width <= 0 || bounds.height <= 0) return + drawChecker(out, bounds, style) + out += RenderCommand.DrawAlphaBar( + x = bounds.x, + y = bounds.y, + width = bounds.width, + height = bounds.height, + rgbColor = color.copy(a = 1f).toArgbInt() + ) + drawBorder(out, bounds, style.inputBorderColor) + val thumbX = bounds.x + (color.a * bounds.width.toFloat()).roundToInt().coerceIn(0, bounds.width - 1) + out += RenderCommand.DrawRect(thumbX - 1, bounds.y - 1, 3, bounds.height + 2, style.thumbOutlineColor) + } +} + +internal class ColorSwatchSurfaceNode( + private val allowEmpty: Boolean = false, + key: Any? +) : DOMNode(key) { + override val styleType: String = "dsgl-system-color-picker-swatch" + + private var style: ColorPickerStyle = ColorPickerStyle() + private var color: RgbaColor? = RgbaColor.WHITE + private var highlighted: Boolean = false + + fun bind(style: ColorPickerStyle, color: RgbaColor?, highlighted: Boolean) { + if (this.style != style || this.color != color || this.highlighted != highlighted) { + markRenderCommandsDirty() + } + this.style = style + this.color = color + this.highlighted = highlighted + } + + override fun measure(ctx: UiMeasureContext): Size { + return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) + } + + override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + bounds = Rect(x, y, width, height) + } + + override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) { + if (bounds.width <= 0 || bounds.height <= 0) return + val localColor = color + if (localColor == null && allowEmpty) { + out += RenderCommand.DrawRect(bounds.x, bounds.y, bounds.width, bounds.height, 0x33222A34) + drawBorder(out, bounds, if (highlighted) style.inputActiveBorderColor else style.recentGridBorderColor) + return + } + drawChecker(out, bounds, style) + out += RenderCommand.DrawRect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + (localColor ?: RgbaColor.WHITE).toArgbInt() + ) + drawBorder(out, bounds, if (highlighted) style.inputActiveBorderColor else style.inputBorderColor) + } +} + +private fun drawChecker(out: MutableList, rect: Rect, style: ColorPickerStyle) { + if (rect.width <= 0 || rect.height <= 0) return + out += RenderCommand.DrawCheckerboard( + x = rect.x, + y = rect.y, + width = rect.width, + height = rect.height, + cellSize = 4, + lightColor = style.checkerLightColor, + darkColor = style.checkerDarkColor + ) +} + +private fun drawBorder(out: MutableList, rect: Rect, color: Int) { + if (rect.width <= 0 || rect.height <= 0) return + out += RenderCommand.DrawRect(rect.x, rect.y, rect.width, 1, color) + out += RenderCommand.DrawRect(rect.x, rect.y + rect.height - 1, rect.width, 1, color) + out += RenderCommand.DrawRect(rect.x, rect.y, 1, rect.height, color) + out += RenderCommand.DrawRect(rect.x + rect.width - 1, rect.y, 1, rect.height, color) +} + diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt index d49cbf3..608fa18 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt @@ -12,10 +12,8 @@ import org.dreamfinity.dsgl.core.dsl.UiScope import org.dreamfinity.dsgl.core.dsl.button import org.dreamfinity.dsgl.core.dsl.div import org.dreamfinity.dsgl.core.dsl.text -import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.TextWrap -import kotlin.math.roundToInt internal class SystemColorPickerPopupBodyNode( private val popupEngine: ColorPickerPopupEngine, @@ -907,301 +905,3 @@ internal class SystemColorPickerEyedropperOverlayNode( } } -private class EyedropperCaptureNode( - key: Any? -) : DOMNode(key) { - override val styleType: String = "dsgl-system-color-picker-eyedropper-capture" - - private var sourceRect: Rect? = null - private var fallbackColor: Int = 0 - - fun bind(sourceRect: Rect, fallbackColor: Int) { - if (this.sourceRect != sourceRect || this.fallbackColor != fallbackColor) { - markRenderCommandsDirty() - } - this.sourceRect = sourceRect - this.fallbackColor = fallbackColor - } - - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } - - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { - bounds = Rect(x, y, width, height) - } - - override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) { - if (display == Display.None) return - val source = sourceRect ?: return - if (source.width <= 0 || source.height <= 0) return - out += RenderCommand.CaptureScreenRegion( - sourceX = source.x, - sourceY = source.y, - sourceWidth = source.width, - sourceHeight = source.height, - fallbackColor = fallbackColor - ) - } -} - -private class EyedropperMagnifierDrawNode( - key: Any? -) : DOMNode(key) { - override val styleType: String = "dsgl-system-color-picker-eyedropper-magnifier" - - private var columns: Int = 1 - private var rows: Int = 1 - private var cellSize: Int = 1 - private var gridEnabled: Boolean = true - private var gridColor: Int = 0x66FFFFFF - - fun bind(columns: Int, rows: Int, cellSize: Int, gridEnabled: Boolean, gridColor: Int) { - val nextColumns = columns.coerceAtLeast(1) - val nextRows = rows.coerceAtLeast(1) - val nextCellSize = cellSize.coerceAtLeast(1) - if (this.columns != nextColumns || - this.rows != nextRows || - this.cellSize != nextCellSize || - this.gridEnabled != gridEnabled || - this.gridColor != gridColor - ) { - markRenderCommandsDirty() - } - this.columns = nextColumns - this.rows = nextRows - this.cellSize = nextCellSize - this.gridEnabled = gridEnabled - this.gridColor = gridColor - } - - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } - - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { - bounds = Rect(x, y, width, height) - } - - override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) { - if (display == Display.None) return - if (bounds.width <= 0 || bounds.height <= 0) return - out += RenderCommand.DrawCapturedScreenRegion( - x = bounds.x, - y = bounds.y, - width = bounds.width, - height = bounds.height - ) - if (!gridEnabled) return - for (column in 1 until columns) { - val lineX = bounds.x + column * cellSize - if (lineX <= bounds.x || lineX >= bounds.x + bounds.width) continue - out += RenderCommand.DrawRect(lineX, bounds.y, 1, bounds.height, gridColor) - } - for (row in 1 until rows) { - val lineY = bounds.y + row * cellSize - if (lineY <= bounds.y || lineY >= bounds.y + bounds.height) continue - out += RenderCommand.DrawRect(bounds.x, lineY, bounds.width, 1, gridColor) - } - } -} - -private class ColorFieldSurfaceNode( - key: Any? -) : DOMNode(key) { - override val styleType: String = "dsgl-system-color-picker-color-field" - - private var style: ColorPickerStyle = ColorPickerStyle() - private var hueDeg: Float = 0f - private var saturation: Float = 0f - private var brightness: Float = 1f - - fun bind(style: ColorPickerStyle, color: RgbaColor, hueDeg: Float) { - val hsv = ColorConversions.rgbToHsv(color, hueDeg) - val nextSaturation = hsv.saturation - val nextBrightness = hsv.brightness - if (this.style != style || - this.hueDeg != hueDeg || - saturation != nextSaturation || - brightness != nextBrightness - ) { - markRenderCommandsDirty() - } - this.style = style - this.hueDeg = hueDeg - saturation = nextSaturation - brightness = nextBrightness - } - - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } - - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { - bounds = Rect(x, y, width, height) - } - - override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) { - if (bounds.width <= 0 || bounds.height <= 0) return - out += RenderCommand.DrawColorField( - x = bounds.x, - y = bounds.y, - width = bounds.width, - height = bounds.height, - hueDeg = hueDeg - ) - drawBorder(out, bounds, style.inputBorderColor) - val thumbX = bounds.x + (saturation * bounds.width.toFloat()).roundToInt().coerceIn(0, bounds.width - 1) - val thumbY = - bounds.y + ((1f - brightness) * bounds.height.toFloat()).roundToInt().coerceIn(0, bounds.height - 1) - out += RenderCommand.DrawRect(thumbX - 3, thumbY - 3, 7, 7, style.thumbShadowColor) - drawBorder(out, Rect(thumbX - 2, thumbY - 2, 5, 5), style.thumbOutlineColor) - } -} - -private class HueSurfaceNode( - key: Any? -) : DOMNode(key) { - override val styleType: String = "dsgl-system-color-picker-hue-slider" - - private var style: ColorPickerStyle = ColorPickerStyle() - private var hueDeg: Float = 0f - - fun bind(style: ColorPickerStyle, hueDeg: Float) { - if (this.style != style || this.hueDeg != hueDeg) { - markRenderCommandsDirty() - } - this.style = style - this.hueDeg = hueDeg - } - - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } - - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { - bounds = Rect(x, y, width, height) - } - - override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) { - if (bounds.width <= 0 || bounds.height <= 0) return - out += RenderCommand.DrawHueBar( - x = bounds.x, - y = bounds.y, - width = bounds.width, - height = bounds.height - ) - drawBorder(out, bounds, style.inputBorderColor) - val thumbX = bounds.x + ((hueDeg / 360f) * bounds.width.toFloat()).roundToInt().coerceIn(0, bounds.width - 1) - out += RenderCommand.DrawRect(thumbX - 1, bounds.y - 1, 3, bounds.height + 2, style.thumbOutlineColor) - } -} - -private class AlphaSurfaceNode( - key: Any? -) : DOMNode(key) { - override val styleType: String = "dsgl-system-color-picker-alpha-slider" - - private var style: ColorPickerStyle = ColorPickerStyle() - private var color: RgbaColor = RgbaColor.WHITE - - fun bind(style: ColorPickerStyle, color: RgbaColor) { - if (this.style != style || this.color != color) { - markRenderCommandsDirty() - } - this.style = style - this.color = color - } - - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } - - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { - bounds = Rect(x, y, width, height) - } - - override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) { - if (bounds.width <= 0 || bounds.height <= 0) return - drawChecker(out, bounds, style) - out += RenderCommand.DrawAlphaBar( - x = bounds.x, - y = bounds.y, - width = bounds.width, - height = bounds.height, - rgbColor = color.copy(a = 1f).toArgbInt() - ) - drawBorder(out, bounds, style.inputBorderColor) - val thumbX = bounds.x + (color.a * bounds.width.toFloat()).roundToInt().coerceIn(0, bounds.width - 1) - out += RenderCommand.DrawRect(thumbX - 1, bounds.y - 1, 3, bounds.height + 2, style.thumbOutlineColor) - } -} - -private class ColorSwatchSurfaceNode( - private val allowEmpty: Boolean = false, - key: Any? -) : DOMNode(key) { - override val styleType: String = "dsgl-system-color-picker-swatch" - - private var style: ColorPickerStyle = ColorPickerStyle() - private var color: RgbaColor? = RgbaColor.WHITE - private var highlighted: Boolean = false - - fun bind(style: ColorPickerStyle, color: RgbaColor?, highlighted: Boolean) { - if (this.style != style || this.color != color || this.highlighted != highlighted) { - markRenderCommandsDirty() - } - this.style = style - this.color = color - this.highlighted = highlighted - } - - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } - - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { - bounds = Rect(x, y, width, height) - } - - override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) { - if (bounds.width <= 0 || bounds.height <= 0) return - val localColor = color - if (localColor == null && allowEmpty) { - out += RenderCommand.DrawRect(bounds.x, bounds.y, bounds.width, bounds.height, 0x33222A34) - drawBorder(out, bounds, if (highlighted) style.inputActiveBorderColor else style.recentGridBorderColor) - return - } - drawChecker(out, bounds, style) - out += RenderCommand.DrawRect( - bounds.x, - bounds.y, - bounds.width, - bounds.height, - (localColor ?: RgbaColor.WHITE).toArgbInt() - ) - drawBorder(out, bounds, if (highlighted) style.inputActiveBorderColor else style.inputBorderColor) - } -} - -private fun drawChecker(out: MutableList, rect: Rect, style: ColorPickerStyle) { - if (rect.width <= 0 || rect.height <= 0) return - out += RenderCommand.DrawCheckerboard( - x = rect.x, - y = rect.y, - width = rect.width, - height = rect.height, - cellSize = 4, - lightColor = style.checkerLightColor, - darkColor = style.checkerDarkColor - ) -} - -private fun drawBorder(out: MutableList, rect: Rect, color: Int) { - if (rect.width <= 0 || rect.height <= 0) return - out += RenderCommand.DrawRect(rect.x, rect.y, rect.width, 1, color) - out += RenderCommand.DrawRect(rect.x, rect.y + rect.height - 1, rect.width, 1, color) - out += RenderCommand.DrawRect(rect.x, rect.y, 1, rect.height, color) - out += RenderCommand.DrawRect(rect.x + rect.width - 1, rect.y, 1, rect.height, color) -} - From 58c61a375580b4faca24472732f3edb836029a18 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Tue, 21 Apr 2026 21:57:55 +0300 Subject: [PATCH 21/78] adding color surface dsl; --- .../SystemColorPickerPopupBodyNode.kt | 29 ++++++++--------- .../dsgl/core/dsl/ColorSurfaceDsl.kt | 31 +++++++++++++++++++ 2 files changed, 46 insertions(+), 14 deletions(-) create mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt index 608fa18..48e898d 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt @@ -10,6 +10,7 @@ import org.dreamfinity.dsgl.core.dom.layout.Size import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.dsl.UiScope import org.dreamfinity.dsgl.core.dsl.button +import org.dreamfinity.dsgl.core.dsl.colorSwatch import org.dreamfinity.dsgl.core.dsl.div import org.dreamfinity.dsgl.core.dsl.text import org.dreamfinity.dsgl.core.style.Display @@ -43,12 +44,12 @@ internal class SystemColorPickerPopupBodyNode( key = "dsgl-system-color-picker-surface-alpha" ).applyParent(this) - private val previousSwatchNode: ColorSwatchSurfaceNode = ColorSwatchSurfaceNode( - key = "dsgl-system-color-picker-swatch-previous" - ).applyParent(this) - private val currentSwatchNode: ColorSwatchSurfaceNode = ColorSwatchSurfaceNode( - key = "dsgl-system-color-picker-swatch-current" - ).applyParent(this) + private val previousSwatchNode: ColorSwatchSurfaceNode = scope.colorSwatch({ + this.key = "dsgl-system-color-picker-swatch-previous" + }) + private val currentSwatchNode: ColorSwatchSurfaceNode = scope.colorSwatch({ + this.key = "dsgl-system-color-picker-swatch-current" + }) private val copyButton: ButtonNode = scope.button("Copy", { this.key = "dsgl-system-color-picker-button-copy" @@ -74,10 +75,10 @@ internal class SystemColorPickerPopupBodyNode( } private val recentSwatchNodes: List = (0 until RECENT_SWATCH_COUNT).map { index -> - ColorSwatchSurfaceNode( - allowEmpty = true, - key = "dsgl-system-color-picker-recent-$index" - ).applyParent(this) + scope.colorSwatch({ + allowEmpty = true + this.key = "dsgl-system-color-picker-recent-$index" + }) } private var appliedStyle: ColorPickerStyle? = null @@ -707,10 +708,10 @@ internal class SystemColorPickerEyedropperOverlayNode( private val centerNode: ContainerNode = scope.div({ this.key = "dsgl-system-color-picker-eyedropper-center" }) - private val swatchNode: ColorSwatchSurfaceNode = ColorSwatchSurfaceNode( - allowEmpty = false, - key = "dsgl-system-color-picker-eyedropper-swatch" - ).applyParent(this) + private val swatchNode: ColorSwatchSurfaceNode = scope.colorSwatch({ + allowEmpty = false + this.key = "dsgl-system-color-picker-eyedropper-swatch" + }) private val modeTextNode: TextNode = createOverlayTextNode( key = "dsgl-system-color-picker-eyedropper-mode", text = "" diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt new file mode 100644 index 0000000..245317c --- /dev/null +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt @@ -0,0 +1,31 @@ +package org.dreamfinity.dsgl.core.dsl + +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerStyle +import org.dreamfinity.dsgl.core.colorpicker.RgbaColor +import org.dreamfinity.dsgl.core.colorpicker.internal.ColorSwatchSurfaceNode +import org.dreamfinity.dsgl.core.hooks.ref.ElementHandle +import org.dreamfinity.dsgl.core.hooks.ref.RefTarget + +internal open class ColorSwatchProps : ComponentProps() { + var allowEmpty: Boolean = false + var color: RgbaColor? = RgbaColor.WHITE + var highlighted: Boolean = false + var palette: ColorPickerStyle = ColorPickerStyle() +} + +@DsglDsl +internal fun UiScope.colorSwatch( + props: ColorSwatchProps.() -> Unit = {}, + ref: RefTarget? = null +) = withProps(ColorSwatchProps().apply(props)) { props -> + ColorSwatchSurfaceNode( + allowEmpty = props.allowEmpty, + key = props.key + ).apply { + bind(style = props.palette, color = props.color, highlighted = props.highlighted) + applyStyle(this, props.style) + applyHandlers(this, props) + applyRef(this, ref) + add(this) + } +} From bd8e606d52212680f49dabcc06e6a99b91ecbc9b Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Tue, 21 Apr 2026 22:19:09 +0300 Subject: [PATCH 22/78] adding due slider dsl; --- .../SystemColorPickerPopupBodyNode.kt | 7 +++--- .../dsgl/core/dsl/ColorSurfaceDsl.kt | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt index 48e898d..14fce37 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt @@ -12,6 +12,7 @@ import org.dreamfinity.dsgl.core.dsl.UiScope import org.dreamfinity.dsgl.core.dsl.button import org.dreamfinity.dsgl.core.dsl.colorSwatch import org.dreamfinity.dsgl.core.dsl.div +import org.dreamfinity.dsgl.core.dsl.hueSlider import org.dreamfinity.dsgl.core.dsl.text import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.TextWrap @@ -37,9 +38,9 @@ internal class SystemColorPickerPopupBodyNode( private val colorFieldNode: ColorFieldSurfaceNode = ColorFieldSurfaceNode( key = "dsgl-system-color-picker-surface-field" ).applyParent(this) - private val hueSliderNode: HueSurfaceNode = HueSurfaceNode( - key = "dsgl-system-color-picker-surface-hue" - ).applyParent(this) + private val hueSliderNode: HueSurfaceNode = scope.hueSlider({ + this.key = "dsgl-system-color-picker-surface-hue" + }) private val alphaSliderNode: AlphaSurfaceNode = AlphaSurfaceNode( key = "dsgl-system-color-picker-surface-alpha" ).applyParent(this) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt index 245317c..472efa5 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt @@ -3,6 +3,7 @@ package org.dreamfinity.dsgl.core.dsl import org.dreamfinity.dsgl.core.colorpicker.ColorPickerStyle import org.dreamfinity.dsgl.core.colorpicker.RgbaColor import org.dreamfinity.dsgl.core.colorpicker.internal.ColorSwatchSurfaceNode +import org.dreamfinity.dsgl.core.colorpicker.internal.HueSurfaceNode import org.dreamfinity.dsgl.core.hooks.ref.ElementHandle import org.dreamfinity.dsgl.core.hooks.ref.RefTarget @@ -13,6 +14,11 @@ internal open class ColorSwatchProps : ComponentProps() { var palette: ColorPickerStyle = ColorPickerStyle() } +internal open class HueSliderProps : ComponentProps() { + var hueDeg: Float = 0f + var palette: ColorPickerStyle = ColorPickerStyle() +} + @DsglDsl internal fun UiScope.colorSwatch( props: ColorSwatchProps.() -> Unit = {}, @@ -29,3 +35,19 @@ internal fun UiScope.colorSwatch( add(this) } } + +@DsglDsl +internal fun UiScope.hueSlider( + props: HueSliderProps.() -> Unit = {}, + ref: RefTarget? = null +) = withProps(HueSliderProps().apply(props)) { props -> + HueSurfaceNode( + key = props.key + ).apply { + bind(style = props.palette, hueDeg = props.hueDeg) + applyStyle(this, props.style) + applyHandlers(this, props) + applyRef(this, ref) + add(this) + } +} From bec7f3176f535ad1720c679da514272dbad332c9 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Tue, 21 Apr 2026 22:39:45 +0300 Subject: [PATCH 23/78] adding alpha slider dsl; --- .../SystemColorPickerPopupBodyNode.kt | 7 +++--- .../dsgl/core/dsl/ColorSurfaceDsl.kt | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt index 14fce37..68ede1b 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt @@ -9,6 +9,7 @@ import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.Size import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.dsl.UiScope +import org.dreamfinity.dsgl.core.dsl.alphaSlider import org.dreamfinity.dsgl.core.dsl.button import org.dreamfinity.dsgl.core.dsl.colorSwatch import org.dreamfinity.dsgl.core.dsl.div @@ -41,9 +42,9 @@ internal class SystemColorPickerPopupBodyNode( private val hueSliderNode: HueSurfaceNode = scope.hueSlider({ this.key = "dsgl-system-color-picker-surface-hue" }) - private val alphaSliderNode: AlphaSurfaceNode = AlphaSurfaceNode( - key = "dsgl-system-color-picker-surface-alpha" - ).applyParent(this) + private val alphaSliderNode: AlphaSurfaceNode = scope.alphaSlider({ + this.key = "dsgl-system-color-picker-surface-alpha" + }) private val previousSwatchNode: ColorSwatchSurfaceNode = scope.colorSwatch({ this.key = "dsgl-system-color-picker-swatch-previous" diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt index 472efa5..0a64a37 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt @@ -2,6 +2,7 @@ package org.dreamfinity.dsgl.core.dsl import org.dreamfinity.dsgl.core.colorpicker.ColorPickerStyle import org.dreamfinity.dsgl.core.colorpicker.RgbaColor +import org.dreamfinity.dsgl.core.colorpicker.internal.AlphaSurfaceNode import org.dreamfinity.dsgl.core.colorpicker.internal.ColorSwatchSurfaceNode import org.dreamfinity.dsgl.core.colorpicker.internal.HueSurfaceNode import org.dreamfinity.dsgl.core.hooks.ref.ElementHandle @@ -19,6 +20,11 @@ internal open class HueSliderProps : ComponentProps() { var palette: ColorPickerStyle = ColorPickerStyle() } +internal open class AlphaSliderProps : ComponentProps() { + var color: RgbaColor = RgbaColor.WHITE + var palette: ColorPickerStyle = ColorPickerStyle() +} + @DsglDsl internal fun UiScope.colorSwatch( props: ColorSwatchProps.() -> Unit = {}, @@ -51,3 +57,19 @@ internal fun UiScope.hueSlider( add(this) } } + +@DsglDsl +internal fun UiScope.alphaSlider( + props: AlphaSliderProps.() -> Unit = {}, + ref: RefTarget? = null +) = withProps(AlphaSliderProps().apply(props)) { props -> + AlphaSurfaceNode( + key = props.key + ).apply { + bind(style = props.palette, color = props.color) + applyStyle(this, props.style) + applyHandlers(this, props) + applyRef(this, ref) + add(this) + } +} From 5a4d21d9f3a66f5800b0a2e878bb0e165364d04e Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Tue, 21 Apr 2026 23:03:42 +0300 Subject: [PATCH 24/78] adding color field (2D) dsl; --- .../SystemColorPickerPopupBodyNode.kt | 7 +++--- .../dsgl/core/dsl/ColorSurfaceDsl.kt | 23 +++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt index 68ede1b..642b891 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt @@ -11,6 +11,7 @@ import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.dsl.UiScope import org.dreamfinity.dsgl.core.dsl.alphaSlider import org.dreamfinity.dsgl.core.dsl.button +import org.dreamfinity.dsgl.core.dsl.colorField import org.dreamfinity.dsgl.core.dsl.colorSwatch import org.dreamfinity.dsgl.core.dsl.div import org.dreamfinity.dsgl.core.dsl.hueSlider @@ -36,9 +37,9 @@ internal class SystemColorPickerPopupBodyNode( private val argbOrderButton: ButtonNode = scope.button("ARGB", { this.key = "dsgl-system-color-picker-order-argb" }) - private val colorFieldNode: ColorFieldSurfaceNode = ColorFieldSurfaceNode( - key = "dsgl-system-color-picker-surface-field" - ).applyParent(this) + private val colorFieldNode: ColorFieldSurfaceNode = scope.colorField({ + this.key = "dsgl-system-color-picker-surface-field" + }) private val hueSliderNode: HueSurfaceNode = scope.hueSlider({ this.key = "dsgl-system-color-picker-surface-hue" }) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt index 0a64a37..88ffbea 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt @@ -3,6 +3,7 @@ package org.dreamfinity.dsgl.core.dsl import org.dreamfinity.dsgl.core.colorpicker.ColorPickerStyle import org.dreamfinity.dsgl.core.colorpicker.RgbaColor import org.dreamfinity.dsgl.core.colorpicker.internal.AlphaSurfaceNode +import org.dreamfinity.dsgl.core.colorpicker.internal.ColorFieldSurfaceNode import org.dreamfinity.dsgl.core.colorpicker.internal.ColorSwatchSurfaceNode import org.dreamfinity.dsgl.core.colorpicker.internal.HueSurfaceNode import org.dreamfinity.dsgl.core.hooks.ref.ElementHandle @@ -25,6 +26,12 @@ internal open class AlphaSliderProps : ComponentProps() { var palette: ColorPickerStyle = ColorPickerStyle() } +internal open class ColorFieldProps : ComponentProps() { + var color: RgbaColor = RgbaColor.WHITE + var hueDeg: Float = 0f + var palette: ColorPickerStyle = ColorPickerStyle() +} + @DsglDsl internal fun UiScope.colorSwatch( props: ColorSwatchProps.() -> Unit = {}, @@ -73,3 +80,19 @@ internal fun UiScope.alphaSlider( add(this) } } + +@DsglDsl +internal fun UiScope.colorField( + props: ColorFieldProps.() -> Unit = {}, + ref: RefTarget? = null +) = withProps(ColorFieldProps().apply(props)) { props -> + ColorFieldSurfaceNode( + key = props.key + ).apply { + bind(style = props.palette, color = props.color, hueDeg = props.hueDeg) + applyStyle(this, props.style) + applyHandlers(this, props) + applyRef(this, ref) + add(this) + } +} From 5a576b893b7bb8dab601c2946e52cd30f8f5e6d2 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Tue, 21 Apr 2026 23:34:11 +0300 Subject: [PATCH 25/78] adding eyedropper dsl; reworking how we draw the grid for eyedropper; --- .../dsgl/mcForge1710/Mc1710UiAdapter.kt | 31 +++++++++++++++++ .../SystemColorPickerCustomSurfaceNodes.kt | 33 +++++++++---------- .../SystemColorPickerPopupBodyNode.kt | 9 ++--- .../dsgl/core/dsl/ColorSurfaceDsl.kt | 31 +++++++++++++++++ .../dsgl/core/render/RenderCommand.kt | 11 ++++++- .../SystemOverlayColorPickerEntryTests.kt | 13 +++++--- 6 files changed, 102 insertions(+), 26 deletions(-) diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/Mc1710UiAdapter.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/Mc1710UiAdapter.kt index e5401e9..7503233 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/Mc1710UiAdapter.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/Mc1710UiAdapter.kt @@ -430,6 +430,37 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U GL11.glTexCoord2f(0f, 0f) GL11.glVertex2f(command.x.toFloat(), (command.y + command.height).toFloat()) GL11.glEnd() + drawCapturedRegionGridOverlay(command) + } + + private fun drawCapturedRegionGridOverlay(command: RenderCommand.DrawCapturedScreenRegion) { + val grid = command.gridOverlay ?: return + val columns = grid.columns.coerceAtLeast(1) + val rows = grid.rows.coerceAtLeast(1) + val magnification = grid.magnification.coerceAtLeast(1) + + GL11.glDisable(GL11.GL_TEXTURE_2D) + GL11.glEnable(GL11.GL_BLEND) + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA) + glColor(applyOpacity(grid.color)) + GL11.glBegin(GL11.GL_LINES) + for (column in 1 until columns) { + val lineX = command.x + column * magnification + if (lineX <= command.x || lineX >= command.x + command.width) continue + val x = lineX + 0.5f + GL11.glVertex2f(x, command.y.toFloat()) + GL11.glVertex2f(x, (command.y + command.height).toFloat()) + } + for (row in 1 until rows) { + val lineY = command.y + row * magnification + if (lineY <= command.y || lineY >= command.y + command.height) continue + val y = lineY + 0.5f + GL11.glVertex2f(command.x.toFloat(), y) + GL11.glVertex2f((command.x + command.width).toFloat(), y) + } + GL11.glEnd() + GL11.glEnable(GL11.GL_TEXTURE_2D) + GL11.glColor4f(1f, 1f, 1f, 1f) } private fun drawCheckerboard(command: RenderCommand.DrawCheckerboard) { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerCustomSurfaceNodes.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerCustomSurfaceNodes.kt index 3931d9a..896128a 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerCustomSurfaceNodes.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerCustomSurfaceNodes.kt @@ -54,17 +54,17 @@ internal class EyedropperMagnifierDrawNode( private var columns: Int = 1 private var rows: Int = 1 - private var cellSize: Int = 1 + private var magnification: Int = 1 private var gridEnabled: Boolean = true private var gridColor: Int = 0x66FFFFFF - fun bind(columns: Int, rows: Int, cellSize: Int, gridEnabled: Boolean, gridColor: Int) { + fun bind(columns: Int, rows: Int, magnification: Int, gridEnabled: Boolean, gridColor: Int) { val nextColumns = columns.coerceAtLeast(1) val nextRows = rows.coerceAtLeast(1) - val nextCellSize = cellSize.coerceAtLeast(1) + val nextMagnification = magnification.coerceAtLeast(1) if (this.columns != nextColumns || this.rows != nextRows || - this.cellSize != nextCellSize || + this.magnification != nextMagnification || this.gridEnabled != gridEnabled || this.gridColor != gridColor ) { @@ -72,7 +72,7 @@ internal class EyedropperMagnifierDrawNode( } this.columns = nextColumns this.rows = nextRows - this.cellSize = nextCellSize + this.magnification = nextMagnification this.gridEnabled = gridEnabled this.gridColor = gridColor } @@ -92,19 +92,18 @@ internal class EyedropperMagnifierDrawNode( x = bounds.x, y = bounds.y, width = bounds.width, - height = bounds.height + height = bounds.height, + gridOverlay = if (gridEnabled) { + RenderCommand.CapturedGridOverlay( + columns = columns, + rows = rows, + magnification = magnification, + color = gridColor + ) + } else { + null + } ) - if (!gridEnabled) return - for (column in 1 until columns) { - val lineX = bounds.x + column * cellSize - if (lineX <= bounds.x || lineX >= bounds.x + bounds.width) continue - out += RenderCommand.DrawRect(lineX, bounds.y, 1, bounds.height, gridColor) - } - for (row in 1 until rows) { - val lineY = bounds.y + row * cellSize - if (lineY <= bounds.y || lineY >= bounds.y + bounds.height) continue - out += RenderCommand.DrawRect(bounds.x, lineY, bounds.width, 1, gridColor) - } } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt index 642b891..bfa304b 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt @@ -14,6 +14,7 @@ import org.dreamfinity.dsgl.core.dsl.button import org.dreamfinity.dsgl.core.dsl.colorField import org.dreamfinity.dsgl.core.dsl.colorSwatch import org.dreamfinity.dsgl.core.dsl.div +import org.dreamfinity.dsgl.core.dsl.eyedropperMagnifier import org.dreamfinity.dsgl.core.dsl.hueSlider import org.dreamfinity.dsgl.core.dsl.text import org.dreamfinity.dsgl.core.style.Display @@ -705,9 +706,9 @@ internal class SystemColorPickerEyedropperOverlayNode( private val panelNode: ContainerNode = scope.div({ this.key = "dsgl-system-color-picker-eyedropper-panel" }) - private val magnifierDrawNode: EyedropperMagnifierDrawNode = EyedropperMagnifierDrawNode( - key = "dsgl-system-color-picker-eyedropper-magnifier" - ).applyParent(this) + private val magnifierDrawNode: EyedropperMagnifierDrawNode = scope.eyedropperMagnifier({ + this.key = "dsgl-system-color-picker-eyedropper-magnifier" + }) private val centerNode: ContainerNode = scope.div({ this.key = "dsgl-system-color-picker-eyedropper-center" }) @@ -795,7 +796,7 @@ internal class SystemColorPickerEyedropperOverlayNode( magnifierDrawNode.bind( columns = state.model.captureSourceRect.width, rows = state.model.captureSourceRect.height, - cellSize = ( + magnification = ( state.model.magnifierRect.width / state.model.captureSourceRect.width.coerceAtLeast(1) ).coerceAtLeast(1), diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt index 88ffbea..37f287c 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt @@ -5,6 +5,7 @@ import org.dreamfinity.dsgl.core.colorpicker.RgbaColor import org.dreamfinity.dsgl.core.colorpicker.internal.AlphaSurfaceNode import org.dreamfinity.dsgl.core.colorpicker.internal.ColorFieldSurfaceNode import org.dreamfinity.dsgl.core.colorpicker.internal.ColorSwatchSurfaceNode +import org.dreamfinity.dsgl.core.colorpicker.internal.EyedropperMagnifierDrawNode import org.dreamfinity.dsgl.core.colorpicker.internal.HueSurfaceNode import org.dreamfinity.dsgl.core.hooks.ref.ElementHandle import org.dreamfinity.dsgl.core.hooks.ref.RefTarget @@ -32,6 +33,14 @@ internal open class ColorFieldProps : ComponentProps() { var palette: ColorPickerStyle = ColorPickerStyle() } +internal open class EyedropperMagnifierProps : ComponentProps() { + var sourceColumns: Int = 1 + var sourceRows: Int = 1 + var magnification: Int = 1 + var showGrid: Boolean = true + var gridColor: Int = 0x66FFFFFF +} + @DsglDsl internal fun UiScope.colorSwatch( props: ColorSwatchProps.() -> Unit = {}, @@ -96,3 +105,25 @@ internal fun UiScope.colorField( add(this) } } + +@DsglDsl +internal fun UiScope.eyedropperMagnifier( + props: EyedropperMagnifierProps.() -> Unit = {}, + ref: RefTarget? = null +) = withProps(EyedropperMagnifierProps().apply(props)) { props -> + EyedropperMagnifierDrawNode( + key = props.key + ).apply { + bind( + columns = props.sourceColumns, + rows = props.sourceRows, + magnification = props.magnification, + gridEnabled = props.showGrid, + gridColor = props.gridColor + ) + applyStyle(this, props.style) + applyHandlers(this, props) + applyRef(this, ref) + add(this) + } +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/render/RenderCommand.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/render/RenderCommand.kt index ddcfd29..62347c9 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/render/RenderCommand.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/render/RenderCommand.kt @@ -133,9 +133,18 @@ sealed class RenderCommand { val x: Int, val y: Int, val width: Int, - val height: Int + val height: Int, + val gridOverlay: CapturedGridOverlay? = null ) : RenderCommand() + /** Optional grid overlay rendered by backend as part of captured-region magnifier pass. */ + data class CapturedGridOverlay( + val columns: Int, + val rows: Int, + val magnification: Int, + val color: Int + ) + /** Item stack draw command. */ data class DrawItemStack( val stack: ItemStackRef, diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt index bf840e5..50cf50b 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt @@ -522,10 +522,15 @@ class SystemOverlayColorPickerEntryTests { assertTrue(commands.none { command -> command is RenderCommand.DrawRect && (command.color == checkerLight || command.color == checkerDark) }) - val gridLines = commands.filterIsInstance().filter { it.color == gridColor } - assertEquals(8, gridLines.size) - assertTrue(gridLines.any { it.width == 1 && it.height == 15 }) - assertTrue(gridLines.any { it.width == 15 && it.height == 1 }) + val capturedRegion = commands.filterIsInstance().single() + val gridOverlay = capturedRegion.gridOverlay ?: error("grid overlay missing") + assertEquals(5, gridOverlay.columns) + assertEquals(5, gridOverlay.rows) + assertEquals(3, gridOverlay.magnification) + assertEquals(gridColor, gridOverlay.color) + assertTrue(commands.none { command -> + command is RenderCommand.DrawRect && command.color == gridColor + }) assertTrue(commands.any { command -> command is RenderCommand.DrawText && command.text.startsWith("Mode:") }) From 16de0d8b70d3cdd0c24e60304e4bb1303f43c6d9 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Tue, 21 Apr 2026 23:54:54 +0300 Subject: [PATCH 26/78] refactoring and modularizing color picker rendering logic with dedicated render state classes; --- .../SystemColorPickerPopupBodyNode.kt | 178 ++++++++++++------ 1 file changed, 125 insertions(+), 53 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt index bfa304b..2270cda 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt @@ -153,6 +153,24 @@ internal class SystemColorPickerPopupBodyNode( ) } + private data class TopControlsRenderState( + val controller: ColorPickerController, + val layout: ColorPickerLayout, + val style: ColorPickerStyle, + val state: ColorPickerState, + val hueDeg: Float, + val hoverX: Int, + val hoverY: Int, + val modeDropdownOpen: Boolean + ) + + private data class RecentSwatchRenderState( + val layout: ColorPickerLayout, + val style: ColorPickerStyle, + val recentColors: List, + val hoveredRecent: Int + ) + private fun renderTopControls( ctx: UiMeasureContext, controller: ColorPickerController, @@ -163,33 +181,59 @@ internal class SystemColorPickerPopupBodyNode( hoverX: Int, hoverY: Int, modeDropdownOpen: Boolean + ) { + val renderState = TopControlsRenderState( + controller = controller, + layout = layout, + style = style, + state = state, + hueDeg = hueDeg, + hoverX = hoverX, + hoverY = hoverY, + modeDropdownOpen = modeDropdownOpen + ) + renderModeSelectControl(ctx, renderState) + renderOrderControls(ctx, renderState) + renderColorSurfaceControls(ctx, renderState) + renderPrimarySwatches(ctx, renderState) + renderActionControls(ctx, renderState) + } + + private fun renderModeSelectControl( + ctx: UiMeasureContext, + state: TopControlsRenderState ) { syncPickerButtonVisual( button = modeSelectButton, - text = if (modeDropdownOpen) "${state.mode.name} ^" else "${state.mode.name} v", - style = style, - hovered = layout.modeSelectRect.contains(hoverX, hoverY), - selected = modeDropdownOpen + text = if (state.modeDropdownOpen) "${state.state.mode.name} ^" else "${state.state.mode.name} v", + style = state.style, + hovered = state.layout.modeSelectRect.contains(state.hoverX, state.hoverY), + selected = state.modeDropdownOpen ) - renderNode(ctx, modeSelectButton, layout.modeSelectRect) + renderNode(ctx, modeSelectButton, state.layout.modeSelectRect) + } - val showOrder = layout.rgbaOrderRect != null && layout.argbOrderRect != null + private fun renderOrderControls( + ctx: UiMeasureContext, + state: TopControlsRenderState + ) { + val showOrder = state.layout.rgbaOrderRect != null && state.layout.argbOrderRect != null if (showOrder) { - val rgbaRect = layout.rgbaOrderRect - val argbRect = layout.argbOrderRect + val rgbaRect = state.layout.rgbaOrderRect + val argbRect = state.layout.argbOrderRect syncPickerButtonVisual( button = rgbaOrderButton, text = null, - style = style, - hovered = rgbaRect.contains(hoverX, hoverY), - selected = state.rgbOrder == RgbChannelOrder.RGBA + style = state.style, + hovered = rgbaRect.contains(state.hoverX, state.hoverY), + selected = state.state.rgbOrder == RgbChannelOrder.RGBA ) syncPickerButtonVisual( button = argbOrderButton, text = null, - style = style, - hovered = argbRect.contains(hoverX, hoverY), - selected = state.rgbOrder == RgbChannelOrder.ARGB + style = state.style, + hovered = argbRect.contains(state.hoverX, state.hoverY), + selected = state.state.rgbOrder == RgbChannelOrder.ARGB ) renderNode(ctx, rgbaOrderButton, rgbaRect) renderNode(ctx, argbOrderButton, argbRect) @@ -197,57 +241,72 @@ internal class SystemColorPickerPopupBodyNode( renderNode(ctx, rgbaOrderButton, null) renderNode(ctx, argbOrderButton, null) } + } - colorFieldNode.bind(style = style, color = state.color, hueDeg = hueDeg) - renderNode(ctx, colorFieldNode, layout.colorFieldRect) + private fun renderColorSurfaceControls( + ctx: UiMeasureContext, + state: TopControlsRenderState + ) { + colorFieldNode.bind(style = state.style, color = state.state.color, hueDeg = state.hueDeg) + renderNode(ctx, colorFieldNode, state.layout.colorFieldRect) - hueSliderNode.bind(style = style, hueDeg = hueDeg) - renderNode(ctx, hueSliderNode, layout.hueRect) + hueSliderNode.bind(style = state.style, hueDeg = state.hueDeg) + renderNode(ctx, hueSliderNode, state.layout.hueRect) - if (state.alphaEnabled && layout.alphaRect != null) { - alphaSliderNode.bind(style = style, color = state.color) - renderNode(ctx, alphaSliderNode, layout.alphaRect) + if (state.state.alphaEnabled && state.layout.alphaRect != null) { + alphaSliderNode.bind(style = state.style, color = state.state.color) + renderNode(ctx, alphaSliderNode, state.layout.alphaRect) } else { renderNode(ctx, alphaSliderNode, null) } + } + private fun renderPrimarySwatches( + ctx: UiMeasureContext, + state: TopControlsRenderState + ) { previousSwatchNode.bind( - style = style, - color = state.previous, - highlighted = layout.previousSwatchRect.contains(hoverX, hoverY) + style = state.style, + color = state.state.previous, + highlighted = state.layout.previousSwatchRect.contains(state.hoverX, state.hoverY) ) currentSwatchNode.bind( - style = style, - color = state.color, - highlighted = layout.currentSwatchRect.contains(hoverX, hoverY) + style = state.style, + color = state.state.color, + highlighted = state.layout.currentSwatchRect.contains(state.hoverX, state.hoverY) ) - renderNode(ctx, previousSwatchNode, layout.previousSwatchRect) - renderNode(ctx, currentSwatchNode, layout.currentSwatchRect) + renderNode(ctx, previousSwatchNode, state.layout.previousSwatchRect) + renderNode(ctx, currentSwatchNode, state.layout.currentSwatchRect) + } + private fun renderActionControls( + ctx: UiMeasureContext, + state: TopControlsRenderState + ) { syncPickerButtonVisual( button = copyButton, text = null, - style = style, - hovered = layout.copyRect.contains(hoverX, hoverY), + style = state.style, + hovered = state.layout.copyRect.contains(state.hoverX, state.hoverY), selected = false ) syncPickerButtonVisual( button = pasteButton, text = null, - style = style, - hovered = layout.pasteRect.contains(hoverX, hoverY), + style = state.style, + hovered = state.layout.pasteRect.contains(state.hoverX, state.hoverY), selected = false ) syncPickerButtonVisual( button = pipetteButton, - text = if (controller.isEyedropperActive()) "Pick..." else "Pipette", - style = style, - hovered = layout.pipetteRect.contains(hoverX, hoverY), - selected = controller.isEyedropperActive() + text = if (state.controller.isEyedropperActive()) "Pick..." else "Pipette", + style = state.style, + hovered = state.layout.pipetteRect.contains(state.hoverX, state.hoverY), + selected = state.controller.isEyedropperActive() ) - renderNode(ctx, copyButton, layout.copyRect) - renderNode(ctx, pasteButton, layout.pasteRect) - renderNode(ctx, pipetteButton, layout.pipetteRect) + renderNode(ctx, copyButton, state.layout.copyRect) + renderNode(ctx, pasteButton, state.layout.pasteRect) + renderNode(ctx, pipetteButton, state.layout.pipetteRect) } private fun renderInputRows( @@ -322,23 +381,36 @@ internal class SystemColorPickerPopupBodyNode( hoverY: Int, recentColors: List ) { - val hoveredRecent = layout.recentRects.indexOfFirst { it.contains(hoverX, hoverY) } + val renderState = RecentSwatchRenderState( + layout = layout, + style = style, + recentColors = recentColors, + hoveredRecent = layout.recentRects.indexOfFirst { it.contains(hoverX, hoverY) } + ) for (index in 0 until RECENT_SWATCH_COUNT) { - val swatchNode = recentSwatchNodes[index] - val swatchRect = layout.recentRects.getOrNull(index) - if (swatchRect == null) { - renderNode(ctx, swatchNode, null) - continue - } - swatchNode.bind( - style = style, - color = recentColors.getOrNull(index), - highlighted = index == hoveredRecent - ) - renderNode(ctx, swatchNode, swatchRect) + renderRecentSwatch(ctx, renderState, index) } } + private fun renderRecentSwatch( + ctx: UiMeasureContext, + state: RecentSwatchRenderState, + index: Int + ) { + val swatchNode = recentSwatchNodes[index] + val swatchRect = state.layout.recentRects.getOrNull(index) + if (swatchRect == null) { + renderNode(ctx, swatchNode, null) + return + } + swatchNode.bind( + style = state.style, + color = state.recentColors.getOrNull(index), + highlighted = index == state.hoveredRecent + ) + renderNode(ctx, swatchNode, swatchRect) + } + private fun applyStaticStyle(style: ColorPickerStyle) { val buttons = buildList { add(modeSelectButton) From cd4031b014350db27a57e0eed126973398df9896 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Wed, 22 Apr 2026 21:20:42 +0300 Subject: [PATCH 27/78] extracting leftover rendering primitives / leafs before migrating to a declarative style; --- .../core/colorpicker/ColorPickerController.kt | 69 ++++- .../colorpicker/ColorPickerPopupRuntime.kt | 23 ++ .../internal/SystemColorPickerOverlayNode.kt | 8 + .../SystemColorPickerPopupBodyNode.kt | 252 ++++++++++++++---- .../overlay/system/SystemOverlayEntries.kt | 2 + .../core/overlay/system/SystemOverlayHost.kt | 23 +- .../SystemOverlayColorPickerEntryTests.kt | 47 +++- 7 files changed, 360 insertions(+), 64 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt index 3914dc8..f354b6e 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt @@ -95,6 +95,10 @@ class ColorPickerController( private var eyedropperBaseColor: RgbaColor = state.color private val eyedropperOverlayDrag: FloatingPaneDragModel = FloatingPaneDragModel() private var eyedropperOverlayRect: Rect? = null + private var domFocusedInputKey: String? = null + private var domInputFocusResyncRequested: Boolean = false + private var domLastFocusedInputKey: String? = null + private var domPendingFocusResyncKey: String? = null var onPreview: ((RgbaColor) -> Unit)? = null var onChange: ((RgbaColor) -> Unit)? = null @@ -115,6 +119,10 @@ class ColorPickerController( modeDropdownOpen = false eyedropperOverlayDrag.end() eyedropperOverlayRect = null + domFocusedInputKey = null + domInputFocusResyncRequested = false + domLastFocusedInputKey = null + domPendingFocusResyncKey = null clearInputEdit() } @@ -152,6 +160,52 @@ class ColorPickerController( return ColorTextCodec.format(state.color, state.mode, state.alphaEnabled, state.rgbOrder) } + internal fun handleDomInputFocused(key: String) { + domFocusedInputKey = key + domLastFocusedInputKey = key + if (activeInputKey == key) { + clearInputEdit() + } + } + + internal fun handleDomInputBlurred(key: String) { + if (domFocusedInputKey == key) { + domFocusedInputKey = null + } + } + + internal fun handleDomInputDraft(key: String, value: String): Boolean { + domFocusedInputKey = key + domLastFocusedInputKey = key + return applyInputDraftValue(key, value) + } + + internal fun commitDomInputEdit(key: String, value: String): Boolean { + domFocusedInputKey = key + domLastFocusedInputKey = key + val applied = applyInputDraftValue(key, value) + commitCurrentColor() + clearInputEdit() + return applied + } + + internal fun cancelDomInputEdit(key: String) { + if (domFocusedInputKey == key) { + domFocusedInputKey = null + } + clearInputEdit() + } + + internal fun resolveDomInputValue(key: String): String = inputValues()[key].orEmpty() + + internal fun consumeDomInputFocusResyncKey(): String? { + if (!domInputFocusResyncRequested) return null + domInputFocusResyncRequested = false + val key = domPendingFocusResyncKey + domPendingFocusResyncKey = null + return key + } + fun beginEyedropper() { if (!state.alphaEnabled) { eyedropperBaseColor = state.color.copy(a = 1f) @@ -692,18 +746,21 @@ class ColorPickerController( if (modeOptionHit != null) { state = state.copy(mode = modeOptionHit.mode) modeDropdownOpen = false + requestDomInputFocusResync() clearInputEdit() return true } if (layout.rgbaOrderRect?.contains(globalX, globalY) == true) { state = state.copy(rgbOrder = RgbChannelOrder.RGBA) modeDropdownOpen = false + requestDomInputFocusResync() clearInputEdit() return true } if (layout.argbOrderRect?.contains(globalX, globalY) == true) { state = state.copy(rgbOrder = RgbChannelOrder.ARGB) modeDropdownOpen = false + requestDomInputFocusResync() clearInputEdit() return true } @@ -876,7 +933,11 @@ class ColorPickerController( } private fun applyInputDraft(key: String): Boolean { - val value = activeInputBuffer.trim() + return applyInputDraftValue(key, activeInputBuffer) + } + + private fun applyInputDraftValue(key: String, rawValue: String): Boolean { + val value = rawValue.trim() if (value.isEmpty()) return false val current = state.color when (key) { @@ -1334,6 +1395,12 @@ class ColorPickerController( } } + private fun requestDomInputFocusResync() { + val key = domFocusedInputKey ?: domLastFocusedInputKey ?: return + domPendingFocusResyncKey = key + domInputFocusResyncRequested = true + } + private data class InputDefinition( val key: String, val label: String diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt index 8b711f2..1462f7b 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt @@ -371,6 +371,29 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { return handled } + fun shouldRouteSystemInputSlotMouseDownToDom(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { + val current = popup ?: return false + if (current.request.ownerScope != OverlayOwnerScope.System) return false + if (button != MouseButton.LEFT) return false + if (current.controller.isEyedropperActive()) return false + return current.layout.inputSlots.any { slot -> slot.inputRect.contains(mouseX, mouseY) } + } + + fun focusSystemInputSlotForDomEditing( + mouseX: Int, + mouseY: Int, + focusInputByIndex: (Int) -> Boolean + ): Boolean { + val current = popup ?: return false + if (current.request.ownerScope != OverlayOwnerScope.System) return false + val slotIndex = current.layout.inputSlots.indexOfFirst { slot -> slot.inputRect.contains(mouseX, mouseY) } + if (slotIndex < 0) return false + val slot = current.layout.inputSlots[slotIndex] + current.controller.handleDomInputFocused(slot.key) + val focused = focusInputByIndex(slotIndex) + return focused + } + fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { val current = popup ?: return false if (current.consumedEyedropperPress && (button == MouseButton.LEFT || button == MouseButton.RIGHT)) { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerOverlayNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerOverlayNode.kt index c210a1c..36ddc27 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerOverlayNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerOverlayNode.kt @@ -28,6 +28,14 @@ internal class SystemColorPickerOverlayNode( cursorY = mouseY } + fun focusInputSlot(index: Int, mouseX: Int, mouseY: Int): Boolean { + return bodyNode.focusInputSlot(index, mouseX, mouseY) + } + + fun syncInputFocusForDomEditing() { + bodyNode.syncFocusedInputForModeOrOrderChange() + } + override fun measure(ctx: UiMeasureContext): Size { return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt index 2270cda..acb54a5 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt @@ -17,6 +17,16 @@ import org.dreamfinity.dsgl.core.dsl.div import org.dreamfinity.dsgl.core.dsl.eyedropperMagnifier import org.dreamfinity.dsgl.core.dsl.hueSlider import org.dreamfinity.dsgl.core.dsl.text +import org.dreamfinity.dsgl.core.event.EventBus +import org.dreamfinity.dsgl.core.event.Events +import org.dreamfinity.dsgl.core.event.FocusManager +import org.dreamfinity.dsgl.core.event.FocusGainEvent +import org.dreamfinity.dsgl.core.event.FocusLoseEvent +import org.dreamfinity.dsgl.core.event.InputEvent +import org.dreamfinity.dsgl.core.event.KeyCodes +import org.dreamfinity.dsgl.core.event.KeyboardKeyDownEvent +import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.event.MouseDownEvent import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.TextWrap @@ -28,6 +38,8 @@ internal class SystemColorPickerPopupBodyNode( private val scope = UiScope(this) private val inputLabelValues: MutableList = MutableList(MAX_INPUT_SLOTS) { "" } + private val inputSemanticKeys: MutableList = MutableList(MAX_INPUT_SLOTS) { null } + private var focusedSemanticInputKey: String? = null private val modeSelectButton: ButtonNode = scope.button("", { this.key = "dsgl-system-color-picker-mode-select" @@ -75,7 +87,9 @@ internal class SystemColorPickerPopupBodyNode( }) } private val inputValueNodes: List = (0 until MAX_INPUT_SLOTS).map { index -> - TextInputNode(key = "dsgl-system-color-picker-input-value-$index").applyParent(this) + TextInputNode(key = "dsgl-system-color-picker-input-value-$index") + .applyParent(this) + .also { node -> configureInputValueNode(index, node) } } private val recentSwatchNodes: List = (0 until RECENT_SWATCH_COUNT).map { index -> @@ -87,6 +101,27 @@ internal class SystemColorPickerPopupBodyNode( private var appliedStyle: ColorPickerStyle? = null + fun focusInputSlot(index: Int, mouseX: Int, mouseY: Int): Boolean { + val inputNode = inputValueNodes.getOrNull(index) ?: return false + if (inputNode.display == Display.None) return false + val key = inputSemanticKeys.getOrNull(index) + focusedSemanticInputKey = key + if (key != null) { + popupEngine.debugActiveController()?.handleDomInputFocused(key) + } + val down = MouseDownEvent(mouseX = mouseX, mouseY = mouseY, mouseButton = MouseButton.LEFT) + down.target = inputNode + EventBus.post(down) + val focused = FocusManager.isFocused(inputNode) + return focused + } + + fun syncFocusedInputForModeOrOrderChange() { + val controller = popupEngine.debugActiveController() ?: return + val layout = popupEngine.debugActiveLayout() ?: return + resyncFocusedInputForModeOrOrderChange(controller, layout) + } + override fun measure(ctx: UiMeasureContext): Size { return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) } @@ -110,11 +145,8 @@ internal class SystemColorPickerPopupBodyNode( val hover = controller.viewHoverPosition() val hoverX = hover.first val hoverY = hover.second - val nowMs = System.currentTimeMillis() val modeDropdownOpen = controller.viewModeDropdownOpen() - val activeInputKey = controller.viewActiveInputKey() - val activeInputBuffer = controller.viewActiveInputBuffer() val inputValues = controller.viewInputValues() val recentColors = controller.viewRecentColors() val definitionsByKey = controller.viewInputDefinitions().associate { it.first to it.second } @@ -137,9 +169,6 @@ internal class SystemColorPickerPopupBodyNode( style = style, hoverX = hoverX, hoverY = hoverY, - nowMs = nowMs, - activeInputKey = activeInputKey, - activeInputBuffer = activeInputBuffer, inputValues = inputValues, definitionsByKey = definitionsByKey ) @@ -171,6 +200,16 @@ internal class SystemColorPickerPopupBodyNode( val hoveredRecent: Int ) + private data class InputRowsRenderState( + val controller: ColorPickerController, + val layout: ColorPickerLayout, + val style: ColorPickerStyle, + val hoverX: Int, + val hoverY: Int, + val inputValues: Map, + val definitionsByKey: Map + ) + private fun renderTopControls( ctx: UiMeasureContext, controller: ColorPickerController, @@ -316,61 +355,99 @@ internal class SystemColorPickerPopupBodyNode( style: ColorPickerStyle, hoverX: Int, hoverY: Int, - nowMs: Long, - activeInputKey: String?, - activeInputBuffer: String, inputValues: Map, definitionsByKey: Map ) { + resyncFocusedInputForModeOrOrderChange(controller, layout) + val renderState = InputRowsRenderState( + controller = controller, + layout = layout, + style = style, + hoverX = hoverX, + hoverY = hoverY, + inputValues = inputValues, + definitionsByKey = definitionsByKey + ) for (index in 0 until MAX_INPUT_SLOTS) { - val inputSlot = layout.inputSlots.getOrNull(index) - val labelNode = inputLabelNodes[index] - val inputNode = inputValueNodes[index] - if (inputSlot == null) { - inputLabelValues[index] = "" - syncTextNodeVisual( - node = labelNode, - text = "", - color = style.mutedTextColor - ) - renderNode(ctx, labelNode, null) - renderNode(ctx, inputNode, null) - continue - } + renderInputRow(ctx, renderState, index) + } + } - val key = inputSlot.key - val label = definitionsByKey[key] ?: inputSlot.label - inputLabelValues[index] = label - syncTextNodeVisual( - node = labelNode, - text = label, - color = style.mutedTextColor - ) + private fun renderInputRow( + ctx: UiMeasureContext, + state: InputRowsRenderState, + index: Int + ) { + val inputSlot = state.layout.inputSlots.getOrNull(index) + val labelNode = inputLabelNodes[index] + val inputNode = inputValueNodes[index] + if (inputSlot == null) { + renderMissingInputRow(ctx, state, labelNode, inputNode, index) + return + } + renderPresentInputRow(ctx, state, labelNode, inputNode, inputSlot, index) + } - val borderColor = when { - activeInputKey == key -> style.inputActiveBorderColor - inputSlot.inputRect.contains(hoverX, hoverY) -> style.buttonHoverColor - else -> style.inputBorderColor - } - val value = if (activeInputKey == key) { - activeInputBuffer + if (controller.viewCaretVisible(nowMs)) "|" else "" - } else { - inputValues[key].orEmpty() - } - syncTextInputVisual( - node = inputNode, - value = value, - border = Border.all(1, borderColor), - background = style.inputBackgroundColor, - focusedBackground = style.inputBackgroundColor, - textColor = style.textColor, - placeholderColor = style.mutedTextColor, - fontSize = style.fontSize - ) + private fun renderMissingInputRow( + ctx: UiMeasureContext, + state: InputRowsRenderState, + labelNode: TextNode, + inputNode: TextInputNode, + index: Int + ) { + inputSemanticKeys[index] = null + if (FocusManager.isFocused(inputNode)) { + FocusManager.clearFocus() + focusedSemanticInputKey = null + } + inputLabelValues[index] = "" + syncTextNodeVisual( + node = labelNode, + text = "", + color = state.style.mutedTextColor + ) + renderNode(ctx, labelNode, null) + renderNode(ctx, inputNode, null) + } - renderNode(ctx, labelNode, inputSlot.labelRect) - renderNode(ctx, inputNode, inputSlot.inputRect) + private fun renderPresentInputRow( + ctx: UiMeasureContext, + state: InputRowsRenderState, + labelNode: TextNode, + inputNode: TextInputNode, + inputSlot: ColorPickerInputSlot, + index: Int + ) { + val key = inputSlot.key + inputSemanticKeys[index] = key + val label = state.definitionsByKey[key] ?: inputSlot.label + inputLabelValues[index] = label + syncTextNodeVisual( + node = labelNode, + text = label, + color = state.style.mutedTextColor + ) + val focused = FocusManager.isFocused(inputNode) + + val borderColor = when { + focused -> state.style.inputActiveBorderColor + inputSlot.inputRect.contains(state.hoverX, state.hoverY) -> state.style.buttonHoverColor + else -> state.style.inputBorderColor } + val value = if (focused) null else state.inputValues[key].orEmpty() + syncTextInputVisual( + node = inputNode, + value = value, + border = Border.all(1, borderColor), + background = state.style.inputBackgroundColor, + focusedBackground = state.style.inputBackgroundColor, + textColor = state.style.textColor, + placeholderColor = state.style.mutedTextColor, + fontSize = state.style.fontSize + ) + + renderNode(ctx, labelNode, inputSlot.labelRect) + renderNode(ctx, inputNode, inputSlot.inputRect) } private fun renderRecentSwatchGrid( @@ -411,6 +488,69 @@ internal class SystemColorPickerPopupBodyNode( renderNode(ctx, swatchNode, swatchRect) } + private fun configureInputValueNode(index: Int, inputNode: TextInputNode) { + EventBus.run { + inputNode.addEventListener(Events.FOCUS) { _: FocusGainEvent -> + val key = inputSemanticKeys[index] ?: return@addEventListener + focusedSemanticInputKey = key + popupEngine.debugActiveController()?.handleDomInputFocused(key) + } + inputNode.addEventListener(Events.BLUR) { _: FocusLoseEvent -> + val key = inputSemanticKeys[index] + if (focusedSemanticInputKey == key) { + focusedSemanticInputKey = null + } + if (key != null) { + popupEngine.debugActiveController()?.handleDomInputBlurred(key) + } + } + inputNode.addEventListener(Events.INPUT) { _: InputEvent -> + val key = inputSemanticKeys[index] ?: return@addEventListener + popupEngine.debugActiveController()?.handleDomInputDraft(key, inputNode.text) + } + inputNode.addEventListener(Events.KEYDOWN) { event: KeyboardKeyDownEvent -> + val key = inputSemanticKeys[index] ?: return@addEventListener + val controller = popupEngine.debugActiveController() ?: return@addEventListener + when (event.keyCode) { + KeyCodes.ENTER -> { + controller.commitDomInputEdit(key, inputNode.text) + event.cancelled = true + } + + KeyCodes.ESCAPE -> { + controller.cancelDomInputEdit(key) + val restoredValue = controller.resolveDomInputValue(key) + if (inputNode.text != restoredValue) { + inputNode.text = restoredValue + inputNode.requestRenderCommandsInvalidation() + } + event.cancelled = true + } + } + } + } + } + + private fun resyncFocusedInputForModeOrOrderChange( + controller: ColorPickerController, + layout: ColorPickerLayout + ) { + val focusedIndex = inputValueNodes.indexOf(FocusManager.focusedNode()) + val focusedSlotKey = if (focusedIndex >= 0) inputSemanticKeys.getOrNull(focusedIndex) else null + val resyncKey = controller.consumeDomInputFocusResyncKey() + ?: focusedSemanticInputKey + ?: focusedSlotKey + ?: return + val targetIndex = layout.inputSlots.indexOfFirst { it.key == resyncKey } + if (targetIndex >= 0) { + FocusManager.requestFocus(inputValueNodes[targetIndex]) + focusedSemanticInputKey = resyncKey + return + } + FocusManager.clearFocus() + focusedSemanticInputKey = null + } + private fun applyStaticStyle(style: ColorPickerStyle) { val buttons = buildList { add(modeSelectButton) @@ -499,7 +639,7 @@ internal class SystemColorPickerPopupBodyNode( private fun syncTextInputVisual( node: TextInputNode, - value: String, + value: String?, border: Border, background: Int, focusedBackground: Int, @@ -508,7 +648,7 @@ internal class SystemColorPickerPopupBodyNode( fontSize: Int ) { var changed = false - if (node.text != value) { + if (value != null && node.text != value) { node.text = value changed = true } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt index 96edee6..f1c7fe7 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt @@ -46,6 +46,8 @@ internal interface SystemOverlayEntry { fun participatesInDomInput(): Boolean = false + fun enablesDomInputFallbackRouting(): Boolean = participatesInDomInput() + fun sync(frame: SystemOverlayFrameContext) fun onInputFrame(viewportWidth: Int, viewportHeight: Int) = Unit diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt index 79d512e..ec6b718 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt @@ -6,8 +6,10 @@ import org.dreamfinity.dsgl.core.colorpicker.internal.InspectorColorPickerHost import org.dreamfinity.dsgl.core.colorpicker.internal.SystemColorPickerOverlayNode import org.dreamfinity.dsgl.core.colorpicker.internal.SystemColorPickerTransientOverlayNode import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.dom.elements.SingleLineInputNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.inspector.InspectorPanelState @@ -51,7 +53,7 @@ class SystemOverlayHost( private var knownViewportHeight: Int = 1 private val domInputRouter: LayerDomInputRouter = LayerDomInputRouter( rootProvider = { - if (activeEntriesTopFirst().any { it.participatesInDomInput() }) rootNode else null + if (activeEntriesTopFirst().any { it.enablesDomInputFallbackRouting() }) rootNode else null } ) @@ -357,6 +359,8 @@ class SystemOverlayHost( private var viewportWidth: Int = 1 private var viewportHeight: Int = 1 + override fun enablesDomInputFallbackRouting(): Boolean = true + override fun sync(frame: SystemOverlayFrameContext) { node.updateCursor(frame.cursorX, frame.cursorY) state.active = popupEngine.isOpenFor(ownerToken) @@ -391,6 +395,7 @@ class SystemOverlayHost( ) { popupEngine.onCursorPosition(frame.cursorX, frame.cursorY) } + node.syncInputFocusForDomEditing() } override fun onInputFrame(viewportWidth: Int, viewportHeight: Int) { @@ -422,6 +427,11 @@ class SystemOverlayHost( if (overlayPanel.handleMouseDown(mouseX, mouseY, button)) { return true } + if (popupEngine.shouldRouteSystemInputSlotMouseDownToDom(mouseX, mouseY, button)) { + return popupEngine.focusSystemInputSlotForDomEditing(mouseX, mouseY) { index -> + node.focusInputSlot(index, mouseX, mouseY) + } + } return popupEngine.handleMouseDown(mouseX, mouseY, button) } @@ -449,9 +459,20 @@ class SystemOverlayHost( override fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean { if (!state.active) return false + if (shouldRouteSystemTextInputKeyDownToDom()) { + return false + } return popupEngine.handleKeyDown(keyCode, keyChar) } + private fun shouldRouteSystemTextInputKeyDownToDom(): Boolean { + if (popupEngine.debugOwnerScope(ownerToken) != OverlayOwnerScope.System) return false + val focused = FocusManager.focusedNode() ?: return false + if (focused !is SingleLineInputNode) return false + val key = focused.key as? String ?: return false + return key.startsWith("dsgl-system-color-picker-input-value-") + } + override fun open( anchorRect: Rect, title: String, diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt index 50cf50b..6ec7fc9 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt @@ -19,6 +19,7 @@ import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.render.RenderCommand @@ -405,17 +406,51 @@ class SystemOverlayColorPickerEntryTests { assertEquals(ColorFormatMode.HSL, modeChanged.mode) val hslLayout = host.debugSystemColorPickerBodyLayout() ?: error("layout missing") - val hueInput = hslLayout.inputSlots.firstOrNull { it.key == "h" } ?: error("h input missing") - assertTrue(host.handleMouseDown(hueInput.inputRect.x + 2, hueInput.inputRect.y + 2, MouseButton.LEFT)) - assertTrue(host.handleKeyDown(KeyCodes.DELETE, 0.toChar())) - assertTrue(host.handleKeyDown(0, '1')) - assertTrue(host.handleKeyDown(0, '8')) + val saturationInput = hslLayout.inputSlots.firstOrNull { it.key == "s" } ?: error("s input missing") + assertTrue(host.handleMouseDown(saturationInput.inputRect.x + 2, saturationInput.inputRect.y + 2, MouseButton.LEFT)) + assertTrue(host.handleKeyDown(KeyCodes.HOME, 0.toChar())) + repeat(4) { + assertTrue(host.handleKeyDown(KeyCodes.DELETE, 0.toChar())) + } assertTrue(host.handleKeyDown(0, '0')) assertTrue(host.handleKeyDown(KeyCodes.ENTER, '\n')) val updated = host.debugSystemColorPickerState() ?: error("state missing") assertEquals(ColorFormatMode.HSL, updated.mode) - assertTrue(updated.color.toArgbInt() != modeChanged.color.toArgbInt()) + val updatedLayout = host.debugSystemColorPickerBodyLayout() ?: error("layout missing") + assertEquals(listOf("h", "s", "l", "a"), updatedLayout.inputSlots.map { it.key }) + } + + @Test + fun `system picker input focus retargets by semantic key across rgb order switch`() { + val host = SystemOverlayHost(InspectorController()) + val pickerHost = host.systemInspectorColorPickerPopupHost() + val root = inspectedRoot() + + pickerHost.open(anchorRect = Rect(120, 120, 20, 18), title = "Popup", state = popupState()) + host.onInputFrame(1200, 800) + host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 128, cursorY = 128, inspectorPointerCaptured = false) + + val initialLayout = host.debugSystemColorPickerBodyLayout() ?: error("layout missing") + val redInput = initialLayout.inputSlots.firstOrNull { it.key == "r" } ?: error("r input missing") + assertTrue(host.handleMouseDown(redInput.inputRect.x + 2, redInput.inputRect.y + 2, MouseButton.LEFT)) + + val argbButton = initialLayout.argbOrderRect ?: error("argb button missing") + assertTrue(host.handleMouseDown(argbButton.x + 2, argbButton.y + 2, MouseButton.LEFT)) + host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = argbButton.x + 2, cursorY = argbButton.y + 2, inspectorPointerCaptured = false) + assertEquals("dsgl-system-color-picker-input-value-1", FocusManager.focusedNode()?.key) + + assertTrue(host.handleKeyDown(KeyCodes.HOME, 0.toChar())) + repeat(4) { + assertTrue(host.handleKeyDown(KeyCodes.DELETE, 0.toChar())) + } + assertTrue(host.handleKeyDown(0, '0')) + assertTrue(host.handleKeyDown(KeyCodes.ENTER, '\n')) + + val updated = host.debugSystemColorPickerState() ?: error("state missing") + assertEquals(RgbChannelOrder.ARGB, updated.rgbOrder) + assertEquals(1f, updated.color.a) + assertEquals("dsgl-system-color-picker-input-value-1", FocusManager.focusedNode()?.key) } From 869d8e83eb3a8878b486bf4d5c56ec77aea5f553 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Wed, 22 Apr 2026 23:15:40 +0300 Subject: [PATCH 28/78] using specific sync func for reconcilation; --- .../SystemColorPickerCustomSurfaceNodes.kt | 46 +++++++ .../org/dreamfinity/dsgl/core/dom/DOMNode.kt | 8 ++ .../dsgl/core/dom/reconcile/DomReconciler.kt | 1 + .../ColorPickerCustomNodeReconcileTests.kt | 127 ++++++++++++++++++ 4 files changed, 182 insertions(+) create mode 100644 core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/ColorPickerCustomNodeReconcileTests.kt diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerCustomSurfaceNodes.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerCustomSurfaceNodes.kt index 896128a..8fc97b0 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerCustomSurfaceNodes.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerCustomSurfaceNodes.kt @@ -134,6 +134,21 @@ internal class ColorFieldSurfaceNode( brightness = nextBrightness } + internal override fun syncCustomFrom(template: DOMNode) { + val typedTemplate = template as ColorFieldSurfaceNode + if (style != typedTemplate.style || + hueDeg != typedTemplate.hueDeg || + saturation != typedTemplate.saturation || + brightness != typedTemplate.brightness + ) { + markRenderCommandsDirty() + } + style = typedTemplate.style + hueDeg = typedTemplate.hueDeg + saturation = typedTemplate.saturation + brightness = typedTemplate.brightness + } + override fun measure(ctx: UiMeasureContext): Size { return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) } @@ -176,6 +191,15 @@ internal class HueSurfaceNode( this.hueDeg = hueDeg } + internal override fun syncCustomFrom(template: DOMNode) { + val typedTemplate = template as HueSurfaceNode + if (style != typedTemplate.style || hueDeg != typedTemplate.hueDeg) { + markRenderCommandsDirty() + } + style = typedTemplate.style + hueDeg = typedTemplate.hueDeg + } + override fun measure(ctx: UiMeasureContext): Size { return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) } @@ -214,6 +238,15 @@ internal class AlphaSurfaceNode( this.color = color } + internal override fun syncCustomFrom(template: DOMNode) { + val typedTemplate = template as AlphaSurfaceNode + if (style != typedTemplate.style || color != typedTemplate.color) { + markRenderCommandsDirty() + } + style = typedTemplate.style + color = typedTemplate.color + } + override fun measure(ctx: UiMeasureContext): Size { return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) } @@ -257,6 +290,19 @@ internal class ColorSwatchSurfaceNode( this.highlighted = highlighted } + internal override fun syncCustomFrom(template: DOMNode) { + val typedTemplate = template as ColorSwatchSurfaceNode + if (style != typedTemplate.style || + color != typedTemplate.color || + highlighted != typedTemplate.highlighted + ) { + markRenderCommandsDirty() + } + style = typedTemplate.style + color = typedTemplate.color + highlighted = typedTemplate.highlighted + } + override fun measure(ctx: UiMeasureContext): Size { return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/DOMNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/DOMNode.kt index a13aca1..26985a7 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/DOMNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/DOMNode.kt @@ -1572,6 +1572,14 @@ abstract class DOMNode( markRenderCommandsDirty() } + /** + * Optional node-specific reconcile sync hook. + * + * Called by reconciler after [syncBaseFrom] only when class and key already match. + * Default behavior is no-op. + */ + internal open fun syncCustomFrom(template: DOMNode) = Unit + internal fun captureStyleDefaults(): ComputedStyleDefaults { val existing = styleDefaultsSnapshot if (existing != null) return existing diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/DomReconciler.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/DomReconciler.kt index 95a14c9..c129038 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/DomReconciler.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/DomReconciler.kt @@ -230,6 +230,7 @@ object DomReconciler { current.syncFrom(template) } } + current.syncCustomFrom(template) } private fun countSubtree(node: DOMNode): Int { diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/ColorPickerCustomNodeReconcileTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/ColorPickerCustomNodeReconcileTests.kt new file mode 100644 index 0000000..89106ab --- /dev/null +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/ColorPickerCustomNodeReconcileTests.kt @@ -0,0 +1,127 @@ +package org.dreamfinity.dsgl.core.dom.reconcile + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertSame +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerStyle +import org.dreamfinity.dsgl.core.colorpicker.RgbaColor +import org.dreamfinity.dsgl.core.colorpicker.internal.AlphaSurfaceNode +import org.dreamfinity.dsgl.core.colorpicker.internal.ColorFieldSurfaceNode +import org.dreamfinity.dsgl.core.colorpicker.internal.ColorSwatchSurfaceNode +import org.dreamfinity.dsgl.core.colorpicker.internal.HueSurfaceNode +import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.dom.applyParent +import org.dreamfinity.dsgl.core.dom.elements.ContainerNode +import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.render.RenderCommand + +class ColorPickerCustomNodeReconcileTests { + private val ctx = object : UiMeasureContext { + override val fontHeight: Int = 9 + override fun measureText(text: String): Int = text.length * 6 + override fun paint(commands: List) = Unit + } + + @Test + fun `color field custom bind state syncs on reconcile reuse`() { + val styleA = ColorPickerStyle(inputBorderColor = 0xFF334455.toInt()) + val styleB = ColorPickerStyle(inputBorderColor = 0xFF7799AA.toInt()) + val currentRoot = ContainerNode(key = "root") + val retained = ColorFieldSurfaceNode(key = "field").applyParent(currentRoot) + retained.bind(style = styleA, color = RgbaColor(1f, 0f, 0f, 1f), hueDeg = 0f) + val before = renderCommands(retained) + + val templateRoot = ContainerNode(key = "root") + val template = ColorFieldSurfaceNode(key = "field").applyParent(templateRoot) + template.bind(style = styleB, color = RgbaColor(0f, 1f, 0f, 1f), hueDeg = 120f) + val expected = renderCommands(template) + assertNotEquals(before, expected) + + DomReconciler.reconcile(currentRoot, templateRoot) + val retainedAfter = currentRoot.children.single() as ColorFieldSurfaceNode + val after = renderCommands(retainedAfter) + + assertSame(retained, retainedAfter) + assertEquals(expected, after) + } + + @Test + fun `hue slider custom bind state syncs on reconcile reuse`() { + val style = ColorPickerStyle() + val currentRoot = ContainerNode(key = "root") + val retained = HueSurfaceNode(key = "hue").applyParent(currentRoot) + retained.bind(style = style, hueDeg = 12f) + val before = renderCommands(retained) + + val templateRoot = ContainerNode(key = "root") + val template = HueSurfaceNode(key = "hue").applyParent(templateRoot) + template.bind(style = style, hueDeg = 222f) + val expected = renderCommands(template) + assertNotEquals(before, expected) + + DomReconciler.reconcile(currentRoot, templateRoot) + val retainedAfter = currentRoot.children.single() as HueSurfaceNode + val after = renderCommands(retainedAfter) + + assertSame(retained, retainedAfter) + assertEquals(expected, after) + } + + @Test + fun `alpha slider custom bind state syncs on reconcile reuse`() { + val style = ColorPickerStyle() + val currentRoot = ContainerNode(key = "root") + val retained = AlphaSurfaceNode(key = "alpha").applyParent(currentRoot) + retained.bind(style = style, color = RgbaColor(0.5f, 0.4f, 0.3f, 1f)) + val before = renderCommands(retained) + + val templateRoot = ContainerNode(key = "root") + val template = AlphaSurfaceNode(key = "alpha").applyParent(templateRoot) + template.bind(style = style, color = RgbaColor(0.5f, 0.4f, 0.3f, 0.2f)) + val expected = renderCommands(template) + assertNotEquals(before, expected) + + DomReconciler.reconcile(currentRoot, templateRoot) + val retainedAfter = currentRoot.children.single() as AlphaSurfaceNode + val after = renderCommands(retainedAfter) + + assertSame(retained, retainedAfter) + assertEquals(expected, after) + } + + @Test + fun `color swatch custom bind state syncs on reconcile reuse`() { + val style = ColorPickerStyle() + val currentRoot = ContainerNode(key = "root") + val retained = ColorSwatchSurfaceNode(key = "swatch").applyParent(currentRoot) + retained.bind(style = style, color = RgbaColor(1f, 0f, 0f, 1f), highlighted = false) + val before = renderCommands(retained) + + val templateRoot = ContainerNode(key = "root") + val template = ColorSwatchSurfaceNode(key = "swatch").applyParent(templateRoot) + template.bind(style = style, color = RgbaColor(0f, 0f, 1f, 1f), highlighted = true) + val expected = renderCommands(template) + assertNotEquals(before, expected) + + DomReconciler.reconcile(currentRoot, templateRoot) + val retainedAfter = currentRoot.children.single() as ColorSwatchSurfaceNode + val after = renderCommands(retainedAfter) + + assertSame(retained, retainedAfter) + assertEquals(expected, after) + } + + private fun renderCommands(node: DOMNode): List { + node.render(ctx, NODE_RECT.x, NODE_RECT.y, NODE_RECT.width, NODE_RECT.height) + return buildList { + node.buildRenderCommands(ctx, this) + } + } + + private companion object { + val NODE_RECT: Rect = Rect(12, 24, 56, 14) + } +} + From 4988e2056a3dc5fb25c257633994f5834a337d04 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Thu, 23 Apr 2026 00:24:49 +0300 Subject: [PATCH 29/78] modularizing and refactoring screen rendering into smaller, descriptive functions for improved clarity and maintainability; --- .../dsgl/mcForge1710/DsglScreenHost.kt | 247 ++++++++++++++++-- 1 file changed, 218 insertions(+), 29 deletions(-) diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index 3b81d60..27b69ae 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -164,12 +164,104 @@ abstract class DsglScreenHost( if (!::adapter.isInitialized) return frameIndex += 1 tracePhase("draw.start") + val frameCursor = prepareFrameCursor() + val rebuiltThisFrame = rebuildIfNeeded() + val tree = domTree ?: return + val dtSeconds = tickFrameAndAnimations(tree, partialTicks) + val layoutPhase = commitLayoutPhaseOrFallback( + tree = tree, + mouseX = mouseX, + mouseY = mouseY, + partialTicks = partialTicks + ) ?: return + val overlayState = syncInspectorAndResolveOverlayState( + tree = tree, + dsglMouseX = frameCursor.mouseX, + dsglMouseY = frameCursor.mouseY + ) + val commands = paintApplicationRootOrFallback( + tree = tree, + stylesAlreadyApplied = layoutPhase.stylesAlreadyApplied, + mouseX = mouseX, + mouseY = mouseY, + partialTicks = partialTicks + ) ?: return + syncFeatureRuntimeFrame( + tree = tree, + dsglMouseX = frameCursor.mouseX, + dsglMouseY = frameCursor.mouseY + ) + val applicationOverlayCommands = collectApplicationOverlayCommands(overlayState.appOverlayRenderEnabled) + val systemOverlayCommands = syncSystemOverlayAndCollectCommands( + tree = tree, + dsglMouseX = frameCursor.mouseX, + dsglMouseY = frameCursor.mouseY, + systemOverlayRenderEnabled = overlayState.systemOverlayRenderEnabled + ) + stageSystemOverlayCommands( + systemOverlayCommands = systemOverlayCommands, + systemOverlayRenderEnabled = overlayState.systemOverlayRenderEnabled + ) + val debugOverlayCommands = collectDebugOverlayCommands() + updateFrameInteractionState( + tree = tree, + dtSeconds = dtSeconds, + dsglMouseX = frameCursor.mouseX, + dsglMouseY = frameCursor.mouseY, + appOverlayInputEnabled = overlayState.appOverlayInputEnabled, + systemOverlayInputEnabled = overlayState.systemOverlayInputEnabled, + inspectorBlocks = overlayState.inspectorBlocks + ) + stageApplicationOverlayCommands( + tree = tree, + applicationOverlayCommands = applicationOverlayCommands, + appOverlayRenderEnabled = overlayState.appOverlayRenderEnabled + ) + composeAndPresentFrame( + tree = tree, + commands = commands, + debugOverlayCommands = debugOverlayCommands, + rebuiltThisFrame = rebuiltThisFrame, + layoutCommittedThisFrame = layoutPhase.layoutCommittedThisFrame + ) + finishDrawScreenFrame( + tree = tree, + mouseX = mouseX, + mouseY = mouseY, + partialTicks = partialTicks + ) + } + + private data class FrameCursorPosition( + val mouseX: Int, + val mouseY: Int + ) + + private data class LayoutPhaseResult( + val stylesAlreadyApplied: Boolean, + val layoutCommittedThisFrame: Boolean + ) + + private data class OverlayLayerFrameState( + val appOverlayRenderEnabled: Boolean, + val systemOverlayRenderEnabled: Boolean, + val appOverlayInputEnabled: Boolean, + val systemOverlayInputEnabled: Boolean, + val inspectorBlocks: Boolean + ) + + private fun prepareFrameCursor(): FrameCursorPosition { updateSize(force = false) val dsglMouseX = lastViewport.rawMouseToDsglX(Mouse.getX()) val dsglMouseY = lastViewport.rawMouseToDsglY(Mouse.getY()) window.onFrame(System.currentTimeMillis()) - val rebuiltThisFrame = rebuildIfNeeded() - val tree = domTree ?: return + return FrameCursorPosition( + mouseX = dsglMouseX, + mouseY = dsglMouseY + ) + } + + private fun tickFrameAndAnimations(tree: DomTree, partialTicks: Float): Double { val nowNanos = System.nanoTime() val dtSeconds = if (lastFrameNanos == 0L) { 1.0 / 60.0 @@ -183,6 +275,15 @@ abstract class DsglScreenHost( if (animationVisualsChanged) { tree.markVisualDirty() } + return dtSeconds + } + + private fun commitLayoutPhaseOrFallback( + tree: DomTree, + mouseX: Int, + mouseY: Int, + partialTicks: Float + ): LayoutPhaseResult? { var stylesAlreadyApplied = false var layoutCommittedThisFrame = false if (needsLayout) { @@ -198,9 +299,20 @@ abstract class DsglScreenHost( flushPendingCleanup() super.drawScreen(mouseX, mouseY, partialTicks) captureColorPickerEyedropperSamples() - return + return null } } + return LayoutPhaseResult( + stylesAlreadyApplied = stylesAlreadyApplied, + layoutCommittedThisFrame = layoutCommittedThisFrame + ) + } + + private fun syncInspectorAndResolveOverlayState( + tree: DomTree, + dsglMouseX: Int, + dsglMouseY: Int + ): OverlayLayerFrameState { inspector.onLayoutCommitted(tree.root, layoutRevision) inspector.onCursorMoved(dsglMouseX, dsglMouseY) inspectorPointerCaptured = inspector.isPointerCaptured @@ -214,6 +326,22 @@ abstract class DsglScreenHost( val inspectorBlocks = systemOverlayInputEnabled && ( inspectorPointerCaptured || inspector.shouldConsumePointer(dsglMouseX, dsglMouseY) ) + return OverlayLayerFrameState( + appOverlayRenderEnabled = appOverlayRenderEnabled, + systemOverlayRenderEnabled = systemOverlayRenderEnabled, + appOverlayInputEnabled = appOverlayInputEnabled, + systemOverlayInputEnabled = systemOverlayInputEnabled, + inspectorBlocks = inspectorBlocks + ) + } + + private fun paintApplicationRootOrFallback( + tree: DomTree, + stylesAlreadyApplied: Boolean, + mouseX: Int, + mouseY: Int, + partialTicks: Float + ): List? { tracePhase("commands.start") if (!stylesAlreadyApplied) { tracePhase("style.start") @@ -229,31 +357,49 @@ abstract class DsglScreenHost( flushPendingCleanup() super.drawScreen(mouseX, mouseY, partialTicks) captureColorPickerEyedropperSamples() - return + return null } if (!stylesAlreadyApplied) { tracePhase("style.end") } + return commands + } + + private fun syncFeatureRuntimeFrame( + tree: DomTree, + dsglMouseX: Int, + dsglMouseY: Int + ) { ContextMenuRuntime.engine.onFrame(adapter, lastWidth, lastHeight, 1f) SelectRuntime.applicationEngine.onFrame(adapter, lastWidth, lastHeight, 1f) SelectRuntime.systemEngine.onFrame(adapter, lastWidth, lastHeight, 1f) ColorPickerRuntime.engine.onFrame(lastWidth, lastHeight) ColorPickerRuntime.engine.onCursorPosition(dsglMouseX, dsglMouseY) refreshActiveColorSamplerOwner(tree.root) - val applicationOverlayCommands = if (!appOverlayRenderEnabled) { + } + + private fun collectApplicationOverlayCommands(appOverlayRenderEnabled: Boolean): List { + if (!appOverlayRenderEnabled) { + return emptyList() + } + return try { + applicationOverlayHost.render(adapter, lastWidth, lastHeight) + applicationOverlayHost.paint(adapter) + } catch (error: Throwable) { + logPipelineError( + key = "draw.applicationOverlay", + message = "[DSGL] Application overlay paint failed; skipping app overlay frame: ${error.message}" + ) emptyList() - } else { - try { - applicationOverlayHost.render(adapter, lastWidth, lastHeight) - applicationOverlayHost.paint(adapter) - } catch (error: Throwable) { - logPipelineError( - key = "draw.applicationOverlay", - message = "[DSGL] Application overlay paint failed; skipping app overlay frame: ${error.message}" - ) - emptyList() - } } + } + + private fun syncSystemOverlayAndCollectCommands( + tree: DomTree, + dsglMouseX: Int, + dsglMouseY: Int, + systemOverlayRenderEnabled: Boolean + ): List { systemOverlayHost.syncFrame( inspectedRoot = tree.root, inspectedLayoutRevision = layoutRevision, @@ -261,20 +407,25 @@ abstract class DsglScreenHost( cursorY = dsglMouseY, inspectorPointerCaptured = inspectorPointerCaptured ) - val systemOverlayCommands = if (!systemOverlayRenderEnabled) { + if (!systemOverlayRenderEnabled) { + return emptyList() + } + return try { + systemOverlayHost.render(adapter, lastWidth, lastHeight) + systemOverlayHost.paint(adapter) + } catch (error: Throwable) { + logPipelineError( + key = "draw.systemOverlay", + message = "[DSGL] System overlay paint failed; skipping system overlay frame: ${error.message}" + ) emptyList() - } else { - try { - systemOverlayHost.render(adapter, lastWidth, lastHeight) - systemOverlayHost.paint(adapter) - } catch (error: Throwable) { - logPipelineError( - key = "draw.systemOverlay", - message = "[DSGL] System overlay paint failed; skipping system overlay frame: ${error.message}" - ) - emptyList() - } } + } + + private fun stageSystemOverlayCommands( + systemOverlayCommands: List, + systemOverlayRenderEnabled: Boolean + ) { systemOverlayCommandsBuffer.clear() systemOverlayCommandsBuffer.addAll(systemOverlayCommands) if (systemOverlayRenderEnabled) { @@ -285,12 +436,26 @@ abstract class DsglScreenHost( systemOverlayCommandsBuffer ) } - val debugOverlayCommands = runCatching { + } + + private fun collectDebugOverlayCommands(): List { + return runCatching { debugOverlayHost.render(lastWidth, lastHeight) debugOverlayHost.paint(adapter) }.getOrElse { emptyList() } + } + + private fun updateFrameInteractionState( + tree: DomTree, + dtSeconds: Double, + dsglMouseX: Int, + dsglMouseY: Int, + appOverlayInputEnabled: Boolean, + systemOverlayInputEnabled: Boolean, + inspectorBlocks: Boolean + ) { val contextMenuBlocks = appOverlayInputEnabled && !inspectorBlocks && ContextMenuRuntime.engine.isOpen() val selectBlocks = appOverlayInputEnabled && !inspectorBlocks && SelectRuntime.applicationEngine.isOpen() val systemSelectBlocks = systemOverlayInputEnabled && SelectRuntime.systemEngine.isOpen() @@ -324,6 +489,13 @@ abstract class DsglScreenHost( } lastMoveX = dsglMouseX lastMoveY = dsglMouseY + } + + private fun stageApplicationOverlayCommands( + tree: DomTree, + applicationOverlayCommands: List, + appOverlayRenderEnabled: Boolean + ) { applicationOverlayCommandsBuffer.clear() if (appOverlayRenderEnabled) { applicationOverlayCommandsBuffer.addAll(applicationOverlayCommands) @@ -350,6 +522,15 @@ abstract class DsglScreenHost( ColorPickerRuntime.engine.appendOverlayCommands(applicationOverlayCommandsBuffer) appendInlineColorPickerOverlayCommands(applicationOverlayCommandsBuffer) } + } + + private fun composeAndPresentFrame( + tree: DomTree, + commands: List, + debugOverlayCommands: List, + rebuiltThisFrame: Boolean, + layoutCommittedThisFrame: Boolean + ) { OverlayLayerContracts.composePaintCommands( applicationRoot = commands, applicationOverlay = applicationOverlayCommandsBuffer, @@ -373,6 +554,14 @@ abstract class DsglScreenHost( } tracePhase("commands.end") adapter.paint(composedCommandsBuffer) + } + + private fun finishDrawScreenFrame( + tree: DomTree, + mouseX: Int, + mouseY: Int, + partialTicks: Float + ) { tracePhase("draw.end") maybeLogPerf(tree) flushPendingCleanup() From fe1c2ffbe708f469028cc6ffe1bfa99c239dd5fa Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Thu, 23 Apr 2026 00:41:07 +0300 Subject: [PATCH 30/78] modularizing and refactoring input handling into smaller, descriptive functions for improved clarity and maintainability --- .../dsgl/mcForge1710/DsglScreenHost.kt | 361 +++++++++++------- 1 file changed, 219 insertions(+), 142 deletions(-) diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index 27b69ae..9e2e36c 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -730,97 +730,150 @@ abstract class DsglScreenHost( val inspectorMouseX = if (lastMoveX == Int.MIN_VALUE) lastMouseX else lastMoveX val inspectorMouseY = if (lastMoveY == Int.MIN_VALUE) lastMouseY else lastMoveY if (Keyboard.getEventKeyState()) { - if (!Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) && keyCode == Keyboard.KEY_F12) { - inspector.toggle() - inspectorPointerCaptured = false - if (inspector.active) { - DndRuntime.engine.cancelActiveDrag() - releaseDragCapture() - clearActiveTarget() - clearHoverChainStates() - } - mc.dispatchKeypresses() - return - } - if (Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) && keyCode == Keyboard.KEY_F12 && inspector.active) { - inspector.toggleMode() - mc.dispatchKeypresses() - return - } - if (keyCode == Keyboard.KEY_F10) { - val demoAnchorX = if (lastMoveX == Int.MIN_VALUE) inspectorMouseX else lastMoveX - val demoAnchorY = if (lastMoveY == Int.MIN_VALUE) inspectorMouseY else lastMoveY - systemOverlayHost.togglePanelDemo(demoAnchorX, demoAnchorY) - mc.dispatchKeypresses() - return - } - if (keyCode == Keyboard.KEY_ESCAPE && inspector.cancelPickMode()) { - logInspectorInput("escape cancelled inspector pick mode") - mc.dispatchKeypresses() - return - } - if (consumeOverlayKeyDown( + if (handleKeyboardKeyDown( keyCode = keyCode, keyChar = keyChar, inspectorMouseX = inspectorMouseX, inspectorMouseY = inspectorMouseY ) - ) { - mc.dispatchKeypresses() - return - } - if (keyCode == Keyboard.KEY_F6) { - StyleEngine.forceReloadStylesheets() - requestRebuild("style reload") - } - if (pressedKeys.add(keyCode)) { - val downEvent = KeyboardKeyDownEvent(keyChar, keyCode) - EventBus.post(downEvent) - if (downEvent.cancelled) { - pressedKeys.remove(keyCode) - } else { - window.onKeyTyped(keyChar, keyCode) - if (keyCode == Keyboard.KEY_ESCAPE) { - mc.displayGuiScreen(null) - } - } - } + ) return } else { - val keyboardBlocked = inspector.active && ( - inspector.shouldConsumeKeyboard(inspectorMouseX, inspectorMouseY) || - inspector.mode == InspectorMode.Locked - ) - if (keyboardBlocked) { - pressedKeys.remove(keyCode) - logInspectorInput("keyboard up consumed keyCode=$keyCode") - mc.dispatchKeypresses() - return + if (handleKeyboardKeyUp(keyCode, keyChar, inspectorMouseX, inspectorMouseY)) return + } + + mc.dispatchKeypresses() + } + + private fun handleKeyboardKeyDown( + keyCode: Int, + keyChar: Char, + inspectorMouseX: Int, + inspectorMouseY: Int + ): Boolean { + if (!Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) && keyCode == Keyboard.KEY_F12) { + inspector.toggle() + inspectorPointerCaptured = false + if (inspector.active) { + DndRuntime.engine.cancelActiveDrag() + releaseDragCapture() + clearActiveTarget() + clearHoverChainStates() } - if (pressedKeys.remove(keyCode)) { - EventBus.post(KeyboardKeyUpEvent(keyChar, keyCode)) + mc.dispatchKeypresses() + return true + } + if (Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) && keyCode == Keyboard.KEY_F12 && inspector.active) { + inspector.toggleMode() + mc.dispatchKeypresses() + return true + } + if (keyCode == Keyboard.KEY_F10) { + val demoAnchorX = if (lastMoveX == Int.MIN_VALUE) inspectorMouseX else lastMoveX + val demoAnchorY = if (lastMoveY == Int.MIN_VALUE) inspectorMouseY else lastMoveY + systemOverlayHost.togglePanelDemo(demoAnchorX, demoAnchorY) + mc.dispatchKeypresses() + return true + } + if (keyCode == Keyboard.KEY_ESCAPE && inspector.cancelPickMode()) { + logInspectorInput("escape cancelled inspector pick mode") + mc.dispatchKeypresses() + return true + } + if (consumeOverlayKeyDown( + keyCode = keyCode, + keyChar = keyChar, + inspectorMouseX = inspectorMouseX, + inspectorMouseY = inspectorMouseY + ) + ) { + mc.dispatchKeypresses() + return true + } + if (keyCode == Keyboard.KEY_F6) { + StyleEngine.forceReloadStylesheets() + requestRebuild("style reload") + } + if (pressedKeys.add(keyCode)) { + val downEvent = KeyboardKeyDownEvent(keyChar, keyCode) + EventBus.post(downEvent) + if (downEvent.cancelled) { + pressedKeys.remove(keyCode) + } else { + window.onKeyTyped(keyChar, keyCode) + if (keyCode == Keyboard.KEY_ESCAPE) { + mc.displayGuiScreen(null) + } } } + return false + } - mc.dispatchKeypresses() + private fun handleKeyboardKeyUp( + keyCode: Int, + keyChar: Char, + inspectorMouseX: Int, + inspectorMouseY: Int + ): Boolean { + val keyboardBlocked = inspector.active && ( + inspector.shouldConsumeKeyboard(inspectorMouseX, inspectorMouseY) || + inspector.mode == InspectorMode.Locked + ) + if (keyboardBlocked) { + pressedKeys.remove(keyCode) + logInspectorInput("keyboard up consumed keyCode=$keyCode") + mc.dispatchKeypresses() + return true + } + if (pressedKeys.remove(keyCode)) { + EventBus.post(KeyboardKeyUpEvent(keyChar, keyCode)) + } + return false } override fun handleMouseInput() { updateSize(force = false) rebuildIfNeeded() - val tree = domTree ?: return + val tree = prepareMouseInputTree() ?: return + val inputEvent = readMouseInputEvent() + syncMouseInputFrame(tree, inputEvent) + if (consumeOverlayPointerPhase(inputEvent)) return + refreshHoverTarget(inputEvent.mouseX, inputEvent.mouseY) + if (inputEvent.mouseButton > 2) return + dispatchApplicationRootPointerPhase(tree, inputEvent) + dispatchApplicationRootWheelPhase(inputEvent) + finishMouseInputEvent(inputEvent) + } + + private data class MouseInputEvent( + val mouseX: Int, + val mouseY: Int, + val dWheel: Int, + val mouseButton: Int + ) + + private fun prepareMouseInputTree(): DomTree? { + val tree = domTree ?: return null if (needsLayout) { if (tryCommitLayout(tree, "handleMouseInput")) { needsLayout = false } else { - return + return null } } + return tree + } - val mouseX = lastViewport.rawMouseToDsglX(Mouse.getEventX()) - val mouseY = lastViewport.rawMouseToDsglY(Mouse.getEventY()) - val dWheel = Mouse.getDWheel() - val mouseButton = Mouse.getEventButton() - inspector.onCursorMoved(mouseX, mouseY) + private fun readMouseInputEvent(): MouseInputEvent { + return MouseInputEvent( + mouseX = lastViewport.rawMouseToDsglX(Mouse.getEventX()), + mouseY = lastViewport.rawMouseToDsglY(Mouse.getEventY()), + dWheel = Mouse.getDWheel(), + mouseButton = Mouse.getEventButton() + ) + } + + private fun syncMouseInputFrame(tree: DomTree, inputEvent: MouseInputEvent) { + inspector.onCursorMoved(inputEvent.mouseX, inputEvent.mouseY) ContextMenuRuntime.engine.onFrame( measureContext = adapter, viewportWidth = lastWidth, @@ -844,102 +897,126 @@ abstract class DsglScreenHost( systemOverlayHost.syncFrame( inspectedRoot = tree.root, inspectedLayoutRevision = layoutRevision, - cursorX = mouseX, - cursorY = mouseY, + cursorX = inputEvent.mouseX, + cursorY = inputEvent.mouseY, inspectorPointerCaptured = inspectorPointerCaptured ) ColorPickerRuntime.engine.onFrame(lastWidth, lastHeight) refreshActiveColorSamplerOwner(tree.root) - val appPressMove = mouseButton == -1 && eventButton != -1 - if (!appPressMove && consumeOverlayPointerEvent(mouseX, mouseY, dWheel, mouseButton)) { - consumeOverlayPointerState(mouseX, mouseY) - return - } - - - refreshHoverTarget(mouseX, mouseY) + } - if (mouseButton > 2) return + private fun consumeOverlayPointerPhase(inputEvent: MouseInputEvent): Boolean { + val appPressMove = inputEvent.mouseButton == -1 && eventButton != -1 + if (!appPressMove && consumeOverlayPointerEvent( + mouseX = inputEvent.mouseX, + mouseY = inputEvent.mouseY, + dWheel = inputEvent.dWheel, + mouseButton = inputEvent.mouseButton + ) + ) { + consumeOverlayPointerState(inputEvent.mouseX, inputEvent.mouseY) + return true + } + return false + } + private fun dispatchApplicationRootPointerPhase(tree: DomTree, inputEvent: MouseInputEvent) { if (Mouse.getEventButtonState()) { - eventButton = mouseButton - lastMouseEvent = Minecraft.getSystemTime() - mapButton(mouseButton)?.let { mappedButton -> - val event = MouseDownEvent(mouseX, mouseY, mappedButton) - event.target = resolvePointerDownTarget() - EventBus.post(event) - DndRuntime.engine.onMouseDown(tree.root, event.target ?: hoverTarget, event) - if (mappedButton == MouseButton.LEFT) { - setActiveTarget(event.target ?: hoverTarget) - val captureTarget = resolveDragCaptureTarget(event.target ?: hoverTarget, mouseX, mouseY) - if (captureTarget != null) { - setDragCapture(captureTarget) - captureTarget.beginPointerCapture(mouseX, mouseY, mappedButton) - } else if (dragCaptureTarget != null) { - releaseDragCapture() - } + dispatchApplicationRootPointerDown(tree, inputEvent) + } else if (inputEvent.mouseButton != -1 && eventButton == inputEvent.mouseButton) { + dispatchApplicationRootPointerUp(tree, inputEvent) + } else if (eventButton != -1 && lastMouseEvent > 0L) { + dispatchApplicationRootPointerDrag(tree, inputEvent) + } + } + + private fun dispatchApplicationRootPointerDown(tree: DomTree, inputEvent: MouseInputEvent) { + eventButton = inputEvent.mouseButton + lastMouseEvent = Minecraft.getSystemTime() + mapButton(inputEvent.mouseButton)?.let { mappedButton -> + val event = MouseDownEvent(inputEvent.mouseX, inputEvent.mouseY, mappedButton) + event.target = resolvePointerDownTarget() + EventBus.post(event) + DndRuntime.engine.onMouseDown(tree.root, event.target ?: hoverTarget, event) + if (mappedButton == MouseButton.LEFT) { + setActiveTarget(event.target ?: hoverTarget) + val captureTarget = + resolveDragCaptureTarget(event.target ?: hoverTarget, inputEvent.mouseX, inputEvent.mouseY) + if (captureTarget != null) { + setDragCapture(captureTarget) + captureTarget.beginPointerCapture(inputEvent.mouseX, inputEvent.mouseY, mappedButton) + } else if (dragCaptureTarget != null) { + releaseDragCapture() } } - } else if (mouseButton != -1 && eventButton == mouseButton) { - val releaseTarget = resolvePointerUpTarget() - val hadDragCapture = dragCaptureTarget != null - eventButton = -1 - mapButton(mouseButton)?.let { mappedButton -> - val upEvent = MouseUpEvent(mouseX, mouseY, mappedButton) - upEvent.target = releaseTarget - EventBus.post(upEvent) - dragCaptureTarget?.endPointerCapture(mouseX, mouseY, mappedButton) - val dndConsumed = DndRuntime.engine.onMouseUp(tree.root, upEvent) - if (!hadDragCapture && !dndConsumed) { - val clickEvent = MouseClickEvent(mouseX, mouseY, mappedButton) - clickEvent.target = resolveClickTarget() - EventBus.post(clickEvent) - } + } + } + + private fun dispatchApplicationRootPointerUp(tree: DomTree, inputEvent: MouseInputEvent) { + val releaseTarget = resolvePointerUpTarget() + val hadDragCapture = dragCaptureTarget != null + eventButton = -1 + mapButton(inputEvent.mouseButton)?.let { mappedButton -> + val upEvent = MouseUpEvent(inputEvent.mouseX, inputEvent.mouseY, mappedButton) + upEvent.target = releaseTarget + EventBus.post(upEvent) + dragCaptureTarget?.endPointerCapture(inputEvent.mouseX, inputEvent.mouseY, mappedButton) + val dndConsumed = DndRuntime.engine.onMouseUp(tree.root, upEvent) + if (!hadDragCapture && !dndConsumed) { + val clickEvent = MouseClickEvent(inputEvent.mouseX, inputEvent.mouseY, mappedButton) + clickEvent.target = resolveClickTarget() + EventBus.post(clickEvent) } - clearActiveTarget() - releaseDragCapture() - } else if (eventButton != -1 && lastMouseEvent > 0L) { - mapButton(eventButton)?.let { mappedButton -> - val dx = mouseX - lastMouseX - val dy = mouseY - lastMouseY - if (dx != 0 || dy != 0) { - DndRuntime.engine.onMouseMove(tree.root, mouseX, mouseY) - val dragEvent = MouseDragEvent( - lastMouseX, - lastMouseY, - dx, - dy, - mappedButton - ) - if (!DndRuntime.engine.isDragging) { - dragEvent.target = dragCaptureTarget ?: hoverTarget - EventBus.post(dragEvent) - } - dragCaptureTarget?.continuePointerCapture( - mouseX = mouseX, - mouseY = mouseY, - mouseDX = dx, - mouseDY = dy, - button = mappedButton - ) + } + clearActiveTarget() + releaseDragCapture() + } + + private fun dispatchApplicationRootPointerDrag(tree: DomTree, inputEvent: MouseInputEvent) { + mapButton(eventButton)?.let { mappedButton -> + val dx = inputEvent.mouseX - lastMouseX + val dy = inputEvent.mouseY - lastMouseY + if (dx != 0 || dy != 0) { + DndRuntime.engine.onMouseMove(tree.root, inputEvent.mouseX, inputEvent.mouseY) + val dragEvent = MouseDragEvent( + lastMouseX, + lastMouseY, + dx, + dy, + mappedButton + ) + if (!DndRuntime.engine.isDragging) { + dragEvent.target = dragCaptureTarget ?: hoverTarget + EventBus.post(dragEvent) } + dragCaptureTarget?.continuePointerCapture( + mouseX = inputEvent.mouseX, + mouseY = inputEvent.mouseY, + mouseDX = dx, + mouseDY = dy, + button = mappedButton + ) } } + } - if (dWheel != 0) { + private fun dispatchApplicationRootWheelPhase(inputEvent: MouseInputEvent) { + if (inputEvent.dWheel != 0) { val wheelTarget = resolveWheelTarget() if (wheelTarget != null) { - val wheelEvent = MouseWheelEvent(mouseX, mouseY, dWheel) + val wheelEvent = MouseWheelEvent(inputEvent.mouseX, inputEvent.mouseY, inputEvent.dWheel) wheelEvent.target = wheelTarget EventBus.post(wheelEvent) if (!wheelEvent.cancelled) { - bubbleGenericWheel(wheelTarget, mouseX, mouseY, dWheel) + bubbleGenericWheel(wheelTarget, inputEvent.mouseX, inputEvent.mouseY, inputEvent.dWheel) } } } + } - lastMouseX = mouseX - lastMouseY = mouseY + private fun finishMouseInputEvent(inputEvent: MouseInputEvent) { + lastMouseX = inputEvent.mouseX + lastMouseY = inputEvent.mouseY } private fun consumeOverlayKeyDown( From 8483c256260cc86c14c2b36586bc8737b58a3eca Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Thu, 23 Apr 2026 01:00:29 +0300 Subject: [PATCH 31/78] refactoring input handling by adding `dispatchManualThenDomFallback` util function for improved code reuse and clarity; --- .../core/overlay/input/LayerInputDispatch.kt | 11 +++++ .../core/overlay/system/SystemOverlayHost.kt | 41 ++++++++++--------- 2 files changed, 32 insertions(+), 20 deletions(-) create mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerInputDispatch.kt diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerInputDispatch.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerInputDispatch.kt new file mode 100644 index 0000000..0b81fa1 --- /dev/null +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerInputDispatch.kt @@ -0,0 +1,11 @@ +package org.dreamfinity.dsgl.core.overlay.input + +internal inline fun dispatchManualThenDomFallback( + manualDispatch: () -> Boolean, + domFallbackDispatch: () -> Boolean +): Boolean { + if (manualDispatch()) { + return true + } + return domFallbackDispatch() +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt index ec6b718..5fa337c 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt @@ -19,6 +19,7 @@ import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.overlay.UiLayerId import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanel import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelStyle +import org.dreamfinity.dsgl.core.overlay.input.dispatchManualThenDomFallback import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.StyleApplicationScope @@ -119,38 +120,38 @@ class SystemOverlayHost( } override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean { - if (dispatchManualInput { entry -> entry.handleMouseMove(mouseX, mouseY) }) { - return true - } - return domInputRouter.handleMouseMove(mouseX, mouseY) + return dispatchManualThenDomFallback( + manualDispatch = { dispatchManualInput { entry -> entry.handleMouseMove(mouseX, mouseY) } }, + domFallbackDispatch = { domInputRouter.handleMouseMove(mouseX, mouseY) } + ) } override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { - if (dispatchManualInput { entry -> entry.handleMouseDown(mouseX, mouseY, button) }) { - return true - } - return domInputRouter.handleMouseDown(mouseX, mouseY, button) + return dispatchManualThenDomFallback( + manualDispatch = { dispatchManualInput { entry -> entry.handleMouseDown(mouseX, mouseY, button) } }, + domFallbackDispatch = { domInputRouter.handleMouseDown(mouseX, mouseY, button) } + ) } override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { - if (dispatchManualInput { entry -> entry.handleMouseUp(mouseX, mouseY, button) }) { - return true - } - return domInputRouter.handleMouseUp(mouseX, mouseY, button) + return dispatchManualThenDomFallback( + manualDispatch = { dispatchManualInput { entry -> entry.handleMouseUp(mouseX, mouseY, button) } }, + domFallbackDispatch = { domInputRouter.handleMouseUp(mouseX, mouseY, button) } + ) } override fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean { - if (dispatchManualInput { entry -> entry.handleMouseWheel(mouseX, mouseY, delta) }) { - return true - } - return domInputRouter.handleMouseWheel(mouseX, mouseY, delta) + return dispatchManualThenDomFallback( + manualDispatch = { dispatchManualInput { entry -> entry.handleMouseWheel(mouseX, mouseY, delta) } }, + domFallbackDispatch = { domInputRouter.handleMouseWheel(mouseX, mouseY, delta) } + ) } override fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean { - if (dispatchManualInput { entry -> entry.handleKeyDown(keyCode, keyChar) }) { - return true - } - return domInputRouter.handleKeyDown(keyCode, keyChar) + return dispatchManualThenDomFallback( + manualDispatch = { dispatchManualInput { entry -> entry.handleKeyDown(keyCode, keyChar) } }, + domFallbackDispatch = { domInputRouter.handleKeyDown(keyCode, keyChar) } + ) } override fun clearRefs() { From d45ab8ff4c55abff96a41ae3f43ebe3f3b6b925a Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Thu, 23 Apr 2026 21:47:05 +0300 Subject: [PATCH 32/78] moving kotlin version into gradle.properties to be stored and managed in a single place; --- build-logic/build.gradle.kts | 9 ++++++++- build.gradle.kts | 4 ++-- gradle.properties | 2 +- settings.gradle.kts | 7 +++++++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 427384a..a4590e7 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -2,12 +2,19 @@ plugins { `kotlin-dsl` } +val kotlinVersion: String = rootProject.projectDir.resolveSibling("gradle.properties") + .readLines() + .firstOrNull { it.startsWith("kotlinVersion=") } + ?.substringAfter("=") + ?.trim() + ?: error("kotlinVersion not found in root gradle.properties") + repositories { gradlePluginPortal() mavenCentral() } dependencies { - implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.10") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") implementation("org.jetbrains.dokka:org.jetbrains.dokka.gradle.plugin:2.1.0") } diff --git a/build.gradle.kts b/build.gradle.kts index 6b7c14a..64cd87b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,8 +8,8 @@ import java.util.zip.DeflaterOutputStream plugins { `java-library` - kotlin("jvm") version "2.3.10" apply false - id("org.jetbrains.kotlin.plugin.serialization") version "2.3.10" apply false + kotlin("jvm") apply false + id("org.jetbrains.kotlin.plugin.serialization") apply false id("org.jetbrains.dokka") version "2.1.0" apply false } diff --git a/gradle.properties b/gradle.properties index 50da364..73e1cfe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ group=org.dreamfinity version=0.0.1 -startParameter.offline=true org.gradle.jvmargs=-Xmx4g +kotlinVersion=2.3.21 # adapters turn on / off for development enableMinecraftForge1710=true diff --git a/settings.gradle.kts b/settings.gradle.kts index 3b9f101..79e3998 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,6 +5,13 @@ pluginManagement { fun isAdapterEnabled(name: String) = providers.gradleProperty("enable$name").orNull?.toBoolean() ?: false + val kotlinVersion: String by settings + + plugins { + kotlin("jvm") version kotlinVersion + id("org.jetbrains.kotlin.plugin.serialization") version kotlinVersion + } + repositories { maven(url = "https://maven.minecraftforge.net") gradlePluginPortal() From 43c7e9166794566bb0970c7283712ae1faf4f79b Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Fri, 24 Apr 2026 02:33:42 +0300 Subject: [PATCH 33/78] adding linter (ktlint) to the project; registering git hook, adding gradle task to add hook; --- .editorconfig | 45 + .githooks/pre-commit | 32 + .../dsgl-linter.conventions.gradle.kts | 18 + .../adapter-build-logic/settings.gradle.kts | 10 +- adapters/mc-forge-1-7-10/build.gradle.kts | 8 +- .../mc-forge-1-7-10/demo/build.gradle.kts | 83 +- .../dsgl/mcForge1710/DsglClientHotkeys.kt | 23 +- .../mcForge1710/DsglMc1710ModContainer.kt | 7 +- .../dsgl/mcForge1710/demo/ShowcaseWindow.kt | 377 +++-- ...dFlexWrapper.kt => CenteredFlexWrapper.kt} | 2 +- .../examples/cookbook/ContextMenuWindow.kt | 11 +- .../demo/examples/cookbook/DragNDropWindow.kt | 53 +- .../examples/cookbook/ModalStackWindow.kt | 50 +- .../demo/examples/cookbook/RefUsageWindow.kt | 9 +- .../demo/examples/quickstart/HelloWindow.kt | 11 +- .../quickstart/HelloWindowWithComponents.kt | 11 +- .../stateAndReactivity/GlobalStateWindow.kt | 11 +- .../demo/sections/AnimationsSection.kt | 150 +- .../demo/sections/ColorPickerSection.kt | 32 +- .../demo/sections/ContextMenuSection.kt | 1015 +++++------ .../demo/sections/CssCascadeSection.kt | 87 +- .../demo/sections/DisplaySection.kt | 174 +- .../demo/sections/DragDropSection.kt | 700 ++++---- .../demo/sections/FocusRebuildSection.kt | 40 +- .../mcForge1710/demo/sections/HooksSection.kt | 72 +- .../demo/sections/InputEventsSection.kt | 64 +- .../demo/sections/InputsGallerySection.kt | 81 +- .../demo/sections/InspectorSection.kt | 23 +- .../demo/sections/InteractionsSection.kt | 45 +- .../demo/sections/LayoutDebugSection.kt | 27 +- .../demo/sections/LayoutStyleSection.kt | 84 +- .../demo/sections/McFeaturesSection.kt | 70 +- .../demo/sections/ModalsSection.kt | 65 +- .../demo/sections/MsdfFontsSection.kt | 64 +- .../demo/sections/OverflowScrollSection.kt | 75 +- .../demo/sections/OverviewSection.kt | 14 +- .../demo/sections/PositionedLayoutSection.kt | 493 ++++-- .../demo/sections/StylesheetsSection.kt | 46 +- .../demo/sections/TextEditingSection.kt | 42 +- .../demo/sections/TextWrapSection.kt | 22 +- .../support/CapabilityChecklistCatalog.kt | 526 +++--- .../mcForge1710/demo/support/DemoSection.kt | 16 +- .../demo/support/EventFormatting.kt | 83 +- .../mcForge1710/demo/support/EventLogEntry.kt | 2 +- .../demo/support/PersistentPanels.kt | 17 +- ...itionedLayoutStickyDemoIntegrationTests.kt | 83 +- .../dreamfinity/dsgl/mcForge1710/DsglFonts.kt | 42 +- .../dsgl/mcForge1710/DsglScreenHost.kt | 590 ++++--- .../dreamfinity/dsgl/mcForge1710/GlUtils.kt | 22 +- .../dsgl/mcForge1710/Mc1710UiAdapter.kt | 656 +++++--- .../dsgl/mcForge1710/McItemStackRef.kt | 4 +- .../RenderCommandTransformStack.kt | 18 +- .../scissorsHelper/ScissorContext.kt | 10 +- .../scissorsHelper/ScissorsArea.kt | 14 +- .../text/MsdfRuntimeDebugSettings.kt | 4 +- .../dsgl/mcForge1710/text/MsdfTextRenderer.kt | 604 ++++--- .../DsglScreenHostUsedGeometryTests.kt | 226 +-- .../StickyControlClipAlignmentTests.kt | 307 ++-- build-logic/build.gradle.kts | 4 + build-logic/settings.gradle.kts | 9 + .../kotlin/dsgl-linter.conventions.gradle.kts | 18 + build.gradle.kts | 10 + core/build.gradle.kts | 34 +- .../org/dreamfinity/dsgl/core/DomTree.kt | 239 +-- .../org/dreamfinity/dsgl/core/DsglColors.kt | 2 +- .../org/dreamfinity/dsgl/core/DsglWindow.kt | 13 +- .../dreamfinity/dsgl/core/HotReloadBridge.kt | 6 +- .../org/dreamfinity/dsgl/core/ItemStackRef.kt | 2 +- .../kotlin/org/dreamfinity/dsgl/core/State.kt | 6 +- .../core/animation/AnimationDslBuilders.kt | 48 +- .../dsgl/core/animation/AnimationModel.kt | 41 +- .../dreamfinity/dsgl/core/animation/Easing.kt | 29 +- .../dsgl/core/animation/KeyframesRegistry.kt | 26 +- .../core/animation/StyleAnimationEngine.kt | 268 +-- .../ActiveColorSamplerOwnership.kt | 37 +- .../core/colorpicker/ColorPickerController.kt | 950 ++++++----- .../colorpicker/ColorPickerInfrastructure.kt | 20 +- .../ColorPickerInteractionSession.kt | 2 +- .../colorpicker/ColorPickerPopupGeometry.kt | 54 +- .../colorpicker/ColorPickerPopupRuntime.kt | 271 +-- .../dsgl/core/colorpicker/ColorPickerState.kt | 14 +- .../dsgl/core/colorpicker/ColorPickerStyle.kt | 3 +- .../core/colorpicker/ColorRecentHistory.kt | 2 +- .../dsgl/core/colorpicker/ColorTextCodec.kt | 157 +- .../dsgl/core/colorpicker/ColorTypes.kt | 94 +- .../SystemColorPickerCustomSurfaceNodes.kt | 225 ++- .../internal/SystemColorPickerOverlayNode.kt | 20 +- .../internal/SystemColorPickerPanelManager.kt | 9 +- .../SystemColorPickerPopupBodyNode.kt | 699 ++++---- .../dsgl/core/components/modal/ModalDsl.kt | 130 +- .../dsgl/core/components/modal/ModalModels.kt | 8 +- .../modal/internal/ModalHostNode.kt | 57 +- .../components/modal/internal/ModalRuntime.kt | 11 +- .../dsgl/core/contextmenu/ContextMenuDsl.kt | 136 +- .../core/contextmenu/ContextMenuEngine.kt | 313 ++-- .../dsgl/core/contextmenu/ContextMenuHost.kt | 7 +- .../ContextMenuMeasurementCache.kt | 139 +- .../dsgl/core/contextmenu/ContextMenuModel.kt | 11 +- .../dsgl/core/contextmenu/ContextMenuStyle.kt | 2 +- .../dsgl/core/contextmenu/PopupPlacement.kt | 14 +- .../core/debug/OverlayDebugControlHost.kt | 206 ++- .../dsgl/core/debug/OverlayLayerDebugState.kt | 54 +- .../core/debug/ScrollPerformanceCounters.kt | 9 +- .../dreamfinity/dsgl/core/dnd/DndBindings.kt | 16 +- .../dsgl/core/dnd/DndDescriptors.kt | 8 +- .../org/dreamfinity/dsgl/core/dnd/DndHooks.kt | 347 ++-- .../dsgl/core/dnd/DndInterfaces.kt | 17 +- .../dreamfinity/dsgl/core/dnd/DndModels.kt | 10 +- .../dreamfinity/dsgl/core/dnd/DndMonitor.kt | 8 +- .../org/dreamfinity/dsgl/core/dnd/DndProps.kt | 2 +- .../dreamfinity/dsgl/core/dnd/DndReorder.kt | 27 +- .../dreamfinity/dsgl/core/dnd/DndRuntime.kt | 2 +- .../dsgl/core/dnd/DragDataTransfer.kt | 12 +- .../dsgl/core/dnd/DragDropEvents.kt | 18 +- .../dsgl/core/dnd/DragPresentation.kt | 47 +- .../core/dnd/internal/DefaultDndEngine.kt | 417 ++--- .../dsgl/core/dom/ContextMenuEvents.kt | 8 +- .../org/dreamfinity/dsgl/core/dom/DOMNode.kt | 1484 +++++++++-------- .../dsgl/core/dom/DomElementEvents.kt | 2 +- .../dsgl/core/dom/PositionedLayoutModel.kt | 200 ++- .../dsgl/core/dom/StickyLayoutModel.kt | 179 +- .../dom/UsedInteractionGeometryResolver.kt | 53 +- .../dsgl/core/dom/debug/LayoutValidator.kt | 179 +- .../dsgl/core/dom/elements/ButtonNode.kt | 121 +- .../core/dom/elements/CheckboxGroupNode.kt | 27 +- .../dom/elements/ColorPickerInlineNode.kt | 83 +- .../dom/elements/ColorPickerPopupPaneNode.kt | 108 +- .../dsgl/core/dom/elements/ContainerNode.kt | 591 ++++--- .../dsgl/core/dom/elements/DateInputNode.kt | 20 +- .../dsgl/core/dom/elements/ImageNode.kt | 11 +- .../dsgl/core/dom/elements/InputOption.kt | 4 +- .../dsgl/core/dom/elements/InputType.kt | 16 +- .../dsgl/core/dom/elements/ItemStackNode.kt | 15 +- .../dsgl/core/dom/elements/NumberInputNode.kt | 2 +- .../core/dom/elements/PasswordInputNode.kt | 8 +- .../dsgl/core/dom/elements/RadioGroupNode.kt | 19 +- .../dsgl/core/dom/elements/RangeInputNode.kt | 53 +- .../dsgl/core/dom/elements/SelectNode.kt | 85 +- .../core/dom/elements/SingleLineInputNode.kt | 78 +- .../dsgl/core/dom/elements/TextAreaNode.kt | 174 +- .../dsgl/core/dom/elements/TextEditState.kt | 4 +- .../dsgl/core/dom/elements/TextInputNode.kt | 4 +- .../dsgl/core/dom/elements/TextNode.kt | 121 +- .../dsgl/core/dom/elements/TextSource.kt | 10 +- .../dsgl/core/dom/elements/ToggleNode.kt | 34 +- .../dom/elements/support/KeyedStateStore.kt | 2 +- .../support/MeasuredTextRangeWidthSource.kt | 69 +- .../dom/elements/support/TextChangeTracker.kt | 2 +- .../core/dom/elements/support/TextEditOps.kt | 24 +- .../support/TextEditShortcutDispatcher.kt | 4 +- .../dom/elements/support/TextLayoutEngine.kt | 140 +- .../dom/elements/support/UndoRedoHistory.kt | 4 +- .../dsgl/core/dom/layout/AffineTransform2D.kt | 42 +- .../dsgl/core/dom/layout/Border.kt | 7 +- .../dsgl/core/dom/layout/Insets.kt | 4 +- .../dreamfinity/dsgl/core/dom/layout/Size.kt | 27 +- .../dsgl/core/dom/layout/UiMeasureContext.kt | 4 +- .../dsgl/core/dom/reconcile/DomReconciler.kt | 49 +- .../dsgl/core/dom/text/ResolvedTextMetrics.kt | 2 +- .../dreamfinity/dsgl/core/dsl/ButtonDsl.kt | 14 +- .../dsgl/core/dsl/ColorPickerDsl.kt | 102 +- .../dsgl/core/dsl/ColorSurfaceDsl.kt | 104 +- .../dsgl/core/dsl/ComponentProps.kt | 4 +- .../dreamfinity/dsgl/core/dsl/ContainerDsl.kt | 10 +- .../org/dreamfinity/dsgl/core/dsl/DsglDsl.kt | 2 +- .../org/dreamfinity/dsgl/core/dsl/InputDsl.kt | 221 +-- .../org/dreamfinity/dsgl/core/dsl/MediaDsl.kt | 36 +- .../dreamfinity/dsgl/core/dsl/StyleScope.kt | 231 +-- .../org/dreamfinity/dsgl/core/dsl/TextDsl.kt | 53 +- .../org/dreamfinity/dsgl/core/dsl/UiScope.kt | 61 +- .../dsgl/core/event/ClickDispatch.kt | 27 +- .../dreamfinity/dsgl/core/event/EventBus.kt | 82 +- .../org/dreamfinity/dsgl/core/event/Events.kt | 2 +- .../dsgl/core/event/FocusGainEvent.kt | 2 +- .../dsgl/core/event/FocusLoseEvent.kt | 2 +- .../dsgl/core/event/HoverDispatcher.kt | 32 +- .../dreamfinity/dsgl/core/event/InputEvent.kt | 2 +- .../dreamfinity/dsgl/core/event/KeyInput.kt | 2 +- .../dsgl/core/event/KeyModifiers.kt | 13 +- .../dsgl/core/event/KeyboardKeyDownEvent.kt | 7 +- .../dsgl/core/event/KeyboardKeyUpEvent.kt | 7 +- .../dsgl/core/event/MouseButton.kt | 4 +- .../dsgl/core/event/MouseClickEvent.kt | 4 +- .../dsgl/core/event/MouseDownEvent.kt | 4 +- .../dsgl/core/event/MouseDragEvent.kt | 4 +- .../dsgl/core/event/MouseEnterEvent.kt | 7 +- .../dreamfinity/dsgl/core/event/MouseEvent.kt | 5 +- .../dsgl/core/event/MouseLeaveEvent.kt | 7 +- .../dsgl/core/event/MouseMoveEvent.kt | 4 +- .../dsgl/core/event/MouseOverEvent.kt | 7 +- .../dsgl/core/event/MouseUpEvent.kt | 4 +- .../dsgl/core/event/MouseWheelEvent.kt | 4 +- .../dsgl/core/event/ValueChangedEvent.kt | 2 +- .../dsgl/core/font/FontDiscovery.kt | 41 +- .../dsgl/core/font/FontRegistry.kt | 452 ++--- .../dsgl/core/font/MsdfFontMeta.kt | 92 +- .../dsgl/core/font/MsdfFontMetaParser.kt | 116 +- .../dsgl/core/font/MsdfMetaJson.kt | 70 +- .../dsgl/core/font/UnicodeCodepoints.kt | 5 +- .../dsgl/core/hooks/ComponentHookRuntime.kt | 579 +++---- .../dreamfinity/dsgl/core/hooks/UseContext.kt | 22 +- .../dreamfinity/dsgl/core/hooks/UseEffect.kt | 18 +- .../dreamfinity/dsgl/core/hooks/UseMemo.kt | 116 +- .../dreamfinity/dsgl/core/hooks/UseReducer.kt | 52 +- .../dreamfinity/dsgl/core/hooks/UseState.kt | 91 +- .../dsgl/core/hooks/ref/ElementHandle.kt | 2 +- .../dsgl/core/hooks/ref/RefManager.kt | 11 +- .../dreamfinity/dsgl/core/hooks/ref/Refs.kt | 76 +- .../dsgl/core/host/DsglWindowHost.kt | 17 +- .../dsgl/core/input/ClipboardBridge.kt | 7 +- .../core/inspector/InspectorController.kt | 976 ++++++----- .../core/inspector/InspectorDomSnapshot.kt | 2 +- .../core/inspector/InspectorEditSession.kt | 2 +- .../core/inspector/InspectorEditorRegistry.kt | 88 +- .../inspector/InspectorGeometrySupport.kt | 116 +- .../inspector/InspectorNativePresentation.kt | 10 +- .../inspector/InspectorPresentationSupport.kt | 10 +- .../InspectorStyleEditorSnapshotBuilder.kt | 403 +++-- .../internal/SystemInspectorOverlayNode.kt | 632 +++---- .../core/overlay/ApplicationOverlayHost.kt | 21 +- .../overlay/ApplicationOverlayRootNode.kt | 27 +- .../ColorPickerPopupOverlayOwnership.kt | 5 +- .../core/overlay/OverlayDebugVisualization.kt | 4 +- .../core/overlay/OverlayLayerContracts.kt | 44 +- .../core/overlay/input/LayerDomInputRouter.kt | 69 +- .../core/overlay/input/LayerInputDispatch.kt | 2 +- .../dsgl/core/overlay/panel/OverlayPanel.kt | 261 +-- .../overlay/panel/OverlayPanelDragSession.kt | 6 +- .../system/SystemOverlayCommandDslRenderer.kt | 9 +- .../system/SystemOverlayDebugCounters.kt | 13 +- .../overlay/system/SystemOverlayEntries.kt | 38 +- .../core/overlay/system/SystemOverlayHost.kt | 398 ++--- .../system/SystemOverlayPanelDemoNode.kt | 124 +- .../SystemOverlayRawRenderCommandNode.kt | 14 +- .../overlay/system/SystemOverlayRootNode.kt | 75 +- .../dsgl/core/popup/FloatingPaneDragModel.kt | 21 +- .../dsgl/core/render/RenderCommand.kt | 37 +- .../dreamfinity/dsgl/core/select/SelectDsl.kt | 99 +- .../dsgl/core/select/SelectEngine.kt | 305 ++-- .../dsgl/core/select/SelectHost.kt | 4 + .../core/select/SelectMeasurementCache.kt | 146 +- .../dsgl/core/select/SelectModel.kt | 11 +- .../dsgl/core/select/SelectRuntime.kt | 14 +- .../dsgl/core/select/SelectStyle.kt | 2 +- .../dreamfinity/dsgl/core/style/CssLength.kt | 181 +- .../dreamfinity/dsgl/core/style/DssParser.kt | 94 +- .../dsgl/core/style/StyleApplicationScope.kt | 3 +- .../dsgl/core/style/StyleEngine.kt | 878 +++++----- .../dsgl/core/style/StyleInspection.kt | 6 +- .../dreamfinity/dsgl/core/style/StyleModel.kt | 203 +-- .../dsgl/core/style/StylePropertyRegistry.kt | 400 +++-- .../dsgl/core/style/StyleSelector.kt | 123 +- .../dsgl/core/style/StyleValueParsing.kt | 283 ++-- .../dsgl/core/style/StylesheetManager.kt | 85 +- .../core/text/MinecraftFormattingParser.kt | 149 +- .../dsgl/core/text/ObfuscationTextSelector.kt | 6 +- .../dsgl/core/text/TextDecorationLayout.kt | 109 +- .../dsgl/core/text/TextStyleMetrics.kt | 39 +- .../dsgl/core/DomTreeCachingTests.kt | 46 +- .../org/dreamfinity/dsgl/core/GlyphsTests.kt | 2 - .../dsgl/core/LayoutStrictModeTests.kt | 36 +- .../dsgl/core/UiScopeHookApiTests.kt | 16 +- .../dreamfinity/dsgl/core/UseContextTests.kt | 20 +- .../dsgl/core/UseEffectHookRuntimeTests.kt | 20 +- .../dsgl/core/UseMemoHookRuntimeTests.kt | 26 +- .../dsgl/core/UseReducerHookRuntimeTests.kt | 52 +- .../dsgl/core/UseStateHookRuntimeTests.kt | 55 +- .../core/animation/AnimationEngineTests.kt | 132 +- .../dsgl/core/animation/EasingTests.kt | 1 - .../colorpicker/ColorPickerControllerTests.kt | 273 +-- .../colorpicker/ColorPickerInlineNodeTests.kt | 219 +-- .../ColorPickerInlineStylingTests.kt | 21 +- .../ColorPickerPopupEngineTests.kt | 233 +-- .../ColorPickerPopupGeometryTests.kt | 75 +- .../core/colorpicker/ColorPickerStateTests.kt | 9 +- .../core/colorpicker/ColorTextCodecTests.kt | 12 +- .../ModalHostHookOwnerPropagationTests.kt | 12 +- .../modal/ModalHostKeyboardRegressionTests.kt | 41 +- .../components/modal/ModalRuntimeTests.kt | 4 +- .../contextmenu/ContextMenuEngineTests.kt | 104 +- .../ContextMenuMeasurementCacheTests.kt | 60 +- .../core/contextmenu/PopupPlacementTests.kt | 47 +- .../debug/OverlayDebugControlHostTests.kt | 93 +- .../dnd/DndHooksRuntimeIntegrationTests.kt | 128 +- .../dnd/internal/DefaultDndEngineTests.kt | 102 +- .../dsgl/core/dom/ContextMenuEventsTests.kt | 28 +- .../dsgl/core/dom/OverflowClippingTests.kt | 223 +-- .../core/dom/OverflowInputClippingTests.kt | 154 +- .../dom/OverflowPositionedClippingTests.kt | 198 ++- .../dom/PositionedLayoutFixedBehaviorTests.kt | 330 ++-- ...itionedLayoutFixedClippingBehaviorTests.kt | 148 +- ...ayoutFixedStackingCharacterizationTests.kt | 213 +-- .../core/dom/PositionedLayoutModelTests.kt | 129 +- .../PositionedLayoutRelativeBehaviorTests.kt | 156 +- .../PositionedLayoutStaticBaselineTests.kt | 183 +- .../PositionedLayoutStickyBehaviorTests.kt | 1138 +++++++------ .../PositionedLayoutZIndexOrderingTests.kt | 80 +- .../core/dom/RangeInputScrollFastPathTests.kt | 235 +-- .../core/dom/ScrollContainerStateTests.kt | 333 ++-- .../dom/ScrollInvalidationSemanticsTests.kt | 53 +- .../dom/ScrollPerformanceCountersTests.kt | 203 ++- .../core/dom/ScrollReactiveSmoothTests.kt | 205 ++- .../dom/ScrollbarRenderingInteractionTests.kt | 480 +++--- .../core/dom/SelectNodeOwnerScopeTests.kt | 39 +- .../dom/SelectPopupAnchoringStickyTests.kt | 159 +- ...tackingContextChildContextBehaviorTests.kt | 317 ++-- .../dom/StackingContextScaffoldingTests.kt | 140 +- .../dom/StickyLayoutModelContractTests.kt | 42 +- ...dGeometryInspectorCharacterizationTests.kt | 333 ++-- .../core/dom/elements/InlineLayoutTests.kt | 467 +++--- .../core/dom/elements/LayoutValidatorTests.kt | 69 +- .../dom/elements/SizeConstraintLayoutTests.kt | 21 +- .../dom/elements/TextLayoutEngineTests.kt | 67 +- .../TextLineSpaceReservationBaselineTests.kt | 594 ++++--- ...PerformanceHotPathCharacterizationTests.kt | 480 +++--- .../dsgl/core/dom/elements/ToggleNodeTests.kt | 71 +- .../ColorPickerCustomNodeReconcileTests.kt | 22 +- .../dom/reconcile/TextSourceReconcileTests.kt | 28 +- .../dsgl/core/event/TransformHitTestTests.kt | 13 +- .../dsgl/core/font/FontDiscoveryTests.kt | 33 +- .../dsgl/core/font/MsdfFontTests.kt | 170 +- .../core/hooks/ComponentHookRuntimeTests.kt | 211 +-- .../dsgl/core/host/ViewportMappingTests.kt | 26 +- .../inspector/InspectorControllerTests.kt | 136 +- .../inspector/InspectorEditSessionTests.kt | 36 +- .../inspector/InspectorEditorRegistryTests.kt | 100 +- ...nspectorStyleEditorSnapshotBuilderTests.kt | 154 +- ...stemInspectorOverlayFocusIsolationTests.kt | 89 +- .../SystemInspectorOverlayInputBoundsTests.kt | 53 +- .../overlay/LiveLayerInteractionPathTests.kt | 220 +-- .../overlay/OverlayDebugVisualizationTests.kt | 78 +- .../OverlayGeometryIntegrationTests.kt | 43 +- .../overlay/OverlayLayerContractsTests.kt | 137 +- .../overlay/input/LayerDomInputRouterTests.kt | 204 ++- .../core/overlay/panel/OverlayPanelTests.kt | 104 +- .../InspectorDragScrollDomMigrationTests.kt | 183 +- .../InspectorDropdownCorrectiveTests.kt | 210 ++- .../system/InspectorInputPathBaselineTests.kt | 159 +- .../system/InspectorPointerAlignmentTests.kt | 170 +- .../InspectorTextEditingDomMigrationTests.kt | 148 +- .../SystemOverlayColorPickerEntryTests.kt | 352 +++- .../system/SystemOverlayDomBridgeTests.kt | 93 +- .../SystemOverlayEntryInfrastructureTests.kt | 65 +- .../SystemOverlayInspectorNativeEntryTests.kt | 534 +++--- .../SystemOverlayPanelDemoEntryTests.kt | 182 +- .../SystemOverlayStyleIsolationTests.kt | 39 +- .../core/popup/FloatingPaneDragModelTests.kt | 32 +- .../dsgl/core/ref/UseRefHookRuntimeTests.kt | 34 +- .../core/render/RenderCommandDrawTextTests.kt | 52 +- .../dsgl/core/select/SelectEngineTests.kt | 182 +- .../select/SelectMeasurementCacheTests.kt | 122 +- .../SelectRuntimeOwnershipBridgeTests.kt | 8 +- .../dsgl/core/style/CssLengthTests.kt | 97 +- .../dsgl/core/style/DssParserTests.kt | 527 +++--- .../style/LineHeightStyleContractTests.kt | 14 +- .../style/OverflowSizeStyleContractTests.kt | 41 +- .../PositionedLayoutStyleContractTests.kt | 35 +- .../core/style/StyleCascadeCombinatorTests.kt | 28 +- .../core/style/StyleDeclarationsHashTests.kt | 31 +- .../core/style/StyleEngineIncrementalTests.kt | 6 +- .../core/style/StyleEngineInspectionTests.kt | 40 +- .../style/StyleLengthUnitsIntegrationTests.kt | 29 +- .../core/style/StylePropertyRegistryTests.kt | 16 +- .../StyleScopeComplexDslContractTests.kt | 8 +- .../style/StyleValueParsingTransformTests.kt | 1 - .../text/MinecraftFormattingParserTests.kt | 211 +-- .../core/text/TextDecorationLayoutTests.kt | 212 +-- gradle/libs.versions.toml | 7 + 368 files changed, 22826 insertions(+), 18295 deletions(-) create mode 100644 .editorconfig create mode 100644 .githooks/pre-commit create mode 100644 adapters/mc-forge-1-7-10/adapter-build-logic/dsgl-linter.conventions.gradle.kts rename adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/containers/{centeredFlexWrapper.kt => CenteredFlexWrapper.kt} (99%) create mode 100644 build-logic/settings.gradle.kts create mode 100644 build-logic/src/main/kotlin/dsgl-linter.conventions.gradle.kts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..dad72e9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,45 @@ +root = true + + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 +tab_width = 4 +insert_final_newline = true +trim_trailing_whitespace = true + + +[*.md] +trim_trailing_whitespace = false + + +[*.{yml,yaml,toml,json}] +indent_size = 2 + + +[*.{kt,kts}] +max_line_length = 120 +ktlint_code_style = ktlint_official +ktlint_experimental = disabled +ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ +ij_kotlin_packages_to_use_import_on_demand = org.dreamfinity.dsgl.core.animation.*,org.dreamfinity.dsgl.core.colorpicker.*,org.dreamfinity.dsgl.core.dnd.*,org.dreamfinity.dsgl.core.dom.elements.*,org.dreamfinity.dsgl.core.dom.elements.support.*,org.dreamfinity.dsgl.core.dom.layout.*,org.dreamfinity.dsgl.core.dsl.*,org.dreamfinity.dsgl.core.event.*,org.dreamfinity.dsgl.core.font.*,org.dreamfinity.dsgl.core.inspector.*,org.dreamfinity.dsgl.core.style.*,org.dreamfinity.dsgl.core.text.*,org.lwjgl.opengl.* +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true +ij_kotlin_indent_before_arrow_on_new_line = false +ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 4 +ktlint_function_signature_body_expression_wrapping = multiline +ktlint_class_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 1 +ktlint_chain_method_rule_force_multiline_when_chain_operator_count_greater_or_equal_than = 3 +ktlint_function_naming_ignore_when_annotated_with = Composable,DsglDsl +ktlint_ignore_back_ticked_identifier = true +ktlint_standard_multiline-expression-wrapping = enabled +ij_formatter_tags_enabled = true +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on + +[**/src/test/kotlin/**/*.kt] +max_line_length = 160 +ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 4 +ij_kotlin_packages_to_use_import_on_demand = kotlin.test.*,org.dreamfinity.dsgl.core.colorpicker.*,org.dreamfinity.dsgl.core.dnd.*,org.dreamfinity.dsgl.core.event.* diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100644 index 0000000..39361e9 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,32 @@ +#!/bin/sh +# Runs ktlintFormat on staged Kotlin files before commit. +# Auto-correctable violations are fixed and re-staged automatically. +# Non-auto-correctable violations abort the commit. + +exec 1>&2 + +STAGED_KT=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.(kt|kts)$') + +if [ -z "$STAGED_KT" ]; then + exit 0 +fi + +echo "[pre-commit] Running ktlintFormat..." + +./gradlew ktlintFormat +STATUS=$? + +# Re-stage any files that ktlint auto-corrected (only from the originally staged set) +echo "$STAGED_KT" | tr ' ' '\n' | while IFS= read -r file; do + [ -f "$file" ] && git add "$file" +done + +if [ $STATUS -ne 0 ]; then + echo "" + echo "[pre-commit] ktlintFormat found violations that cannot be auto-corrected." + echo "[pre-commit] Fix them manually, then re-run git commit." + exit 1 +fi + +echo "[pre-commit] ktlintFormat passed." +exit 0 \ No newline at end of file diff --git a/adapters/mc-forge-1-7-10/adapter-build-logic/dsgl-linter.conventions.gradle.kts b/adapters/mc-forge-1-7-10/adapter-build-logic/dsgl-linter.conventions.gradle.kts new file mode 100644 index 0000000..78c3063 --- /dev/null +++ b/adapters/mc-forge-1-7-10/adapter-build-logic/dsgl-linter.conventions.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("org.jlleitschuh.gradle.ktlint") +} + +ktlint { + version.set("1.5.0") + outputToConsole.set(true) + coloredOutput.set(true) + reporters { + reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.PLAIN) + reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE) + } + filter { + exclude("**/generated/**") + exclude("**/build/**") + include("**/*.kt") + } +} \ No newline at end of file diff --git a/adapters/mc-forge-1-7-10/adapter-build-logic/settings.gradle.kts b/adapters/mc-forge-1-7-10/adapter-build-logic/settings.gradle.kts index 6e9783e..e66bc4b 100644 --- a/adapters/mc-forge-1-7-10/adapter-build-logic/settings.gradle.kts +++ b/adapters/mc-forge-1-7-10/adapter-build-logic/settings.gradle.kts @@ -1,5 +1,3 @@ -import org.gradle.kotlin.dsl.maven - pluginManagement { includeBuild("../../../build-logic") repositories { @@ -8,3 +6,11 @@ pluginManagement { maven("https://maven.minecraftforge.net") } } + +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../../../gradle/libs.versions.toml")) + } + } +} diff --git a/adapters/mc-forge-1-7-10/build.gradle.kts b/adapters/mc-forge-1-7-10/build.gradle.kts index 097e256..8a0e02c 100644 --- a/adapters/mc-forge-1-7-10/build.gradle.kts +++ b/adapters/mc-forge-1-7-10/build.gradle.kts @@ -2,6 +2,7 @@ plugins { id("dsgl-mc-adapter.conventions") id("dsgl-mc-forge-1-7-10.conventions") id("dsgl-releaseable-module.conventions") + id("dsgl-linter.conventions") } dsglRelease { @@ -9,9 +10,10 @@ dsglRelease { } dependencies { - val coreProject = findProject(":core") - ?: findProject(":dsgl:core") - ?: error("DSGL core project not found (expected :core or :dsgl:core).") + val coreProject = + findProject(":core") + ?: findProject(":dsgl:core") + ?: error("DSGL core project not found (expected :core or :dsgl:core).") implementation(coreProject) testImplementation(kotlin("test-junit")) testImplementation(kotlin("test")) diff --git a/adapters/mc-forge-1-7-10/demo/build.gradle.kts b/adapters/mc-forge-1-7-10/demo/build.gradle.kts index 00b01ad..1daa4bb 100644 --- a/adapters/mc-forge-1-7-10/demo/build.gradle.kts +++ b/adapters/mc-forge-1-7-10/demo/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("dsgl-mc-adapter.conventions") id("dsgl-mc-forge-1-7-10.conventions") + id("dsgl-linter.conventions") } val modId: String by project @@ -23,16 +24,17 @@ val dsglOverlayDebug: String by project val dsglOverlayControls: String by project val hotReloadAgentLibraryName: String? by project -val baseModMetadataTokens = mapOf( - "modId" to modId, - "modGroup" to modGroup, - "modName" to modName, - "modAuthor" to modAuthor, - "modDescription" to modDescription, - "modCredits" to modCredits, - "modIcon" to modIcon, - "gameVersion" to gameVersion -) +val baseModMetadataTokens = + mapOf( + "modId" to modId, + "modGroup" to modGroup, + "modName" to modName, + "modAuthor" to modAuthor, + "modDescription" to modDescription, + "modCredits" to modCredits, + "modIcon" to modIcon, + "gameVersion" to gameVersion, + ) fun currentModVersion(): String { val dynamic = (findProperty("modVersion") as? String)?.trim() @@ -41,25 +43,24 @@ fun currentModVersion(): String { throw GradleException("Missing required property 'modVersion' for mc-forge-1-7-10-demo module.") } -fun currentModMetadataTokens(): Map { - return baseModMetadataTokens + ("modVersion" to currentModVersion()) -} +fun currentModMetadataTokens(): Map = baseModMetadataTokens + ("modVersion" to currentModVersion()) fun hotReloadAgentLibraryFile(): File { val explicitLibraryName = hotReloadAgentLibraryName?.trim()?.takeIf { it.isNotEmpty() } val osName = System.getProperty("os.name")?.lowercase() - val libraryName = explicitLibraryName ?: when { - osName == null -> throw GradleException( - "Unable to determine current operating system for DSGL hot-reload agent, and 'hotReloadAgentLibraryName' is not set." - ) - - osName.startsWith("windows") -> "dsgl_hot_reload_agent.dll" - osName.startsWith("linux") -> "libdsgl_hot_reload_agent.so" - osName.startsWith("mac") || osName.startsWith("darwin") -> "libdsgl_hot_reload_agent.dylib" - else -> throw GradleException( - "Unsupported operating system for DSGL hot-reload agent: $osName, and 'hotReloadAgentLibraryName' is not set." - ) - } + val libraryName = + explicitLibraryName ?: when { + osName == null -> throw GradleException( + "Unable to determine current operating system for DSGL hot-reload agent, and 'hotReloadAgentLibraryName' is not set.", + ) + + osName.startsWith("windows") -> "dsgl_hot_reload_agent.dll" + osName.startsWith("linux") -> "libdsgl_hot_reload_agent.so" + osName.startsWith("mac") || osName.startsWith("darwin") -> "libdsgl_hot_reload_agent.dylib" + else -> throw GradleException( + "Unsupported operating system for DSGL hot-reload agent: $osName, and 'hotReloadAgentLibraryName' is not set.", + ) + } return project.rootDir.resolve("dsgl-hot-reload-agent/target/release/$libraryName") } @@ -93,22 +94,23 @@ val generateModMetadata by tasks.registering { const val MOD_CREDITS: String = "${tokens["modCredits"]}" const val MOD_ICON: String = "${tokens["modIcon"]}" } - """.trimIndent() + """.trimIndent(), ) } } tasks { runClient { - var jvmArgs = listOf( - "-Ddsgl.msdf.debug=$msdfDebug", - "-Ddsgl.msdf.debug.decorations=$msdfDebugDecorations", - "-Ddsgl.msdf.debug.performance=$msdfDebugPerformance", - "-Ddsgl.rebuild.trace=$rebuildTrace", - "-Ddsgl.perf.debug=$perfDebug", - "-Ddsgl.overlay.debug=$dsglOverlayDebug", - "-Ddsgl.overlay.controls=$dsglOverlayControls", - ) + var jvmArgs = + listOf( + "-Ddsgl.msdf.debug=$msdfDebug", + "-Ddsgl.msdf.debug.decorations=$msdfDebugDecorations", + "-Ddsgl.msdf.debug.performance=$msdfDebugPerformance", + "-Ddsgl.rebuild.trace=$rebuildTrace", + "-Ddsgl.perf.debug=$perfDebug", + "-Ddsgl.overlay.debug=$dsglOverlayDebug", + "-Ddsgl.overlay.controls=$dsglOverlayControls", + ) if (hotReload.toBoolean()) { jvmArgs = jvmArgs + listOf("-agentpath:${hotReloadAgentLibraryFile().absolutePath}") @@ -131,7 +133,10 @@ tasks { } kotlin { - sourceSets.getByName("main").kotlin.srcDir(generatedModMetadataDir) + sourceSets + .getByName("main") + .kotlin + .srcDir(generatedModMetadataDir) } tasks.named("compileKotlin") { @@ -203,8 +208,10 @@ repositories { } dependencies { - implementation("org.dreamfinity:dsgl-core:0.0.1") - implementation("org.dreamfinity:dsgl-mc-forge-1-7-10:0.0.1:dev") + implementation(project(":core")) + implementation(project(":adapters:mc-forge-1-7-10")) +// implementation("org.dreamfinity:dsgl-core:0.0.1") +// implementation("org.dreamfinity:dsgl-mc-forge-1-7-10:0.0.1:dev") testImplementation(kotlin("test-junit")) testImplementation(kotlin("test")) } diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglClientHotkeys.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglClientHotkeys.kt index 16f488e..f6f6354 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglClientHotkeys.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglClientHotkeys.kt @@ -16,26 +16,31 @@ import org.lwjgl.input.Keyboard */ @SideOnly(Side.CLIENT) object DsglClientHotkeys { - private val openShowcaseKey = KeyBinding( - "key.dsgl.open_showcase", - Keyboard.KEY_J, - "key.categories.dsgl" - ) + private val openShowcaseKey = + KeyBinding( + "key.dsgl.open_showcase", + Keyboard.KEY_J, + "key.categories.dsgl", + ) private var registered: Boolean = false fun register() { if (registered) return ClientRegistry.registerKeyBinding(openShowcaseKey) - FMLCommonHandler.instance().bus().register(this) + FMLCommonHandler + .instance() + .bus() + .register(this) registered = true } @SubscribeEvent fun onKeyInput(event: InputEvent.KeyInputEvent) { when { - openShowcaseKey.isPressed -> Minecraft - .getMinecraft() - .displayGuiScreen(object : DsglScreenHost({ ShowcaseWindow() }) {}) + openShowcaseKey.isPressed -> + Minecraft + .getMinecraft() + .displayGuiScreen(object : DsglScreenHost({ ShowcaseWindow() }) {}) } } } diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglMc1710ModContainer.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglMc1710ModContainer.kt index 3591169..d49a153 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglMc1710ModContainer.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglMc1710ModContainer.kt @@ -13,12 +13,15 @@ import net.minecraft.client.Minecraft name = DsglMc1710DemoGeneratedMetadata.MOD_NAME, version = DsglMc1710DemoGeneratedMetadata.MOD_VERSION, acceptedMinecraftVersions = DsglMc1710DemoGeneratedMetadata.MC_VERSION_RANGE, - useMetadata = true + useMetadata = true, ) class DsglMc1710ModContainer { @Mod.EventHandler fun onInit(event: FMLInitializationEvent) { - if (FMLCommonHandler.instance().side.isClient) { + if (FMLCommonHandler + .instance() + .side.isClient + ) { DsglFonts.ensureInitialized(Minecraft.getMinecraft().mcDataDir, javaClass.classLoader) DsglClientHotkeys.register() } diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/ShowcaseWindow.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/ShowcaseWindow.kt index 2e078bb..aaa996b 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/ShowcaseWindow.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/ShowcaseWindow.kt @@ -11,21 +11,54 @@ import org.dreamfinity.dsgl.core.DsglWindow import org.dreamfinity.dsgl.core.animation.keyframes import org.dreamfinity.dsgl.core.components.modal.ModalSpec import org.dreamfinity.dsgl.core.components.modal.modalHost +import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.Event import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection import org.dreamfinity.dsgl.core.style.JustifyContent import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.mcForge1710.McItemStackRef -import org.dreamfinity.dsgl.core.dsl.* -import org.dreamfinity.dsgl.mcForge1710.demo.sections.* -import org.dreamfinity.dsgl.mcForge1710.demo.support.* +import org.dreamfinity.dsgl.mcForge1710.demo.sections.McFeaturesShellProps +import org.dreamfinity.dsgl.mcForge1710.demo.sections.animationsSection +import org.dreamfinity.dsgl.mcForge1710.demo.sections.colorPickerSection +import org.dreamfinity.dsgl.mcForge1710.demo.sections.contextMenuSection +import org.dreamfinity.dsgl.mcForge1710.demo.sections.cssCascadeCombinatorsSection +import org.dreamfinity.dsgl.mcForge1710.demo.sections.displaySection +import org.dreamfinity.dsgl.mcForge1710.demo.sections.dragNDropSection +import org.dreamfinity.dsgl.mcForge1710.demo.sections.focusRebuildSection +import org.dreamfinity.dsgl.mcForge1710.demo.sections.hooksSection +import org.dreamfinity.dsgl.mcForge1710.demo.sections.inputEventsSection +import org.dreamfinity.dsgl.mcForge1710.demo.sections.inputsGallerySection +import org.dreamfinity.dsgl.mcForge1710.demo.sections.inspectorSection +import org.dreamfinity.dsgl.mcForge1710.demo.sections.interactionsSection +import org.dreamfinity.dsgl.mcForge1710.demo.sections.layoutDebugSection +import org.dreamfinity.dsgl.mcForge1710.demo.sections.layoutStyleSection +import org.dreamfinity.dsgl.mcForge1710.demo.sections.mcFeaturesSection +import org.dreamfinity.dsgl.mcForge1710.demo.sections.modalsSection +import org.dreamfinity.dsgl.mcForge1710.demo.sections.msdfFontsSection +import org.dreamfinity.dsgl.mcForge1710.demo.sections.overflowScrollSection +import org.dreamfinity.dsgl.mcForge1710.demo.sections.overviewSection +import org.dreamfinity.dsgl.mcForge1710.demo.sections.positionedLayoutSection +import org.dreamfinity.dsgl.mcForge1710.demo.sections.stylesheetsSection +import org.dreamfinity.dsgl.mcForge1710.demo.sections.textEditingSection +import org.dreamfinity.dsgl.mcForge1710.demo.sections.textWrapSection +import org.dreamfinity.dsgl.mcForge1710.demo.support.CapabilityChecklistCatalog +import org.dreamfinity.dsgl.mcForge1710.demo.support.CapabilityId +import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_ACCENT +import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_BG +import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED +import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_OK +import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_SURFACE +import org.dreamfinity.dsgl.mcForge1710.demo.support.DemoSection +import org.dreamfinity.dsgl.mcForge1710.demo.support.EventLogEntry +import org.dreamfinity.dsgl.mcForge1710.demo.support.formatEventLine +import org.dreamfinity.dsgl.mcForge1710.demo.support.renderChecklistPanel +import org.dreamfinity.dsgl.mcForge1710.demo.support.renderEventInspectorPanel import java.awt.image.BufferedImage import java.io.File import javax.imageio.ImageIO class ShowcaseWindow : DsglWindow() { - private var viewportWidth: Int = 320 private var viewportHeight: Int = 240 internal val viewportWidthPx: Int @@ -97,7 +130,7 @@ class ShowcaseWindow : DsglWindow() { color = DEMO_MUTED padding = 4.px } - } + }, ) div({ @@ -117,9 +150,11 @@ class ShowcaseWindow : DsglWindow() { gap = 4.px backgroundColor = DEMO_SURFACE color = DsglColors.TEXT - border { width = 1.px; color = DsglColors.BORDER } + border { + width = 1.px + color = DsglColors.BORDER + } } - }) { text("Sections", { style = { color = DsglColors.WHITE } }) DemoSection.entries.forEach { section -> @@ -143,144 +178,174 @@ class ShowcaseWindow : DsglWindow() { gap = 4.px backgroundColor = DEMO_SURFACE color = DsglColors.TEXT - border { width = 1.px; color = DsglColors.BORDER } + border { + width = 1.px + color = DsglColors.BORDER + } } }) { text(selectedSection.title, { style = { color = DsglColors.WHITE } }) text(selectedSection.subtitle, { style = { color = DEMO_MUTED } }) when (selectedSection) { - DemoSection.OVERVIEW -> overviewSection( - implementedCapabilities = implementedCapabilities, - onManualInvalidate = ::requestManualInvalidate, - onInfo = ::appendInfo - ) - - DemoSection.INSPECTOR -> inspectorSection( - onInfo = ::appendInfo - ) - - DemoSection.LAYOUT_STYLE -> layoutStyleSection( - onInfo = ::appendInfo, - onLogHook = { hookName, event, note -> logHook(hookName, event, note) } - ) - - DemoSection.LAYOUT_DEBUG -> layoutDebugSection( - onClearLogs = ::clearEventLogs, - onInfo = ::appendInfo - ) - - DemoSection.POSITIONED_LAYOUT -> positionedLayoutSection( - viewportWidthPx = viewportWidthPx - ) - - DemoSection.OVERFLOW_SCROLL -> overflowScrollSection( - onInfo = ::appendInfo - ) - - DemoSection.DISPLAY -> displaySection( - onInfo = ::appendInfo, - onLogHook = { hookName, event, note -> logHook(hookName, event, note) } - ) - - DemoSection.TEXT_WRAP -> textWrapSection( - onInfo = ::appendInfo - ) - - DemoSection.MSDF_FONTS -> msdfFontsSection( - onInfo = ::appendInfo - ) - - DemoSection.ANIMATIONS -> animationsSection( - onInfo = ::appendInfo - ) - - DemoSection.MODALS -> modalsSection( - modals = demoModals, - onPushModal = ::pushModal, - onRemoveModal = ::removeModal, - onPopTopModal = ::popTopModal, - onClearModals = { demoModals = emptyList() }, - onInfo = ::appendInfo - ) - - DemoSection.CONTEXT_MENU -> contextMenuSection( - onInfo = ::appendInfo - ) - - DemoSection.STYLESHEETS -> stylesheetsSection( - onLogHook = { hookName, event, note -> logHook(hookName, event, note) }, - onInfo = ::appendInfo, - loadStylesheetText = { loadStylesheetEditorFromFile("styles section load") }, - saveStylesheetText = { content -> - saveStylesheetEditorToFile( - content, - "styles section save" - ) - }, - onReloadStylesheets = { reloadStylesheetsProgrammatically("styles section button") } - ) - - DemoSection.CSS_CASCADE -> cssCascadeCombinatorsSection( - onLogHook = { hookName, event, note -> logHook(hookName, event, note) } - ) - - DemoSection.INPUTS -> inputsGallerySection( - clippingScrollDemoText = clippingScrollDemoText, - onClippingScrollDemoTextChange = { clippingScrollDemoText = it } - ) - - DemoSection.INPUT_EVENTS -> inputEventsSection( - onLogHook = { hookName, event, note -> logHook(hookName, event, note) } - ) + DemoSection.OVERVIEW -> + overviewSection( + implementedCapabilities = implementedCapabilities, + onManualInvalidate = ::requestManualInvalidate, + onInfo = ::appendInfo, + ) - DemoSection.COLOR_PICKER -> colorPickerSection() + DemoSection.INSPECTOR -> + inspectorSection( + onInfo = ::appendInfo, + ) + + DemoSection.LAYOUT_STYLE -> + layoutStyleSection( + onInfo = ::appendInfo, + onLogHook = { hookName, event, note -> logHook(hookName, event, note) }, + ) - DemoSection.TEXT_EDITING -> textEditingSection( - onLogHook = { hookName, event, note -> logHook(hookName, event, note) } - ) - - DemoSection.REFS -> hooksSection( - onInfo = ::appendInfo, - onLogHook = { hookName, event, note -> logHook(hookName, event, note) } - ) - - DemoSection.DRAG_DROP -> dragNDropSection( - onInfo = ::appendInfo, - onClearLogs = ::clearEventLogs, - onLogHook = { hookName, event, note -> logHook(hookName, event, note) } - ) - - DemoSection.INTERACTIONS -> interactionsSection( - onInfo = ::appendInfo, - onLogHook = { hookName, event, note -> logHook(hookName, event, note) } - ) - - DemoSection.FOCUS_REBUILD -> focusRebuildSection( - renderPasses = renderPasses, - onManualInvalidate = ::requestManualInvalidate, - onInfo = ::appendInfo, - onLogHook = { hookName, event, note -> logHook(hookName, event, note) } - ) - - DemoSection.MC_FEATURES -> mcFeaturesSection( - props = McFeaturesShellProps( + DemoSection.LAYOUT_DEBUG -> + layoutDebugSection( + onClearLogs = ::clearEventLogs, + onInfo = ::appendInfo, + ) + + DemoSection.POSITIONED_LAYOUT -> + positionedLayoutSection( viewportWidthPx = viewportWidthPx, - viewportHeightPx = viewportHeightPx, - mediaReady = mediaReady, - resourceImageSource = resourceImageSource, - fileImageSource = fileImageSource, - httpImageSource = httpImageSource, - flatItemRef = flatItemRef, - blockItemRef = blockItemRef, + ) + + DemoSection.OVERFLOW_SCROLL -> + overflowScrollSection( + onInfo = ::appendInfo, + ) + + DemoSection.DISPLAY -> + displaySection( + onInfo = ::appendInfo, + onLogHook = { hookName, event, note -> logHook(hookName, event, note) }, + ) + + DemoSection.TEXT_WRAP -> + textWrapSection( + onInfo = ::appendInfo, + ) + + DemoSection.MSDF_FONTS -> + msdfFontsSection( + onInfo = ::appendInfo, + ) + + DemoSection.ANIMATIONS -> + animationsSection( + onInfo = ::appendInfo, + ) + + DemoSection.MODALS -> + modalsSection( + modals = demoModals, + onPushModal = ::pushModal, + onRemoveModal = ::removeModal, + onPopTopModal = ::popTopModal, + onClearModals = { demoModals = emptyList() }, + onInfo = ::appendInfo, + ) + + DemoSection.CONTEXT_MENU -> + contextMenuSection( + onInfo = ::appendInfo, + ) + + DemoSection.STYLESHEETS -> + stylesheetsSection( + onLogHook = { hookName, event, note -> logHook(hookName, event, note) }, + onInfo = ::appendInfo, + loadStylesheetText = { loadStylesheetEditorFromFile("styles section load") }, + saveStylesheetText = { content -> + saveStylesheetEditorToFile( + content, + "styles section save", + ) + }, + onReloadStylesheets = { + reloadStylesheetsProgrammatically( + "styles section button", + ) + }, + ) + + DemoSection.CSS_CASCADE -> + cssCascadeCombinatorsSection( + onLogHook = { hookName, event, note -> logHook(hookName, event, note) }, + ) + + DemoSection.INPUTS -> + inputsGallerySection( clippingScrollDemoText = clippingScrollDemoText, onClippingScrollDemoTextChange = { clippingScrollDemoText = it }, - currentGuiScale = ::currentGuiScale, - guiScaleLabel = ::guiScaleLabel, - setGuiScale = ::setGuiScale, - cycleGuiScale = ::cycleGuiScale, - onLogHook = { hookName, event, note -> logHook(hookName, event, note) } ) - ) + + DemoSection.INPUT_EVENTS -> + inputEventsSection( + onLogHook = { hookName, event, note -> logHook(hookName, event, note) }, + ) + + DemoSection.COLOR_PICKER -> colorPickerSection() + + DemoSection.TEXT_EDITING -> + textEditingSection( + onLogHook = { hookName, event, note -> logHook(hookName, event, note) }, + ) + + DemoSection.REFS -> + hooksSection( + onInfo = ::appendInfo, + onLogHook = { hookName, event, note -> logHook(hookName, event, note) }, + ) + + DemoSection.DRAG_DROP -> + dragNDropSection( + onInfo = ::appendInfo, + onClearLogs = ::clearEventLogs, + onLogHook = { hookName, event, note -> logHook(hookName, event, note) }, + ) + + DemoSection.INTERACTIONS -> + interactionsSection( + onInfo = ::appendInfo, + onLogHook = { hookName, event, note -> logHook(hookName, event, note) }, + ) + + DemoSection.FOCUS_REBUILD -> + focusRebuildSection( + renderPasses = renderPasses, + onManualInvalidate = ::requestManualInvalidate, + onInfo = ::appendInfo, + onLogHook = { hookName, event, note -> logHook(hookName, event, note) }, + ) + + DemoSection.MC_FEATURES -> + mcFeaturesSection( + props = + McFeaturesShellProps( + viewportWidthPx = viewportWidthPx, + viewportHeightPx = viewportHeightPx, + mediaReady = mediaReady, + resourceImageSource = resourceImageSource, + fileImageSource = fileImageSource, + httpImageSource = httpImageSource, + flatItemRef = flatItemRef, + blockItemRef = blockItemRef, + clippingScrollDemoText = clippingScrollDemoText, + onClippingScrollDemoTextChange = { clippingScrollDemoText = it }, + currentGuiScale = ::currentGuiScale, + guiScaleLabel = ::guiScaleLabel, + setGuiScale = ::setGuiScale, + cycleGuiScale = ::cycleGuiScale, + onLogHook = { hookName, event, note -> logHook(hookName, event, note) }, + ), + ) } } @@ -297,14 +362,14 @@ class ShowcaseWindow : DsglWindow() { eventLogs = eventLogs, maxEventLogs = maxEventLogs, visibleEventLines = visibleEventLines, - onClearLogs = ::clearEventLogs + onClearLogs = ::clearEventLogs, ) renderChecklistPanel( implementedCapabilities = implementedCapabilities, checklistPage = checklistPage, checklistPageSize = checklistPageSize, onSetChecklistPage = { checklistPage = it }, - onMoveChecklistPage = ::moveChecklistPage + onMoveChecklistPage = ::moveChecklistPage, ) } } @@ -348,7 +413,12 @@ class ShowcaseWindow : DsglWindow() { invalidate() } - internal fun logHook(hookName: String, event: Event, note: String? = null, color: Int = DsglColors.TEXT) { + internal fun logHook( + hookName: String, + event: Event, + note: String? = null, + color: Int = DsglColors.TEXT, + ) { val line = formatEventLine(hookName, event, note) appendLog(line, color) } @@ -357,19 +427,20 @@ class ShowcaseWindow : DsglWindow() { appendLog(message, DEMO_OK) } - internal fun currentGuiScale(): Int { - return Minecraft.getMinecraft().gameSettings.guiScale.coerceIn(0, 4) - } + internal fun currentGuiScale(): Int = + Minecraft + .getMinecraft() + .gameSettings.guiScale + .coerceIn(0, 4) - internal fun guiScaleLabel(value: Int = currentGuiScale()): String { - return when (value.coerceIn(0, 4)) { + internal fun guiScaleLabel(value: Int = currentGuiScale()): String = + when (value.coerceIn(0, 4)) { 0 -> "Auto" 1 -> "1x" 2 -> "2x" 3 -> "3x" else -> "4x" } - } internal fun setGuiScale(value: Int) { val mc = Minecraft.getMinecraft() @@ -438,12 +509,12 @@ class ShowcaseWindow : DsglWindow() { writeDemoImage( File(dataDir, "dsgl/demo/local_showcase.png"), 0xFF3B71A5.toInt(), - 0xFFF7B25B.toInt() + 0xFFF7B25B.toInt(), ) writeDemoImage( File(dataDir, "dsgl/cache/downloads/demo.local/assets/showcase_http.png"), 0xFF2D8757.toInt(), - 0xFFC8E66B.toInt() + 0xFFC8E66B.toInt(), ) writeDemoFolderIcon(File(dataDir, "dsgl/demo/folder.png")) writeDemoDocumentIcon(File(dataDir, "dsgl/demo/document.png")) @@ -596,7 +667,7 @@ class ShowcaseWindow : DsglWindow() { border-width: 1px; padding: 2px 4px; } - """.trimIndent() + """.trimIndent(), ) appendInfo("Created demo stylesheet: ${stylesheetFile.name}") created = true @@ -654,7 +725,7 @@ class ShowcaseWindow : DsglWindow() { border-width: 1px; padding: 2px 4px; } - """.trimIndent() + """.trimIndent(), ) appendInfo("Patched demo stylesheet with CSS units section") created = true @@ -689,7 +760,7 @@ class ShowcaseWindow : DsglWindow() { border-color: #555555; color: #8E8E8E; } - """.trimIndent() + """.trimIndent(), ) appendInfo("Patched demo stylesheet with select styles") created = true @@ -711,7 +782,10 @@ class ShowcaseWindow : DsglWindow() { color = 0xFFFF6B6B.toInt() } at(50f) { - transform { rotate(180f); scale(1.08f) } + transform { + rotate(180f) + scale(1.08f) + } opacity = 1f color = 0xFF6BCB77.toInt() } @@ -846,7 +920,7 @@ class ShowcaseWindow : DsglWindow() { .cascade-mixed > .header + .body .title { color: #F6D66F; } - """.trimIndent() + """.trimIndent(), ) StyleEngine.forceReloadStylesheets() } catch (ex: Exception) { @@ -950,6 +1024,3 @@ class ShowcaseWindow : DsglWindow() { return out.toString() } } - - - diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/containers/centeredFlexWrapper.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/containers/CenteredFlexWrapper.kt similarity index 99% rename from adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/containers/centeredFlexWrapper.kt rename to adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/containers/CenteredFlexWrapper.kt index dc587ec..a94918e 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/containers/centeredFlexWrapper.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/containers/CenteredFlexWrapper.kt @@ -20,4 +20,4 @@ internal fun UiScope.centeredFlexWrapper(direction: FlexDirection = FlexDirectio } }) { content() - } \ No newline at end of file + } diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/cookbook/ContextMenuWindow.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/cookbook/ContextMenuWindow.kt index 0d58d16..fcefd81 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/cookbook/ContextMenuWindow.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/cookbook/ContextMenuWindow.kt @@ -11,11 +11,12 @@ import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.mcForge1710.demo.examples.containers.centeredFlexWrapper class ContextMenuWindow : DsglWindow() { - override fun render() = ui { - centeredFlexWrapper { - contextMenuRecipe() + override fun render() = + ui { + centeredFlexWrapper { + contextMenuRecipe() + } } - } } fun UiScope.contextMenuRecipe() { @@ -33,7 +34,7 @@ fun UiScope.contextMenuRecipe() { item("Rename") { onClick { lastAction = "rename" } } separator() item("Delete") { onClick { lastAction = "delete" } } - } + }, ) } diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/cookbook/DragNDropWindow.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/cookbook/DragNDropWindow.kt index fcb2ea4..01c42c6 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/cookbook/DragNDropWindow.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/cookbook/DragNDropWindow.kt @@ -18,31 +18,35 @@ import org.dreamfinity.dsgl.mcForge1710.McItemStackRef import org.dreamfinity.dsgl.mcForge1710.demo.examples.containers.centeredFlexWrapper class DragNDropWindow : DsglWindow() { - override fun render() = ui { - centeredFlexWrapper { - dragBucketRecipe() + override fun render() = + ui { + centeredFlexWrapper { + dragBucketRecipe() + } } - } } - -private data class Card(val id: String, val label: String) +private data class Card( + val id: String, + val label: String, +) fun UiScope.dragBucketRecipe() { var lane by useState(listOf(Card("apple", "Apple"), Card("bread", "Bread"))) var done by useState(emptyList()) - val doneDrop = useDroppable( - id = "bucket.done", - nodeKey = "bucket.done", - accepts = { active -> !active.id.isNullOrBlank() }, - onDrop = { _, active -> - val movedId = active?.id ?: return@useDroppable - val moved = lane.firstOrNull { it.id == movedId } ?: return@useDroppable - lane = lane.filterNot { it.id == movedId } - done = done + moved - } - ) + val doneDrop = + useDroppable( + id = "bucket.done", + nodeKey = "bucket.done", + accepts = { active -> !active.id.isNullOrBlank() }, + onDrop = { _, active -> + val movedId = active?.id ?: return@useDroppable + val moved = lane.firstOrNull { it.id == movedId } ?: return@useDroppable + lane = lane.filterNot { it.id == movedId } + done = done + moved + }, + ) div({ key = "recipe.done.bucket" @@ -52,11 +56,21 @@ fun UiScope.dragBucketRecipe() { } applyDroppable(doneDrop) }) { - div({ style = { display = Display.Flex; alignItems = AlignItems.Center } }) { + div({ + style = { + display = Display.Flex + alignItems = AlignItems.Center + } + }) { text("Done (${done.size})") } done.forEach { card -> - div({ style = { display = Display.Flex; alignItems = AlignItems.Center } }) { + div({ + style = { + display = Display.Flex + alignItems = AlignItems.Center + } + }) { GameRegistry.findItem("minecraft", card.id)?.let { item -> itemStack(McItemStackRef(ItemStack(item, 1, 0)), { size = 32 }) } ?: text("?") @@ -82,4 +96,3 @@ fun UiScope.dragBucketRecipe() { } } } - diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/cookbook/ModalStackWindow.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/cookbook/ModalStackWindow.kt index 8398563..9b61e57 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/cookbook/ModalStackWindow.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/cookbook/ModalStackWindow.kt @@ -1,7 +1,12 @@ package org.dreamfinity.dsgl.mcForge1710.demo.examples.cookbook import org.dreamfinity.dsgl.core.DsglWindow -import org.dreamfinity.dsgl.core.components.modal.* +import org.dreamfinity.dsgl.core.components.modal.ModalSpec +import org.dreamfinity.dsgl.core.components.modal.modalBody +import org.dreamfinity.dsgl.core.components.modal.modalFooter +import org.dreamfinity.dsgl.core.components.modal.modalHeader +import org.dreamfinity.dsgl.core.components.modal.modalHost +import org.dreamfinity.dsgl.core.components.modal.modalTitle import org.dreamfinity.dsgl.core.dsl.UiScope import org.dreamfinity.dsgl.core.dsl.button import org.dreamfinity.dsgl.core.dsl.text @@ -9,11 +14,12 @@ import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.mcForge1710.demo.examples.containers.centeredFlexWrapper class ModalStackWindow : DsglWindow() { - override fun render() = ui { - centeredFlexWrapper { - modalStackRecipe() + override fun render() = + ui { + centeredFlexWrapper { + modalStackRecipe() + } } - } } private fun UiScope.modalStackRecipe() { @@ -26,23 +32,25 @@ private fun UiScope.modalStackRecipe() { modalHost(modals = modals, modalKey = "recipe.modal.host") { button("Open modal", { onMouseClick = { - modals += ModalSpec( - key = "recipe.modal.basic", - onHide = { removeModal("recipe.modal.basic") } - ) { scope -> - modalHeader(closeButton = true, onHide = scope.dismiss) { - modalTitle("Recipe modal") - } - modalBody { - text("Modal content") - button("Open another modal", { onMouseClick = { - - } }) - } - modalFooter { - button("Close", { onMouseClick = { scope.dismiss?.invoke() } }) + modals += + ModalSpec( + key = "recipe.modal.basic", + onHide = { removeModal("recipe.modal.basic") }, + ) { scope -> + modalHeader(closeButton = true, onHide = scope.dismiss) { + modalTitle("Recipe modal") + } + modalBody { + text("Modal content") + button("Open another modal", { + onMouseClick = { + } + }) + } + modalFooter { + button("Close", { onMouseClick = { scope.dismiss?.invoke() } }) + } } - } } }) } diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/cookbook/RefUsageWindow.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/cookbook/RefUsageWindow.kt index e0a3b0e..d6e48df 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/cookbook/RefUsageWindow.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/cookbook/RefUsageWindow.kt @@ -10,11 +10,12 @@ import org.dreamfinity.dsgl.core.hooks.ref.useRef import org.dreamfinity.dsgl.mcForge1710.demo.examples.containers.centeredFlexWrapper class RefUsageWindow : DsglWindow() { - override fun render() = ui { - centeredFlexWrapper { - focusRecipe() + override fun render() = + ui { + centeredFlexWrapper { + focusRecipe() + } } - } } fun UiScope.focusRecipe() { diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/quickstart/HelloWindow.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/quickstart/HelloWindow.kt index 38713b9..7293d5c 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/quickstart/HelloWindow.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/quickstart/HelloWindow.kt @@ -11,15 +11,16 @@ import org.dreamfinity.dsgl.mcForge1710.demo.examples.containers.centeredFlexWra class HelloWindow : DsglWindow() { private var clicks by state(0) - override fun render(): DomTree = ui { - centeredFlexWrapper { - helloDSGL(clicks, { clicks += 1 }) + override fun render(): DomTree = + ui { + centeredFlexWrapper { + helloDSGL(clicks, { clicks += 1 }) + } } - } } private fun UiScope.helloDSGL(clicks: Int, setClicks: (_: Event) -> Unit) { text("Hello DSGL") text("Clicks: $clicks") button("Click me #${clicks + 1}th time", { onMouseClick = setClicks }) -} \ No newline at end of file +} diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/quickstart/HelloWindowWithComponents.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/quickstart/HelloWindowWithComponents.kt index b89af77..8aaceac 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/quickstart/HelloWindowWithComponents.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/quickstart/HelloWindowWithComponents.kt @@ -11,12 +11,13 @@ import org.dreamfinity.dsgl.core.style.FlexDirection import org.dreamfinity.dsgl.mcForge1710.demo.examples.containers.centeredFlexWrapper class HelloWindowWithComponents : DsglWindow() { - override fun render() = ui { - centeredFlexWrapper(direction = FlexDirection.Row) { - counterCard("Left panel") - counterCard("Right panel") + override fun render() = + ui { + centeredFlexWrapper(direction = FlexDirection.Row) { + counterCard("Left panel") + counterCard("Right panel") + } } - } } private fun UiScope.counterCard(title: String) { diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/stateAndReactivity/GlobalStateWindow.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/stateAndReactivity/GlobalStateWindow.kt index 25c6fdb..eff7017 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/stateAndReactivity/GlobalStateWindow.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/stateAndReactivity/GlobalStateWindow.kt @@ -10,14 +10,15 @@ import org.dreamfinity.dsgl.mcForge1710.demo.examples.containers.centeredFlexWra class GlobalStateWindow : DsglWindow() { private var counter by state(0) - override fun render() = ui { - centeredFlexWrapper { - globalStateCounter(counter, { counter += 1 }) + override fun render() = + ui { + centeredFlexWrapper { + globalStateCounter(counter, { counter += 1 }) + } } - } } private fun UiScope.globalStateCounter(counter: Int, setCounter: (_: Event) -> Unit) { button("Increment", { onMouseClick = setCounter }) text("Counter: $counter") -} \ No newline at end of file +} diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/AnimationsSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/AnimationsSection.kt index 97c3d6b..34ee3e4 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/AnimationsSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/AnimationsSection.kt @@ -10,27 +10,30 @@ import org.dreamfinity.dsgl.core.style.FlexDirection import org.dreamfinity.dsgl.core.style.JustifyContent import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED -private val easingOptions: List> = listOf( - "linear" to Easings.LINEAR, - "ease" to Easings.EASE, - "ease-in" to Easings.EASE_IN, - "ease-out" to Easings.EASE_OUT, - "ease-in-out" to Easings.EASE_IN_OUT -) +private val easingOptions: List> = + listOf( + "linear" to Easings.LINEAR, + "ease" to Easings.EASE, + "ease-in" to Easings.EASE_IN, + "ease-out" to Easings.EASE_OUT, + "ease-in-out" to Easings.EASE_IN_OUT, + ) -private val directionOptions: List = listOf( - AnimationDirection.Normal, - AnimationDirection.Reverse, - AnimationDirection.Alternate, - AnimationDirection.AlternateReverse -) +private val directionOptions: List = + listOf( + AnimationDirection.Normal, + AnimationDirection.Reverse, + AnimationDirection.Alternate, + AnimationDirection.AlternateReverse, + ) -private val fillModeOptions: List = listOf( - AnimationFillMode.None, - AnimationFillMode.Forwards, - AnimationFillMode.Backwards, - AnimationFillMode.Both -) +private val fillModeOptions: List = + listOf( + AnimationFillMode.None, + AnimationFillMode.Forwards, + AnimationFillMode.Backwards, + AnimationFillMode.Both, + ) fun UiScope.animationsSection(onInfo: (String) -> Unit) { var animationsToggle by useState(false) @@ -47,15 +50,19 @@ fun UiScope.animationsSection(onInfo: (String) -> Unit) { var animationsBezierY2 by useState(67L) val duration = animationsDurationMs.toInt().coerceIn(200, 6000) - val customBezier = cubicBezier( - animationsBezierX1.toFloat() / 100f, - animationsBezierY1.toFloat() / 100f, - animationsBezierX2.toFloat() / 100f, - animationsBezierY2.toFloat() / 100f - ) - val dynamicEasingOptions = easingOptions + listOf( - "custom($animationsBezierX1,$animationsBezierY1,$animationsBezierX2,$animationsBezierY2)" to customBezier - ) + val customBezier = + cubicBezier( + animationsBezierX1.toFloat() / 100f, + animationsBezierY1.toFloat() / 100f, + animationsBezierX2.toFloat() / 100f, + animationsBezierY2.toFloat() / 100f, + ) + val dynamicEasingOptions = + easingOptions + + listOf( + "custom($animationsBezierX1,$animationsBezierY1,$animationsBezierX2,$animationsBezierY2)" to + customBezier, + ) val easingIndex = animationsEasingIndex.coerceIn(0, dynamicEasingOptions.lastIndex) val directionIndex = animationsDirectionIndex.coerceIn(0, directionOptions.lastIndex) val fillIndex = animationsFillModeIndex.coerceIn(0, fillModeOptions.lastIndex) @@ -77,7 +84,7 @@ fun UiScope.animationsSection(onInfo: (String) -> Unit) { text("Transforms + Transitions + Keyframes") text( "Transforms are layout-neutral; hit testing follows transformed geometry.", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) div({ @@ -94,7 +101,7 @@ fun UiScope.animationsSection(onInfo: (String) -> Unit) { animationsToggle = !animationsToggle onInfo("Animation retarget toggle=$animationsToggle") } - } + }, ) button(if (animationsPaused) "Play" else "Pause", { onMouseClick = { @@ -136,7 +143,7 @@ fun UiScope.animationsSection(onInfo: (String) -> Unit) { value = duration.toLong(), min = 200, max = 6000, - step = 50 + step = 50, ), { key = "animations.duration.slider" @@ -145,7 +152,7 @@ fun UiScope.animationsSection(onInfo: (String) -> Unit) { val next = (event.parsedValue as? Long) ?: event.value.toLongOrNull() ?: duration.toLong() animationsDurationMs = next.coerceIn(200, 6000) } - } + }, ) } @@ -173,7 +180,10 @@ fun UiScope.animationsSection(onInfo: (String) -> Unit) { flexDirection = FlexDirection.Row alignItems = AlignItems.Center justifyContent = JustifyContent.Start - border { width = 1.px; color = 0xFF3F4D5E.toInt() } + border { + width = 1.px + color = 0xFF3F4D5E.toInt() + } } }) { div({ @@ -199,10 +209,16 @@ fun UiScope.animationsSection(onInfo: (String) -> Unit) { scale(scale) rotate(if (animationsToggle) 8f else 0f) } - transformOrigin { x = 0.5f; y = 0.5f } + transformOrigin { + x = 0.5f + y = 0.5f + } opacity = if (animationsToggle) 0.65f else 1f foregroundColor = if (animationsToggle) 0xFFA4F0C2.toInt() else 0xFFEAF3FF.toInt() - border { width = 1.px; color = 0xFF56677A.toInt() } + border { + width = 1.px + color = 0xFF56677A.toInt() + } padding { all(4.px) } } }) { @@ -226,11 +242,17 @@ fun UiScope.animationsSection(onInfo: (String) -> Unit) { iterationCount = iterations, direction = direction, fillMode = fillMode, - playState = playState + playState = playState, ) } - transformOrigin { x = 0.5f; y = 0.5f } - border { width = 1.px; color = 0xFF5F5F72.toInt() } + transformOrigin { + x = 0.5f + y = 0.5f + } + border { + width = 1.px + color = 0xFF5F5F72.toInt() + } padding { all(4.px) } } }) { @@ -248,11 +270,17 @@ fun UiScope.animationsSection(onInfo: (String) -> Unit) { transform { rotate(if (animationsToggle) 12f else 0f) } - transformOrigin { x = 0.5f; y = 0.5f } + transformOrigin { + x = 0.5f + y = 0.5f + } transition { property(StyleAnimProps.transform, 260, easing = Easings.EASE_IN_OUT) } - border { width = 1.px; color = 0xFF4C6077.toInt() } + border { + width = 1.px + color = 0xFF4C6077.toInt() + } } }) { div({ @@ -264,13 +292,16 @@ fun UiScope.animationsSection(onInfo: (String) -> Unit) { transform { translate( if (animationsToggle) 10f else 0f, - if (animationsToggle) 4f else 0f + if (animationsToggle) 4f else 0f, ) } transition { property(StyleAnimProps.transform, 220, easing = Easings.EASE_OUT) } - border { width = 1.px; color = 0xFF7593B8.toInt() } + border { + width = 1.px + color = 0xFF7593B8.toInt() + } } }) { text("Nested", { style = { color = 0xFFEAF3FF.toInt() } }) @@ -280,16 +311,27 @@ fun UiScope.animationsSection(onInfo: (String) -> Unit) { text({ val debug = StyleAnimationEngine.debugSnapshotForKey("animations.keyframes.card") - val transitionDebug = debug?.activeTransitions?.joinToString(", ").orEmpty().ifBlank { "-" } - val keyframesDebug = debug?.activeKeyframes?.joinToString(", ").orEmpty().ifBlank { "-" } - val transformDebug = debug?.effectiveTransform?.let { - "tx=${it.translateX},ty=${it.translateY},sx=${it.scaleX},sy=${it.scaleY},rot=${it.rotateDeg}" - } ?: "-" + val transitionDebug = + debug + ?.activeTransitions + ?.joinToString(", ") + .orEmpty() + .ifBlank { "-" } + val keyframesDebug = + debug + ?.activeKeyframes + ?.joinToString(", ") + .orEmpty() + .ifBlank { "-" } + val transformDebug = + debug?.effectiveTransform?.let { + "tx=${it.translateX},ty=${it.translateY},sx=${it.scaleX},sy=${it.scaleY},rot=${it.rotateDeg}" + } ?: "-" "debug: hover=$animationsHover toggle=$animationsToggle " + - "easing=$easingName direction=${direction.name} fill=${fillMode.name} " + - "play=${playState.name} iterations=${if (animationsUseInfinite) "infinite" else "3"} " + - "activeTransitions=$transitionDebug activeKeyframes=$keyframesDebug " + - "effectiveOpacity=${debug?.effectiveOpacity ?: 1f} transform={$transformDebug}" + "easing=$easingName direction=${direction.name} fill=${fillMode.name} " + + "play=${playState.name} iterations=${if (animationsUseInfinite) "infinite" else "3"} " + + "activeTransitions=$transitionDebug activeKeyframes=$keyframesDebug " + + "effectiveOpacity=${debug?.effectiveOpacity ?: 1f} transform={$transformDebug}" style = { color = DEMO_MUTED } }) @@ -300,7 +342,7 @@ private fun UiScope.bezierSliderRow( label: String, key: String, value: Long, - onChange: (Long) -> Unit + onChange: (Long) -> Unit, ) { div({ style = { @@ -315,7 +357,7 @@ private fun UiScope.bezierSliderRow( value = value, min = 0, max = 100, - step = 1 + step = 1, ), { this.key = key @@ -324,7 +366,7 @@ private fun UiScope.bezierSliderRow( val next = (event.parsedValue as? Long) ?: event.value.toLongOrNull() ?: value onChange(next.coerceIn(0, 100)) } - } + }, ) } } diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ColorPickerSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ColorPickerSection.kt index df51561..fb36beb 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ColorPickerSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ColorPickerSection.kt @@ -1,13 +1,13 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections -import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.colorpicker.* import org.dreamfinity.dsgl.core.dom.layout.Rect -import org.dreamfinity.dsgl.core.style.Display -import org.dreamfinity.dsgl.core.style.FlexDirection +import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.hooks.useEffect import org.dreamfinity.dsgl.core.hooks.useMemo import org.dreamfinity.dsgl.core.hooks.useState +import org.dreamfinity.dsgl.core.style.Display +import org.dreamfinity.dsgl.core.style.FlexDirection import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED fun UiScope.colorPickerSection() { @@ -30,13 +30,14 @@ fun UiScope.colorPickerSection() { sharedColorPickerManager.open( anchorRect = Rect(mouseX, mouseY, 1, 1), title = "Shared Picker [$target]", - state = ColorPickerState( - color = current, - previous = current, - mode = colorInlineMode, - alphaEnabled = colorPickerAlphaEnabled, - closeOnSelect = false - ), + state = + ColorPickerState( + color = current, + previous = current, + mode = colorInlineMode, + alphaEnabled = colorPickerAlphaEnabled, + closeOnSelect = false, + ), closeOnOutsideClick = false, onPreview = { color -> if (target == "A") { @@ -59,7 +60,7 @@ fun UiScope.colorPickerSection() { colorSharedB = color } colorPickerLastCommit = colorLabel(color) - } + }, ) } @@ -74,11 +75,11 @@ fun UiScope.colorPickerSection() { text("Reusable color picker: inline + popup pane + shared popup manager") text( "Pipette samples current rendered game window surface. Copy/paste accepts hex/rgb/hsl/hsb.", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) text( "Inline picker follows app styling. Inspector picker (F12) is rendered in isolated system overlay styles.", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) div({ @@ -197,6 +198,5 @@ fun UiScope.colorPickerSection() { } } -private fun colorLabel(color: RgbaColor): String { - return ColorTextCodec.format(color, ColorFormatMode.HEX, includeAlpha = true) -} +private fun colorLabel(color: RgbaColor): String = + ColorTextCodec.format(color, ColorFormatMode.HEX, includeAlpha = true) diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ContextMenuSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ContextMenuSection.kt index 9c31171..f266bc2 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ContextMenuSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ContextMenuSection.kt @@ -1,20 +1,20 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections -import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.contextmenu.ContextMenuRuntime import org.dreamfinity.dsgl.core.contextmenu.ContextMenuStyle import org.dreamfinity.dsgl.core.contextmenu.contextMenu import org.dreamfinity.dsgl.core.dnd.* import org.dreamfinity.dsgl.core.dom.elements.InputType import org.dreamfinity.dsgl.core.dom.onContextMenu +import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.core.style.AlignItems import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection import org.dreamfinity.dsgl.core.style.JustifyContent -import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED import java.util.ArrayDeque @@ -33,12 +33,12 @@ private data class ContextMenuDemoFile( val sizeKb: Int, val isDirectory: Boolean, val locked: Boolean, - val updatedAtOrder: Long + val updatedAtOrder: Long, ) private data class ContextMenuBreadcrumb( val id: String, - val label: String + val label: String, ) private data class ContextMenuSectionState( @@ -67,18 +67,19 @@ private data class ContextMenuSectionState( val files: List = defaultContextMenuFiles(), val fileSequence: Long = 100L, val lastClickEntryId: String? = null, - val lastClickMs: Long = 0L + val lastClickMs: Long = 0L, ) fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { var state by useState(ContextMenuSectionState()) fun recordContextMenuAction(target: String, action: String) { - state = state.copy( - lastTarget = target, - lastAction = action, - actionCount = state.actionCount + 1 - ) + state = + state.copy( + lastTarget = target, + lastAction = action, + actionCount = state.actionCount + 1, + ) onInfo("Context menu [$target]: $action") } @@ -87,15 +88,16 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { mouseX: Int, mouseY: Int, localX: Int, - localY: Int + localY: Int, ) { - state = state.copy( - cursorOwner = owner, - cursorX = mouseX, - cursorY = mouseY, - cursorLocalX = localX, - cursorLocalY = localY - ) + state = + state.copy( + cursorOwner = owner, + cursorX = mouseX, + cursorY = mouseY, + cursorLocalX = localX, + cursorLocalY = localY, + ) } fun contextMenuEntryById(entryId: String?): ContextMenuDemoFile? { @@ -148,38 +150,41 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { backHistory = backHistory + currentState.currentDirectoryId forwardHistory = emptyList() } - state = currentState.copy( - currentDirectoryId = directory.id, - backHistory = backHistory, - forwardHistory = forwardHistory, - renameTargetId = null, - dragHoverDirectoryId = null - ) + state = + currentState.copy( + currentDirectoryId = directory.id, + backHistory = backHistory, + forwardHistory = forwardHistory, + renameTargetId = null, + dragHoverDirectoryId = null, + ) recordContextMenuAction("navigator", "open ${contextMenuCurrentPath()}") } fun contextMenuNavigateBack() { if (!contextMenuCanGoBack()) return val previous = state.backHistory.last() - state = state.copy( - backHistory = state.backHistory.dropLast(1), - forwardHistory = listOf(state.currentDirectoryId) + state.forwardHistory, - currentDirectoryId = previous, - renameTargetId = null, - dragHoverDirectoryId = null - ) + state = + state.copy( + backHistory = state.backHistory.dropLast(1), + forwardHistory = listOf(state.currentDirectoryId) + state.forwardHistory, + currentDirectoryId = previous, + renameTargetId = null, + dragHoverDirectoryId = null, + ) recordContextMenuAction("navigator", "back to ${contextMenuCurrentPath()}") } fun contextMenuNavigateForward() { val target = state.forwardHistory.firstOrNull() ?: return - state = state.copy( - forwardHistory = state.forwardHistory.drop(1), - backHistory = state.backHistory + state.currentDirectoryId, - currentDirectoryId = target, - renameTargetId = null, - dragHoverDirectoryId = null - ) + state = + state.copy( + forwardHistory = state.forwardHistory.drop(1), + backHistory = state.backHistory + state.currentDirectoryId, + currentDirectoryId = target, + renameTargetId = null, + dragHoverDirectoryId = null, + ) recordContextMenuAction("navigator", "forward to ${contextMenuCurrentPath()}") } @@ -188,28 +193,29 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { val parentId = current.parentId ?: return contextMenuOpenDirectory(parentId, pushHistory = true) } + fun contextMenuSetSortMode(mode: String) { state = state.copy(sortMode = mode) recordContextMenuAction("background", "sort by ${mode.lowercase()}") } - fun contextMenuVisibleFiles(): List { - return sortedChildrenForDirectory( + fun contextMenuVisibleFiles(): List = + sortedChildrenForDirectory( files = state.files, directoryId = state.currentDirectoryId, - sortMode = state.sortMode + sortMode = state.sortMode, ) - } fun contextMenuHandleEntryClick(file: ContextMenuDemoFile) { val now = System.currentTimeMillis() val isDoubleClick = state.lastClickEntryId == file.id && (now - state.lastClickMs) <= CONTEXT_MENU_DOUBLE_CLICK_MS - state = state.copy( - fileSelection = file.name, - lastClickEntryId = file.id, - lastClickMs = now - ) + state = + state.copy( + fileSelection = file.name, + lastClickEntryId = file.id, + lastClickMs = now, + ) if (file.isDirectory && isDoubleClick) { contextMenuOpenDirectory(file.id, pushHistory = true) } @@ -220,20 +226,22 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { val parent = currentState.files.firstOrNull { it.id == parentId && it.isDirectory } ?: return var sequence = currentState.fileSequence sequence += 1L - val created = ContextMenuDemoFile( - id = "fs.$sequence", - parentId = parent.id, - name = uniqueContextMenuName(currentState.files, parent.id, "New Folder"), - sizeKb = 0, - isDirectory = true, - locked = false, - updatedAtOrder = sequence - ) - state = currentState.copy( - files = currentState.files + created, - fileSelection = created.name, - fileSequence = sequence - ) + val created = + ContextMenuDemoFile( + id = "fs.$sequence", + parentId = parent.id, + name = uniqueContextMenuName(currentState.files, parent.id, "New Folder"), + sizeKb = 0, + isDirectory = true, + locked = false, + updatedAtOrder = sequence, + ) + state = + currentState.copy( + files = currentState.files + created, + fileSelection = created.name, + fileSequence = sequence, + ) recordContextMenuAction("background", "new folder ${created.name}") } @@ -243,34 +251,37 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { var updatedState = currentState if (parent.id != currentState.currentDirectoryId) { val backHistory = currentState.backHistory + currentState.currentDirectoryId - updatedState = currentState.copy( - currentDirectoryId = parent.id, - backHistory = backHistory, - forwardHistory = emptyList(), - renameTargetId = null, - dragHoverDirectoryId = null - ) + updatedState = + currentState.copy( + currentDirectoryId = parent.id, + backHistory = backHistory, + forwardHistory = emptyList(), + renameTargetId = null, + dragHoverDirectoryId = null, + ) } var sequence = updatedState.fileSequence sequence += 1L val name = uniqueContextMenuName(updatedState.files, parent.id, "new-file.txt") - val created = ContextMenuDemoFile( - id = "fs.$sequence", - parentId = parent.id, - name = name, - sizeKb = 1, - isDirectory = false, - locked = false, - updatedAtOrder = sequence - ) - state = updatedState.copy( - files = updatedState.files + created, - fileSelection = created.name, - renameTargetId = created.id, - renameDraft = created.name, - fileSequence = sequence - ) + val created = + ContextMenuDemoFile( + id = "fs.$sequence", + parentId = parent.id, + name = name, + sizeKb = 1, + isDirectory = false, + locked = false, + updatedAtOrder = sequence, + ) + state = + updatedState.copy( + files = updatedState.files + created, + fileSelection = created.name, + renameTargetId = created.id, + renameDraft = created.name, + fileSequence = sequence, + ) recordContextMenuAction("background", "new file $name") } @@ -285,11 +296,12 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { fun contextMenuBeginRename(file: ContextMenuDemoFile) { if (file.locked) return - state = state.copy( - renameTargetId = file.id, - renameDraft = file.name, - fileSelection = file.name - ) + state = + state.copy( + renameTargetId = file.id, + renameDraft = file.name, + fileSelection = file.name, + ) } fun contextMenuCancelRename() { @@ -298,10 +310,11 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { fun contextMenuApplyRename() { val targetId = state.renameTargetId ?: return - val target = contextMenuEntryById(targetId) ?: run { - state = state.copy(renameTargetId = null) - return - } + val target = + contextMenuEntryById(targetId) ?: run { + state = state.copy(renameTargetId = null) + return + } if (target.locked) { state = state.copy(renameTargetId = null) return @@ -309,42 +322,46 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { val draft = state.renameDraft.trim() if (draft.isEmpty()) return - val resolved = if (draft == target.name) { - draft - } else { - uniqueContextMenuName(state.files, target.parentId ?: CONTEXT_MENU_ROOT_ID, draft) - } + val resolved = + if (draft == target.name) { + draft + } else { + uniqueContextMenuName(state.files, target.parentId ?: CONTEXT_MENU_ROOT_ID, draft) + } val sequence = state.fileSequence + 1L - state = state.copy( - files = state.files.map { current -> - if (current.id == target.id) { - current.copy(name = resolved, updatedAtOrder = sequence) - } else { - current - } - }, - fileSelection = resolved, - renameTargetId = null, - renameDraft = "", - fileSequence = sequence - ) + state = + state.copy( + files = + state.files.map { current -> + if (current.id == target.id) { + current.copy(name = resolved, updatedAtOrder = sequence) + } else { + current + } + }, + fileSelection = resolved, + renameTargetId = null, + renameDraft = "", + fileSequence = sequence, + ) recordContextMenuAction(target.name, "rename to $resolved") } fun contextMenuCopyFile(file: ContextMenuDemoFile) { - state = state.copy( - clipboardHasData = true, - clipboardEntryName = file.name, - clipboardEntryId = file.id, - fileSelection = file.name - ) + state = + state.copy( + clipboardHasData = true, + clipboardEntryName = file.name, + clipboardEntryId = file.id, + fileSelection = file.name, + ) recordContextMenuAction(file.name, "copied to clipboard") } fun contextMenuDuplicateFile( file: ContextMenuDemoFile, - targetParentId: String = file.parentId ?: state.currentDirectoryId + targetParentId: String = file.parentId ?: state.currentDirectoryId, ) { val currentState = state val targetParent = currentState.files.firstOrNull { it.id == targetParentId && it.isDirectory } ?: return @@ -354,6 +371,7 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { val idRemap = linkedMapOf() var sequence = currentState.fileSequence + fun nextId(): String { sequence += 1L return "fs.$sequence" @@ -368,32 +386,36 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { idRemap[file.id] = rootCopyId val rootName = uniqueContextMenuName(currentState.files, targetParent.id, file.name, "copy") val copies = ArrayList(descendants.size) - copies += file.copy( - id = rootCopyId, - parentId = targetParent.id, - name = rootName, - locked = false, - updatedAtOrder = nextOrder() - ) + copies += + file.copy( + id = rootCopyId, + parentId = targetParent.id, + name = rootName, + locked = false, + updatedAtOrder = nextOrder(), + ) descendants.drop(1).forEach { child -> val parentCopyId = idRemap[child.parentId] ?: return@forEach val childCopyId = nextId() idRemap[child.id] = childCopyId - copies += child.copy( - id = childCopyId, - parentId = parentCopyId, - locked = false, - updatedAtOrder = nextOrder() - ) + copies += + child.copy( + id = childCopyId, + parentId = parentCopyId, + locked = false, + updatedAtOrder = nextOrder(), + ) } - state = currentState.copy( - files = currentState.files + copies, - fileSelection = rootName, - fileSequence = sequence - ) + state = + currentState.copy( + files = currentState.files + copies, + fileSelection = rootName, + fileSequence = sequence, + ) recordContextMenuAction(file.name, "duplicate as $rootName") } + fun contextMenuCanDropIntoDirectory(entryId: String, destinationDirectoryId: String): Boolean { val entry = contextMenuEntryById(entryId) ?: return false val destination = contextMenuEntryById(destinationDirectoryId) ?: return false @@ -409,22 +431,24 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { val destination = contextMenuEntryById(destinationDirectoryId) ?: return val resolvedName = uniqueContextMenuName(state.files, destination.id, file.name) val sequence = state.fileSequence + 1L - state = state.copy( - files = state.files.map { current -> - if (current.id == file.id) { - current.copy( - parentId = destination.id, - name = resolvedName, - updatedAtOrder = sequence - ) - } else { - current - } - }, - fileSelection = resolvedName, - dragHoverDirectoryId = null, - fileSequence = sequence - ) + state = + state.copy( + files = + state.files.map { current -> + if (current.id == file.id) { + current.copy( + parentId = destination.id, + name = resolvedName, + updatedAtOrder = sequence, + ) + } else { + current + } + }, + fileSelection = resolvedName, + dragHoverDirectoryId = null, + fileSequence = sequence, + ) recordContextMenuAction(file.name, "move to ${destination.name}") } @@ -453,29 +477,31 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { val nextFiles = state.files.filterNot { subtreeIds.contains(it.id) } val nextRenameTarget = state.renameTargetId?.takeUnless { subtreeIds.contains(it) } var nextSelection = state.fileSelection - if (nextSelection != "none" && sortedChildrenForDirectory( + if (nextSelection != "none" && + sortedChildrenForDirectory( files = nextFiles, directoryId = nextCurrentDirectory, - sortMode = state.sortMode + sortMode = state.sortMode, ).none { it.name == nextSelection } ) { nextSelection = sortedChildrenForDirectory( files = nextFiles, directoryId = nextCurrentDirectory, - sortMode = state.sortMode + sortMode = state.sortMode, ).firstOrNull()?.name ?: "none" } - state = state.copy( - files = nextFiles, - currentDirectoryId = nextCurrentDirectory, - backHistory = nextBackHistory, - forwardHistory = nextForwardHistory, - clipboardHasData = nextClipboardHasData, - clipboardEntryId = nextClipboardEntryId, - renameTargetId = nextRenameTarget, - fileSelection = nextSelection - ) + state = + state.copy( + files = nextFiles, + currentDirectoryId = nextCurrentDirectory, + backHistory = nextBackHistory, + forwardHistory = nextForwardHistory, + clipboardHasData = nextClipboardHasData, + clipboardEntryId = nextClipboardEntryId, + renameTargetId = nextRenameTarget, + fileSelection = nextSelection, + ) recordContextMenuAction(file.name, "delete") } @@ -488,218 +514,230 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { } fun contextMenuRefreshWorkspace() { - state = state.copy( - files = state.files.map { file -> - file.copy(updatedAtOrder = file.updatedAtOrder + 1L) - } - ) + state = + state.copy( + files = + state.files.map { file -> + file.copy(updatedAtOrder = file.updatedAtOrder + 1L) + }, + ) recordContextMenuAction("background", "refresh") } - fun buildBackgroundMenu() = contextMenu(id = "demo.context.background") { - submenu("Create", id = "create") { - icon("+") - item("File", id = "create.file") { - icon("FI") - onClick { contextMenuCreateFile() } - } - item("Directory", id = "create.dir") { - icon("FD") - onClick { contextMenuCreateFolder() } + fun buildBackgroundMenu() = + contextMenu(id = "demo.context.background") { + submenu("Create", id = "create") { + icon("+") + item("File", id = "create.file") { + icon("FI") + onClick { contextMenuCreateFile() } + } + item("Directory", id = "create.dir") { + icon("FD") + onClick { contextMenuCreateFolder() } + } } - } - - item("Paste", id = "paste") { - icon("CL") - hint("Ctrl+V") - enabledIf { state.clipboardHasData } - onClick { contextMenuPasteIntoWorkspace() } - } - submenu("Sort by", id = "sort") { - icon("AZ") - item("Name", id = "sort.name") { - checkedIf { state.sortMode == "Name" } - onClick { contextMenuSetSortMode("Name") } + item("Paste", id = "paste") { + icon("CL") + hint("Ctrl+V") + enabledIf { state.clipboardHasData } + onClick { contextMenuPasteIntoWorkspace() } } - item("Date", id = "sort.date") { - checkedIf { state.sortMode == "Date" } - onClick { contextMenuSetSortMode("Date") } - } - item("Size", id = "sort.size") { - checkedIf { state.sortMode == "Size" } - onClick { contextMenuSetSortMode("Size") } + + submenu("Sort by", id = "sort") { + icon("AZ") + item("Name", id = "sort.name") { + checkedIf { state.sortMode == "Name" } + onClick { contextMenuSetSortMode("Name") } + } + item("Date", id = "sort.date") { + checkedIf { state.sortMode == "Date" } + onClick { contextMenuSetSortMode("Date") } + } + item("Size", id = "sort.size") { + checkedIf { state.sortMode == "Size" } + onClick { contextMenuSetSortMode("Size") } + } } - } - separator("main.sep") + separator("main.sep") - item("Refresh", id = "refresh") { - icon("RF") - onClick { contextMenuRefreshWorkspace() } + item("Refresh", id = "refresh") { + icon("RF") + onClick { contextMenuRefreshWorkspace() } + } } - } - fun buildEntryMenu(file: ContextMenuDemoFile) = contextMenu(id = "demo.context.entry.${file.id}") { - if (file.isDirectory) { - item("Open", id = "entry.open") { - icon("OP") - onClick { contextMenuOpenDirectory(file.id, pushHistory = true) } - } - submenu("Create Inside", id = "entry.createInside") { - icon("+") - item("File", id = "entry.createInside.file") { - icon("FI") - onClick { contextMenuCreateFile(file.id) } + fun buildEntryMenu(file: ContextMenuDemoFile) = + contextMenu(id = "demo.context.entry.${file.id}") { + if (file.isDirectory) { + item("Open", id = "entry.open") { + icon("OP") + onClick { contextMenuOpenDirectory(file.id, pushHistory = true) } } - item("Directory", id = "entry.createInside.dir") { - icon("FD") - onClick { contextMenuCreateFolder(file.id) } + submenu("Create Inside", id = "entry.createInside") { + icon("+") + item("File", id = "entry.createInside.file") { + icon("FI") + onClick { contextMenuCreateFile(file.id) } + } + item("Directory", id = "entry.createInside.dir") { + icon("FD") + onClick { contextMenuCreateFolder(file.id) } + } } + separator("entry.sep.open") } - separator("entry.sep.open") - } - item("Duplicate", id = "entry.duplicate") { - icon("CP") - onClick { contextMenuDuplicateFile(file) } - } + item("Duplicate", id = "entry.duplicate") { + icon("CP") + onClick { contextMenuDuplicateFile(file) } + } - item("Rename", id = "entry.rename") { - icon("RN") - enabledIf { !file.locked } - onClick { contextMenuBeginRename(file) } - } + item("Rename", id = "entry.rename") { + icon("RN") + enabledIf { !file.locked } + onClick { contextMenuBeginRename(file) } + } - item("Delete", id = "entry.delete") { - icon("DL") - enabledIf { !file.locked } - onClick { contextMenuDeleteFile(file) } - } + item("Delete", id = "entry.delete") { + icon("DL") + enabledIf { !file.locked } + onClick { contextMenuDeleteFile(file) } + } - separator("entry.sep.copy") + separator("entry.sep.copy") - item("Copy", id = "entry.copy") { - icon("CY") - onClick { contextMenuCopyFile(file) } + item("Copy", id = "entry.copy") { + icon("CY") + onClick { contextMenuCopyFile(file) } + } } - } + fun UiScope.contextMenuEntryTile(file: ContextMenuDemoFile) { val tileKey = "context.fs.tile.${file.id}" val iconURL = iconFor(file) - val draggable = useDraggable( - id = file.id, - nodeKey = tileKey, - type = "context.fs.entry", - data = file.id, - previewMode = DragPreviewMode.GHOST, - hideSourceWhileDragging = false, - renderPreview = { - val offset = TILE_GHOST_SIZE / 2 - image(iconURL, -offset, -offset, TILE_GHOST_SIZE, TILE_GHOST_SIZE) - rect(-offset, -offset, TILE_GHOST_SIZE, TILE_GHOST_SIZE, 0x66000000) - }, - onDragStart = { event -> - event.dataTransfer.setDragImage(tileKey, 0, 0) - } - ) - val droppable = if (file.isDirectory) { - useDroppable( - id = "context.fs.dir.${file.id}", + val draggable = + useDraggable( + id = file.id, nodeKey = tileKey, - accepts = { active -> - val activeId = active.id ?: return@useDroppable false - contextMenuCanDropIntoDirectory(activeId, file.id) - }, - onDragEnter = { event, active -> - val activeId = active?.id - if (activeId != null && contextMenuCanDropIntoDirectory(activeId, file.id)) { - state = state.copy(dragHoverDirectoryId = file.id) - event.cancelled = true - } - }, - onDragOver = { event, active -> - val activeId = active?.id - if (activeId != null && contextMenuCanDropIntoDirectory(activeId, file.id)) { - state = state.copy(dragHoverDirectoryId = file.id) - event.cancelled = true - } + type = "context.fs.entry", + data = file.id, + previewMode = DragPreviewMode.GHOST, + hideSourceWhileDragging = false, + renderPreview = { + val offset = TILE_GHOST_SIZE / 2 + image(iconURL, -offset, -offset, TILE_GHOST_SIZE, TILE_GHOST_SIZE) + rect(-offset, -offset, TILE_GHOST_SIZE, TILE_GHOST_SIZE, 0x66000000) }, - onDragLeave = { event, _ -> - if (state.dragHoverDirectoryId == file.id) { - state = state.copy(dragHoverDirectoryId = null) - } - event.cancelled = true + onDragStart = { event -> + event.dataTransfer.setDragImage(tileKey, 0, 0) }, - onDrop = { event, active -> - val moving = contextMenuEntryById(active?.id) ?: return@useDroppable - contextMenuMoveFile(moving, file.id) - event.cancelled = true - } ) - } else { - null - } + val droppable = + if (file.isDirectory) { + useDroppable( + id = "context.fs.dir.${file.id}", + nodeKey = tileKey, + accepts = { active -> + val activeId = active.id ?: return@useDroppable false + contextMenuCanDropIntoDirectory(activeId, file.id) + }, + onDragEnter = { event, active -> + val activeId = active?.id + if (activeId != null && contextMenuCanDropIntoDirectory(activeId, file.id)) { + state = state.copy(dragHoverDirectoryId = file.id) + event.cancelled = true + } + }, + onDragOver = { event, active -> + val activeId = active?.id + if (activeId != null && contextMenuCanDropIntoDirectory(activeId, file.id)) { + state = state.copy(dragHoverDirectoryId = file.id) + event.cancelled = true + } + }, + onDragLeave = { event, _ -> + if (state.dragHoverDirectoryId == file.id) { + state = state.copy(dragHoverDirectoryId = null) + } + event.cancelled = true + }, + onDrop = { event, active -> + val moving = contextMenuEntryById(active?.id) ?: return@useDroppable + contextMenuMoveFile(moving, file.id) + event.cancelled = true + }, + ) + } else { + null + } val isEditingName = state.renameTargetId == file.id val isSelected = state.fileSelection == file.name val isDropHover = state.dragHoverDirectoryId == file.id - val tileNode = div({ - key = tileKey - onMouseClick = { event -> - if (!isEditingName && event.mouseButton == MouseButton.LEFT) { - contextMenuHandleEntryClick(file) - } - } - style = { - width = TILE_WIDTH.px - minHeight = (TILE_WIDTH + 18).px - backgroundColor = when { - isDropHover -> 0xFF43607A.toInt() - isSelected -> 0xFF3A5168.toInt() - else -> 0xFF33414E.toInt() + val tileNode = + div({ + key = tileKey + onMouseClick = { event -> + if (!isEditingName && event.mouseButton == MouseButton.LEFT) { + contextMenuHandleEntryClick(file) + } } - border { width = 1.px; color = if (isDropHover) 0xFF9BC2E9.toInt() else 0xFF596B7D.toInt() } - display = Display.Flex - flexDirection = FlexDirection.Column - alignItems = AlignItems.Center - justifyContent = JustifyContent.Center - } - applyDraggable(draggable) - if (droppable != null) { - applyDroppable(droppable) - } - }) { - img(iconURL, { style = { - width = TILE_ICON_SIZE.px - height = TILE_ICON_SIZE.px - } - }) - if (isEditingName) { - input( - InputType.Text( - value = state.renameDraft, - placeholder = "Name" - ), - { - key = "contextMenu.rename.inline.${file.id}" - onInput = { event -> - state = state.copy(renameDraft = event.value) - } - onKeyDown = { event -> - when (event.keyCode) { - KeyCodes.ENTER -> contextMenuApplyRename() - KeyCodes.ESCAPE -> contextMenuCancelRename() - } + width = TILE_WIDTH.px + minHeight = (TILE_WIDTH + 18).px + backgroundColor = + when { + isDropHover -> 0xFF43607A.toInt() + isSelected -> 0xFF3A5168.toInt() + else -> 0xFF33414E.toInt() } + border { + width = 1.px + color = if (isDropHover) 0xFF9BC2E9.toInt() else 0xFF596B7D.toInt() + } + display = Display.Flex + flexDirection = FlexDirection.Column + alignItems = AlignItems.Center + justifyContent = JustifyContent.Center + } + applyDraggable(draggable) + if (droppable != null) { + applyDroppable(droppable) + } + }) { + img(iconURL, { + style = { + width = TILE_ICON_SIZE.px + height = TILE_ICON_SIZE.px } - ) - } else { - text(file.name, { - style = { color = if (file.locked) 0xFFE9A56E.toInt() else 0xFFEAF2FD.toInt() } }) + if (isEditingName) { + input( + InputType.Text( + value = state.renameDraft, + placeholder = "Name", + ), + { + key = "contextMenu.rename.inline.${file.id}" + onInput = { event -> + state = state.copy(renameDraft = event.value) + } + onKeyDown = { event -> + when (event.keyCode) { + KeyCodes.ENTER -> contextMenuApplyRename() + KeyCodes.ESCAPE -> contextMenuCancelRename() + } + } + }, + ) + } else { + text(file.name, { + style = { color = if (file.locked) 0xFFE9A56E.toInt() else 0xFFEAF2FD.toInt() } + }) + } } - } tileNode.onContextMenu { val anchorX = anchorRect?.x ?: mouseX val anchorY = anchorRect?.y ?: mouseY @@ -708,7 +746,7 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { mouseX = mouseX, mouseY = mouseY, localX = mouseX - anchorX, - localY = mouseY - anchorY + localY = mouseY - anchorY, ) openMenu(buildEntryMenu(file)) } @@ -734,8 +772,8 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { hintTextColor = 0xFFC5D2E1.toInt(), separatorColor = 0xFF4C6074.toInt(), checkMarkColor = 0xFF8BD59D.toInt(), - submenuArrowColor = 0xFFC9D7E6.toInt() - ) + submenuArrowColor = 0xFFC9D7E6.toInt(), + ), ) div({ @@ -749,11 +787,11 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { text("Pseudo filesystem: tile view + context menu + drag/drop") text( "path=${contextMenuCurrentPath()} sort=${state.sortMode} selected=${state.fileSelection}", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) text( "lastAction=${state.lastAction} target=${state.lastTarget} actions=${state.actionCount}", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) div({ @@ -785,7 +823,10 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { padding = 3.px gap = 2.px backgroundColor = 0xFF2A313B.toInt() - border { width = 1.px; color = 0xFF5B6A7A.toInt() } + border { + width = 1.px + color = 0xFF5B6A7A.toInt() + } } }) { div({ @@ -796,7 +837,10 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { backgroundColor = 0xFF25303A.toInt() display = Display.Flex flexDirection = FlexDirection.Row - border { width = 1.px; color = 0xFF4F6175.toInt() } + border { + width = 1.px + color = 0xFF4F6175.toInt() + } } }) { button("<", { @@ -815,43 +859,44 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { text("/", { style = { color = DEMO_MUTED } }) } val breadcrumbKey = "section.contextMenu.path.${breadcrumb.id}" - val breadcrumbDrop = useDroppable( - id = "context.fs.path.${breadcrumb.id}", - nodeKey = breadcrumbKey, - accepts = { active -> - val activeId = active.id ?: return@useDroppable false - contextMenuCanDropIntoDirectory(activeId, breadcrumb.id) - }, - onDragEnter = { event, active -> - if (event.target?.key != breadcrumbKey) return@useDroppable - val activeId = active?.id ?: return@useDroppable - if (contextMenuCanDropIntoDirectory(activeId, breadcrumb.id)) { - state = state.copy(dragHoverDirectoryId = breadcrumb.id) + val breadcrumbDrop = + useDroppable( + id = "context.fs.path.${breadcrumb.id}", + nodeKey = breadcrumbKey, + accepts = { active -> + val activeId = active.id ?: return@useDroppable false + contextMenuCanDropIntoDirectory(activeId, breadcrumb.id) + }, + onDragEnter = { event, active -> + if (event.target?.key != breadcrumbKey) return@useDroppable + val activeId = active?.id ?: return@useDroppable + if (contextMenuCanDropIntoDirectory(activeId, breadcrumb.id)) { + state = state.copy(dragHoverDirectoryId = breadcrumb.id) + event.cancelled = true + } + }, + onDragOver = { event, active -> + if (event.target?.key != breadcrumbKey) return@useDroppable + val activeId = active?.id ?: return@useDroppable + if (contextMenuCanDropIntoDirectory(activeId, breadcrumb.id)) { + state = state.copy(dragHoverDirectoryId = breadcrumb.id) + event.cancelled = true + } + }, + onDragLeave = { event, _ -> + if (event.target?.key != breadcrumbKey) return@useDroppable + if (state.dragHoverDirectoryId == breadcrumb.id) { + state = state.copy(dragHoverDirectoryId = null) + } event.cancelled = true - } - }, - onDragOver = { event, active -> - if (event.target?.key != breadcrumbKey) return@useDroppable - val activeId = active?.id ?: return@useDroppable - if (contextMenuCanDropIntoDirectory(activeId, breadcrumb.id)) { - state = state.copy(dragHoverDirectoryId = breadcrumb.id) + }, + onDrop = { event, active -> + if (event.target?.key != breadcrumbKey) return@useDroppable + val moving = contextMenuEntryById(active?.id) ?: return@useDroppable + contextMenuMoveFile(moving, breadcrumb.id) event.cancelled = true - } - }, - onDragLeave = { event, _ -> - if (event.target?.key != breadcrumbKey) return@useDroppable - if (state.dragHoverDirectoryId == breadcrumb.id) { - state = state.copy(dragHoverDirectoryId = null) - } - event.cancelled = true - }, - onDrop = { event, active -> - if (event.target?.key != breadcrumbKey) return@useDroppable - val moving = contextMenuEntryById(active?.id) ?: return@useDroppable - contextMenuMoveFile(moving, breadcrumb.id) - event.cancelled = true - } - ) + }, + ) val isCurrent = breadcrumb.id == state.currentDirectoryId val isDropHover = state.dragHoverDirectoryId == breadcrumb.id button(breadcrumb.label, { @@ -860,75 +905,94 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { contextMenuOpenDirectory(breadcrumb.id, pushHistory = true) } style = { - backgroundColor = when { - isDropHover -> 0xFF40617F.toInt() - isCurrent -> 0xFF364A5E.toInt() - else -> 0xFF2B3A4A.toInt() + backgroundColor = + when { + isDropHover -> 0xFF40617F.toInt() + isCurrent -> 0xFF364A5E.toInt() + else -> 0xFF2B3A4A.toInt() + } + border { + width = 1.px + color = if (isDropHover) 0xFF9BC2E9.toInt() else 0xFF5B6F84.toInt() } - border { width = 1.px; color = if (isDropHover) 0xFF9BC2E9.toInt() else 0xFF5B6F84.toInt() } } applyDroppable(breadcrumbDrop) }) } } - val listDroppable = useDroppable( - id = "context.fs.current.${state.currentDirectoryId}", - nodeKey = "section.contextMenu.list", - accepts = { active -> - val activeId = active.id ?: return@useDroppable false - contextMenuCanDropIntoDirectory(activeId, state.currentDirectoryId) - }, - onDragEnter = { event, active -> - if (event.target?.key != "section.contextMenu.list") return@useDroppable - val activeId = active?.id - if (activeId != null && contextMenuCanDropIntoDirectory(activeId, state.currentDirectoryId)) { - state = state.copy(dragHoverDirectoryId = state.currentDirectoryId) - } - }, - onDragOver = { event, active -> - if (event.target?.key != "section.contextMenu.list") return@useDroppable - val activeId = active?.id - if (activeId != null && contextMenuCanDropIntoDirectory(activeId, state.currentDirectoryId)) { - state = state.copy(dragHoverDirectoryId = state.currentDirectoryId) - } - }, - onDragLeave = { event, _ -> - if (event.target?.key != "section.contextMenu.list") return@useDroppable - if (state.dragHoverDirectoryId == state.currentDirectoryId) { - state = state.copy(dragHoverDirectoryId = null) - } - }, - onDrop = { event, active -> - if (event.target?.key != "section.contextMenu.list") return@useDroppable - val moving = contextMenuEntryById(active?.id) ?: return@useDroppable - contextMenuMoveFile(moving, state.currentDirectoryId) - } - ) + val listDroppable = + useDroppable( + id = "context.fs.current.${state.currentDirectoryId}", + nodeKey = "section.contextMenu.list", + accepts = { active -> + val activeId = active.id ?: return@useDroppable false + contextMenuCanDropIntoDirectory(activeId, state.currentDirectoryId) + }, + onDragEnter = { event, active -> + if (event.target?.key != "section.contextMenu.list") return@useDroppable + val activeId = active?.id + if (activeId != null && contextMenuCanDropIntoDirectory(activeId, state.currentDirectoryId)) { + state = state.copy(dragHoverDirectoryId = state.currentDirectoryId) + } + }, + onDragOver = { event, active -> + if (event.target?.key != "section.contextMenu.list") return@useDroppable + val activeId = active?.id + if (activeId != null && contextMenuCanDropIntoDirectory(activeId, state.currentDirectoryId)) { + state = state.copy(dragHoverDirectoryId = state.currentDirectoryId) + } + }, + onDragLeave = { event, _ -> + if (event.target?.key != "section.contextMenu.list") return@useDroppable + if (state.dragHoverDirectoryId == state.currentDirectoryId) { + state = state.copy(dragHoverDirectoryId = null) + } + }, + onDrop = { event, active -> + if (event.target?.key != "section.contextMenu.list") return@useDroppable + val moving = contextMenuEntryById(active?.id) ?: return@useDroppable + contextMenuMoveFile(moving, state.currentDirectoryId) + }, + ) - val listNode = div({ - key = "section.contextMenu.list" - style = { - gap = 4.px - padding = 4.px - backgroundColor = if (state.dragHoverDirectoryId == state.currentDirectoryId) 0xFF2F4358.toInt() else 0xFF2B343F.toInt() - border { width = 1.px; color = 0xFF4F6175.toInt() } - display = Display.Grid - gridColumns = 4 - flexGrow = 1f - } - applyDroppable(listDroppable) - }) { - if (entries.isEmpty()) { - div({ style = { padding = 2.px } }) { - text("Folder is empty. Right-click to create file/folder.", { style = { color = DEMO_MUTED } }) + val listNode = + div({ + key = "section.contextMenu.list" + style = { + gap = 4.px + padding = 4.px + backgroundColor = + if (state.dragHoverDirectoryId == + state.currentDirectoryId + ) { + 0xFF2F4358.toInt() + } else { + 0xFF2B343F.toInt() + } + border { + width = 1.px + color = 0xFF4F6175.toInt() + } + display = Display.Grid + gridColumns = 4 + flexGrow = 1f } - } else { - entries.forEach { file -> - contextMenuEntryTile(file) + applyDroppable(listDroppable) + }) { + if (entries.isEmpty()) { + div({ style = { padding = 2.px } }) { + text( + "Folder is empty. Right-click to create file/folder.", + { style = { color = DEMO_MUTED } }, + ) + } + } else { + entries.forEach { file -> + contextMenuEntryTile(file) + } } } - } listNode.onContextMenu { val anchorX = anchorRect?.x ?: mouseX val anchorY = anchorRect?.y ?: mouseY @@ -937,7 +1001,7 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { mouseX = mouseX, mouseY = mouseY, localX = mouseX - anchorX, - localY = mouseY - anchorY + localY = mouseY - anchorY, ) openMenu(buildBackgroundMenu()) } @@ -945,32 +1009,31 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { } } -private fun iconFor(file: ContextMenuDemoFile): String { - return if (file.isDirectory) ICON_FOLDER else ICON_DOCUMENT -} +private fun iconFor(file: ContextMenuDemoFile): String = if (file.isDirectory) ICON_FOLDER else ICON_DOCUMENT private fun uniqueContextMenuName( files: List, parentId: String, baseName: String, - variant: String = "new" + variant: String = "new", ): String { - val existing = files - .asSequence() - .filter { it.parentId == parentId } - .map { it.name } - .toHashSet() + val existing = + files + .asSequence() + .filter { it.parentId == parentId } + .map { it.name } + .toHashSet() if (!existing.contains(baseName)) { return baseName } val (stem, extension) = splitContextMenuName(baseName) - fun candidate(index: Int): String { - return when (variant) { + + fun candidate(index: Int): String = + when (variant) { "copy" -> if (index == 1) "$stem copy$extension" else "$stem copy $index$extension" "renamed" -> if (index == 1) "$stem (renamed)$extension" else "$stem (renamed $index)$extension" else -> "$stem $index$extension" } - } var index = 1 var next = candidate(index) @@ -989,24 +1052,23 @@ private fun splitContextMenuName(name: String): Pair { name to "" } } + private fun sortedChildrenForDirectory( files: List, directoryId: String, - sortMode: String + sortMode: String, ): List { val children = files.filter { it.parentId == directoryId } - val comparator = when (sortMode) { - "Date" -> compareByDescending { it.updatedAtOrder }.thenBy { it.name.lowercase() } - "Size" -> compareByDescending { it.sizeKb }.thenBy { it.name.lowercase() } - else -> compareBy { it.name.lowercase() } - } + val comparator = + when (sortMode) { + "Date" -> compareByDescending { it.updatedAtOrder }.thenBy { it.name.lowercase() } + "Size" -> compareByDescending { it.sizeKb }.thenBy { it.name.lowercase() } + else -> compareBy { it.name.lowercase() } + } return children.sortedWith(compareBy { !it.isDirectory }.then(comparator)) } -private fun collectSubtree( - rootId: String, - byId: Map -): List { +private fun collectSubtree(rootId: String, byId: Map): List { val root = byId[rootId] ?: return emptyList() val queue = ArrayDeque() val orderedIds = ArrayList() @@ -1022,15 +1084,15 @@ private fun collectSubtree( return orderedIds.mapNotNull { byId[it] } } -private fun collectSubtreeIds(files: List, rootId: String): Set { - return collectSubtree(rootId, files.associateBy { it.id }).map { it.id }.toSet() -} +private fun collectSubtreeIds(files: List, rootId: String): Set = + collectSubtree( + rootId, + files.associateBy { + it.id + }, + ).map { it.id }.toSet() -private fun isDescendantDirectory( - files: List, - ancestorId: String, - candidateId: String -): Boolean { +private fun isDescendantDirectory(files: List, ancestorId: String, candidateId: String): Boolean { if (ancestorId == candidateId) return true val byId = files.associateBy { it.id } var current = byId[candidateId] @@ -1041,8 +1103,8 @@ private fun isDescendantDirectory( return false } -private fun defaultContextMenuFiles(): List { - return listOf( +private fun defaultContextMenuFiles(): List = + listOf( ContextMenuDemoFile(CONTEXT_MENU_ROOT_ID, null, "Workspace", 0, true, true, 1L), ContextMenuDemoFile("fs.docs", CONTEXT_MENU_ROOT_ID, "Documents", 0, true, false, 2L), ContextMenuDemoFile("fs.downloads", CONTEXT_MENU_ROOT_ID, "Downloads", 0, true, false, 3L), @@ -1060,6 +1122,5 @@ private fun defaultContextMenuFiles(): List { ContextMenuDemoFile("fs.archive.2025", "fs.projects.archive", "2025", 0, true, false, 15L), ContextMenuDemoFile("fs.archive.2026", "fs.projects.archive", "2026", 0, true, false, 16L), ContextMenuDemoFile("fs.archive.2026.q1", "fs.archive.2026", "Q1-report.md", 7, false, false, 17L), - ContextMenuDemoFile("fs.downloads.asset", "fs.downloads", "texture-pack.zip", 24, false, false, 18L) + ContextMenuDemoFile("fs.downloads.asset", "fs.downloads", "texture-pack.zip", 24, false, false, 18L), ) -} diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/CssCascadeSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/CssCascadeSection.kt index f5983ad..bfba9fc 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/CssCascadeSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/CssCascadeSection.kt @@ -2,9 +2,9 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.Event +import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection -import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED fun UiScope.cssCascadeCombinatorsSection(onLogHook: (String, Event, String?) -> Unit) { @@ -18,23 +18,28 @@ fun UiScope.cssCascadeCombinatorsSection(onLogHook: (String, Event, String?) -> val parentThemeClass = if (cascadeParentDark) "dark" else "light" val ruleBlockClass = if (cascadeRuleAEnabled) "rule-a" else "rule-b" - val adjacentOrder = if (cascadeAdjacentSwapOrder) { - listOf("adj-target-1", "adj-source", "adj-target-2") - } else { - listOf("adj-source", "adj-target-1", "adj-target-2") - } - val generalItems = buildList { - add("gen-0") - if (cascadeGeneralInsertExtra) { - add("gen-extra") + val adjacentOrder = + if (cascadeAdjacentSwapOrder) { + listOf("adj-target-1", "adj-source", "adj-target-2") + } else { + listOf("adj-source", "adj-target-1", "adj-target-2") + } + val generalItems = + buildList { + add("gen-0") + if (cascadeGeneralInsertExtra) { + add("gen-extra") + } + add("gen-1") + add("gen-2") + add("gen-3") + } + val effectiveWarningIndex = + if (generalItems.isEmpty()) { + 0 + } else { + (cascadeGeneralWarningIndex.toInt().coerceAtLeast(0)) % generalItems.size } - add("gen-1") - add("gen-2") - add("gen-3") - } - val effectiveWarningIndex = if (generalItems.isEmpty()) 0 else { - (cascadeGeneralWarningIndex.toInt().coerceAtLeast(0)) % generalItems.size - } div({ key = "section.cssCascade" @@ -44,10 +49,12 @@ fun UiScope.cssCascadeCombinatorsSection(onLogHook: (String, Event, String?) -> flexDirection = FlexDirection.Column } }) { - text("CSS-like cascade demo: descendant/child/sibling selectors, specificity, source order, !important, inheritance.") + text( + "CSS-like cascade demo: descendant/child/sibling selectors, specificity, source order, !important, inheritance.", + ) text( "Use the controls to toggle classes, swap siblings, and insert/remove items.", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) div({ @@ -66,7 +73,7 @@ fun UiScope.cssCascadeCombinatorsSection(onLogHook: (String, Event, String?) -> cascadeParentDark = !cascadeParentDark onLogHook("css.cascade.toggle.parentClass", event, "dark=$cascadeParentDark") } - } + }, ) button(if (cascadeRuleAEnabled) "Rule block: A" else "Rule block: B", { key = "section.cssCascade.toggleRuleBlock" @@ -89,7 +96,7 @@ fun UiScope.cssCascadeCombinatorsSection(onLogHook: (String, Event, String?) -> text("Inheritance target: this text should inherit parent color class '$parentThemeClass'.") text( "Descendant vs child: direct item should be green, nested item blue, outside item inherited.", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) div({ @@ -158,7 +165,7 @@ fun UiScope.cssCascadeCombinatorsSection(onLogHook: (String, Event, String?) -> }) { text( "Adjacent sibling (+): only immediate .adj-target after .adj-source should change.", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) div({ key = "section.cssCascade.adj.controls" @@ -177,10 +184,10 @@ fun UiScope.cssCascadeCombinatorsSection(onLogHook: (String, Event, String?) -> onLogHook( "css.cascade.adj.toggleSource", event, - "enabled=$cascadeAdjacentSourceEnabled" + "enabled=$cascadeAdjacentSourceEnabled", ) } - } + }, ) button(if (cascadeAdjacentSwapOrder) "Order: swapped" else "Order: default", { key = "section.cssCascade.adj.swap" @@ -200,17 +207,21 @@ fun UiScope.cssCascadeCombinatorsSection(onLogHook: (String, Event, String?) -> } }) { adjacentOrder.forEach { item -> - val classNames = buildString { - append("adj-item ") - when (item) { - "adj-source" -> { - if (cascadeAdjacentSourceEnabled) append("adj-source") - else append("adj-neutral") - } + val classNames = + buildString { + append("adj-item ") + when (item) { + "adj-source" -> { + if (cascadeAdjacentSourceEnabled) { + append("adj-source") + } else { + append("adj-neutral") + } + } - else -> append("adj-target") + else -> append("adj-target") + } } - } text(item, { key = "section.cssCascade.$item" className = classNames @@ -220,7 +231,7 @@ fun UiScope.cssCascadeCombinatorsSection(onLogHook: (String, Event, String?) -> text( "General sibling (~): all .gen-target after .warning should change.", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) div({ key = "section.cssCascade.gen.controls" @@ -238,7 +249,7 @@ fun UiScope.cssCascadeCombinatorsSection(onLogHook: (String, Event, String?) -> onLogHook( "css.cascade.gen.moveWarning", event, - "index=$cascadeGeneralWarningIndex" + "index=$cascadeGeneralWarningIndex", ) } }) @@ -249,7 +260,7 @@ fun UiScope.cssCascadeCombinatorsSection(onLogHook: (String, Event, String?) -> onLogHook( "css.cascade.gen.toggleExtra", event, - "extra=$cascadeGeneralInsertExtra" + "extra=$cascadeGeneralInsertExtra", ) } }) @@ -274,7 +285,7 @@ fun UiScope.cssCascadeCombinatorsSection(onLogHook: (String, Event, String?) -> text( "Mixed chain: .cascade-mixed > .header + .body .title", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) button(if (cascadeMixedSpacerEnabled) "Spacer: ON (break +)" else "Spacer: OFF (adjacent)", { key = "section.cssCascade.mixed.toggleSpacer" @@ -283,7 +294,7 @@ fun UiScope.cssCascadeCombinatorsSection(onLogHook: (String, Event, String?) -> onLogHook( "css.cascade.mixed.toggleSpacer", event, - "spacer=$cascadeMixedSpacerEnabled" + "spacer=$cascadeMixedSpacerEnabled", ) } }) diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/DisplaySection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/DisplaySection.kt index fcb00b2..92ade1f 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/DisplaySection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/DisplaySection.kt @@ -1,32 +1,31 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections -import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.dom.elements.InputType +import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.Event -import org.dreamfinity.dsgl.core.style.* import org.dreamfinity.dsgl.core.hooks.useState +import org.dreamfinity.dsgl.core.style.* import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED -private val JUSTIFY_OPTIONS = listOf( - "start" to JustifyContent.Start, - "center" to JustifyContent.Center, - "end" to JustifyContent.End, - "space-between" to JustifyContent.SpaceBetween, - "space-around" to JustifyContent.SpaceAround, - "space-evenly" to JustifyContent.SpaceEvenly -) +private val JUSTIFY_OPTIONS = + listOf( + "start" to JustifyContent.Start, + "center" to JustifyContent.Center, + "end" to JustifyContent.End, + "space-between" to JustifyContent.SpaceBetween, + "space-around" to JustifyContent.SpaceAround, + "space-evenly" to JustifyContent.SpaceEvenly, + ) -private val ALIGN_OPTIONS = listOf( - "start" to AlignItems.Start, - "center" to AlignItems.Center, - "end" to AlignItems.End, - "stretch" to AlignItems.Stretch -) +private val ALIGN_OPTIONS = + listOf( + "start" to AlignItems.Start, + "center" to AlignItems.Center, + "end" to AlignItems.End, + "stretch" to AlignItems.Stretch, + ) -fun UiScope.displaySection( - onInfo: (String) -> Unit, - onLogHook: (String, Event, String?) -> Unit -) { +fun UiScope.displaySection(onInfo: (String) -> Unit, onLogHook: (String, Event, String?) -> Unit) { var displayBlockLargeGap by useState(false) var displayInlineWidth by useState(132L) var displayShowHidden by useState(true) @@ -73,7 +72,10 @@ fun UiScope.displaySection( backgroundColor = 0xFF2B3542.toInt() display = Display.Block gap = (if (displayBlockLargeGap) 6 else 2).px - border { width = 1.px; color = 0xFF657688.toInt() } + border { + width = 1.px + color = 0xFF657688.toInt() + } } }) { repeat(3) { index -> @@ -82,7 +84,10 @@ fun UiScope.displaySection( style = { padding = 2.px backgroundColor = (0xFF3A4B60 + index * 0x000A0A00).toInt() - border { width = 1.px; color = 0xFF8095AA.toInt() } + border { + width = 1.px + color = 0xFF8095AA.toInt() + } } }) { text("Block item ${index + 1}") @@ -96,7 +101,7 @@ fun UiScope.displaySection( value = inlineWidth.toLong(), min = inlineMinWidth.toLong(), max = inlineMaxWidth.toLong(), - step = 4 + step = 4, ), { key = "display.inline.width" @@ -105,11 +110,12 @@ fun UiScope.displaySection( val next = (event.parsedValue as? Long) ?: event.value.toLongOrNull() ?: inlineWidth.toLong() displayInlineWidth = next.coerceIn(inlineMinWidth.toLong(), inlineMaxWidth.toLong()) } - } + }, ) dynamicText( { "inline container width=$inlineWidth (drag slider to force wrapping)" }, - { style = { color = DEMO_MUTED } }) + { style = { color = DEMO_MUTED } }, + ) div({ key = "display.inline.container" style = { @@ -117,7 +123,10 @@ fun UiScope.displaySection( padding = 3.px backgroundColor = 0xFF2E3946.toInt() display = Display.Inline - border { width = 1.px; color = 0xFF607181.toInt() } + border { + width = 1.px + color = 0xFF607181.toInt() + } gap = 2.px } }) { @@ -128,8 +137,16 @@ fun UiScope.displaySection( padding = 2.px backgroundColor = 0xFF40556B.toInt() display = Display.Inline - margin { top = 1.px; right = 2.px; bottom = 1.px; left = 1.px } - border { width = 1.px; color = 0xFF90A7BE.toInt() } + margin { + top = 1.px + right = 2.px + bottom = 1.px + left = 1.px + } + border { + width = 1.px + color = 0xFF90A7BE.toInt() + } } }) { text(label) @@ -141,8 +158,16 @@ fun UiScope.displaySection( padding = 2.px backgroundColor = 0xFF3C5D4A.toInt() display = Display.Inline - margin { top = 1.px; right = 2.px; bottom = 1.px; left = 1.px } - border { width = 1.px; color = 0xFF86B197.toInt() } + margin { + top = 1.px + right = 2.px + bottom = 1.px + left = 1.px + } + border { + width = 1.px + color = 0xFF86B197.toInt() + } } }) { text("flex") @@ -153,7 +178,10 @@ fun UiScope.displaySection( backgroundColor = 0xFF2E4739.toInt() display = Display.Flex flexDirection = FlexDirection.Row - border { width = 1.px; color = 0xFF5B8D73.toInt() } + border { + width = 1.px + color = 0xFF5B8D73.toInt() + } } }) { div({ @@ -178,8 +206,16 @@ fun UiScope.displaySection( padding = 2.px backgroundColor = 0xFF5E4B3C.toInt() display = Display.Inline - margin { top = 1.px; right = 2.px; bottom = 1.px; left = 1.px } - border { width = 1.px; color = 0xFFB58E6A.toInt() } + margin { + top = 1.px + right = 2.px + bottom = 1.px + left = 1.px + } + border { + width = 1.px + color = 0xFFB58E6A.toInt() + } } }) { div({ @@ -187,7 +223,10 @@ fun UiScope.displaySection( padding = 1.px backgroundColor = 0xFF4B3B30.toInt() display = Display.Block - border { width = 1.px; color = 0xFF8B6A51.toInt() } + border { + width = 1.px + color = 0xFF8B6A51.toInt() + } gap = 1.px } }) { @@ -213,11 +252,11 @@ fun UiScope.displaySection( displayShowHidden = !displayShowHidden onInfo("Display.none visible=$displayShowHidden") } - } + }, ) text( "targetClicks=$displayNoneClicks (should not change while hidden)", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) } div({ @@ -229,7 +268,10 @@ fun UiScope.displaySection( padding = 3.px backgroundColor = 0xFF303A46.toInt() gap = 2.px - border { width = 1.px; color = 0xFF64788B.toInt() } + border { + width = 1.px + color = 0xFF64788B.toInt() + } } }) { div({ @@ -242,7 +284,10 @@ fun UiScope.displaySection( padding = 2.px backgroundColor = 0xFF5A3E3E.toInt() display = if (displayShowHidden) Display.Block else Display.None - border { width = 1.px; color = 0xFFB07B7B.toInt() } + border { + width = 1.px + color = 0xFFB07B7B.toInt() + } } }) { text("Toggle target (click me)") @@ -272,7 +317,7 @@ fun UiScope.displaySection( if (displayGridLargeGap) "gap: large" else "gap: compact", { onMouseClick = { displayGridLargeGap = !displayGridLargeGap } - } + }, ) } text("Row uses fixed-size items so justify spacing is easier to compare.", { @@ -289,7 +334,10 @@ fun UiScope.displaySection( justifyContent = justify.second alignItems = AlignItems.Center gap = 0.px - border { width = 1.px; color = 0xFF7E93A8.toInt() } + border { + width = 1.px + color = 0xFF7E93A8.toInt() + } } }) { dot("left", "A", 0xFFB3D6FF.toInt(), 0xFFDEEFFF.toInt()) @@ -310,7 +358,10 @@ fun UiScope.displaySection( justifyContent = justify.second alignItems = align.second gap = (if (displayGridLargeGap) 8 else 2).px - border { width = 1.px; color = 0xFF6C7E90.toInt() } + border { + width = 1.px + color = 0xFF6C7E90.toInt() + } } }) { flexRowCell("0", "1", 14, 1, 0xFF46627C.toInt()) @@ -325,7 +376,10 @@ fun UiScope.displaySection( padding = 2.px backgroundColor = 0xFF2A3340.toInt() gap = 2.px - border { width = 1.px; color = 0xFF6B7E92.toInt() } + border { + width = 1.px + color = 0xFF6B7E92.toInt() + } display = Display.Flex flexDirection = FlexDirection.Column } @@ -359,7 +413,7 @@ fun UiScope.displaySection( value = gridColumns.toLong(), min = 2, max = 6, - step = 1 + step = 1, ), { key = "display.grid.columns" @@ -368,11 +422,11 @@ fun UiScope.displaySection( val next = (event.parsedValue as? Long) ?: event.value.toLongOrNull() ?: gridColumns.toLong() displayGridColumns = next.coerceIn(2, 6) } - } + }, ) text( "gridColumns=$displayGridColumns (first tile spans 2 columns)", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) div({ key = "display.grid.container" @@ -385,7 +439,10 @@ fun UiScope.displaySection( gap = (if (displayGridLargeGap) 4 else 2).px alignItems = align.second justifyItems = JustifyItems.Stretch - border { width = 1.px; color = 0xFF70849A.toInt() } + border { + width = 1.px + color = 0xFF70849A.toInt() + } } }) { repeat(10) { index -> @@ -397,7 +454,10 @@ fun UiScope.displaySection( if (index == 0) { gridColumnSpan = 2 } - border { width = 1.px; color = 0xFF93AACC.toInt() } + border { + width = 1.px + color = 0xFF93AACC.toInt() + } } }) { text("Cell ${index + 1}") @@ -407,18 +467,32 @@ fun UiScope.displaySection( } } -private fun UiScope.dot(keyPart: String, label: String, fill: Int, borderColor: Int) { +private fun UiScope.dot( + keyPart: String, + label: String, + fill: Int, + borderColor: Int, +) { div({ key = "display.flex.justify.dot.$keyPart" style = { display = Display.Inline backgroundColor = fill - border { width = 1.px; color = borderColor } + border { + width = 1.px + color = borderColor + } } }) { text(label) } } -private fun UiScope.flexRowCell(keyPart: String, label: String, widthPx: Int, paddingPx: Int, color: Int) { +private fun UiScope.flexRowCell( + keyPart: String, + label: String, + widthPx: Int, + paddingPx: Int, + color: Int, +) { div({ key = "display.flex.row.item.$keyPart" style = { diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/DragDropSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/DragDropSection.kt index 3f8b568..e4e4dbe 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/DragDropSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/DragDropSection.kt @@ -2,16 +2,16 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections import net.minecraft.init.Items import net.minecraft.item.ItemStack -import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.dnd.* import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.Event +import org.dreamfinity.dsgl.core.hooks.useEffect +import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.core.style.AlignItems import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection import org.dreamfinity.dsgl.core.style.JustifyItems -import org.dreamfinity.dsgl.core.hooks.useEffect -import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.mcForge1710.McItemStackRef import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED @@ -20,13 +20,13 @@ private const val HIGHLIGHT_DELTA = 22 private data class DndDemoItem( val id: String, val label: String, - val stack: McItemStackRef + val stack: McItemStackRef, ) private enum class DndLaneIndicator { NONE, BEFORE, - AFTER + AFTER, } private data class DndSectionState( @@ -48,41 +48,44 @@ private data class DndSectionState( val debugCandidatesCount: Int = 0, val debugInsertPosition: String = "none", val debugExcludesActiveCard: Boolean = true, - val boxes: Map> = linkedMapOf( - "box-a" to emptyList(), - "box-b" to emptyList(), - "box-c" to emptyList() - ) + val boxes: Map> = + linkedMapOf( + "box-a" to emptyList(), + "box-b" to emptyList(), + "box-c" to emptyList(), + ), ) private data class LaneHoverIntent( val targetId: String?, val insertAfter: Boolean, - val append: Boolean + val append: Boolean, ) fun UiScope.dragNDropSection( onInfo: (String) -> Unit, onClearLogs: () -> Unit, - onLogHook: (String, Event, String?) -> Unit + onLogHook: (String, Event, String?) -> Unit, ) { var state by useState(DndSectionState()) fun logHook(name: String, event: Event, note: String? = null) = onLogHook(name, event, note) fun clearLaneReorderHover() { - state = state.copy( - reorderHoverTargetId = null, - reorderHoverInsertAfter = false, - reorderHoverLaneAppend = false - ) + state = + state.copy( + reorderHoverTargetId = null, + reorderHoverInsertAfter = false, + reorderHoverLaneAppend = false, + ) } fun resetDndItems(source: String) { - state = DndSectionState( - items = defaultDndItems(), - smoothFactor = state.smoothFactor - ) + state = + DndSectionState( + items = defaultDndItems(), + smoothFactor = state.smoothFactor, + ) onInfo("DnD demo list reset by $source") } @@ -93,13 +96,16 @@ fun UiScope.dragNDropSection( onInfo("DnD smoothing k=${"%.1f".format(next)}") } - fun extractCard(cardId: String): Triple, LinkedHashMap>>? { + fun extractCard( + cardId: String, + ): Triple, LinkedHashMap>>? { val lane = state.items.toMutableList() - val boxes = linkedMapOf>().apply { - state.boxes.forEach { (key, list) -> - this[key] = list.toMutableList() + val boxes = + linkedMapOf>().apply { + state.boxes.forEach { (key, list) -> + this[key] = list.toMutableList() + } } - } val laneIndex = lane.indexOfFirst { it.id == cardId } if (laneIndex >= 0) { val card = lane.removeAt(laneIndex) @@ -119,19 +125,23 @@ fun UiScope.dragNDropSection( draggedId: String, targetId: String?, insertAfter: Boolean?, - dropOnLane: Boolean + dropOnLane: Boolean, ): Boolean { - val laneIds = state.items.map { it.id }.toMutableList() + val laneIds = + state.items + .map { it.id } + .toMutableList() val sourceIndex = laneIds.indexOf(draggedId) if (sourceIndex < 0) return true val removed = laneIds.removeAt(sourceIndex) val targetIndex = targetId?.let { laneIds.indexOf(it) } ?: -1 - val destinationIndex = when { - dropOnLane || targetId == null -> laneIds.size - targetIndex < 0 -> laneIds.size - insertAfter == true -> (targetIndex + 1).coerceAtMost(laneIds.size) - else -> targetIndex.coerceIn(0, laneIds.size) - } + val destinationIndex = + when { + dropOnLane || targetId == null -> laneIds.size + targetIndex < 0 -> laneIds.size + insertAfter == true -> (targetIndex + 1).coerceAtMost(laneIds.size) + else -> targetIndex.coerceIn(0, laneIds.size) + } laneIds.add(destinationIndex, removed) return laneIds != state.items.map { it.id } } @@ -140,26 +150,28 @@ fun UiScope.dragNDropSection( draggedId: String, targetId: String?, insertAfter: Boolean?, - dropOnLane: Boolean + dropOnLane: Boolean, ): Boolean { val extracted = extractCard(draggedId) ?: return false val card = extracted.first val lane = extracted.second.toMutableList() val boxes = extracted.third val targetIndex = targetId?.let { id -> lane.indexOfFirst { it.id == id } } ?: -1 - val insertIndex = when { - dropOnLane || targetId == null || targetIndex < 0 -> lane.size - insertAfter == true -> (targetIndex + 1).coerceAtMost(lane.size) - else -> targetIndex.coerceIn(0, lane.size) - } + val insertIndex = + when { + dropOnLane || targetId == null || targetIndex < 0 -> lane.size + insertAfter == true -> (targetIndex + 1).coerceAtMost(lane.size) + else -> targetIndex.coerceIn(0, lane.size) + } lane.add(insertIndex, card) if (!dropOnLane && !wouldLaneReorderChange(draggedId, targetId, insertAfter, dropOnLane)) { return false } - state = state.copy( - items = lane, - boxes = boxes.mapValuesTo(linkedMapOf()) { (_, value) -> value.toList() } - ) + state = + state.copy( + items = lane, + boxes = boxes.mapValuesTo(linkedMapOf()) { (_, value) -> value.toList() }, + ) return true } @@ -169,25 +181,28 @@ fun UiScope.dragNDropSection( val boxes = extracted.third val target = boxes.getOrPut(boxId) { mutableListOf() } target.add(extracted.first) - state = state.copy( - items = lane, - boxes = boxes.mapValuesTo(linkedMapOf()) { (_, value) -> value.toList() } - ) + state = + state.copy( + items = lane, + boxes = boxes.mapValuesTo(linkedMapOf()) { (_, value) -> value.toList() }, + ) return true } fun handleDndStart(item: DndDemoItem, event: DragStartEvent) { val sourceBounds = event.target?.bounds - val offsetX = if (sourceBounds != null) { - (event.mouseX - sourceBounds.x).coerceIn(0, sourceBounds.width.coerceAtLeast(1)) - } else { - 0 - } - val offsetY = if (sourceBounds != null) { - (event.mouseY - sourceBounds.y).coerceIn(0, sourceBounds.height.coerceAtLeast(1)) - } else { - 0 - } + val offsetX = + if (sourceBounds != null) { + (event.mouseX - sourceBounds.x).coerceIn(0, sourceBounds.width.coerceAtLeast(1)) + } else { + 0 + } + val offsetY = + if (sourceBounds != null) { + (event.mouseY - sourceBounds.y).coerceIn(0, sourceBounds.height.coerceAtLeast(1)) + } else { + 0 + } event.dataTransfer.setData("text/plain", item.label) event.dataTransfer.setData("application/x-dsgl-item-id", item.id) event.dataTransfer.effectAllowed = EffectAllowed.COPY_MOVE @@ -195,33 +210,54 @@ fun UiScope.dragNDropSection( if (!state.ghostEnabled) { event.dataTransfer.hideGhost() } - val sourceKey = event.target?.key?.toString() + val sourceKey = + event.target + ?.key + ?.toString() if (!sourceKey.isNullOrBlank()) { event.dataTransfer.setDragImage(sourceKey, offsetX, offsetY) } clearLaneReorderHover() - state = state.copy( - activeItem = item.label, - transferTypes = event.dataTransfer.types.sorted().joinToString(",").ifBlank { "-" }, - dropEffect = event.dataTransfer.dropEffect.name.lowercase(), - lastAction = "dragstart ${item.label}", - debugOverId = "none", - debugOverContainerId = "none", - debugCandidatesCount = 0, - debugInsertPosition = "none", - debugExcludesActiveCard = true - ) - val mode = event.target?.dragPreviewMode?.name?.lowercase() ?: "unknown" + state = + state.copy( + activeItem = item.label, + transferTypes = + event.dataTransfer.types + .sorted() + .joinToString(",") + .ifBlank { "-" }, + dropEffect = + event.dataTransfer.dropEffect.name + .lowercase(), + lastAction = "dragstart ${item.label}", + debugOverId = "none", + debugOverContainerId = "none", + debugCandidatesCount = 0, + debugInsertPosition = "none", + debugExcludesActiveCard = true, + ) + val mode = + event.target + ?.dragPreviewMode + ?.name + ?.lowercase() ?: "unknown" logHook("dnd.onDragStart", event, "item=${item.id} mode=$mode") } fun handleDndDrag(event: DragEvent) { val tick = state.dragTickCount + 1 - state = state.copy( - dragTickCount = tick, - transferTypes = event.dataTransfer.types.sorted().joinToString(",").ifBlank { "-" }, - dropEffect = event.dataTransfer.dropEffect.name.lowercase() - ) + state = + state.copy( + dragTickCount = tick, + transferTypes = + event.dataTransfer.types + .sorted() + .joinToString(",") + .ifBlank { "-" }, + dropEffect = + event.dataTransfer.dropEffect.name + .lowercase(), + ) if (tick % 5 == 0) { logHook("dnd.onDrag", event, "tick=$tick") } @@ -234,51 +270,49 @@ fun UiScope.dragNDropSection( val id = extractCardIdFromDragKey(child.key) ?: return@mapNotNull null if (excludedCardId != null && id == excludedCardId) return@mapNotNull null id to child - } - .sortedBy { (_, node) -> node.bounds.y } + }.sortedBy { (_, node) -> node.bounds.y } } - fun resolveLaneIntentFromMouse( - laneNode: DOMNode?, - mouseY: Int, - excludedCardId: String? - ): LaneHoverIntent { + fun resolveLaneIntentFromMouse(laneNode: DOMNode?, mouseY: Int, excludedCardId: String?): LaneHoverIntent { val cards = laneCards(laneNode, excludedCardId) if (cards.isEmpty()) return LaneHoverIntent(targetId = null, insertAfter = false, append = true) val lastCard = cards.last().second if (mouseY >= lastCard.bounds.y + lastCard.bounds.height + 4) { return LaneHoverIntent(targetId = null, insertAfter = false, append = true) } - val target = cards.minByOrNull { (_, node) -> - kotlin.math.abs(mouseY - (node.bounds.y + (node.bounds.height / 2))) - } ?: return LaneHoverIntent(targetId = null, insertAfter = false, append = true) + val target = + cards.minByOrNull { (_, node) -> + kotlin.math.abs(mouseY - (node.bounds.y + (node.bounds.height / 2))) + } ?: return LaneHoverIntent(targetId = null, insertAfter = false, append = true) val splitY = target.second.bounds.y + (target.second.bounds.height / 2) return LaneHoverIntent(targetId = target.first, insertAfter = mouseY >= splitY, append = false) } - fun laneCandidateCount(laneNode: DOMNode?, excludedCardId: String?): Int { - return laneCards(laneNode, excludedCardId).size - } + fun laneCandidateCount(laneNode: DOMNode?, excludedCardId: String?): Int = laneCards(laneNode, excludedCardId).size fun handleDndLaneOver(event: DragOverEvent) { val laneNode = event.target val draggedId = event.dataTransfer.getData("application/x-dsgl-item-id") val intent = resolveLaneIntentFromMouse(laneNode, event.mouseY, draggedId) - state = state.copy( - reorderHoverLaneAppend = intent.append, - reorderHoverTargetId = intent.targetId, - reorderHoverInsertAfter = intent.insertAfter, - debugOverContainerId = "lane", - debugOverId = intent.targetId ?: "append", - debugCandidatesCount = laneCandidateCount(laneNode, draggedId), - debugInsertPosition = when { - intent.append -> "append" - intent.insertAfter -> "after" - else -> "before" - }, - debugExcludesActiveCard = true, - dropEffect = event.dataTransfer.dropEffect.name.lowercase() - ) + state = + state.copy( + reorderHoverLaneAppend = intent.append, + reorderHoverTargetId = intent.targetId, + reorderHoverInsertAfter = intent.insertAfter, + debugOverContainerId = "lane", + debugOverId = intent.targetId ?: "append", + debugCandidatesCount = laneCandidateCount(laneNode, draggedId), + debugInsertPosition = + when { + intent.append -> "append" + intent.insertAfter -> "after" + else -> "before" + }, + debugExcludesActiveCard = true, + dropEffect = + event.dataTransfer.dropEffect.name + .lowercase(), + ) event.acceptDrop(DropEffect.MOVE) } @@ -286,30 +320,40 @@ fun UiScope.dragNDropSection( val draggedId = event.dataTransfer.getData("application/x-dsgl-item-id") ?: return val laneNode = event.target val intent = resolveLaneIntentFromMouse(laneNode, event.mouseY, draggedId) - val moved = commitLaneReorderDrop( - draggedId = draggedId, - targetId = intent.targetId, - insertAfter = if (intent.append) null else intent.insertAfter, - dropOnLane = intent.append - ) + val moved = + commitLaneReorderDrop( + draggedId = draggedId, + targetId = intent.targetId, + insertAfter = if (intent.append) null else intent.insertAfter, + dropOnLane = intent.append, + ) if (moved) { onInfo( "Lane drop: drag=$draggedId target=${intent.targetId ?: "lane"} pos=${ - if (intent.append) "append" else if (intent.insertAfter) "after" else "before" - }" + if (intent.append) { + "append" + } else if (intent.insertAfter) { + "after" + } else { + "before" + } + }", ) } clearLaneReorderHover() - state = state.copy( - dropEffect = event.dataTransfer.dropEffect.name.lowercase(), - debugOverId = "none", - debugOverContainerId = "none", - debugInsertPosition = "none" - ) + state = + state.copy( + dropEffect = + event.dataTransfer.dropEffect.name + .lowercase(), + debugOverId = "none", + debugOverContainerId = "none", + debugInsertPosition = "none", + ) logHook( "dnd.reorder.lane.onDrop", event, - "dragged=$draggedId target=${intent.targetId ?: "lane"} append=${intent.append}" + "dragged=$draggedId target=${intent.targetId ?: "lane"} append=${intent.append}", ) } @@ -317,17 +361,20 @@ fun UiScope.dragNDropSection( val draggedId = event.dataTransfer.getData("application/x-dsgl-item-id") if (draggedId != null && draggedId == targetCardId) return val laneNode = event.target?.parent - state = state.copy( - reorderHoverTargetId = targetCardId, - reorderHoverInsertAfter = insertAfter, - reorderHoverLaneAppend = false, - debugOverContainerId = "lane", - debugOverId = targetCardId, - debugCandidatesCount = laneCandidateCount(laneNode, draggedId), - debugInsertPosition = if (insertAfter) "after" else "before", - debugExcludesActiveCard = true, - dropEffect = event.dataTransfer.dropEffect.name.lowercase() - ) + state = + state.copy( + reorderHoverTargetId = targetCardId, + reorderHoverInsertAfter = insertAfter, + reorderHoverLaneAppend = false, + debugOverContainerId = "lane", + debugOverId = targetCardId, + debugCandidatesCount = laneCandidateCount(laneNode, draggedId), + debugInsertPosition = if (insertAfter) "after" else "before", + debugExcludesActiveCard = true, + dropEffect = + event.dataTransfer.dropEffect.name + .lowercase(), + ) event.acceptDrop(DropEffect.MOVE) event.cancelled = true } @@ -340,31 +387,37 @@ fun UiScope.dragNDropSection( onInfo("Card drop: drag=$draggedId target=$targetCardId pos=${if (insertAfter) "after" else "before"}") } clearLaneReorderHover() - state = state.copy( - debugOverId = "none", - debugOverContainerId = "none", - debugInsertPosition = "none", - dropEffect = event.dataTransfer.dropEffect.name.lowercase() - ) + state = + state.copy( + debugOverId = "none", + debugOverContainerId = "none", + debugInsertPosition = "none", + dropEffect = + event.dataTransfer.dropEffect.name + .lowercase(), + ) event.cancelled = true logHook( "dnd.reorder.card.onDrop", event, - "dragged=$draggedId target=$targetCardId pos=${if (insertAfter) "after" else "before"}" + "dragged=$draggedId target=$targetCardId pos=${if (insertAfter) "after" else "before"}", ) } fun handleDndBoxOver(boxId: String, event: DragOverEvent) { clearLaneReorderHover() - state = state.copy( - hoverZone = boxId, - debugOverId = boxId, - debugOverContainerId = "box:$boxId", - debugInsertPosition = "drop", - debugCandidatesCount = 1, - debugExcludesActiveCard = true, - dropEffect = event.dataTransfer.dropEffect.name.lowercase() - ) + state = + state.copy( + hoverZone = boxId, + debugOverId = boxId, + debugOverContainerId = "box:$boxId", + debugInsertPosition = "drop", + debugCandidatesCount = 1, + debugExcludesActiveCard = true, + dropEffect = + event.dataTransfer.dropEffect.name + .lowercase(), + ) event.acceptDrop(DropEffect.MOVE) } @@ -374,22 +427,30 @@ fun UiScope.dragNDropSection( if (moved) { state = state.copy(hoverZone = boxId, lastAction = "moved $draggedId to $boxId") } - state = state.copy(dropEffect = event.dataTransfer.dropEffect.name.lowercase()) + state = + state.copy( + dropEffect = + event.dataTransfer.dropEffect.name + .lowercase(), + ) logHook("dnd.$boxId.onDrop", event, "dragged=$draggedId") } fun handleDndEnd(event: DragEndEvent) { clearLaneReorderHover() - state = state.copy( - hoverZone = "none", - dropEffect = event.finalDropEffect.name.lowercase(), - lastAction = "dragend drop=${event.didDrop} effect=${event.finalDropEffect.name.lowercase()}", - activeItem = "none", - debugOverId = "none", - debugOverContainerId = "none", - debugCandidatesCount = 0, - debugInsertPosition = "none" - ) + state = + state.copy( + hoverZone = "none", + dropEffect = + event.finalDropEffect.name + .lowercase(), + lastAction = "dragend drop=${event.didDrop} effect=${event.finalDropEffect.name.lowercase()}", + activeItem = "none", + debugOverId = "none", + debugOverContainerId = "none", + debugCandidatesCount = 0, + debugInsertPosition = "none", + ) logHook("dnd.onDragEnd", event, "drop=${event.didDrop}") } @@ -397,12 +458,13 @@ fun UiScope.dragNDropSection( if (state.reorderHoverLaneAppend) return DndLaneIndicator.NONE if (state.reorderHoverTargetId != cardId) return DndLaneIndicator.NONE val draggedId = extractCardIdFromDragKey(sourceKey) ?: return DndLaneIndicator.NONE - val wouldChange = wouldLaneReorderChange( - draggedId = draggedId, - targetId = cardId, - insertAfter = state.reorderHoverInsertAfter, - dropOnLane = false - ) + val wouldChange = + wouldLaneReorderChange( + draggedId = draggedId, + targetId = cardId, + insertAfter = state.reorderHoverInsertAfter, + dropOnLane = false, + ) if (!wouldChange) return DndLaneIndicator.NONE return if (state.reorderHoverInsertAfter) DndLaneIndicator.AFTER else DndLaneIndicator.BEFORE } @@ -423,32 +485,38 @@ fun UiScope.dragNDropSection( useDragDropMonitor( DragDropMonitorCallbacks( onDragMove = { active, over -> - state = state.copy( - activeItem = active.id ?: active.sourceKey?.toString() ?: "none", - debugOverContainerId = if (over == null) "none" else "target", - debugOverId = over?.toString() ?: "none" - ) + state = + state.copy( + activeItem = active.id ?: active.sourceKey?.toString() ?: "none", + debugOverContainerId = if (over == null) "none" else "target", + debugOverId = over?.toString() ?: "none", + ) }, onDragOver = { active, over -> - state = state.copy( - dropEffect = active.dropEffect.name.lowercase(), - debugOverId = over?.toString() ?: "none" - ) + state = + state.copy( + dropEffect = + active.dropEffect.name + .lowercase(), + debugOverId = over?.toString() ?: "none", + ) }, onDragEnd = { _, _, effect -> - state = state.copy( - dropEffect = effect.name.lowercase(), - debugOverId = "none", - debugOverContainerId = "none" - ) + state = + state.copy( + dropEffect = effect.name.lowercase(), + debugOverId = "none", + debugOverContainerId = "none", + ) }, onDragCancel = { - state = state.copy( - debugOverId = "none", - debugOverContainerId = "none" - ) - } - ) + state = + state.copy( + debugOverId = "none", + debugOverContainerId = "none", + ) + }, + ), ) val monitor = DndSystem.monitor() @@ -464,18 +532,18 @@ fun UiScope.dragNDropSection( text("Drag preview modes: ORIGINAL (detached source) and GHOST (overlay preview).") text( "active=${state.activeItem} mode=${monitor.mode?.name ?: "none"} effect=${state.dropEffect} hover=${state.hoverZone}", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) text("types=${state.transferTypes} dragTicks=${state.dragTickCount} action=${state.lastAction}", { style = { color = DEMO_MUTED } }) text( "debug active=${monitor.sourceKey ?: "none"} over=${state.debugOverId} container=${state.debugOverContainerId}", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) text( "candidates=${state.debugCandidatesCount} insert=${state.debugInsertPosition} excludeActive=${state.debugExcludesActiveCard}", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) div({ style = { @@ -535,7 +603,7 @@ fun UiScope.dragNDropSection( onCardOver = ::handleDndCardReorderOver, onCardDrop = ::handleDndCardReorderDrop, laneIndicatorForCard = ::laneIndicatorForCard, - shouldShowLaneAppendGap = ::shouldShowLaneAppendGap + shouldShowLaneAppendGap = ::shouldShowLaneAppendGap, ) renderGhostModeBoxes( state = state, @@ -546,7 +614,7 @@ fun UiScope.dragNDropSection( onBoxDrop = ::handleDndBoxDrop, onHoverZone = { boxId -> state = state.copy(hoverZone = boxId) }, onLogHook = ::logHook, - onReset = { resetDndItems("button") } + onReset = { resetDndItems("button") }, ) } } @@ -564,7 +632,7 @@ private fun UiScope.originalModeReorder( onCardOver: (String, Boolean, DragOverEvent) -> Unit, onCardDrop: (String, Boolean, DropEvent) -> Unit, laneIndicatorForCard: (String, Any?) -> DndLaneIndicator, - shouldShowLaneAppendGap: (Any?) -> Boolean + shouldShowLaneAppendGap: (Any?) -> Boolean, ) { val draggedId = extractCardIdFromDragKey(sourceKey) @@ -575,7 +643,10 @@ private fun UiScope.originalModeReorder( gap = 3.px padding = 3.px backgroundColor = 0xFF2D333B.toInt() - border { width = 1.px; color = 0xFF6B7785.toInt() } + border { + width = 1.px + color = 0xFF6B7785.toInt() + } display = Display.Flex flexDirection = FlexDirection.Column flexGrow = 1.0f @@ -583,14 +654,15 @@ private fun UiScope.originalModeReorder( }) { text("ORIGINAL mode: reorder list") text("Detached source follows cursor; slot uses placeholder.", { style = { color = DEMO_MUTED } }) - val laneDroppable = useDroppable( - id = "lane", - nodeKey = "dnd.lane.column", - accepts = { active -> !active.id.isNullOrBlank() }, - onDragOver = { event, _ -> onLaneOver(event) }, - onDragLeave = { _, _ -> onLaneLeave() }, - onDrop = { event, _ -> onLaneDrop(event) } - ) + val laneDroppable = + useDroppable( + id = "lane", + nodeKey = "dnd.lane.column", + accepts = { active -> !active.id.isNullOrBlank() }, + onDragOver = { event, _ -> onLaneOver(event) }, + onDragLeave = { _, _ -> onLaneLeave() }, + onDrop = { event, _ -> onLaneDrop(event) }, + ) div({ style = { display = Display.Flex @@ -605,7 +677,10 @@ private fun UiScope.originalModeReorder( style = { gap = 6.px backgroundColor = if (state.reorderHoverLaneAppend) 0x2A9EC4E3 else 0x00000000 - border { width = 1.px; color = if (state.reorderHoverLaneAppend) 0xFF9EC4E3.toInt() else 0x44405058 } + border { + width = 1.px + color = if (state.reorderHoverLaneAppend) 0xFF9EC4E3.toInt() else 0x44405058 + } display = Display.Flex flexDirection = FlexDirection.Column } @@ -618,15 +693,16 @@ private fun UiScope.originalModeReorder( state.items.forEach { item -> val indicator = laneIndicatorForCard(item.id, sourceKey) val isDraggedItem = draggedId != null && draggedId == item.id - val sortable = useSortable( - id = item.id, - nodeKey = "dnd.lane.card.${item.id}", - containerId = "lane", - items = state.items.map { it.id }, - data = item, - previewMode = DragPreviewMode.ORIGINAL, - hideSourceWhileDragging = true - ) + val sortable = + useSortable( + id = item.id, + nodeKey = "dnd.lane.card.${item.id}", + containerId = "lane", + items = state.items.map { it.id }, + data = item, + previewMode = DragPreviewMode.ORIGINAL, + hideSourceWhileDragging = true, + ) cardWithItem( item = item, @@ -635,17 +711,28 @@ private fun UiScope.originalModeReorder( draggableEnabled = !isDraggedItem, highlighted = indicator != DndLaneIndicator.NONE, insertionIndicator = indicator, - extraListeners = DndListeners( - onDragStart = { event -> onStart(item, event) }, - onDrag = { event -> onDrag(event) }, - onDragEnd = { event -> onEnd(event) }, - onDragOver = if (isDraggedItem) null else { event -> - onCardOver(item.id, resolveInsertAfter(event), event) - }, - onDrop = if (isDraggedItem) null else { event -> - onCardDrop(item.id, resolveInsertAfter(event), event) - } - ) + extraListeners = + DndListeners( + onDragStart = { event -> onStart(item, event) }, + onDrag = { event -> onDrag(event) }, + onDragEnd = { event -> onEnd(event) }, + onDragOver = + if (isDraggedItem) { + null + } else { + { event -> + onCardOver(item.id, resolveInsertAfter(event), event) + } + }, + onDrop = + if (isDraggedItem) { + null + } else { + { event -> + onCardDrop(item.id, resolveInsertAfter(event), event) + } + }, + ), ) } @@ -654,7 +741,10 @@ private fun UiScope.originalModeReorder( key = "dnd.lane.append.gap" style = { backgroundColor = 0x2A9EC4E3 - border { width = 1.px; color = 0xFF9EC4E3.toInt() } + border { + width = 1.px + color = 0xFF9EC4E3.toInt() + } borderRadius = 3.px display = Display.Flex flexDirection = FlexDirection.Column @@ -678,7 +768,7 @@ private fun UiScope.renderGhostModeBoxes( onBoxDrop: (String, DropEvent) -> Unit, onHoverZone: (String) -> Unit, onLogHook: (String, Event, String?) -> Unit, - onReset: () -> Unit + onReset: () -> Unit, ) { div({ key = "dnd.ghost.panel" @@ -688,7 +778,10 @@ private fun UiScope.renderGhostModeBoxes( gap = 4.px padding = 3.px backgroundColor = 0xFF2D333B.toInt() - border { width = 1.px; color = 0xFF6B7785.toInt() } + border { + width = 1.px + color = 0xFF6B7785.toInt() + } display = Display.Flex flexDirection = FlexDirection.Column } @@ -709,7 +802,7 @@ private fun UiScope.renderGhostModeBoxes( onBoxOver = onBoxOver, onBoxDrop = onBoxDrop, onHoverZone = onHoverZone, - onLogHook = onLogHook + onLogHook = onLogHook, ) dropBox( state = state, @@ -724,7 +817,7 @@ private fun UiScope.renderGhostModeBoxes( onBoxOver = onBoxOver, onBoxDrop = onBoxDrop, onHoverZone = onHoverZone, - onLogHook = onLogHook + onLogHook = onLogHook, ) dropBox( state = state, @@ -739,7 +832,7 @@ private fun UiScope.renderGhostModeBoxes( onBoxOver = onBoxOver, onBoxDrop = onBoxDrop, onHoverZone = onHoverZone, - onLogHook = onLogHook + onLogHook = onLogHook, ) button("Reset DnD", { onMouseClick = { onReset() } }) @@ -754,7 +847,7 @@ private fun UiScope.cardWithItem( draggableEnabled: Boolean = true, highlighted: Boolean, insertionIndicator: DndLaneIndicator = DndLaneIndicator.NONE, - extraListeners: DndListeners = DndListeners() + extraListeners: DndListeners = DndListeners(), ) { val draggingThis = sortable?.isDragging ?: draggable?.isDragging ?: DndSystem.monitor(cardKey).isDragging val accent = itemAccentColor(item.id) @@ -773,16 +866,32 @@ private fun UiScope.cardWithItem( alignItems = AlignItems.Center padding = 2.px gap = 1.px - backgroundColor = when { - draggingThis -> lighten(base, HIGHLIGHT_DELTA + 8) - highlighted -> lighten(base, HIGHLIGHT_DELTA) - else -> base + backgroundColor = + when { + draggingThis -> lighten(base, HIGHLIGHT_DELTA + 8) + highlighted -> lighten(base, HIGHLIGHT_DELTA) + else -> base + } + border { + width = 1.px + color = accent } - border { width = 1.px; color = accent } borderRadius = 3.px when (insertionIndicator) { - DndLaneIndicator.BEFORE -> margin { top = insertionGap.px; right = 0.px; bottom = 0.px; left = 0.px } - DndLaneIndicator.AFTER -> margin { top = 0.px; right = 0.px; bottom = insertionGap.px; left = 0.px } + DndLaneIndicator.BEFORE -> + margin { + top = insertionGap.px + right = 0.px + bottom = 0.px + left = 0.px + } + DndLaneIndicator.AFTER -> + margin { + top = 0.px + right = 0.px + bottom = insertionGap.px + left = 0.px + } DndLaneIndicator.NONE -> Unit } } @@ -806,7 +915,10 @@ private fun UiScope.cardWithItem( itemStack(item.stack, { key = "dnd.stack.${item.id}" style = { - border { width = 1.px; color = 0x553A4452 } + border { + width = 1.px + color = 0x553A4452 + } backgroundColor = 0x2219222B } }) @@ -829,33 +941,37 @@ private fun UiScope.dropBox( onBoxOver: (String, DragOverEvent) -> Unit, onBoxDrop: (String, DropEvent) -> Unit, onHoverZone: (String) -> Unit, - onLogHook: (String, Event, String?) -> Unit + onLogHook: (String, Event, String?) -> Unit, ) { val highlighted = state.hoverZone == boxId - val dropDescriptor = useDroppable( - id = boxId, - nodeKey = boxKey, - accepts = { active -> !active.id.isNullOrBlank() }, - onDragEnter = { event, _ -> - onHoverZone(boxId) - onLogHook("dnd.$boxId.onDragEnter", event, null) - }, - onDragOver = { event, _ -> onBoxOver(boxId, event) }, - onDragLeave = { event, _ -> - if (highlighted) { - onHoverZone("none") - } - onLogHook("dnd.$boxId.onDragLeave", event, null) - }, - onDrop = { event, _ -> onBoxDrop(boxId, event) } - ) + val dropDescriptor = + useDroppable( + id = boxId, + nodeKey = boxKey, + accepts = { active -> !active.id.isNullOrBlank() }, + onDragEnter = { event, _ -> + onHoverZone(boxId) + onLogHook("dnd.$boxId.onDragEnter", event, null) + }, + onDragOver = { event, _ -> onBoxOver(boxId, event) }, + onDragLeave = { event, _ -> + if (highlighted) { + onHoverZone("none") + } + onLogHook("dnd.$boxId.onDragLeave", event, null) + }, + onDrop = { event, _ -> onBoxDrop(boxId, event) }, + ) div({ key = boxKey style = { padding = 4.px gap = 2.px backgroundColor = if (highlighted) lighten(color, HIGHLIGHT_DELTA) else color - border { width = 1.px; this.color = 0xFF8A94A2.toInt() } + border { + width = 1.px + this.color = 0xFF8A94A2.toInt() + } borderRadius = 3.px } applyDroppable(dropDescriptor) @@ -873,22 +989,23 @@ private fun UiScope.dropBox( } }) { cards.take(5).forEach { item -> - val draggable = useDraggable( - id = item.id, - nodeKey = "dnd.box.$boxId.card.${item.id}", - type = "card", - data = item, - previewMode = DragPreviewMode.GHOST, - hideSourceWhileDragging = state.hideSourceWhileDragging, - onDragStart = { event -> onStart(item, event) }, - onDrag = { event -> onDrag(event) }, - onDragEnd = { event -> onEnd(event) } - ) + val draggable = + useDraggable( + id = item.id, + nodeKey = "dnd.box.$boxId.card.${item.id}", + type = "card", + data = item, + previewMode = DragPreviewMode.GHOST, + hideSourceWhileDragging = state.hideSourceWhileDragging, + onDragStart = { event -> onStart(item, event) }, + onDrag = { event -> onDrag(event) }, + onDragEnd = { event -> onEnd(event) }, + ) cardWithItem( item = item, cardKey = "dnd.box.$boxId.card.${item.id}", draggable = draggable, - highlighted = false + highlighted = false, ) } if (cards.size > 5) { @@ -907,25 +1024,23 @@ private fun extractCardIdFromDragKey(sourceKey: Any?): String? { return key.substring(markerIndex + marker.length).takeIf { it.isNotBlank() } } -private fun itemBaseColor(itemId: String): Int { - return when (itemId) { +private fun itemBaseColor(itemId: String): Int = + when (itemId) { "apple" -> 0xFF355841.toInt() "bread" -> 0xFF68543A.toInt() "carrot" -> 0xFF6A4A2B.toInt() "diamond" -> 0xFF315A70.toInt() else -> 0xFF3C4B5A.toInt() } -} -private fun itemAccentColor(itemId: String): Int { - return when (itemId) { +private fun itemAccentColor(itemId: String): Int = + when (itemId) { "apple" -> 0xFF7BCEA0.toInt() "bread" -> 0xFFE7BE79.toInt() "carrot" -> 0xFFFFB46E.toInt() "diamond" -> 0xFF8ED2FF.toInt() else -> 0xFF9BB0C4.toInt() } -} private fun lighten(color: Int, delta: Int): Int { val a = (color ushr 24) and 0xFF @@ -947,25 +1062,26 @@ private fun resolveInsertAfter(event: DropEvent): Boolean { return event.mouseY >= splitY } -private fun defaultDndItems(): List = listOf( - DndDemoItem( - id = "apple", - label = "Apple", - stack = McItemStackRef(ItemStack(Items.apple, 1, 0)) - ), - DndDemoItem( - id = "bread", - label = "Bread", - stack = McItemStackRef(ItemStack(Items.bread, 1, 0)) - ), - DndDemoItem( - id = "carrot", - label = "Carrot", - stack = McItemStackRef(ItemStack(Items.carrot, 1, 0)) - ), - DndDemoItem( - id = "diamond", - label = "Diamond", - stack = McItemStackRef(ItemStack(Items.diamond, 1, 0)) +private fun defaultDndItems(): List = + listOf( + DndDemoItem( + id = "apple", + label = "Apple", + stack = McItemStackRef(ItemStack(Items.apple, 1, 0)), + ), + DndDemoItem( + id = "bread", + label = "Bread", + stack = McItemStackRef(ItemStack(Items.bread, 1, 0)), + ), + DndDemoItem( + id = "carrot", + label = "Carrot", + stack = McItemStackRef(ItemStack(Items.carrot, 1, 0)), + ), + DndDemoItem( + id = "diamond", + label = "Diamond", + stack = McItemStackRef(ItemStack(Items.diamond, 1, 0)), + ), ) -) diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/FocusRebuildSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/FocusRebuildSection.kt index 7ec04de..5008c71 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/FocusRebuildSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/FocusRebuildSection.kt @@ -1,22 +1,22 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections -import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.dom.elements.InputType +import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.Event import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.KeyInput import org.dreamfinity.dsgl.core.event.KeyModifiers import org.dreamfinity.dsgl.core.event.KeyboardKeyDownEvent +import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection -import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED fun UiScope.focusRebuildSection( renderPasses: Int, onManualInvalidate: (String) -> Unit, onInfo: (String) -> Unit, - onLogHook: (String, Event, String?) -> Unit + onLogHook: (String, Event, String?) -> Unit, ) { var focusStableValue by useState("") var focusUnstableValue by useState("") @@ -32,14 +32,15 @@ fun UiScope.focusRebuildSection( onManualInvalidate(reason) } - div({ - key = "section.focusRebuild" - style = { - gap = 4.px - display = Display.Flex - flexDirection = FlexDirection.Column - } - } + div( + { + key = "section.focusRebuild" + style = { + gap = 4.px + display = Display.Flex + flexDirection = FlexDirection.Column + } + }, ) { text("Stable key focus test: focus first field, press Enter to rebuild, keep typing.") text("Unstable key field changes key version and demonstrates focus/key instability.", { @@ -48,17 +49,17 @@ fun UiScope.focusRebuildSection( text( "renderPasses=$renderPasses autoState=$autoRebuildCounter manualInvalidates=$manualInvalidateCount", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) text( "stableEnterRebuilds=$focusStableEnterRebuilds unstableKeyVersion=$focusKeyVersion", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) input( InputType.Text( value = focusStableValue, - placeholder = "Stable key input (press Enter to rebuild)" + placeholder = "Stable key input (press Enter to rebuild)", ), { key = "focus.stable.input" @@ -76,13 +77,13 @@ fun UiScope.focusRebuildSection( onKeyUp = { event -> onLogHook("focus.stable.onKeyUp", event, null) } - } + }, ) input( InputType.Text( value = focusUnstableValue, - placeholder = "Unstable key input" + placeholder = "Unstable key input", ), { key = "focus.unstable.input.$focusKeyVersion" @@ -91,7 +92,7 @@ fun UiScope.focusRebuildSection( focusUnstableValue = applyTextMutation(focusUnstableValue, event, maxLength = 28) onLogHook("focus.unstable.onKeyDown", event, null) } - } + }, ) div({ @@ -131,7 +132,7 @@ fun UiScope.focusRebuildSection( }) text( "lastManualReason=$lastManualReason", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) } } @@ -141,7 +142,7 @@ private fun applyTextMutation( current: String, event: KeyboardKeyDownEvent, allowedChars: String? = null, - maxLength: Int? = null + maxLength: Int? = null, ): String { if (event.keyCode == KeyCodes.BACKSPACE) { if (current.isEmpty()) return current @@ -156,4 +157,3 @@ private fun applyTextMutation( if (maxLength != null && next.length > maxLength) return current return next } - diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/HooksSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/HooksSection.kt index 71037ef..da0cbdf 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/HooksSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/HooksSection.kt @@ -5,15 +5,15 @@ import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.Event import org.dreamfinity.dsgl.core.hooks.createContext import org.dreamfinity.dsgl.core.hooks.provideContext +import org.dreamfinity.dsgl.core.hooks.ref.ElementHandle +import org.dreamfinity.dsgl.core.hooks.ref.RefTarget +import org.dreamfinity.dsgl.core.hooks.ref.useRef import org.dreamfinity.dsgl.core.hooks.useCallback import org.dreamfinity.dsgl.core.hooks.useContext import org.dreamfinity.dsgl.core.hooks.useEffect import org.dreamfinity.dsgl.core.hooks.useMemo import org.dreamfinity.dsgl.core.hooks.useReducer import org.dreamfinity.dsgl.core.hooks.useState -import org.dreamfinity.dsgl.core.hooks.ref.ElementHandle -import org.dreamfinity.dsgl.core.hooks.ref.RefTarget -import org.dreamfinity.dsgl.core.hooks.ref.useRef import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection import org.dreamfinity.dsgl.core.style.Overflow @@ -22,10 +22,7 @@ import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_SURFACE_ALT private val hooksThemeContext = createContext(defaultValue = "System", name = "HooksTheme") -fun UiScope.hooksSection( - onInfo: (String) -> Unit, - onLogHook: (String, Event, String?) -> Unit -) { +fun UiScope.hooksSection(onInfo: (String) -> Unit, onLogHook: (String, Event, String?) -> Unit) { div({ key = "section.hooks" style = { @@ -49,10 +46,7 @@ fun UiScope.hooksSection( } } -private fun UiScope.overviewUseRef( - onInfo: (String) -> Unit, - onLogHook: (String, Event, String?) -> Unit -) { +private fun UiScope.overviewUseRef(onInfo: (String) -> Unit, onLogHook: (String, Event, String?) -> Unit) { var refsInputValue by useState("Ref demo input") var refsRebuildCount by useState(0) var refsCallbackMounted by useState(true) @@ -80,7 +74,7 @@ private fun UiScope.overviewUseRef( input( InputType.Text( value = refsInputValue, - placeholder = "Focusable input with stable key" + placeholder = "Focusable input with stable key", ), { key = "refs.input.primary" @@ -89,7 +83,7 @@ private fun UiScope.overviewUseRef( onLogHook("refs.input.onInput", event, "value=${event.value}") } }, - ref = inputRef + ref = inputRef, ) div({ @@ -132,18 +126,19 @@ private fun UiScope.overviewUseRef( backgroundColor = 0xFF313844.toInt() } }, - ref = panelRef + ref = panelRef, ) { text("Bounds target panel", { style = { color = 0xFFE2EAFF.toInt() } }) } text({ val bounds = panelRef.current?.bounds - value = if (bounds == null) { - "panelRef.current: null" - } else { - "panelRef.bounds: x=${bounds.x} y=${bounds.y} w=${bounds.width} h=${bounds.height}" - } + value = + if (bounds == null) { + "panelRef.current: null" + } else { + "panelRef.bounds: x=${bounds.x} y=${bounds.y} w=${bounds.width} h=${bounds.height}" + } style = { color = DEMO_MUTED } }) @@ -156,7 +151,7 @@ private fun UiScope.overviewUseRef( padding = 4.px } }, - ref = refsCallbackRef + ref = refsCallbackRef, ) { text("Callback ref target", { style = { color = 0xFFC5E8C5.toInt() } }) } @@ -164,7 +159,7 @@ private fun UiScope.overviewUseRef( text( "callback attaches=$refsCallbackAttachCount detaches=$refsCallbackDetachCount last=$refsCallbackLast", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) } } @@ -193,12 +188,12 @@ private fun UiScope.overviewUseState() { input( InputType.Text( value = hooksStateLocalText, - placeholder = "Local useState text" + placeholder = "Local useState text", ), { key = "hooks.useState.localText" onInput = { event -> hooksStateLocalText = event.value } - } + }, ) text("mounted state: count=$hooksStateLocalCount text=$hooksStateLocalText", { style = { color = DEMO_MUTED } @@ -265,13 +260,14 @@ private fun UiScope.overviewUseCallback() { val hooksCallbackIdentityRef by useRef() val hooksCallbackIdentity = System.identityHashCode(hooksCallback as Any) val hooksCallbackPreviousIdentity = hooksCallbackIdentityRef.current - val hooksCallbackIdentityStatus = if (hooksCallbackPreviousIdentity == null) { - "first render" - } else if (hooksCallbackIdentity == hooksCallbackPreviousIdentity) { - "stable" - } else { - "changed" - } + val hooksCallbackIdentityStatus = + if (hooksCallbackPreviousIdentity == null) { + "first render" + } else if (hooksCallbackIdentity == hooksCallbackPreviousIdentity) { + "stable" + } else { + "changed" + } hooksCallbackIdentityRef.current = hooksCallbackIdentity hookCard("useCallback", "Function identity is stable until dependency changes") { div({ @@ -323,10 +319,11 @@ private fun UiScope.overviewUseReducer() { } if (hooksReducerMounted) { - val (hooksReducerCount, dispatchReducer) = useReducer( - initialState = 0, - reducer = { old: Int, action: Int -> old + action } - ) + val (hooksReducerCount, dispatchReducer) = + useReducer( + initialState = 0, + reducer = { old: Int, action: Int -> old + action }, + ) div({ style = { gap = 4.px @@ -412,6 +409,7 @@ private fun UiScope.overviewUseEffect() { var hooksEffectNoise by useState(0) var hooksEffectLogRevision by useState(0) val hooksEffectLogBuffer by useRef(mutableListOf()) + fun appendEffectLog(line: String) { val buffer = hooksEffectLogBuffer.current ?: return buffer += line @@ -475,11 +473,7 @@ private fun UiScope.overviewUseEffect() { } } -private fun UiScope.hookCard( - title: String, - subtitle: String, - content: UiScope.() -> Unit -) { +private fun UiScope.hookCard(title: String, subtitle: String, content: UiScope.() -> Unit) { div({ key = "hooks.card.$title" style = { diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputEventsSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputEventsSection.kt index d799a71..bcfef74 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputEventsSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputEventsSection.kt @@ -1,33 +1,35 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections -import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.dom.elements.InputOption import org.dreamfinity.dsgl.core.dom.elements.InputType +import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.Event import org.dreamfinity.dsgl.core.event.FocusGainEvent import org.dreamfinity.dsgl.core.event.FocusLoseEvent import org.dreamfinity.dsgl.core.event.InputEvent import org.dreamfinity.dsgl.core.event.ValueChangedEvent +import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection import org.dreamfinity.dsgl.core.style.Overflow -import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_SURFACE_ALT import java.time.LocalTime import java.time.format.DateTimeFormatter -private val inputEventCheckboxOptions = listOf( - InputOption("alpha", "Alpha"), - InputOption("beta", "Beta"), - InputOption("gamma", "Gamma") -) +private val inputEventCheckboxOptions = + listOf( + InputOption("alpha", "Alpha"), + InputOption("beta", "Beta"), + InputOption("gamma", "Gamma"), + ) -private val inputEventRadioOptions = listOf( - InputOption("north", "North"), - InputOption("center", "Center"), - InputOption("south", "South") -) +private val inputEventRadioOptions = + listOf( + InputOption("north", "North"), + InputOption("center", "Center"), + InputOption("south", "South"), + ) private val inputEventTimeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS") @@ -39,7 +41,12 @@ fun UiScope.inputEventsSection(onLogHook: (String, Event, String?) -> Unit) { var inputEventRangeValue by useState(35L) var inputEventLogEntries by useState(emptyList()) - fun appendInputEvent(control: String, phase: String, value: String, event: Event) { + fun appendInputEvent( + control: String, + phase: String, + value: String, + event: Event, + ) { val time = LocalTime.now().format(inputEventTimeFormatter) val line = "$time $control.$phase value=$value" inputEventLogEntries = (listOf(line) + inputEventLogEntries).take(8) @@ -62,7 +69,11 @@ fun UiScope.inputEventsSection(onLogHook: (String, Event, String?) -> Unit) { return emptySet() } - fun checkboxValueString(): String = inputEventCheckboxValue.toList().sorted().joinToString(",") + fun checkboxValueString(): String = + inputEventCheckboxValue + .toList() + .sorted() + .joinToString(",") div({ key = "section.inputEvents" @@ -75,7 +86,7 @@ fun UiScope.inputEventsSection(onLogHook: (String, Event, String?) -> Unit) { text("HTML-like events demo: onFocus/onBlur/onInput/onChange") text( "Proof case: type in text field, then click elsewhere -> onInput per key, onChange on blur.", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) div({ @@ -114,7 +125,7 @@ fun UiScope.inputEventsSection(onLogHook: (String, Event, String?) -> Unit) { inputEventTextValue = event.value appendInputEvent("text", "change", event.value, event) } - } + }, ) text("Textarea") @@ -158,7 +169,7 @@ fun UiScope.inputEventsSection(onLogHook: (String, Event, String?) -> Unit) { variants = inputEventCheckboxOptions, selected = inputEventCheckboxValue, minSelected = 0, - maxSelected = 3 + maxSelected = 3, ), { key = "inputEvents.checkbox" @@ -177,14 +188,14 @@ fun UiScope.inputEventsSection(onLogHook: (String, Event, String?) -> Unit) { inputEventCheckboxValue = parseCheckboxSelection(event.parsedValue) appendInputEvent("checkbox", "change", event.value, event) } - } + }, ) text("Radio") input( InputType.Radio( variants = inputEventRadioOptions, - selected = inputEventRadioValue + selected = inputEventRadioValue, ), { key = "inputEvents.radio" @@ -203,7 +214,7 @@ fun UiScope.inputEventsSection(onLogHook: (String, Event, String?) -> Unit) { inputEventRadioValue = event.parsedValue as? String appendInputEvent("radio", "change", event.value, event) } - } + }, ) text("Range") @@ -212,7 +223,7 @@ fun UiScope.inputEventsSection(onLogHook: (String, Event, String?) -> Unit) { value = inputEventRangeValue, min = 0, max = 100, - step = 1 + step = 1, ), { key = "inputEvents.range" @@ -231,11 +242,11 @@ fun UiScope.inputEventsSection(onLogHook: (String, Event, String?) -> Unit) { inputEventRangeValue = (event.parsedValue as? Long) ?: inputEventRangeValue appendInputEvent("range", "change", event.value, event) } - } + }, ) text( "Range value=$inputEventRangeValue", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) } } @@ -252,7 +263,7 @@ fun UiScope.inputEventsSection(onLogHook: (String, Event, String?) -> Unit) { }) text( "Entries=${inputEventLogEntries.size}", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) } @@ -266,7 +277,10 @@ fun UiScope.inputEventsSection(onLogHook: (String, Event, String?) -> Unit) { overflowY = Overflow.Auto backgroundColor = DEMO_SURFACE_ALT padding = 3.px - border { width = 1.px; color = 0xFF6A7785.toInt() } + border { + width = 1.px + color = 0xFF6A7785.toInt() + } } }) { if (inputEventLogEntries.isEmpty()) { diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt index 553d33d..d242636 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt @@ -1,8 +1,8 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections -import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.dom.elements.InputOption import org.dreamfinity.dsgl.core.dom.elements.InputType +import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.core.select.SelectRuntime @@ -13,22 +13,21 @@ import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED import java.time.Instant import java.time.ZoneId -private val inputsCheckboxOptions = listOf( - InputOption("alpha", "Alpha"), - InputOption("beta", "Beta"), - InputOption("gamma", "Gamma") -) +private val inputsCheckboxOptions = + listOf( + InputOption("alpha", "Alpha"), + InputOption("beta", "Beta"), + InputOption("gamma", "Gamma"), + ) -private val inputsRadioOptions = listOf( - InputOption("north", "North"), - InputOption("center", "Center"), - InputOption("south", "South") -) +private val inputsRadioOptions = + listOf( + InputOption("north", "North"), + InputOption("center", "Center"), + InputOption("south", "South"), + ) -fun UiScope.inputsGallerySection( - clippingScrollDemoText: String, - onClippingScrollDemoTextChange: (String) -> Unit -) { +fun UiScope.inputsGallerySection(clippingScrollDemoText: String, onClippingScrollDemoTextChange: (String) -> Unit) { var openedAt by useState(Instant.now()) var timeZoneId by useState(ZoneId.systemDefault()) var sharedRangeValue by useState(35L) @@ -58,7 +57,11 @@ fun UiScope.inputsGallerySection( return emptySet() } - fun checkboxValueString(): String = inputCheckboxValue.toList().sorted().joinToString(",") + fun checkboxValueString(): String = + inputCheckboxValue + .toList() + .sorted() + .joinToString(",") SelectRuntime.engine.setStyle( SelectStyle( @@ -69,8 +72,8 @@ fun UiScope.inputsGallerySection( optionSelectedBackgroundColor = 0xFF2A4258.toInt(), groupTextColor = 0xFFB7C6D6.toInt(), openDurationMs = 120L, - closeDurationMs = 90L - ) + closeDurationMs = 90L, + ), ) div({ @@ -84,7 +87,7 @@ fun UiScope.inputsGallerySection( text("All InputType variants are interactive below.") text( "Validation examples: allowed chars, min/max, step, date format.", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) div({ @@ -109,12 +112,12 @@ fun UiScope.inputsGallerySection( value = "A1", placeholder = "hex", allowedChars = "0123456789ABCDEF", - maxLength = 8 + maxLength = 8, ), { key = "input.text" style = { width = 100.percent } - } + }, ) text("Password (max 12)") @@ -122,12 +125,12 @@ fun UiScope.inputsGallerySection( InputType.Password( value = "", placeholder = "secret", - maxLength = 12 + maxLength = 12, ), { key = "input.password" style = { width = 100.percent } - } + }, ) text("Number (10..20, wheel when focused)") @@ -136,12 +139,12 @@ fun UiScope.inputsGallerySection( value = 15, placeholder = "10..20", min = 10, - max = 20 + max = 20, ), { key = "input.number.basic" style = { width = 100.percent } - } + }, ) text("Number 0..100 wired with slider below") @@ -150,7 +153,7 @@ fun UiScope.inputsGallerySection( value = sharedRangeValue, placeholder = "0..100", min = 0, - max = 100 + max = 100, ), { key = "input.number.shared" @@ -161,7 +164,7 @@ fun UiScope.inputsGallerySection( onValueChange = { event -> sharedRangeValue = event.value.toLongOrNull() ?: sharedRangeValue } - } + }, ) text("Range (step 5, value=$sharedRangeValue)") @@ -170,7 +173,7 @@ fun UiScope.inputsGallerySection( value = sharedRangeValue, min = 0, max = 100, - step = 5 + step = 5, ), { key = "input.range" @@ -181,7 +184,7 @@ fun UiScope.inputsGallerySection( onValueChange = { event -> sharedRangeValue = event.value.toLongOrNull() ?: sharedRangeValue } - } + }, ) } @@ -200,7 +203,7 @@ fun UiScope.inputsGallerySection( variants = inputsCheckboxOptions, selected = inputCheckboxValue, minSelected = 1, - maxSelected = 2 + maxSelected = 2, ), { key = "input.checkbox" @@ -211,7 +214,7 @@ fun UiScope.inputsGallerySection( onValueChange = { event -> inputCheckboxValue = parseCheckboxSelection(event.parsedValue) } - } + }, ) text("Selected: ${checkboxValueString()}", { style = { color = DEMO_MUTED } }) @@ -219,7 +222,7 @@ fun UiScope.inputsGallerySection( input( InputType.Radio( variants = inputsRadioOptions, - selected = inputRadioValue + selected = inputRadioValue, ), { key = "input.radio" @@ -230,7 +233,7 @@ fun UiScope.inputsGallerySection( onValueChange = { event -> inputRadioValue = event.parsedValue as? String } - } + }, ) text("Selected: ${inputRadioValue ?: "-"}", { style = { color = DEMO_MUTED } }) @@ -264,12 +267,12 @@ fun UiScope.inputsGallerySection( input( InputType.Date( value = openedAt, - zoneId = timeZoneId + zoneId = timeZoneId, ), { key = "input.date" style = { width = 100.percent } - } + }, ) text("Opened: $openedAt", { style = { color = DEMO_MUTED } }) @@ -289,7 +292,7 @@ fun UiScope.inputsGallerySection( text("Select (overlay popup + keyboard + disabled options)") text( "Use Enter/Space/ArrowDown when focused. Esc closes popup. Wheel scrolls long list.", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) div({ @@ -380,7 +383,7 @@ fun UiScope.inputsGallerySection( onMouseClick = { selectDynamicAlt = !selectDynamicAlt } - } + }, ) text("Dynamic options") select({ @@ -406,7 +409,7 @@ fun UiScope.inputsGallerySection( } text( "Select state: basic=${selectBasicValue ?: "-"} many=${selectManyValue ?: "-"} dynamic=${selectDynamicValue ?: "-"}", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) text("Clipping + internal scrolling demo (100 lines prefilled)", { style = { color = DEMO_MUTED } }) @@ -421,7 +424,7 @@ fun UiScope.inputsGallerySection( button("Clear Focus", { onMouseClick = { FocusManager.clearFocus() } }) text( "1) Clear focus 2) wheel-scroll textarea 3) click visible text: caret must land exactly under cursor", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) } div({ diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InspectorSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InspectorSection.kt index 22eaf46..a783ee4 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InspectorSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InspectorSection.kt @@ -1,10 +1,10 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections -import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.dom.elements.InputType +import org.dreamfinity.dsgl.core.dsl.* +import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection -import org.dreamfinity.dsgl.core.hooks.useState private const val INSPECTOR_MUTED_TEXT: Int = 0xFFB0B7C1.toInt() @@ -26,7 +26,10 @@ fun UiScope.inspectorSection(onInfo: (String) -> Unit) { text("F9: switch mode (Pick/Locked)", { style = { color = INSPECTOR_MUTED_TEXT } }) text("Expanded panel: click Min to collapse into floating chip.", { style = { color = INSPECTOR_MUTED_TEXT } }) text("Minimized chip: drag to move, click (no drag) to restore.", { style = { color = INSPECTOR_MUTED_TEXT } }) - text("Expanded panel: drag header to move; drag edges/corners to resize.", { style = { color = INSPECTOR_MUTED_TEXT } }) + text("Expanded panel: drag header to move; drag edges/corners to resize.", { + style = + { color = INSPECTOR_MUTED_TEXT } + }) text("Style editor now uses typed controls: dropdowns, text inputs, numeric input + units.", { style = { color = INSPECTOR_MUTED_TEXT } }) @@ -46,11 +49,13 @@ fun UiScope.inspectorSection(onInfo: (String) -> Unit) { gap = 4.px padding = 4.px backgroundColor = 0x3338424F - border { width = 1.px; color = 0xFF5F6E80.toInt() } + border { + width = 1.px + color = 0xFF5F6E80.toInt() + } display = Display.Flex flexDirection = FlexDirection.Column } - }) { text("Sample subtree for inspection (hover/click with inspector ON).") text("Behind-inspector click counter: $inspectorBehindClickCounter", { @@ -77,7 +82,7 @@ fun UiScope.inspectorSection(onInfo: (String) -> Unit) { onInput = { event -> inspectorInputValue = event.value } - } + }, ) } div({ @@ -94,9 +99,11 @@ fun UiScope.inspectorSection(onInfo: (String) -> Unit) { style = { padding = 2.px backgroundColor = 0x22496699 - border { width = 1.px; color = 0x665A9CE0 } + border { + width = 1.px + color = 0x665A9CE0 + } } - }) { text("cell-$index") } diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InteractionsSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InteractionsSection.kt index e40891f..0f197be 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InteractionsSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InteractionsSection.kt @@ -1,20 +1,17 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections -import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.dom.elements.InputType +import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.Event import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.hooks.ref.useRef +import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection -import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_SURFACE_ALT -fun UiScope.interactionsSection( - onInfo: (String) -> Unit, - onLogHook: (String, Event, String?) -> Unit -) { +fun UiScope.interactionsSection(onInfo: (String) -> Unit, onLogHook: (String, Event, String?) -> Unit) { var mouseEnterCount by useState(0) var mouseLeaveCount by useState(0) var mouseOverCount by useState(0) @@ -127,13 +124,16 @@ fun UiScope.interactionsSection( height = 52.px padding = 4.px backgroundColor = DEMO_SURFACE_ALT - border { width = 1.px; color = 0xFF6E7A89.toInt() } + border { + width = 1.px + color = 0xFF6E7A89.toInt() + } } }) { text("Move, click, drag and wheel here") text( "E$mouseEnterCount L$mouseLeaveCount O$mouseOverCount M$mouseMoveCount D$mouseDownCount/$mouseUpCount C$mouseClickCount G$mouseDragCount W$mouseWheelCount", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) } @@ -145,7 +145,8 @@ fun UiScope.interactionsSection( } }) { input( - InputType.Text(placeholder = "onKeyDown/onKeyUp"), { + InputType.Text(placeholder = "onKeyDown/onKeyUp"), + { key = "interactions.key.downUp" style = { width = 100.percent } onKeyDown = { event -> @@ -161,10 +162,11 @@ fun UiScope.interactionsSection( keyUpCount += 1 onLogHook("onKeyUp", event, null) } - } + }, ) input( - InputType.Text(placeholder = "onKeyPressed/onKeyReleased"), { + InputType.Text(placeholder = "onKeyPressed/onKeyReleased"), + { key = "interactions.key.aliases" style = { width = 100.percent } onKeyPressed = { event -> @@ -175,13 +177,13 @@ fun UiScope.interactionsSection( keyReleasedCount += 1 onLogHook("onKeyReleased", event, null) } - } + }, ) } text( "Key counters: down=$keyDownCount up=$keyUpCount pressed=$keyPressedCount released=$keyReleasedCount enter=$enterActionCount", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) div({ @@ -198,11 +200,11 @@ fun UiScope.interactionsSection( cancellationEnabled = !cancellationEnabled onInfo("Interactions: cancellation=$cancellationEnabled") } - } + }, ) text( "Parent=$cancellationParentHits Child=$cancellationChildHits", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) } @@ -216,7 +218,10 @@ fun UiScope.interactionsSection( width = 100.percent padding = 3.px backgroundColor = 0xFF353D46.toInt() - border { width = 1.px; color = 0xFF708090.toInt() } + border { + width = 1.px + color = 0xFF708090.toInt() + } } }) { text("Parent click area") @@ -230,13 +235,16 @@ fun UiScope.interactionsSection( onLogHook( "child.onMouseClick", event, - if (cancellationEnabled) "cancelled=true" else "cancelled=false" + if (cancellationEnabled) "cancelled=true" else "cancelled=false", ) } style = { padding = 3.px backgroundColor = 0xFF4D5560.toInt() - border { width = 1.px; color = 0xFF9AA5B1.toInt() } + border { + width = 1.px + color = 0xFF9AA5B1.toInt() + } } }) { text("Child area") @@ -244,4 +252,3 @@ fun UiScope.interactionsSection( } } } - diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutDebugSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutDebugSection.kt index 8811bdb..61c1078 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutDebugSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutDebugSection.kt @@ -1,12 +1,12 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections -import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.dom.debug.LayoutDebug import org.dreamfinity.dsgl.core.dom.elements.InputType +import org.dreamfinity.dsgl.core.dsl.* +import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection import org.dreamfinity.dsgl.core.style.TextWrap -import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED private const val WRAP_DEBUG_TEXT_A = @@ -14,10 +14,7 @@ private const val WRAP_DEBUG_TEXT_A = private const val WRAP_DEBUG_TEXT_B = "Wrapped text B: this block should always appear below text A with no overlap." -fun UiScope.layoutDebugSection( - onClearLogs: () -> Unit, - onInfo: (String) -> Unit -) { +fun UiScope.layoutDebugSection(onClearLogs: () -> Unit, onInfo: (String) -> Unit) { val minWidth = 96 val maxWidth = 320 var layoutDebugStrict by useState(LayoutDebug.strictBounds) @@ -54,7 +51,7 @@ fun UiScope.layoutDebugSection( LayoutDebug.strictBounds = layoutDebugStrict onInfo("LayoutDebug.strict=$layoutDebugStrict") } - } + }, ) button( if (layoutDebugDraw) "draw bounds: on" else "draw bounds: off", @@ -64,7 +61,7 @@ fun UiScope.layoutDebugSection( LayoutDebug.drawBounds = layoutDebugDraw onInfo("LayoutDebug.drawBounds=$layoutDebugDraw") } - } + }, ) button("clear logs", { onMouseClick = { onClearLogs() } @@ -72,7 +69,7 @@ fun UiScope.layoutDebugSection( } text( "validatorViolations=${LayoutDebug.lastViolationCount} strict=${LayoutDebug.strictBounds} draw=${LayoutDebug.drawBounds}", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) input( @@ -80,7 +77,7 @@ fun UiScope.layoutDebugSection( value = wrapWidth.toLong(), min = minWidth.toLong(), max = maxWidth.toLong(), - step = 2 + step = 2, ), { key = "layoutDebug.wrapWidth" @@ -89,7 +86,7 @@ fun UiScope.layoutDebugSection( val next = (event.parsedValue as? Long) ?: event.value.toLongOrNull() ?: wrapWidth.toLong() layoutDebugWrapWidth = next.coerceIn(minWidth.toLong(), maxWidth.toLong()) } - } + }, ) text("wrap test width=$wrapWidth", { style = { color = DEMO_MUTED } }) @@ -102,9 +99,11 @@ fun UiScope.layoutDebugSection( backgroundColor = 0xFF2D3745.toInt() display = Display.Flex flexDirection = FlexDirection.Column - border { width = 1.px; color = 0xFF70859C.toInt() } + border { + width = 1.px + color = 0xFF70859C.toInt() + } } - }) { text("Case: wrapped text stack", { style = { textWrap = TextWrap.Wrap } }) text(WRAP_DEBUG_TEXT_A, { style = { textWrap = TextWrap.Wrap } }) @@ -118,5 +117,3 @@ fun UiScope.layoutDebugSection( } } } - - diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutStyleSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutStyleSection.kt index adfcffd..5f9bff6 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutStyleSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutStyleSection.kt @@ -1,22 +1,19 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections -import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.Event import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.event.MouseDragEvent import org.dreamfinity.dsgl.core.hooks.ref.useRef +import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection -import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_SURFACE_ALT import kotlin.math.abs -fun UiScope.layoutStyleSection( - onInfo: (String) -> Unit, - onLogHook: (String, Event, String?) -> Unit -) { +fun UiScope.layoutStyleSection(onInfo: (String) -> Unit, onLogHook: (String, Event, String?) -> Unit) { var styleUseMargin by useState(true) var styleUsePadding by useState(true) var styleUseBorder by useState(true) @@ -54,7 +51,7 @@ fun UiScope.layoutStyleSection( }) { text( "Toggle values and click boxes to verify row/column behavior.", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) div({ @@ -71,7 +68,8 @@ fun UiScope.layoutStyleSection( styleLargeGap = !styleLargeGap onInfo("Layout: gap=${if (styleLargeGap) "large" else "compact"}") } - }) + }, + ) button( if (styleFixedSize) "Size: Fixed" else "Size: Auto", { @@ -79,7 +77,7 @@ fun UiScope.layoutStyleSection( styleFixedSize = !styleFixedSize onInfo("Layout: fixedSize=$styleFixedSize") } - } + }, ) } @@ -102,7 +100,10 @@ fun UiScope.layoutStyleSection( height = fixedSize?.px padding = 2.px backgroundColor = 0xFF3A4A5A.toInt() - border { width = 1.px; color = 0xFF5E89B5.toInt() } + border { + width = 1.px + color = 0xFF5E89B5.toInt() + } } }) { text("R${index + 1}") @@ -128,7 +129,10 @@ fun UiScope.layoutStyleSection( width = if (styleFixedSize) 72.px else null padding = 2.px backgroundColor = 0xFF43404F.toInt() - border { width = 1.px; color = 0xFF786AA6.toInt() } + border { + width = 1.px + color = 0xFF786AA6.toInt() + } } }) { text("Column box ${index + 1}") @@ -147,19 +151,19 @@ fun UiScope.layoutStyleSection( if (styleUseMargin) "Margin ON" else "Margin OFF", { onMouseClick = { styleUseMargin = !styleUseMargin } - } + }, ) button( if (styleUsePadding) "Padding ON" else "Padding OFF", { onMouseClick = { styleUsePadding = !styleUsePadding } - } + }, ) button( if (styleUseBorder) "Border ON" else "Border OFF", { onMouseClick = { styleUseBorder = !styleUseBorder } - } + }, ) } @@ -171,15 +175,27 @@ fun UiScope.layoutStyleSection( style = { width = 100.percent backgroundColor = DEMO_SURFACE_ALT - if (styleUseMargin) margin { top = 4.px; right = 0.px; bottom = 0.px; left = 8.px } + if (styleUseMargin) { + margin { + top = 4.px + right = 0.px + bottom = 0.px + left = 8.px + } + } if (styleUsePadding) padding { all(4.px) } - if (styleUseBorder) border { width = 1.px; color = 0xFF90A4AE.toInt() } + if (styleUseBorder) { + border { + width = 1.px + color = 0xFF90A4AE.toInt() + } + } } }) { text("Style target (margin/padding/border)") text( "margin=$styleUseMargin padding=$styleUsePadding border=$styleUseBorder", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) } @@ -197,7 +213,7 @@ fun UiScope.layoutStyleSection( stackOverlayEnabled = !stackOverlayEnabled onInfo("Layout: stackOverlay=$stackOverlayEnabled") } - } + }, ) button("Reset Overlay", { onMouseClick = { @@ -210,7 +226,7 @@ fun UiScope.layoutStyleSection( }) text( "Overlay: $layoutOverlayX,$layoutOverlayY clicks=$overlayClicks", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) } } @@ -223,9 +239,17 @@ fun UiScope.layoutStyleSection( val overlayNode = findNodeInPath(event.target, "layout.stack.overlay") ?: return@onMouseDown layoutOverlayDragging = true overlayDragAnchorXRef.current = - (event.mouseX - overlayNode.bounds.x).coerceIn(0, overlayNode.bounds.width.coerceAtLeast(1)) + (event.mouseX - overlayNode.bounds.x).coerceIn( + 0, + overlayNode.bounds.width + .coerceAtLeast(1), + ) overlayDragAnchorYRef.current = - (event.mouseY - overlayNode.bounds.y).coerceIn(0, overlayNode.bounds.height.coerceAtLeast(1)) + (event.mouseY - overlayNode.bounds.y).coerceIn( + 0, + overlayNode.bounds.height + .coerceAtLeast(1), + ) overlayDragMovedRef.current = false } onMouseDrag = { event -> @@ -237,7 +261,7 @@ fun UiScope.layoutStyleSection( currentX = layoutOverlayX, currentY = layoutOverlayY, anchorX = overlayDragAnchorXRef.current ?: 0, - anchorY = overlayDragAnchorYRef.current ?: 0 + anchorY = overlayDragAnchorYRef.current ?: 0, ) { nextX, nextY, moved -> if (moved) { overlayDragMovedRef.current = true @@ -263,14 +287,22 @@ fun UiScope.layoutStyleSection( width = overlayWidth.px height = overlayHeight.px backgroundColor = 0xCC5A3131.toInt() - margin { top = layoutOverlayY.px; right = 0.px; bottom = 0.px; left = layoutOverlayX.px } + margin { + top = layoutOverlayY.px + right = 0.px + bottom = 0.px + left = layoutOverlayX.px + } padding { all(4.px) } - border { width = 1.px; color = 0xFF8D4848.toInt() } + border { + width = 1.px + color = 0xFF8D4848.toInt() + } } }) { text( if (layoutOverlayDragging) "Overlay (dragging...)" else "Overlay (drag me)", - { style = { color = 0xFFF5F7FA.toInt() } } + { style = { color = 0xFFF5F7FA.toInt() } }, ) } } @@ -286,7 +318,7 @@ private fun updateOverlayDrag( currentY: Int, anchorX: Int, anchorY: Int, - onUpdate: (nextX: Int, nextY: Int, moved: Boolean) -> Unit + onUpdate: (nextX: Int, nextY: Int, moved: Boolean) -> Unit, ) { if (!isDragging) return val stackNode = findNodeInPath(event.target, "section.layoutStyle.stack") ?: return diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/McFeaturesSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/McFeaturesSection.kt index 1b8b17d..f1d7b1b 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/McFeaturesSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/McFeaturesSection.kt @@ -1,14 +1,14 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections import org.dreamfinity.dsgl.core.DsglColors -import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.dom.elements.InputType +import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.Event +import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.core.style.AlignItems import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection import org.dreamfinity.dsgl.core.style.JustifyContent -import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.mcForge1710.McItemStackRef import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED import kotlin.math.roundToLong @@ -28,7 +28,7 @@ data class McFeaturesShellProps( val guiScaleLabel: (Int) -> String, val setGuiScale: (Int) -> Unit, val cycleGuiScale: (Int) -> Unit, - val onLogHook: (String, Event, String?) -> Unit + val onLogHook: (String, Event, String?) -> Unit, ) fun UiScope.mcFeaturesSection(props: McFeaturesShellProps) { @@ -36,7 +36,9 @@ fun UiScope.mcFeaturesSection(props: McFeaturesShellProps) { var itemRotX by useState(-11.0) fun itemRotYLong(): Long = itemRotY.roundToLong().coerceIn(0L, 360L) + fun itemRotXLong(): Long = itemRotX.roundToLong().coerceIn(-89L, 89L) + fun adjustItemRotation(deltaY: Double = 0.0, deltaX: Double = 0.0) { var normalized = (itemRotY + deltaY) % 360.0 if (normalized < 0.0) normalized += 360.0 @@ -55,12 +57,14 @@ fun UiScope.mcFeaturesSection(props: McFeaturesShellProps) { } }) { text( - "DSGL viewport=${props.viewportWidthPx}x${props.viewportHeightPx}px, guiScale=${props.guiScaleLabel(guiScaleValue)}", - { style = { color = DsglColors.WHITE } } + "DSGL viewport=${props.viewportWidthPx}x${props.viewportHeightPx}px, guiScale=${props.guiScaleLabel( + guiScaleValue, + )}", + { style = { color = DsglColors.WHITE } }, ) text( "Change guiScale below: vanilla UI changes, DSGL layout should stay pixel-stable.", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) div({ @@ -116,7 +120,10 @@ fun UiScope.mcFeaturesSection(props: McFeaturesShellProps) { width = 100.percent maxWidth = 360.px padding = 4.px - border { width = 1.px; color = 0xFF5D6A76.toInt() } + border { + width = 1.px + color = 0xFF5D6A76.toInt() + } backgroundColor = 0xFF1A222A.toInt() gap = 4.px display = Display.Flex @@ -136,7 +143,10 @@ fun UiScope.mcFeaturesSection(props: McFeaturesShellProps) { style = { width = 18.px height = 10.px - border { width = 1.px; color = 0xFF3F4B56.toInt() } + border { + width = 1.px + color = 0xFF3F4B56.toInt() + } backgroundColor = if ((row + col) % 2 == 0) 0xFF1F2D38.toInt() else 0xFF243544.toInt() } }) {} @@ -158,7 +168,10 @@ fun UiScope.mcFeaturesSection(props: McFeaturesShellProps) { width = 160.px height = 102.px padding = 4.px - border { width = 1.px; color = 0xFF6A7784.toInt() } + border { + width = 1.px + color = 0xFF6A7784.toInt() + } backgroundColor = 0xFF111922.toInt() gap = 3.px display = Display.Flex @@ -247,7 +260,7 @@ fun UiScope.mcFeaturesSection(props: McFeaturesShellProps) { text("Image sources: resource + file:// + http(s):// cached path.") text( "mediaReady=${props.mediaReady} file=${if (props.mediaReady) "prepared" else "failed"}", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) div({ @@ -271,7 +284,10 @@ fun UiScope.mcFeaturesSection(props: McFeaturesShellProps) { style = { width = 36.px height = 36.px - border { width = 1.px; color = 0xFF66737F.toInt() } + border { + width = 1.px + color = 0xFF66737F.toInt() + } } }) } @@ -289,7 +305,10 @@ fun UiScope.mcFeaturesSection(props: McFeaturesShellProps) { style = { width = 36.px height = 36.px - border { width = 1.px; color = 0xFF66737F.toInt() } + border { + width = 1.px + color = 0xFF66737F.toInt() + } } }) } @@ -307,7 +326,10 @@ fun UiScope.mcFeaturesSection(props: McFeaturesShellProps) { style = { width = 36.px height = 36.px - border { width = 1.px; color = 0xFF66737F.toInt() } + border { + width = 1.px + color = 0xFF66737F.toInt() + } } }) } @@ -327,7 +349,10 @@ fun UiScope.mcFeaturesSection(props: McFeaturesShellProps) { key = "mc.item.2d" style = { width = 64.px - border { width = 1.px; color = 0xFF586A7A.toInt() } + border { + width = 1.px + color = 0xFF586A7A.toInt() + } } }) itemStack(props.blockItemRef, { @@ -337,7 +362,10 @@ fun UiScope.mcFeaturesSection(props: McFeaturesShellProps) { key = "mc.item.3d" style = { width = 70.px - border { width = 1.px; color = 0xFF586A7A.toInt() } + border { + width = 1.px + color = 0xFF586A7A.toInt() + } } }) } @@ -345,14 +373,14 @@ fun UiScope.mcFeaturesSection(props: McFeaturesShellProps) { text("Rotation controls (drag sliders or use step buttons)") text( "Drag outside slider bounds: value should keep updating until mouse up.", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) input( InputType.Range( value = itemRotYLong(), min = 0, max = 360, - step = 5 + step = 5, ), { key = "mc.rotation.slider.yaw" @@ -373,7 +401,7 @@ fun UiScope.mcFeaturesSection(props: McFeaturesShellProps) { itemRotY = next.toDouble() props.onLogHook("mc.rotY.onChange", event, "rotY=${itemRotYLong()}") } - } + }, ) input( @@ -381,7 +409,7 @@ fun UiScope.mcFeaturesSection(props: McFeaturesShellProps) { value = itemRotXLong(), min = -89, max = 89, - step = 1 + step = 1, ), { key = "mc.rotation.slider.pitch" @@ -402,7 +430,7 @@ fun UiScope.mcFeaturesSection(props: McFeaturesShellProps) { itemRotX = next.toDouble() props.onLogHook("mc.rotX.onChange", event, "rotX=${itemRotXLong()}") } - } + }, ) div({ @@ -434,7 +462,7 @@ fun UiScope.mcFeaturesSection(props: McFeaturesShellProps) { text( "rotY=${itemRotYLong()} rotX=${itemRotXLong()}", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) } } diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ModalsSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ModalsSection.kt index ee9032c..717c991 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ModalsSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ModalsSection.kt @@ -1,10 +1,16 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections +import org.dreamfinity.dsgl.core.components.modal.BackdropMode +import org.dreamfinity.dsgl.core.components.modal.ModalSize +import org.dreamfinity.dsgl.core.components.modal.ModalSpec +import org.dreamfinity.dsgl.core.components.modal.modalBody +import org.dreamfinity.dsgl.core.components.modal.modalFooter +import org.dreamfinity.dsgl.core.components.modal.modalHeader +import org.dreamfinity.dsgl.core.components.modal.modalTitle import org.dreamfinity.dsgl.core.dsl.* -import org.dreamfinity.dsgl.core.components.modal.* +import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection -import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED fun UiScope.modalsSection( @@ -13,7 +19,7 @@ fun UiScope.modalsSection( onRemoveModal: (String) -> Unit, onPopTopModal: () -> Unit, onClearModals: () -> Unit, - onInfo: (String) -> Unit + onInfo: (String) -> Unit, ) { var modalBackgroundCounter by useState(0) @@ -76,26 +82,31 @@ fun UiScope.modalsSection( } text({ - val stack = if (modals.isEmpty()) "[]" else modals.joinToString( - prefix = "[", - postfix = "]" - ) { it.key } + val stack = + if (modals.isEmpty()) { + "[]" + } else { + modals.joinToString( + prefix = "[", + postfix = "]", + ) { it.key } + } value = "Stack=$stack" style = { color = DEMO_MUTED } }) text( "Background counter=$modalBackgroundCounter", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) } } -private fun basicModal(onRemoveModal: (String) -> Unit): ModalSpec { - return ModalSpec( +private fun basicModal(onRemoveModal: (String) -> Unit): ModalSpec = + ModalSpec( key = "modal.basic", backdrop = BackdropMode.True, keyboard = true, - onHide = { onRemoveModal("modal.basic") } + onHide = { onRemoveModal("modal.basic") }, ) { scope -> modalHeader(closeButton = true, onHide = scope.dismiss) { modalTitle("Basic Modal") @@ -109,14 +120,13 @@ private fun basicModal(onRemoveModal: (String) -> Unit): ModalSpec { }) } } -} -private fun staticModal(onRemoveModal: (String) -> Unit): ModalSpec { - return ModalSpec( +private fun staticModal(onRemoveModal: (String) -> Unit): ModalSpec = + ModalSpec( key = "modal.static", backdrop = BackdropMode.Static, keyboard = false, - onHide = { onRemoveModal("modal.static") } + onHide = { onRemoveModal("modal.static") }, ) { scope -> modalHeader(closeButton = true, onHide = scope.dismiss) { modalTitle("Static Backdrop") @@ -131,14 +141,13 @@ private fun staticModal(onRemoveModal: (String) -> Unit): ModalSpec { }) } } -} -private fun largeCenteredModal(onRemoveModal: (String) -> Unit): ModalSpec { - return ModalSpec( +private fun largeCenteredModal(onRemoveModal: (String) -> Unit): ModalSpec = + ModalSpec( key = "modal.large", size = ModalSize.Lg, centered = true, - onHide = { onRemoveModal("modal.large") } + onHide = { onRemoveModal("modal.large") }, ) { scope -> modalHeader(closeButton = true, onHide = scope.dismiss) { modalTitle("Large Centered") @@ -153,15 +162,11 @@ private fun largeCenteredModal(onRemoveModal: (String) -> Unit): ModalSpec { }) } } -} -private fun flowStep1Modal( - onPushModal: (ModalSpec) -> Unit, - onRemoveModal: (String) -> Unit -): ModalSpec { - return ModalSpec( +private fun flowStep1Modal(onPushModal: (ModalSpec) -> Unit, onRemoveModal: (String) -> Unit): ModalSpec = + ModalSpec( key = "modal.flow.1", - onHide = { onRemoveModal("modal.flow.1") } + onHide = { onRemoveModal("modal.flow.1") }, ) { scope -> modalHeader(closeButton = true, onHide = scope.dismiss) { modalTitle("Flow Step 1") @@ -180,13 +185,12 @@ private fun flowStep1Modal( }) } } -} -private fun flowStep2Modal(onRemoveModal: (String) -> Unit): ModalSpec { - return ModalSpec( +private fun flowStep2Modal(onRemoveModal: (String) -> Unit): ModalSpec = + ModalSpec( key = "modal.flow.2", centered = true, - onHide = { onRemoveModal("modal.flow.2") } + onHide = { onRemoveModal("modal.flow.2") }, ) { scope -> modalHeader(closeButton = true, onHide = scope.dismiss) { modalTitle("Flow Step 2") @@ -200,4 +204,3 @@ private fun flowStep2Modal(onRemoveModal: (String) -> Unit): ModalSpec { }) } } -} diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/MsdfFontsSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/MsdfFontsSection.kt index bba9971..5872faa 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/MsdfFontsSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/MsdfFontsSection.kt @@ -1,8 +1,9 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections -import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.dom.elements.InputType +import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.font.FontRegistry +import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection import org.dreamfinity.dsgl.core.style.FontStyle @@ -10,16 +11,16 @@ import org.dreamfinity.dsgl.core.style.FontWeight import org.dreamfinity.dsgl.core.style.TextDecoration import org.dreamfinity.dsgl.core.style.TextFormatting import org.dreamfinity.dsgl.core.style.TextWrap -import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED import org.dreamfinity.dsgl.mcForge1710.text.MsdfRuntimeDebugSettings -private val COLOR_PRESETS = listOf( - 0xFFFFFFFF.toInt(), - 0xFFFFC857.toInt(), - 0xFF8EE3F5.toInt(), - 0xFFFF7E67.toInt() -) +private val COLOR_PRESETS = + listOf( + 0xFFFFFFFF.toInt(), + 0xFFFFC857.toInt(), + 0xFF8EE3F5.toInt(), + 0xFFFF7E67.toInt(), + ) private const val SAMPLE_PARAGRAPH = "MSDF/MTSDF text rendering demo in DSGL. This paragraph should wrap cleanly in a fixed-width panel and respect font switches, opacity, and size." @@ -66,7 +67,7 @@ fun UiScope.msdfFontsSection(onInfo: (String) -> Unit) { text("MSDF Fonts") text( "All DSGL DrawText commands go through MSDF/MTSDF rendering. Switch font/size/color/opacity and verify wrapping.", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) text("DREAMFINITY", { style = { fontId = "telegrafico" } }) @@ -104,7 +105,7 @@ fun UiScope.msdfFontsSection(onInfo: (String) -> Unit) { msdfParseMinecraftFormatting = !msdfParseMinecraftFormatting onInfo("MSDF formatting=$msdfParseMinecraftFormatting") } - } + }, ) button( if (msdfShowBaselineGuides) { @@ -118,11 +119,11 @@ fun UiScope.msdfFontsSection(onInfo: (String) -> Unit) { MsdfRuntimeDebugSettings.decorationGuidesEnabled = msdfShowBaselineGuides System.setProperty( "dsgl.msdf.debug.decorations", - msdfShowBaselineGuides.toString() + msdfShowBaselineGuides.toString(), ) onInfo("MSDF guides=$msdfShowBaselineGuides") } - } + }, ) } @@ -131,7 +132,7 @@ fun UiScope.msdfFontsSection(onInfo: (String) -> Unit) { value = msdfOpacityPercent, min = 0, max = 100, - step = 1 + step = 1, ), { key = "msdf.opacity" @@ -140,14 +141,14 @@ fun UiScope.msdfFontsSection(onInfo: (String) -> Unit) { val next = (event.parsedValue as? Long) ?: event.value.toLongOrNull() ?: msdfOpacityPercent msdfOpacityPercent = next.coerceIn(0, 100) } - } + }, ) input( InputType.Range( value = msdfFontSizePx, min = 6, max = 48, - step = 1 + step = 1, ), { key = "msdf.fontSize" @@ -156,14 +157,14 @@ fun UiScope.msdfFontsSection(onInfo: (String) -> Unit) { val next = (event.parsedValue as? Long) ?: event.value.toLongOrNull() ?: msdfFontSizePx msdfFontSizePx = next.coerceIn(6, 48) } - } + }, ) input( InputType.Range( value = panelWidthPercent, min = 0L, max = 100L, - step = 2 + step = 2, ), { key = "msdf.wrapWidth" @@ -172,12 +173,12 @@ fun UiScope.msdfFontsSection(onInfo: (String) -> Unit) { val next = (event.parsedValue as? Long) ?: event.value.toLongOrNull() ?: panelWidthPercent msdfWrapWidthPercent = next.coerceIn(0L, 100L) } - } + }, ) text( "fontId=${selectedFont.fontId} source=${selectedFont.source.name.lowercase()} fontSize=$fontSize opacity=$textOpacity panelWidth=$panelWidthPercent% formatting=${formattingMode.name.lowercase()} guides=$msdfShowBaselineGuides", - { style = { this.color = DEMO_MUTED } } + { style = { this.color = DEMO_MUTED } }, ) text( "Drop external font packages into /dsgl/fonts//.ttf + -meta.json + -mtsdf.png and restart.", @@ -186,17 +187,19 @@ fun UiScope.msdfFontsSection(onInfo: (String) -> Unit) { color = DEMO_MUTED textWrap = TextWrap.Wrap } - } + }, ) text({ - val preview = selectableFonts - .take(8) - .joinToString(", ") { "${it.fontId}[${it.source.name.lowercase()}]" } - value = if (selectableFonts.size > 8) { - "Registered fonts (${selectableFonts.size}): $preview ..." - } else { - "Registered fonts (${selectableFonts.size}): $preview" - } + val preview = + selectableFonts + .take(8) + .joinToString(", ") { "${it.fontId}[${it.source.name.lowercase()}]" } + value = + if (selectableFonts.size > 8) { + "Registered fonts (${selectableFonts.size}): $preview ..." + } else { + "Registered fonts (${selectableFonts.size}): $preview" + } style = { color = DEMO_MUTED @@ -213,7 +216,10 @@ fun UiScope.msdfFontsSection(onInfo: (String) -> Unit) { backgroundColor = 0xFF233040.toInt() display = Display.Flex flexDirection = FlexDirection.Column - border { width = 1.px; color = 0xFF5F7288.toInt() } + border { + width = 1.px + color = 0xFF5F7288.toInt() + } } }) { text("Header text", { diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/OverflowScrollSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/OverflowScrollSection.kt index ae26573..aa2aab6 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/OverflowScrollSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/OverflowScrollSection.kt @@ -1,26 +1,28 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections -import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.dom.elements.InputType +import org.dreamfinity.dsgl.core.dsl.* +import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection import org.dreamfinity.dsgl.core.style.Overflow -import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED -private val OVERFLOW_MODES = listOf( - Overflow.Visible, - Overflow.Hidden, - Overflow.Scroll, - Overflow.Auto -) +private val OVERFLOW_MODES = + listOf( + Overflow.Visible, + Overflow.Hidden, + Overflow.Scroll, + Overflow.Auto, + ) -private fun Overflow.label(): String = when (this) { - Overflow.Visible -> "visible" - Overflow.Hidden -> "hidden" - Overflow.Scroll -> "scroll" - Overflow.Auto -> "auto" -} +private fun Overflow.label(): String = + when (this) { + Overflow.Visible -> "visible" + Overflow.Hidden -> "hidden" + Overflow.Scroll -> "scroll" + Overflow.Auto -> "auto" + } private fun nextOverflow(current: Overflow): Overflow { val idx = OVERFLOW_MODES.indexOf(current) @@ -64,7 +66,7 @@ fun UiScope.overflowScrollSection(onInfo: (String) -> Unit) { text("Overflow/scroll viewport playground") text( "Scrollbar presence is state-only for now, but gutters already reduce viewport size.", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) div({ @@ -107,7 +109,7 @@ fun UiScope.overflowScrollSection(onInfo: (String) -> Unit) { value = viewportWidth.toLong(), min = viewportMinWidth.toLong(), max = viewportMaxWidth.toLong(), - step = 2 + step = 2, ), { key = "section.overflowScroll.viewportWidth" @@ -116,7 +118,7 @@ fun UiScope.overflowScrollSection(onInfo: (String) -> Unit) { val next = (event.parsedValue as? Long) ?: event.value.toLongOrNull() ?: viewportWidth.toLong() overflowDemoViewportWidth = next.coerceIn(viewportMinWidth.toLong(), viewportMaxWidth.toLong()) } - } + }, ) text("Viewport width = $viewportWidth", { style = { color = DEMO_MUTED } }) @@ -125,7 +127,7 @@ fun UiScope.overflowScrollSection(onInfo: (String) -> Unit) { value = viewportHeight.toLong(), min = viewportMinHeight.toLong(), max = viewportMaxHeight.toLong(), - step = 2 + step = 2, ), { key = "section.overflowScroll.viewportHeight" @@ -134,7 +136,7 @@ fun UiScope.overflowScrollSection(onInfo: (String) -> Unit) { val next = (event.parsedValue as? Long) ?: event.value.toLongOrNull() ?: viewportHeight.toLong() overflowDemoViewportHeight = next.coerceIn(viewportMinHeight.toLong(), viewportMaxHeight.toLong()) } - } + }, ) text("Viewport height = $viewportHeight", { style = { color = DEMO_MUTED } }) @@ -143,7 +145,7 @@ fun UiScope.overflowScrollSection(onInfo: (String) -> Unit) { value = demoContentWidth.toLong(), min = contentMinWidth.toLong(), max = contentMaxWidth.toLong(), - step = 2 + step = 2, ), { key = "section.overflowScroll.contentWidth" @@ -152,7 +154,7 @@ fun UiScope.overflowScrollSection(onInfo: (String) -> Unit) { val next = (event.parsedValue as? Long) ?: event.value.toLongOrNull() ?: demoContentWidth.toLong() overflowDemoContentWidth = next.coerceIn(contentMinWidth.toLong(), contentMaxWidth.toLong()) } - } + }, ) text("Content width = $demoContentWidth", { style = { color = DEMO_MUTED } }) @@ -161,7 +163,7 @@ fun UiScope.overflowScrollSection(onInfo: (String) -> Unit) { value = demoContentHeight.toLong(), min = contentMinHeight.toLong(), max = contentMaxHeight.toLong(), - step = 2 + step = 2, ), { key = "section.overflowScroll.contentHeight" @@ -170,13 +172,13 @@ fun UiScope.overflowScrollSection(onInfo: (String) -> Unit) { val next = (event.parsedValue as? Long) ?: event.value.toLongOrNull() ?: demoContentHeight.toLong() overflowDemoContentHeight = next.coerceIn(contentMinHeight.toLong(), contentMaxHeight.toLong()) } - } + }, ) text("Content height = $demoContentHeight", { style = { color = DEMO_MUTED } }) text( "Clicks: visible=$overflowDemoVisibleClicks edge=$overflowDemoEdgeClicks (edge click only when visible)", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) overflowDemoCard( @@ -196,7 +198,7 @@ fun UiScope.overflowScrollSection(onInfo: (String) -> Unit) { onEdgeClick = { overflowDemoEdgeClicks += 1 onInfo("Overflow demo edge click") - } + }, ) overflowDemoCard( @@ -210,7 +212,7 @@ fun UiScope.overflowScrollSection(onInfo: (String) -> Unit) { overflowY = Overflow.Scroll, keyPrefix = "section.overflowScroll.preset.scroll", onVisibleClick = {}, - onEdgeClick = {} + onEdgeClick = {}, ) overflowDemoCard( @@ -224,7 +226,7 @@ fun UiScope.overflowScrollSection(onInfo: (String) -> Unit) { overflowY = Overflow.Auto, keyPrefix = "section.overflowScroll.preset.cross", onVisibleClick = {}, - onEdgeClick = {} + onEdgeClick = {}, ) } } @@ -240,7 +242,7 @@ private fun UiScope.overflowDemoCard( overflowY: Overflow, keyPrefix: String, onVisibleClick: () -> Unit, - onEdgeClick: () -> Unit + onEdgeClick: () -> Unit, ) { div({ key = "$keyPrefix.card" @@ -251,14 +253,17 @@ private fun UiScope.overflowDemoCard( gap = 2.px padding = 2.px backgroundColor = 0xFF2B3440.toInt() - border { width = 1.px; color = 0xFF5E7286.toInt() } + border { + width = 1.px + color = 0xFF5E7286.toInt() + } } }) { text(title) text(note, { style = { color = DEMO_MUTED } }) text( "viewport=${viewportWidth}x$viewportHeight content=${contentWidth}x$contentHeight overflow-x=${overflowX.label()} overflow-y=${overflowY.label()}", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) div({ @@ -268,7 +273,10 @@ private fun UiScope.overflowDemoCard( height = viewportHeight.px this.overflowX = overflowX this.overflowY = overflowY - border { width = 1.px; color = 0xFF8AA0B5.toInt() } + border { + width = 1.px + color = 0xFF8AA0B5.toInt() + } backgroundColor = 0xFF23303D.toInt() padding = 2.px } @@ -281,7 +289,10 @@ private fun UiScope.overflowDemoCard( display = Display.Flex flexDirection = FlexDirection.Column gap = 1.px - border { width = 1.px; color = 0xFF7992AA.toInt() } + border { + width = 1.px + color = 0xFF7992AA.toInt() + } backgroundColor = 0xFF384C60.toInt() padding = 2.px } diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/OverviewSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/OverviewSection.kt index d4ef336..7e5e96b 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/OverviewSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/OverviewSection.kt @@ -1,19 +1,19 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections import org.dreamfinity.dsgl.core.dsl.* +import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection -import org.dreamfinity.dsgl.core.hooks.useState -import org.dreamfinity.dsgl.mcForge1710.demo.support.CapabilityId import org.dreamfinity.dsgl.mcForge1710.demo.support.CapabilityChecklistCatalog import org.dreamfinity.dsgl.mcForge1710.demo.support.CapabilityGroup +import org.dreamfinity.dsgl.mcForge1710.demo.support.CapabilityId import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_OK fun UiScope.overviewSection( implementedCapabilities: Set, onManualInvalidate: (String) -> Unit, - onInfo: (String) -> Unit + onInfo: (String) -> Unit, ) { var manualInvalidateCount by useState(0) var lastManualReason by useState("none") @@ -77,14 +77,16 @@ fun UiScope.overviewSection( text("Checklist groups", { style = { color = DEMO_OK } }) CapabilityGroup.entries.forEach { group -> - val required = CapabilityChecklistCatalog.required.filter { it.group == group }.size + val required = + CapabilityChecklistCatalog.required + .filter { it.group == group } + .size val implemented = implementedCapabilities.count { it.group == group } val ok = implemented == required text( "${group.title}: $implemented/$required", - { style = { color = if (ok) DEMO_OK else 0xFFE06A6A.toInt() } } + { style = { color = if (ok) DEMO_OK else 0xFFE06A6A.toInt() } }, ) } } } - diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutSection.kt index 30264f6..3c99eac 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutSection.kt @@ -1,19 +1,20 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections -import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.dom.elements.InputOption import org.dreamfinity.dsgl.core.dom.elements.InputType -import org.dreamfinity.dsgl.core.style.* +import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.hooks.useState +import org.dreamfinity.dsgl.core.style.* import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED -private val POSITION_MODE_OPTIONS = listOf( - PositionMode.Static, - PositionMode.Relative, - PositionMode.Absolute, - PositionMode.Fixed, - PositionMode.Sticky -) +private val POSITION_MODE_OPTIONS = + listOf( + PositionMode.Static, + PositionMode.Relative, + PositionMode.Absolute, + PositionMode.Fixed, + PositionMode.Sticky, + ) private const val OFFSET_MIN = -72 private const val OFFSET_MAX = 120 @@ -65,7 +66,6 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { val fixedBaseLeft = (viewportWidthPx - 236).coerceAtLeast(104) val fixedBaseTop = 72 - div({ key = "section.positionedLayout" style = { @@ -80,7 +80,7 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { text("Positioned layout verification surface: static/relative/absolute/fixed + z-index + scroll + hit-testing") text( "Top-level fixed and root-anchored absolute badges are intentional: they prove root-space anchoring.", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) controls( @@ -132,13 +132,16 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { positionedDemoScrollClicks = 0 positionedDemoStickyTopClicks = 0 positionedDemoStickyCombinedClicks = 0 - } - ) + }, + ), ) div({ style = { - border { width = 1.px; color = 0xFF6A7D8F.toInt() } + border { + width = 1.px + color = 0xFF6A7D8F.toInt() + } padding = 5.px } }) { @@ -147,7 +150,10 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { key = "positioned.mode.playground" style = { position = PositionMode.Relative - border { width = 1.px; color = 0xFF6A7D8F.toInt() } + border { + width = 1.px + color = 0xFF6A7D8F.toInt() + } backgroundColor = 0xFF2A3440.toInt() padding = 5.px } @@ -158,22 +164,27 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { onMouseClick = { positionedDemoLastClick = "mode-$demoMode" } style = { position = demoMode - left = if (positionedDemoUseLeft) { - leftOffset.px - } else { - null - } + left = + if (positionedDemoUseLeft) { + leftOffset.px + } else { + null + } right = rightOffset.px - top = if (positionedDemoUseTop) { - topOffset.px - } else { - null - } + top = + if (positionedDemoUseTop) { + topOffset.px + } else { + null + } bottom = bottomOffset.px padding = 5.px zIndex = zBlue backgroundColor = 0xCC476487.toInt() - border { width = 1.px; color = 0xFFBFD8EE.toInt() } + border { + width = 1.px + color = 0xFFBFD8EE.toInt() + } } }) { text("mode=$demoMode") @@ -184,7 +195,10 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { div({ style = { - border { width = 1.px; color = 0xFF6A7D8F.toInt() } + border { + width = 1.px + color = 0xFF6A7D8F.toInt() + } padding = 5.px } }) { @@ -196,7 +210,10 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { flexDirection = FlexDirection.Column gap = 2.px padding = 5.px - border { width = 1.px; color = 0xFF667A8D.toInt() } + border { + width = 1.px + color = 0xFF667A8D.toInt() + } backgroundColor = 0xFF2A313A.toInt() } }) { @@ -204,7 +221,10 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { style = { padding = 5.px backgroundColor = 0xFF364352.toInt() - border { width = 1.px; color = 0xFF7E97AD.toInt() } + border { + width = 1.px + color = 0xFF7E97AD.toInt() + } } }) { text("Flow item before") } div({ @@ -221,21 +241,30 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { bottom = bottomOffset.px padding = 5.px backgroundColor = 0xFF3F5A73.toInt() - border { width = 1.px; color = 0xFF9DB7CF.toInt() } + border { + width = 1.px + color = 0xFF9DB7CF.toInt() + } } }) { text("position: static + offsets (still in normal slot)") } div({ style = { padding = 5.px backgroundColor = 0xFF364352.toInt() - border { width = 1.px; color = 0xFF7E97AD.toInt() } + border { + width = 1.px + color = 0xFF7E97AD.toInt() + } } }) { text("Flow item after") } } } div({ style = { - border { width = 1.px; color = 0xFF6A7D8F.toInt() } + border { + width = 1.px + color = 0xFF6A7D8F.toInt() + } padding = 5.px } }) { @@ -247,7 +276,10 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { flexDirection = FlexDirection.Row gap = 3.px padding = 5.px - border { width = 1.px; color = 0xFF6D7C8A.toInt() } + border { + width = 1.px + color = 0xFF6D7C8A.toInt() + } backgroundColor = 0xFF29323D.toInt() } }) { @@ -255,7 +287,10 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { style = { padding = 5.px backgroundColor = 0xFF455B70.toInt() - border { width = 1.px; color = 0xFF91A9BF.toInt() } + border { + width = 1.px + color = 0xFF91A9BF.toInt() + } } }) { text("left") } div({ @@ -264,21 +299,26 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { onMouseClick = { positionedDemoLastClick = "relative-target" } style = { position = PositionMode.Relative - left = if (positionedDemoUseLeft) { - leftOffset.px - } else { - null - } + left = + if (positionedDemoUseLeft) { + leftOffset.px + } else { + null + } right = rightOffset.px - top = if (positionedDemoUseTop) { - topOffset.px - } else { - null - } + top = + if (positionedDemoUseTop) { + topOffset.px + } else { + null + } bottom = bottomOffset.px padding = 5.px backgroundColor = 0xFF4A6A87.toInt() - border { width = 1.px; color = 0xFFB5CFE6.toInt() } + border { + width = 1.px + color = 0xFFB5CFE6.toInt() + } zIndex = zBlue } }) { text("relative target") } @@ -286,7 +326,10 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { style = { padding = 5.px backgroundColor = 0xFF455B70.toInt() - border { width = 1.px; color = 0xFF91A9BF.toInt() } + border { + width = 1.px + color = 0xFF91A9BF.toInt() + } } }) { text("right") } } @@ -294,7 +337,10 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { div({ style = { - border { width = 1.px; color = 0xFF6A7D8F.toInt() } + border { + width = 1.px + color = 0xFF6A7D8F.toInt() + } padding = 5.px } }) { @@ -306,20 +352,27 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { flexDirection = FlexDirection.Column gap = 2.px padding = 5.px - border { width = 1.px; color = 0xFF6A7E8C.toInt() } + border { + width = 1.px + color = 0xFF6A7E8C.toInt() + } backgroundColor = 0xFF2B333E.toInt() } }) { text( "Root-anchored absolute badge appears outside this card (near top-middle).", - { style = { color = DEMO_MUTED } }) + { style = { color = DEMO_MUTED } }, + ) div({ key = "positioned.absolute.container" style = { position = PositionMode.Relative overflowY = Overflow.Auto padding = 5.px - border { width = 1.px; color = 0xFF8AA0B4.toInt() } + border { + width = 1.px + color = 0xFF8AA0B4.toInt() + } backgroundColor = 0xFF344252.toInt() } }) { @@ -331,21 +384,26 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { } style = { position = PositionMode.Absolute - left = if (positionedDemoUseLeft) { - leftOffset.px - } else { - null - } + left = + if (positionedDemoUseLeft) { + leftOffset.px + } else { + null + } right = rightOffset.px - top = if (positionedDemoUseTop) { - topOffset.px - } else { - null - } + top = + if (positionedDemoUseTop) { + topOffset.px + } else { + null + } bottom = bottomOffset.px padding = 5.px backgroundColor = 0xAA2B4E73.toInt() - border { width = 1.px; color = 0xFFD2E8FF.toInt() } + border { + width = 1.px + color = 0xFFD2E8FF.toInt() + } zIndex = zGreen } }) { text("absolute in relative ancestor") } @@ -358,7 +416,10 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { div({ style = { - border { width = 1.px; color = 0xFF6A7D8F.toInt() } + border { + width = 1.px + color = 0xFF6A7D8F.toInt() + } padding = 5.px } }) { @@ -372,7 +433,10 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { top = rootAnchoredTop.px padding = 5.px backgroundColor = 0xCC2C5A89.toInt() - border { width = 1.px; color = 0xFFB9D9FA.toInt() } + border { + width = 1.px + color = 0xFFB9D9FA.toInt() + } zIndex = 18 } }) { @@ -386,7 +450,10 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { position = PositionMode.Relative overflowY = Overflow.Auto padding = 5.px - border { width = 1.px; color = 0xFF73879A.toInt() } + border { + width = 1.px + color = 0xFF73879A.toInt() + } backgroundColor = 0xFF27323E.toInt() display = Display.Flex flexDirection = FlexDirection.Column @@ -402,7 +469,10 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { top = (topOffset / 2).px padding = 5.px backgroundColor = 0xFF496B8A.toInt() - border { width = 1.px; color = 0xFFB6D5EE.toInt() } + border { + width = 1.px + color = 0xFFB6D5EE.toInt() + } } }) { text("relative in scroller") @@ -415,7 +485,10 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { top = 34.px padding = 5.px backgroundColor = 0xAA345469.toInt() - border { width = 1.px; color = 0xFFCFE6F4.toInt() } + border { + width = 1.px + color = 0xFFCFE6F4.toInt() + } zIndex = 7 } }) { text("absolute in scroller") } @@ -429,7 +502,7 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { positionedDemoLastClick = "scroll-action" } }) - text("scroll action clicks=${positionedDemoScrollClicks}", { style = { color = DEMO_MUTED } }) + text("scroll action clicks=$positionedDemoScrollClicks", { style = { color = DEMO_MUTED } }) } } @@ -439,21 +512,26 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { onMouseClick = { positionedDemoLastClick = "fixed-badge" } style = { position = PositionMode.Fixed - left = if (positionedDemoUseLeft) { - (fixedBaseLeft + leftOffset).coerceAtLeast(0).px - } else { - null - } + left = + if (positionedDemoUseLeft) { + (fixedBaseLeft + leftOffset).coerceAtLeast(0).px + } else { + null + } right = rightOffset.px - top = if (positionedDemoUseTop) { - (fixedBaseTop + topOffset).coerceAtLeast(0).px - } else { - null - } + top = + if (positionedDemoUseTop) { + (fixedBaseTop + topOffset).coerceAtLeast(0).px + } else { + null + } bottom = bottomOffset.px padding = 5.px backgroundColor = 0xCC4F3C73.toInt() - border { width = 1.px; color = 0xFFE3D5F6.toInt() } + border { + width = 1.px + color = 0xFFE3D5F6.toInt() + } zIndex = 28 } }) { @@ -463,31 +541,36 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { div({ key = "positioned.sticky.surface" style = { - border { width = 1.px; color = 0xFF6A7D8F.toInt() } + border { + width = 1.px + color = 0xFF6A7D8F.toInt() + } padding = 5.px display = Display.Flex flexDirection = FlexDirection.Column gap = 3.px } }) { - text("H. Sticky: in-flow slot + visual stick with per-axis nearest scroll container and direct-parent clamp.") + text( + "H. Sticky: in-flow slot + visual stick with per-axis nearest scroll container and direct-parent clamp.", + ) text( "Inspector target key: positioned.sticky.xy.target", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) stickyVerticalGroup( onSetLastHover = { positionedDemoLastHover = it }, onSetLastClick = { positionedDemoLastClick = it }, stickyTopClicks = positionedDemoStickyTopClicks, - onStickyTopClick = { positionedDemoStickyTopClicks += 1 } + onStickyTopClick = { positionedDemoStickyTopClicks += 1 }, ) stickyHorizontalGroup() stickyXYGroup( onSetLastHover = { positionedDemoLastHover = it }, onSetLastClick = { positionedDemoLastClick = it }, stickyCombinedClicks = positionedDemoStickyCombinedClicks, - onStickyCombinedClick = { positionedDemoStickyCombinedClicks += 1 } + onStickyCombinedClick = { positionedDemoStickyCombinedClicks += 1 }, ) stickyNoInsets() stickyClamp() @@ -498,7 +581,10 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { key = "positioned.z.overlap" style = { position = PositionMode.Relative - border { width = 1.px; color = 0xFF6F8498.toInt() } + border { + width = 1.px + color = 0xFF6F8498.toInt() + } backgroundColor = 0xFF283340.toInt() } }) { @@ -513,7 +599,7 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { onClick = { positionedDemoBlueClicks += 1 positionedDemoLastClick = "blue" - } + }, ) positionedOverlapCard( key = "positioned.z.green", @@ -526,7 +612,7 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { onClick = { positionedDemoGreenClicks += 1 positionedDemoLastClick = "green" - } + }, ) positionedOverlapCard( key = "positioned.z.red", @@ -539,12 +625,12 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { onClick = { positionedDemoRedClicks += 1 positionedDemoLastClick = "red" - } + }, ) } text( - "clicks blue=${positionedDemoBlueClicks}, green=${positionedDemoGreenClicks}, red=${positionedDemoRedClicks}", - { style = { color = DEMO_MUTED } } + "clicks blue=$positionedDemoBlueClicks, green=$positionedDemoGreenClicks, red=$positionedDemoRedClicks", + { style = { color = DEMO_MUTED } }, ) div({ @@ -558,11 +644,11 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { if (positionedDemoTieSwap) "tie order: second->first" else "tie order: first->second", { onMouseClick = { positionedDemoTieSwap = !positionedDemoTieSwap } - } + }, ) text( "same z, later DOM child should win overlap hit", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) } @@ -570,7 +656,10 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { key = "positioned.tie.sample" style = { position = PositionMode.Relative - border { width = 1.px; color = 0xFF6C7F91.toInt() } + border { + width = 1.px + color = 0xFF6C7F91.toInt() + } backgroundColor = 0xFF283440.toInt() } }) { @@ -585,7 +674,7 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { onTieClick = { positionedDemoTieSecondClicks += 1 positionedDemoLastClick = it - } + }, ) positionedTieCard( label = "first", @@ -597,7 +686,7 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { onTieClick = { positionedDemoTieFirstClicks += 1 positionedDemoLastClick = it - } + }, ) } else { positionedTieCard( @@ -610,7 +699,7 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { onTieClick = { positionedDemoTieFirstClicks += 1 positionedDemoLastClick = it - } + }, ) positionedTieCard( label = "second", @@ -622,13 +711,13 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { onTieClick = { positionedDemoTieSecondClicks += 1 positionedDemoLastClick = it - } + }, ) } } text( - "tie clicks first=${positionedDemoTieFirstClicks}, second=${positionedDemoTieSecondClicks}", - { style = { color = DEMO_MUTED } } + "tie clicks first=$positionedDemoTieFirstClicks, second=$positionedDemoTieSecondClicks", + { style = { color = DEMO_MUTED } }, ) text("Mixed static vs positioned overlap (stage rule).") @@ -636,7 +725,10 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { key = "positioned.mixed.sample" style = { position = PositionMode.Relative - border { width = 1.px; color = 0xFF6E8196.toInt() } + border { + width = 1.px + color = 0xFF6E8196.toInt() + } backgroundColor = 0xFF293541.toInt() } }) { @@ -652,7 +744,10 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { zIndex = 999 padding = 5.px backgroundColor = 0xFF667F9A.toInt() - border { width = 1.px; color = 0xFFC4D8EB.toInt() } + border { + width = 1.px + color = 0xFFC4D8EB.toInt() + } } }) { text("static z=999") @@ -671,21 +766,32 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { zIndex = -100 padding = 5.px backgroundColor = 0xCC2F536F.toInt() - border { width = 1.px; color = 0xFFB8D7EE.toInt() } + border { + width = 1.px + color = 0xFFB8D7EE.toInt() + } } }) { text("positioned z=-100") } } text( - "mixed clicks static=${positionedDemoMixedStaticClicks}, positioned=${positionedDemoMixedPositionedClicks}", - { style = { color = DEMO_MUTED; minHeight = 1.em } } + "mixed clicks static=$positionedDemoMixedStaticClicks, positioned=$positionedDemoMixedPositionedClicks", + { + style = { + color = DEMO_MUTED + minHeight = 1.em + } + }, ) repeat(40) { div({ style = { padding = 1.px - border { width = 1.px; color = 0xFF617A90.toInt() } + border { + width = 1.px + color = 0xFF617A90.toInt() + } } }) { text("Hi there, #$it pyj", { style { fontSize = it.px } }) @@ -698,12 +804,15 @@ private fun UiScope.stickyVerticalGroup( onSetLastHover: (String) -> Unit, onSetLastClick: (String) -> Unit, stickyTopClicks: Int, - onStickyTopClick: () -> Unit + onStickyTopClick: () -> Unit, ) { div({ key = "positioned.sticky.vertical.group" style = { - border { width = 1.px; color = 0xFF6C8096.toInt() } + border { + width = 1.px + color = 0xFF6C8096.toInt() + } padding = 4.px display = Display.Flex flexDirection = FlexDirection.Column @@ -721,7 +830,10 @@ private fun UiScope.stickyVerticalGroup( div({ style = { width = 50.percent - border { width = 1.px; color = 0xFF6A7E90.toInt() } + border { + width = 1.px + color = 0xFF6A7E90.toInt() + } padding = 3.px } }) { @@ -730,7 +842,10 @@ private fun UiScope.stickyVerticalGroup( key = "positioned.sticky.vertical.top.scroller" style = { overflowY = Overflow.Auto - border { width = 1.px; color = 0xFF8097AB.toInt() } + border { + width = 1.px + color = 0xFF8097AB.toInt() + } maxHeight = 7.em display = Display.Flex flexDirection = FlexDirection.Column @@ -759,7 +874,10 @@ private fun UiScope.stickyVerticalGroup( div({ style = { width = 50.percent - border { width = 1.px; color = 0xFF6A7E90.toInt() } + border { + width = 1.px + color = 0xFF6A7E90.toInt() + } padding = 3.px } }) { @@ -768,7 +886,10 @@ private fun UiScope.stickyVerticalGroup( key = "positioned.sticky.vertical.bottom.scroller" style = { overflowY = Overflow.Auto - border { width = 1.px; color = 0xFF8097AB.toInt() } + border { + width = 1.px + color = 0xFF8097AB.toInt() + } maxHeight = 7.em display = Display.Flex flexDirection = FlexDirection.Column @@ -786,7 +907,10 @@ private fun UiScope.stickyVerticalGroup( bottom = 0.px zIndex = 5 padding = 3.px - border { width = 1.px; color = 0xFF9FC2DF.toInt() } + border { + width = 1.px + color = 0xFF9FC2DF.toInt() + } backgroundColor = 0xCC446181.toInt() } }) { @@ -802,7 +926,10 @@ private fun UiScope.stickyVerticalGroup( key = "positioned.sticky.vertical.precedence.scroller" style = { overflowY = Overflow.Auto - border { width = 1.px; color = 0xFF8097AB.toInt() } + border { + width = 1.px + color = 0xFF8097AB.toInt() + } maxHeight = 6.em display = Display.Flex flexDirection = FlexDirection.Column @@ -818,7 +945,10 @@ private fun UiScope.stickyVerticalGroup( bottom = 0.px zIndex = 5 padding = 3.px - border { width = 1.px; color = 0xFF9FC2DF.toInt() } + border { + width = 1.px + color = 0xFF9FC2DF.toInt() + } backgroundColor = 0xCC3F5871.toInt() } }) { text("top+bottom set -> top wins (top=6)") } @@ -826,7 +956,7 @@ private fun UiScope.stickyVerticalGroup( } text( "sticky top clicks=$stickyTopClicks", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) } } @@ -835,7 +965,10 @@ private fun UiScope.stickyHorizontalGroup() { div({ key = "positioned.sticky.horizontal.group" style = { - border { width = 1.px; color = 0xFF6C8096.toInt() } + border { + width = 1.px + color = 0xFF6C8096.toInt() + } padding = 4.px display = Display.Flex flexDirection = FlexDirection.Column @@ -847,7 +980,10 @@ private fun UiScope.stickyHorizontalGroup() { key = "positioned.sticky.horizontal.left.scroller" style = { overflowX = Overflow.Auto - border { width = 1.px; color = 0xFF8097AB.toInt() } + border { + width = 1.px + color = 0xFF8097AB.toInt() + } display = Display.Flex flexDirection = FlexDirection.Row gap = 2.px @@ -861,7 +997,10 @@ private fun UiScope.stickyHorizontalGroup() { left = 0.px zIndex = 5 padding = 3.px - border { width = 1.px; color = 0xFF9FC2DF.toInt() } + border { + width = 1.px + color = 0xFF9FC2DF.toInt() + } backgroundColor = 0xCC3F617B.toInt() } }) { text("left=0") } @@ -873,7 +1012,10 @@ private fun UiScope.stickyHorizontalGroup() { key = "positioned.sticky.horizontal.right.scroller" style = { overflowX = Overflow.Auto - border { width = 1.px; color = 0xFF8097AB.toInt() } + border { + width = 1.px + color = 0xFF8097AB.toInt() + } display = Display.Flex flexDirection = FlexDirection.Row gap = 2.px @@ -890,7 +1032,10 @@ private fun UiScope.stickyHorizontalGroup() { right = 0.px zIndex = 5 padding = 3.px - border { width = 1.px; color = 0xFF9FC2DF.toInt() } + border { + width = 1.px + color = 0xFF9FC2DF.toInt() + } backgroundColor = 0xCC45637F.toInt() } }) { text("right=0") } @@ -902,7 +1047,10 @@ private fun UiScope.stickyHorizontalGroup() { key = "positioned.sticky.horizontal.precedence.scroller" style = { overflowX = Overflow.Auto - border { width = 1.px; color = 0xFF8097AB.toInt() } + border { + width = 1.px + color = 0xFF8097AB.toInt() + } display = Display.Flex flexDirection = FlexDirection.Row gap = 2.px @@ -917,7 +1065,10 @@ private fun UiScope.stickyHorizontalGroup() { right = 0.px zIndex = 5 padding = 3.px - border { width = 1.px; color = 0xFF9FC2DF.toInt() } + border { + width = 1.px + color = 0xFF9FC2DF.toInt() + } backgroundColor = 0xCC415A74.toInt() } }) { text("left+right set -> left wins (left=8)") } @@ -926,19 +1077,21 @@ private fun UiScope.stickyHorizontalGroup() { } } } - } private fun UiScope.stickyXYGroup( onSetLastHover: (String) -> Unit, onSetLastClick: (String) -> Unit, stickyCombinedClicks: Int, - onStickyCombinedClick: () -> Unit + onStickyCombinedClick: () -> Unit, ) { div({ key = "positioned.sticky.xy.group" style = { - border { width = 1.px; color = 0xFF6C8096.toInt() } + border { + width = 1.px + color = 0xFF6C8096.toInt() + } padding = 4.px display = Display.Flex flexDirection = FlexDirection.Column @@ -951,7 +1104,10 @@ private fun UiScope.stickyXYGroup( style = { overflowX = Overflow.Auto overflowY = Overflow.Auto - border { width = 1.px; color = 0xFF8097AB.toInt() } + border { + width = 1.px + color = 0xFF8097AB.toInt() + } maxHeight = 7.em display = Display.Flex flexDirection = FlexDirection.Column @@ -979,7 +1135,7 @@ private fun UiScope.stickyXYGroup( } text( "sticky x+y clicks=$stickyCombinedClicks", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) } } @@ -988,7 +1144,10 @@ private fun UiScope.stickyNoInsets() { div({ key = "positioned.sticky.inactive.group" style = { - border { width = 1.px; color = 0xFF6C8096.toInt() } + border { + width = 1.px + color = 0xFF6C8096.toInt() + } padding = 4.px display = Display.Flex flexDirection = FlexDirection.Column @@ -1001,7 +1160,10 @@ private fun UiScope.stickyNoInsets() { style = { overflowX = Overflow.Auto overflowY = Overflow.Auto - border { width = 1.px; color = 0xFF8097AB.toInt() } + border { + width = 1.px + color = 0xFF8097AB.toInt() + } maxHeight = 6.em display = Display.Flex flexDirection = FlexDirection.Column @@ -1014,7 +1176,10 @@ private fun UiScope.stickyNoInsets() { style = { position = PositionMode.Sticky padding = 3.px - border { width = 1.px; color = 0xFF9FC2DF.toInt() } + border { + width = 1.px + color = 0xFF9FC2DF.toInt() + } backgroundColor = 0xCC455F78.toInt() } }) { text("sticky without insets") } @@ -1029,7 +1194,10 @@ private fun UiScope.stickyClamp() { div({ key = "positioned.sticky.clamp.group" style = { - border { width = 1.px; color = 0xFF6C8096.toInt() } + border { + width = 1.px + color = 0xFF6C8096.toInt() + } padding = 4.px display = Display.Flex flexDirection = FlexDirection.Column @@ -1041,7 +1209,10 @@ private fun UiScope.stickyClamp() { key = "positioned.sticky.clamp.scroller" style = { overflowY = Overflow.Auto - border { width = 1.px; color = 0xFF8097AB.toInt() } + border { + width = 1.px + color = 0xFF8097AB.toInt() + } maxHeight = 7.em display = Display.Flex flexDirection = FlexDirection.Column @@ -1053,7 +1224,10 @@ private fun UiScope.stickyClamp() { div({ key = "positioned.sticky.clamp.parent" style = { - border { width = 1.px; color = 0xFF8FA5B9.toInt() } + border { + width = 1.px + color = 0xFF8FA5B9.toInt() + } backgroundColor = 0xFF2F3D4C.toInt() maxHeight = 6.em overflowY = Overflow.Auto @@ -1070,7 +1244,10 @@ private fun UiScope.stickyClamp() { top = 0.px zIndex = 6 padding = 3.px - border { width = 1.px; color = 0xFF9FC2DF.toInt() } + border { + width = 1.px + color = 0xFF9FC2DF.toInt() + } backgroundColor = 0xCC3F5A74.toInt() } }) { text("clamped sticky top") } @@ -1105,7 +1282,7 @@ data class ControlsProps( val onSetZBlue: (Long) -> Unit, val onSetZGreen: (Long) -> Unit, val onSetZRed: (Long) -> Unit, - val onReset: () -> Unit + val onReset: () -> Unit, ) private fun UiScope.controls(props: ControlsProps) { @@ -1116,7 +1293,10 @@ private fun UiScope.controls(props: ControlsProps) { flexDirection = FlexDirection.Column gap = 3.px padding = 5.px - border { width = 1.px; color = 0xFF617A90.toInt() } + border { + width = 1.px + color = 0xFF617A90.toInt() + } backgroundColor = 0xFF2A3541.toInt() position = PositionMode.Sticky top = 0.px @@ -1137,13 +1317,13 @@ private fun UiScope.controls(props: ControlsProps) { if (props.useLeft) "h: left first" else "h: right fallback", { onMouseClick = { props.onToggleUseLeft() } - } + }, ) button( if (props.useTop) "v: top first" else "v: bottom fallback", { onMouseClick = { props.onToggleUseTop() } - } + }, ) button("Reset", { onMouseClick = { props.onReset() } @@ -1165,7 +1345,7 @@ private fun UiScope.controls(props: ControlsProps) { value = props.leftOffset.toLong(), min = OFFSET_MIN.toLong(), max = OFFSET_MAX.toLong(), - onChange = props.onSetLeft + onChange = props.onSetLeft, ) positionedRangeControl( label = "right", @@ -1173,7 +1353,7 @@ private fun UiScope.controls(props: ControlsProps) { value = props.rightOffset.toLong(), min = 0, max = OFFSET_MAX.toLong(), - onChange = props.onSetRight + onChange = props.onSetRight, ) positionedRangeControl( label = "top", @@ -1181,7 +1361,7 @@ private fun UiScope.controls(props: ControlsProps) { value = props.topOffset.toLong(), min = OFFSET_MIN.toLong(), max = OFFSET_MAX.toLong(), - onChange = props.onSetTop + onChange = props.onSetTop, ) positionedRangeControl( label = "bottom", @@ -1189,7 +1369,7 @@ private fun UiScope.controls(props: ControlsProps) { value = props.bottomOffset.toLong(), min = 0, max = OFFSET_MAX.toLong(), - onChange = props.onSetBottom + onChange = props.onSetBottom, ) positionedRangeControl( label = "z blue", @@ -1197,7 +1377,7 @@ private fun UiScope.controls(props: ControlsProps) { value = props.zBlue.toLong(), min = Z_MIN.toLong(), max = Z_MAX.toLong(), - onChange = props.onSetZBlue + onChange = props.onSetZBlue, ) positionedRangeControl( label = "z green", @@ -1205,7 +1385,7 @@ private fun UiScope.controls(props: ControlsProps) { value = props.zGreen.toLong(), min = Z_MIN.toLong(), max = Z_MAX.toLong(), - onChange = props.onSetZGreen + onChange = props.onSetZGreen, ) positionedRangeControl( label = "z red", @@ -1213,11 +1393,11 @@ private fun UiScope.controls(props: ControlsProps) { value = props.zRed.toLong(), min = Z_MIN.toLong(), max = Z_MAX.toLong(), - onChange = props.onSetZRed + onChange = props.onSetZRed, ) text( "hover=${props.lastHover} click=${props.lastClick}", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) } } @@ -1228,7 +1408,7 @@ private fun UiScope.positionedRangeControl( value: Long, min: Long, max: Long, - onChange: (Long) -> Unit + onChange: (Long) -> Unit, ) { div({ this.key = "$key-container" @@ -1250,7 +1430,7 @@ private fun UiScope.positionedRangeControl( value = value, min = min, max = max, - step = 1 + step = 1, ), { this.key = key @@ -1261,7 +1441,7 @@ private fun UiScope.positionedRangeControl( style = { width = 90.percent } - } + }, ) } } @@ -1274,7 +1454,7 @@ private fun UiScope.positionedOverlapCard( zIndex: Int, color: Int, onHover: () -> Unit, - onClick: () -> Unit + onClick: () -> Unit, ) { div({ this.key = key @@ -1286,7 +1466,10 @@ private fun UiScope.positionedOverlapCard( this.top = top.px padding = 5.px backgroundColor = color - border { width = 1.px; this.color = 0xFFE6F1FD.toInt() } + border { + width = 1.px + this.color = 0xFFE6F1FD.toInt() + } this.zIndex = zIndex } }) { @@ -1301,7 +1484,7 @@ private fun UiScope.positionedTieCard( zIndex: Int, color: Int, onSetLastHover: (String) -> Unit, - onTieClick: (String) -> Unit + onTieClick: (String) -> Unit, ) { div({ key = "positioned.tie.$label" @@ -1313,13 +1496,13 @@ private fun UiScope.positionedTieCard( this.top = top.px padding = 5.px backgroundColor = color - border { width = 1.px; this.color = 0xFFDDEDFD.toInt() } + border { + width = 1.px + this.color = 0xFFDDEDFD.toInt() + } this.zIndex = zIndex } }) { text("$label z=$zIndex") } } - - - diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/StylesheetsSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/StylesheetsSection.kt index 7623206..338669e 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/StylesheetsSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/StylesheetsSection.kt @@ -1,12 +1,12 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections -import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.dom.elements.InputType +import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.Event -import org.dreamfinity.dsgl.core.style.Display -import org.dreamfinity.dsgl.core.style.FlexDirection import org.dreamfinity.dsgl.core.hooks.useMemo import org.dreamfinity.dsgl.core.hooks.useState +import org.dreamfinity.dsgl.core.style.Display +import org.dreamfinity.dsgl.core.style.FlexDirection import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED fun UiScope.stylesheetsSection( @@ -14,7 +14,7 @@ fun UiScope.stylesheetsSection( onInfo: (String) -> Unit, loadStylesheetText: () -> String, saveStylesheetText: (String) -> Unit, - onReloadStylesheets: () -> Unit + onReloadStylesheets: () -> Unit, ) { val initialLoad by useMemo { runCatching { loadStylesheetText() } @@ -27,8 +27,11 @@ fun UiScope.stylesheetsSection( if (initialLoad.isSuccess) { "loaded" } else { - "load failed: ${initialLoad.exceptionOrNull()?.javaClass?.simpleName ?: "unknown"}" - } + "load failed: ${initialLoad + .exceptionOrNull() + ?.javaClass + ?.simpleName ?: "unknown"}" + }, ) div({ @@ -51,7 +54,10 @@ fun UiScope.stylesheetsSection( style = { padding = 4.px gap = 3.px - border { width = 1.px; color = 0xFF5E6A77.toInt() } + border { + width = 1.px + color = 0xFF5E6A77.toInt() + } } }) { text("Demo stylesheet editor: showcase_styles.dss") @@ -121,7 +127,7 @@ fun UiScope.stylesheetsSection( } text( "status=$stylesheetEditorStatus; reloads=$stylesheetReloadCount; clicks=$stylesheetDemoClickCount", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) } @@ -132,7 +138,10 @@ fun UiScope.stylesheetsSection( style = { padding = 4.px gap = 3.px - border { width = 1.px; color = 0xFF5E6A77.toInt() } + border { + width = 1.px + color = 0xFF5E6A77.toInt() + } } }) { text("Selector matrix", { @@ -213,7 +222,10 @@ fun UiScope.stylesheetsSection( style = { padding = 4.px gap = 3.px - border { width = 1.px; color = 0xFF5E6A77.toInt() } + border { + width = 1.px + color = 0xFF5E6A77.toInt() + } } }) { text("Pseudo-states: :hover, :active, :focus, :disabled") @@ -236,7 +248,7 @@ fun UiScope.stylesheetsSection( input( InputType.Text( value = stylesheetDemoTextValue, - placeholder = "Focus target" + placeholder = "Focus target", ), { key = "styles.state.focusInput" @@ -247,7 +259,7 @@ fun UiScope.stylesheetsSection( stylesheetDemoTextValue = event.value onLogHook("styles.state.focusInput.onInput", event, "value=${event.value}") } - } + }, ) button("Disabled", { key = "styles.state.disabled" @@ -265,7 +277,10 @@ fun UiScope.stylesheetsSection( style = { padding = 4.px gap = 2.px - border { width = 1.px; color = 0xFF5E6A77.toInt() } + border { + width = 1.px + color = 0xFF5E6A77.toInt() + } } }) { text("Variable demo uses :root { --primary: ... } and var(--primary)") @@ -284,7 +299,10 @@ fun UiScope.stylesheetsSection( style = { padding = 4.px gap = 3.px - border { width = 1.px; color = 0xFF5E6A77.toInt() } + border { + width = 1.px + color = 0xFF5E6A77.toInt() + } } }) { text("CSS units demo: px, em, %, vw, vh") diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/TextEditingSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/TextEditingSection.kt index a27f3cf..4cb4ae7 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/TextEditingSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/TextEditingSection.kt @@ -1,13 +1,13 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections -import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.dom.elements.InputType +import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.Event import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.KeyModifiers +import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection -import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_OK @@ -20,7 +20,7 @@ fun UiScope.textEditingSection(onLogHook: (String, Event, String?) -> Unit) { var textEditingSingleValue by useState("Edit this line") var textEditingPasswordValue by useState("secret42") var textEditingAreaValue by useState( - "Line 1: drag-select me\nLine 2: use Shift+Arrows\nLine 3: Ctrl/Cmd+C/V/X" + "Line 1: drag-select me\nLine 2: use Shift+Arrows\nLine 3: Ctrl/Cmd+C/V/X", ) var textEditingSawSelectionDrag by useState(false) var textEditingSawShiftSelection by useState(false) @@ -44,7 +44,7 @@ fun UiScope.textEditingSection(onLogHook: (String, Event, String?) -> Unit) { input( InputType.Text( value = textEditingSingleValue, - placeholder = "Type and select text" + placeholder = "Type and select text", ), { key = SINGLE_KEY @@ -70,7 +70,7 @@ fun UiScope.textEditingSection(onLogHook: (String, Event, String?) -> Unit) { onLogHook("textEditing.clipboard", event, "key=$SINGLE_KEY code=${event.keyCode}") } } - } + }, ) text("Single-line: caret + selection visible in control", { style = { color = DEMO_MUTED } @@ -80,7 +80,7 @@ fun UiScope.textEditingSection(onLogHook: (String, Event, String?) -> Unit) { input( InputType.Password( value = textEditingPasswordValue, - placeholder = "password" + placeholder = "password", ), { key = PASSWORD_KEY @@ -106,7 +106,7 @@ fun UiScope.textEditingSection(onLogHook: (String, Event, String?) -> Unit) { onLogHook("textEditing.clipboard", event, "key=$PASSWORD_KEY code=${event.keyCode}") } } - } + }, ) text("Password: masked selection/caret behavior", { style = { color = DEMO_MUTED } @@ -176,19 +176,17 @@ private fun UiScope.checklistLine(textValue: String, done: Boolean) { }) } -private fun isArrowLike(keyCode: Int): Boolean { - return keyCode == KeyCodes.LEFT || - keyCode == KeyCodes.RIGHT || - keyCode == KeyCodes.UP || - keyCode == KeyCodes.DOWN || - keyCode == KeyCodes.HOME || - keyCode == KeyCodes.END -} +private fun isArrowLike(keyCode: Int): Boolean = + keyCode == KeyCodes.LEFT || + keyCode == KeyCodes.RIGHT || + keyCode == KeyCodes.UP || + keyCode == KeyCodes.DOWN || + keyCode == KeyCodes.HOME || + keyCode == KeyCodes.END -private fun isClipboardShortcut(keyCode: Int): Boolean { - return keyCode == KeyCodes.C || - keyCode == KeyCodes.X || - keyCode == KeyCodes.V || - keyCode == KeyCodes.A || - keyCode == KeyCodes.Z -} +private fun isClipboardShortcut(keyCode: Int): Boolean = + keyCode == KeyCodes.C || + keyCode == KeyCodes.X || + keyCode == KeyCodes.V || + keyCode == KeyCodes.A || + keyCode == KeyCodes.Z diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/TextWrapSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/TextWrapSection.kt index 41e4731..474da41 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/TextWrapSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/TextWrapSection.kt @@ -1,11 +1,11 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections -import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.dom.elements.InputType +import org.dreamfinity.dsgl.core.dsl.* +import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection import org.dreamfinity.dsgl.core.style.TextWrap -import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED private const val WRAP_SAMPLE_TEXT = @@ -33,7 +33,7 @@ fun UiScope.textWrapSection(onInfo: (String) -> Unit) { text("Text Wrap: wrap / nowrap") text( "Wrap keeps text inside panel width; NoWrap keeps one line and may overflow or clip.", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) div({ @@ -50,7 +50,7 @@ fun UiScope.textWrapSection(onInfo: (String) -> Unit) { textWrapNoWrap = !textWrapNoWrap onInfo("TextWrap mode=${if (textWrapNoWrap) "nowrap" else "wrap"}") } - } + }, ) button("Reset width", { onMouseClick = { @@ -64,7 +64,7 @@ fun UiScope.textWrapSection(onInfo: (String) -> Unit) { value = panelWidth.toLong(), min = minWidth.toLong(), max = maxWidth.toLong(), - step = 2 + step = 2, ), { key = "textWrap.width" @@ -73,11 +73,11 @@ fun UiScope.textWrapSection(onInfo: (String) -> Unit) { val next = (event.parsedValue as? Long) ?: event.value.toLongOrNull() ?: panelWidth.toLong() textWrapWidth = next.coerceIn(minWidth.toLong(), maxWidth.toLong()) } - } + }, ) text( "panelWidth=$panelWidth mode=${if (mode == TextWrap.Wrap) "wrap" else "nowrap"}", - { style = { color = DEMO_MUTED } } + { style = { color = DEMO_MUTED } }, ) div({ @@ -89,9 +89,11 @@ fun UiScope.textWrapSection(onInfo: (String) -> Unit) { padding = 3.px backgroundColor = 0xFF2B3542.toInt() gap = 2.px - border { width = 1.px; color = 0xFF6F8298.toInt() } + border { + width = 1.px + color = 0xFF6F8298.toInt() + } } - }) { text("Text node (static)", { style = { textWrap = mode } }) text(WRAP_SAMPLE_TEXT, { style = { textWrap = mode } }) @@ -116,5 +118,3 @@ fun UiScope.textWrapSection(onInfo: (String) -> Unit) { } } } - - diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/CapabilityChecklistCatalog.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/CapabilityChecklistCatalog.kt index 4b57750..0fe6157 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/CapabilityChecklistCatalog.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/CapabilityChecklistCatalog.kt @@ -1,16 +1,18 @@ package org.dreamfinity.dsgl.mcForge1710.demo.support -enum class CapabilityGroup(val title: String) { +enum class CapabilityGroup( + val title: String, +) { DSL_BUILDERS("DSL Builders"), INPUT_TYPES("Input Types"), EVENT_HOOKS("Event Hooks"), SHOWCASE_FEATURES("Showcase Features"), - MC_ADAPTER_FEATURES("MC Adapter Features") + MC_ADAPTER_FEATURES("MC Adapter Features"), } enum class CapabilityId( val label: String, - val group: CapabilityGroup + val group: CapabilityGroup, ) { BUILDER_DIV("builder: div", CapabilityGroup.DSL_BUILDERS), BUILDER_OVERLAY("builder: overlay", CapabilityGroup.DSL_BUILDERS), @@ -105,259 +107,281 @@ enum class CapabilityId( IMAGE_HTTP("Image: http(s):// path", CapabilityGroup.MC_ADAPTER_FEATURES), ITEMSTACK_2D("Item stack: 2D item", CapabilityGroup.MC_ADAPTER_FEATURES), ITEMSTACK_3D("Item stack: 3D block", CapabilityGroup.MC_ADAPTER_FEATURES), - ITEMSTACK_ROTATION("Item stack rotation controls", CapabilityGroup.MC_ADAPTER_FEATURES) + ITEMSTACK_ROTATION("Item stack rotation controls", CapabilityGroup.MC_ADAPTER_FEATURES), } object CapabilityChecklistCatalog { val required: List = CapabilityId.entries - fun capabilitiesForSection(section: DemoSection): Set = when (section) { - DemoSection.OVERVIEW -> setOf( - CapabilityId.BUILDER_TEXT, - CapabilityId.BUILDER_TEXT_LAMBDA, - CapabilityId.BUILDER_BUTTON, - CapabilityId.EVENT_INSPECTOR, - CapabilityId.CAPABILITY_CHECKLIST - ) - - DemoSection.INSPECTOR -> setOf( - CapabilityId.BUILDER_DIV, - CapabilityId.BUILDER_TEXT, - CapabilityId.BUILDER_BUTTON, - CapabilityId.BUILDER_INPUT - ) - - DemoSection.LAYOUT_STYLE -> setOf( - CapabilityId.BUILDER_DIV, - CapabilityId.BUILDER_OVERLAY, - CapabilityId.HOOK_MOUSE_CLICK, - CapabilityId.LAYOUT_GAP_FIXED, - CapabilityId.STYLE_MARGIN_PADDING_BORDER, - CapabilityId.OVERLAY_BEHAVIOR - ) - - DemoSection.LAYOUT_DEBUG -> setOf( - CapabilityId.BUILDER_DIV, - CapabilityId.BUILDER_TEXT, - CapabilityId.BUILDER_BUTTON, - CapabilityId.BUILDER_INPUT, - CapabilityId.LAYOUT_VALIDATOR - ) - - DemoSection.POSITIONED_LAYOUT -> setOf( - CapabilityId.BUILDER_DIV, - CapabilityId.BUILDER_TEXT, - CapabilityId.BUILDER_BUTTON, - CapabilityId.BUILDER_INPUT, - CapabilityId.HOOK_MOUSE_ENTER, - CapabilityId.HOOK_MOUSE_LEAVE, - CapabilityId.HOOK_MOUSE_CLICK, - CapabilityId.HOOK_MOUSE_WHEEL, - CapabilityId.OVERLAY_BEHAVIOR - ) - - DemoSection.OVERFLOW_SCROLL -> setOf( - CapabilityId.BUILDER_DIV, - CapabilityId.BUILDER_TEXT, - CapabilityId.BUILDER_BUTTON, - CapabilityId.BUILDER_INPUT, - CapabilityId.HOOK_MOUSE_CLICK, - ) - - DemoSection.DISPLAY -> setOf( - CapabilityId.BUILDER_DIV, - CapabilityId.BUILDER_INPUT, - CapabilityId.BUILDER_BUTTON, - CapabilityId.HOOK_MOUSE_CLICK, - CapabilityId.DISPLAY_BLOCK, - CapabilityId.DISPLAY_INLINE, - CapabilityId.DISPLAY_NONE, - CapabilityId.DISPLAY_FLEX, - CapabilityId.DISPLAY_GRID - ) - - DemoSection.TEXT_WRAP -> setOf( - CapabilityId.BUILDER_TEXT, - CapabilityId.BUILDER_TEXT_LAMBDA, - CapabilityId.BUILDER_BUTTON, - CapabilityId.BUILDER_INPUT, - CapabilityId.TEXT_WRAP - ) - - DemoSection.MSDF_FONTS -> setOf( - CapabilityId.BUILDER_TEXT, - CapabilityId.BUILDER_TEXT_LAMBDA, - CapabilityId.BUILDER_INPUT, - CapabilityId.BUILDER_BUTTON, - CapabilityId.MSDF_FONT_SWITCH, - CapabilityId.MSDF_OPACITY, - CapabilityId.MSDF_WRAP - ) - - DemoSection.ANIMATIONS -> setOf( - CapabilityId.BUILDER_DIV, - CapabilityId.BUILDER_TEXT, - CapabilityId.BUILDER_TEXT_LAMBDA, - CapabilityId.BUILDER_BUTTON, - CapabilityId.BUILDER_INPUT, - CapabilityId.HOOK_MOUSE_ENTER, - CapabilityId.HOOK_MOUSE_LEAVE, - CapabilityId.ANIMATION_TRANSFORM, - CapabilityId.ANIMATION_OPACITY, - CapabilityId.ANIMATION_KEYFRAMES - ) - - DemoSection.MODALS -> setOf( - CapabilityId.BUILDER_DIV, - CapabilityId.BUILDER_INPUT, - CapabilityId.BUILDER_BUTTON, - CapabilityId.MODAL_HOST, - CapabilityId.MODAL_STACKING, - CapabilityId.MODAL_BACKDROP, - CapabilityId.MODAL_ESCAPE, - CapabilityId.MODAL_FOCUS_TRAP - ) - - DemoSection.CONTEXT_MENU -> setOf( - CapabilityId.BUILDER_DIV, - CapabilityId.BUILDER_TEXT, - CapabilityId.BUILDER_BUTTON, - CapabilityId.HOOK_MOUSE_DOWN, - CapabilityId.CONTEXT_MENU_OVERLAY, - CapabilityId.CONTEXT_MENU_NESTED, - CapabilityId.CONTEXT_MENU_ANCHORED, - CapabilityId.CONTEXT_MENU_SCROLL - ) - - DemoSection.INPUTS -> setOf( - CapabilityId.BUILDER_INPUT, - CapabilityId.BUILDER_TEXTAREA, - CapabilityId.INPUT_TEXT, - CapabilityId.INPUT_PASSWORD, - CapabilityId.INPUT_NUMBER, - CapabilityId.INPUT_RANGE, - CapabilityId.INPUT_CHECKBOX, - CapabilityId.INPUT_RADIO, - CapabilityId.INPUT_DATE - ) - - DemoSection.INPUT_EVENTS -> setOf( - CapabilityId.BUILDER_INPUT, - CapabilityId.BUILDER_TEXTAREA, - CapabilityId.HOOK_FOCUS, - CapabilityId.HOOK_BLUR, - CapabilityId.HOOK_INPUT, - CapabilityId.HOOK_CHANGE - ) - - DemoSection.COLOR_PICKER -> setOf( - CapabilityId.BUILDER_DIV, - CapabilityId.BUILDER_BUTTON, - CapabilityId.BUILDER_TEXT, - CapabilityId.BUILDER_TEXT_LAMBDA, - CapabilityId.HOOK_MOUSE_DOWN, - CapabilityId.HOOK_MOUSE_CLICK - ) - - DemoSection.TEXT_EDITING -> setOf( - CapabilityId.BUILDER_INPUT, - CapabilityId.BUILDER_TEXTAREA, - CapabilityId.HOOK_MOUSE_DOWN, - CapabilityId.HOOK_MOUSE_DRAG, - CapabilityId.HOOK_KEY_DOWN, - CapabilityId.HOOK_INPUT - ) - - DemoSection.REFS -> setOf( - CapabilityId.BUILDER_DIV, - CapabilityId.BUILDER_INPUT, - CapabilityId.BUILDER_BUTTON, - CapabilityId.BUILDER_TEXT_LAMBDA, - CapabilityId.REFS_OBJECT, - CapabilityId.REFS_CALLBACK, - CapabilityId.REFS_IMPERATIVE_FOCUS - ) - - DemoSection.DRAG_DROP -> setOf( - CapabilityId.BUILDER_DIV, - CapabilityId.BUILDER_TEXT, - CapabilityId.BUILDER_TEXT_LAMBDA, - CapabilityId.HOOK_DRAG_START, - CapabilityId.HOOK_DRAG, - CapabilityId.HOOK_DRAG_END, - CapabilityId.HOOK_DRAG_ENTER, - CapabilityId.HOOK_DRAG_OVER, - CapabilityId.HOOK_DRAG_LEAVE, - CapabilityId.HOOK_DROP, - CapabilityId.DND_SMOOTH_GHOST, - CapabilityId.DND_DATA_TRANSFER, - CapabilityId.DND_DROP_EFFECT - ) - - DemoSection.INTERACTIONS -> setOf( - CapabilityId.HOOK_MOUSE_ENTER, - CapabilityId.HOOK_MOUSE_LEAVE, - CapabilityId.HOOK_MOUSE_OVER, - CapabilityId.HOOK_MOUSE_MOVE, - CapabilityId.HOOK_MOUSE_DOWN, - CapabilityId.HOOK_MOUSE_UP, - CapabilityId.HOOK_MOUSE_DRAG, - CapabilityId.HOOK_MOUSE_WHEEL, - CapabilityId.HOOK_KEY_DOWN, - CapabilityId.HOOK_KEY_UP, - CapabilityId.HOOK_KEY_PRESSED, - CapabilityId.HOOK_KEY_RELEASED, - CapabilityId.EVENT_CANCELLATION - ) - - DemoSection.FOCUS_REBUILD -> setOf( - CapabilityId.HOOK_KEY_DOWN, - CapabilityId.HOOK_KEY_UP, - CapabilityId.FOCUS_RETENTION, - CapabilityId.STATE_REBUILD, - CapabilityId.MANUAL_INVALIDATE - ) - - DemoSection.STYLESHEETS -> setOf( - CapabilityId.BUILDER_DIV, - CapabilityId.BUILDER_TEXT, - CapabilityId.BUILDER_TEXT_LAMBDA, - CapabilityId.BUILDER_BUTTON, - CapabilityId.BUILDER_INPUT, - CapabilityId.STYLESHEET_SELECTORS, - CapabilityId.STYLESHEET_PSEUDO_STATES, - CapabilityId.STYLESHEET_VARIABLES, - CapabilityId.STYLESHEET_INLINE_OVERRIDE, - CapabilityId.STYLESHEET_PROGRAMMATIC_RELOAD - ) - - DemoSection.CSS_CASCADE -> setOf( - CapabilityId.BUILDER_DIV, - CapabilityId.BUILDER_TEXT, - CapabilityId.BUILDER_TEXT_LAMBDA, - CapabilityId.BUILDER_BUTTON, - CapabilityId.HOOK_MOUSE_CLICK, - CapabilityId.STYLESHEET_SELECTORS, - CapabilityId.STYLESHEET_COMBINATORS, - CapabilityId.STYLESHEET_CASCADE - ) - - DemoSection.MC_FEATURES -> setOf( - CapabilityId.BUILDER_IMG, - CapabilityId.BUILDER_ITEM_STACK, - CapabilityId.IMAGE_RESOURCE, - CapabilityId.IMAGE_FILE, - CapabilityId.IMAGE_HTTP, - CapabilityId.ITEMSTACK_2D, - CapabilityId.ITEMSTACK_3D, - CapabilityId.ITEMSTACK_ROTATION - ) - } - - fun implementedByAllSections(): Set { - return DemoSection.entries + fun capabilitiesForSection(section: DemoSection): Set = + when (section) { + DemoSection.OVERVIEW -> + setOf( + CapabilityId.BUILDER_TEXT, + CapabilityId.BUILDER_TEXT_LAMBDA, + CapabilityId.BUILDER_BUTTON, + CapabilityId.EVENT_INSPECTOR, + CapabilityId.CAPABILITY_CHECKLIST, + ) + + DemoSection.INSPECTOR -> + setOf( + CapabilityId.BUILDER_DIV, + CapabilityId.BUILDER_TEXT, + CapabilityId.BUILDER_BUTTON, + CapabilityId.BUILDER_INPUT, + ) + + DemoSection.LAYOUT_STYLE -> + setOf( + CapabilityId.BUILDER_DIV, + CapabilityId.BUILDER_OVERLAY, + CapabilityId.HOOK_MOUSE_CLICK, + CapabilityId.LAYOUT_GAP_FIXED, + CapabilityId.STYLE_MARGIN_PADDING_BORDER, + CapabilityId.OVERLAY_BEHAVIOR, + ) + + DemoSection.LAYOUT_DEBUG -> + setOf( + CapabilityId.BUILDER_DIV, + CapabilityId.BUILDER_TEXT, + CapabilityId.BUILDER_BUTTON, + CapabilityId.BUILDER_INPUT, + CapabilityId.LAYOUT_VALIDATOR, + ) + + DemoSection.POSITIONED_LAYOUT -> + setOf( + CapabilityId.BUILDER_DIV, + CapabilityId.BUILDER_TEXT, + CapabilityId.BUILDER_BUTTON, + CapabilityId.BUILDER_INPUT, + CapabilityId.HOOK_MOUSE_ENTER, + CapabilityId.HOOK_MOUSE_LEAVE, + CapabilityId.HOOK_MOUSE_CLICK, + CapabilityId.HOOK_MOUSE_WHEEL, + CapabilityId.OVERLAY_BEHAVIOR, + ) + + DemoSection.OVERFLOW_SCROLL -> + setOf( + CapabilityId.BUILDER_DIV, + CapabilityId.BUILDER_TEXT, + CapabilityId.BUILDER_BUTTON, + CapabilityId.BUILDER_INPUT, + CapabilityId.HOOK_MOUSE_CLICK, + ) + + DemoSection.DISPLAY -> + setOf( + CapabilityId.BUILDER_DIV, + CapabilityId.BUILDER_INPUT, + CapabilityId.BUILDER_BUTTON, + CapabilityId.HOOK_MOUSE_CLICK, + CapabilityId.DISPLAY_BLOCK, + CapabilityId.DISPLAY_INLINE, + CapabilityId.DISPLAY_NONE, + CapabilityId.DISPLAY_FLEX, + CapabilityId.DISPLAY_GRID, + ) + + DemoSection.TEXT_WRAP -> + setOf( + CapabilityId.BUILDER_TEXT, + CapabilityId.BUILDER_TEXT_LAMBDA, + CapabilityId.BUILDER_BUTTON, + CapabilityId.BUILDER_INPUT, + CapabilityId.TEXT_WRAP, + ) + + DemoSection.MSDF_FONTS -> + setOf( + CapabilityId.BUILDER_TEXT, + CapabilityId.BUILDER_TEXT_LAMBDA, + CapabilityId.BUILDER_INPUT, + CapabilityId.BUILDER_BUTTON, + CapabilityId.MSDF_FONT_SWITCH, + CapabilityId.MSDF_OPACITY, + CapabilityId.MSDF_WRAP, + ) + + DemoSection.ANIMATIONS -> + setOf( + CapabilityId.BUILDER_DIV, + CapabilityId.BUILDER_TEXT, + CapabilityId.BUILDER_TEXT_LAMBDA, + CapabilityId.BUILDER_BUTTON, + CapabilityId.BUILDER_INPUT, + CapabilityId.HOOK_MOUSE_ENTER, + CapabilityId.HOOK_MOUSE_LEAVE, + CapabilityId.ANIMATION_TRANSFORM, + CapabilityId.ANIMATION_OPACITY, + CapabilityId.ANIMATION_KEYFRAMES, + ) + + DemoSection.MODALS -> + setOf( + CapabilityId.BUILDER_DIV, + CapabilityId.BUILDER_INPUT, + CapabilityId.BUILDER_BUTTON, + CapabilityId.MODAL_HOST, + CapabilityId.MODAL_STACKING, + CapabilityId.MODAL_BACKDROP, + CapabilityId.MODAL_ESCAPE, + CapabilityId.MODAL_FOCUS_TRAP, + ) + + DemoSection.CONTEXT_MENU -> + setOf( + CapabilityId.BUILDER_DIV, + CapabilityId.BUILDER_TEXT, + CapabilityId.BUILDER_BUTTON, + CapabilityId.HOOK_MOUSE_DOWN, + CapabilityId.CONTEXT_MENU_OVERLAY, + CapabilityId.CONTEXT_MENU_NESTED, + CapabilityId.CONTEXT_MENU_ANCHORED, + CapabilityId.CONTEXT_MENU_SCROLL, + ) + + DemoSection.INPUTS -> + setOf( + CapabilityId.BUILDER_INPUT, + CapabilityId.BUILDER_TEXTAREA, + CapabilityId.INPUT_TEXT, + CapabilityId.INPUT_PASSWORD, + CapabilityId.INPUT_NUMBER, + CapabilityId.INPUT_RANGE, + CapabilityId.INPUT_CHECKBOX, + CapabilityId.INPUT_RADIO, + CapabilityId.INPUT_DATE, + ) + + DemoSection.INPUT_EVENTS -> + setOf( + CapabilityId.BUILDER_INPUT, + CapabilityId.BUILDER_TEXTAREA, + CapabilityId.HOOK_FOCUS, + CapabilityId.HOOK_BLUR, + CapabilityId.HOOK_INPUT, + CapabilityId.HOOK_CHANGE, + ) + + DemoSection.COLOR_PICKER -> + setOf( + CapabilityId.BUILDER_DIV, + CapabilityId.BUILDER_BUTTON, + CapabilityId.BUILDER_TEXT, + CapabilityId.BUILDER_TEXT_LAMBDA, + CapabilityId.HOOK_MOUSE_DOWN, + CapabilityId.HOOK_MOUSE_CLICK, + ) + + DemoSection.TEXT_EDITING -> + setOf( + CapabilityId.BUILDER_INPUT, + CapabilityId.BUILDER_TEXTAREA, + CapabilityId.HOOK_MOUSE_DOWN, + CapabilityId.HOOK_MOUSE_DRAG, + CapabilityId.HOOK_KEY_DOWN, + CapabilityId.HOOK_INPUT, + ) + + DemoSection.REFS -> + setOf( + CapabilityId.BUILDER_DIV, + CapabilityId.BUILDER_INPUT, + CapabilityId.BUILDER_BUTTON, + CapabilityId.BUILDER_TEXT_LAMBDA, + CapabilityId.REFS_OBJECT, + CapabilityId.REFS_CALLBACK, + CapabilityId.REFS_IMPERATIVE_FOCUS, + ) + + DemoSection.DRAG_DROP -> + setOf( + CapabilityId.BUILDER_DIV, + CapabilityId.BUILDER_TEXT, + CapabilityId.BUILDER_TEXT_LAMBDA, + CapabilityId.HOOK_DRAG_START, + CapabilityId.HOOK_DRAG, + CapabilityId.HOOK_DRAG_END, + CapabilityId.HOOK_DRAG_ENTER, + CapabilityId.HOOK_DRAG_OVER, + CapabilityId.HOOK_DRAG_LEAVE, + CapabilityId.HOOK_DROP, + CapabilityId.DND_SMOOTH_GHOST, + CapabilityId.DND_DATA_TRANSFER, + CapabilityId.DND_DROP_EFFECT, + ) + + DemoSection.INTERACTIONS -> + setOf( + CapabilityId.HOOK_MOUSE_ENTER, + CapabilityId.HOOK_MOUSE_LEAVE, + CapabilityId.HOOK_MOUSE_OVER, + CapabilityId.HOOK_MOUSE_MOVE, + CapabilityId.HOOK_MOUSE_DOWN, + CapabilityId.HOOK_MOUSE_UP, + CapabilityId.HOOK_MOUSE_DRAG, + CapabilityId.HOOK_MOUSE_WHEEL, + CapabilityId.HOOK_KEY_DOWN, + CapabilityId.HOOK_KEY_UP, + CapabilityId.HOOK_KEY_PRESSED, + CapabilityId.HOOK_KEY_RELEASED, + CapabilityId.EVENT_CANCELLATION, + ) + + DemoSection.FOCUS_REBUILD -> + setOf( + CapabilityId.HOOK_KEY_DOWN, + CapabilityId.HOOK_KEY_UP, + CapabilityId.FOCUS_RETENTION, + CapabilityId.STATE_REBUILD, + CapabilityId.MANUAL_INVALIDATE, + ) + + DemoSection.STYLESHEETS -> + setOf( + CapabilityId.BUILDER_DIV, + CapabilityId.BUILDER_TEXT, + CapabilityId.BUILDER_TEXT_LAMBDA, + CapabilityId.BUILDER_BUTTON, + CapabilityId.BUILDER_INPUT, + CapabilityId.STYLESHEET_SELECTORS, + CapabilityId.STYLESHEET_PSEUDO_STATES, + CapabilityId.STYLESHEET_VARIABLES, + CapabilityId.STYLESHEET_INLINE_OVERRIDE, + CapabilityId.STYLESHEET_PROGRAMMATIC_RELOAD, + ) + + DemoSection.CSS_CASCADE -> + setOf( + CapabilityId.BUILDER_DIV, + CapabilityId.BUILDER_TEXT, + CapabilityId.BUILDER_TEXT_LAMBDA, + CapabilityId.BUILDER_BUTTON, + CapabilityId.HOOK_MOUSE_CLICK, + CapabilityId.STYLESHEET_SELECTORS, + CapabilityId.STYLESHEET_COMBINATORS, + CapabilityId.STYLESHEET_CASCADE, + ) + + DemoSection.MC_FEATURES -> + setOf( + CapabilityId.BUILDER_IMG, + CapabilityId.BUILDER_ITEM_STACK, + CapabilityId.IMAGE_RESOURCE, + CapabilityId.IMAGE_FILE, + CapabilityId.IMAGE_HTTP, + CapabilityId.ITEMSTACK_2D, + CapabilityId.ITEMSTACK_3D, + CapabilityId.ITEMSTACK_ROTATION, + ) + } + + fun implementedByAllSections(): Set = + DemoSection.entries .flatMap { capabilitiesForSection(it) } .toSet() - } } - diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/DemoSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/DemoSection.kt index 498ca61..3e1085d 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/DemoSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/DemoSection.kt @@ -2,20 +2,26 @@ package org.dreamfinity.dsgl.mcForge1710.demo.support enum class DemoSection( val title: String, - val subtitle: String + val subtitle: String, ) { OVERVIEW("Overview", "How to use the showcase"), INSPECTOR("Inspector", "Global in-game element/style/layout inspector (F12/F9)"), LAYOUT_STYLE("Layout & Style", "Containers, gaps, fixed sizes, style DSL"), LAYOUT_DEBUG("Layout Debug", "Strict bounds validator and diagnostics"), - POSITIONED_LAYOUT("Positioned Layout", "static/relative/absolute/fixed/sticky + z-index overlap, scroll and hit-testing"), + POSITIONED_LAYOUT( + "Positioned Layout", + "static/relative/absolute/fixed/sticky + z-index overlap, scroll and hit-testing", + ), OVERFLOW_SCROLL("Overflow & Scroll", "Viewport clipping, gutters, and cross-axis overflow forcing"), DISPLAY("Display", "block/inline/none/flex/grid layout behaviors"), TEXT_WRAP("Text Wrap", "wrap/nowrap behavior for text rendering"), MSDF_FONTS("MSDF Fonts", "MTSDF atlas fonts: switch font, size, color, opacity, wrapping"), ANIMATIONS("Animations & Transforms", "Transform hit-testing, transitions, keyframes, easing"), STYLESHEETS("Stylesheets", "Selectors, variables, pseudo-states, inline override"), - CSS_CASCADE("CSS Cascade & Combinators", "Descendant/child/sibling selectors, specificity, source order, !important, inheritance"), + CSS_CASCADE( + "CSS Cascade & Combinators", + "Descendant/child/sibling selectors, specificity, source order, !important, inheritance", + ), MODALS("Modals", "Declarative stacked modal host (RB-inspired)"), CONTEXT_MENU("Context Menu", "Right-click nested menus with overlay-first hit testing"), INPUTS("Inputs Gallery", "All input factory variants and textarea"), @@ -26,7 +32,5 @@ enum class DemoSection( DRAG_DROP("Drag & Drop", "HTML-like drag events, DataTransfer and smooth ghost"), INTERACTIONS("Interactions", "Mouse/key hooks, bubbling, cancellation"), FOCUS_REBUILD("Focus & Rebuild", "Focus retention and invalidation"), - MC_FEATURES("MC Features", "Pixel viewport rendering, clipping and item stacks") + MC_FEATURES("MC Features", "Pixel viewport rendering, clipping and item stacks"), } - - diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/EventFormatting.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/EventFormatting.kt index 962b809..15b7ad2 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/EventFormatting.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/EventFormatting.kt @@ -3,37 +3,56 @@ package org.dreamfinity.dsgl.mcForge1710.demo.support import org.dreamfinity.dsgl.core.dnd.* import org.dreamfinity.dsgl.core.event.* -fun formatEventLine( - hookName: String, - event: Event, - note: String? = null -): String { - val targetKey = event.target?.key?.toString() ?: "none" - val coords = when (event) { - is MouseEvent -> "xy=${event.mouseX},${event.mouseY}" - else -> "xy=-" - } - val payload = when (event) { - is MouseDownEvent -> "btn=${event.mouseButton.name.lowercase()}" - is MouseUpEvent -> "btn=${event.mouseButton.name.lowercase()}" - is MouseClickEvent -> "btn=${event.mouseButton.name.lowercase()}" - is MouseDragEvent -> "drag=${event.dx},${event.dy} btn=${event.mouseButton.name.lowercase()}" - is MouseWheelEvent -> "wheel=${event.dWheel}" - is KeyboardKeyDownEvent -> "key=${event.keyCode} char=${safeChar(event.keyChar)}" - is KeyboardKeyUpEvent -> "key=${event.keyCode} char=${safeChar(event.keyChar)}" - is FocusGainEvent -> "prev=${event.previousTargetKey ?: "none"}" - is FocusLoseEvent -> "next=${event.nextTargetKey ?: "none"}" - is InputEvent -> "value=${event.value} parsed=${event.parsedValue ?: "null"}" - is ValueChangedEvent -> "value=${event.value} parsed=${event.parsedValue ?: "null"}" - is DragStartEvent -> "source=${event.sourceKey ?: "none"} types=${event.dataTransfer.types.joinToString(",")}" - is DragEvent -> "source=${event.sourceKey ?: "none"} effect=${event.dataTransfer.dropEffect.name.lowercase()}" - is DragEndEvent -> "drop=${event.didDrop} effect=${event.finalDropEffect.name.lowercase()} target=${event.dropTargetKey ?: "none"}" - is DragEnterEvent -> "source=${event.sourceKey ?: "none"}" - is DragOverEvent -> "source=${event.sourceKey ?: "none"} effect=${event.dataTransfer.dropEffect.name.lowercase()} accepted=${event.dropAccepted || event.cancelled}" - is DragLeaveEvent -> "source=${event.sourceKey ?: "none"}" - is DropEvent -> "source=${event.sourceKey ?: "none"} types=${event.dataTransfer.types.joinToString(",")}" - else -> "" - } +fun formatEventLine(hookName: String, event: Event, note: String? = null): String { + val targetKey = + event.target + ?.key + ?.toString() ?: "none" + val coords = + when (event) { + is MouseEvent -> "xy=${event.mouseX},${event.mouseY}" + else -> "xy=-" + } + val payload = + when (event) { + is MouseDownEvent -> "btn=${event.mouseButton.name.lowercase()}" + is MouseUpEvent -> "btn=${event.mouseButton.name.lowercase()}" + is MouseClickEvent -> "btn=${event.mouseButton.name.lowercase()}" + is MouseDragEvent -> "drag=${event.dx},${event.dy} btn=${event.mouseButton.name.lowercase()}" + is MouseWheelEvent -> "wheel=${event.dWheel}" + is KeyboardKeyDownEvent -> "key=${event.keyCode} char=${safeChar(event.keyChar)}" + is KeyboardKeyUpEvent -> "key=${event.keyCode} char=${safeChar(event.keyChar)}" + is FocusGainEvent -> "prev=${event.previousTargetKey ?: "none"}" + is FocusLoseEvent -> "next=${event.nextTargetKey ?: "none"}" + is InputEvent -> "value=${event.value} parsed=${event.parsedValue ?: "null"}" + is ValueChangedEvent -> "value=${event.value} parsed=${event.parsedValue ?: "null"}" + is DragStartEvent -> "source=${event.sourceKey ?: "none"} types=${event.dataTransfer.types.joinToString( + ",", + )}" + is DragEvent -> { + val effect = + event.dataTransfer.dropEffect.name + .lowercase() + "source=${event.sourceKey ?: "none"} effect=$effect" + } + is DragEndEvent -> { + val effect = + event.finalDropEffect.name + .lowercase() + "drop=${event.didDrop} effect=$effect target=${event.dropTargetKey ?: "none"}" + } + is DragEnterEvent -> "source=${event.sourceKey ?: "none"}" + is DragOverEvent -> { + val effect = + event.dataTransfer.dropEffect.name + .lowercase() + val accepted = event.dropAccepted || event.cancelled + "source=${event.sourceKey ?: "none"} effect=$effect accepted=$accepted" + } + is DragLeaveEvent -> "source=${event.sourceKey ?: "none"}" + is DropEvent -> "source=${event.sourceKey ?: "none"} types=${event.dataTransfer.types.joinToString(",")}" + else -> "" + } val notePart = if (note.isNullOrBlank()) "" else " note=$note" val raw = "$hookName ${event.type.name} target=$targetKey $coords $payload shift=${KeyModifiers.shiftDown} ctrl=${KeyModifiers.controlDown} meta=${KeyModifiers.metaDown} shortcut=${KeyModifiers.shortcutDown}$notePart" @@ -53,4 +72,4 @@ private fun safeChar(ch: Char): String { if (ch == '\t') return "\\t" if (ch.code < 32) return "\\u%04x".format(ch.code) return ch.toString() -} \ No newline at end of file +} diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/EventLogEntry.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/EventLogEntry.kt index 2764ab2..b4f0667 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/EventLogEntry.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/EventLogEntry.kt @@ -3,5 +3,5 @@ package org.dreamfinity.dsgl.mcForge1710.demo.support data class EventLogEntry( val sequence: Int, val line: String, - val color: Int + val color: Int, ) diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/PersistentPanels.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/PersistentPanels.kt index 6b63a0d..3714b4a 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/PersistentPanels.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/PersistentPanels.kt @@ -12,7 +12,7 @@ fun UiScope.renderEventInspectorPanel( eventLogs: List, maxEventLogs: Int, visibleEventLines: Int, - onClearLogs: () -> Unit + onClearLogs: () -> Unit, ) { div({ key = "panel.eventInspector" @@ -24,9 +24,11 @@ fun UiScope.renderEventInspectorPanel( padding = 20.px backgroundColor = DEMO_SURFACE_ALT color = DsglColors.TEXT - border { width = 1.px; color = DsglColors.BORDER } + border { + width = 1.px + color = DsglColors.BORDER + } } - }) { div({ style = { @@ -58,7 +60,7 @@ fun UiScope.renderChecklistPanel( checklistPage: Int, checklistPageSize: Int, onSetChecklistPage: (Int) -> Unit, - onMoveChecklistPage: (Int) -> Unit + onMoveChecklistPage: (Int) -> Unit, ) { val required = CapabilityChecklistCatalog.required val pageSize = checklistPageSize @@ -81,9 +83,11 @@ fun UiScope.renderChecklistPanel( gap = 4.px backgroundColor = DEMO_SURFACE_ALT color = DsglColors.TEXT - border { width = 1.px; color = DsglColors.BORDER } + border { + width = 1.px + color = DsglColors.BORDER + } } - }) { text("Capability Checklist") div({ @@ -110,4 +114,3 @@ fun UiScope.renderChecklistPanel( text("Missing: $missing / ${required.size}", { style = { color = if (missing == 0) DEMO_OK else DEMO_ERR } }) } } - diff --git a/adapters/mc-forge-1-7-10/demo/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutStickyDemoIntegrationTests.kt b/adapters/mc-forge-1-7-10/demo/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutStickyDemoIntegrationTests.kt index 46256b6..a8d1c39 100644 --- a/adapters/mc-forge-1-7-10/demo/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutStickyDemoIntegrationTests.kt +++ b/adapters/mc-forge-1-7-10/demo/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutStickyDemoIntegrationTests.kt @@ -26,11 +26,14 @@ class PositionedLayoutStickyDemoIntegrationTests { private val width = 1024 private val height = 720 - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -94,7 +97,7 @@ class PositionedLayoutStickyDemoIntegrationTests { val topViewport = topScroller.scrollContainerState().viewportRect assertTrue( intersects(topRect, topViewport), - "Sticky top target must remain visible in its scroller viewport; targetRect=$topRect viewport=$topViewport" + "Sticky top target must remain visible in its scroller viewport; targetRect=$topRect viewport=$topViewport", ) val topPoint = findPointInsideTarget(fixture.tree.root, topTarget, topRect) @@ -104,7 +107,7 @@ class PositionedLayoutStickyDemoIntegrationTests { assertNotNull( topPoint, "Expected a hover-resolvable point inside sticky top target. " + - "targetRect=$topRect viewport=$topViewport centerWinner=$topCenterWinner" + "targetRect=$topRect viewport=$topViewport centerWinner=$topCenterWinner", ) assertEquals(topTarget, collectHoverChain(fixture.tree.root, topPoint.first, topPoint.second).lastOrNull()) @@ -133,7 +136,7 @@ class PositionedLayoutStickyDemoIntegrationTests { "onNativeDomExpandedPanelRect", Rect(700, 30, 280, 260), width, - height + height, ) inspector.onCursorMoved(xyPoint.first, xyPoint.second) invokeInspectorInternalByName(inspector, "buildDomSnapshot", width, height) @@ -146,23 +149,24 @@ class PositionedLayoutStickyDemoIntegrationTests { private data class Fixture( val window: ShowcaseWindow, - val tree: DomTree + val tree: DomTree, ) + private fun renderFixture(): Fixture { val window = ShowcaseWindow() window.onResize(width, height) window.selectedSection = DemoSection.POSITIONED_LAYOUT window.beginRenderBuild() - val tree = try { - window.render() - } finally { - window.endRenderBuild() - } + val tree = + try { + window.render() + } finally { + window.endRenderBuild() + } tree.render(ctx, width, height) return Fixture(window = window, tree = tree) } - private fun scrollMainSectionToSticky(fixture: Fixture) { val sectionScroller = requireContainer(fixture.tree.root, "section.positionedLayout") val stickySurface = requireNode(fixture.tree.root, "positioned.sticky.surface") @@ -176,14 +180,11 @@ class PositionedLayoutStickyDemoIntegrationTests { fixture.tree.render(ctx, width, height) } - private fun requireContainer(root: DOMNode, key: String): ContainerNode { - return requireNode(root, key) as? ContainerNode + private fun requireContainer(root: DOMNode, key: String): ContainerNode = + requireNode(root, key) as? ContainerNode ?: error("Expected container with key '$key'") - } - private fun requireNode(root: DOMNode, key: String): DOMNode { - return findByKey(root, key) ?: error("Node with key '$key' not found") - } + private fun requireNode(root: DOMNode, key: String): DOMNode = findByKey(root, key) ?: error("Node with key '$key' not found") private fun findByKey(root: DOMNode, key: String): DOMNode? { if (root.key?.toString() == key) return root @@ -236,9 +237,11 @@ class PositionedLayoutStickyDemoIntegrationTests { } } - private fun hoverWinnerKey(root: DOMNode, x: Int, y: Int): String? { - return collectHoverChain(root, x, y).lastOrNull()?.key?.toString() - } + private fun hoverWinnerKey(root: DOMNode, x: Int, y: Int): String? = + collectHoverChain(root, x, y) + .lastOrNull() + ?.key + ?.toString() private fun intersects(a: Rect, b: Rect): Boolean { val noOverlapX = a.x + a.width <= b.x || b.x + b.width <= a.x @@ -255,11 +258,7 @@ class PositionedLayoutStickyDemoIntegrationTests { return borderRectField.get(snapshot) as? Rect } - private fun invokeInspectorInternalByName( - inspector: InspectorController, - methodName: String, - vararg args: Any? - ): Any? { + private fun invokeInspectorInternalByName(inspector: InspectorController, methodName: String, vararg args: Any?): Any? { val method = findMethodByNameAndArity(inspector.javaClass, methodName, args.size) method.isAccessible = true return method.invoke(inspector, *args) @@ -275,32 +274,26 @@ class PositionedLayoutStickyDemoIntegrationTests { error("Field '$fieldName' not found on ${clazz.name}") } - private fun findMethod( - clazz: Class<*>, - methodName: String, - parameterTypes: Array> - ): Method { + private fun findMethod(clazz: Class<*>, methodName: String, parameterTypes: Array>): Method { var current: Class<*>? = clazz while (current != null) { - val method = current.declaredMethods.firstOrNull { - it.name == methodName && it.parameterTypes.contentEquals(parameterTypes) - } + val method = + current.declaredMethods.firstOrNull { + it.name == methodName && it.parameterTypes.contentEquals(parameterTypes) + } if (method != null) return method current = current.superclass } error("Method '$methodName' not found on ${clazz.name}") } - private fun findMethodByNameAndArity( - clazz: Class<*>, - methodName: String, - arity: Int - ): Method { + private fun findMethodByNameAndArity(clazz: Class<*>, methodName: String, arity: Int): Method { var current: Class<*>? = clazz while (current != null) { - val method = current.declaredMethods.firstOrNull { - (it.name == methodName || it.name.startsWith("$methodName$")) && it.parameterCount == arity - } + val method = + current.declaredMethods.firstOrNull { + (it.name == methodName || it.name.startsWith("$methodName$")) && it.parameterCount == arity + } if (method != null) return method current = current.superclass } diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglFonts.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglFonts.kt index 8a0a7d5..44307ee 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglFonts.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglFonts.kt @@ -13,32 +13,36 @@ object DsglFonts { @Volatile private var lastSummary: FontPreloadSummary? = null - private val warmFontIds: Set = linkedSetOf( - FontRegistry.DEFAULT_FONT_ID, - FontRegistry.FONT_UBUNTU, - FontRegistry.FONT_JB_MONO, - FontRegistry.TELEGRAFICO, - FontRegistry.FALLBACK_FONT_ID - ) + private val warmFontIds: Set = + linkedSetOf( + FontRegistry.DEFAULT_FONT_ID, + FontRegistry.FONT_UBUNTU, + FontRegistry.FONT_JB_MONO, + FontRegistry.TELEGRAFICO, + FontRegistry.FALLBACK_FONT_ID, + ) fun ensureInitialized(gameDir: File, classLoader: ClassLoader = javaClass.classLoader): FontPreloadSummary { if (initialized) { - return lastSummary ?: FontRegistry.discoverAndPreloadFonts( - externalFontsDir = File(gameDir, "dsgl/fonts"), - classLoader = classLoader - ).also { lastSummary = it } + return lastSummary ?: FontRegistry + .discoverAndPreloadFonts( + externalFontsDir = File(gameDir, "dsgl/fonts"), + classLoader = classLoader, + ).also { lastSummary = it } } synchronized(lock) { if (initialized) { - return lastSummary ?: FontRegistry.discoverAndPreloadFonts( - externalFontsDir = File(gameDir, "dsgl/fonts"), - classLoader = classLoader - ).also { lastSummary = it } + return lastSummary ?: FontRegistry + .discoverAndPreloadFonts( + externalFontsDir = File(gameDir, "dsgl/fonts"), + classLoader = classLoader, + ).also { lastSummary = it } } - val summary = FontRegistry.discoverAndPreloadFonts( - externalFontsDir = File(gameDir, "dsgl/fonts"), - classLoader = classLoader - ) + val summary = + FontRegistry.discoverAndPreloadFonts( + externalFontsDir = File(gameDir, "dsgl/fonts"), + classLoader = classLoader, + ) val warmed = FontRegistry.predecodeAtlases(warmFontIds) if (warmed > 0) { println("[DSGL-MSDF] predecoded atlases for $warmed warm fonts") diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index 9e2e36c..a3a0ef9 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -43,7 +43,10 @@ import org.lwjgl.input.Mouse import java.io.File import java.time.Instant import java.time.ZoneId -import java.util.* +import java.util.ArrayList +import java.util.Collections +import java.util.IdentityHashMap +import java.util.LinkedHashMap /** * Minecraft 1.7.10 host that owns UI lifecycle and boilerplate. @@ -54,8 +57,9 @@ import java.util.* @SideOnly(Side.CLIENT) abstract class DsglScreenHost( private val windowFactory: () -> DsglWindow, - var rendersCount: Long = 0 -) : GuiScreen(), DsglWindowHost { + var rendersCount: Long = 0, +) : GuiScreen(), + DsglWindowHost { companion object { @Volatile private var stylesPreloadedOnce: Boolean = false @@ -102,28 +106,32 @@ abstract class DsglScreenHost( private var activeColorSamplerOwner: ActiveColorSamplerOwner = ActiveColorSamplerOwner.None private var activeInlineColorSamplerNode: ColorPickerInlineNode? = null private val inspectorInputDebug: Boolean = false - private val perfDebug: Boolean = java.lang.Boolean.getBoolean("dsgl.perf.debug") - private val phaseTraceDebug: Boolean = java.lang.Boolean.getBoolean("dsgl.rebuild.trace") + private val perfDebug: Boolean = + java.lang.Boolean + .getBoolean("dsgl.perf.debug") + private val phaseTraceDebug: Boolean = + java.lang.Boolean + .getBoolean("dsgl.rebuild.trace") private var lastPerfLogMs: Long = 0L private var frameIndex: Long = 0L private var blankFrameGuardSkips: Long = 0L private val pipelineErrorLogTimes: MutableMap = linkedMapOf() - private val clipboardAccess: ClipboardAccess = object : ClipboardAccess { - override fun readText(): String { - return try { - getClipboardString() ?: "" - } catch (_: Exception) { - "" - } - } + private val clipboardAccess: ClipboardAccess = + object : ClipboardAccess { + override fun readText(): String = + try { + getClipboardString() ?: "" + } catch (_: Exception) { + "" + } - override fun writeText(value: String) { - try { - setClipboardString(value) - } catch (_: Exception) { + override fun writeText(value: String) { + try { + setClipboardString(value) + } catch (_: Exception) { + } } } - } override fun initGui() { DsglFonts.ensureInitialized(mc.mcDataDir, javaClass.classLoader) @@ -133,10 +141,14 @@ abstract class DsglScreenHost( object : ScreenColorSampler { override fun sampleColorAt(x: Int, y: Int): Int? = adapter.sampleScreenColor(x, y) - override fun sampleArea(x: Int, y: Int, width: Int, height: Int, outArgb: IntArray): Boolean { - return adapter.sampleScreenArea(x, y, width, height, outArgb) - } - } + override fun sampleArea( + x: Int, + y: Int, + width: Int, + height: Int, + outArgb: IntArray, + ): Boolean = adapter.sampleScreenArea(x, y, width, height, outArgb) + }, ) inspector.deactivate() inspectorPointerCaptured = false @@ -168,39 +180,43 @@ abstract class DsglScreenHost( val rebuiltThisFrame = rebuildIfNeeded() val tree = domTree ?: return val dtSeconds = tickFrameAndAnimations(tree, partialTicks) - val layoutPhase = commitLayoutPhaseOrFallback( - tree = tree, - mouseX = mouseX, - mouseY = mouseY, - partialTicks = partialTicks - ) ?: return - val overlayState = syncInspectorAndResolveOverlayState( - tree = tree, - dsglMouseX = frameCursor.mouseX, - dsglMouseY = frameCursor.mouseY - ) - val commands = paintApplicationRootOrFallback( - tree = tree, - stylesAlreadyApplied = layoutPhase.stylesAlreadyApplied, - mouseX = mouseX, - mouseY = mouseY, - partialTicks = partialTicks - ) ?: return + val layoutPhase = + commitLayoutPhaseOrFallback( + tree = tree, + mouseX = mouseX, + mouseY = mouseY, + partialTicks = partialTicks, + ) ?: return + val overlayState = + syncInspectorAndResolveOverlayState( + tree = tree, + dsglMouseX = frameCursor.mouseX, + dsglMouseY = frameCursor.mouseY, + ) + val commands = + paintApplicationRootOrFallback( + tree = tree, + stylesAlreadyApplied = layoutPhase.stylesAlreadyApplied, + mouseX = mouseX, + mouseY = mouseY, + partialTicks = partialTicks, + ) ?: return syncFeatureRuntimeFrame( - tree = tree, - dsglMouseX = frameCursor.mouseX, - dsglMouseY = frameCursor.mouseY - ) - val applicationOverlayCommands = collectApplicationOverlayCommands(overlayState.appOverlayRenderEnabled) - val systemOverlayCommands = syncSystemOverlayAndCollectCommands( tree = tree, dsglMouseX = frameCursor.mouseX, dsglMouseY = frameCursor.mouseY, - systemOverlayRenderEnabled = overlayState.systemOverlayRenderEnabled ) + val applicationOverlayCommands = collectApplicationOverlayCommands(overlayState.appOverlayRenderEnabled) + val systemOverlayCommands = + syncSystemOverlayAndCollectCommands( + tree = tree, + dsglMouseX = frameCursor.mouseX, + dsglMouseY = frameCursor.mouseY, + systemOverlayRenderEnabled = overlayState.systemOverlayRenderEnabled, + ) stageSystemOverlayCommands( systemOverlayCommands = systemOverlayCommands, - systemOverlayRenderEnabled = overlayState.systemOverlayRenderEnabled + systemOverlayRenderEnabled = overlayState.systemOverlayRenderEnabled, ) val debugOverlayCommands = collectDebugOverlayCommands() updateFrameInteractionState( @@ -210,36 +226,36 @@ abstract class DsglScreenHost( dsglMouseY = frameCursor.mouseY, appOverlayInputEnabled = overlayState.appOverlayInputEnabled, systemOverlayInputEnabled = overlayState.systemOverlayInputEnabled, - inspectorBlocks = overlayState.inspectorBlocks + inspectorBlocks = overlayState.inspectorBlocks, ) stageApplicationOverlayCommands( tree = tree, applicationOverlayCommands = applicationOverlayCommands, - appOverlayRenderEnabled = overlayState.appOverlayRenderEnabled + appOverlayRenderEnabled = overlayState.appOverlayRenderEnabled, ) composeAndPresentFrame( tree = tree, commands = commands, debugOverlayCommands = debugOverlayCommands, rebuiltThisFrame = rebuiltThisFrame, - layoutCommittedThisFrame = layoutPhase.layoutCommittedThisFrame + layoutCommittedThisFrame = layoutPhase.layoutCommittedThisFrame, ) finishDrawScreenFrame( tree = tree, mouseX = mouseX, mouseY = mouseY, - partialTicks = partialTicks + partialTicks = partialTicks, ) } private data class FrameCursorPosition( val mouseX: Int, - val mouseY: Int + val mouseY: Int, ) private data class LayoutPhaseResult( val stylesAlreadyApplied: Boolean, - val layoutCommittedThisFrame: Boolean + val layoutCommittedThisFrame: Boolean, ) private data class OverlayLayerFrameState( @@ -247,7 +263,7 @@ abstract class DsglScreenHost( val systemOverlayRenderEnabled: Boolean, val appOverlayInputEnabled: Boolean, val systemOverlayInputEnabled: Boolean, - val inspectorBlocks: Boolean + val inspectorBlocks: Boolean, ) private fun prepareFrameCursor(): FrameCursorPosition { @@ -257,17 +273,18 @@ abstract class DsglScreenHost( window.onFrame(System.currentTimeMillis()) return FrameCursorPosition( mouseX = dsglMouseX, - mouseY = dsglMouseY + mouseY = dsglMouseY, ) } private fun tickFrameAndAnimations(tree: DomTree, partialTicks: Float): Double { val nowNanos = System.nanoTime() - val dtSeconds = if (lastFrameNanos == 0L) { - 1.0 / 60.0 - } else { - ((nowNanos - lastFrameNanos).toDouble() / 1_000_000_000.0).coerceIn(0.0, 0.25) - } + val dtSeconds = + if (lastFrameNanos == 0L) { + 1.0 / 60.0 + } else { + ((nowNanos - lastFrameNanos).toDouble() / 1_000_000_000.0).coerceIn(0.0, 0.25) + } lastFrameNanos = nowNanos OverlayLayerDebugState.updateFrameTiming(dtSeconds) window.tick(dtSeconds.toFloat(), partialTicks) @@ -282,7 +299,7 @@ abstract class DsglScreenHost( tree: DomTree, mouseX: Int, mouseY: Int, - partialTicks: Float + partialTicks: Float, ): LayoutPhaseResult? { var stylesAlreadyApplied = false var layoutCommittedThisFrame = false @@ -304,14 +321,14 @@ abstract class DsglScreenHost( } return LayoutPhaseResult( stylesAlreadyApplied = stylesAlreadyApplied, - layoutCommittedThisFrame = layoutCommittedThisFrame + layoutCommittedThisFrame = layoutCommittedThisFrame, ) } private fun syncInspectorAndResolveOverlayState( tree: DomTree, dsglMouseX: Int, - dsglMouseY: Int + dsglMouseY: Int, ): OverlayLayerFrameState { inspector.onLayoutCommitted(tree.root, layoutRevision) inspector.onCursorMoved(dsglMouseX, dsglMouseY) @@ -323,15 +340,17 @@ abstract class DsglScreenHost( val systemOverlayRenderEnabled = OverlayLayerDebugState.isRenderEnabled(UiLayerId.SystemOverlay) val appOverlayInputEnabled = OverlayLayerDebugState.isInputEnabled(UiLayerId.ApplicationOverlay) val systemOverlayInputEnabled = OverlayLayerDebugState.isInputEnabled(UiLayerId.SystemOverlay) - val inspectorBlocks = systemOverlayInputEnabled && ( - inspectorPointerCaptured || inspector.shouldConsumePointer(dsglMouseX, dsglMouseY) + val inspectorBlocks = + systemOverlayInputEnabled && + ( + inspectorPointerCaptured || inspector.shouldConsumePointer(dsglMouseX, dsglMouseY) ) return OverlayLayerFrameState( appOverlayRenderEnabled = appOverlayRenderEnabled, systemOverlayRenderEnabled = systemOverlayRenderEnabled, appOverlayInputEnabled = appOverlayInputEnabled, systemOverlayInputEnabled = systemOverlayInputEnabled, - inspectorBlocks = inspectorBlocks + inspectorBlocks = inspectorBlocks, ) } @@ -340,36 +359,33 @@ abstract class DsglScreenHost( stylesAlreadyApplied: Boolean, mouseX: Int, mouseY: Int, - partialTicks: Float + partialTicks: Float, ): List? { tracePhase("commands.start") if (!stylesAlreadyApplied) { tracePhase("style.start") } - val commands = try { - tree.paint(adapter, applyStyles = !stylesAlreadyApplied) - } catch (error: Throwable) { - logPipelineError( - key = "draw.paint", - message = "[DSGL] Paint pipeline failed; rendering previous committed frame: ${error.message}" - ) - adapter.paint(composedCommandsBuffer) - flushPendingCleanup() - super.drawScreen(mouseX, mouseY, partialTicks) - captureColorPickerEyedropperSamples() - return null - } + val commands = + try { + tree.paint(adapter, applyStyles = !stylesAlreadyApplied) + } catch (error: Throwable) { + logPipelineError( + key = "draw.paint", + message = "[DSGL] Paint pipeline failed; rendering previous committed frame: ${error.message}", + ) + adapter.paint(composedCommandsBuffer) + flushPendingCleanup() + super.drawScreen(mouseX, mouseY, partialTicks) + captureColorPickerEyedropperSamples() + return null + } if (!stylesAlreadyApplied) { tracePhase("style.end") } return commands } - private fun syncFeatureRuntimeFrame( - tree: DomTree, - dsglMouseX: Int, - dsglMouseY: Int - ) { + private fun syncFeatureRuntimeFrame(tree: DomTree, dsglMouseX: Int, dsglMouseY: Int) { ContextMenuRuntime.engine.onFrame(adapter, lastWidth, lastHeight, 1f) SelectRuntime.applicationEngine.onFrame(adapter, lastWidth, lastHeight, 1f) SelectRuntime.systemEngine.onFrame(adapter, lastWidth, lastHeight, 1f) @@ -388,7 +404,7 @@ abstract class DsglScreenHost( } catch (error: Throwable) { logPipelineError( key = "draw.applicationOverlay", - message = "[DSGL] Application overlay paint failed; skipping app overlay frame: ${error.message}" + message = "[DSGL] Application overlay paint failed; skipping app overlay frame: ${error.message}", ) emptyList() } @@ -398,14 +414,14 @@ abstract class DsglScreenHost( tree: DomTree, dsglMouseX: Int, dsglMouseY: Int, - systemOverlayRenderEnabled: Boolean + systemOverlayRenderEnabled: Boolean, ): List { systemOverlayHost.syncFrame( inspectedRoot = tree.root, inspectedLayoutRevision = layoutRevision, cursorX = dsglMouseX, cursorY = dsglMouseY, - inspectorPointerCaptured = inspectorPointerCaptured + inspectorPointerCaptured = inspectorPointerCaptured, ) if (!systemOverlayRenderEnabled) { return emptyList() @@ -416,7 +432,7 @@ abstract class DsglScreenHost( } catch (error: Throwable) { logPipelineError( key = "draw.systemOverlay", - message = "[DSGL] System overlay paint failed; skipping system overlay frame: ${error.message}" + message = "[DSGL] System overlay paint failed; skipping system overlay frame: ${error.message}", ) emptyList() } @@ -424,7 +440,7 @@ abstract class DsglScreenHost( private fun stageSystemOverlayCommands( systemOverlayCommands: List, - systemOverlayRenderEnabled: Boolean + systemOverlayRenderEnabled: Boolean, ) { systemOverlayCommandsBuffer.clear() systemOverlayCommandsBuffer.addAll(systemOverlayCommands) @@ -433,19 +449,18 @@ abstract class DsglScreenHost( adapter, lastWidth, lastHeight, - systemOverlayCommandsBuffer + systemOverlayCommandsBuffer, ) } } - private fun collectDebugOverlayCommands(): List { - return runCatching { + private fun collectDebugOverlayCommands(): List = + runCatching { debugOverlayHost.render(lastWidth, lastHeight) debugOverlayHost.paint(adapter) }.getOrElse { emptyList() } - } private fun updateFrameInteractionState( tree: DomTree, @@ -454,14 +469,16 @@ abstract class DsglScreenHost( dsglMouseY: Int, appOverlayInputEnabled: Boolean, systemOverlayInputEnabled: Boolean, - inspectorBlocks: Boolean + inspectorBlocks: Boolean, ) { val contextMenuBlocks = appOverlayInputEnabled && !inspectorBlocks && ContextMenuRuntime.engine.isOpen() val selectBlocks = appOverlayInputEnabled && !inspectorBlocks && SelectRuntime.applicationEngine.isOpen() val systemSelectBlocks = systemOverlayInputEnabled && SelectRuntime.systemEngine.isOpen() val inlineSamplerOwnsSession = activeColorSamplerOwner is ActiveColorSamplerOwner.Inline - val colorPickerBlocks = !inspectorBlocks && ( - (systemOverlayInputEnabled && systemOverlayHost.isSystemColorPickerOpen()) || + val colorPickerBlocks = + !inspectorBlocks && + ( + (systemOverlayInputEnabled && systemOverlayHost.isSystemColorPickerOpen()) || (appOverlayInputEnabled && ColorPickerRuntime.engine.isOpen() && !inlineSamplerOwnsSession) ) if (!inspectorBlocks && !contextMenuBlocks && !selectBlocks && !systemSelectBlocks && !colorPickerBlocks) { @@ -494,7 +511,7 @@ abstract class DsglScreenHost( private fun stageApplicationOverlayCommands( tree: DomTree, applicationOverlayCommands: List, - appOverlayRenderEnabled: Boolean + appOverlayRenderEnabled: Boolean, ) { applicationOverlayCommandsBuffer.clear() if (appOverlayRenderEnabled) { @@ -505,19 +522,19 @@ abstract class DsglScreenHost( adapter, lastWidth, lastHeight, - applicationOverlayCommandsBuffer + applicationOverlayCommandsBuffer, ) SelectRuntime.applicationEngine.appendOverlayCommands( adapter, lastWidth, lastHeight, - applicationOverlayCommandsBuffer + applicationOverlayCommandsBuffer, ) ContextMenuRuntime.engine.appendOverlayCommands( adapter, lastWidth, lastHeight, - applicationOverlayCommandsBuffer + applicationOverlayCommandsBuffer, ) ColorPickerRuntime.engine.appendOverlayCommands(applicationOverlayCommandsBuffer) appendInlineColorPickerOverlayCommands(applicationOverlayCommandsBuffer) @@ -529,7 +546,7 @@ abstract class DsglScreenHost( commands: List, debugOverlayCommands: List, rebuiltThisFrame: Boolean, - layoutCommittedThisFrame: Boolean + layoutCommittedThisFrame: Boolean, ) { OverlayLayerContracts.composePaintCommands( applicationRoot = commands, @@ -537,14 +554,15 @@ abstract class DsglScreenHost( systemOverlay = systemOverlayCommandsBuffer, debug = debugOverlayCommands, out = stagingCommandsBuffer, - shouldRenderLayer = OverlayLayerDebugState::isRenderEnabled - ) - val keepPrevious = shouldKeepPreviousFrameCommands( - tree = tree, - rebuiltThisFrame = rebuiltThisFrame, - layoutCommittedThisFrame = layoutCommittedThisFrame, - candidate = stagingCommandsBuffer + shouldRenderLayer = OverlayLayerDebugState::isRenderEnabled, ) + val keepPrevious = + shouldKeepPreviousFrameCommands( + tree = tree, + rebuiltThisFrame = rebuiltThisFrame, + layoutCommittedThisFrame = layoutCommittedThisFrame, + candidate = stagingCommandsBuffer, + ) if (!keepPrevious) { composedCommandsBuffer.clear() composedCommandsBuffer.addAll(stagingCommandsBuffer) @@ -560,7 +578,7 @@ abstract class DsglScreenHost( tree: DomTree, mouseX: Int, mouseY: Int, - partialTicks: Float + partialTicks: Float, ) { tracePhase("draw.end") maybeLogPerf(tree) @@ -617,9 +635,7 @@ abstract class DsglScreenHost( override fun requestRedraw() { } - override fun getViewport(): Viewport { - return lastViewport - } + override fun getViewport(): Viewport = lastViewport private fun updateSize(force: Boolean) { val viewport = adapter.viewport() @@ -678,7 +694,7 @@ abstract class DsglScreenHost( window.discardRenderBuild() logPipelineError( key = "rebuild", - message = "[DSGL] Rebuild failed; keeping previous committed frame/tree: ${error.message}" + message = "[DSGL] Rebuild failed; keeping previous committed frame/tree: ${error.message}", ) false } @@ -712,7 +728,7 @@ abstract class DsglScreenHost( } throw IllegalStateException( - "Hot-reload hook remount recovery exceeded $maxAttempts attempts: ${lastRemountRequest?.message}" + "Hot-reload hook remount recovery exceeded $maxAttempts attempts: ${lastRemountRequest?.message}", ) } @@ -721,7 +737,7 @@ abstract class DsglScreenHost( KeyModifiers.sync( shift = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT), control = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL), - meta = Keyboard.isKeyDown(Keyboard.KEY_LMETA) || Keyboard.isKeyDown(Keyboard.KEY_RMETA) + meta = Keyboard.isKeyDown(Keyboard.KEY_LMETA) || Keyboard.isKeyDown(Keyboard.KEY_RMETA), ) systemOverlayHost.onInputFrame(lastWidth, lastHeight) ColorPickerRuntime.engine.onFrame(lastWidth, lastHeight) @@ -734,9 +750,11 @@ abstract class DsglScreenHost( keyCode = keyCode, keyChar = keyChar, inspectorMouseX = inspectorMouseX, - inspectorMouseY = inspectorMouseY + inspectorMouseY = inspectorMouseY, ) - ) return + ) { + return + } } else { if (handleKeyboardKeyUp(keyCode, keyChar, inspectorMouseX, inspectorMouseY)) return } @@ -748,7 +766,7 @@ abstract class DsglScreenHost( keyCode: Int, keyChar: Char, inspectorMouseX: Int, - inspectorMouseY: Int + inspectorMouseY: Int, ): Boolean { if (!Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) && keyCode == Keyboard.KEY_F12) { inspector.toggle() @@ -783,7 +801,7 @@ abstract class DsglScreenHost( keyCode = keyCode, keyChar = keyChar, inspectorMouseX = inspectorMouseX, - inspectorMouseY = inspectorMouseY + inspectorMouseY = inspectorMouseY, ) ) { mc.dispatchKeypresses() @@ -812,10 +830,12 @@ abstract class DsglScreenHost( keyCode: Int, keyChar: Char, inspectorMouseX: Int, - inspectorMouseY: Int + inspectorMouseY: Int, ): Boolean { - val keyboardBlocked = inspector.active && ( - inspector.shouldConsumeKeyboard(inspectorMouseX, inspectorMouseY) || + val keyboardBlocked = + inspector.active && + ( + inspector.shouldConsumeKeyboard(inspectorMouseX, inspectorMouseY) || inspector.mode == InspectorMode.Locked ) if (keyboardBlocked) { @@ -848,7 +868,7 @@ abstract class DsglScreenHost( val mouseX: Int, val mouseY: Int, val dWheel: Int, - val mouseButton: Int + val mouseButton: Int, ) private fun prepareMouseInputTree(): DomTree? { @@ -863,14 +883,13 @@ abstract class DsglScreenHost( return tree } - private fun readMouseInputEvent(): MouseInputEvent { - return MouseInputEvent( + private fun readMouseInputEvent(): MouseInputEvent = + MouseInputEvent( mouseX = lastViewport.rawMouseToDsglX(Mouse.getEventX()), mouseY = lastViewport.rawMouseToDsglY(Mouse.getEventY()), dWheel = Mouse.getDWheel(), - mouseButton = Mouse.getEventButton() + mouseButton = Mouse.getEventButton(), ) - } private fun syncMouseInputFrame(tree: DomTree, inputEvent: MouseInputEvent) { inspector.onCursorMoved(inputEvent.mouseX, inputEvent.mouseY) @@ -878,19 +897,19 @@ abstract class DsglScreenHost( measureContext = adapter, viewportWidth = lastWidth, viewportHeight = lastHeight, - viewportScale = 1f + viewportScale = 1f, ) SelectRuntime.applicationEngine.onFrame( measureContext = adapter, viewportWidth = lastWidth, viewportHeight = lastHeight, - viewportScale = 1f + viewportScale = 1f, ) SelectRuntime.systemEngine.onFrame( measureContext = adapter, viewportWidth = lastWidth, viewportHeight = lastHeight, - viewportScale = 1f + viewportScale = 1f, ) systemOverlayHost.onInputFrame(lastWidth, lastHeight) inspectorPointerCaptured = inspector.isPointerCaptured @@ -899,7 +918,7 @@ abstract class DsglScreenHost( inspectedLayoutRevision = layoutRevision, cursorX = inputEvent.mouseX, cursorY = inputEvent.mouseY, - inspectorPointerCaptured = inspectorPointerCaptured + inspectorPointerCaptured = inspectorPointerCaptured, ) ColorPickerRuntime.engine.onFrame(lastWidth, lastHeight) refreshActiveColorSamplerOwner(tree.root) @@ -907,11 +926,12 @@ abstract class DsglScreenHost( private fun consumeOverlayPointerPhase(inputEvent: MouseInputEvent): Boolean { val appPressMove = inputEvent.mouseButton == -1 && eventButton != -1 - if (!appPressMove && consumeOverlayPointerEvent( + if (!appPressMove && + consumeOverlayPointerEvent( mouseX = inputEvent.mouseX, mouseY = inputEvent.mouseY, dWheel = inputEvent.dWheel, - mouseButton = inputEvent.mouseButton + mouseButton = inputEvent.mouseButton, ) ) { consumeOverlayPointerState(inputEvent.mouseX, inputEvent.mouseY) @@ -978,13 +998,14 @@ abstract class DsglScreenHost( val dy = inputEvent.mouseY - lastMouseY if (dx != 0 || dy != 0) { DndRuntime.engine.onMouseMove(tree.root, inputEvent.mouseX, inputEvent.mouseY) - val dragEvent = MouseDragEvent( - lastMouseX, - lastMouseY, - dx, - dy, - mappedButton - ) + val dragEvent = + MouseDragEvent( + lastMouseX, + lastMouseY, + dx, + dy, + mappedButton, + ) if (!DndRuntime.engine.isDragging) { dragEvent.target = dragCaptureTarget ?: hoverTarget EventBus.post(dragEvent) @@ -994,7 +1015,7 @@ abstract class DsglScreenHost( mouseY = inputEvent.mouseY, mouseDX = dx, mouseDY = dy, - button = mappedButton + button = mappedButton, ) } } @@ -1023,25 +1044,27 @@ abstract class DsglScreenHost( keyCode: Int, keyChar: Char, inspectorMouseX: Int, - inspectorMouseY: Int + inspectorMouseY: Int, ): Boolean { - val consumedBy = OverlayLayerContracts.firstInputConsumer( - canConsume = { layer -> - when (layer) { - UiLayerId.Debug -> debugOverlayHost.handleKeyDown(keyCode, keyChar) - UiLayerId.SystemOverlay -> consumeSystemOverlayKeyDown( - keyCode = keyCode, - keyChar = keyChar, - inspectorMouseX = inspectorMouseX, - inspectorMouseY = inspectorMouseY - ) - - UiLayerId.ApplicationOverlay -> consumeApplicationOverlayKeyDown(keyCode, keyChar) - UiLayerId.ApplicationRoot -> false - } - }, - isLayerInputEnabled = OverlayLayerDebugState::isInputEnabled - ) + val consumedBy = + OverlayLayerContracts.firstInputConsumer( + canConsume = { layer -> + when (layer) { + UiLayerId.Debug -> debugOverlayHost.handleKeyDown(keyCode, keyChar) + UiLayerId.SystemOverlay -> + consumeSystemOverlayKeyDown( + keyCode = keyCode, + keyChar = keyChar, + inspectorMouseX = inspectorMouseX, + inspectorMouseY = inspectorMouseY, + ) + + UiLayerId.ApplicationOverlay -> consumeApplicationOverlayKeyDown(keyCode, keyChar) + UiLayerId.ApplicationRoot -> false + } + }, + isLayerInputEnabled = OverlayLayerDebugState::isInputEnabled, + ) return consumedBy != null } @@ -1049,7 +1072,7 @@ abstract class DsglScreenHost( keyCode: Int, keyChar: Char, inspectorMouseX: Int, - inspectorMouseY: Int + inspectorMouseY: Int, ): Boolean { if (SelectRuntime.systemEngine.handleKeyDown(keyCode, keyChar)) { return true @@ -1057,8 +1080,10 @@ abstract class DsglScreenHost( if (systemOverlayHost.handleKeyDown(keyCode, keyChar)) { return true } - val keyboardBlocked = inspector.active && ( - inspector.shouldConsumeKeyboard(inspectorMouseX, inspectorMouseY) || + val keyboardBlocked = + inspector.active && + ( + inspector.shouldConsumeKeyboard(inspectorMouseX, inspectorMouseY) || inspector.mode == InspectorMode.Locked ) if (keyboardBlocked) { @@ -1088,45 +1113,49 @@ abstract class DsglScreenHost( mouseX: Int, mouseY: Int, dWheel: Int, - mouseButton: Int + mouseButton: Int, ): Boolean { val mappedButton = mapButton(mouseButton) val buttonPressed = Mouse.getEventButtonState() - val consumedBy = OverlayLayerContracts.firstInputConsumer( - canConsume = { layer -> - when (layer) { - UiLayerId.Debug -> consumeDebugPointerEvent( - mouseX = mouseX, - mouseY = mouseY, - dWheel = dWheel, - mappedButton = mappedButton, - mouseButton = mouseButton, - buttonPressed = buttonPressed - ) - - UiLayerId.SystemOverlay -> consumeSystemOverlayPointerEvent( - mouseX = mouseX, - mouseY = mouseY, - dWheel = dWheel, - mouseButton = mouseButton, - mappedButton = mappedButton, - buttonPressed = buttonPressed - ) - - UiLayerId.ApplicationOverlay -> consumeApplicationOverlayPointerEvent( - mouseX = mouseX, - mouseY = mouseY, - dWheel = dWheel, - mouseButton = mouseButton, - mappedButton = mappedButton, - buttonPressed = buttonPressed - ) - - UiLayerId.ApplicationRoot -> false - } - }, - isLayerInputEnabled = OverlayLayerDebugState::isInputEnabled - ) + val consumedBy = + OverlayLayerContracts.firstInputConsumer( + canConsume = { layer -> + when (layer) { + UiLayerId.Debug -> + consumeDebugPointerEvent( + mouseX = mouseX, + mouseY = mouseY, + dWheel = dWheel, + mappedButton = mappedButton, + mouseButton = mouseButton, + buttonPressed = buttonPressed, + ) + + UiLayerId.SystemOverlay -> + consumeSystemOverlayPointerEvent( + mouseX = mouseX, + mouseY = mouseY, + dWheel = dWheel, + mouseButton = mouseButton, + mappedButton = mappedButton, + buttonPressed = buttonPressed, + ) + + UiLayerId.ApplicationOverlay -> + consumeApplicationOverlayPointerEvent( + mouseX = mouseX, + mouseY = mouseY, + dWheel = dWheel, + mouseButton = mouseButton, + mappedButton = mappedButton, + buttonPressed = buttonPressed, + ) + + UiLayerId.ApplicationRoot -> false + } + }, + isLayerInputEnabled = OverlayLayerDebugState::isInputEnabled, + ) return consumedBy != null } @@ -1136,7 +1165,7 @@ abstract class DsglScreenHost( dWheel: Int, mappedButton: MouseButton?, mouseButton: Int, - buttonPressed: Boolean + buttonPressed: Boolean, ): Boolean { if (dWheel != 0 && debugOverlayHost.handleMouseWheel(mouseX, mouseY, dWheel)) { return true @@ -1160,7 +1189,7 @@ abstract class DsglScreenHost( dWheel: Int, mouseButton: Int, mappedButton: MouseButton?, - buttonPressed: Boolean + buttonPressed: Boolean, ): Boolean { if (dWheel != 0 && SelectRuntime.systemEngine.handleMouseWheel(mouseX, mouseY, dWheel)) { return true @@ -1169,19 +1198,21 @@ abstract class DsglScreenHost( return true } if (mouseButton != -1 && mappedButton != null) { - val consumedBySystemSelect = if (buttonPressed) { - SelectRuntime.systemEngine.handleMouseDown(mouseX, mouseY, mappedButton) - } else { - SelectRuntime.systemEngine.handleMouseUp(mouseX, mouseY, mappedButton) - } + val consumedBySystemSelect = + if (buttonPressed) { + SelectRuntime.systemEngine.handleMouseDown(mouseX, mouseY, mappedButton) + } else { + SelectRuntime.systemEngine.handleMouseUp(mouseX, mouseY, mappedButton) + } if (consumedBySystemSelect) { return true } - val consumedBySystemOverlay = if (buttonPressed) { - systemOverlayHost.handleMouseDown(mouseX, mouseY, mappedButton) - } else { - systemOverlayHost.handleMouseUp(mouseX, mouseY, mappedButton) - } + val consumedBySystemOverlay = + if (buttonPressed) { + systemOverlayHost.handleMouseDown(mouseX, mouseY, mappedButton) + } else { + systemOverlayHost.handleMouseUp(mouseX, mouseY, mappedButton) + } if (consumedBySystemOverlay) { return true } @@ -1206,7 +1237,7 @@ abstract class DsglScreenHost( dWheel: Int, mouseButton: Int, mappedButton: MouseButton?, - buttonPressed: Boolean + buttonPressed: Boolean, ): Boolean { val inlineSamplerOwnsSession = activeColorSamplerOwner is ActiveColorSamplerOwner.Inline if (!inlineSamplerOwnsSession) { @@ -1214,11 +1245,12 @@ abstract class DsglScreenHost( return true } if (mouseButton != -1 && mappedButton != null) { - val consumedByColorPicker = if (buttonPressed) { - ColorPickerRuntime.engine.handleMouseDown(mouseX, mouseY, mappedButton) - } else { - ColorPickerRuntime.engine.handleMouseUp(mouseX, mouseY, mappedButton) - } + val consumedByColorPicker = + if (buttonPressed) { + ColorPickerRuntime.engine.handleMouseDown(mouseX, mouseY, mappedButton) + } else { + ColorPickerRuntime.engine.handleMouseUp(mouseX, mouseY, mappedButton) + } if (consumedByColorPicker) { return true } @@ -1231,11 +1263,12 @@ abstract class DsglScreenHost( return true } if (mouseButton != -1 && mappedButton != null) { - val consumedByAppOverlay = if (buttonPressed) { - applicationOverlayHost.handleMouseDown(mouseX, mouseY, mappedButton) - } else { - applicationOverlayHost.handleMouseUp(mouseX, mouseY, mappedButton) - } + val consumedByAppOverlay = + if (buttonPressed) { + applicationOverlayHost.handleMouseDown(mouseX, mouseY, mappedButton) + } else { + applicationOverlayHost.handleMouseUp(mouseX, mouseY, mappedButton) + } if (consumedByAppOverlay) { return true } @@ -1250,19 +1283,21 @@ abstract class DsglScreenHost( return true } if (mouseButton != -1 && mappedButton != null) { - val consumedByContextMenu = if (buttonPressed) { - ContextMenuRuntime.engine.handleMouseDown(mouseX, mouseY, mappedButton) - } else { - ContextMenuRuntime.engine.handleMouseUp(mouseX, mouseY, mappedButton) - } + val consumedByContextMenu = + if (buttonPressed) { + ContextMenuRuntime.engine.handleMouseDown(mouseX, mouseY, mappedButton) + } else { + ContextMenuRuntime.engine.handleMouseUp(mouseX, mouseY, mappedButton) + } if (consumedByContextMenu) { return true } - val consumedBySelect = if (buttonPressed) { - SelectRuntime.applicationEngine.handleMouseDown(mouseX, mouseY, mappedButton) - } else { - SelectRuntime.applicationEngine.handleMouseUp(mouseX, mouseY, mappedButton) - } + val consumedBySelect = + if (buttonPressed) { + SelectRuntime.applicationEngine.handleMouseDown(mouseX, mouseY, mappedButton) + } else { + SelectRuntime.applicationEngine.handleMouseUp(mouseX, mouseY, mappedButton) + } if (consumedBySelect) { return true } @@ -1285,14 +1320,13 @@ abstract class DsglScreenHost( lastMouseY = mouseY } - private fun mapButton(button: Int): MouseButton? { - return when (button) { + private fun mapButton(button: Int): MouseButton? = + when (button) { 0 -> MouseButton.LEFT 1 -> MouseButton.RIGHT 2 -> MouseButton.MIDDLE else -> null } - } init { inspector.installColorPickerHost(systemOverlayHost.systemInspectorColorPickerPopupHost()) @@ -1307,20 +1341,19 @@ abstract class DsglScreenHost( if (focusedInline != null && focusedInline.wantsGlobalPointerInput()) { inlineByToken.putIfAbsent(colorSamplerToken(focusedInline), focusedInline) } - activeColorSamplerOwner = colorSamplerOwnershipRouter.update( - popupEyedropperActive = ColorPickerRuntime.engine.hasActiveEyedropper(), - inlineActiveTokens = inlineByToken.keys.toSet() - ) - activeInlineColorSamplerNode = when (val owner = activeColorSamplerOwner) { - is ActiveColorSamplerOwner.Inline -> inlineByToken[owner.token] - else -> null - } + activeColorSamplerOwner = + colorSamplerOwnershipRouter.update( + popupEyedropperActive = ColorPickerRuntime.engine.hasActiveEyedropper(), + inlineActiveTokens = inlineByToken.keys.toSet(), + ) + activeInlineColorSamplerNode = + when (val owner = activeColorSamplerOwner) { + is ActiveColorSamplerOwner.Inline -> inlineByToken[owner.token] + else -> null + } } - private fun collectActiveInlineColorSamplers( - node: DOMNode, - out: MutableMap - ) { + private fun collectActiveInlineColorSamplers(node: DOMNode, out: MutableMap) { if (node is ColorPickerInlineNode && node.wantsGlobalPointerInput()) { out.putIfAbsent(colorSamplerToken(node), node) } @@ -1329,9 +1362,7 @@ abstract class DsglScreenHost( } } - private fun colorSamplerToken(node: ColorPickerInlineNode): Any { - return node.key ?: node - } + private fun colorSamplerToken(node: ColorPickerInlineNode): Any = node.key ?: node private fun resolveForcedPointerTarget(): DOMNode? { if (activeColorSamplerOwner is ActiveColorSamplerOwner.Inline) { @@ -1352,7 +1383,7 @@ abstract class DsglScreenHost( inline.appendEyedropperOverlayCommands( viewportWidth = lastWidth.coerceAtLeast(1), viewportHeight = lastHeight.coerceAtLeast(1), - out = out + out = out, ) } } @@ -1362,7 +1393,9 @@ abstract class DsglScreenHost( if (OverlayLayerContracts.resolveTransientLayer(OverlayOwnerScope.System) == UiLayerId.SystemOverlay) { systemOverlayHost.captureSystemColorPickerEyedropperSample() } - if (OverlayLayerContracts.resolveTransientLayer(OverlayOwnerScope.Application) != UiLayerId.ApplicationOverlay) { + if (OverlayLayerContracts.resolveTransientLayer(OverlayOwnerScope.Application) != + UiLayerId.ApplicationOverlay + ) { return } when (activeColorSamplerOwner) { @@ -1491,11 +1524,7 @@ abstract class DsglScreenHost( } } - private fun findByKeyAndClass( - node: DOMNode, - key: Any, - cls: Class - ): DOMNode? { + private fun findByKeyAndClass(node: DOMNode, key: Any, cls: Class): DOMNode? { if (node.key == key && node.javaClass == cls) return node for (child in node.children) { val found = findByKeyAndClass(child, key, cls) @@ -1525,17 +1554,11 @@ abstract class DsglScreenHost( hoverTarget = chain.lastOrNull() } - private fun resolvePointerDownTarget(): DOMNode? { - return resolveForcedPointerTarget() ?: hoverTarget - } + private fun resolvePointerDownTarget(): DOMNode? = resolveForcedPointerTarget() ?: hoverTarget - private fun resolvePointerUpTarget(): DOMNode? { - return dragCaptureTarget ?: resolveForcedPointerTarget() ?: hoverTarget - } + private fun resolvePointerUpTarget(): DOMNode? = dragCaptureTarget ?: resolveForcedPointerTarget() ?: hoverTarget - private fun resolveClickTarget(): DOMNode? { - return hoverTarget - } + private fun resolveClickTarget(): DOMNode? = hoverTarget private fun resolveWheelTarget(): DOMNode? { val focused = FocusManager.focusedNode() @@ -1548,7 +1571,12 @@ abstract class DsglScreenHost( return hoverTarget } - private fun bubbleGenericWheel(target: DOMNode, mouseX: Int, mouseY: Int, delta: Int): Boolean { + private fun bubbleGenericWheel( + target: DOMNode, + mouseX: Int, + mouseY: Int, + delta: Int, + ): Boolean { var current: DOMNode? = target while (current != null) { if (current.handleGenericWheel(mouseX, mouseY, delta)) { @@ -1587,7 +1615,9 @@ abstract class DsglScreenHost( if (lastWidth > 0 && lastHeight > 0 && (rootBounds.width <= 0 || rootBounds.height <= 0)) { logPipelineError( key = "layout.$phase.invalidBounds", - message = "[DSGL] Layout commit produced invalid root bounds ${rootBounds.width}x${rootBounds.height} in $phase." + message = + "[DSGL] Layout commit produced invalid root bounds " + + "${rootBounds.width}x${rootBounds.height} in $phase.", ) return false } @@ -1597,7 +1627,7 @@ abstract class DsglScreenHost( } catch (error: Throwable) { logPipelineError( key = "layout.$phase", - message = "[DSGL] Layout commit failed in $phase; keeping previous frame: ${error.message}" + message = "[DSGL] Layout commit failed in $phase; keeping previous frame: ${error.message}", ) false } @@ -1607,13 +1637,16 @@ abstract class DsglScreenHost( tree: DomTree, rebuiltThisFrame: Boolean, layoutCommittedThisFrame: Boolean, - candidate: List + candidate: List, ): Boolean { val shape = validateCommandShape(candidate) if (!shape.valid) { logPipelineError( key = "shape.guard", - message = "[DSGL] Guarded invalid command shape (clip=${shape.clipDepth}, transform=${shape.transformDepth}, opacity=${shape.opacityDepth}); keeping previous frame." + message = + "[DSGL] Guarded invalid command shape " + + "(clip=${shape.clipDepth}, transform=${shape.transformDepth}, " + + "opacity=${shape.opacityDepth}); keeping previous frame.", ) return composedCommandsBuffer.isNotEmpty() } @@ -1623,7 +1656,7 @@ abstract class DsglScreenHost( if (!hasRenderableNodes(tree.root)) return false logPipelineError( key = "blank.guard", - message = "[DSGL] Guarded against blank rebuild frame; keeping previous commands." + message = "[DSGL] Guarded against blank rebuild frame; keeping previous commands.", ) return true } @@ -1632,7 +1665,7 @@ abstract class DsglScreenHost( val valid: Boolean, val clipDepth: Int, val transformDepth: Int, - val opacityDepth: Int + val opacityDepth: Int, ) private fun validateCommandShape(commands: List): CommandShape { @@ -1666,7 +1699,7 @@ abstract class DsglScreenHost( valid = clipDepth == 0 && transformDepth == 0 && opacityDepth == 0, clipDepth = clipDepth, transformDepth = transformDepth, - opacityDepth = opacityDepth + opacityDepth = opacityDepth, ) } @@ -1702,9 +1735,10 @@ abstract class DsglScreenHost( val styleStats = StyleEngine.lastStyleApplyReport() println( "[DSGL-PERF] frames=${paintStats.frames} commandRebuilds=${paintStats.commandRebuilds} " + - "chunkVisited=${paintStats.chunkNodesVisitedLastFrame} chunkRebuilt=${paintStats.chunkNodesRebuiltLastFrame} " + - "styled=${styleStats.visitedNodes} styleCacheHit=${styleStats.cacheHits} " + - "styleRecomputed=${styleStats.recomputedNodes} blankGuardSkips=$blankFrameGuardSkips" + "chunkVisited=${paintStats.chunkNodesVisitedLastFrame} " + + "chunkRebuilt=${paintStats.chunkNodesRebuiltLastFrame} " + + "styled=${styleStats.visitedNodes} styleCacheHit=${styleStats.cacheHits} " + + "styleRecomputed=${styleStats.recomputedNodes} blankGuardSkips=$blankFrameGuardSkips", ) } } diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/GlUtils.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/GlUtils.kt index d9ab9bf..151e317 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/GlUtils.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/GlUtils.kt @@ -6,10 +6,7 @@ import org.lwjgl.opengl.GL11 /** * Executes a block with an OpenGL matrix/attribute stack push/pop. */ -inline fun withStack( - attributes: List, - block: () -> Unit -) { +inline fun withStack(attributes: List, block: () -> Unit) { val attributesBitMask = attributes.reduce { a, b -> a or b } withStack(attributesBitMask) { block() } } @@ -17,10 +14,7 @@ inline fun withStack( /** * Executes a block with an OpenGL matrix/attribute stack push/pop. */ -inline fun withStack( - attributesBitMask: Int = 0, - block: () -> Unit -) { +inline fun withStack(attributesBitMask: Int = 0, block: () -> Unit) { if (attributesBitMask > 0) { GL11.glPushAttrib(attributesBitMask) } @@ -38,11 +32,7 @@ inline fun withStack( /** * Enables and disables GL capabilities for the duration of [block]. */ -inline fun withAttributes( - enable: List = emptyList(), - disable: List = emptyList(), - block: () -> Unit -) { +inline fun withAttributes(enable: List = emptyList(), disable: List = emptyList(), block: () -> Unit) { for (capability in enable) { GL11.glEnable(capability) } @@ -64,11 +54,7 @@ inline fun withAttributes( /** * Executes a block with an OpenGL matrix/attribute stack push/pop. */ -inline fun withAttributes( - enableBitMask: Int? = null, - disableBitMask: Int? = null, - block: () -> Unit -) { +inline fun withAttributes(enableBitMask: Int? = null, disableBitMask: Int? = null, block: () -> Unit) { enableBitMask?.let { GL11.glEnable(it) } disableBitMask?.let { GL11.glDisable(it) } try { diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/Mc1710UiAdapter.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/Mc1710UiAdapter.kt index 7503233..5b8653c 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/Mc1710UiAdapter.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/Mc1710UiAdapter.kt @@ -24,19 +24,22 @@ import javax.imageio.ImageIO /** * Minecraft 1.7.10 adapter that turns DSGL render commands into Minecraft calls. */ -class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : UiMeasureContext { +class Mc1710UiAdapter( + private val mc: Minecraft, + var paintsCount: Long = 0L, +) : UiMeasureContext { private enum class ReadbackApi { OpenGl30, ArbFramebufferObject, ExtFramebufferObject, - Legacy + Legacy, } private data class ReadbackBindingState( val readFramebufferBinding: Int, val drawFramebufferBinding: Int, val framebufferBinding: Int, - val currentReadBuffer: Int + val currentReadBuffer: Int, ) { val usingFramebufferObject: Boolean get() = readFramebufferBinding != 0 @@ -45,19 +48,19 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U private data class ReadbackSetup( val previousReadBuffer: Int, val appliedReadBuffer: Int, - val shouldRestore: Boolean + val shouldRestore: Boolean, ) private data class FramebufferBindingSnapshot( val readFramebufferBinding: Int, val drawFramebufferBinding: Int, - val framebufferBinding: Int + val framebufferBinding: Int, ) private data class SceneTextureSource( val textureId: Int, val textureWidth: Int, - val textureHeight: Int + val textureHeight: Int, ) private data class MagnifierCaptureShader( @@ -67,21 +70,23 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U val sourceSizeUniform: Int, val viewportSizeUniform: Int, val sourceTextureSizeUniform: Int, - val fallbackColorUniform: Int + val fallbackColorUniform: Int, ) companion object { private val imageCache: MutableMap = HashMap() private val dynamicTexturesCache: MutableMap = HashMap() - private val MAGNIFIER_CAPTURE_VERTEX_SHADER: String = """ + private val MAGNIFIER_CAPTURE_VERTEX_SHADER: String = + """ #version 120 varying vec2 vUv; void main() { gl_Position = gl_Vertex; vUv = gl_MultiTexCoord0.xy; } - """.trimIndent() - private val MAGNIFIER_CAPTURE_FRAGMENT_SHADER: String = """ + """.trimIndent() + private val MAGNIFIER_CAPTURE_FRAGMENT_SHADER: String = + """ #version 120 uniform sampler2D uSourceTexture; uniform vec2 uSourceOriginTopLeft; @@ -111,7 +116,7 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U vec4 sampled = texture2D(uSourceTexture, sourceUv); gl_FragColor = vec4(sampled.rgb, 1.0); } - """.trimIndent() + """.trimIndent() } private val itemRenderer: RenderItem = RenderItem() @@ -119,7 +124,9 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U private val opacityStack: MutableList = ArrayList(8) private var opacityMultiplier: Float = 1f private val errorLogTimes: MutableMap = linkedMapOf() - private val readbackDiagnosticsVerbose: Boolean = java.lang.Boolean.getBoolean("dsgl.readback.diagnostics.verbose") + private val readbackDiagnosticsVerbose: Boolean = + java.lang.Boolean + .getBoolean("dsgl.readback.diagnostics.verbose") private val readbackApi: ReadbackApi by lazy(LazyThreadSafetyMode.NONE) { resolveReadbackApi() } private val samplePixelBuffer = BufferUtils.createByteBuffer(4) @@ -145,29 +152,25 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U private var cachedDisplayHeight: Int = -1 override fun measureText(text: String): Int = textRenderer.measureText(text, null, null) - override fun measureText(text: String, fontId: String?, fontSize: Int?): Int { - return textRenderer.measureText(text, fontId, fontSize) - } + + override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = + textRenderer.measureText(text, fontId, fontSize) + override fun measureTextRange( text: String, startIndex: Int, endIndexExclusive: Int, fontId: String?, - fontSize: Int? - ): Int { - return textRenderer.measureTextRange(text, startIndex, endIndexExclusive, fontId, fontSize) - } + fontSize: Int?, + ): Int = textRenderer.measureTextRange(text, startIndex, endIndexExclusive, fontId, fontSize) override val fontHeight: Int get() = textRenderer.lineHeight(FontRegistry.DEFAULT_FONT_ID, null) - override fun fontHeight(fontId: String?, fontSize: Int?): Int { - return textRenderer.lineHeight(fontId, fontSize) - } + override fun fontHeight(fontId: String?, fontSize: Int?): Int = textRenderer.lineHeight(fontId, fontSize) - override fun fontLineMetrics(fontId: String?, fontSize: Int?): FontLineMetrics? { - return textRenderer.fontLineMetrics(fontId, fontSize) - } + override fun fontLineMetrics(fontId: String?, fontSize: Int?): FontLineMetrics? = + textRenderer.fontLineMetrics(fontId, fontSize) fun viewport(): Viewport { val displayWidth = mc.displayWidth.coerceAtLeast(1) @@ -175,13 +178,14 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U if (displayWidth != cachedDisplayWidth || displayHeight != cachedDisplayHeight) { cachedDisplayWidth = displayWidth cachedDisplayHeight = displayHeight - cachedViewport = Viewport( - width = displayWidth, - height = displayHeight, - scale = 1f, - x = 0, - y = 0 - ) + cachedViewport = + Viewport( + width = displayWidth, + height = displayHeight, + scale = 1f, + x = 0, + y = 0, + ) } return cachedViewport } @@ -200,7 +204,7 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U sourceY = y, sourceWidth = 1, sourceHeight = 1, - setup = setup + setup = setup, ) } try { @@ -218,7 +222,13 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U } } - fun sampleScreenArea(x: Int, y: Int, width: Int, height: Int, outArgb: IntArray): Boolean { + fun sampleScreenArea( + x: Int, + y: Int, + width: Int, + height: Int, + outArgb: IntArray, + ): Boolean { if (width <= 0 || height <= 0) return false val required = width * height if (outArgb.size < required) return false @@ -253,7 +263,7 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U sourceY = srcY, sourceWidth = srcW, sourceHeight = srcH, - setup = setup + setup = setup, ) } try { @@ -302,7 +312,9 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U if (readbackDiagnosticsVerbose) { logRateLimited( key = "magnifier:capture:fallback", - message = "[DSGL-Magnifier] Falling back to solid fill preview. sourceTexture=${sceneTextureSource?.textureId ?: 0} shaderReady=${shader != null}" + message = + "[DSGL-Magnifier] Falling back to solid fill preview. " + + "sourceTexture=${sceneTextureSource?.textureId ?: 0} shaderReady=${shader != null}", ) } return @@ -342,18 +354,18 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U ARBShaderObjects.glUniform2fARB( shader.sourceOriginUniform, command.sourceX.toFloat(), - command.sourceY.toFloat() + command.sourceY.toFloat(), ) ARBShaderObjects.glUniform2fARB(shader.sourceSizeUniform, sourceWidth.toFloat(), sourceHeight.toFloat()) ARBShaderObjects.glUniform2fARB( shader.viewportSizeUniform, viewport.width.toFloat(), - viewport.height.toFloat() + viewport.height.toFloat(), ) ARBShaderObjects.glUniform2fARB( shader.sourceTextureSizeUniform, sceneTextureSource.textureWidth.toFloat(), - sceneTextureSource.textureHeight.toFloat() + sceneTextureSource.textureHeight.toFloat(), ) val fallbackAlpha = ((command.fallbackColor ushr 24) and 0xFF) / 255f val fallbackRed = ((command.fallbackColor ushr 16) and 0xFF) / 255f @@ -364,7 +376,7 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U fallbackRed, fallbackGreen, fallbackBlue, - fallbackAlpha + fallbackAlpha, ) GL11.glColor4f(1f, 1f, 1f, 1f) GL11.glBegin(GL11.GL_QUADS) @@ -382,7 +394,7 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U if (readbackDiagnosticsVerbose) { logRateLimited( key = "magnifier:capture:error", - message = "[DSGL-Magnifier] GPU capture failed: ${error.message ?: error::class.java.simpleName}" + message = "[DSGL-Magnifier] GPU capture failed: ${error.message ?: error::class.java.simpleName}", ) } } finally { @@ -395,11 +407,12 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U previousViewportX, previousViewportY, previousViewportWidth, - previousViewportHeight + previousViewportHeight, ) } capturedRegionValid = - renderingSucceeded || fillCapturedRegionFallbackTexture(command.fallbackColor, sourceWidth, sourceHeight) + renderingSucceeded || + fillCapturedRegionFallbackTexture(command.fallbackColor, sourceWidth, sourceHeight) } private fun drawCapturedScreenRegion(command: RenderCommand.DrawCapturedScreenRegion) { @@ -410,7 +423,7 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U command.y, command.x + command.width, command.y + command.height, - applyOpacity(capturedRegionFallbackColor) + applyOpacity(capturedRegionFallbackColor), ) return } @@ -473,7 +486,7 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U command.y, command.x + command.width, command.y + command.height, - applyOpacity(command.lightColor) + applyOpacity(command.lightColor), ) return } @@ -516,10 +529,26 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U val light = argbToRgbaBytes(lightColor) val dark = argbToRgbaBytes(darkColor) checkerTextureUploadBuffer.clear() - checkerTextureUploadBuffer.put(light[0]).put(light[1]).put(light[2]).put(light[3]) - checkerTextureUploadBuffer.put(dark[0]).put(dark[1]).put(dark[2]).put(dark[3]) - checkerTextureUploadBuffer.put(dark[0]).put(dark[1]).put(dark[2]).put(dark[3]) - checkerTextureUploadBuffer.put(light[0]).put(light[1]).put(light[2]).put(light[3]) + checkerTextureUploadBuffer + .put(light[0]) + .put(light[1]) + .put(light[2]) + .put(light[3]) + checkerTextureUploadBuffer + .put(dark[0]) + .put(dark[1]) + .put(dark[2]) + .put(dark[3]) + checkerTextureUploadBuffer + .put(dark[0]) + .put(dark[1]) + .put(dark[2]) + .put(dark[3]) + checkerTextureUploadBuffer + .put(light[0]) + .put(light[1]) + .put(light[2]) + .put(light[3]) checkerTextureUploadBuffer.flip() GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1) @@ -532,21 +561,23 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, - checkerTextureUploadBuffer + checkerTextureUploadBuffer, ) checkerTextureCache[key] = textureId while (checkerTextureCache.size > maxCheckerTextures) { - val eldest = checkerTextureCache.entries.iterator().next() + val eldest = + checkerTextureCache.entries + .iterator() + .next() GL11.glDeleteTextures(eldest.value) checkerTextureCache.remove(eldest.key) } return textureId } - private fun checkerTextureKey(lightColor: Int, darkColor: Int): Long { - return (lightColor.toLong() shl 32) xor (darkColor.toLong() and 0xFFFF_FFFFL) - } + private fun checkerTextureKey(lightColor: Int, darkColor: Int): Long = + (lightColor.toLong() shl 32) xor (darkColor.toLong() and 0xFFFF_FFFFL) private fun ensureCapturedRegionTexture(width: Int, height: Int) { if (capturedRegionTextureId == 0) { @@ -574,7 +605,7 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, - null as java.nio.ByteBuffer? + null as java.nio.ByteBuffer?, ) } @@ -587,11 +618,12 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U private fun resolveActiveSceneTextureSource(): SceneTextureSource? { val state = detectReadbackBindingState() if (!state.usingFramebufferObject) return null - val colorAttachment = if (isColorAttachmentReadBuffer(state.currentReadBuffer)) { - state.currentReadBuffer - } else { - defaultColorAttachmentReadBuffer() - } + val colorAttachment = + if (isColorAttachmentReadBuffer(state.currentReadBuffer)) { + state.currentReadBuffer + } else { + defaultColorAttachmentReadBuffer() + } val objectType = getFramebufferAttachmentObjectType(colorAttachment) if (objectType != GL11.GL_TEXTURE) return null val textureId = getFramebufferAttachmentObjectName(colorAttachment) @@ -608,87 +640,95 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U } } - private fun getFramebufferAttachmentObjectType(colorAttachment: Int): Int { - return when (readbackApi) { - ReadbackApi.OpenGl30 -> GL30.glGetFramebufferAttachmentParameteri( - GL30.GL_READ_FRAMEBUFFER, - colorAttachment, - GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE - ) + private fun getFramebufferAttachmentObjectType(colorAttachment: Int): Int = + when (readbackApi) { + ReadbackApi.OpenGl30 -> + GL30.glGetFramebufferAttachmentParameteri( + GL30.GL_READ_FRAMEBUFFER, + colorAttachment, + GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, + ) - ReadbackApi.ArbFramebufferObject -> ARBFramebufferObject.glGetFramebufferAttachmentParameteri( - ARBFramebufferObject.GL_READ_FRAMEBUFFER, - colorAttachment, - ARBFramebufferObject.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE - ) + ReadbackApi.ArbFramebufferObject -> + ARBFramebufferObject.glGetFramebufferAttachmentParameteri( + ARBFramebufferObject.GL_READ_FRAMEBUFFER, + colorAttachment, + ARBFramebufferObject.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, + ) - ReadbackApi.ExtFramebufferObject -> EXTFramebufferObject.glGetFramebufferAttachmentParameteriEXT( - EXTFramebufferObject.GL_FRAMEBUFFER_EXT, - colorAttachment, - EXTFramebufferObject.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE_EXT - ) + ReadbackApi.ExtFramebufferObject -> + EXTFramebufferObject.glGetFramebufferAttachmentParameteriEXT( + EXTFramebufferObject.GL_FRAMEBUFFER_EXT, + colorAttachment, + EXTFramebufferObject.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE_EXT, + ) ReadbackApi.Legacy -> GL11.GL_NONE } - } - private fun getFramebufferAttachmentObjectName(colorAttachment: Int): Int { - return when (readbackApi) { - ReadbackApi.OpenGl30 -> GL30.glGetFramebufferAttachmentParameteri( - GL30.GL_READ_FRAMEBUFFER, - colorAttachment, - GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME - ) + private fun getFramebufferAttachmentObjectName(colorAttachment: Int): Int = + when (readbackApi) { + ReadbackApi.OpenGl30 -> + GL30.glGetFramebufferAttachmentParameteri( + GL30.GL_READ_FRAMEBUFFER, + colorAttachment, + GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, + ) - ReadbackApi.ArbFramebufferObject -> ARBFramebufferObject.glGetFramebufferAttachmentParameteri( - ARBFramebufferObject.GL_READ_FRAMEBUFFER, - colorAttachment, - ARBFramebufferObject.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME - ) + ReadbackApi.ArbFramebufferObject -> + ARBFramebufferObject.glGetFramebufferAttachmentParameteri( + ARBFramebufferObject.GL_READ_FRAMEBUFFER, + colorAttachment, + ARBFramebufferObject.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, + ) - ReadbackApi.ExtFramebufferObject -> EXTFramebufferObject.glGetFramebufferAttachmentParameteriEXT( - EXTFramebufferObject.GL_FRAMEBUFFER_EXT, - colorAttachment, - EXTFramebufferObject.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME_EXT - ) + ReadbackApi.ExtFramebufferObject -> + EXTFramebufferObject.glGetFramebufferAttachmentParameteriEXT( + EXTFramebufferObject.GL_FRAMEBUFFER_EXT, + colorAttachment, + EXTFramebufferObject.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME_EXT, + ) ReadbackApi.Legacy -> 0 } - } private fun ensureMagnifierCaptureShader(): MagnifierCaptureShader? { magnifierCaptureShader?.let { return it } if (magnifierCaptureShaderInitFailed) return null return try { - val vertexShader = compileShaderObject( - type = ARBVertexShader.GL_VERTEX_SHADER_ARB, - source = MAGNIFIER_CAPTURE_VERTEX_SHADER - ) - val fragmentShader = compileShaderObject( - type = ARBFragmentShader.GL_FRAGMENT_SHADER_ARB, - source = MAGNIFIER_CAPTURE_FRAGMENT_SHADER - ) + val vertexShader = + compileShaderObject( + type = ARBVertexShader.GL_VERTEX_SHADER_ARB, + source = MAGNIFIER_CAPTURE_VERTEX_SHADER, + ) + val fragmentShader = + compileShaderObject( + type = ARBFragmentShader.GL_FRAGMENT_SHADER_ARB, + source = MAGNIFIER_CAPTURE_FRAGMENT_SHADER, + ) val program = ARBShaderObjects.glCreateProgramObjectARB() ARBShaderObjects.glAttachObjectARB(program, vertexShader) ARBShaderObjects.glAttachObjectARB(program, fragmentShader) ARBShaderObjects.glLinkProgramARB(program) - val linkStatus = ARBShaderObjects.glGetObjectParameteriARB( - program, - ARBShaderObjects.GL_OBJECT_LINK_STATUS_ARB - ) + val linkStatus = + ARBShaderObjects.glGetObjectParameteriARB( + program, + ARBShaderObjects.GL_OBJECT_LINK_STATUS_ARB, + ) if (linkStatus == GL11.GL_FALSE) { val info = ARBShaderObjects.glGetInfoLogARB(program, 4096) throw IllegalStateException("Magnifier shader link failed: $info") } - val shader = MagnifierCaptureShader( - programId = program, - sourceTextureUniform = ARBShaderObjects.glGetUniformLocationARB(program, "uSourceTexture"), - sourceOriginUniform = ARBShaderObjects.glGetUniformLocationARB(program, "uSourceOriginTopLeft"), - sourceSizeUniform = ARBShaderObjects.glGetUniformLocationARB(program, "uSourceSize"), - viewportSizeUniform = ARBShaderObjects.glGetUniformLocationARB(program, "uViewportSize"), - sourceTextureSizeUniform = ARBShaderObjects.glGetUniformLocationARB(program, "uSourceTextureSize"), - fallbackColorUniform = ARBShaderObjects.glGetUniformLocationARB(program, "uFallbackColor") - ) + val shader = + MagnifierCaptureShader( + programId = program, + sourceTextureUniform = ARBShaderObjects.glGetUniformLocationARB(program, "uSourceTexture"), + sourceOriginUniform = ARBShaderObjects.glGetUniformLocationARB(program, "uSourceOriginTopLeft"), + sourceSizeUniform = ARBShaderObjects.glGetUniformLocationARB(program, "uSourceSize"), + viewportSizeUniform = ARBShaderObjects.glGetUniformLocationARB(program, "uViewportSize"), + sourceTextureSizeUniform = ARBShaderObjects.glGetUniformLocationARB(program, "uSourceTextureSize"), + fallbackColorUniform = ARBShaderObjects.glGetUniformLocationARB(program, "uFallbackColor"), + ) magnifierCaptureShader = shader shader } catch (error: Throwable) { @@ -696,7 +736,9 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U if (readbackDiagnosticsVerbose) { logRateLimited( key = "magnifier:shader:init", - message = "[DSGL-Magnifier] Failed to initialize capture shader: ${error.message ?: error::class.java.simpleName}" + message = + "[DSGL-Magnifier] Failed to initialize capture shader: " + + "${error.message ?: error::class.java.simpleName}", ) } null @@ -707,10 +749,11 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U val shader = ARBShaderObjects.glCreateShaderObjectARB(type) ARBShaderObjects.glShaderSourceARB(shader, source) ARBShaderObjects.glCompileShaderARB(shader) - val compileStatus = ARBShaderObjects.glGetObjectParameteriARB( - shader, - ARBShaderObjects.GL_OBJECT_COMPILE_STATUS_ARB - ) + val compileStatus = + ARBShaderObjects.glGetObjectParameteriARB( + shader, + ARBShaderObjects.GL_OBJECT_COMPILE_STATUS_ARB, + ) if (compileStatus == GL11.GL_FALSE) { val info = ARBShaderObjects.glGetInfoLogARB(shader, 4096) throw IllegalStateException("Magnifier shader compile failed: $info") @@ -718,22 +761,20 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U return shader } - private fun generateFramebufferObject(): Int { - return when (readbackApi) { + private fun generateFramebufferObject(): Int = + when (readbackApi) { ReadbackApi.OpenGl30 -> GL30.glGenFramebuffers() ReadbackApi.ArbFramebufferObject -> ARBFramebufferObject.glGenFramebuffers() ReadbackApi.ExtFramebufferObject -> EXTFramebufferObject.glGenFramebuffersEXT() ReadbackApi.Legacy -> 0 } - } - private fun snapshotFramebufferBindings(): FramebufferBindingSnapshot { - return FramebufferBindingSnapshot( + private fun snapshotFramebufferBindings(): FramebufferBindingSnapshot = + FramebufferBindingSnapshot( readFramebufferBinding = currentReadFramebufferBinding(), drawFramebufferBinding = currentDrawFramebufferBinding(), - framebufferBinding = currentFramebufferBinding() + framebufferBinding = currentFramebufferBinding(), ) - } private fun restoreFramebufferBindings(snapshot: FramebufferBindingSnapshot) { when (readbackApi) { @@ -745,18 +786,18 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U ReadbackApi.ArbFramebufferObject -> { ARBFramebufferObject.glBindFramebuffer( ARBFramebufferObject.GL_READ_FRAMEBUFFER, - snapshot.readFramebufferBinding + snapshot.readFramebufferBinding, ) ARBFramebufferObject.glBindFramebuffer( ARBFramebufferObject.GL_DRAW_FRAMEBUFFER, - snapshot.drawFramebufferBinding + snapshot.drawFramebufferBinding, ) } ReadbackApi.ExtFramebufferObject -> { EXTFramebufferObject.glBindFramebufferEXT( EXTFramebufferObject.GL_FRAMEBUFFER_EXT, - snapshot.framebufferBinding + snapshot.framebufferBinding, ) } @@ -767,15 +808,17 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U private fun bindDrawFramebuffer(framebufferId: Int) { when (readbackApi) { ReadbackApi.OpenGl30 -> GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, framebufferId) - ReadbackApi.ArbFramebufferObject -> ARBFramebufferObject.glBindFramebuffer( - ARBFramebufferObject.GL_DRAW_FRAMEBUFFER, - framebufferId - ) + ReadbackApi.ArbFramebufferObject -> + ARBFramebufferObject.glBindFramebuffer( + ARBFramebufferObject.GL_DRAW_FRAMEBUFFER, + framebufferId, + ) - ReadbackApi.ExtFramebufferObject -> EXTFramebufferObject.glBindFramebufferEXT( - EXTFramebufferObject.GL_FRAMEBUFFER_EXT, - framebufferId - ) + ReadbackApi.ExtFramebufferObject -> + EXTFramebufferObject.glBindFramebufferEXT( + EXTFramebufferObject.GL_FRAMEBUFFER_EXT, + framebufferId, + ) ReadbackApi.Legacy -> Unit } @@ -783,54 +826,56 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U private fun attachCapturedRegionTextureToFramebuffer() { when (readbackApi) { - ReadbackApi.OpenGl30 -> GL30.glFramebufferTexture2D( - GL30.GL_DRAW_FRAMEBUFFER, - GL30.GL_COLOR_ATTACHMENT0, - GL11.GL_TEXTURE_2D, - capturedRegionTextureId, - 0 - ) + ReadbackApi.OpenGl30 -> + GL30.glFramebufferTexture2D( + GL30.GL_DRAW_FRAMEBUFFER, + GL30.GL_COLOR_ATTACHMENT0, + GL11.GL_TEXTURE_2D, + capturedRegionTextureId, + 0, + ) - ReadbackApi.ArbFramebufferObject -> ARBFramebufferObject.glFramebufferTexture2D( - ARBFramebufferObject.GL_DRAW_FRAMEBUFFER, - ARBFramebufferObject.GL_COLOR_ATTACHMENT0, - GL11.GL_TEXTURE_2D, - capturedRegionTextureId, - 0 - ) + ReadbackApi.ArbFramebufferObject -> + ARBFramebufferObject.glFramebufferTexture2D( + ARBFramebufferObject.GL_DRAW_FRAMEBUFFER, + ARBFramebufferObject.GL_COLOR_ATTACHMENT0, + GL11.GL_TEXTURE_2D, + capturedRegionTextureId, + 0, + ) - ReadbackApi.ExtFramebufferObject -> EXTFramebufferObject.glFramebufferTexture2DEXT( - EXTFramebufferObject.GL_FRAMEBUFFER_EXT, - EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT, - GL11.GL_TEXTURE_2D, - capturedRegionTextureId, - 0 - ) + ReadbackApi.ExtFramebufferObject -> + EXTFramebufferObject.glFramebufferTexture2DEXT( + EXTFramebufferObject.GL_FRAMEBUFFER_EXT, + EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT, + GL11.GL_TEXTURE_2D, + capturedRegionTextureId, + 0, + ) ReadbackApi.Legacy -> Unit } } - private fun isCurrentFramebufferComplete(): Boolean { - return when (readbackApi) { - ReadbackApi.OpenGl30 -> GL30.glCheckFramebufferStatus(GL30.GL_DRAW_FRAMEBUFFER) == GL30.GL_FRAMEBUFFER_COMPLETE - ReadbackApi.ArbFramebufferObject -> ARBFramebufferObject.glCheckFramebufferStatus( - ARBFramebufferObject.GL_DRAW_FRAMEBUFFER - ) == ARBFramebufferObject.GL_FRAMEBUFFER_COMPLETE + private fun isCurrentFramebufferComplete(): Boolean = + when (readbackApi) { + ReadbackApi.OpenGl30 -> + GL30.glCheckFramebufferStatus(GL30.GL_DRAW_FRAMEBUFFER) == + GL30.GL_FRAMEBUFFER_COMPLETE + ReadbackApi.ArbFramebufferObject -> + ARBFramebufferObject.glCheckFramebufferStatus( + ARBFramebufferObject.GL_DRAW_FRAMEBUFFER, + ) == ARBFramebufferObject.GL_FRAMEBUFFER_COMPLETE - ReadbackApi.ExtFramebufferObject -> EXTFramebufferObject.glCheckFramebufferStatusEXT( - EXTFramebufferObject.GL_FRAMEBUFFER_EXT - ) == EXTFramebufferObject.GL_FRAMEBUFFER_COMPLETE_EXT + ReadbackApi.ExtFramebufferObject -> + EXTFramebufferObject.glCheckFramebufferStatusEXT( + EXTFramebufferObject.GL_FRAMEBUFFER_EXT, + ) == EXTFramebufferObject.GL_FRAMEBUFFER_COMPLETE_EXT ReadbackApi.Legacy -> false } - } - private fun fillCapturedRegionFallbackTexture( - fallbackColor: Int, - width: Int, - height: Int - ): Boolean { + private fun fillCapturedRegionFallbackTexture(fallbackColor: Int, width: Int, height: Int): Boolean { val snapshot = snapshotFramebufferBindings() val previousReadBuffer = GL11.glGetInteger(GL11.GL_READ_BUFFER) val previousDrawBuffer = GL11.glGetInteger(GL11.GL_DRAW_BUFFER) @@ -866,7 +911,7 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U previousClearRed, previousClearGreen, previousClearBlue, - previousClearAlpha + previousClearAlpha, ) GL11.glReadBuffer(previousReadBuffer) GL11.glDrawBuffer(previousDrawBuffer) @@ -875,7 +920,7 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U previousViewportX, previousViewportY, previousViewportWidth, - previousViewportHeight + previousViewportHeight, ) } } @@ -886,8 +931,11 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U } private fun viewportXFromSnapshot(): Int = glIntStateQueryBuffer.get(0) + private fun viewportYFromSnapshot(): Int = glIntStateQueryBuffer.get(1) + private fun viewportWidthFromSnapshot(): Int = glIntStateQueryBuffer.get(2) + private fun viewportHeightFromSnapshot(): Int = glIntStateQueryBuffer.get(3) private fun snapshotClearColorState() { @@ -896,8 +944,11 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U } private fun clearRedFromSnapshot(): Float = glFloatStateQueryBuffer.get(0) + private fun clearGreenFromSnapshot(): Float = glFloatStateQueryBuffer.get(1) + private fun clearBlueFromSnapshot(): Float = glFloatStateQueryBuffer.get(2) + private fun clearAlphaFromSnapshot(): Float = glFloatStateQueryBuffer.get(3) private fun argbToRgbaBytes(argb: Int, forceOpaqueAlpha: Boolean = false): ByteArray { @@ -938,7 +989,7 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U command.y, command.x + command.width, command.y + command.height, - applyOpacity(command.color) + applyOpacity(command.color), ) } @@ -948,7 +999,7 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U y = command.y, width = command.width, height = command.height, - hueDeg = command.hueDeg + hueDeg = command.hueDeg, ) } @@ -957,7 +1008,7 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U x = command.x, y = command.y, width = command.width, - height = command.height + height = command.height, ) } @@ -967,7 +1018,7 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U y = command.y, width = command.width, height = command.height, - rgbColor = command.rgbColor + rgbColor = command.rgbColor, ) } @@ -979,17 +1030,19 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U try { textRenderer.draw( command = command, - opacityMultiplier = opacityMultiplier + opacityMultiplier = opacityMultiplier, ) } catch (error: LinkageError) { logRateLimited( key = "drawText:linkage", - message = "[DSGL] Skipping DrawText due linkage error in text renderer: ${error.message}" + message = + "[DSGL] Skipping DrawText due linkage error in text renderer: " + + "${error.message}", ) } catch (error: Throwable) { logRateLimited( key = "drawText:runtime", - message = "[DSGL] Skipping DrawText due renderer error: ${error.message}" + message = "[DSGL] Skipping DrawText due renderer error: ${error.message}", ) } } @@ -1007,7 +1060,7 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U command.width, command.height, command.width.toFloat(), - command.height.toFloat() + command.height.toFloat(), ) } @@ -1028,23 +1081,24 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U size = command.size, width = command.width, rotY = command.rotYDeg, - rotX = command.rotXDeg + rotX = command.rotXDeg, ) } is RenderCommand.PushClip -> { - val transformedClip = transformStack.resolveClipRect( - x = command.x, - y = command.y, - width = command.width, - height = command.height - ) + val transformedClip = + transformStack.resolveClipRect( + x = command.x, + y = command.y, + width = command.width, + height = command.height, + ) pushClip( viewport = viewport, guiX = transformedClip.x, guiY = transformedClip.y, guiWidth = transformedClip.width, - guiHeight = transformedClip.height + guiHeight = transformedClip.height, ) } @@ -1102,7 +1156,13 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U return (color and 0x00FF_FFFF) or (scaled shl 24) } - private fun drawColorField(x: Int, y: Int, width: Int, height: Int, hueDeg: Float) { + private fun drawColorField( + x: Int, + y: Int, + width: Int, + height: Int, + hueDeg: Float, + ) { if (width <= 0 || height <= 0) return val normalizedHue = ((hueDeg % 360f) + 360f) % 360f @@ -1110,19 +1170,30 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U drawGradientBlock { drawHorizontalGradientRectRaw( - x, y, width, height, + x, + y, + width, + height, applyOpacity(0xFFFFFFFF.toInt()), - applyOpacity(hueColor) + applyOpacity(hueColor), ) drawVerticalGradientRectRaw( - x, y, width, height, + x, + y, + width, + height, applyOpacity(0x00000000), - applyOpacity(0xFF000000.toInt()) + applyOpacity(0xFF000000.toInt()), ) } } - private fun drawHueBar(x: Int, y: Int, width: Int, height: Int) { + private fun drawHueBar( + x: Int, + y: Int, + width: Int, + height: Int, + ) { if (width <= 0 || height <= 0) return val segments = 6 val hueStops = floatArrayOf(0f, 60f, 120f, 180f, 240f, 300f, 360f) @@ -1138,7 +1209,13 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U } } - private fun drawAlphaBar(x: Int, y: Int, width: Int, height: Int, rgbColor: Int) { + private fun drawAlphaBar( + x: Int, + y: Int, + width: Int, + height: Int, + rgbColor: Int, + ) { if (width <= 0 || height <= 0) return val rgbOnly = rgbColor and 0x00FF_FFFF val leftColor = applyOpacity(rgbOnly) @@ -1146,7 +1223,14 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U drawHorizontalGradientRect(x, y, width, height, leftColor, rightColor) } - private fun drawHorizontalGradientRect(x: Int, y: Int, width: Int, height: Int, leftColor: Int, rightColor: Int) { + private fun drawHorizontalGradientRect( + x: Int, + y: Int, + width: Int, + height: Int, + leftColor: Int, + rightColor: Int, + ) { if (width <= 0 || height <= 0) return GL11.glDisable(GL11.GL_TEXTURE_2D) GL11.glEnable(GL11.GL_BLEND) @@ -1158,7 +1242,14 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U GL11.glColor4f(1f, 1f, 1f, 1f) } - private fun drawVerticalGradientRect(x: Int, y: Int, width: Int, height: Int, topColor: Int, bottomColor: Int) { + private fun drawVerticalGradientRect( + x: Int, + y: Int, + width: Int, + height: Int, + topColor: Int, + bottomColor: Int, + ) { if (width <= 0 || height <= 0) return GL11.glDisable(GL11.GL_TEXTURE_2D) GL11.glDisable(GL11.GL_ALPHA) @@ -1198,7 +1289,7 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U width: Int, height: Int, leftColor: Int, - rightColor: Int + rightColor: Int, ) { GL11.glBegin(GL11.GL_QUADS) glColor(leftColor) @@ -1216,7 +1307,7 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U width: Int, height: Int, topColor: Int, - bottomColor: Int + bottomColor: Int, ) { GL11.glBegin(GL11.GL_QUADS) glColor(topColor) @@ -1236,7 +1327,7 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U topLeftColor: Int, topRightColor: Int, bottomRightColor: Int, - bottomLeftColor: Int + bottomLeftColor: Int, ) { if (width <= 0 || height <= 0) return GL11.glDisable(GL11.GL_TEXTURE_2D) @@ -1273,14 +1364,15 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U val c = v * s val x = c * (1f - kotlin.math.abs((h / 60f) % 2f - 1f)) val m = v - c - val (r1, g1, b1) = when { - h < 60f -> Triple(c, x, 0f) - h < 120f -> Triple(x, c, 0f) - h < 180f -> Triple(0f, c, x) - h < 240f -> Triple(0f, x, c) - h < 300f -> Triple(x, 0f, c) - else -> Triple(c, 0f, x) - } + val (r1, g1, b1) = + when { + h < 60f -> Triple(c, x, 0f) + h < 120f -> Triple(x, c, 0f) + h < 180f -> Triple(0f, c, x) + h < 240f -> Triple(0f, x, c) + h < 300f -> Triple(x, 0f, c) + else -> Triple(c, 0f, x) + } val r = ((r1 + m) * 255f).toInt().coerceIn(0, 255) val g = ((g1 + m) * 255f).toInt().coerceIn(0, 255) val b = ((b1 + m) * 255f).toInt().coerceIn(0, 255) @@ -1294,14 +1386,14 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U return ReadbackSetup( previousReadBuffer = previousReadBuffer, appliedReadBuffer = desiredReadBuffer, - shouldRestore = false + shouldRestore = false, ) } GL11.glReadBuffer(desiredReadBuffer) return ReadbackSetup( previousReadBuffer = previousReadBuffer, appliedReadBuffer = desiredReadBuffer, - shouldRestore = true + shouldRestore = true, ) } @@ -1332,41 +1424,37 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U } } - private fun currentReadFramebufferBinding(): Int { - return when (readbackApi) { + private fun currentReadFramebufferBinding(): Int = + when (readbackApi) { ReadbackApi.OpenGl30 -> GL11.glGetInteger(GL30.GL_READ_FRAMEBUFFER_BINDING) ReadbackApi.ArbFramebufferObject -> GL11.glGetInteger(ARBFramebufferObject.GL_READ_FRAMEBUFFER_BINDING) ReadbackApi.ExtFramebufferObject -> GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT) ReadbackApi.Legacy -> 0 } - } - private fun currentDrawFramebufferBinding(): Int { - return when (readbackApi) { + private fun currentDrawFramebufferBinding(): Int = + when (readbackApi) { ReadbackApi.OpenGl30 -> GL11.glGetInteger(GL30.GL_DRAW_FRAMEBUFFER_BINDING) ReadbackApi.ArbFramebufferObject -> GL11.glGetInteger(ARBFramebufferObject.GL_DRAW_FRAMEBUFFER_BINDING) ReadbackApi.ExtFramebufferObject -> GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT) ReadbackApi.Legacy -> 0 } - } - private fun currentFramebufferBinding(): Int { - return when (readbackApi) { + private fun currentFramebufferBinding(): Int = + when (readbackApi) { ReadbackApi.OpenGl30 -> GL11.glGetInteger(GL30.GL_FRAMEBUFFER_BINDING) ReadbackApi.ArbFramebufferObject -> GL11.glGetInteger(ARBFramebufferObject.GL_FRAMEBUFFER_BINDING) ReadbackApi.ExtFramebufferObject -> GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT) ReadbackApi.Legacy -> 0 } - } - private fun defaultColorAttachmentReadBuffer(): Int { - return when (readbackApi) { + private fun defaultColorAttachmentReadBuffer(): Int = + when (readbackApi) { ReadbackApi.OpenGl30 -> GL30.GL_COLOR_ATTACHMENT0 ReadbackApi.ArbFramebufferObject -> ARBFramebufferObject.GL_COLOR_ATTACHMENT0 ReadbackApi.ExtFramebufferObject -> EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT ReadbackApi.Legacy -> GL11.GL_BACK } - } private fun detectReadbackBindingState(): ReadbackBindingState { val readFramebufferBinding = currentReadFramebufferBinding() @@ -1374,7 +1462,7 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U readFramebufferBinding = readFramebufferBinding, drawFramebufferBinding = currentDrawFramebufferBinding(), framebufferBinding = currentFramebufferBinding(), - currentReadBuffer = GL11.glGetInteger(GL11.GL_READ_BUFFER) + currentReadBuffer = GL11.glGetInteger(GL11.GL_READ_BUFFER), ) } @@ -1384,36 +1472,44 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U sourceY: Int, sourceWidth: Int, sourceHeight: Int, - setup: ReadbackSetup + setup: ReadbackSetup, ) { val state = detectReadbackBindingState() val recommended = selectReadBufferForActiveTarget(state.currentReadBuffer) val appliedCompatible = isReadBufferCompatibleWithActiveTarget(setup.appliedReadBuffer, state) val previousCompatible = isReadBufferCompatibleWithActiveTarget(setup.previousReadBuffer, state) - val message = buildString { - append("[DSGL-Readback] path=").append(path) - append(" src=(").append(sourceX).append(',').append(sourceY).append(' ') - append(sourceWidth).append('x').append(sourceHeight).append(')') - append(" readFbo=").append(state.readFramebufferBinding) - append(" drawFbo=").append(state.drawFramebufferBinding) - append(" fbo=").append(state.framebufferBinding) - append(" previousReadBuffer=").append(glEnumName(setup.previousReadBuffer)) - append(" appliedReadBuffer=").append(glEnumName(setup.appliedReadBuffer)) - append(" currentReadBuffer=").append(glEnumName(state.currentReadBuffer)) - append(" changed=").append(setup.shouldRestore) - append(" previousCompatible=").append(previousCompatible) - append(" appliedCompatible=").append(appliedCompatible) - append(" recommendedReadBuffer=").append(glEnumName(recommended)) - append(" api=").append(readbackApi.name) - } + val message = + buildString { + append("[DSGL-Readback] path=").append(path) + append(" src=(") + .append(sourceX) + .append(',') + .append(sourceY) + .append(' ') + append(sourceWidth) + .append('x') + .append(sourceHeight) + .append(')') + append(" readFbo=").append(state.readFramebufferBinding) + append(" drawFbo=").append(state.drawFramebufferBinding) + append(" fbo=").append(state.framebufferBinding) + append(" previousReadBuffer=").append(glEnumName(setup.previousReadBuffer)) + append(" appliedReadBuffer=").append(glEnumName(setup.appliedReadBuffer)) + append(" currentReadBuffer=").append(glEnumName(state.currentReadBuffer)) + append(" changed=").append(setup.shouldRestore) + append(" previousCompatible=").append(previousCompatible) + append(" appliedCompatible=").append(appliedCompatible) + append(" recommendedReadBuffer=").append(glEnumName(recommended)) + append(" api=").append(readbackApi.name) + } logRateLimited( key = "readback:$path:${state.readFramebufferBinding}:${setup.appliedReadBuffer}", - message = message + message = message, ) } - private fun isReadBufferCompatibleWithActiveTarget(readBuffer: Int, state: ReadbackBindingState): Boolean { - return if (state.usingFramebufferObject) { + private fun isReadBufferCompatibleWithActiveTarget(readBuffer: Int, state: ReadbackBindingState): Boolean = + if (state.usingFramebufferObject) { readBuffer == GL11.GL_NONE || isColorAttachmentReadBuffer(readBuffer) } else { when (readBuffer) { @@ -1424,24 +1520,27 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U GL11.GL_FRONT_LEFT, GL11.GL_FRONT_RIGHT, GL11.GL_BACK_LEFT, - GL11.GL_BACK_RIGHT -> true + GL11.GL_BACK_RIGHT, + -> true else -> false } } - } - private fun isColorAttachmentReadBuffer(readBuffer: Int): Boolean { - return when (readbackApi) { + private fun isColorAttachmentReadBuffer(readBuffer: Int): Boolean = + when (readbackApi) { ReadbackApi.OpenGl30 -> readBuffer in GL30.GL_COLOR_ATTACHMENT0..(GL30.GL_COLOR_ATTACHMENT0 + 31) - ReadbackApi.ArbFramebufferObject -> readBuffer in ARBFramebufferObject.GL_COLOR_ATTACHMENT0..(ARBFramebufferObject.GL_COLOR_ATTACHMENT0 + 15) - ReadbackApi.ExtFramebufferObject -> readBuffer in EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT..(EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT + 15) + ReadbackApi.ArbFramebufferObject -> + readBuffer in + ARBFramebufferObject.GL_COLOR_ATTACHMENT0..(ARBFramebufferObject.GL_COLOR_ATTACHMENT0 + 15) + ReadbackApi.ExtFramebufferObject -> + readBuffer in + EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT..(EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT + 15) ReadbackApi.Legacy -> false } - } - private fun glEnumName(value: Int): String { - return when (value) { + private fun glEnumName(value: Int): String = + when (value) { GL11.GL_NONE -> "GL_NONE" GL11.GL_FRONT -> "GL_FRONT" GL11.GL_BACK -> "GL_BACK" @@ -1453,13 +1552,13 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U GL11.GL_BACK_RIGHT -> "GL_BACK_RIGHT" GL30.GL_COLOR_ATTACHMENT0, ARBFramebufferObject.GL_COLOR_ATTACHMENT0, - EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT -> "GL_COLOR_ATTACHMENT0" + EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT, + -> "GL_COLOR_ATTACHMENT0" else -> { val hex = Integer.toHexString(value).uppercase() "0x$hex" } } - } private fun logRateLimited(key: String, message: String) { val now = System.currentTimeMillis() @@ -1469,16 +1568,26 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U println(message) } - private fun pushClip(viewport: Viewport, guiX: Int, guiY: Int, guiWidth: Int, guiHeight: Int) { + private fun pushClip( + viewport: Viewport, + guiX: Int, + guiY: Int, + guiWidth: Int, + guiHeight: Int, + ) { val scissor = viewport.dsglRectToGlScissor(guiX, guiY, guiWidth, guiHeight) ScissorContext.push(scissor.x, scissor.y, scissor.width, scissor.height) } - private fun isBlockStack(stack: ItemStack): Boolean { - return stack.item is ItemBlock - } + private fun isBlockStack(stack: ItemStack): Boolean = stack.item is ItemBlock - private fun draw2DItem(stack: ItemStack, x: Int, y: Int, size: Int, maxWidth: Int) { + private fun draw2DItem( + stack: ItemStack, + x: Int, + y: Int, + size: Int, + maxWidth: Int, + ) { val drawX = x + ((maxWidth - size) / 2).coerceAtLeast(0) withStack { withAttributes(enable = listOf(GL11.GL_DEPTH_TEST)) { @@ -1490,7 +1599,15 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U } } - private fun draw3DItem(stack: ItemStack, x: Int, y: Int, size: Int, width: Int, rotY: Double, rotX: Double) { + private fun draw3DItem( + stack: ItemStack, + x: Int, + y: Int, + size: Int, + width: Int, + rotY: Double, + rotX: Double, + ) { val scale = size / 16.0f val drawX = x + ((width - size) / 2).coerceAtLeast(0) @@ -1516,7 +1633,7 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U size: Int, width: Int, rotY: Double, - rotX: Double + rotX: Double, ) { withStack(attributesBitMask = GL11.GL_ALL_ATTRIB_BITS) { val previousZ = itemRenderer.zLevel @@ -1579,8 +1696,8 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U return File(baseDir, host + File.separator + path) } - private fun downloadToFile(url: URL, file: File): Boolean { - return try { + private fun downloadToFile(url: URL, file: File): Boolean = + try { file.parentFile?.mkdirs() url.openStream().use { input -> file.outputStream().use { output -> @@ -1591,7 +1708,6 @@ class Mc1710UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : U } catch (ex: Exception) { false } - } private fun loadDynamicTexture(file: File, cacheKey: String): ResourceLocation? { if (!file.exists()) return null diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/McItemStackRef.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/McItemStackRef.kt index b14c9c8..bc1b1cc 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/McItemStackRef.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/McItemStackRef.kt @@ -6,4 +6,6 @@ import org.dreamfinity.dsgl.core.ItemStackRef /** * Wrapper for Minecraft 1.7.10 [ItemStack] to satisfy [ItemStackRef]. */ -class McItemStackRef(val stack: ItemStack) : ItemStackRef +class McItemStackRef( + val stack: ItemStack, +) : ItemStackRef diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/RenderCommandTransformStack.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/RenderCommandTransformStack.kt index 85eaf1f..81d3102 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/RenderCommandTransformStack.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/RenderCommandTransformStack.kt @@ -1,15 +1,15 @@ package org.dreamfinity.dsgl.mcForge1710 -import kotlin.math.ceil -import kotlin.math.floor import org.dreamfinity.dsgl.core.dom.layout.AffineTransform2D import org.dreamfinity.dsgl.core.render.RenderCommand +import kotlin.math.ceil +import kotlin.math.floor internal data class GuiClipRect( val x: Int, val y: Int, val width: Int, - val height: Int + val height: Int, ) internal class RenderCommandTransformStack { @@ -32,11 +32,14 @@ internal class RenderCommandTransformStack { fun currentTransform(): AffineTransform2D = current - fun transformPoint(x: Float, y: Float): Pair { - return current.transform(x, y) - } + fun transformPoint(x: Float, y: Float): Pair = current.transform(x, y) - fun resolveClipRect(x: Int, y: Int, width: Int, height: Int): GuiClipRect { + fun resolveClipRect( + x: Int, + y: Int, + width: Int, + height: Int, + ): GuiClipRect { val safeWidth = width.coerceAtLeast(0) val safeHeight = height.coerceAtLeast(0) if (safeWidth == 0 || safeHeight == 0) { @@ -76,4 +79,3 @@ internal class RenderCommandTransformStack { .times(fromOrigin) } } - diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/scissorsHelper/ScissorContext.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/scissorsHelper/ScissorContext.kt index 1f0ffe8..8477b58 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/scissorsHelper/ScissorContext.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/scissorsHelper/ScissorContext.kt @@ -1,14 +1,20 @@ package org.dreamfinity.dsgl.mcForge1710.scissorsHelper import org.lwjgl.opengl.GL11 -import java.util.* +import java.util.ArrayDeque +import java.util.Deque object ScissorContext { val instance = ScissorContext val stack: Deque = ArrayDeque() var scissorsEnabledByContext = false - fun push(x: Number, y: Number, width: Number, height: Number): ScissorsArea { + fun push( + x: Number, + y: Number, + width: Number, + height: Number, + ): ScissorsArea { if (stack.isEmpty()) { scissorsEnabledByContext = !GL11.glIsEnabled(GL11.GL_SCISSOR_TEST) if (scissorsEnabledByContext) GL11.glEnable(GL11.GL_SCISSOR_TEST) diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/scissorsHelper/ScissorsArea.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/scissorsHelper/ScissorsArea.kt index dc36566..346a9d2 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/scissorsHelper/ScissorsArea.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/scissorsHelper/ScissorsArea.kt @@ -3,10 +3,15 @@ package org.dreamfinity.dsgl.mcForge1710.scissorsHelper import kotlin.math.max import kotlin.math.min -data class ScissorsArea(val x: Int, val y: Int, val width: Int, val height: Int) +data class ScissorsArea( + val x: Int, + val y: Int, + val width: Int, + val height: Int, +) -infix fun ScissorsArea.intersectionWith(another: ScissorsArea?): ScissorsArea { - return another?.let { +infix fun ScissorsArea.intersectionWith(another: ScissorsArea?): ScissorsArea = + another?.let { val x1 = max(this.x, another.x) val x2 = min(this.x + this.width, another.x + another.width) val y1 = max(this.y, another.y) @@ -15,7 +20,6 @@ infix fun ScissorsArea.intersectionWith(another: ScissorsArea?): ScissorsArea { x1, y1, max(0, x2 - x1), - max(0, y2 - y1) + max(0, y2 - y1), ) } ?: this -} diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/text/MsdfRuntimeDebugSettings.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/text/MsdfRuntimeDebugSettings.kt index 652aecf..072ab80 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/text/MsdfRuntimeDebugSettings.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/text/MsdfRuntimeDebugSettings.kt @@ -2,5 +2,7 @@ package org.dreamfinity.dsgl.mcForge1710.text object MsdfRuntimeDebugSettings { @Volatile - var decorationGuidesEnabled: Boolean = java.lang.Boolean.getBoolean("dsgl.msdf.debug.decorations") + var decorationGuidesEnabled: Boolean = + java.lang.Boolean + .getBoolean("dsgl.msdf.debug.decorations") } diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/text/MsdfTextRenderer.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/text/MsdfTextRenderer.kt index 343ffc8..8f29b9f 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/text/MsdfTextRenderer.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/text/MsdfTextRenderer.kt @@ -1,20 +1,22 @@ package org.dreamfinity.dsgl.mcForge1710.text -import org.dreamfinity.dsgl.core.font.* import org.dreamfinity.dsgl.core.dom.layout.FontLineMetrics +import org.dreamfinity.dsgl.core.font.* import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.TextFormatting import org.dreamfinity.dsgl.core.text.* import org.lwjgl.BufferUtils import org.lwjgl.opengl.* import java.nio.ByteBuffer -import java.util.* +import java.util.ArrayList +import java.util.Collections +import java.util.LinkedHashMap import java.util.concurrent.ConcurrentHashMap internal class MsdfTextRenderer { private data class PreparedText( val text: String, - val styleSpans: List + val styleSpans: List, ) private data class LayoutCacheKey( @@ -23,17 +25,21 @@ internal class MsdfTextRenderer { val fontSize: Int, val textFormatting: TextFormatting, val baseFlagsMask: Int, - val styleSpansHash: Int + val styleSpansHash: Int, ) private data class CachedLineLayout( val start: Int, - val shaped: ShapedText + val shaped: ShapedText, ) - private data class LayoutCacheEntry(val lines: List) + private data class LayoutCacheEntry( + val lines: List, + ) - private class SegmentBuffer(initialCapacity: Int = 64) { + private class SegmentBuffer( + initialCapacity: Int = 64, + ) { private var startX = FloatArray(initialCapacity) private var endX = FloatArray(initialCapacity) private var y = FloatArray(initialCapacity) @@ -54,7 +60,7 @@ internal class MsdfTextRenderer { segmentEndX: Float, segmentY: Float, segmentThickness: Float, - segmentColor: Int + segmentColor: Int, ) { if (segmentEndX <= segmentStartX) return val last = size - 1 @@ -79,10 +85,15 @@ internal class MsdfTextRenderer { } fun kindAt(index: Int): Int = kind[index] + fun startXAt(index: Int): Float = startX[index] + fun endXAt(index: Int): Float = endX[index] + fun yAt(index: Int): Float = y[index] + fun thicknessAt(index: Int): Float = thickness[index] + fun colorAt(index: Int): Int = color[index] private fun ensureCapacity(required: Int) { @@ -107,7 +118,7 @@ internal class MsdfTextRenderer { var glyphVectorRequests: Long = 0L, var glyphResolutionRequests: Long = 0L, var textureUploads: Long = 0L, - var textureUploadBytes: Long = 0L + var textureUploadBytes: Long = 0L, ) private data class DecorationSegment( @@ -115,27 +126,27 @@ internal class MsdfTextRenderer { var endX: Float, val y: Float, val thickness: Float, - val color: Int + val color: Int, ) private data class ObfuscationBuckets( val byAdvanceBucket: Map>, val expandedByAdvanceBucket: Map>, val sortedKeys: List, - val allGlyphs: List + val allGlyphs: List, ) private data class LineSlice( val start: Int, - val endExclusive: Int + val endExclusive: Int, ) private val textures: MutableMap = linkedMapOf() private val layoutCache: MutableMap = object : LinkedHashMap(64, 0.75f, true) { - override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean { - return size > MAX_LAYOUT_CACHE_ENTRIES - } + override fun removeEldestEntry( + eldest: MutableMap.MutableEntry?, + ): Boolean = size > MAX_LAYOUT_CACHE_ENTRIES } private var programId: Int = 0 private var uniformAtlas: Int = -1 @@ -143,7 +154,8 @@ internal class MsdfTextRenderer { private val errorLogTimes: MutableMap = linkedMapOf() private val debugLogKeys: MutableSet = Collections.newSetFromMap(ConcurrentHashMap()) private val debugGlyphResolutionEnabled: Boolean by lazy(mode = LazyThreadSafetyMode.NONE) { - java.lang.Boolean.getBoolean("dsgl.msdf.debug") + java.lang.Boolean + .getBoolean("dsgl.msdf.debug") } private val obfuscationBuckets: MutableMap = linkedMapOf() private var obfuscationLastNano: Long = System.nanoTime() @@ -153,34 +165,36 @@ internal class MsdfTextRenderer { private val segmentBuffer = SegmentBuffer(96) private val debugCounters = RendererDebugCounters() private val debugPerformanceEnabled: Boolean by lazy(mode = LazyThreadSafetyMode.NONE) { - java.lang.Boolean.getBoolean("dsgl.msdf.debug.performance") + java.lang.Boolean + .getBoolean("dsgl.msdf.debug.performance") } private var debugLastLogMs: Long = 0L - fun measureText(text: String, fontId: String?, fontSize: Int?): Int { - return FontRegistry.measureText(text, fontId, fontSize) - } + + fun measureText(text: String, fontId: String?, fontSize: Int?): Int = + FontRegistry.measureText(text, fontId, fontSize) fun measureTextRange( text: String, startIndex: Int, endIndexExclusive: Int, fontId: String?, - fontSize: Int? + fontSize: Int?, ): Int { - val shaped = FontRegistry.shapeTextRange( - text = text, - startIndex = startIndex, - endIndexExclusive = endIndexExclusive, - fontId = fontId, - fontSize = fontSize, - formattingMode = "plain" - ) - return shaped.width.toInt().coerceAtLeast(0) + val shaped = + FontRegistry.shapeTextRange( + text = text, + startIndex = startIndex, + endIndexExclusive = endIndexExclusive, + fontId = fontId, + fontSize = fontSize, + formattingMode = "plain", + ) + return shaped.width + .toInt() + .coerceAtLeast(0) } - fun lineHeight(fontId: String?, fontSize: Int?): Int { - return FontRegistry.lineHeight(fontId, fontSize) - } + fun lineHeight(fontId: String?, fontSize: Int?): Int = FontRegistry.lineHeight(fontId, fontSize) fun fontLineMetrics(fontId: String?, fontSize: Int?): FontLineMetrics? { val font = FontRegistry.get(fontId) ?: return null @@ -190,25 +204,29 @@ internal class MsdfTextRenderer { emSize = metrics.emSize, lineHeightEm = metrics.lineHeight, ascenderEm = metrics.ascender, - descenderEm = metrics.descender + descenderEm = metrics.descender, ) } fun draw(command: RenderCommand.DrawText, opacityMultiplier: Float) { debugCounters.drawCalls += 1 val primaryFont = FontRegistry.get(command.fontId) ?: return - val runtimeFallbackFont = FontRegistry.get(FontRegistry.FALLBACK_FONT_ID) - ?.takeIf { it.descriptor.fontId != primaryFont.descriptor.fontId } - val missingGlyphFont = FontRegistry.get(FontRegistry.FALLBACK_FONT_ID) - ?: FontRegistry.get(FontRegistry.DEFAULT_FONT_ID) + val runtimeFallbackFont = + FontRegistry + .get(FontRegistry.FALLBACK_FONT_ID) + ?.takeIf { it.descriptor.fontId != primaryFont.descriptor.fontId } + val missingGlyphFont = + FontRegistry.get(FontRegistry.FALLBACK_FONT_ID) + ?: FontRegistry.get(FontRegistry.DEFAULT_FONT_ID) val fontSize = FontRegistry.resolveFontSize(command.fontSize) val prepared = prepareText(command) - val layoutEntry = getOrBuildLayoutCacheEntry( - command = command, - prepared = prepared, - primaryFont = primaryFont, - fontSize = fontSize - ) + val layoutEntry = + getOrBuildLayoutCacheEntry( + command = command, + prepared = prepared, + primaryFont = primaryFont, + fontSize = fontSize, + ) if (prepared.text.isEmpty() || layoutEntry.lines.isEmpty()) return val debugDecorationGuidesEnabled = MsdfRuntimeDebugSettings.decorationGuidesEnabled @@ -222,15 +240,20 @@ internal class MsdfTextRenderer { try { val primaryScalePx = TextDecorationLayout.scalePx(fontSize, primaryFont.meta.metrics.emSize) - val lineHeight = primaryFont.meta.lineHeightPx(fontSize).toFloat().coerceAtLeast(1f) - val fontDecorationMetrics = DecorationFontMetrics( - emSize = primaryFont.meta.metrics.emSize, - lineHeightEm = primaryFont.meta.metrics.lineHeight, - ascenderEm = primaryFont.meta.metrics.ascender, - descenderEm = primaryFont.meta.metrics.descender, - underlineYEm = primaryFont.meta.metrics.underlineY, - underlineThicknessEm = primaryFont.meta.metrics.underlineThickness - ) + val lineHeight = + primaryFont.meta + .lineHeightPx(fontSize) + .toFloat() + .coerceAtLeast(1f) + val fontDecorationMetrics = + DecorationFontMetrics( + emSize = primaryFont.meta.metrics.emSize, + lineHeightEm = primaryFont.meta.metrics.lineHeight, + ascenderEm = primaryFont.meta.metrics.ascender, + descenderEm = primaryFont.meta.metrics.descender, + underlineYEm = primaryFont.meta.metrics.underlineY, + underlineThicknessEm = primaryFont.meta.metrics.underlineThickness, + ) debugGlyphResolution(prepared.text, primaryFont) if (!useProgram()) return @@ -273,25 +296,28 @@ internal class MsdfTextRenderer { try { layoutEntry.lines.forEach { line -> - val baselineY = TextDecorationLayout.baselineY( - lineTopY = lineTop, - ascenderEm = primaryFont.meta.metrics.ascender, - scalePx = primaryScalePx - ) + val baselineY = + TextDecorationLayout.baselineY( + lineTopY = lineTop, + ascenderEm = primaryFont.meta.metrics.ascender, + scalePx = primaryScalePx, + ) val shaped = line.shaped - val lineRecord = TextVisualLine( - lineIndex = lineIndex, - lineTopY = lineTop, - baselineY = baselineY, - lineHeightPx = lineHeight, - glyphStartIndex = globalGlyphIndex, - glyphEndIndexExclusive = globalGlyphIndex + shaped.glyphs.size - ) - val lineMetrics = TextDecorationLayout.resolveLineMetrics( - line = lineRecord, - fontMetrics = fontDecorationMetrics, - fontPx = fontSize - ) + val lineRecord = + TextVisualLine( + lineIndex = lineIndex, + lineTopY = lineTop, + baselineY = baselineY, + lineHeightPx = lineHeight, + glyphStartIndex = globalGlyphIndex, + glyphEndIndexExclusive = globalGlyphIndex + shaped.glyphs.size, + ) + val lineMetrics = + TextDecorationLayout.resolveLineMetrics( + line = lineRecord, + fontMetrics = fontDecorationMetrics, + fontPx = fontSize, + ) var lineStartX = command.x.toFloat() var lineEndX = lineStartX var glyphIndexInLine = 0 @@ -301,59 +327,66 @@ internal class MsdfTextRenderer { shaped.glyphs.forEach { shapedGlyph -> val globalCharStart = line.start + shapedGlyph.charStart - while (spanIndex < prepared.styleSpans.size && globalCharStart >= prepared.styleSpans[spanIndex].end) { - spanIndex += 1 - } - val resolvedSpanIndex = if ( - spanIndex < prepared.styleSpans.size && - globalCharStart >= prepared.styleSpans[spanIndex].start && - globalCharStart < prepared.styleSpans[spanIndex].end + while (spanIndex < prepared.styleSpans.size && + globalCharStart >= prepared.styleSpans[spanIndex].end ) { - spanIndex - } else { - -1 + spanIndex += 1 } + val resolvedSpanIndex = + if ( + spanIndex < prepared.styleSpans.size && + globalCharStart >= prepared.styleSpans[spanIndex].start && + globalCharStart < prepared.styleSpans[spanIndex].end + ) { + spanIndex + } else { + -1 + } if (resolvedSpanIndex != activeResolvedSpanIndex) { activeResolvedSpanIndex = resolvedSpanIndex if (resolvedSpanIndex >= 0) { val span = prepared.styleSpans[resolvedSpanIndex] activeStyleColor = withOpacity(span.color, opacityMultiplier) - activeStyleFlags = flagsMask( - bold = span.bold, - italic = span.italic, - underline = span.underline, - strikethrough = span.strikethrough, - obfuscated = span.obfuscated - ) + activeStyleFlags = + flagsMask( + bold = span.bold, + italic = span.italic, + underline = span.underline, + strikethrough = span.strikethrough, + obfuscated = span.obfuscated, + ) } else { activeStyleColor = withOpacity(command.color, opacityMultiplier) activeStyleFlags = baseFlagsMask(command) } } - val shapedFont = if (shapedGlyph.fontId == cachedShapedFontId) { - cachedShapedFont - } else { - (FontRegistry.get(shapedGlyph.fontId) ?: primaryFont).also { resolved -> - cachedShapedFontId = shapedGlyph.fontId - cachedShapedFont = resolved + val shapedFont = + if (shapedGlyph.fontId == cachedShapedFontId) { + cachedShapedFont + } else { + (FontRegistry.get(shapedGlyph.fontId) ?: primaryFont).also { resolved -> + cachedShapedFontId = shapedGlyph.fontId + cachedShapedFont = resolved + } + } + val forceMissingGlyphFont = + if ( + shapedGlyph.sourceCodepoint == REPLACEMENT_CODEPOINT || + isShapedGlyphMissingInFont(shapedFont, shapedGlyph) + ) { + missingGlyphFont ?: runtimeFallbackFont ?: shapedFont + } else { + null } - } - val forceMissingGlyphFont = if ( - shapedGlyph.sourceCodepoint == REPLACEMENT_CODEPOINT || - isShapedGlyphMissingInFont(shapedFont, shapedGlyph) - ) { - missingGlyphFont ?: runtimeFallbackFont ?: shapedFont - } else { - null - } var glyphFont = forceMissingGlyphFont ?: shapedFont - var glyph = if (forceMissingGlyphFont != null) { - preferredMissingGlyph(glyphFont) - } else { - resolveGlyphForShapedInput(shapedFont, shapedGlyph) - } + var glyph = + if (forceMissingGlyphFont != null) { + preferredMissingGlyph(glyphFont) + } else { + resolveGlyphForShapedInput(shapedFont, shapedGlyph) + } if (glyph == null && runtimeFallbackFont != null) { val fallbackByCodepoint = resolveGlyphForShapedInput(runtimeFallbackFont, shapedGlyph) if (fallbackByCodepoint != null) { @@ -405,14 +438,16 @@ internal class MsdfTextRenderer { } val drawGlyph = - if (styleObfuscated && ObfuscationTextSelector.shouldObfuscateCodepoint(shapedGlyph.sourceCodepoint)) { + if (styleObfuscated && + ObfuscationTextSelector.shouldObfuscateCodepoint(shapedGlyph.sourceCodepoint) + ) { resolveObfuscatedGlyph( font = glyphFont, sourceKey = command.sourceKey ?: command.text, original = resolvedGlyph, lineIndex = lineIndex, glyphIndexInLine = glyphIndexInLine, - avoidGlyphIndex = lastObfuscatedGlyphIndex + avoidGlyphIndex = lastObfuscatedGlyphIndex, ) } else { resolvedGlyph @@ -428,7 +463,7 @@ internal class MsdfTextRenderer { atlasHeight = texture.height, fontScalePx = glyphScale, italic = styleItalic, - italicSkewPx = glyphScale * 0.2f + italicSkewPx = glyphScale * 0.2f, ) if (styleBold) { emitGlyphQuad( @@ -439,7 +474,7 @@ internal class MsdfTextRenderer { atlasHeight = texture.height, fontScalePx = glyphScale, italic = styleItalic, - italicSkewPx = glyphScale * 0.2f + italicSkewPx = glyphScale * 0.2f, ) } lastObfuscatedGlyphIndex = if (styleObfuscated) effectiveGlyph.glyphIndex else null @@ -455,7 +490,7 @@ internal class MsdfTextRenderer { segmentEndX = glyphEndX, segmentY = lineMetrics.underlineY, segmentThickness = lineMetrics.underlineThickness, - segmentColor = activeStyleColor + segmentColor = activeStyleColor, ) } if (styleStrikethrough) { @@ -465,7 +500,7 @@ internal class MsdfTextRenderer { segmentEndX = glyphEndX, segmentY = lineMetrics.strikethroughY, segmentThickness = lineMetrics.strikethroughThickness, - segmentColor = activeStyleColor + segmentColor = activeStyleColor, ) } } @@ -479,12 +514,13 @@ internal class MsdfTextRenderer { segmentKind = SEGMENT_DEBUG_BASELINE, segmentStartX = lineStartX, segmentEndX = lineEndX, - segmentY = lineRecord.baselineY.coerceIn( - lineRecord.lineTopY, - lineRecord.lineTopY + lineRecord.lineHeightPx - ), + segmentY = + lineRecord.baselineY.coerceIn( + lineRecord.lineTopY, + lineRecord.lineTopY + lineRecord.lineHeightPx, + ), segmentThickness = 1f, - segmentColor = 0x66FFAA00 + segmentColor = 0x66FFAA00, ) segmentBuffer.appendMerged( segmentKind = SEGMENT_DEBUG_UNDERLINE, @@ -492,7 +528,7 @@ internal class MsdfTextRenderer { segmentEndX = lineEndX, segmentY = lineMetrics.underlineY, segmentThickness = lineMetrics.underlineThickness, - segmentColor = 0x6600FF00 + segmentColor = 0x6600FF00, ) segmentBuffer.appendMerged( segmentKind = SEGMENT_DEBUG_STRIKE, @@ -500,7 +536,7 @@ internal class MsdfTextRenderer { segmentEndX = lineEndX, segmentY = lineMetrics.strikethroughY, segmentThickness = lineMetrics.strikethroughThickness, - segmentColor = 0x66FF00FF + segmentColor = 0x66FF00FF, ) } @@ -527,43 +563,46 @@ internal class MsdfTextRenderer { if (command.textFormatting != TextFormatting.Minecraft) { return PreparedText( text = command.text, - styleSpans = command.textStyleSpans + styleSpans = command.textStyleSpans, ) } if (command.textStyleSpans.isNotEmpty()) { return PreparedText( text = command.text, - styleSpans = command.textStyleSpans + styleSpans = command.textStyleSpans, ) } val parsed = MinecraftFormattingParser.parse(command.text, TextFormatting.Minecraft) - val spans = MinecraftFormattingParser.resolveStyleSpans( - parsed = parsed, - baseColor = command.color, - baseFlags = TextStyleFlags( - bold = command.bold, - italic = command.italic, - underline = command.underline, - strikethrough = command.strikethrough, - obfuscated = command.obfuscated - ) - ).map { span -> - RenderCommand.TextStyleSpan( - start = span.start, - end = span.end, - color = span.color, - bold = span.flags.bold, - italic = span.flags.italic, - underline = span.flags.underline, - strikethrough = span.flags.strikethrough, - obfuscated = span.flags.obfuscated - ) - } + val spans = + MinecraftFormattingParser + .resolveStyleSpans( + parsed = parsed, + baseColor = command.color, + baseFlags = + TextStyleFlags( + bold = command.bold, + italic = command.italic, + underline = command.underline, + strikethrough = command.strikethrough, + obfuscated = command.obfuscated, + ), + ).map { span -> + RenderCommand.TextStyleSpan( + start = span.start, + end = span.end, + color = span.color, + bold = span.flags.bold, + italic = span.flags.italic, + underline = span.flags.underline, + strikethrough = span.flags.strikethrough, + obfuscated = span.flags.obfuscated, + ) + } return PreparedText( text = parsed.plainText, - styleSpans = spans + styleSpans = spans, ) } @@ -586,21 +625,24 @@ internal class MsdfTextRenderer { private fun resolveGlyphForShapedInput(font: LoadedMsdfFont, shapedGlyph: ShapedGlyph): MsdfGlyph? { val sourceCodepoint = shapedGlyph.sourceCodepoint val canUseGlyphIndex = shapedGlyph.fontId == font.descriptor.fontId - val fromIndex = if (canUseGlyphIndex) { - font.meta.glyphByIndex(shapedGlyph.glyphIndex) - } else { - null - } + val fromIndex = + if (canUseGlyphIndex) { + font.meta.glyphByIndex(shapedGlyph.glyphIndex) + } else { + null + } val indexLooksMissing = isMissingGlyphIndex(font, shapedGlyph.glyphIndex, fromIndex) - val indexMatchesSource = if (!indexLooksMissing) { - val fromIndexCodepoint = fromIndex?.codepoint - fromIndex != null && ( - fromIndexCodepoint == null || + val indexMatchesSource = + if (!indexLooksMissing) { + val fromIndexCodepoint = fromIndex?.codepoint + fromIndex != null && + ( + fromIndexCodepoint == null || fromIndexCodepoint == sourceCodepoint ) - } else { - false - } + } else { + false + } if (sourceCodepoint == REPLACEMENT_CODEPOINT) { return preferredMissingGlyph(font) @@ -623,9 +665,10 @@ internal class MsdfTextRenderer { val questionByIndex = font.preferredQuestionGlyphIndex?.let(meta::glyphByIndex) if (questionByIndex != null) return questionByIndex - val replacementByIndex = font.preferredMissingGlyphIndex - ?.takeIf { it != 0 } - ?.let(meta::glyphByIndex) + val replacementByIndex = + font.preferredMissingGlyphIndex + ?.takeIf { it != 0 } + ?.let(meta::glyphByIndex) if (replacementByIndex != null) return replacementByIndex val notDef = meta.glyphByIndex(0) if (notDef != null) return notDef @@ -654,7 +697,7 @@ internal class MsdfTextRenderer { atlasHeight: Int, fontScalePx: Float, italic: Boolean, - italicSkewPx: Float + italicSkewPx: Float, ) { val plane = glyph.planeBounds ?: return val atlas = glyph.atlasBounds ?: return @@ -684,16 +727,17 @@ internal class MsdfTextRenderer { command: RenderCommand.DrawText, prepared: PreparedText, primaryFont: LoadedMsdfFont, - fontSize: Int + fontSize: Int, ): LayoutCacheEntry { - val key = LayoutCacheKey( - text = prepared.text, - primaryFontId = primaryFont.descriptor.fontId, - fontSize = fontSize, - textFormatting = command.textFormatting, - baseFlagsMask = baseFlagsMask(command), - styleSpansHash = styleSpansFingerprint(prepared.styleSpans) - ) + val key = + LayoutCacheKey( + text = prepared.text, + primaryFontId = primaryFont.descriptor.fontId, + fontSize = fontSize, + textFormatting = command.textFormatting, + baseFlagsMask = baseFlagsMask(command), + styleSpansHash = styleSpansFingerprint(prepared.styleSpans), + ) synchronized(layoutCache) { val cached = layoutCache[key] if (cached != null) { @@ -707,18 +751,20 @@ internal class MsdfTextRenderer { val lines = ArrayList(slices.size) slices.forEach { slice -> debugCounters.glyphVectorRequests += 1 - val shaped = FontRegistry.shapeTextRange( - text = prepared.text, - startIndex = slice.start, - endIndexExclusive = slice.endExclusive, - fontId = command.fontId, - fontSize = fontSize, - formattingMode = command.textFormatting.name - ) - lines += CachedLineLayout( - start = slice.start, - shaped = shaped - ) + val shaped = + FontRegistry.shapeTextRange( + text = prepared.text, + startIndex = slice.start, + endIndexExclusive = slice.endExclusive, + fontId = command.fontId, + fontSize = fontSize, + formattingMode = command.textFormatting.name, + ) + lines += + CachedLineLayout( + start = slice.start, + shaped = shaped, + ) } val built = LayoutCacheEntry(lines = lines) @@ -748,7 +794,8 @@ internal class MsdfTextRenderer { var index = 0 while (index < segments.size) { val kind = segments.kindAt(index) - val isDebug = kind == SEGMENT_DEBUG_BASELINE || + val isDebug = + kind == SEGMENT_DEBUG_BASELINE || kind == SEGMENT_DEBUG_UNDERLINE || kind == SEGMENT_DEBUG_STRIKE if (isDebug && !includeDebug) { @@ -762,10 +809,11 @@ internal class MsdfTextRenderer { val a = ((color ushr 24) and 0xFF) / 255f GL11.glColor4f(r, g, b, a) val y0 = segments.yAt(index) - val y1 = maxOf( - y0 + 0.5f, - y0 + segments.thicknessAt(index) - ) + val y1 = + maxOf( + y0 + 0.5f, + y0 + segments.thicknessAt(index), + ) GL11.glVertex2f(segments.startXAt(index), y0) GL11.glVertex2f(segments.endXAt(index), y0) GL11.glVertex2f(segments.endXAt(index), y1) @@ -782,22 +830,21 @@ internal class MsdfTextRenderer { } } - private fun baseFlagsMask(command: RenderCommand.DrawText): Int { - return flagsMask( + private fun baseFlagsMask(command: RenderCommand.DrawText): Int = + flagsMask( bold = command.bold, italic = command.italic, underline = command.underline, strikethrough = command.strikethrough, - obfuscated = command.obfuscated + obfuscated = command.obfuscated, ) - } private fun flagsMask( bold: Boolean, italic: Boolean, underline: Boolean, strikethrough: Boolean, - obfuscated: Boolean + obfuscated: Boolean, ): Int { var mask = 0 if (bold) mask = mask or STYLE_FLAG_BOLD @@ -829,27 +876,30 @@ internal class MsdfTextRenderer { original: MsdfGlyph, lineIndex: Int, glyphIndexInLine: Int, - avoidGlyphIndex: Int? + avoidGlyphIndex: Int?, ): MsdfGlyph? { - val buckets = obfuscationBuckets.getOrPut(font.descriptor.fontId) { - buildObfuscationBuckets(font) - } + val buckets = + obfuscationBuckets.getOrPut(font.descriptor.fontId) { + buildObfuscationBuckets(font) + } if (buckets.allGlyphs.isEmpty()) return original val baseBucket = advanceBucketKey(original.advance) - val candidates = buckets.expandedByAdvanceBucket[baseBucket] - ?: nearestExpandedCandidates(buckets, baseBucket) - ?: buckets.allGlyphs + val candidates = + buckets.expandedByAdvanceBucket[baseBucket] + ?: nearestExpandedCandidates(buckets, baseBucket) + ?: buckets.allGlyphs if (candidates.isEmpty()) return original val originalKey = original.codepoint ?: original.glyphIndex - val primaryIndex = ObfuscationTextSelector.selectCandidateIndex( - sourceKey = sourceKey, - lineIndex = lineIndex, - glyphIndexInLine = glyphIndexInLine, - timeSlice = obfuscationTimeSlice, - originalCodepoint = originalKey, - candidateCount = candidates.size - ) + val primaryIndex = + ObfuscationTextSelector.selectCandidateIndex( + sourceKey = sourceKey, + lineIndex = lineIndex, + glyphIndexInLine = glyphIndexInLine, + timeSlice = obfuscationTimeSlice, + originalCodepoint = originalKey, + candidateCount = candidates.size, + ) val primary = candidates[primaryIndex] if (avoidGlyphIndex == null || candidates.size <= 1 || primary.glyphIndex != avoidGlyphIndex) { return primary @@ -861,17 +911,18 @@ internal class MsdfTextRenderer { } private fun buildObfuscationBuckets(font: LoadedMsdfFont): ObfuscationBuckets { - val glyphs = font.meta.glyphsByIndex.values - .filter { glyph -> - val codepoint = glyph.codepoint - glyph.drawable && (codepoint == null || !TextStyleMetrics.isWhitespaceCodepoint(codepoint)) - } + val glyphs = + font.meta.glyphsByIndex.values + .filter { glyph -> + val codepoint = glyph.codepoint + glyph.drawable && (codepoint == null || !TextStyleMetrics.isWhitespaceCodepoint(codepoint)) + } if (glyphs.isEmpty()) { return ObfuscationBuckets( byAdvanceBucket = emptyMap(), expandedByAdvanceBucket = emptyMap(), sortedKeys = emptyList(), - allGlyphs = emptyList() + allGlyphs = emptyList(), ) } val grouped = linkedMapOf>() @@ -882,23 +933,22 @@ internal class MsdfTextRenderer { val frozenGrouped = grouped.mapValues { (_, value) -> value.toList() } val expanded = linkedMapOf>() sorted.forEach { key -> - expanded[key] = expandCandidatesForBucket( - grouped = frozenGrouped, - sortedKeys = sorted, - baseKey = key - ) + expanded[key] = + expandCandidatesForBucket( + grouped = frozenGrouped, + sortedKeys = sorted, + baseKey = key, + ) } return ObfuscationBuckets( byAdvanceBucket = frozenGrouped, expandedByAdvanceBucket = expanded, sortedKeys = sorted, - allGlyphs = glyphs + allGlyphs = glyphs, ) } - private fun advanceBucketKey(advance: Float): Int { - return (advance * 100f).toInt() - } + private fun advanceBucketKey(advance: Float): Int = (advance * 100f).toInt() private fun nearestExpandedCandidates(buckets: ObfuscationBuckets, key: Int): List? { val keys = buckets.sortedKeys @@ -918,7 +968,7 @@ internal class MsdfTextRenderer { private fun expandCandidatesForBucket( grouped: Map>, sortedKeys: List, - baseKey: Int + baseKey: Int, ): List { val byDistance = sortedKeys.sortedBy { key -> kotlin.math.abs(key - baseKey) } val out = ArrayList(MIN_OBFUSCATION_CANDIDATES) @@ -970,7 +1020,7 @@ internal class MsdfTextRenderer { val height = bitmap.height.coerceAtLeast(1) if (width > maxTextureSize || height > maxTextureSize) { throw IllegalStateException( - "Atlas '${font.descriptor.fontId}' is ${width}x${height}, exceeds GL_MAX_TEXTURE_SIZE=$maxTextureSize" + "Atlas '${font.descriptor.fontId}' is ${width}x$height, exceeds GL_MAX_TEXTURE_SIZE=$maxTextureSize", ) } val buffer = BufferUtils.createByteBuffer(width * height * 4) @@ -983,25 +1033,26 @@ internal class MsdfTextRenderer { GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR) GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP) GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP) - val glError = scopedGlError { - GL11.glTexImage2D( - GL11.GL_TEXTURE_2D, - 0, - GL11.GL_RGBA8, - width, - height, - 0, - GL11.GL_RGBA, - GL11.GL_UNSIGNED_BYTE, - buffer - ) - } + val glError = + scopedGlError { + GL11.glTexImage2D( + GL11.GL_TEXTURE_2D, + 0, + GL11.GL_RGBA8, + width, + height, + 0, + GL11.GL_RGBA, + GL11.GL_UNSIGNED_BYTE, + buffer, + ) + } if (glError != GL11.GL_NO_ERROR) { GL11.glDeleteTextures(textureId) throw IllegalStateException( - "glTexImage2D failed for '${font.descriptor.fontId}' (${width}x${height}), glError=0x${ + "glTexImage2D failed for '${font.descriptor.fontId}' (${width}x$height), glError=0x${ glError.toString(16) - }" + }", ) } debugCounters.textureUploads += 1 @@ -1029,11 +1080,11 @@ internal class MsdfTextRenderer { private fun useProgram(): Boolean { if (programId == 0) { - val loaded = runCatching { createProgram() } - .onFailure { error -> - logRateLimited("shader:init", "[DSGL-MSDF] Failed to initialize shader: ${error.message}") - } - .getOrNull() ?: return false + val loaded = + runCatching { createProgram() } + .onFailure { error -> + logRateLimited("shader:init", "[DSGL-MSDF] Failed to initialize shader: ${error.message}") + }.getOrNull() ?: return false programId = loaded uniformAtlas = ARBShaderObjects.glGetUniformLocationARB(programId, "uAtlas") uniformPxRange = ARBShaderObjects.glGetUniformLocationARB(programId, "uPxRange") @@ -1043,23 +1094,26 @@ internal class MsdfTextRenderer { } private fun createProgram(): Int { - val vertexShader = compileShader( - type = ARBVertexShader.GL_VERTEX_SHADER_ARB, - source = VERTEX_SHADER_SOURCE - ) - val fragmentShader = compileShader( - type = ARBFragmentShader.GL_FRAGMENT_SHADER_ARB, - source = FRAGMENT_SHADER_SOURCE - ) + val vertexShader = + compileShader( + type = ARBVertexShader.GL_VERTEX_SHADER_ARB, + source = VERTEX_SHADER_SOURCE, + ) + val fragmentShader = + compileShader( + type = ARBFragmentShader.GL_FRAGMENT_SHADER_ARB, + source = FRAGMENT_SHADER_SOURCE, + ) val program = ARBShaderObjects.glCreateProgramObjectARB() ARBShaderObjects.glAttachObjectARB(program, vertexShader) ARBShaderObjects.glAttachObjectARB(program, fragmentShader) ARBShaderObjects.glLinkProgramARB(program) - val linkStatus = ARBShaderObjects.glGetObjectParameteriARB( - program, - ARBShaderObjects.GL_OBJECT_LINK_STATUS_ARB - ) + val linkStatus = + ARBShaderObjects.glGetObjectParameteriARB( + program, + ARBShaderObjects.GL_OBJECT_LINK_STATUS_ARB, + ) if (linkStatus == GL11.GL_FALSE) { val info = ARBShaderObjects.glGetInfoLogARB(program, 4096) throw IllegalStateException("Program link failed: $info") @@ -1071,10 +1125,11 @@ internal class MsdfTextRenderer { val shader = ARBShaderObjects.glCreateShaderObjectARB(type) ARBShaderObjects.glShaderSourceARB(shader, source) ARBShaderObjects.glCompileShaderARB(shader) - val compileStatus = ARBShaderObjects.glGetObjectParameteriARB( - shader, - ARBShaderObjects.GL_OBJECT_COMPILE_STATUS_ARB - ) + val compileStatus = + ARBShaderObjects.glGetObjectParameteriARB( + shader, + ARBShaderObjects.GL_OBJECT_COMPILE_STATUS_ARB, + ) if (compileStatus == GL11.GL_FALSE) { val info = ARBShaderObjects.glGetInfoLogARB(shader, 4096) throw IllegalStateException("Shader compile failed: $info") @@ -1105,10 +1160,12 @@ internal class MsdfTextRenderer { val layoutSize = synchronized(layoutCache) { layoutCache.size } println( "[DSGL-MSDF] drawCalls=${debugCounters.drawCalls} " + - "layoutCache hit=${debugCounters.layoutCacheHits} miss=${debugCounters.layoutCacheMisses} size=$layoutSize " + - "glyphVectors=${debugCounters.glyphVectorRequests} glyphResolves=${debugCounters.glyphResolutionRequests} " + - "textureUploads=${debugCounters.textureUploads} " + - "textureUploadBytes=${debugCounters.textureUploadBytes}" + "layoutCache hit=${debugCounters.layoutCacheHits} " + + "miss=${debugCounters.layoutCacheMisses} size=$layoutSize " + + "glyphVectors=${debugCounters.glyphVectorRequests} " + + "glyphResolves=${debugCounters.glyphResolutionRequests} " + + "textureUploads=${debugCounters.textureUploads} " + + "textureUploadBytes=${debugCounters.textureUploadBytes}", ) } @@ -1118,12 +1175,13 @@ internal class MsdfTextRenderer { val key = "${font.descriptor.fontId}|$sample" if (!debugLogKeys.add(key)) return - val shaped = FontRegistry.shapeText( - text = sample, - fontId = font.descriptor.fontId, - fontSize = FontRegistry.DEFAULT_FONT_SIZE, - formattingMode = "debug" - ) + val shaped = + FontRegistry.shapeText( + text = sample, + fontId = font.descriptor.fontId, + fontSize = FontRegistry.DEFAULT_FONT_SIZE, + formattingMode = "debug", + ) println("[DSGL-MSDF] text='$sample' glyphs=${shaped.glyphs.size} runs=${shaped.runs.size}") shaped.glyphs.take(32).forEach { glyph -> val loaded = FontRegistry.get(glyph.fontId) @@ -1131,8 +1189,8 @@ internal class MsdfTextRenderer { println( "[DSGL-MSDF] font=${glyph.fontId} glyphIndex=${glyph.glyphIndex} sourceCp=U+%04X found=%s".format( glyph.sourceCodepoint, - atlasGlyph != null - ) + atlasGlyph != null, + ), ) } } diff --git a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostUsedGeometryTests.kt b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostUsedGeometryTests.kt index 7baa683..561c6fc 100644 --- a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostUsedGeometryTests.kt +++ b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostUsedGeometryTests.kt @@ -16,17 +16,20 @@ import org.dreamfinity.dsgl.core.style.StyleDeclarations import org.dreamfinity.dsgl.core.style.StyleExpression import org.dreamfinity.dsgl.core.style.StyleProperty import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue import org.junit.Assert.assertNull import org.junit.Assert.assertSame +import org.junit.Assert.assertTrue import org.junit.Test class DsglScreenHostUsedGeometryTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @Test fun `app host hover and click target match core for absolute outside ancestor bounds`() { @@ -97,24 +100,26 @@ class DsglScreenHostUsedGeometryTests { assertNull(hoverTarget(host)) assertEquals( collectHoverChain(fixture.root, 225, 28).lastOrNull(), - hoverTarget(host) + hoverTarget(host), ) } @Test fun `rebuild churn drains detached cleanup and keeps listener registrations bounded`() { - val window = object : DsglWindow() { - private var generation: Int = 0 - - override fun render(): DomTree { - generation += 1 - val root = ContainerNode(key = "rebuild-root-$generation") - ButtonNode(text = "btn-$generation", key = "btn-$generation").apply { - onMouseClick = {} - }.applyParent(root) - return DomTree(root) + val window = + object : DsglWindow() { + private var generation: Int = 0 + + override fun render(): DomTree { + generation += 1 + val root = ContainerNode(key = "rebuild-root-$generation") + ButtonNode(text = "btn-$generation", key = "btn-$generation") + .apply { + onMouseClick = {} + }.applyParent(root) + return DomTree(root) + } } - } val host = object : DsglScreenHost(window) {} host.window = window host.debugSetNeedsRenderForTests(true) @@ -127,7 +132,7 @@ class DsglScreenHostUsedGeometryTests { assertEquals( "detached cleanup queue must be drained in the same rebuild cycle", 0, - host.debugPendingCleanupCount() + host.debugPendingCleanupCount(), ) } @@ -136,20 +141,21 @@ class DsglScreenHostUsedGeometryTests { val callbackDelta = after.registeredCallbacks - baseline.registeredCallbacks assertTrue( "listener node registrations grew unexpectedly: baseline=$baseline after=$after", - nodeDelta <= 2 + nodeDelta <= 2, ) assertTrue( "listener callback registrations grew unexpectedly: baseline=$baseline after=$after", - callbackDelta <= 24 + callbackDelta <= 24, ) } private fun createHostWithTree(tree: DomTree): DsglScreenHost { - val host = object : DsglScreenHost(object : DsglWindow() { - override fun render(): DomTree { - return tree - } - }) {} + val host = + object : DsglScreenHost( + object : DsglWindow() { + override fun render(): DomTree = tree + }, + ) {} host.debugBindTreeForTests(tree, needsLayout = false) return host } @@ -158,130 +164,142 @@ class DsglScreenHostUsedGeometryTests { host.debugRefreshHoverTargetForTests(mouseX, mouseY) } - private fun hoverTarget(host: DsglScreenHost): DOMNode? { - return host.debugHoverTargetForTests() - } + private fun hoverTarget(host: DsglScreenHost): DOMNode? = host.debugHoverTargetForTests() - private fun resolvePointerDownTarget(host: DsglScreenHost): DOMNode? { - return host.debugResolvePointerDownTargetForTests() - } + private fun resolvePointerDownTarget(host: DsglScreenHost): DOMNode? = host.debugResolvePointerDownTargetForTests() - private fun resolveClickTarget(host: DsglScreenHost): DOMNode? { - return host.debugResolveClickTargetForTests() - } + private fun resolveClickTarget(host: DsglScreenHost): DOMNode? = host.debugResolveClickTargetForTests() private data class AbsoluteOutsideAncestorFixture( val tree: DomTree, val root: ContainerNode, - val child: ButtonNode + val child: ButtonNode, ) private fun createAbsoluteOutsideAncestorFixture(): AbsoluteOutsideAncestorFixture { val root = ContainerNode(key = "abs-root") - val ancestor = ContainerNode(key = "abs-ancestor").apply { - width = 40 - height = 40 - inlineStyleDeclarations = styleDeclarations(StyleProperty.POSITION to "relative") - }.applyParent(root) - val child = ButtonNode("abs-child", key = "abs-child").apply { - width = 36 - height = 16 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "absolute", - StyleProperty.LEFT to "100px", - StyleProperty.TOP to "5px" - ) - }.applyParent(ancestor) + val ancestor = + ContainerNode(key = "abs-ancestor") + .apply { + width = 40 + height = 40 + inlineStyleDeclarations = styleDeclarations(StyleProperty.POSITION to "relative") + }.applyParent(root) + val child = + ButtonNode("abs-child", key = "abs-child") + .apply { + width = 36 + height = 16 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "absolute", + StyleProperty.LEFT to "100px", + StyleProperty.TOP to "5px", + ) + }.applyParent(ancestor) return AbsoluteOutsideAncestorFixture(DomTree(root), root, child) } private data class PositionedOverlapFixture( val tree: DomTree, val root: ContainerNode, - val fixed: ButtonNode + val fixed: ButtonNode, ) private fun createPositionedOverlapFixture(): PositionedOverlapFixture { val root = ContainerNode(key = "root", stackLayout = true) - val early = ContainerNode(key = "early", stackLayout = true).apply { - width = 120 - height = 60 - }.applyParent(root) - ContainerNode(key = "later-container", stackLayout = true).apply { - width = 120 - height = 60 - }.apply { - ButtonNode("later", key = "later").apply { - width = 72 - height = 24 - }.applyParent(this) - }.applyParent(root) - val fixed = ButtonNode("fixed", key = "fixed").apply { - width = 72 - height = 24 - zIndex = 9_999 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "8px", - StyleProperty.TOP to "8px" - ) - }.applyParent(early) + val early = + ContainerNode(key = "early", stackLayout = true) + .apply { + width = 120 + height = 60 + }.applyParent(root) + ContainerNode(key = "later-container", stackLayout = true) + .apply { + width = 120 + height = 60 + }.apply { + ButtonNode("later", key = "later") + .apply { + width = 72 + height = 24 + }.applyParent(this) + }.applyParent(root) + val fixed = + ButtonNode("fixed", key = "fixed") + .apply { + width = 72 + height = 24 + zIndex = 9_999 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "8px", + StyleProperty.TOP to "8px", + ) + }.applyParent(early) return PositionedOverlapFixture(DomTree(root), root, fixed) } private data class ClipSemanticsFixture( val tree: DomTree, val root: ContainerNode, - val fixed: ButtonNode + val fixed: ButtonNode, ) private fun createClipSemanticsFixture(): ClipSemanticsFixture { val root = ContainerNode(key = "clip-root", stackLayout = true) - val overflowParent = ContainerNode(key = "clip-parent").apply { - width = 80 - height = 40 - overflowY = Overflow.Hidden - inlineStyleDeclarations = styleDeclarations(StyleProperty.POSITION to "relative") - }.applyParent(root) - val fixed = ButtonNode("fixed", key = "clip-fixed").apply { - width = 40 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "180px", - StyleProperty.TOP to "20px" - ) - }.applyParent(overflowParent) - ButtonNode("absolute", key = "clip-absolute").apply { - width = 40 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "absolute", - StyleProperty.LEFT to "140px", - StyleProperty.TOP to "90px" - ) - }.applyParent(overflowParent) + val overflowParent = + ContainerNode(key = "clip-parent") + .apply { + width = 80 + height = 40 + overflowY = Overflow.Hidden + inlineStyleDeclarations = styleDeclarations(StyleProperty.POSITION to "relative") + }.applyParent(root) + val fixed = + ButtonNode("fixed", key = "clip-fixed") + .apply { + width = 40 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "180px", + StyleProperty.TOP to "20px", + ) + }.applyParent(overflowParent) + ButtonNode("absolute", key = "clip-absolute") + .apply { + width = 40 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "absolute", + StyleProperty.LEFT to "140px", + StyleProperty.TOP to "90px", + ) + }.applyParent(overflowParent) return ClipSemanticsFixture(DomTree(root), root, fixed) } - private fun styleDeclarations(vararg entries: Pair): StyleDeclarations { - return StyleDeclarations().apply { + private fun styleDeclarations(vararg entries: Pair): StyleDeclarations = + StyleDeclarations().apply { entries.forEach { (property, literal) -> set(property, StyleExpression.Literal(literal)) } } - } private data class EventBusListenerSnapshot( val registeredNodes: Int, - val registeredCallbacks: Int + val registeredCallbacks: Int, ) private fun debugEventBusSnapshot(): EventBusListenerSnapshot { val snapshot = EventBus.debugListenerSnapshot() return EventBusListenerSnapshot( registeredNodes = snapshot.registeredNodes, - registeredCallbacks = snapshot.registeredCallbacks + registeredCallbacks = snapshot.registeredCallbacks, ) } } diff --git a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/StickyControlClipAlignmentTests.kt b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/StickyControlClipAlignmentTests.kt index 4abb01d..9a1f791 100644 --- a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/StickyControlClipAlignmentTests.kt +++ b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/StickyControlClipAlignmentTests.kt @@ -1,11 +1,5 @@ package org.dreamfinity.dsgl.mcForge1710 -import kotlin.math.abs -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertNotEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.DomTree import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent @@ -24,16 +18,25 @@ import org.dreamfinity.dsgl.core.style.StyleDeclarations import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleExpression import org.dreamfinity.dsgl.core.style.StyleProperty +import kotlin.math.abs +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue class StickyControlClipAlignmentTests { private val viewportWidth = 420 private val viewportHeight = 260 - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -44,11 +47,12 @@ class StickyControlClipAlignmentTests { @Test fun `non-sticky text input keeps draw and clip aligned`() { - val fixture = createControlFixture( - sticky = false, - scrollY = 0, - controlFactory = { TextInputNode(text = "hello", key = "align-text") } - ) + val fixture = + createControlFixture( + sticky = false, + scrollY = 0, + controlFactory = { TextInputNode(text = "hello", key = "align-text") }, + ) FocusManager.requestFocus(fixture.control as SingleLineInputNode) fixture.tree.paint(ctx) @@ -59,11 +63,12 @@ class StickyControlClipAlignmentTests { @Test fun `sticky-clamped text input keeps shell text and caret clip-aligned`() { - val fixture = createControlFixture( - sticky = true, - scrollY = 46, - controlFactory = { TextInputNode(text = "hello", key = "align-sticky-text") } - ) + val fixture = + createControlFixture( + sticky = true, + scrollY = 46, + controlFactory = { TextInputNode(text = "hello", key = "align-sticky-text") }, + ) FocusManager.requestFocus(fixture.control as SingleLineInputNode) fixture.tree.paint(ctx) @@ -77,11 +82,12 @@ class StickyControlClipAlignmentTests { @Test fun `sticky-clamped number input keeps text and clip aligned`() { - val fixture = createControlFixture( - sticky = true, - scrollY = 46, - controlFactory = { NumberInputNode(value = 42, key = "align-sticky-number") } - ) + val fixture = + createControlFixture( + sticky = true, + scrollY = 46, + controlFactory = { NumberInputNode(value = 42, key = "align-sticky-number") }, + ) fixture.tree.paint(ctx) val visible = visibleRect(fixture.control) @@ -93,19 +99,21 @@ class StickyControlClipAlignmentTests { @Test fun `sticky-clamped closed select keeps label text and clip aligned`() { - val fixture = createControlFixture( - sticky = true, - scrollY = 46, - controlFactory = { - SelectNode( - model = selectModel(id = "sticky-select-model") { - option("a", "Alpha") - option("b", "Beta") - }, - key = "align-sticky-select" - ) - } - ) + val fixture = + createControlFixture( + sticky = true, + scrollY = 46, + controlFactory = { + SelectNode( + model = + selectModel(id = "sticky-select-model") { + option("a", "Alpha") + option("b", "Beta") + }, + key = "align-sticky-select", + ) + }, + ) fixture.tree.paint(ctx) val visible = visibleRect(fixture.control) @@ -117,37 +125,47 @@ class StickyControlClipAlignmentTests { @Test fun `nested clip path stays coherent for sticky-clamped text input`() { - val root = ContainerNode(key = "nested-clip-root").apply { - width = 220 - height = 86 - overflowY = Overflow.Auto - } - ContainerNode(key = "nested-clip-top-spacer").apply { - width = 220 - height = 22 - }.applyParent(root) - val stickyRow = ContainerNode(key = "nested-clip-sticky-row").apply { - width = 220 - height = 30 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.TOP to "0px" - ) - }.applyParent(root) - val nestedClip = ContainerNode(key = "nested-clip-wrapper").apply { - width = 180 - height = 20 - overflowX = Overflow.Hidden - overflowY = Overflow.Hidden - }.applyParent(stickyRow) - val input = TextInputNode(text = "nested", key = "nested-clip-input").apply { - width = 140 - height = 18 - }.applyParent(nestedClip) - ContainerNode(key = "nested-clip-filler").apply { - width = 220 - height = 280 - }.applyParent(root) + val root = + ContainerNode(key = "nested-clip-root").apply { + width = 220 + height = 86 + overflowY = Overflow.Auto + } + ContainerNode(key = "nested-clip-top-spacer") + .apply { + width = 220 + height = 22 + }.applyParent(root) + val stickyRow = + ContainerNode(key = "nested-clip-sticky-row") + .apply { + width = 220 + height = 30 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.TOP to "0px", + ) + }.applyParent(root) + val nestedClip = + ContainerNode(key = "nested-clip-wrapper") + .apply { + width = 180 + height = 20 + overflowX = Overflow.Hidden + overflowY = Overflow.Hidden + }.applyParent(stickyRow) + val input = + TextInputNode(text = "nested", key = "nested-clip-input") + .apply { + width = 140 + height = 18 + }.applyParent(nestedClip) + ContainerNode(key = "nested-clip-filler") + .apply { + width = 220 + height = 280 + }.applyParent(root) val tree = DomTree(root) tree.render(ctx, viewportWidth, viewportHeight) @@ -168,40 +186,44 @@ class StickyControlClipAlignmentTests { assertNotEquals(input.bounds.y, visible.y) } - private fun createControlFixture( - sticky: Boolean, - scrollY: Int, - controlFactory: () -> DOMNode - ): ControlFixture { - val root = ContainerNode(key = "clip-align-root").apply { - width = 220 - height = 86 - overflowY = Overflow.Auto - } + private fun createControlFixture(sticky: Boolean, scrollY: Int, controlFactory: () -> DOMNode): ControlFixture { + val root = + ContainerNode(key = "clip-align-root").apply { + width = 220 + height = 86 + overflowY = Overflow.Auto + } if (sticky) { - ContainerNode(key = "clip-align-top-spacer").apply { + ContainerNode(key = "clip-align-top-spacer") + .apply { + width = 220 + height = 22 + }.applyParent(root) + } + val host = + ContainerNode(key = "clip-align-host") + .apply { + width = 220 + height = 28 + if (sticky) { + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.TOP to "0px", + ) + } + }.applyParent(root) + val control = + controlFactory() + .apply { + width = 140 + height = 18 + }.applyParent(host) + ContainerNode(key = "clip-align-filler") + .apply { width = 220 - height = 22 + height = 280 }.applyParent(root) - } - val host = ContainerNode(key = "clip-align-host").apply { - width = 220 - height = 28 - if (sticky) { - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.TOP to "0px" - ) - } - }.applyParent(root) - val control = controlFactory().apply { - width = 140 - height = 18 - }.applyParent(host) - ContainerNode(key = "clip-align-filler").apply { - width = 220 - height = 280 - }.applyParent(root) val tree = DomTree(root) tree.render(ctx, viewportWidth, viewportHeight) @@ -232,22 +254,24 @@ class StickyControlClipAlignmentTests { is RenderCommand.PopClip -> if (clipStack.isNotEmpty()) clipStack.removeLast() is RenderCommand.DrawText -> { val point = transform.transformPoint(command.x.toFloat(), command.y.toFloat()) - texts += ObservedText( - text = command.text, - x = floorToInt(point.first), - y = floorToInt(point.second), - activeClip = clipStack.lastOrNull() - ) + texts += + ObservedText( + text = command.text, + x = floorToInt(point.first), + y = floorToInt(point.second), + activeClip = clipStack.lastOrNull(), + ) } is RenderCommand.DrawRect -> { val transformed = transform.resolveClipRect(command.x, command.y, command.width, command.height) - rects += ObservedRect( - transformed = transformed, - rawWidth = command.width.coerceAtLeast(0), - rawHeight = command.height.coerceAtLeast(0), - activeClip = clipStack.lastOrNull() - ) + rects += + ObservedRect( + transformed = transformed, + rawWidth = command.width.coerceAtLeast(0), + rawHeight = command.height.coerceAtLeast(0), + activeClip = clipStack.lastOrNull(), + ) } else -> Unit @@ -262,23 +286,24 @@ class StickyControlClipAlignmentTests { assertNotNull(observed.activeClip, "Expected active clip while drawing '$text'") assertTrue( contains(observed.activeClip, observed.x, observed.y), - "Expected transformed clip to contain transformed text point for '$text': point=(${observed.x},${observed.y}) clip=${observed.activeClip}" + "Expected transformed clip to contain transformed text point for '$text': point=(${observed.x},${observed.y}) clip=${observed.activeClip}", ) } private fun assertCaretInsideActiveClip(observations: CommandObservations, nearText: String) { val text = observations.texts.firstOrNull { it.text == nearText } assertNotNull(text, "Expected DrawText('$nearText') for caret alignment assertion") - val caret = observations.rects.firstOrNull { rect -> - rect.rawWidth == 1 && - rect.activeClip != null && - abs(rect.transformed.y - text.y) <= 2 - } + val caret = + observations.rects.firstOrNull { rect -> + rect.rawWidth == 1 && + rect.activeClip != null && + abs(rect.transformed.y - text.y) <= 2 + } assertNotNull(caret, "Expected caret-like 1px rect near text '$nearText'") assertNotNull(caret.activeClip) assertTrue( containsRect(caret.activeClip, caret.transformed), - "Expected caret rect to be clipped by transformed active clip: caret=${caret.transformed} clip=${caret.activeClip}" + "Expected caret rect to be clipped by transformed active clip: caret=${caret.transformed} clip=${caret.activeClip}", ) } @@ -293,10 +318,24 @@ class StickyControlClipAlignmentTests { val maxX = maxOf(topLeft.first, topRight.first, bottomLeft.first, bottomRight.first) val minY = minOf(topLeft.second, topRight.second, bottomLeft.second, bottomRight.second) val maxY = maxOf(topLeft.second, topRight.second, bottomLeft.second, bottomRight.second) - val x = kotlin.math.floor(minX.toDouble()).toInt() - val y = kotlin.math.floor(minY.toDouble()).toInt() - val w = kotlin.math.ceil((maxX - minX).toDouble()).toInt().coerceAtLeast(0) - val h = kotlin.math.ceil((maxY - minY).toDouble()).toInt().coerceAtLeast(0) + val x = + kotlin.math + .floor(minX.toDouble()) + .toInt() + val y = + kotlin.math + .floor(minY.toDouble()) + .toInt() + val w = + kotlin.math + .ceil((maxX - minX).toDouble()) + .toInt() + .coerceAtLeast(0) + val h = + kotlin.math + .ceil((maxY - minY).toDouble()) + .toInt() + .coerceAtLeast(0) return Rect(x, y, w, h) } @@ -315,43 +354,45 @@ class StickyControlClipAlignmentTests { bottom <= clip.y + clip.height } - private fun floorToInt(value: Float): Int = kotlin.math.floor(value.toDouble()).toInt() + private fun floorToInt(value: Float): Int = + kotlin.math + .floor(value.toDouble()) + .toInt() - private fun styleDeclarations(vararg entries: Pair): StyleDeclarations { - return StyleDeclarations().apply { + private fun styleDeclarations(vararg entries: Pair): StyleDeclarations = + StyleDeclarations().apply { entries.forEach { (property, literal) -> set(property, StyleExpression.Literal(literal)) } } - } private data class ControlFixture( val tree: DomTree, - val control: DOMNode + val control: DOMNode, ) private data class CommandObservations( val pushClips: List, val texts: List, - val rects: List + val rects: List, ) private data class ObservedClipPush( val raw: GuiClipRect, - val transformed: GuiClipRect + val transformed: GuiClipRect, ) private data class ObservedText( val text: String, val x: Int, val y: Int, - val activeClip: GuiClipRect? + val activeClip: GuiClipRect?, ) private data class ObservedRect( val transformed: GuiClipRect, val rawWidth: Int, val rawHeight: Int, - val activeClip: GuiClipRect? + val activeClip: GuiClipRect?, ) } diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index a4590e7..d2eb1b8 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -17,4 +17,8 @@ repositories { dependencies { implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") implementation("org.jetbrains.dokka:org.jetbrains.dokka.gradle.plugin:2.1.0") + implementation(libs.plugins.ktlint.toDep()) } + +fun Provider.toDep() = + map { "${it.pluginId}:${it.pluginId}.gradle.plugin:${it.version}" } diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 0000000..aa5e146 --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1,9 @@ +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} + +rootProject.name = "build-logic" diff --git a/build-logic/src/main/kotlin/dsgl-linter.conventions.gradle.kts b/build-logic/src/main/kotlin/dsgl-linter.conventions.gradle.kts new file mode 100644 index 0000000..78c3063 --- /dev/null +++ b/build-logic/src/main/kotlin/dsgl-linter.conventions.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("org.jlleitschuh.gradle.ktlint") +} + +ktlint { + version.set("1.5.0") + outputToConsole.set(true) + coloredOutput.set(true) + reporters { + reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.PLAIN) + reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE) + } + filter { + exclude("**/generated/**") + exclude("**/build/**") + include("**/*.kt") + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 64cd87b..de60cb2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -449,3 +449,13 @@ tasks.register("runDemoClient") { description = "Run Minecraft client with DSGL showcase demo module." dependsOn(":adapters:mc-forge-1-7-10:demo:runClient") } + +tasks.register("installGitHooks") { + group = "setup" + description = "Configure git to use hooks from .githooks/. Run once after cloning." + commandLine("git", "config", "core.hooksPath", ".githooks") + doLast { + fileTree(".githooks").forEach { it.setExecutable(true) } + logger.lifecycle("Git hooks installed. Hook path set to .githooks/") + } +} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 7dc5fd7..26e0766 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("dsgl-core.conventions") + id("dsgl-linter.conventions") id("dsgl-releaseable-module.conventions") id("org.jetbrains.kotlin.plugin.serialization") jacoco @@ -11,9 +12,10 @@ dependencies { testImplementation(kotlin("test")) } -val minLineCoverageRatio = providers - .gradleProperty("dsgl.jacoco.min.line") - .orElse("0.35") +val minLineCoverageRatio = + providers + .gradleProperty("dsgl.jacoco.min.line") + .orElse("0.35") jacoco { toolVersion = "0.8.12" @@ -51,13 +53,14 @@ tasks.check { } tasks.processResources { - val allowedCoreFontBases = setOf( - "fonts/minecraft/MinecraftDefault-Regular", - "fonts/ubuntu/Ubuntu-Regular", - "fonts/noto/Noto_Sans/NotoSans-Regular", - "fonts/jetbrains_mono/JetBrainsMono-Regular", - "fonts/telegrafico/telegrafico" - ) + val allowedCoreFontBases = + setOf( + "fonts/minecraft/MinecraftDefault-Regular", + "fonts/ubuntu/Ubuntu-Regular", + "fonts/noto/Noto_Sans/NotoSans-Regular", + "fonts/jetbrains_mono/JetBrainsMono-Regular", + "fonts/telegrafico/telegrafico", + ) eachFile { if (isDirectory || !path.startsWith("fonts/")) { @@ -65,17 +68,18 @@ tasks.processResources { } val isAllowedFontArtifact = path.endsWith(".ttf") || - path.endsWith(".json") || - path.endsWith(".rgba.deflate") + path.endsWith(".json") || + path.endsWith(".rgba.deflate") if (!isAllowedFontArtifact) { exclude() return@eachFile } - val isAllowedCoreFontArtifact = allowedCoreFontBases.any { base -> - path == "$base.ttf" || + val isAllowedCoreFontArtifact = + allowedCoreFontBases.any { base -> + path == "$base.ttf" || path == "$base-meta.json" || path == "$base-mtsdf.rgba.deflate" - } + } if (!isAllowedCoreFontArtifact) { exclude() } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/DomTree.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/DomTree.kt index bd49ec9..5cc2b1c 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/DomTree.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/DomTree.kt @@ -18,7 +18,9 @@ import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.PositionMode import org.dreamfinity.dsgl.core.style.StyleApplicationScope import org.dreamfinity.dsgl.core.style.StyleEngine -import java.util.* +import java.util.Collections +import java.util.IdentityHashMap +import java.util.WeakHashMap /** * Retained DOM tree. Render phase builds this tree, paint phase draws it. @@ -28,7 +30,7 @@ import java.util.* */ class DomTree( var root: DOMNode, - private val styleScope: StyleApplicationScope = StyleApplicationScope.Application + private val styleScope: StyleApplicationScope = StyleApplicationScope.Application, ) { data class PaintStats( val frames: Long, @@ -37,7 +39,7 @@ class DomTree( val chunkNodesRebuiltLastFrame: Int, val styledNodesLastFrame: Int, val styleCacheHitsLastFrame: Int, - val styleRecomputedLastFrame: Int + val styleRecomputedLastFrame: Int, ) private var lastWidth: Int = 0 @@ -58,14 +60,17 @@ class DomTree( private var chunkTreeChangedThisFrame: Boolean = false private var lastPaintBuildErrorMs: Long = 0L private val chunksByNode: MutableMap = WeakHashMap() - private val debugCommandStackChecks: Boolean = java.lang.Boolean.getBoolean("dsgl.render.debug.stack") - private var lastStyleReport: StyleEngine.StyleApplyReport = StyleEngine.StyleApplyReport( - layoutDirty = false, - visualDirty = false, - visitedNodes = 0, - cacheHits = 0, - recomputedNodes = 0 - ) + private val debugCommandStackChecks: Boolean = + java.lang.Boolean + .getBoolean("dsgl.render.debug.stack") + private var lastStyleReport: StyleEngine.StyleApplyReport = + StyleEngine.StyleApplyReport( + layoutDirty = false, + visualDirty = false, + visitedNodes = 0, + cacheHits = 0, + recomputedNodes = 0, + ) /** Measures and lays out the tree for the given viewport. */ fun render(ctx: UiMeasureContext, width: Int, height: Int) { @@ -77,7 +82,7 @@ class DomTree( root.resolveLayoutStyleValues( ctx = ctx, parentContentWidth = width, - parentContentHeight = height + parentContentHeight = height, ) root.render(ctx, 0, 0, width, height) validateLayout(ctx) @@ -98,31 +103,32 @@ class DomTree( } val scrollInvalidation = root.consumeScrollInvalidationRecursively() val styleRevision = if (applyStyles) StyleEngine.currentStyleRevision(styleScope) else lastStyleRevision - val styleReport = if (applyStyles && (styleRevision != lastStyleRevision || !laidOut)) { - val styleStartNanos = System.nanoTime() - StyleEngine.applyStylesRecursivelyDetailed(root, styleScope).also { - lastStyleReport = it - lastStyleRevision = styleRevision - ScrollPerformanceCounters.recordStyleApplyDuration(System.nanoTime() - styleStartNanos) + val styleReport = + if (applyStyles && (styleRevision != lastStyleRevision || !laidOut)) { + val styleStartNanos = System.nanoTime() + StyleEngine.applyStylesRecursivelyDetailed(root, styleScope).also { + lastStyleReport = it + lastStyleRevision = styleRevision + ScrollPerformanceCounters.recordStyleApplyDuration(System.nanoTime() - styleStartNanos) + } + } else { + StyleEngine.StyleApplyReport( + layoutDirty = false, + visualDirty = false, + visitedNodes = 0, + cacheHits = 0, + recomputedNodes = 0, + ) } - } else { - StyleEngine.StyleApplyReport( - layoutDirty = false, - visualDirty = false, - visitedNodes = 0, - cacheHits = 0, - recomputedNodes = 0 - ) - } val canUseGuardedScrollVisualFastPath = laidOut && - lastWidth > 0 && - lastHeight > 0 && - styleScope != StyleApplicationScope.SystemOverlay && - !styleReport.layoutDirty && - !styleReport.visualDirty && - scrollInvalidation.visualDirty && - !scrollInvalidation.layoutDirty + lastWidth > 0 && + lastHeight > 0 && + styleScope != StyleApplicationScope.SystemOverlay && + !styleReport.layoutDirty && + !styleReport.visualDirty && + scrollInvalidation.visualDirty && + !scrollInvalidation.layoutDirty val stickyLayoutInvalidated = styleReport.layoutDirty || scrollInvalidation.layoutDirty if (stickyLayoutInvalidated) { root.invalidateStickyVisualOffsetsRecursively() @@ -138,7 +144,12 @@ class DomTree( } val requiresSystemOverlayScrollLayoutFallback = styleScope == StyleApplicationScope.SystemOverlay && scrollInvalidation.visualDirty - if ((!laidOut || styleReport.layoutDirty || scrollInvalidation.layoutDirty || requiresSystemOverlayScrollLayoutFallback) && + if (( + !laidOut || + styleReport.layoutDirty || + scrollInvalidation.layoutDirty || + requiresSystemOverlayScrollLayoutFallback + ) && lastWidth > 0 && lastHeight > 0 ) { @@ -146,7 +157,7 @@ class DomTree( root.resolveLayoutStyleValues( ctx = ctx, parentContentWidth = lastWidth, - parentContentHeight = lastHeight + parentContentHeight = lastHeight, ) root.render(ctx, 0, 0, lastWidth, lastHeight) val resolvedStickyNodes = root.refreshStickyVisualOffsetsRecursively() @@ -188,17 +199,16 @@ class DomTree( return result } - fun paintStats(): PaintStats { - return PaintStats( + fun paintStats(): PaintStats = + PaintStats( frames = frames, commandRebuilds = commandRebuilds, chunkNodesVisitedLastFrame = chunkNodesVisitedLastFrame, chunkNodesRebuiltLastFrame = chunkNodesRebuiltLastFrame, styledNodesLastFrame = lastStyleReport.visitedNodes, styleCacheHitsLastFrame = lastStyleReport.cacheHits, - styleRecomputedLastFrame = lastStyleReport.recomputedNodes + styleRecomputedLastFrame = lastStyleReport.recomputedNodes, ) - } fun markVisualDirty() { commandsDirty = true @@ -211,12 +221,13 @@ class DomTree( private fun nextScrollAnimationDtSeconds(): Double { val nowNanos = System.nanoTime() - val dtSeconds = if (lastScrollAnimationNanos == 0L) { - 1.0 / 60.0 - } else { - ((nowNanos - lastScrollAnimationNanos).toDouble() / 1_000_000_000.0) - .coerceIn(1.0 / 240.0, 0.2) - } + val dtSeconds = + if (lastScrollAnimationNanos == 0L) { + 1.0 / 60.0 + } else { + ((nowNanos - lastScrollAnimationNanos).toDouble() / 1_000_000_000.0) + .coerceIn(1.0 / 240.0, 0.2) + } lastScrollAnimationNanos = nowNanos return dtSeconds } @@ -227,12 +238,13 @@ class DomTree( chunkNodesVisitedLastFrame = 0 chunkNodesRebuiltLastFrame = 0 chunkTreeChangedThisFrame = false - val chunkChanged = if (!strictInvalidLayout) { - rebuildChunkRecursive(root, ctx, nowMs) - chunkTreeChangedThisFrame - } else { - false - } + val chunkChanged = + if (!strictInvalidLayout) { + rebuildChunkRecursive(root, ctx, nowMs) + chunkTreeChangedThisFrame + } else { + false + } val changed = commandsDirty || chunkChanged if (!changed) { return false @@ -256,7 +268,9 @@ class DomTree( val now = System.currentTimeMillis() if (now - lastPaintBuildErrorMs >= 2_000L) { lastPaintBuildErrorMs = now - println("[DSGL-DomTree] Paint command rebuild failed; keeping previous frame commands: ${error.message}") + println( + "[DSGL-DomTree] Paint command rebuild failed; keeping previous frame commands: ${error.message}", + ) } false } @@ -327,40 +341,42 @@ class DomTree( val chunk = chunksByNode.getOrPut(node) { RenderCommandChunk() } val nodeHidden = node.dragRenderHidden || node.display == Display.None - val childSignature = if (nodeHidden) { - if (chunk.children.isNotEmpty()) { - chunk.children.clear() - chunkTreeChangedThisFrame = true - } - 0L - } else { - var signature = 1L - val expectedChildren = node.orderedChildrenForPaintTraversal() - var childrenChanged = chunk.children.size != expectedChildren.size - if (childrenChanged) { - chunk.children.clear() - } - expectedChildren.forEachIndexed { index, child -> - val childChunk = rebuildChunkRecursive(child, ctx, nowMs) + val childSignature = + if (nodeHidden) { + if (chunk.children.isNotEmpty()) { + chunk.children.clear() + chunkTreeChangedThisFrame = true + } + 0L + } else { + var signature = 1L + val expectedChildren = node.orderedChildrenForPaintTraversal() + var childrenChanged = chunk.children.size != expectedChildren.size if (childrenChanged) { - chunk.children += childChunk - } else if (chunk.children[index] !== childChunk) { - childrenChanged = true + chunk.children.clear() } - signature = 31L * signature + childChunk.subtreeSignature - } - if (childrenChanged) { - chunkTreeChangedThisFrame = true - chunk.children.clear() - expectedChildren.forEach { child -> - chunk.children += chunksByNode.getOrPut(child) { RenderCommandChunk() } + expectedChildren.forEachIndexed { index, child -> + val childChunk = rebuildChunkRecursive(child, ctx, nowMs) + if (childrenChanged) { + chunk.children += childChunk + } else if (chunk.children[index] !== childChunk) { + childrenChanged = true + } + signature = 31L * signature + childChunk.subtreeSignature + } + if (childrenChanged) { + chunkTreeChangedThisFrame = true + chunk.children.clear() + expectedChildren.forEach { child -> + chunk.children += chunksByNode.getOrPut(child) { RenderCommandChunk() } + } } + signature } - signature - } val nodeSignature = node.renderCommandsSignature(nowMs) - val rebuildSelf = chunk.lastNodeSignature != nodeSignature || + val rebuildSelf = + chunk.lastNodeSignature != nodeSignature || chunk.lastChildrenSignature != childSignature || chunk.lastNodeSignature == Long.MIN_VALUE @@ -381,7 +397,7 @@ class DomTree( node: DOMNode, chunk: RenderCommandChunk, ctx: UiMeasureContext, - nodeHidden: Boolean + nodeHidden: Boolean, ) { chunk.prefixCommands.clear() chunk.selfCommands.clear() @@ -394,33 +410,36 @@ class DomTree( val activeOpacity = node.effectiveOpacity() val transformPushed = !activeTransform.isIdentity() val opacityPushed = activeOpacity < 0.999f - val promotedFixedRootViewportClipRect = if (node.position == PositionMode.Fixed) { - node.fixedViewportClipRectForPromotedParticipation() - } else { - null - } + val promotedFixedRootViewportClipRect = + if (node.position == PositionMode.Fixed) { + node.fixedViewportClipRectForPromotedParticipation() + } else { + null + } if (promotedFixedRootViewportClipRect != null) { - chunk.prefixCommands += RenderCommand.PushClip( - x = promotedFixedRootViewportClipRect.x, - y = promotedFixedRootViewportClipRect.y, - width = promotedFixedRootViewportClipRect.width.coerceAtLeast(0), - height = promotedFixedRootViewportClipRect.height.coerceAtLeast(0) - ) + chunk.prefixCommands += + RenderCommand.PushClip( + x = promotedFixedRootViewportClipRect.x, + y = promotedFixedRootViewportClipRect.y, + width = promotedFixedRootViewportClipRect.width.coerceAtLeast(0), + height = promotedFixedRootViewportClipRect.height.coerceAtLeast(0), + ) } if (transformPushed) { val ox = node.bounds.x + node.bounds.width * node.transformOrigin.originX val oy = node.bounds.y + node.bounds.height * node.transformOrigin.originY - chunk.prefixCommands += RenderCommand.PushTransform( - originX = ox, - originY = oy, - translateX = activeTransform.translateX, - translateY = activeTransform.translateY, - scaleX = activeTransform.scaleX, - scaleY = activeTransform.scaleY, - rotateDeg = activeTransform.rotateDeg - ) + chunk.prefixCommands += + RenderCommand.PushTransform( + originX = ox, + originY = oy, + translateX = activeTransform.translateX, + translateY = activeTransform.translateY, + scaleX = activeTransform.scaleX, + scaleY = activeTransform.scaleY, + rotateDeg = activeTransform.rotateDeg, + ) } if (opacityPushed) { chunk.prefixCommands += RenderCommand.PushOpacity(activeOpacity) @@ -432,12 +451,13 @@ class DomTree( val clipRect = node.overflowViewportRect() if (clipRect != null) { - chunk.childrenPrefixCommands += RenderCommand.PushClip( - x = clipRect.x, - y = clipRect.y, - width = clipRect.width.coerceAtLeast(0), - height = clipRect.height.coerceAtLeast(0) - ) + chunk.childrenPrefixCommands += + RenderCommand.PushClip( + x = clipRect.x, + y = clipRect.y, + width = clipRect.width.coerceAtLeast(0), + height = clipRect.height.coerceAtLeast(0), + ) chunk.childrenSuffixCommands += RenderCommand.PopClip } @@ -499,9 +519,8 @@ class DomTree( val first = violations.first() println( "[DSGL-Layout] strict mode invalidated paint/hit-test due to ${first.code} " + - "key=${first.nodeKey} parent=${first.parentKey}: ${first.message}" + "key=${first.nodeKey} parent=${first.parentKey}: ${first.message}", ) } } } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/DsglColors.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/DsglColors.kt index 0e19ede..91d7f4c 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/DsglColors.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/DsglColors.kt @@ -10,4 +10,4 @@ object DsglColors { const val BUTTON: Int = 0xFF3A3A40.toInt() const val TEXT: Int = 0xFFE0E0E0.toInt() const val BORDER: Int = 0xFF000000.toInt() -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/DsglWindow.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/DsglWindow.kt index 43406e6..e3d1446 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/DsglWindow.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/DsglWindow.kt @@ -42,9 +42,7 @@ abstract class DsglWindow { * * This is intentionally separate from component-local hook state. */ - fun state(initial: T): MutableState { - return mutableStateOf(initial) { invalidate() } - } + fun state(initial: T): MutableState = mutableStateOf(initial) { invalidate() } /** Build the current UI tree. Called by the host on rebuild. */ abstract fun render(): DomTree @@ -52,16 +50,12 @@ abstract class DsglWindow { /** * Builds a tree bound to this window so UiScope hook APIs can resolve the owning runtime. */ - protected fun ui(block: UiScope.() -> Unit): DomTree { - return ui(this, block) - } + protected fun ui(block: UiScope.() -> Unit): DomTree = ui(this, block) /** * Builds a tree from a custom root bound to this window. */ - protected fun ui(root: DOMNode, block: UiScope.() -> Unit): DomTree { - return ui(this, root, block) - } + protected fun ui(root: DOMNode, block: UiScope.() -> Unit): DomTree = ui(this, root, block) /** Lifecycle callback when the UI is opened by the host. */ open fun onOpen() {} @@ -134,4 +128,3 @@ abstract class DsglWindow { internal fun hookRuntime(): ComponentHookRuntime = componentHookRuntime } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/HotReloadBridge.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/HotReloadBridge.kt index 596e8bc..6d5cf24 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/HotReloadBridge.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/HotReloadBridge.kt @@ -12,7 +12,5 @@ object HotReloadBridge { } @JvmStatic - fun consumeHotSwap(): Boolean { - return HOTSWAP_PENDING.getAndSet(false) - } -} \ No newline at end of file + fun consumeHotSwap(): Boolean = HOTSWAP_PENDING.getAndSet(false) +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/ItemStackRef.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/ItemStackRef.kt index ff8f6ab..4898e96 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/ItemStackRef.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/ItemStackRef.kt @@ -3,4 +3,4 @@ package org.dreamfinity.dsgl.core /** * Opaque reference to a platform-specific ItemStack. */ -interface ItemStackRef \ No newline at end of file +interface ItemStackRef diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/State.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/State.kt index 7e5d204..b197a21 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/State.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/State.kt @@ -14,7 +14,7 @@ interface State { */ class MutableState( initial: T, - private val onChange: () -> Unit = {} + private val onChange: () -> Unit = {}, ) : State { private var _value: T = initial @@ -34,6 +34,4 @@ class MutableState( } /** Creates mutable state with an optional change callback. */ -fun mutableStateOf(initial: T, onChange: () -> Unit = {}): MutableState { - return MutableState(initial, onChange) -} \ No newline at end of file +fun mutableStateOf(initial: T, onChange: () -> Unit = {}): MutableState = MutableState(initial, onChange) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/AnimationDslBuilders.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/AnimationDslBuilders.kt index d4292d5..be17782 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/AnimationDslBuilders.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/AnimationDslBuilders.kt @@ -12,14 +12,15 @@ class TransitionBuilder internal constructor() { property: AnimatedStyleProperty, durationMs: Int, delayMs: Int = 0, - easing: Easing = Easings.EASE + easing: Easing = Easings.EASE, ) { - specs += TransitionPropertySpec( - property = property, - durationMs = durationMs.coerceAtLeast(0), - delayMs = delayMs.coerceAtLeast(0), - easing = easing - ) + specs += + TransitionPropertySpec( + property = property, + durationMs = durationMs.coerceAtLeast(0), + delayMs = delayMs.coerceAtLeast(0), + easing = easing, + ) } internal fun build(): TransitionSpec = TransitionSpec(specs.toList()) @@ -36,32 +37,27 @@ class AnimationListBuilder internal constructor() { iterationCount: IterationCount = IterationCount.Count(1), direction: AnimationDirection = AnimationDirection.Normal, fillMode: AnimationFillMode = AnimationFillMode.None, - playState: AnimationPlayState = AnimationPlayState.Running + playState: AnimationPlayState = AnimationPlayState.Running, ) { - specs += AnimationSpec( - name = name, - durationMs = durationMs.coerceAtLeast(0), - delayMs = delayMs.coerceAtLeast(0), - easing = easing, - iterationCount = iterationCount, - direction = direction, - fillMode = fillMode, - playState = playState - ) + specs += + AnimationSpec( + name = name, + durationMs = durationMs.coerceAtLeast(0), + delayMs = delayMs.coerceAtLeast(0), + easing = easing, + iterationCount = iterationCount, + direction = direction, + fillMode = fillMode, + playState = playState, + ) } internal fun build(): List = specs.toList() } -fun keyframes( - name: String, - block: KeyframesBuilder.() -> Unit -) { +fun keyframes(name: String, block: KeyframesBuilder.() -> Unit) { val definition = KeyframesBuilder(name).apply(block).build() KeyframesRegistry.register(definition) } -fun transform(block: UiTransformBuilder.() -> Unit): UiTransform { - return UiTransformBuilder().apply(block).build() -} - +fun transform(block: UiTransformBuilder.() -> Unit): UiTransform = UiTransformBuilder().apply(block).build() diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/AnimationModel.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/AnimationModel.kt index 26bf4c4..e31226b 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/AnimationModel.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/AnimationModel.kt @@ -5,7 +5,7 @@ import org.dreamfinity.dsgl.core.style.UiTransform enum class AnimatedStyleProperty { Transform, Opacity, - Color + Color, } object StyleAnimProps { @@ -17,7 +17,7 @@ object StyleAnimProps { data class StylePropertyKey( val id: String, val property: AnimatedStyleProperty, - val animatable: Animatable + val animatable: Animatable, ) interface Animatable { @@ -25,9 +25,7 @@ interface Animatable { } object FloatAnimatable : Animatable { - override fun interpolate(from: Float, to: Float, t: Float): Float { - return from + (to - from) * t - } + override fun interpolate(from: Float, to: Float, t: Float): Float = from + (to - from) * t } object ColorAnimatable : Animatable { @@ -58,7 +56,7 @@ object TransformAnimatable : Animatable { translateY = from.translateY + (to.translateY - from.translateY) * t, scaleX = from.scaleX + (to.scaleX - from.scaleX) * t, scaleY = from.scaleY + (to.scaleY - from.scaleY) * t, - rotateDeg = from.rotateDeg + rotateDelta * t + rotateDeg = from.rotateDeg + rotateDelta * t, ) } @@ -81,45 +79,50 @@ data class TransitionPropertySpec( val property: AnimatedStyleProperty, val durationMs: Int, val delayMs: Int = 0, - val easing: Easing = Easings.EASE + val easing: Easing = Easings.EASE, ) data class TransitionSpec( - val properties: List + val properties: List, ) { companion object { val NONE: TransitionSpec = TransitionSpec(emptyList()) } - fun forProperty(property: AnimatedStyleProperty): TransitionPropertySpec? { - return properties.lastOrNull { it.property == property } - } + fun forProperty(property: AnimatedStyleProperty): TransitionPropertySpec? = + properties.lastOrNull { + it.property == property + } } enum class AnimationDirection { Normal, Reverse, Alternate, - AlternateReverse + AlternateReverse, } enum class AnimationFillMode { None, Forwards, Backwards, - Both + Both, } enum class AnimationPlayState { Running, - Paused + Paused, } sealed class IterationCount { - data class Count(val value: Int) : IterationCount() + data class Count( + val value: Int, + ) : IterationCount() + data object Infinite : IterationCount() fun isInfinite(): Boolean = this is Infinite + fun finiteValueOrNull(): Int? = (this as? Count)?.value } @@ -131,23 +134,23 @@ data class AnimationSpec( val iterationCount: IterationCount = IterationCount.Count(1), val direction: AnimationDirection = AnimationDirection.Normal, val fillMode: AnimationFillMode = AnimationFillMode.None, - val playState: AnimationPlayState = AnimationPlayState.Running + val playState: AnimationPlayState = AnimationPlayState.Running, ) data class KeyframeValue( val transform: UiTransform? = null, val opacity: Float? = null, - val color: Int? = null + val color: Int? = null, ) data class Keyframe( val fraction: Float, - val value: KeyframeValue + val value: KeyframeValue, ) data class KeyframesDefinition( val name: String, - val frames: List + val frames: List, ) { init { require(frames.isNotEmpty()) { "Keyframes '$name' must contain at least one frame." } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/Easing.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/Easing.kt index 9b6b57e..1a20377 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/Easing.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/Easing.kt @@ -13,20 +13,26 @@ object Easings { val EASE_OUT: Easing = cubicBezier(0f, 0f, 0.58f, 1f) val EASE_IN_OUT: Easing = cubicBezier(0.42f, 0f, 0.58f, 1f) - fun cubicBezier(x1: Float, y1: Float, x2: Float, y2: Float): Easing { - return CubicBezierEasing(x1, y1, x2, y2) - } + fun cubicBezier( + x1: Float, + y1: Float, + x2: Float, + y2: Float, + ): Easing = CubicBezierEasing(x1, y1, x2, y2) } -fun cubicBezier(x1: Float, y1: Float, x2: Float, y2: Float): Easing { - return Easings.cubicBezier(x1, y1, x2, y2) -} +fun cubicBezier( + x1: Float, + y1: Float, + x2: Float, + y2: Float, +): Easing = Easings.cubicBezier(x1, y1, x2, y2) private class CubicBezierEasing( private val x1: Float, private val y1: Float, private val x2: Float, - private val y2: Float + private val y2: Float, ) : Easing { override fun map(t: Float): Float { val clamped = t.coerceIn(0f, 1f) @@ -52,15 +58,14 @@ private class CubicBezierEasing( private fun sampleCurveX(t: Double): Double { val omt = 1.0 - t return 3.0 * omt * omt * t * x1 + - 3.0 * omt * t * t * x2 + - t * t * t + 3.0 * omt * t * t * x2 + + t * t * t } private fun sampleCurveY(t: Double): Double { val omt = 1.0 - t return 3.0 * omt * omt * t * y1 + - 3.0 * omt * t * t * y2 + - t * t * t + 3.0 * omt * t * t * y2 + + t * t * t } } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/KeyframesRegistry.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/KeyframesRegistry.kt index ba20bb3..380ddf2 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/KeyframesRegistry.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/KeyframesRegistry.kt @@ -18,7 +18,7 @@ object KeyframesRegistry { } class KeyframesBuilder internal constructor( - private val name: String + private val name: String, ) { private val frames: MutableList = ArrayList() @@ -48,13 +48,12 @@ class KeyframeScope internal constructor() { transform = UiTransformBuilder().apply(block).build() } - internal fun build(): KeyframeValue { - return KeyframeValue( + internal fun build(): KeyframeValue = + KeyframeValue( transform = transform, opacity = opacity?.coerceIn(0f, 1f), - color = color + color = color, ) - } } class UiTransformBuilder internal constructor() { @@ -78,17 +77,16 @@ class UiTransformBuilder internal constructor() { rotate += deg } - fun build(): UiTransform { - return UiTransform(translateX = tx, translateY = ty, scaleX = sx, scaleY = sy, rotateDeg = rotate) - } + fun build(): UiTransform = + UiTransform(translateX = tx, translateY = ty, scaleX = sx, scaleY = sy, rotateDeg = rotate) } internal fun KeyframesDefinition.normalized(): KeyframesDefinition { - val normalizedFrames = frames - .sortedBy { it.fraction } - .map { frame -> - frame.copy(fraction = frame.fraction.coerceIn(0f, 1f)) - } + val normalizedFrames = + frames + .sortedBy { it.fraction } + .map { frame -> + frame.copy(fraction = frame.fraction.coerceIn(0f, 1f)) + } return copy(frames = normalizedFrames) } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/StyleAnimationEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/StyleAnimationEngine.kt index f3d634d..5baf9eb 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/StyleAnimationEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/StyleAnimationEngine.kt @@ -11,7 +11,7 @@ object StyleAnimationEngine { val activeKeyframes: List, val effectiveTransform: UiTransform, val effectiveOpacity: Float, - val effectiveColor: Int? + val effectiveColor: Int?, ) private data class TransitionState( @@ -22,7 +22,7 @@ object StyleAnimationEngine { val delaySec: Double, val durationSec: Double, val easing: Easing, - val animatable: Animatable + val animatable: Animatable, ) { fun valueAt(nowSec: Double): Pair { if (durationSec <= 0.0) return to to true @@ -39,7 +39,7 @@ object StyleAnimationEngine { val spec: AnimationSpec, var startedAtSec: Double, var pausedElapsedSec: Double? = null, - var previousPlayState: AnimationPlayState = spec.playState + var previousPlayState: AnimationPlayState = spec.playState, ) private data class NodeAnimationState( @@ -52,7 +52,7 @@ object StyleAnimationEngine { var effectiveTransform: UiTransform = UiTransform.IDENTITY, var effectiveOpacity: Float = 1f, var effectiveColor: Int? = null, - var lastSeenFrame: Long = 0L + var lastSeenFrame: Long = 0L, ) private val states: MutableMap = linkedMapOf() @@ -75,21 +75,21 @@ object StyleAnimationEngine { transitions = transitions, property = AnimatedStyleProperty.Transform, previousValue = previous.transform, - currentValue = current.transform + currentValue = current.transform, ) maybeRetargetTransition( state = state, transitions = transitions, property = AnimatedStyleProperty.Opacity, previousValue = previous.opacity, - currentValue = current.opacity + currentValue = current.opacity, ) maybeRetargetTransition( state = state, transitions = transitions, property = AnimatedStyleProperty.Color, previousValue = previous.foregroundColor, - currentValue = current.foregroundColor + currentValue = current.foregroundColor, ) } @@ -130,25 +130,38 @@ object StyleAnimationEngine { } private fun debugSnapshot(state: NodeAnimationState): DebugSnapshot { - val transitions = state.transitions.values.map { transition -> - val local = (nowSec - transition.startSec - transition.delaySec).coerceAtLeast(0.0) - val progress = if (transition.durationSec <= 0.0) 1.0 else (local / transition.durationSec).coerceIn(0.0, 1.0) - "${transition.property.name.lowercase()}:${(progress * 100.0).toInt()}%" - } - val keyframes = state.keyframes.map { running -> - val elapsedSec = resolveElapsed(running, nowSec) - val delaySec = running.spec.delayMs.coerceAtLeast(0) / 1000.0 - val durationSec = running.spec.durationMs.coerceAtLeast(1) / 1000.0 - val active = (elapsedSec - delaySec).coerceAtLeast(0.0) - val rawProgress = if (durationSec <= 0.0) 1.0 else (active % durationSec) / durationSec - "${running.spec.name}:${(rawProgress * 100.0).toInt()}%:${running.spec.playState.name.lowercase()}" - } + val transitions = + state.transitions.values.map { transition -> + val local = (nowSec - transition.startSec - transition.delaySec).coerceAtLeast(0.0) + val progress = + if (transition.durationSec <= + 0.0 + ) { + 1.0 + } else { + (local / transition.durationSec).coerceIn(0.0, 1.0) + } + "${transition.property.name.lowercase()}:${(progress * 100.0).toInt()}%" + } + val keyframes = + state.keyframes.map { running -> + val elapsedSec = resolveElapsed(running, nowSec) + val delaySec = + running.spec.delayMs + .coerceAtLeast(0) / 1000.0 + val durationSec = + running.spec.durationMs + .coerceAtLeast(1) / 1000.0 + val active = (elapsedSec - delaySec).coerceAtLeast(0.0) + val rawProgress = if (durationSec <= 0.0) 1.0 else (active % durationSec) / durationSec + "${running.spec.name}:${(rawProgress * 100.0).toInt()}%:${running.spec.playState.name.lowercase()}" + } return DebugSnapshot( activeTransitions = transitions, activeKeyframes = keyframes, effectiveTransform = state.effectiveTransform, effectiveOpacity = state.effectiveOpacity, - effectiveColor = state.effectiveColor + effectiveColor = state.effectiveColor, ) } @@ -186,10 +199,11 @@ object StyleAnimationEngine { } if (node.applyAnimationVisuals( - transform = resolvedTransform, - opacity = resolvedOpacity, - color = resolvedColor - )) { + transform = resolvedTransform, + opacity = resolvedOpacity, + color = resolvedColor, + ) + ) { changed = true } state.effectiveTransform = node.effectiveTransform() @@ -204,7 +218,7 @@ object StyleAnimationEngine { state.effectiveColor = node.animationColorOverride() logRateLimited( key = "anim:${node.key ?: node.styleType}", - message = "[DSGL-Anim] ${node.key ?: node.styleType}: ${error.message}" + message = "[DSGL-Anim] ${node.key ?: node.styleType}: ${error.message}", ) } } @@ -232,24 +246,26 @@ object StyleAnimationEngine { transitions: TransitionSpec, property: AnimatedStyleProperty, previousValue: UiTransform, - currentValue: UiTransform + currentValue: UiTransform, ) { if (previousValue == currentValue) return - val spec = transitions.forProperty(property) ?: run { - state.transitions.remove(property) - return - } + val spec = + transitions.forProperty(property) ?: run { + state.transitions.remove(property) + return + } val currentVisual = transitionValue(state, property) ?: previousValue - state.transitions[property] = TransitionState( - property = property, - from = currentVisual, - to = currentValue, - startSec = nowSec, - delaySec = spec.delayMs.coerceAtLeast(0) / 1000.0, - durationSec = spec.durationMs.coerceAtLeast(0) / 1000.0, - easing = spec.easing, - animatable = TransformAnimatable - ) + state.transitions[property] = + TransitionState( + property = property, + from = currentVisual, + to = currentValue, + startSec = nowSec, + delaySec = spec.delayMs.coerceAtLeast(0) / 1000.0, + durationSec = spec.durationMs.coerceAtLeast(0) / 1000.0, + easing = spec.easing, + animatable = TransformAnimatable, + ) } private fun maybeRetargetTransition( @@ -257,24 +273,26 @@ object StyleAnimationEngine { transitions: TransitionSpec, property: AnimatedStyleProperty, previousValue: Float, - currentValue: Float + currentValue: Float, ) { if (previousValue == currentValue) return - val spec = transitions.forProperty(property) ?: run { - state.transitions.remove(property) - return - } + val spec = + transitions.forProperty(property) ?: run { + state.transitions.remove(property) + return + } val currentVisual = transitionValue(state, property) ?: previousValue - state.transitions[property] = TransitionState( - property = property, - from = currentVisual, - to = currentValue, - startSec = nowSec, - delaySec = spec.delayMs.coerceAtLeast(0) / 1000.0, - durationSec = spec.durationMs.coerceAtLeast(0) / 1000.0, - easing = spec.easing, - animatable = FloatAnimatable - ) + state.transitions[property] = + TransitionState( + property = property, + from = currentVisual, + to = currentValue, + startSec = nowSec, + delaySec = spec.delayMs.coerceAtLeast(0) / 1000.0, + durationSec = spec.durationMs.coerceAtLeast(0) / 1000.0, + easing = spec.easing, + animatable = FloatAnimatable, + ) } private fun maybeRetargetTransition( @@ -282,24 +300,26 @@ object StyleAnimationEngine { transitions: TransitionSpec, property: AnimatedStyleProperty, previousValue: Int, - currentValue: Int + currentValue: Int, ) { if (previousValue == currentValue) return - val spec = transitions.forProperty(property) ?: run { - state.transitions.remove(property) - return - } + val spec = + transitions.forProperty(property) ?: run { + state.transitions.remove(property) + return + } val currentVisual = transitionValue(state, property) ?: previousValue - state.transitions[property] = TransitionState( - property = property, - from = currentVisual, - to = currentValue, - startSec = nowSec, - delaySec = spec.delayMs.coerceAtLeast(0) / 1000.0, - durationSec = spec.durationMs.coerceAtLeast(0) / 1000.0, - easing = spec.easing, - animatable = ColorAnimatable - ) + state.transitions[property] = + TransitionState( + property = property, + from = currentVisual, + to = currentValue, + startSec = nowSec, + delaySec = spec.delayMs.coerceAtLeast(0) / 1000.0, + durationSec = spec.durationMs.coerceAtLeast(0) / 1000.0, + easing = spec.easing, + animatable = ColorAnimatable, + ) } @Suppress("UNCHECKED_CAST") @@ -312,10 +332,7 @@ object StyleAnimationEngine { return value } - private fun sampleKeyframe( - definition: KeyframesDefinition, - running: RunningKeyframe - ): KeyframeValue { + private fun sampleKeyframe(definition: KeyframesDefinition, running: RunningKeyframe): KeyframeValue { val spec = running.spec if (spec.durationMs <= 0) { return sampleDefinition(definition, progress = 1f, easing = spec.easing) @@ -329,12 +346,15 @@ object StyleAnimationEngine { if (activeSec < 0.0) { return if (spec.fillMode == AnimationFillMode.Backwards || spec.fillMode == AnimationFillMode.Both) { - val startProgress = when (spec.direction) { - AnimationDirection.Reverse, - AnimationDirection.AlternateReverse -> 1f - AnimationDirection.Normal, - AnimationDirection.Alternate -> 0f - } + val startProgress = + when (spec.direction) { + AnimationDirection.Reverse, + AnimationDirection.AlternateReverse, + -> 1f + AnimationDirection.Normal, + AnimationDirection.Alternate, + -> 0f + } sampleDefinition(definition, startProgress, spec.easing) } else { KeyframeValue() @@ -377,60 +397,89 @@ object StyleAnimationEngine { return nowSec - running.startedAtSec } - private fun sampleDefinition( - definition: KeyframesDefinition, - progress: Float, - easing: Easing - ): KeyframeValue { - return KeyframeValue( + private fun sampleDefinition(definition: KeyframesDefinition, progress: Float, easing: Easing): KeyframeValue = + KeyframeValue( transform = sampleTransform(definition, progress, easing), opacity = sampleOpacity(definition, progress, easing), - color = sampleColor(definition, progress, easing) + color = sampleColor(definition, progress, easing), ) - } private fun sampleTransform(definition: KeyframesDefinition, progress: Float, easing: Easing): UiTransform? { val frames = definition.frames.filter { it.value.transform != null } if (frames.isEmpty()) return null - if (progress <= frames.first().fraction) return frames.first().value.transform - if (progress >= frames.last().fraction) return frames.last().value.transform - val pair = surrounding(frames, progress) ?: return frames.last().value.transform + if (progress <= frames.first().fraction) { + return frames + .first() + .value.transform + } + if (progress >= frames.last().fraction) { + return frames + .last() + .value.transform + } + val pair = + surrounding(frames, progress) ?: return frames + .last() + .value.transform val t = normalizedProgress(pair.first.fraction, pair.second.fraction, progress) val eased = easing.map(t) return TransformAnimatable.interpolate( pair.first.value.transform!!, pair.second.value.transform!!, - eased + eased, ) } private fun sampleOpacity(definition: KeyframesDefinition, progress: Float, easing: Easing): Float? { val frames = definition.frames.filter { it.value.opacity != null } if (frames.isEmpty()) return null - if (progress <= frames.first().fraction) return frames.first().value.opacity - if (progress >= frames.last().fraction) return frames.last().value.opacity - val pair = surrounding(frames, progress) ?: return frames.last().value.opacity + if (progress <= frames.first().fraction) { + return frames + .first() + .value.opacity + } + if (progress >= frames.last().fraction) { + return frames + .last() + .value.opacity + } + val pair = + surrounding(frames, progress) ?: return frames + .last() + .value.opacity val t = normalizedProgress(pair.first.fraction, pair.second.fraction, progress) val eased = easing.map(t) - return FloatAnimatable.interpolate( - pair.first.value.opacity!!, - pair.second.value.opacity!!, - eased - ).coerceIn(0f, 1f) + return FloatAnimatable + .interpolate( + pair.first.value.opacity!!, + pair.second.value.opacity!!, + eased, + ).coerceIn(0f, 1f) } private fun sampleColor(definition: KeyframesDefinition, progress: Float, easing: Easing): Int? { val frames = definition.frames.filter { it.value.color != null } if (frames.isEmpty()) return null - if (progress <= frames.first().fraction) return frames.first().value.color - if (progress >= frames.last().fraction) return frames.last().value.color - val pair = surrounding(frames, progress) ?: return frames.last().value.color + if (progress <= frames.first().fraction) { + return frames + .first() + .value.color + } + if (progress >= frames.last().fraction) { + return frames + .last() + .value.color + } + val pair = + surrounding(frames, progress) ?: return frames + .last() + .value.color val t = normalizedProgress(pair.first.fraction, pair.second.fraction, progress) val eased = easing.map(t) return ColorAnimatable.interpolate( pair.first.value.color!!, pair.second.value.color!!, - eased + eased, ) } @@ -450,18 +499,15 @@ object StyleAnimationEngine { return ((value - left) / span).coerceIn(0f, 1f) } - private fun isIterationReversed(direction: AnimationDirection, iterationIndex: Int): Boolean { - return when (direction) { + private fun isIterationReversed(direction: AnimationDirection, iterationIndex: Int): Boolean = + when (direction) { AnimationDirection.Normal -> false AnimationDirection.Reverse -> true AnimationDirection.Alternate -> iterationIndex % 2 == 1 AnimationDirection.AlternateReverse -> iterationIndex % 2 == 0 } - } - private fun animationToken(node: DOMNode): Any { - return node.key ?: node - } + private fun animationToken(node: DOMNode): Any = node.key ?: node private fun logRateLimited(key: String, message: String) { val last = errorRateLimitByKey[key] diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ActiveColorSamplerOwnership.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ActiveColorSamplerOwnership.kt index 29cdf26..fab8ae0 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ActiveColorSamplerOwnership.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ActiveColorSamplerOwnership.kt @@ -2,8 +2,12 @@ package org.dreamfinity.dsgl.core.colorpicker sealed interface ActiveColorSamplerOwner { data object None : ActiveColorSamplerOwner + data object Popup : ActiveColorSamplerOwner - data class Inline(val token: Any) : ActiveColorSamplerOwner + + data class Inline( + val token: Any, + ) : ActiveColorSamplerOwner } /** @@ -22,26 +26,25 @@ class ActiveColorSamplerOwnershipRouter { previousInlineActiveTokens = emptySet() } - fun update( - popupEyedropperActive: Boolean, - inlineActiveTokens: Set - ): ActiveColorSamplerOwner { + fun update(popupEyedropperActive: Boolean, inlineActiveTokens: Set): ActiveColorSamplerOwner { val currentInlineTokens = inlineActiveTokens.toSet() val popupActivatedNow = popupEyedropperActive && !previousPopupActive - val inlineActivatedNow = currentInlineTokens.firstOrNull { token -> - !previousInlineActiveTokens.contains(token) - } + val inlineActivatedNow = + currentInlineTokens.firstOrNull { token -> + !previousInlineActiveTokens.contains(token) + } - val next = when { - inlineActivatedNow != null -> ActiveColorSamplerOwner.Inline(inlineActivatedNow) - activeOwner is ActiveColorSamplerOwner.Inline && + val next = + when { + inlineActivatedNow != null -> ActiveColorSamplerOwner.Inline(inlineActivatedNow) + activeOwner is ActiveColorSamplerOwner.Inline && currentInlineTokens.contains((activeOwner as ActiveColorSamplerOwner.Inline).token) -> activeOwner - popupActivatedNow -> ActiveColorSamplerOwner.Popup - activeOwner === ActiveColorSamplerOwner.Popup && popupEyedropperActive -> ActiveColorSamplerOwner.Popup - currentInlineTokens.isNotEmpty() -> ActiveColorSamplerOwner.Inline(currentInlineTokens.first()) - popupEyedropperActive -> ActiveColorSamplerOwner.Popup - else -> ActiveColorSamplerOwner.None - } + popupActivatedNow -> ActiveColorSamplerOwner.Popup + activeOwner === ActiveColorSamplerOwner.Popup && popupEyedropperActive -> ActiveColorSamplerOwner.Popup + currentInlineTokens.isNotEmpty() -> ActiveColorSamplerOwner.Inline(currentInlineTokens.first()) + popupEyedropperActive -> ActiveColorSamplerOwner.Popup + else -> ActiveColorSamplerOwner.None + } activeOwner = next previousPopupActive = popupEyedropperActive diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt index f354b6e..d72e409 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt @@ -12,12 +12,12 @@ data class ColorPickerInputSlot( val key: String, val label: String, val labelRect: Rect, - val inputRect: Rect + val inputRect: Rect, ) data class ColorPickerModeOptionSlot( val mode: ColorFormatMode, - val rect: Rect + val rect: Rect, ) data class ColorPickerLayout( @@ -36,7 +36,7 @@ data class ColorPickerLayout( val pasteRect: Rect, val pipetteRect: Rect, val inputSlots: List, - val recentRects: List + val recentRects: List, ) data class ColorPickerEyedropperOverlayModel( @@ -46,17 +46,18 @@ data class ColorPickerEyedropperOverlayModel( val centerRect: Rect, val swatchRect: Rect, val modeText: String, - val valueText: String + val valueText: String, ) class ColorPickerController( initial: ColorPickerState, private val style: ColorPickerStyle = ColorPickerStyle(), private val recentHistory: ColorRecentHistory = ColorRecentHistory(), - private val screenSampler: ScreenColorSampler? = ScreenColorSampler { x, y -> - val sampled = ScreenColorSamplerBridge.sampleColorAt(x, y) ?: return@ScreenColorSampler null - sampled.toArgbInt() - } + private val screenSampler: ScreenColorSampler? = + ScreenColorSampler { x, y -> + val sampled = ScreenColorSamplerBridge.sampleColorAt(x, y) ?: return@ScreenColorSampler null + sampled.toArgbInt() + }, ) { private var state: ColorPickerState = initial.withColor(initial.color) private var hueDeg: Float = ColorConversions.rgbToHsv(state.color).hueDeg @@ -148,17 +149,14 @@ class ColorPickerController( internal fun viewInputValues(): Map = inputValues() - internal fun viewInputDefinitions(): List> { - return inputDefinitions().map { it.key to it.label } - } + internal fun viewInputDefinitions(): List> = inputDefinitions().map { it.key to it.label } internal fun viewRecentColors(): List = recentHistory.snapshot() internal fun viewCaretVisible(nowMs: Long): Boolean = caretVisible(nowMs) - internal fun viewFormattedColor(): String { - return ColorTextCodec.format(state.color, state.mode, state.alphaEnabled, state.rgbOrder) - } + internal fun viewFormattedColor(): String = + ColorTextCodec.format(state.color, state.mode, state.alphaEnabled, state.rgbOrder) internal fun handleDomInputFocused(key: String) { domFocusedInputKey = key @@ -238,21 +236,24 @@ class ColorPickerController( val modeRowGap = style.modeRowGap.coerceAtLeast(1) val orderLabelWidth = style.rgbOrderLabelWidth.coerceAtLeast(32) val modeSelectMinWidth = style.modeSelectMinWidth.coerceAtLeast(84) - val showRgbOrderSwitch = state.alphaEnabled && - innerW >= (modeSelectMinWidth + modeRowGap + orderLabelWidth * 2 + orderSlotGap) - val modeRowReserved = if (showRgbOrderSwitch) { - orderLabelWidth * 2 + orderSlotGap + modeRowGap - } else { - 0 - } + val showRgbOrderSwitch = + state.alphaEnabled && + innerW >= (modeSelectMinWidth + modeRowGap + orderLabelWidth * 2 + orderSlotGap) + val modeRowReserved = + if (showRgbOrderSwitch) { + orderLabelWidth * 2 + orderSlotGap + modeRowGap + } else { + 0 + } val modeSelectMax = (innerW - modeRowReserved).coerceAtLeast(1) val modeSelectWidth = minOf(style.modeSelectWidth.coerceAtLeast(84), modeSelectMax).coerceAtLeast(1) - val modeSelectRect = Rect( - x = innerX, - y = innerY, - width = modeSelectWidth, - height = style.modeSelectHeight - ) + val modeSelectRect = + Rect( + x = innerX, + y = innerY, + width = modeSelectWidth, + height = style.modeSelectHeight, + ) val rgbaOrderRect: Rect? val argbOrderRect: Rect? if (showRgbOrderSwitch) { @@ -263,18 +264,20 @@ class ColorPickerController( val firstWidth = ((orderAvailable - gap) / 2).coerceAtLeast(1) val secondX = orderStartX + firstWidth + gap val secondWidth = (innerX + innerW - secondX).coerceAtLeast(1) - rgbaOrderRect = Rect( - x = orderStartX, - y = innerY, - width = firstWidth, - height = style.modeSelectHeight - ) - argbOrderRect = Rect( - x = secondX, - y = innerY, - width = secondWidth, - height = style.modeSelectHeight - ) + rgbaOrderRect = + Rect( + x = orderStartX, + y = innerY, + width = firstWidth, + height = style.modeSelectHeight, + ) + argbOrderRect = + Rect( + x = secondX, + y = innerY, + width = secondWidth, + height = style.modeSelectHeight, + ) } else { rgbaOrderRect = null argbOrderRect = null @@ -284,49 +287,54 @@ class ColorPickerController( argbOrderRect = null } val modeOptions = ArrayList(ColorFormatMode.entries.size) - val modeOptionsRect = if (modeDropdownOpen) { - val optionHeight = style.modeOptionHeight - val popupWidth = maxOf(modeSelectRect.width, style.modeSelectMinWidth) - val popupHeight = optionHeight * ColorFormatMode.entries.size + 2 - val minX = innerX - val maxX = (innerX + innerW - popupWidth).coerceAtLeast(innerX) - val popupX = if (minX <= maxX) modeSelectRect.x.coerceIn(minX, maxX) else minX - val preferredBelowY = modeSelectRect.y + modeSelectRect.height + 2 - val minY = innerY - val maxY = (bounds.y + bounds.height - padding - popupHeight).coerceAtLeast(minY) - val popupY = if (preferredBelowY <= maxY) { - preferredBelowY + val modeOptionsRect = + if (modeDropdownOpen) { + val optionHeight = style.modeOptionHeight + val popupWidth = maxOf(modeSelectRect.width, style.modeSelectMinWidth) + val popupHeight = optionHeight * ColorFormatMode.entries.size + 2 + val minX = innerX + val maxX = (innerX + innerW - popupWidth).coerceAtLeast(innerX) + val popupX = if (minX <= maxX) modeSelectRect.x.coerceIn(minX, maxX) else minX + val preferredBelowY = modeSelectRect.y + modeSelectRect.height + 2 + val minY = innerY + val maxY = (bounds.y + bounds.height - padding - popupHeight).coerceAtLeast(minY) + val popupY = + if (preferredBelowY <= maxY) { + preferredBelowY + } else { + if (minY <= maxY) (modeSelectRect.y - popupHeight - 2).coerceIn(minY, maxY) else minY + } + ColorFormatMode.entries.forEachIndexed { index, mode -> + modeOptions += + ColorPickerModeOptionSlot( + mode = mode, + rect = + Rect( + x = popupX + 1, + y = popupY + 1 + index * optionHeight, + width = popupWidth - 2, + height = optionHeight, + ), + ) + } + Rect(popupX, popupY, popupWidth, popupHeight) } else { - if (minY <= maxY) (modeSelectRect.y - popupHeight - 2).coerceIn(minY, maxY) else minY + null } - ColorFormatMode.entries.forEachIndexed { index, mode -> - modeOptions += ColorPickerModeOptionSlot( - mode = mode, - rect = Rect( - x = popupX + 1, - y = popupY + 1 + index * optionHeight, - width = popupWidth - 2, - height = optionHeight - ) - ) - } - Rect(popupX, popupY, popupWidth, popupHeight) - } else { - null - } var y = innerY + style.modeSelectHeight + rowGap val fieldRect = Rect(innerX, y, innerW, style.colorFieldHeight) y += style.colorFieldHeight + rowGap val hueRect = Rect(innerX, y, innerW, style.sliderHeight) y += style.sliderHeight + rowGap - val alphaRect = if (state.alphaEnabled) { - Rect(innerX, y, innerW, style.sliderHeight).also { - y += style.sliderHeight + rowGap + val alphaRect = + if (state.alphaEnabled) { + Rect(innerX, y, innerW, style.sliderHeight).also { + y += style.sliderHeight + rowGap + } + } else { + null } - } else { - null - } val swatchWidth = ((innerW - rowGap * 4) / 5).coerceAtLeast(24) val previousSwatchRect = Rect(innerX, y, swatchWidth, style.swatchHeight) @@ -343,31 +351,35 @@ class ColorPickerController( val labelWidth = style.inputLabelWidth.coerceAtLeast(12) inputDefs.forEachIndexed { index, def -> val slotX = innerX + index * (inputWidth + rowGap) - val localLabelWidth = if (def.key == "hex") { - labelWidth + 8 - } else { - labelWidth - } + val localLabelWidth = + if (def.key == "hex") { + labelWidth + 8 + } else { + labelWidth + } val inputX = slotX + localLabelWidth + style.inputLabelGap val inputW = (inputWidth - localLabelWidth - style.inputLabelGap).coerceAtLeast(20) - val labelRect = Rect( - x = slotX, - y = y, - width = localLabelWidth, - height = style.inputHeight - ) - val inputRect = Rect( - x = inputX, - y = y, - width = inputW, - height = style.inputHeight - ) - inputs += ColorPickerInputSlot( - key = def.key, - label = def.label, - labelRect = labelRect, - inputRect = inputRect - ) + val labelRect = + Rect( + x = slotX, + y = y, + width = localLabelWidth, + height = style.inputHeight, + ) + val inputRect = + Rect( + x = inputX, + y = y, + width = inputW, + height = style.inputHeight, + ) + inputs += + ColorPickerInputSlot( + key = def.key, + label = def.label, + labelRect = labelRect, + inputRect = inputRect, + ) } y += style.inputHeight + rowGap @@ -402,19 +414,20 @@ class ColorPickerController( pasteRect = pasteRect, pipetteRect = pipetteRect, inputSlots = inputs, - recentRects = recentRects + recentRects = recentRects, ) } fun preferredHeight(alphaEnabled: Boolean = state.alphaEnabled): Int { val rowGap = style.rowGap - val core = style.padding * 2 + - style.modeSelectHeight + rowGap + - style.colorFieldHeight + rowGap + - style.sliderHeight + rowGap + - (if (alphaEnabled) style.sliderHeight + rowGap else 0) + - style.swatchHeight + rowGap + - style.inputHeight + rowGap + val core = + style.padding * 2 + + style.modeSelectHeight + rowGap + + style.colorFieldHeight + rowGap + + style.sliderHeight + rowGap + + (if (alphaEnabled) style.sliderHeight + rowGap else 0) + + style.swatchHeight + rowGap + + style.inputHeight + rowGap val recentGrid = style.recentCellSize * 8 + style.recentCellGap * 7 return core + recentGrid } @@ -422,7 +435,7 @@ class ColorPickerController( fun appendCommands( layout: ColorPickerLayout, out: MutableList, - nowMs: Long = System.currentTimeMillis() + nowMs: Long = System.currentTimeMillis(), ) { val bounds = layout.bounds out += RenderCommand.DrawRect(bounds.x, bounds.y, bounds.width, bounds.height, style.panelBackgroundColor) @@ -451,34 +464,51 @@ class ColorPickerController( layout.inputSlots.forEach { slot -> val active = activeInputKey == slot.key val hovered = slot.inputRect.contains(hoverX, hoverY) - val border = when { - active -> style.inputActiveBorderColor - hovered -> style.buttonHoverColor - else -> style.inputBorderColor - } - out += RenderCommand.DrawText( - text = slot.label, - x = slot.labelRect.x + 2, - y = slot.labelRect.y + 2, - color = style.mutedTextColor, - fontSize = style.fontSize - ) - out += RenderCommand.DrawRect( - slot.inputRect.x, - slot.inputRect.y, - slot.inputRect.width, - slot.inputRect.height, - style.inputBackgroundColor - ) + val border = + when { + active -> style.inputActiveBorderColor + hovered -> style.buttonHoverColor + else -> style.inputBorderColor + } + out += + RenderCommand.DrawText( + text = slot.label, + x = slot.labelRect.x + 2, + y = slot.labelRect.y + 2, + color = style.mutedTextColor, + fontSize = style.fontSize, + ) + out += + RenderCommand.DrawRect( + slot.inputRect.x, + slot.inputRect.y, + slot.inputRect.width, + slot.inputRect.height, + style.inputBackgroundColor, + ) drawBorder(out, slot.inputRect, border) - val value = if (active) activeInputBuffer + if (caretVisible(nowMs)) "|" else "" else inputValues[slot.key].orEmpty() - out += RenderCommand.DrawText( - text = truncate(value, 12), - x = slot.inputRect.x + 4, - y = slot.inputRect.y + 2, - color = style.textColor, - fontSize = style.fontSize - ) + val value = + if (active) { + activeInputBuffer + + if (caretVisible( + nowMs, + ) + ) { + "|" + } else { + "" + } + } else { + inputValues[slot.key].orEmpty() + } + out += + RenderCommand.DrawText( + text = truncate(value, 12), + x = slot.inputRect.x + 4, + y = slot.inputRect.y + 2, + color = style.textColor, + fontSize = style.fontSize, + ) } val recents = recentHistory.snapshot() @@ -498,11 +528,7 @@ class ColorPickerController( drawModeOptions(layout, out) } - fun appendEyedropperOverlay( - viewportWidth: Int, - viewportHeight: Int, - out: MutableList - ) { + fun appendEyedropperOverlay(viewportWidth: Int, viewportHeight: Int, out: MutableList) { if (!eyedropperActive) return if (hoverX == Int.MIN_VALUE || hoverY == Int.MIN_VALUE) return @@ -518,45 +544,49 @@ class ColorPickerController( val preferredX = hoverX + style.eyedropperGapToCursor val preferredY = hoverY + style.eyedropperGapToCursor - val desiredRect = clampOverlayRect( - rect = Rect(preferredX, preferredY, panelWidth, panelHeight), - viewportWidth = viewportWidth, - viewportHeight = viewportHeight - ) + val desiredRect = + clampOverlayRect( + rect = Rect(preferredX, preferredY, panelWidth, panelHeight), + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + ) val currentRect = eyedropperOverlayRect if (currentRect == null || currentRect.width != panelWidth || currentRect.height != panelHeight) { eyedropperOverlayRect = desiredRect eyedropperOverlayDrag.begin(mouseX = hoverX, mouseY = hoverY, rect = desiredRect) } - val nextRect = eyedropperOverlayDrag.update( - mouseX = hoverX, - mouseY = hoverY, - viewportWidth = viewportWidth, - viewportHeight = viewportHeight, - clamp = ::clampOverlayRect - ) + val nextRect = + eyedropperOverlayDrag.update( + mouseX = hoverX, + mouseY = hoverY, + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + clamp = ::clampOverlayRect, + ) eyedropperOverlayRect = nextRect val panelX = nextRect.x val panelY = nextRect.y val magnifierX = panelX + 4 val magnifierY = panelY + 4 - out += RenderCommand.CaptureScreenRegion( - sourceX = hoverX - gridSize / 2, - sourceY = hoverY - gridSize / 2, - sourceWidth = gridSize, - sourceHeight = gridSize, - fallbackColor = state.color.toArgbInt() - ) + out += + RenderCommand.CaptureScreenRegion( + sourceX = hoverX - gridSize / 2, + sourceY = hoverY - gridSize / 2, + sourceWidth = gridSize, + sourceHeight = gridSize, + fallbackColor = state.color.toArgbInt(), + ) out += RenderCommand.DrawRect(panelX + 2, panelY + 2, panelWidth, panelHeight, style.panelShadowColor) out += RenderCommand.DrawRect(panelX, panelY, panelWidth, panelHeight, style.eyedropperOverlayBackgroundColor) drawBorder(out, Rect(panelX, panelY, panelWidth, panelHeight), style.eyedropperOverlayBorderColor) - out += RenderCommand.DrawCapturedScreenRegion( - x = magnifierX, - y = magnifierY, - width = magnifierContentSize, - height = magnifierContentSize - ) + out += + RenderCommand.DrawCapturedScreenRegion( + x = magnifierX, + y = magnifierY, + width = magnifierContentSize, + height = magnifierContentSize, + ) if (style.eyedropperGridOverlayEnabled) { drawEyedropperGridOverlay( out = out, @@ -564,18 +594,23 @@ class ColorPickerController( columns = gridSize, rows = gridSize, cellSize = cell, - color = style.eyedropperGridOverlayColor + color = style.eyedropperGridOverlayColor, ) } - drawBorder(out, Rect(magnifierX, magnifierY, magnifierContentSize, magnifierContentSize), style.inputBorderColor) + drawBorder( + out, + Rect(magnifierX, magnifierY, magnifierContentSize, magnifierContentSize), + style.inputBorderColor, + ) val center = gridSize / 2 - val centerRect = Rect( - x = magnifierX + center * cell, - y = magnifierY + center * cell, - width = cell, - height = cell - ) + val centerRect = + Rect( + x = magnifierX + center * cell, + y = magnifierY + center * cell, + width = cell, + height = cell, + ) drawBorder(out, centerRect, style.eyedropperCenterBorderColor) val tooltipY = magnifierY + magnifierContentSize + 6 @@ -583,31 +618,34 @@ class ColorPickerController( val swatchRect = Rect(panelX + 6, tooltipY + 5, swatchSize, swatchSize) drawSwatch(swatchRect, state.color, out) - val modeText = if (state.mode == ColorFormatMode.RGB && state.alphaEnabled) { - "Mode: ${state.mode.name} (${state.rgbOrder.name})" - } else { - "Mode: ${state.mode.name}" - } + val modeText = + if (state.mode == ColorFormatMode.RGB && state.alphaEnabled) { + "Mode: ${state.mode.name} (${state.rgbOrder.name})" + } else { + "Mode: ${state.mode.name}" + } val valueText = ColorTextCodec.format(state.color, state.mode, state.alphaEnabled, state.rgbOrder) - out += RenderCommand.DrawText( - text = modeText, - x = swatchRect.x + swatchRect.width + 8, - y = tooltipY + 6, - color = style.mutedTextColor, - fontSize = style.fontSize - ) - out += RenderCommand.DrawText( - text = valueText, - x = swatchRect.x + swatchRect.width + 8, - y = tooltipY + 6 + style.fontSize, - color = style.textColor, - fontSize = style.fontSize - ) + out += + RenderCommand.DrawText( + text = modeText, + x = swatchRect.x + swatchRect.width + 8, + y = tooltipY + 6, + color = style.mutedTextColor, + fontSize = style.fontSize, + ) + out += + RenderCommand.DrawText( + text = valueText, + x = swatchRect.x + swatchRect.width + 8, + y = tooltipY + 6 + style.fontSize, + color = style.textColor, + fontSize = style.fontSize, + ) } internal fun resolveEyedropperOverlayModel( viewportWidth: Int, - viewportHeight: Int + viewportHeight: Int, ): ColorPickerEyedropperOverlayModel? { if (!eyedropperActive) return null if (hoverX == Int.MIN_VALUE || hoverY == Int.MIN_VALUE) return null @@ -624,60 +662,66 @@ class ColorPickerController( val preferredX = hoverX + style.eyedropperGapToCursor val preferredY = hoverY + style.eyedropperGapToCursor - val desiredRect = clampOverlayRect( - rect = Rect(preferredX, preferredY, panelWidth, panelHeight), - viewportWidth = viewportWidth, - viewportHeight = viewportHeight - ) + val desiredRect = + clampOverlayRect( + rect = Rect(preferredX, preferredY, panelWidth, panelHeight), + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + ) val currentRect = eyedropperOverlayRect if (currentRect == null || currentRect.width != panelWidth || currentRect.height != panelHeight) { eyedropperOverlayRect = desiredRect eyedropperOverlayDrag.begin(mouseX = hoverX, mouseY = hoverY, rect = desiredRect) } - val nextRect = eyedropperOverlayDrag.update( - mouseX = hoverX, - mouseY = hoverY, - viewportWidth = viewportWidth, - viewportHeight = viewportHeight, - clamp = ::clampOverlayRect - ) + val nextRect = + eyedropperOverlayDrag.update( + mouseX = hoverX, + mouseY = hoverY, + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + clamp = ::clampOverlayRect, + ) eyedropperOverlayRect = nextRect - val magnifierRect = Rect( - x = nextRect.x + 4, - y = nextRect.y + 4, - width = magnifierContentSize, - height = magnifierContentSize - ) + val magnifierRect = + Rect( + x = nextRect.x + 4, + y = nextRect.y + 4, + width = magnifierContentSize, + height = magnifierContentSize, + ) val center = gridSize / 2 - val centerRect = Rect( - x = magnifierRect.x + center * cell, - y = magnifierRect.y + center * cell, - width = cell, - height = cell - ) + val centerRect = + Rect( + x = magnifierRect.x + center * cell, + y = magnifierRect.y + center * cell, + width = cell, + height = cell, + ) val tooltipY = magnifierRect.y + magnifierRect.height + 6 val swatchSize = tooltipHeight - 10 val swatchRect = Rect(nextRect.x + 6, tooltipY + 5, swatchSize, swatchSize) - val modeText = if (state.mode == ColorFormatMode.RGB && state.alphaEnabled) { - "Mode: ${state.mode.name} (${state.rgbOrder.name})" - } else { - "Mode: ${state.mode.name}" - } + val modeText = + if (state.mode == ColorFormatMode.RGB && state.alphaEnabled) { + "Mode: ${state.mode.name} (${state.rgbOrder.name})" + } else { + "Mode: ${state.mode.name}" + } val valueText = ColorTextCodec.format(state.color, state.mode, state.alphaEnabled, state.rgbOrder) return ColorPickerEyedropperOverlayModel( panelRect = nextRect, magnifierRect = magnifierRect, - captureSourceRect = Rect( - x = hoverX - gridSize / 2, - y = hoverY - gridSize / 2, - width = gridSize, - height = gridSize - ), + captureSourceRect = + Rect( + x = hoverX - gridSize / 2, + y = hoverY - gridSize / 2, + width = gridSize, + height = gridSize, + ), centerRect = centerRect, swatchRect = swatchRect, modeText = modeText, - valueText = valueText + valueText = valueText, ) } @@ -712,7 +756,12 @@ class ColorPickerController( } } - fun handleMouseDown(globalX: Int, globalY: Int, button: MouseButton, layout: ColorPickerLayout): Boolean { + fun handleMouseDown( + globalX: Int, + globalY: Int, + button: MouseButton, + layout: ColorPickerLayout, + ): Boolean { interaction.setHover(globalX, globalY) if (eyedropperActive) { return when (button) { @@ -735,14 +784,16 @@ class ColorPickerController( modeDropdownOpen = false return true } - return layout.bounds.contains(globalX, globalY) || layout.modeOptionsRect?.contains(globalX, globalY) == true + return layout.bounds.contains(globalX, globalY) || + layout.modeOptionsRect?.contains(globalX, globalY) == true } - val modeOptionHit = if (modeDropdownOpen) { - layout.modeOptions.firstOrNull { it.rect.contains(globalX, globalY) } - } else { - null - } + val modeOptionHit = + if (modeDropdownOpen) { + layout.modeOptions.firstOrNull { it.rect.contains(globalX, globalY) } + } else { + null + } if (modeOptionHit != null) { state = state.copy(mode = modeOptionHit.mode) modeDropdownOpen = false @@ -812,10 +863,11 @@ class ColorPickerController( if (parsed != null) { val next = if (state.alphaEnabled) parsed.color else parsed.color.copy(a = 1f) applyColor(next, notifyPreview = true, commit = false) - state = state.copy( - mode = parsed.detectedMode, - rgbOrder = parsed.detectedRgbOrder ?: state.rgbOrder - ) + state = + state.copy( + mode = parsed.detectedMode, + rgbOrder = parsed.detectedRgbOrder ?: state.rgbOrder, + ) } return true } @@ -867,21 +919,23 @@ class ColorPickerController( if (KeyModifiers.shortcutDown && keyCode == KeyCodes.V) { val parsed = ColorClipboardSupport.paste() ?: return true applyColor(parsed.color, notifyPreview = true, commit = false) - state = state.copy( - mode = parsed.detectedMode, - rgbOrder = parsed.detectedRgbOrder ?: state.rgbOrder - ) + state = + state.copy( + mode = parsed.detectedMode, + rgbOrder = parsed.detectedRgbOrder ?: state.rgbOrder, + ) return true } - val key = activeInputKey ?: run { - if (keyCode == KeyCodes.ESCAPE) { - if (modeDropdownOpen) { - modeDropdownOpen = false - return true + val key = + activeInputKey ?: run { + if (keyCode == KeyCodes.ESCAPE) { + if (modeDropdownOpen) { + modeDropdownOpen = false + return true + } } + return false } - return false - } when (keyCode) { KeyCodes.ESCAPE -> { clearInputEdit() @@ -932,9 +986,7 @@ class ColorPickerController( interaction.textInput.clear() } - private fun applyInputDraft(key: String): Boolean { - return applyInputDraftValue(key, activeInputBuffer) - } + private fun applyInputDraft(key: String): Boolean = applyInputDraftValue(key, activeInputBuffer) private fun applyInputDraftValue(key: String, rawValue: String): Boolean { val value = rawValue.trim() @@ -952,12 +1004,13 @@ class ColorPickerController( val g = if (key == "g") value.toFloatOrNull() ?: return false else current.g * 255f val b = if (key == "b") value.toFloatOrNull() ?: return false else current.b * 255f val a = if (key == "a") value.toFloatOrNull() ?: return false else current.a * 100f - val next = RgbaColor( - r = (r / 255f).coerceIn(0f, 1f), - g = (g / 255f).coerceIn(0f, 1f), - b = (b / 255f).coerceIn(0f, 1f), - a = (a / 100f).coerceIn(0f, 1f) - ) + val next = + RgbaColor( + r = (r / 255f).coerceIn(0f, 1f), + g = (g / 255f).coerceIn(0f, 1f), + b = (b / 255f).coerceIn(0f, 1f), + a = (a / 100f).coerceIn(0f, 1f), + ) applyColor(next, notifyPreview = true, commit = false) return true } @@ -966,33 +1019,46 @@ class ColorPickerController( val hue = if (key == "h") value.toFloatOrNull() ?: return false else hueDeg val currentHsl = ColorConversions.rgbToHsl(current, hueDeg) val currentHsv = ColorConversions.rgbToHsv(current, hueDeg) - val sValue = if (key == "s") value.toFloatOrNull() ?: return false else { - if (state.mode == ColorFormatMode.HSL) currentHsl.saturation * 100f else currentHsv.saturation * 100f - } - val thirdValue = when { - key == "l" || (key == "v" && state.mode == ColorFormatMode.HSB) -> value.toFloatOrNull() ?: return false - state.mode == ColorFormatMode.HSL -> currentHsl.lightness * 100f - else -> currentHsv.brightness * 100f - } - val next = if (state.mode == ColorFormatMode.HSL) { - ColorConversions.hslToRgb( - HslColor( - hueDeg = normalizeHue(hue), - saturation = (sValue / 100f).coerceIn(0f, 1f), - lightness = (thirdValue / 100f).coerceIn(0f, 1f) - ), - alpha = current.a - ) - } else { - ColorConversions.hsvToRgb( - HsvColor( - hueDeg = normalizeHue(hue), - saturation = (sValue / 100f).coerceIn(0f, 1f), - brightness = (thirdValue / 100f).coerceIn(0f, 1f) - ), - alpha = current.a - ) - } + val sValue = + if (key == "s") { + value.toFloatOrNull() ?: return false + } else { + if (state.mode == + ColorFormatMode.HSL + ) { + currentHsl.saturation * 100f + } else { + currentHsv.saturation * 100f + } + } + val thirdValue = + when { + key == "l" || (key == "v" && state.mode == ColorFormatMode.HSB) -> + value.toFloatOrNull() + ?: return false + state.mode == ColorFormatMode.HSL -> currentHsl.lightness * 100f + else -> currentHsv.brightness * 100f + } + val next = + if (state.mode == ColorFormatMode.HSL) { + ColorConversions.hslToRgb( + HslColor( + hueDeg = normalizeHue(hue), + saturation = (sValue / 100f).coerceIn(0f, 1f), + lightness = (thirdValue / 100f).coerceIn(0f, 1f), + ), + alpha = current.a, + ) + } else { + ColorConversions.hsvToRgb( + HsvColor( + hueDeg = normalizeHue(hue), + saturation = (sValue / 100f).coerceIn(0f, 1f), + brightness = (thirdValue / 100f).coerceIn(0f, 1f), + ), + alpha = current.a, + ) + } applyColor(next, notifyPreview = true, commit = false) return true } @@ -1008,11 +1074,12 @@ class ColorPickerController( } ColorFormatMode.RGB -> { - val rgbValues = hashMapOf( - "r" to ((state.color.r * 255f).roundToInt().coerceIn(0, 255)).toString(), - "g" to ((state.color.g * 255f).roundToInt().coerceIn(0, 255)).toString(), - "b" to ((state.color.b * 255f).roundToInt().coerceIn(0, 255)).toString() - ) + val rgbValues = + hashMapOf( + "r" to ((state.color.r * 255f).roundToInt().coerceIn(0, 255)).toString(), + "g" to ((state.color.g * 255f).roundToInt().coerceIn(0, 255)).toString(), + "b" to ((state.color.b * 255f).roundToInt().coerceIn(0, 255)).toString(), + ) if (state.alphaEnabled) { rgbValues["a"] = ((state.color.a * 100f).roundToInt().coerceIn(0, 100)).toString() } @@ -1023,7 +1090,10 @@ class ColorPickerController( ColorFormatMode.HSL -> { val hsl = ColorConversions.rgbToHsl(state.color, hueDeg) - values["h"] = hsl.hueDeg.roundToInt().toString() + values["h"] = + hsl.hueDeg + .roundToInt() + .toString() values["s"] = (hsl.saturation * 100f).roundToInt().toString() values["l"] = (hsl.lightness * 100f).roundToInt().toString() if (state.alphaEnabled) { @@ -1033,7 +1103,10 @@ class ColorPickerController( ColorFormatMode.HSB -> { val hsb = ColorConversions.rgbToHsv(state.color, hueDeg) - values["h"] = hsb.hueDeg.roundToInt().toString() + values["h"] = + hsb.hueDeg + .roundToInt() + .toString() values["s"] = (hsb.saturation * 100f).roundToInt().toString() values["v"] = (hsb.brightness * 100f).roundToInt().toString() if (state.alphaEnabled) { @@ -1050,13 +1123,14 @@ class ColorPickerController( ColorFormatMode.RGB -> { val defs = ArrayList() rgbInputOrder().forEach { key -> - defs += when (key) { - "r" -> InputDefinition("r", "R") - "g" -> InputDefinition("g", "G") - "b" -> InputDefinition("b", "B") - "a" -> InputDefinition("a", "A%") - else -> return@forEach - } + defs += + when (key) { + "r" -> InputDefinition("r", "R") + "g" -> InputDefinition("g", "G") + "b" -> InputDefinition("b", "B") + "a" -> InputDefinition("a", "A%") + else -> return@forEach + } } defs } @@ -1081,43 +1155,75 @@ class ColorPickerController( } } - private fun updateFromField(globalX: Int, globalY: Int, rect: Rect, commit: Boolean) { - val px = ((globalX - rect.x).toFloat() / rect.width.coerceAtLeast(1).toFloat()).coerceIn(0f, 1f) - val py = ((globalY - rect.y).toFloat() / rect.height.coerceAtLeast(1).toFloat()).coerceIn(0f, 1f) + private fun updateFromField( + globalX: Int, + globalY: Int, + rect: Rect, + commit: Boolean, + ) { + val px = + ( + (globalX - rect.x).toFloat() / + rect.width + .coerceAtLeast(1) + .toFloat() + ).coerceIn(0f, 1f) + val py = + ( + (globalY - rect.y).toFloat() / + rect.height + .coerceAtLeast(1) + .toFloat() + ).coerceIn(0f, 1f) val saturation = px val brightness = 1f - py - val next = ColorConversions.hsvToRgb( - hsv = HsvColor(hueDeg = hueDeg, saturation = saturation, brightness = brightness), - alpha = state.color.a - ) + val next = + ColorConversions.hsvToRgb( + hsv = HsvColor(hueDeg = hueDeg, saturation = saturation, brightness = brightness), + alpha = state.color.a, + ) applyColor(next, notifyPreview = true, commit = commit) } private fun updateFromHue(globalX: Int, rect: Rect, commit: Boolean) { - val progress = ((globalX - rect.x).toFloat() / rect.width.coerceAtLeast(1).toFloat()).coerceIn(0f, 1f) + val progress = + ( + (globalX - rect.x).toFloat() / + rect.width + .coerceAtLeast(1) + .toFloat() + ).coerceIn(0f, 1f) hueDeg = progress * 360f val hsv = ColorConversions.rgbToHsv(state.color, hueDeg) - val next = ColorConversions.hsvToRgb( - hsv.copy(hueDeg = hueDeg), - alpha = state.color.a - ) + val next = + ColorConversions.hsvToRgb( + hsv.copy(hueDeg = hueDeg), + alpha = state.color.a, + ) applyColor(next, notifyPreview = true, commit = commit) } private fun updateFromAlpha(globalX: Int, rect: Rect, commit: Boolean) { if (!state.alphaEnabled) return - val progress = ((globalX - rect.x).toFloat() / rect.width.coerceAtLeast(1).toFloat()).coerceIn(0f, 1f) + val progress = + ( + (globalX - rect.x).toFloat() / + rect.width + .coerceAtLeast(1) + .toFloat() + ).coerceIn(0f, 1f) applyColor(state.color.copy(a = progress), notifyPreview = true, commit = commit) } private fun sampleEyedropper(x: Int, y: Int, commit: Boolean) { val argb = screenSampler?.sampleColorAt(x, y) ?: return val sampled = RgbaColor.fromArgbInt(argb) - val color = if (state.alphaEnabled) { - sampled.copy(a = state.color.a) - } else { - sampled.copy(a = 1f) - } + val color = + if (state.alphaEnabled) { + sampled.copy(a = state.color.a) + } else { + sampled.copy(a = 1f) + } applyColor(color, notifyPreview = true, commit = commit) } @@ -1137,7 +1243,7 @@ class ColorPickerController( x = rect.x.coerceIn(minX, maxX), y = rect.y.coerceIn(minY, maxY), width = rect.width, - height = rect.height + height = rect.height, ) } @@ -1167,20 +1273,22 @@ class ColorPickerController( val fill = if (hovered || modeDropdownOpen) style.buttonHoverColor else style.buttonBackgroundColor out += RenderCommand.DrawRect(rect.x, rect.y, rect.width, rect.height, fill) drawBorder(out, rect, if (modeDropdownOpen) style.inputActiveBorderColor else style.inputBorderColor) - out += RenderCommand.DrawText( - text = state.mode.name, - x = rect.x + 6, - y = rect.y + 2, - color = style.textColor, - fontSize = style.fontSize - ) - out += RenderCommand.DrawText( - text = if (modeDropdownOpen) "^" else "v", - x = rect.x + rect.width - 12, - y = rect.y + 2, - color = style.textColor, - fontSize = style.fontSize - ) + out += + RenderCommand.DrawText( + text = state.mode.name, + x = rect.x + 6, + y = rect.y + 2, + color = style.textColor, + fontSize = style.fontSize, + ) + out += + RenderCommand.DrawText( + text = if (modeDropdownOpen) "^" else "v", + x = rect.x + rect.width - 12, + y = rect.y + 2, + color = style.textColor, + fontSize = style.fontSize, + ) } private fun drawRgbOrderSwitch(layout: ColorPickerLayout, out: MutableList) { @@ -1191,14 +1299,14 @@ class ColorPickerController( label = "RGBA", active = state.rgbOrder == RgbChannelOrder.RGBA, hovered = rgbaRect.contains(hoverX, hoverY), - out = out + out = out, ) drawOrderLabel( rect = argbRect, label = "ARGB", active = state.rgbOrder == RgbChannelOrder.ARGB, hovered = argbRect.contains(hoverX, hoverY), - out = out + out = out, ) } @@ -1207,66 +1315,84 @@ class ColorPickerController( label: String, active: Boolean, hovered: Boolean, - out: MutableList + out: MutableList, ) { - val fill = when { - active -> style.buttonActiveColor - hovered -> style.buttonHoverColor - else -> style.buttonBackgroundColor - } + val fill = + when { + active -> style.buttonActiveColor + hovered -> style.buttonHoverColor + else -> style.buttonBackgroundColor + } out += RenderCommand.DrawRect(rect.x, rect.y, rect.width, rect.height, fill) drawBorder(out, rect, if (active) style.inputActiveBorderColor else style.inputBorderColor) - out += RenderCommand.DrawText( - text = label, - x = rect.x + 6, - y = rect.y + 2, - color = style.textColor, - fontSize = style.fontSize - ) + out += + RenderCommand.DrawText( + text = label, + x = rect.x + 6, + y = rect.y + 2, + color = style.textColor, + fontSize = style.fontSize, + ) } private fun drawModeOptions(layout: ColorPickerLayout, out: MutableList) { val popupRect = layout.modeOptionsRect ?: return - out += RenderCommand.DrawRect(popupRect.x, popupRect.y, popupRect.width, popupRect.height, style.inputBackgroundColor) + out += + RenderCommand.DrawRect( + popupRect.x, + popupRect.y, + popupRect.width, + popupRect.height, + style.inputBackgroundColor, + ) drawBorder(out, popupRect, style.inputActiveBorderColor) layout.modeOptions.forEach { option -> val hovered = option.rect.contains(hoverX, hoverY) val selected = option.mode == state.mode if (hovered || selected) { - out += RenderCommand.DrawRect( - option.rect.x, - option.rect.y, - option.rect.width, - option.rect.height, - if (selected) style.buttonActiveColor else style.buttonHoverColor - ) + out += + RenderCommand.DrawRect( + option.rect.x, + option.rect.y, + option.rect.width, + option.rect.height, + if (selected) style.buttonActiveColor else style.buttonHoverColor, + ) } - out += RenderCommand.DrawText( - text = option.mode.name, - x = option.rect.x + 6, - y = option.rect.y + 2, - color = style.textColor, - fontSize = style.fontSize - ) + out += + RenderCommand.DrawText( + text = option.mode.name, + x = option.rect.x + 6, + y = option.rect.y + 2, + color = style.textColor, + fontSize = style.fontSize, + ) } } - private fun drawButton(rect: Rect, label: String, hovered: Boolean, out: MutableList) { - out += RenderCommand.DrawRect( - rect.x, - rect.y, - rect.width, - rect.height, - if (hovered) style.buttonHoverColor else style.buttonBackgroundColor - ) + private fun drawButton( + rect: Rect, + label: String, + hovered: Boolean, + out: MutableList, + ) { + out += + RenderCommand.DrawRect( + rect.x, + rect.y, + rect.width, + rect.height, + if (hovered) style.buttonHoverColor else style.buttonBackgroundColor, + ) drawBorder(out, rect, style.inputBorderColor) - out += RenderCommand.DrawText( - text = label, - x = rect.x + 4, - y = rect.y + 2, - color = style.textColor, - fontSize = style.fontSize - ) + out += + RenderCommand.DrawText( + text = label, + x = rect.x + 4, + y = rect.y + 2, + color = style.textColor, + fontSize = style.fontSize, + ) } private fun drawSwatch(rect: Rect, color: RgbaColor, out: MutableList) { @@ -1276,13 +1402,14 @@ class ColorPickerController( } private fun drawColorField(rect: Rect, out: MutableList) { - out += RenderCommand.DrawColorField( - x = rect.x, - y = rect.y, - width = rect.width, - height = rect.height, - hueDeg = hueDeg - ) + out += + RenderCommand.DrawColorField( + x = rect.x, + y = rect.y, + width = rect.width, + height = rect.height, + hueDeg = hueDeg, + ) drawBorder(out, rect, style.inputBorderColor) } @@ -1295,12 +1422,13 @@ class ColorPickerController( } private fun drawHueSlider(rect: Rect, out: MutableList) { - out += RenderCommand.DrawHueBar( - x = rect.x, - y = rect.y, - width = rect.width, - height = rect.height - ) + out += + RenderCommand.DrawHueBar( + x = rect.x, + y = rect.y, + width = rect.width, + height = rect.height, + ) drawBorder(out, rect, style.inputBorderColor) } @@ -1312,13 +1440,14 @@ class ColorPickerController( private fun drawAlphaSlider(rect: Rect, out: MutableList) { drawChecker(rect, out) val rgb = state.color.copy(a = 1f) - out += RenderCommand.DrawAlphaBar( - x = rect.x, - y = rect.y, - width = rect.width, - height = rect.height, - rgbColor = rgb.toArgbInt() - ) + out += + RenderCommand.DrawAlphaBar( + x = rect.x, + y = rect.y, + width = rect.width, + height = rect.height, + rgbColor = rgb.toArgbInt(), + ) drawBorder(out, rect, style.inputBorderColor) } @@ -1333,7 +1462,7 @@ class ColorPickerController( columns: Int, rows: Int, cellSize: Int, - color: Int + color: Int, ) { if (rect.width <= 1 || rect.height <= 1) return if (cellSize <= 0) return @@ -1350,17 +1479,19 @@ class ColorPickerController( out += RenderCommand.DrawRect(rect.x, lineY, rect.width, 1, color) } } + private fun drawChecker(rect: Rect, out: MutableList) { if (rect.width <= 0 || rect.height <= 0) return - out += RenderCommand.DrawCheckerboard( - x = rect.x, - y = rect.y, - width = rect.width, - height = rect.height, - cellSize = 4, - lightColor = style.checkerLightColor, - darkColor = style.checkerDarkColor - ) + out += + RenderCommand.DrawCheckerboard( + x = rect.x, + y = rect.y, + width = rect.width, + height = rect.height, + cellSize = 4, + lightColor = style.checkerLightColor, + darkColor = style.checkerDarkColor, + ) } private fun drawBorder(out: MutableList, rect: Rect, color: Int) { @@ -1371,9 +1502,7 @@ class ColorPickerController( out += RenderCommand.DrawRect(rect.x + rect.width - 1, rect.y, 1, rect.height, color) } - private fun caretVisible(nowMs: Long): Boolean { - return ((nowMs / 500L) % 2L) == 0L - } + private fun caretVisible(nowMs: Long): Boolean = ((nowMs / 500L) % 2L) == 0L private fun truncate(value: String, maxChars: Int): String { if (value.length <= maxChars) return value @@ -1403,7 +1532,6 @@ class ColorPickerController( private data class InputDefinition( val key: String, - val label: String + val label: String, ) } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInfrastructure.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInfrastructure.kt index ebd4398..d57ef83 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInfrastructure.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInfrastructure.kt @@ -5,7 +5,13 @@ import org.dreamfinity.dsgl.core.input.ClipboardBridge fun interface ScreenColorSampler { fun sampleColorAt(x: Int, y: Int): Int? - fun sampleArea(x: Int, y: Int, width: Int, height: Int, outArgb: IntArray): Boolean { + fun sampleArea( + x: Int, + y: Int, + width: Int, + height: Int, + outArgb: IntArray, + ): Boolean { if (width <= 0 || height <= 0) return false val required = width * height if (outArgb.size < required) return false @@ -44,9 +50,13 @@ object ScreenColorSamplerBridge { return RgbaColor.fromArgbInt(argb).normalized() } - fun sampleArea(x: Int, y: Int, width: Int, height: Int, outArgb: IntArray): Boolean { - return sampler?.sampleArea(x, y, width, height, outArgb) == true - } + fun sampleArea( + x: Int, + y: Int, + width: Int, + height: Int, + outArgb: IntArray, + ): Boolean = sampler?.sampleArea(x, y, width, height, outArgb) == true } object ColorClipboardSupport { @@ -58,7 +68,7 @@ object ColorClipboardSupport { color: RgbaColor, mode: ColorFormatMode, includeAlpha: Boolean, - rgbOrder: RgbChannelOrder = RgbChannelOrder.RGBA + rgbOrder: RgbChannelOrder = RgbChannelOrder.RGBA, ) { val text = ColorTextCodec.format(color, mode, includeAlpha, rgbOrder) ClipboardBridge.writeText(text) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInteractionSession.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInteractionSession.kt index e8469ea..58d85a3 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInteractionSession.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInteractionSession.kt @@ -4,7 +4,7 @@ internal enum class ColorPickerDragTarget { None, Field, Hue, - Alpha + Alpha, } internal class ColorPickerTextInputSession { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupGeometry.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupGeometry.kt index b04a126..b2bc7db 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupGeometry.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupGeometry.kt @@ -9,7 +9,7 @@ internal data class ColorPickerPopupFrame( val panelRect: Rect, val headerRect: Rect, val bodyRect: Rect, - val closeRect: Rect + val closeRect: Rect, ) internal class ColorPickerPopupPositionStore { @@ -25,9 +25,8 @@ internal class ColorPickerPopupPositionStore { internal object ColorPickerPopupGeometry { private const val MIN_VALID_VIEWPORT_SIZE = 2 - fun hasValidViewport(viewportWidth: Int, viewportHeight: Int): Boolean { - return viewportWidth >= MIN_VALID_VIEWPORT_SIZE && viewportHeight >= MIN_VALID_VIEWPORT_SIZE - } + fun hasValidViewport(viewportWidth: Int, viewportHeight: Int): Boolean = + viewportWidth >= MIN_VALID_VIEWPORT_SIZE && viewportHeight >= MIN_VALID_VIEWPORT_SIZE fun resolvePanelRect( owner: Any, @@ -38,7 +37,7 @@ internal object ColorPickerPopupGeometry { viewportHeight: Int, keepPosition: Boolean, currentRect: Rect?, - store: ColorPickerPopupPositionStore + store: ColorPickerPopupPositionStore, ): Rect { val viewportReady = hasValidViewport(viewportWidth, viewportHeight) if (keepPosition && currentRect != null) { @@ -57,41 +56,44 @@ internal object ColorPickerPopupGeometry { x = anchorRect.x, y = anchorRect.y + anchorRect.height, width = width, - height = height + height = height, ) } - val placement = PopupPlacement.resolve( - PopupPlacementRequest( - preferredRect = Rect( - anchorRect.x, - anchorRect.y + anchorRect.height, - width, - height + val placement = + PopupPlacement.resolve( + PopupPlacementRequest( + preferredRect = + Rect( + anchorRect.x, + anchorRect.y + anchorRect.height, + width, + height, + ), + popupSize = Size(width, height), + viewport = Rect(0, 0, viewportWidth.coerceAtLeast(1), viewportHeight.coerceAtLeast(1)), + padding = 8, + horizontalFlipX = anchorRect.x + anchorRect.width - width, ), - popupSize = Size(width, height), - viewport = Rect(0, 0, viewportWidth.coerceAtLeast(1), viewportHeight.coerceAtLeast(1)), - padding = 8, - horizontalFlipX = anchorRect.x + anchorRect.width - width ) - ) return placement.rect } fun buildFrame(panelRect: Rect, headerHeight: Int, panelPadding: Int): ColorPickerPopupFrame { val headerRect = Rect(panelRect.x, panelRect.y, panelRect.width, headerHeight) - val bodyRect = Rect( - panelRect.x + panelPadding, - panelRect.y + headerHeight + panelPadding, - (panelRect.width - panelPadding * 2).coerceAtLeast(1), - (panelRect.height - headerHeight - panelPadding * 2).coerceAtLeast(1) - ) + val bodyRect = + Rect( + panelRect.x + panelPadding, + panelRect.y + headerHeight + panelPadding, + (panelRect.width - panelPadding * 2).coerceAtLeast(1), + (panelRect.height - headerHeight - panelPadding * 2).coerceAtLeast(1), + ) val closeRect = Rect(panelRect.x + panelRect.width - 20, panelRect.y + 4, 16, 16) return ColorPickerPopupFrame( panelRect = panelRect, headerRect = headerRect, bodyRect = bodyRect, - closeRect = closeRect + closeRect = closeRect, ) } @@ -107,7 +109,7 @@ internal object ColorPickerPopupGeometry { x = rect.x.coerceIn(minX, maxX), y = rect.y.coerceIn(minY, maxY), width = rect.width, - height = rect.height + height = rect.height, ) } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt index 1462f7b..cc4ad57 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt @@ -9,9 +9,13 @@ import org.dreamfinity.dsgl.core.render.RenderCommand interface ColorPickerPopupHost { fun open(request: ColorPickerPopupRequest) + fun close(owner: Any) + fun closeAll() + fun isOpenFor(owner: Any): Boolean + fun isOpen(): Boolean } @@ -28,7 +32,7 @@ data class ColorPickerPopupRequest( val onPreview: ((RgbaColor) -> Unit)? = null, val onChange: ((RgbaColor) -> Unit)? = null, val onCommit: ((RgbaColor) -> Unit)? = null, - val onClose: (() -> Unit)? = null + val onClose: (() -> Unit)? = null, ) class ColorPickerPopupEngine : ColorPickerPopupHost { @@ -42,7 +46,7 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { var closeRect: Rect, var layout: ColorPickerLayout, val dragModel: FloatingPaneDragModel = FloatingPaneDragModel(), - var consumedEyedropperPress: Boolean = false + var consumedEyedropperPress: Boolean = false, ) private var popup: PopupState? = null @@ -60,29 +64,32 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { } current?.let { positionStore.remember(it.owner, it.panelRect) - it.request.onClose?.invoke() + it.request.onClose + ?.invoke() } - val controller = ColorPickerController( - initial = request.state, - style = request.style - ) + val controller = + ColorPickerController( + initial = request.state, + style = request.style, + ) val rememberedPanel = positionStore.remembered(request.owner) val initialX = rememberedPanel?.x ?: request.anchorRect.x val initialY = rememberedPanel?.y ?: request.anchorRect.y val initialRect = Rect(initialX, initialY, request.width.coerceAtLeast(220), 1) val initialBody = Rect(initialRect.x + panelPadding, initialRect.y + headerHeight + panelPadding, 1, 1) val initialLayout = controller.buildLayout(initialBody) - val state = PopupState( - owner = request.owner, - request = request, - controller = controller, - panelRect = initialRect, - headerRect = Rect(initialRect.x, initialRect.y, initialRect.width, headerHeight), - bodyRect = initialBody, - closeRect = Rect(initialRect.x + initialRect.width - 20, initialRect.y + 3, 16, 18), - layout = initialLayout - ) + val state = + PopupState( + owner = request.owner, + request = request, + controller = controller, + panelRect = initialRect, + headerRect = Rect(initialRect.x, initialRect.y, initialRect.width, headerHeight), + bodyRect = initialBody, + closeRect = Rect(initialRect.x + initialRect.width - 20, initialRect.y + 3, 16, 18), + layout = initialLayout, + ) popup = state bindController(state) relayout(state, keepPosition = rememberedPanel != null) @@ -123,7 +130,8 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { if (current.owner != owner) return positionStore.remember(current.owner, current.panelRect) current.dragModel.end() - current.request.onClose?.invoke() + current.request.onClose + ?.invoke() popup = null } @@ -131,7 +139,8 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { val current = popup ?: return positionStore.remember(current.owner, current.panelRect) current.dragModel.end() - current.request.onClose?.invoke() + current.request.onClose + ?.invoke() popup = null } @@ -190,38 +199,27 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { return current.controller } - internal fun debugActiveController(): ColorPickerController? { - return popup?.controller - } + internal fun debugActiveController(): ColorPickerController? = popup?.controller - internal fun debugActiveLayout(): ColorPickerLayout? { - return popup?.layout - } + internal fun debugActiveLayout(): ColorPickerLayout? = popup?.layout - internal fun debugActiveStyle(): ColorPickerStyle? { - return popup?.request?.style - } + internal fun debugActiveStyle(): ColorPickerStyle? = popup?.request?.style - internal fun debugActivePanelRect(): Rect? { - return popup?.panelRect - } + internal fun debugActivePanelRect(): Rect? = popup?.panelRect - internal fun debugActiveOwnerScope(): OverlayOwnerScope? { - return popup?.request?.ownerScope - } + internal fun debugActiveOwnerScope(): OverlayOwnerScope? = popup?.request?.ownerScope - internal fun debugIsDraggingPopup(): Boolean { - return popup?.dragModel?.dragging == true - } + internal fun debugIsDraggingPopup(): Boolean = popup?.dragModel?.dragging == true internal fun forcePanelRect(owner: Any, panelRect: Rect) { val current = popup ?: return if (current.owner != owner) return - val clamped = ColorPickerPopupGeometry.clampPanel( - rect = panelRect, - viewportWidth = viewportWidth, - viewportHeight = viewportHeight - ) + val clamped = + ColorPickerPopupGeometry.clampPanel( + rect = panelRect, + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + ) if (clamped == current.panelRect) return current.panelRect = clamped rebuildRects(current) @@ -238,13 +236,14 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { fun onCursorPosition(mouseX: Int, mouseY: Int) { val current = popup ?: return if (current.dragModel.dragging) { - val clamped = current.dragModel.update( - mouseX = mouseX, - mouseY = mouseY, - viewportWidth = viewportWidth, - viewportHeight = viewportHeight, - clamp = ColorPickerPopupGeometry::clampPanel - ) + val clamped = + current.dragModel.update( + mouseX = mouseX, + mouseY = mouseY, + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + clamp = ColorPickerPopupGeometry::clampPanel, + ) if (clamped != current.panelRect) { current.panelRect = clamped rebuildRects(current) @@ -259,57 +258,79 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { popup?.controller?.sampleEyedropperAtHover() } - fun hasActiveEyedropper(): Boolean { - return popup?.controller?.isEyedropperActive() == true - } + fun hasActiveEyedropper(): Boolean = popup?.controller?.isEyedropperActive() == true fun appendOverlayCommands(out: MutableList) { val current = popup ?: return refreshLayout(current) val panel = current.panelRect out += RenderCommand.PushClip(0, 0, viewportWidth.coerceAtLeast(1), viewportHeight.coerceAtLeast(1)) - out += RenderCommand.DrawRect(panel.x + 2, panel.y + 2, panel.width, panel.height, current.request.style.panelShadowColor) - out += RenderCommand.DrawRect(panel.x, panel.y, panel.width, panel.height, current.request.style.panelBackgroundColor) + out += + RenderCommand.DrawRect( + panel.x + 2, + panel.y + 2, + panel.width, + panel.height, + current.request.style.panelShadowColor, + ) + out += + RenderCommand.DrawRect( + panel.x, + panel.y, + panel.width, + panel.height, + current.request.style.panelBackgroundColor, + ) drawBorder(out, panel, current.request.style.panelBorderColor) - out += RenderCommand.DrawRect( - current.headerRect.x, - current.headerRect.y, - current.headerRect.width, - current.headerRect.height, - current.request.style.buttonBackgroundColor - ) + out += + RenderCommand.DrawRect( + current.headerRect.x, + current.headerRect.y, + current.headerRect.width, + current.headerRect.height, + current.request.style.buttonBackgroundColor, + ) drawBorder(out, current.headerRect, current.request.style.inputBorderColor) - out += RenderCommand.DrawText( - text = current.request.title, - x = current.headerRect.x + 6, - y = current.headerRect.y + 3, - color = current.request.style.textColor, - fontSize = current.request.style.fontSize - ) - out += RenderCommand.DrawRect( - current.closeRect.x, - current.closeRect.y, - current.closeRect.width, - current.closeRect.height, - current.request.style.buttonBackgroundColor - ) + out += + RenderCommand.DrawText( + text = current.request.title, + x = current.headerRect.x + 6, + y = current.headerRect.y + 3, + color = current.request.style.textColor, + fontSize = current.request.style.fontSize, + ) + out += + RenderCommand.DrawRect( + current.closeRect.x, + current.closeRect.y, + current.closeRect.width, + current.closeRect.height, + current.request.style.buttonBackgroundColor, + ) drawBorder(out, current.closeRect, current.request.style.inputBorderColor) - out += RenderCommand.DrawText( - text = "x", - x = current.closeRect.x + 5, - y = current.closeRect.y + 2, - color = current.request.style.textColor, - fontSize = current.request.style.fontSize - ) + out += + RenderCommand.DrawText( + text = "x", + x = current.closeRect.x + 5, + y = current.closeRect.y + 2, + color = current.request.style.textColor, + fontSize = current.request.style.fontSize, + ) - out += RenderCommand.PushClip(current.bodyRect.x, current.bodyRect.y, current.bodyRect.width, current.bodyRect.height) + out += + RenderCommand.PushClip( + current.bodyRect.x, + current.bodyRect.y, + current.bodyRect.width, + current.bodyRect.height, + ) appendOverlayBodyCommands(out) out += RenderCommand.PopClip appendEyedropperOverlayCommands( viewportWidth = viewportWidth.coerceAtLeast(1), viewportHeight = viewportHeight.coerceAtLeast(1), - out = out + out = out, ) out += RenderCommand.PopClip } @@ -322,13 +343,13 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { internal fun appendEyedropperOverlayCommands( viewportWidth: Int = this.viewportWidth.coerceAtLeast(1), viewportHeight: Int = this.viewportHeight.coerceAtLeast(1), - out: MutableList + out: MutableList, ) { val current = popup ?: return current.controller.appendEyedropperOverlay( viewportWidth = viewportWidth.coerceAtLeast(1), viewportHeight = viewportHeight.coerceAtLeast(1), - out = out + out = out, ) } @@ -376,17 +397,16 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { if (current.request.ownerScope != OverlayOwnerScope.System) return false if (button != MouseButton.LEFT) return false if (current.controller.isEyedropperActive()) return false - return current.layout.inputSlots.any { slot -> slot.inputRect.contains(mouseX, mouseY) } + return current.layout.inputSlots + .any { slot -> slot.inputRect.contains(mouseX, mouseY) } } - fun focusSystemInputSlotForDomEditing( - mouseX: Int, - mouseY: Int, - focusInputByIndex: (Int) -> Boolean - ): Boolean { + fun focusSystemInputSlotForDomEditing(mouseX: Int, mouseY: Int, focusInputByIndex: (Int) -> Boolean): Boolean { val current = popup ?: return false if (current.request.ownerScope != OverlayOwnerScope.System) return false - val slotIndex = current.layout.inputSlots.indexOfFirst { slot -> slot.inputRect.contains(mouseX, mouseY) } + val slotIndex = + current.layout.inputSlots + .indexOfFirst { slot -> slot.inputRect.contains(mouseX, mouseY) } if (slotIndex < 0) return false val slot = current.layout.inputSlots[slotIndex] current.controller.handleDomInputFocused(slot.key) @@ -428,7 +448,8 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { state.controller.onPreview = state.request.onPreview state.controller.onChange = state.request.onChange state.controller.onCommit = { color -> - state.request.onCommit?.invoke(color) + state.request.onCommit + ?.invoke(color) if (state.request.state.closeOnSelect) { close(state.owner) } @@ -440,29 +461,33 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { } private fun relayout(state: PopupState, keepPosition: Boolean) { - val width = state.request.width.coerceAtLeast(state.request.style.minWidth) + val width = + state.request.width + .coerceAtLeast(state.request.style.minWidth) val bodyHeight = state.controller.preferredHeight(state.request.state.alphaEnabled) val height = headerHeight + panelPadding + bodyHeight + panelPadding - state.panelRect = ColorPickerPopupGeometry.resolvePanelRect( - owner = state.owner, - anchorRect = state.request.anchorRect, - width = width, - height = height, - viewportWidth = viewportWidth, - viewportHeight = viewportHeight, - keepPosition = keepPosition, - currentRect = state.panelRect, - store = positionStore - ) + state.panelRect = + ColorPickerPopupGeometry.resolvePanelRect( + owner = state.owner, + anchorRect = state.request.anchorRect, + width = width, + height = height, + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + keepPosition = keepPosition, + currentRect = state.panelRect, + store = positionStore, + ) rebuildRects(state) } private fun rebuildRects(state: PopupState) { - val frame = ColorPickerPopupGeometry.buildFrame( - panelRect = state.panelRect, - headerHeight = headerHeight, - panelPadding = panelPadding - ) + val frame = + ColorPickerPopupGeometry.buildFrame( + panelRect = state.panelRect, + headerHeight = headerHeight, + panelPadding = panelPadding, + ) state.panelRect = frame.panelRect state.headerRect = frame.headerRect state.bodyRect = frame.bodyRect @@ -482,19 +507,18 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { out += RenderCommand.DrawRect(rect.x + rect.width - 1, rect.y, 1, rect.height, color) } - private fun sameStateContract(a: ColorPickerState, b: ColorPickerState): Boolean { - return a.color.toArgbInt() == b.color.toArgbInt() && - a.previous.toArgbInt() == b.previous.toArgbInt() && - a.mode == b.mode && - a.rgbOrder == b.rgbOrder && - a.alphaEnabled == b.alphaEnabled && - a.closeOnSelect == b.closeOnSelect - } + private fun sameStateContract(a: ColorPickerState, b: ColorPickerState): Boolean = + a.color.toArgbInt() == b.color.toArgbInt() && + a.previous.toArgbInt() == b.previous.toArgbInt() && + a.mode == b.mode && + a.rgbOrder == b.rgbOrder && + a.alphaEnabled == b.alphaEnabled && + a.closeOnSelect == b.closeOnSelect } class ColorPickerPopupManager( private val host: ColorPickerPopupHost = ColorPickerRuntime.host, - private val ownerToken: Any = Any() + private val ownerToken: Any = Any(), ) { fun open( ownerScope: OverlayOwnerScope = OverlayOwnerScope.Application, @@ -508,7 +532,7 @@ class ColorPickerPopupManager( onPreview: ((RgbaColor) -> Unit)? = null, onChange: ((RgbaColor) -> Unit)? = null, onCommit: ((RgbaColor) -> Unit)? = null, - onClose: (() -> Unit)? = null + onClose: (() -> Unit)? = null, ) { host.open( ColorPickerPopupRequest( @@ -524,8 +548,8 @@ class ColorPickerPopupManager( onPreview = onPreview, onChange = onChange, onCommit = onCommit, - onClose = onClose - ) + onClose = onClose, + ), ) } @@ -540,4 +564,3 @@ object ColorPickerRuntime { val engine: ColorPickerPopupEngine = ColorPickerPopupEngine() val host: ColorPickerPopupHost = engine } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerState.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerState.kt index 5a21647..5a009a5 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerState.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerState.kt @@ -6,21 +6,21 @@ data class ColorPickerState( val mode: ColorFormatMode = ColorFormatMode.HEX, val rgbOrder: RgbChannelOrder = RgbChannelOrder.RGBA, val alphaEnabled: Boolean = true, - val closeOnSelect: Boolean = true + val closeOnSelect: Boolean = true, ) { constructor( color: RgbaColor, previous: RgbaColor = color, mode: ColorFormatMode = ColorFormatMode.HEX, alphaEnabled: Boolean = true, - closeOnSelect: Boolean = true + closeOnSelect: Boolean = true, ) : this( color = color, previous = previous, mode = mode, rgbOrder = RgbChannelOrder.RGBA, alphaEnabled = alphaEnabled, - closeOnSelect = closeOnSelect + closeOnSelect = closeOnSelect, ) fun withColor(next: RgbaColor): ColorPickerState { @@ -29,11 +29,7 @@ data class ColorPickerState( return copy(color = merged) } - fun withCommittedCurrent(): ColorPickerState { - return copy(previous = color) - } + fun withCommittedCurrent(): ColorPickerState = copy(previous = color) - fun withRestoredPrevious(): ColorPickerState { - return copy(color = previous) - } + fun withRestoredPrevious(): ColorPickerState = copy(color = previous) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerStyle.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerStyle.kt index b1ae63e..74da8ec 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerStyle.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerStyle.kt @@ -48,6 +48,5 @@ data class ColorPickerStyle( val recentCellGap: Int = 2, val minWidth: Int = 280, val eyedropperGridOverlayEnabled: Boolean = true, - val eyedropperGridOverlayColor: Int = 0x22FFFFff.toInt() + val eyedropperGridOverlayColor: Int = 0x22FFFFff.toInt(), ) - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorRecentHistory.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorRecentHistory.kt index d82fa4e..db209b8 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorRecentHistory.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorRecentHistory.kt @@ -1,7 +1,7 @@ package org.dreamfinity.dsgl.core.colorpicker class ColorRecentHistory( - val capacity: Int = 64 + val capacity: Int = 64, ) { private val colors: MutableList = ArrayList(capacity.coerceAtLeast(1)) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorTextCodec.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorTextCodec.kt index 1c1286c..6ba4791 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorTextCodec.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorTextCodec.kt @@ -6,26 +6,21 @@ import kotlin.math.roundToInt data class ParsedColorText( val color: RgbaColor, val detectedMode: ColorFormatMode, - val detectedRgbOrder: RgbChannelOrder? = null + val detectedRgbOrder: RgbChannelOrder? = null, ) { constructor( color: RgbaColor, - detectedMode: ColorFormatMode + detectedMode: ColorFormatMode, ) : this( color = color, detectedMode = detectedMode, - detectedRgbOrder = null + detectedRgbOrder = null, ) } object ColorTextCodec { - fun format( - color: RgbaColor, - mode: ColorFormatMode, - includeAlpha: Boolean - ): String { - return format(color, mode, includeAlpha, RgbChannelOrder.RGBA) - } + fun format(color: RgbaColor, mode: ColorFormatMode, includeAlpha: Boolean): String = + format(color, mode, includeAlpha, RgbChannelOrder.RGBA) fun parse(raw: String): ParsedColorText? { val text = raw.trim() @@ -41,7 +36,7 @@ object ColorTextCodec { color: RgbaColor, mode: ColorFormatMode, includeAlpha: Boolean, - rgbOrder: RgbChannelOrder = RgbChannelOrder.RGBA + rgbOrder: RgbChannelOrder = RgbChannelOrder.RGBA, ): String { val normalized = color.normalized() return when (mode) { @@ -65,11 +60,7 @@ object ColorTextCodec { } } - fun formatRgb( - color: RgbaColor, - includeAlpha: Boolean, - rgbOrder: RgbChannelOrder = RgbChannelOrder.RGBA - ): String { + fun formatRgb(color: RgbaColor, includeAlpha: Boolean, rgbOrder: RgbChannelOrder = RgbChannelOrder.RGBA): String { val c = color.normalized() val a = formatFraction(c.a) val r = (c.r * 255f + 0.5f).toInt().coerceIn(0, 255) @@ -85,54 +76,52 @@ object ColorTextCodec { } } - fun formatRgb(color: RgbaColor, includeAlpha: Boolean): String { - return formatRgb(color, includeAlpha, RgbChannelOrder.RGBA) - } + fun formatRgb(color: RgbaColor, includeAlpha: Boolean): String = + formatRgb(color, includeAlpha, RgbChannelOrder.RGBA) - fun formatHsl( - color: RgbaColor, - includeAlpha: Boolean, - fallbackHueDeg: Float = 0f - ): String { + fun formatHsl(color: RgbaColor, includeAlpha: Boolean, fallbackHueDeg: Float = 0f): String { val c = color.normalized() val hsl = ColorConversions.rgbToHsl(c, fallbackHueDeg) - val h = hsl.hueDeg.roundToInt().coerceIn(0, 360) + val h = + hsl.hueDeg + .roundToInt() + .coerceIn(0, 360) val s = (hsl.saturation * 100f).roundToInt().coerceIn(0, 100) val l = (hsl.lightness * 100f).roundToInt().coerceIn(0, 100) return if (includeAlpha) { - "hsla($h, ${s}%, ${l}%, ${formatFraction(c.a)})" + "hsla($h, $s%, $l%, ${formatFraction(c.a)})" } else { - "hsl($h, ${s}%, ${l}%)" + "hsl($h, $s%, $l%)" } } - fun formatHsb( - color: RgbaColor, - includeAlpha: Boolean, - fallbackHueDeg: Float = 0f - ): String { + fun formatHsb(color: RgbaColor, includeAlpha: Boolean, fallbackHueDeg: Float = 0f): String { val c = color.normalized() val hsb = ColorConversions.rgbToHsv(c, fallbackHueDeg) - val h = hsb.hueDeg.roundToInt().coerceIn(0, 360) + val h = + hsb.hueDeg + .roundToInt() + .coerceIn(0, 360) val s = (hsb.saturation * 100f).roundToInt().coerceIn(0, 100) val b = (hsb.brightness * 100f).roundToInt().coerceIn(0, 100) return if (includeAlpha) { - "hsba($h, ${s}%, ${b}%, ${formatFraction(c.a)})" + "hsba($h, $s%, $b%, ${formatFraction(c.a)})" } else { - "hsb($h, ${s}%, ${b}%)" + "hsb($h, $s%, $b%)" } } private fun parseHex(raw: String): RgbaColor? { if (!raw.startsWith("#")) return null val hex = raw.substring(1) - val expanded = when (hex.length) { - 3 -> "${hex[0]}${hex[0]}${hex[1]}${hex[1]}${hex[2]}${hex[2]}FF" - 4 -> "${hex[0]}${hex[0]}${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}" - 6 -> "${hex}FF" - 8 -> hex - else -> return null - } + val expanded = + when (hex.length) { + 3 -> "${hex[0]}${hex[0]}${hex[1]}${hex[1]}${hex[2]}${hex[2]}FF" + 4 -> "${hex[0]}${hex[0]}${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}" + 6 -> "${hex}FF" + 8 -> hex + else -> return null + } val value = expanded.toLongOrNull(16) ?: return null val r = ((value ushr 24) and 0xFF).toInt() / 255f val g = ((value ushr 16) and 0xFF).toInt() / 255f @@ -142,42 +131,45 @@ object ColorTextCodec { } private fun parseRgbLike(raw: String): ParsedColorText? { - val prefix = when { - raw.startsWith("rgba(", ignoreCase = true) -> "rgba" - raw.startsWith("argb(", ignoreCase = true) -> "argb" - raw.startsWith("rgb(", ignoreCase = true) -> "rgb" - else -> return null - } + val prefix = + when { + raw.startsWith("rgba(", ignoreCase = true) -> "rgba" + raw.startsWith("argb(", ignoreCase = true) -> "argb" + raw.startsWith("rgb(", ignoreCase = true) -> "rgb" + else -> return null + } val values = parseFunctionArgs(raw, prefix) ?: return null if (prefix == "rgb" && values.size != 3) return null if ((prefix == "rgba" || prefix == "argb") && values.size != 4) return null if (values.size !in 3..4) return null - val (r, g, b, a, order) = if (prefix == "argb") { - val a = parseAlphaComponent(values[0]) ?: return null - val r = parseRgbComponent(values[1]) ?: return null - val g = parseRgbComponent(values[2]) ?: return null - val b = parseRgbComponent(values[3]) ?: return null - RgbParseResult(r, g, b, a, RgbChannelOrder.ARGB) - } else { - val r = parseRgbComponent(values[0]) ?: return null - val g = parseRgbComponent(values[1]) ?: return null - val b = parseRgbComponent(values[2]) ?: return null - val a = if (values.size >= 4) parseAlphaComponent(values[3]) ?: return null else 1f - RgbParseResult(r, g, b, a, if (values.size == 4) RgbChannelOrder.RGBA else null) - } + val (r, g, b, a, order) = + if (prefix == "argb") { + val a = parseAlphaComponent(values[0]) ?: return null + val r = parseRgbComponent(values[1]) ?: return null + val g = parseRgbComponent(values[2]) ?: return null + val b = parseRgbComponent(values[3]) ?: return null + RgbParseResult(r, g, b, a, RgbChannelOrder.ARGB) + } else { + val r = parseRgbComponent(values[0]) ?: return null + val g = parseRgbComponent(values[1]) ?: return null + val b = parseRgbComponent(values[2]) ?: return null + val a = if (values.size >= 4) parseAlphaComponent(values[3]) ?: return null else 1f + RgbParseResult(r, g, b, a, if (values.size == 4) RgbChannelOrder.RGBA else null) + } return ParsedColorText( color = RgbaColor(r, g, b, a).normalized(), detectedMode = ColorFormatMode.RGB, - detectedRgbOrder = order + detectedRgbOrder = order, ) } private fun parseHslLike(raw: String): RgbaColor? { - val prefix = when { - raw.startsWith("hsla(", ignoreCase = true) -> "hsla" - raw.startsWith("hsl(", ignoreCase = true) -> "hsl" - else -> return null - } + val prefix = + when { + raw.startsWith("hsla(", ignoreCase = true) -> "hsla" + raw.startsWith("hsl(", ignoreCase = true) -> "hsl" + else -> return null + } val values = parseFunctionArgs(raw, prefix) ?: return null if (prefix == "hsl" && values.size != 3) return null if (prefix == "hsla" && values.size != 4) return null @@ -187,17 +179,18 @@ object ColorTextCodec { val a = if (values.size >= 4) parseAlphaComponent(values[3]) ?: return null else 1f return ColorConversions.hslToRgb( hsl = HslColor(h, s, l), - alpha = a + alpha = a, ) } private fun parseHsbLike(raw: String): RgbaColor? { - val prefix = when { - raw.startsWith("hsba(", ignoreCase = true) -> "hsba" - raw.startsWith("hsv(", ignoreCase = true) -> "hsv" - raw.startsWith("hsb(", ignoreCase = true) -> "hsb" - else -> return null - } + val prefix = + when { + raw.startsWith("hsba(", ignoreCase = true) -> "hsba" + raw.startsWith("hsv(", ignoreCase = true) -> "hsv" + raw.startsWith("hsb(", ignoreCase = true) -> "hsb" + else -> return null + } val values = parseFunctionArgs(raw, prefix) ?: return null if ((prefix == "hsb" || prefix == "hsv") && values.size != 3) return null if (prefix == "hsba" && values.size != 4) return null @@ -207,7 +200,7 @@ object ColorTextCodec { val a = if (values.size >= 4) parseAlphaComponent(values[3]) ?: return null else 1f return ColorConversions.hsvToRgb( hsv = HsvColor(h, s, b), - alpha = a + alpha = a, ) } @@ -215,7 +208,11 @@ object ColorTextCodec { if (!raw.endsWith(")")) return null val head = raw.indexOf('(') if (head < 0) return null - val name = raw.substring(0, head).trim().lowercase() + val name = + raw + .substring(0, head) + .trim() + .lowercase() if (name != prefix) return null val body = raw.substring(head + 1, raw.length - 1) if (body.isBlank()) return emptyList() @@ -254,7 +251,11 @@ object ColorTextCodec { } private fun parseHue(raw: String): Float? { - val normalized = raw.trim().removeSuffix("deg").trim() + val normalized = + raw + .trim() + .removeSuffix("deg") + .trim() val value = normalized.toFloatOrNull() ?: return null var hue = value % 360f if (hue < 0f) hue += 360f @@ -294,6 +295,6 @@ object ColorTextCodec { val g: Float, val b: Float, val a: Float, - val order: RgbChannelOrder? + val order: RgbChannelOrder?, ) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorTypes.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorTypes.kt index 53fe0ad..8b42af6 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorTypes.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorTypes.kt @@ -6,28 +6,27 @@ enum class ColorFormatMode { HEX, RGB, HSL, - HSB + HSB, } enum class RgbChannelOrder { RGBA, - ARGB + ARGB, } data class RgbaColor( val r: Float, val g: Float, val b: Float, - val a: Float = 1f + val a: Float = 1f, ) { - fun normalized(): RgbaColor { - return RgbaColor( + fun normalized(): RgbaColor = + RgbaColor( r = r.coerceIn(0f, 1f), g = g.coerceIn(0f, 1f), b = b.coerceIn(0f, 1f), - a = a.coerceIn(0f, 1f) + a = a.coerceIn(0f, 1f), ) - } fun toArgbInt(): Int { val c = normalized() @@ -54,7 +53,7 @@ data class RgbaColor( data class HsvColor( val hueDeg: Float, val saturation: Float, - val brightness: Float + val brightness: Float, ) { fun normalized(): HsvColor { var hue = hueDeg % 360f @@ -62,7 +61,7 @@ data class HsvColor( return HsvColor( hueDeg = hue, saturation = saturation.coerceIn(0f, 1f), - brightness = brightness.coerceIn(0f, 1f) + brightness = brightness.coerceIn(0f, 1f), ) } } @@ -70,7 +69,7 @@ data class HsvColor( data class HslColor( val hueDeg: Float, val saturation: Float, - val lightness: Float + val lightness: Float, ) { fun normalized(): HslColor { var hue = hueDeg % 360f @@ -78,7 +77,7 @@ data class HslColor( return HslColor( hueDeg = hue, saturation = saturation.coerceIn(0f, 1f), - lightness = lightness.coerceIn(0f, 1f) + lightness = lightness.coerceIn(0f, 1f), ) } } @@ -91,12 +90,13 @@ object ColorConversions { val delta = max - min val brightness = max val saturation = if (max <= 0f) 0f else delta / max - val hue = when { - delta <= 1e-6f -> normalizeHue(fallbackHueDeg) - abs(max - c.r) < 1e-6f -> normalizeHue((60f * ((c.g - c.b) / delta) + 360f) % 360f) - abs(max - c.g) < 1e-6f -> normalizeHue(60f * ((c.b - c.r) / delta) + 120f) - else -> normalizeHue(60f * ((c.r - c.g) / delta) + 240f) - } + val hue = + when { + delta <= 1e-6f -> normalizeHue(fallbackHueDeg) + abs(max - c.r) < 1e-6f -> normalizeHue((60f * ((c.g - c.b) / delta) + 360f) % 360f) + abs(max - c.g) < 1e-6f -> normalizeHue(60f * ((c.b - c.r) / delta) + 120f) + else -> normalizeHue(60f * ((c.r - c.g) / delta) + 240f) + } return HsvColor(hue, saturation, brightness).normalized() } @@ -106,19 +106,20 @@ object ColorConversions { val c = n.brightness * n.saturation val x = c * (1f - abs(h % 2f - 1f)) val m = n.brightness - c - val (r1, g1, b1) = when { - h < 1f -> Triple(c, x, 0f) - h < 2f -> Triple(x, c, 0f) - h < 3f -> Triple(0f, c, x) - h < 4f -> Triple(0f, x, c) - h < 5f -> Triple(x, 0f, c) - else -> Triple(c, 0f, x) - } + val (r1, g1, b1) = + when { + h < 1f -> Triple(c, x, 0f) + h < 2f -> Triple(x, c, 0f) + h < 3f -> Triple(0f, c, x) + h < 4f -> Triple(0f, x, c) + h < 5f -> Triple(x, 0f, c) + else -> Triple(c, 0f, x) + } return RgbaColor( r = r1 + m, g = g1 + m, b = b1 + m, - a = alpha.coerceIn(0f, 1f) + a = alpha.coerceIn(0f, 1f), ).normalized() } @@ -128,17 +129,19 @@ object ColorConversions { val min = minOf(c.r, minOf(c.g, c.b)) val delta = max - min val lightness = (max + min) * 0.5f - val saturation = if (delta <= 1e-6f) { - 0f - } else { - delta / (1f - abs(2f * lightness - 1f)) - } - val hue = when { - delta <= 1e-6f -> normalizeHue(fallbackHueDeg) - abs(max - c.r) < 1e-6f -> normalizeHue(60f * (((c.g - c.b) / delta) % 6f)) - abs(max - c.g) < 1e-6f -> normalizeHue(60f * (((c.b - c.r) / delta) + 2f)) - else -> normalizeHue(60f * (((c.r - c.g) / delta) + 4f)) - } + val saturation = + if (delta <= 1e-6f) { + 0f + } else { + delta / (1f - abs(2f * lightness - 1f)) + } + val hue = + when { + delta <= 1e-6f -> normalizeHue(fallbackHueDeg) + abs(max - c.r) < 1e-6f -> normalizeHue(60f * (((c.g - c.b) / delta) % 6f)) + abs(max - c.g) < 1e-6f -> normalizeHue(60f * (((c.b - c.r) / delta) + 2f)) + else -> normalizeHue(60f * (((c.r - c.g) / delta) + 4f)) + } return HslColor(hue, saturation.coerceIn(0f, 1f), lightness.coerceIn(0f, 1f)) } @@ -148,14 +151,15 @@ object ColorConversions { val h = n.hueDeg / 60f val x = c * (1f - abs(h % 2f - 1f)) val m = n.lightness - c * 0.5f - val (r1, g1, b1) = when { - h < 1f -> Triple(c, x, 0f) - h < 2f -> Triple(x, c, 0f) - h < 3f -> Triple(0f, c, x) - h < 4f -> Triple(0f, x, c) - h < 5f -> Triple(x, 0f, c) - else -> Triple(c, 0f, x) - } + val (r1, g1, b1) = + when { + h < 1f -> Triple(c, x, 0f) + h < 2f -> Triple(x, c, 0f) + h < 3f -> Triple(0f, c, x) + h < 4f -> Triple(0f, x, c) + h < 5f -> Triple(x, 0f, c) + else -> Triple(c, 0f, x) + } return RgbaColor(r1 + m, g1 + m, b1 + m, alpha.coerceIn(0f, 1f)).normalized() } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerCustomSurfaceNodes.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerCustomSurfaceNodes.kt index 8fc97b0..7fd0512 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerCustomSurfaceNodes.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerCustomSurfaceNodes.kt @@ -10,7 +10,7 @@ import org.dreamfinity.dsgl.core.style.Display import kotlin.math.roundToInt internal class EyedropperCaptureNode( - key: Any? + key: Any?, ) : DOMNode(key) { override val styleType: String = "dsgl-system-color-picker-eyedropper-capture" @@ -25,11 +25,16 @@ internal class EyedropperCaptureNode( this.fallbackColor = fallbackColor } - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } + override fun measure(ctx: UiMeasureContext): Size = + Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) } @@ -37,18 +42,19 @@ internal class EyedropperCaptureNode( if (display == Display.None) return val source = sourceRect ?: return if (source.width <= 0 || source.height <= 0) return - out += RenderCommand.CaptureScreenRegion( - sourceX = source.x, - sourceY = source.y, - sourceWidth = source.width, - sourceHeight = source.height, - fallbackColor = fallbackColor - ) + out += + RenderCommand.CaptureScreenRegion( + sourceX = source.x, + sourceY = source.y, + sourceWidth = source.width, + sourceHeight = source.height, + fallbackColor = fallbackColor, + ) } } internal class EyedropperMagnifierDrawNode( - key: Any? + key: Any?, ) : DOMNode(key) { override val styleType: String = "dsgl-system-color-picker-eyedropper-magnifier" @@ -58,7 +64,13 @@ internal class EyedropperMagnifierDrawNode( private var gridEnabled: Boolean = true private var gridColor: Int = 0x66FFFFFF - fun bind(columns: Int, rows: Int, magnification: Int, gridEnabled: Boolean, gridColor: Int) { + fun bind( + columns: Int, + rows: Int, + magnification: Int, + gridEnabled: Boolean, + gridColor: Int, + ) { val nextColumns = columns.coerceAtLeast(1) val nextRows = rows.coerceAtLeast(1) val nextMagnification = magnification.coerceAtLeast(1) @@ -77,38 +89,45 @@ internal class EyedropperMagnifierDrawNode( this.gridColor = gridColor } - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } + override fun measure(ctx: UiMeasureContext): Size = + Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) } override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) { if (display == Display.None) return if (bounds.width <= 0 || bounds.height <= 0) return - out += RenderCommand.DrawCapturedScreenRegion( - x = bounds.x, - y = bounds.y, - width = bounds.width, - height = bounds.height, - gridOverlay = if (gridEnabled) { - RenderCommand.CapturedGridOverlay( - columns = columns, - rows = rows, - magnification = magnification, - color = gridColor - ) - } else { - null - } - ) + out += + RenderCommand.DrawCapturedScreenRegion( + x = bounds.x, + y = bounds.y, + width = bounds.width, + height = bounds.height, + gridOverlay = + if (gridEnabled) { + RenderCommand.CapturedGridOverlay( + columns = columns, + rows = rows, + magnification = magnification, + color = gridColor, + ) + } else { + null + }, + ) } } internal class ColorFieldSurfaceNode( - key: Any? + key: Any?, ) : DOMNode(key) { override val styleType: String = "dsgl-system-color-picker-color-field" @@ -149,23 +168,29 @@ internal class ColorFieldSurfaceNode( brightness = typedTemplate.brightness } - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } + override fun measure(ctx: UiMeasureContext): Size = + Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) } override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) { if (bounds.width <= 0 || bounds.height <= 0) return - out += RenderCommand.DrawColorField( - x = bounds.x, - y = bounds.y, - width = bounds.width, - height = bounds.height, - hueDeg = hueDeg - ) + out += + RenderCommand.DrawColorField( + x = bounds.x, + y = bounds.y, + width = bounds.width, + height = bounds.height, + hueDeg = hueDeg, + ) drawBorder(out, bounds, style.inputBorderColor) val thumbX = bounds.x + (saturation * bounds.width.toFloat()).roundToInt().coerceIn(0, bounds.width - 1) val thumbY = @@ -176,7 +201,7 @@ internal class ColorFieldSurfaceNode( } internal class HueSurfaceNode( - key: Any? + key: Any?, ) : DOMNode(key) { override val styleType: String = "dsgl-system-color-picker-hue-slider" @@ -200,22 +225,28 @@ internal class HueSurfaceNode( hueDeg = typedTemplate.hueDeg } - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } + override fun measure(ctx: UiMeasureContext): Size = + Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) } override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) { if (bounds.width <= 0 || bounds.height <= 0) return - out += RenderCommand.DrawHueBar( - x = bounds.x, - y = bounds.y, - width = bounds.width, - height = bounds.height - ) + out += + RenderCommand.DrawHueBar( + x = bounds.x, + y = bounds.y, + width = bounds.width, + height = bounds.height, + ) drawBorder(out, bounds, style.inputBorderColor) val thumbX = bounds.x + ((hueDeg / 360f) * bounds.width.toFloat()).roundToInt().coerceIn(0, bounds.width - 1) out += RenderCommand.DrawRect(thumbX - 1, bounds.y - 1, 3, bounds.height + 2, style.thumbOutlineColor) @@ -223,7 +254,7 @@ internal class HueSurfaceNode( } internal class AlphaSurfaceNode( - key: Any? + key: Any?, ) : DOMNode(key) { override val styleType: String = "dsgl-system-color-picker-alpha-slider" @@ -247,24 +278,30 @@ internal class AlphaSurfaceNode( color = typedTemplate.color } - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } + override fun measure(ctx: UiMeasureContext): Size = + Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) } override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) { if (bounds.width <= 0 || bounds.height <= 0) return drawChecker(out, bounds, style) - out += RenderCommand.DrawAlphaBar( - x = bounds.x, - y = bounds.y, - width = bounds.width, - height = bounds.height, - rgbColor = color.copy(a = 1f).toArgbInt() - ) + out += + RenderCommand.DrawAlphaBar( + x = bounds.x, + y = bounds.y, + width = bounds.width, + height = bounds.height, + rgbColor = color.copy(a = 1f).toArgbInt(), + ) drawBorder(out, bounds, style.inputBorderColor) val thumbX = bounds.x + (color.a * bounds.width.toFloat()).roundToInt().coerceIn(0, bounds.width - 1) out += RenderCommand.DrawRect(thumbX - 1, bounds.y - 1, 3, bounds.height + 2, style.thumbOutlineColor) @@ -273,7 +310,7 @@ internal class AlphaSurfaceNode( internal class ColorSwatchSurfaceNode( private val allowEmpty: Boolean = false, - key: Any? + key: Any?, ) : DOMNode(key) { override val styleType: String = "dsgl-system-color-picker-swatch" @@ -303,11 +340,16 @@ internal class ColorSwatchSurfaceNode( highlighted = typedTemplate.highlighted } - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } + override fun measure(ctx: UiMeasureContext): Size = + Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) } @@ -320,28 +362,30 @@ internal class ColorSwatchSurfaceNode( return } drawChecker(out, bounds, style) - out += RenderCommand.DrawRect( - bounds.x, - bounds.y, - bounds.width, - bounds.height, - (localColor ?: RgbaColor.WHITE).toArgbInt() - ) + out += + RenderCommand.DrawRect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + (localColor ?: RgbaColor.WHITE).toArgbInt(), + ) drawBorder(out, bounds, if (highlighted) style.inputActiveBorderColor else style.inputBorderColor) } } private fun drawChecker(out: MutableList, rect: Rect, style: ColorPickerStyle) { if (rect.width <= 0 || rect.height <= 0) return - out += RenderCommand.DrawCheckerboard( - x = rect.x, - y = rect.y, - width = rect.width, - height = rect.height, - cellSize = 4, - lightColor = style.checkerLightColor, - darkColor = style.checkerDarkColor - ) + out += + RenderCommand.DrawCheckerboard( + x = rect.x, + y = rect.y, + width = rect.width, + height = rect.height, + cellSize = 4, + lightColor = style.checkerLightColor, + darkColor = style.checkerDarkColor, + ) } private fun drawBorder(out: MutableList, rect: Rect, color: Int) { @@ -351,4 +395,3 @@ private fun drawBorder(out: MutableList, rect: Rect, color: Int) out += RenderCommand.DrawRect(rect.x, rect.y, 1, rect.height, color) out += RenderCommand.DrawRect(rect.x + rect.width - 1, rect.y, 1, rect.height, color) } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerOverlayNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerOverlayNode.kt index 36ddc27..c4d4d51 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerOverlayNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerOverlayNode.kt @@ -12,7 +12,7 @@ import org.dreamfinity.dsgl.core.style.Display internal class SystemColorPickerOverlayNode( private val popupEngine: ColorPickerPopupEngine, private val overlayPanel: OverlayPanel, - key: Any? = "dsgl-system-color-picker" + key: Any? = "dsgl-system-color-picker", ) : DOMNode(key) { override val styleType: String = "dsgl-system-color-picker" @@ -28,19 +28,22 @@ internal class SystemColorPickerOverlayNode( cursorY = mouseY } - fun focusInputSlot(index: Int, mouseX: Int, mouseY: Int): Boolean { - return bodyNode.focusInputSlot(index, mouseX, mouseY) - } + fun focusInputSlot(index: Int, mouseX: Int, mouseY: Int): Boolean = bodyNode.focusInputSlot(index, mouseX, mouseY) fun syncInputFocusForDomEditing() { bodyNode.syncFocusedInputForModeOrOrderChange() } - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } + override fun measure(ctx: UiMeasureContext): Size = + Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) popupEngine.onFrame(width, height) popupEngine.onCursorPosition(cursorX, cursorY) @@ -50,4 +53,3 @@ internal class SystemColorPickerOverlayNode( panelNode.render(ctx, x, y, width, height) } } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPanelManager.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPanelManager.kt index f590d45..5edc5e5 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPanelManager.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPanelManager.kt @@ -19,7 +19,7 @@ interface InspectorColorPickerHost { onPreview: ((RgbaColor) -> Unit)? = null, onChange: ((RgbaColor) -> Unit)? = null, onCommit: ((RgbaColor) -> Unit)? = null, - onClose: (() -> Unit)? = null + onClose: (() -> Unit)? = null, ) fun close() @@ -28,7 +28,7 @@ interface InspectorColorPickerHost { } internal class SystemColorPickerPanelManager( - private val delegate: ColorPickerPopupManager = ColorPickerPopupManager() + private val delegate: ColorPickerPopupManager = ColorPickerPopupManager(), ) : InspectorColorPickerHost { override fun open( anchorRect: Rect, @@ -41,7 +41,7 @@ internal class SystemColorPickerPanelManager( onPreview: ((RgbaColor) -> Unit)?, onChange: ((RgbaColor) -> Unit)?, onCommit: ((RgbaColor) -> Unit)?, - onClose: (() -> Unit)? + onClose: (() -> Unit)?, ) { delegate.open( ownerScope = OverlayOwnerScope.System, @@ -55,7 +55,7 @@ internal class SystemColorPickerPanelManager( onPreview = onPreview, onChange = onChange, onCommit = onCommit, - onClose = onClose + onClose = onClose, ) } @@ -65,4 +65,3 @@ internal class SystemColorPickerPanelManager( override fun isOpen(): Boolean = delegate.isOpen() } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt index acb54a5..129af3a 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt @@ -8,31 +8,14 @@ import org.dreamfinity.dsgl.core.dom.layout.Border import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.Size import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext -import org.dreamfinity.dsgl.core.dsl.UiScope -import org.dreamfinity.dsgl.core.dsl.alphaSlider -import org.dreamfinity.dsgl.core.dsl.button -import org.dreamfinity.dsgl.core.dsl.colorField -import org.dreamfinity.dsgl.core.dsl.colorSwatch -import org.dreamfinity.dsgl.core.dsl.div -import org.dreamfinity.dsgl.core.dsl.eyedropperMagnifier -import org.dreamfinity.dsgl.core.dsl.hueSlider -import org.dreamfinity.dsgl.core.dsl.text -import org.dreamfinity.dsgl.core.event.EventBus -import org.dreamfinity.dsgl.core.event.Events -import org.dreamfinity.dsgl.core.event.FocusManager -import org.dreamfinity.dsgl.core.event.FocusGainEvent -import org.dreamfinity.dsgl.core.event.FocusLoseEvent -import org.dreamfinity.dsgl.core.event.InputEvent -import org.dreamfinity.dsgl.core.event.KeyCodes -import org.dreamfinity.dsgl.core.event.KeyboardKeyDownEvent -import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.event.MouseDownEvent +import org.dreamfinity.dsgl.core.dsl.* +import org.dreamfinity.dsgl.core.event.* import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.TextWrap internal class SystemColorPickerPopupBodyNode( private val popupEngine: ColorPickerPopupEngine, - key: Any? = "dsgl-system-color-picker-native-body" + key: Any? = "dsgl-system-color-picker-native-body", ) : DOMNode(key) { override val styleType: String = "dsgl-system-color-picker-native-body" @@ -41,63 +24,109 @@ internal class SystemColorPickerPopupBodyNode( private val inputSemanticKeys: MutableList = MutableList(MAX_INPUT_SLOTS) { null } private var focusedSemanticInputKey: String? = null - private val modeSelectButton: ButtonNode = scope.button("", { - this.key = "dsgl-system-color-picker-mode-select" - }) - private val rgbaOrderButton: ButtonNode = scope.button("RGBA", { - this.key = "dsgl-system-color-picker-order-rgba" - }) - private val argbOrderButton: ButtonNode = scope.button("ARGB", { - this.key = "dsgl-system-color-picker-order-argb" - }) - private val colorFieldNode: ColorFieldSurfaceNode = scope.colorField({ - this.key = "dsgl-system-color-picker-surface-field" - }) - private val hueSliderNode: HueSurfaceNode = scope.hueSlider({ - this.key = "dsgl-system-color-picker-surface-hue" - }) - private val alphaSliderNode: AlphaSurfaceNode = scope.alphaSlider({ - this.key = "dsgl-system-color-picker-surface-alpha" - }) - - private val previousSwatchNode: ColorSwatchSurfaceNode = scope.colorSwatch({ - this.key = "dsgl-system-color-picker-swatch-previous" - }) - private val currentSwatchNode: ColorSwatchSurfaceNode = scope.colorSwatch({ - this.key = "dsgl-system-color-picker-swatch-current" - }) - - private val copyButton: ButtonNode = scope.button("Copy", { - this.key = "dsgl-system-color-picker-button-copy" - }) - private val pasteButton: ButtonNode = scope.button("Paste", { - this.key = "dsgl-system-color-picker-button-paste" - }) - private val pipetteButton: ButtonNode = scope.button("Pipette", { - this.key = "dsgl-system-color-picker-button-pipette" - }) - - private val inputLabelNodes: List = (0 until MAX_INPUT_SLOTS).map { index -> - scope.text(props = { - this.key = "dsgl-system-color-picker-input-label-$index" - source = TextSource.Dynamic { inputLabelValues[index] } - style = { - textWrap = TextWrap.NoWrap - } - }) - } - private val inputValueNodes: List = (0 until MAX_INPUT_SLOTS).map { index -> - TextInputNode(key = "dsgl-system-color-picker-input-value-$index") - .applyParent(this) - .also { node -> configureInputValueNode(index, node) } - } + private val modeSelectButton: ButtonNode = + scope.button( + "", + { + this.key = "dsgl-system-color-picker-mode-select" + }, + ) + private val rgbaOrderButton: ButtonNode = + scope.button( + "RGBA", + { + this.key = "dsgl-system-color-picker-order-rgba" + }, + ) + private val argbOrderButton: ButtonNode = + scope.button( + "ARGB", + { + this.key = "dsgl-system-color-picker-order-argb" + }, + ) + private val colorFieldNode: ColorFieldSurfaceNode = + scope.colorField( + { + this.key = "dsgl-system-color-picker-surface-field" + }, + ) + private val hueSliderNode: HueSurfaceNode = + scope.hueSlider( + { + this.key = "dsgl-system-color-picker-surface-hue" + }, + ) + private val alphaSliderNode: AlphaSurfaceNode = + scope.alphaSlider( + { + this.key = "dsgl-system-color-picker-surface-alpha" + }, + ) - private val recentSwatchNodes: List = (0 until RECENT_SWATCH_COUNT).map { index -> - scope.colorSwatch({ - allowEmpty = true - this.key = "dsgl-system-color-picker-recent-$index" - }) - } + private val previousSwatchNode: ColorSwatchSurfaceNode = + scope.colorSwatch( + { + this.key = "dsgl-system-color-picker-swatch-previous" + }, + ) + private val currentSwatchNode: ColorSwatchSurfaceNode = + scope.colorSwatch( + { + this.key = "dsgl-system-color-picker-swatch-current" + }, + ) + + private val copyButton: ButtonNode = + scope.button( + "Copy", + { + this.key = "dsgl-system-color-picker-button-copy" + }, + ) + private val pasteButton: ButtonNode = + scope.button( + "Paste", + { + this.key = "dsgl-system-color-picker-button-paste" + }, + ) + private val pipetteButton: ButtonNode = + scope.button( + "Pipette", + { + this.key = "dsgl-system-color-picker-button-pipette" + }, + ) + + private val inputLabelNodes: List = + (0 until MAX_INPUT_SLOTS).map { index -> + scope.text( + props = { + this.key = "dsgl-system-color-picker-input-label-$index" + source = TextSource.Dynamic { inputLabelValues[index] } + style = { + textWrap = TextWrap.NoWrap + } + }, + ) + } + private val inputValueNodes: List = + (0 until MAX_INPUT_SLOTS).map { index -> + TextInputNode(key = "dsgl-system-color-picker-input-value-$index") + .applyParent(this) + .also { node -> configureInputValueNode(index, node) } + } + + private val recentSwatchNodes: List = + (0 until RECENT_SWATCH_COUNT).map { index -> + scope.colorSwatch( + { + allowEmpty = true + this.key = "dsgl-system-color-picker-recent-$index" + }, + ) + } private var appliedStyle: ColorPickerStyle? = null @@ -122,11 +151,16 @@ internal class SystemColorPickerPopupBodyNode( resyncFocusedInputForModeOrOrderChange(controller, layout) } - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } + override fun measure(ctx: UiMeasureContext): Size = + Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) val controller = popupEngine.debugActiveController() if (controller == null || popupEngine.debugActivePanelRect() == null) { @@ -160,7 +194,7 @@ internal class SystemColorPickerPopupBodyNode( hueDeg = hueDeg, hoverX = hoverX, hoverY = hoverY, - modeDropdownOpen = modeDropdownOpen + modeDropdownOpen = modeDropdownOpen, ) renderInputRows( ctx = ctx, @@ -170,7 +204,7 @@ internal class SystemColorPickerPopupBodyNode( hoverX = hoverX, hoverY = hoverY, inputValues = inputValues, - definitionsByKey = definitionsByKey + definitionsByKey = definitionsByKey, ) renderRecentSwatchGrid( ctx = ctx, @@ -178,7 +212,7 @@ internal class SystemColorPickerPopupBodyNode( style = style, hoverX = hoverX, hoverY = hoverY, - recentColors = recentColors + recentColors = recentColors, ) } @@ -190,14 +224,14 @@ internal class SystemColorPickerPopupBodyNode( val hueDeg: Float, val hoverX: Int, val hoverY: Int, - val modeDropdownOpen: Boolean + val modeDropdownOpen: Boolean, ) private data class RecentSwatchRenderState( val layout: ColorPickerLayout, val style: ColorPickerStyle, val recentColors: List, - val hoveredRecent: Int + val hoveredRecent: Int, ) private data class InputRowsRenderState( @@ -207,7 +241,7 @@ internal class SystemColorPickerPopupBodyNode( val hoverX: Int, val hoverY: Int, val inputValues: Map, - val definitionsByKey: Map + val definitionsByKey: Map, ) private fun renderTopControls( @@ -219,18 +253,19 @@ internal class SystemColorPickerPopupBodyNode( hueDeg: Float, hoverX: Int, hoverY: Int, - modeDropdownOpen: Boolean + modeDropdownOpen: Boolean, ) { - val renderState = TopControlsRenderState( - controller = controller, - layout = layout, - style = style, - state = state, - hueDeg = hueDeg, - hoverX = hoverX, - hoverY = hoverY, - modeDropdownOpen = modeDropdownOpen - ) + val renderState = + TopControlsRenderState( + controller = controller, + layout = layout, + style = style, + state = state, + hueDeg = hueDeg, + hoverX = hoverX, + hoverY = hoverY, + modeDropdownOpen = modeDropdownOpen, + ) renderModeSelectControl(ctx, renderState) renderOrderControls(ctx, renderState) renderColorSurfaceControls(ctx, renderState) @@ -238,24 +273,20 @@ internal class SystemColorPickerPopupBodyNode( renderActionControls(ctx, renderState) } - private fun renderModeSelectControl( - ctx: UiMeasureContext, - state: TopControlsRenderState - ) { + private fun renderModeSelectControl(ctx: UiMeasureContext, state: TopControlsRenderState) { syncPickerButtonVisual( button = modeSelectButton, text = if (state.modeDropdownOpen) "${state.state.mode.name} ^" else "${state.state.mode.name} v", style = state.style, - hovered = state.layout.modeSelectRect.contains(state.hoverX, state.hoverY), - selected = state.modeDropdownOpen + hovered = + state.layout.modeSelectRect + .contains(state.hoverX, state.hoverY), + selected = state.modeDropdownOpen, ) renderNode(ctx, modeSelectButton, state.layout.modeSelectRect) } - private fun renderOrderControls( - ctx: UiMeasureContext, - state: TopControlsRenderState - ) { + private fun renderOrderControls(ctx: UiMeasureContext, state: TopControlsRenderState) { val showOrder = state.layout.rgbaOrderRect != null && state.layout.argbOrderRect != null if (showOrder) { val rgbaRect = state.layout.rgbaOrderRect @@ -265,14 +296,14 @@ internal class SystemColorPickerPopupBodyNode( text = null, style = state.style, hovered = rgbaRect.contains(state.hoverX, state.hoverY), - selected = state.state.rgbOrder == RgbChannelOrder.RGBA + selected = state.state.rgbOrder == RgbChannelOrder.RGBA, ) syncPickerButtonVisual( button = argbOrderButton, text = null, style = state.style, hovered = argbRect.contains(state.hoverX, state.hoverY), - selected = state.state.rgbOrder == RgbChannelOrder.ARGB + selected = state.state.rgbOrder == RgbChannelOrder.ARGB, ) renderNode(ctx, rgbaOrderButton, rgbaRect) renderNode(ctx, argbOrderButton, argbRect) @@ -282,10 +313,7 @@ internal class SystemColorPickerPopupBodyNode( } } - private fun renderColorSurfaceControls( - ctx: UiMeasureContext, - state: TopControlsRenderState - ) { + private fun renderColorSurfaceControls(ctx: UiMeasureContext, state: TopControlsRenderState) { colorFieldNode.bind(style = state.style, color = state.state.color, hueDeg = state.hueDeg) renderNode(ctx, colorFieldNode, state.layout.colorFieldRect) @@ -300,48 +328,52 @@ internal class SystemColorPickerPopupBodyNode( } } - private fun renderPrimarySwatches( - ctx: UiMeasureContext, - state: TopControlsRenderState - ) { + private fun renderPrimarySwatches(ctx: UiMeasureContext, state: TopControlsRenderState) { previousSwatchNode.bind( style = state.style, color = state.state.previous, - highlighted = state.layout.previousSwatchRect.contains(state.hoverX, state.hoverY) + highlighted = + state.layout.previousSwatchRect + .contains(state.hoverX, state.hoverY), ) currentSwatchNode.bind( style = state.style, color = state.state.color, - highlighted = state.layout.currentSwatchRect.contains(state.hoverX, state.hoverY) + highlighted = + state.layout.currentSwatchRect + .contains(state.hoverX, state.hoverY), ) renderNode(ctx, previousSwatchNode, state.layout.previousSwatchRect) renderNode(ctx, currentSwatchNode, state.layout.currentSwatchRect) } - private fun renderActionControls( - ctx: UiMeasureContext, - state: TopControlsRenderState - ) { + private fun renderActionControls(ctx: UiMeasureContext, state: TopControlsRenderState) { syncPickerButtonVisual( button = copyButton, text = null, style = state.style, - hovered = state.layout.copyRect.contains(state.hoverX, state.hoverY), - selected = false + hovered = + state.layout.copyRect + .contains(state.hoverX, state.hoverY), + selected = false, ) syncPickerButtonVisual( button = pasteButton, text = null, style = state.style, - hovered = state.layout.pasteRect.contains(state.hoverX, state.hoverY), - selected = false + hovered = + state.layout.pasteRect + .contains(state.hoverX, state.hoverY), + selected = false, ) syncPickerButtonVisual( button = pipetteButton, text = if (state.controller.isEyedropperActive()) "Pick..." else "Pipette", style = state.style, - hovered = state.layout.pipetteRect.contains(state.hoverX, state.hoverY), - selected = state.controller.isEyedropperActive() + hovered = + state.layout.pipetteRect + .contains(state.hoverX, state.hoverY), + selected = state.controller.isEyedropperActive(), ) renderNode(ctx, copyButton, state.layout.copyRect) renderNode(ctx, pasteButton, state.layout.pasteRect) @@ -356,29 +388,28 @@ internal class SystemColorPickerPopupBodyNode( hoverX: Int, hoverY: Int, inputValues: Map, - definitionsByKey: Map + definitionsByKey: Map, ) { resyncFocusedInputForModeOrOrderChange(controller, layout) - val renderState = InputRowsRenderState( - controller = controller, - layout = layout, - style = style, - hoverX = hoverX, - hoverY = hoverY, - inputValues = inputValues, - definitionsByKey = definitionsByKey - ) + val renderState = + InputRowsRenderState( + controller = controller, + layout = layout, + style = style, + hoverX = hoverX, + hoverY = hoverY, + inputValues = inputValues, + definitionsByKey = definitionsByKey, + ) for (index in 0 until MAX_INPUT_SLOTS) { renderInputRow(ctx, renderState, index) } } - private fun renderInputRow( - ctx: UiMeasureContext, - state: InputRowsRenderState, - index: Int - ) { - val inputSlot = state.layout.inputSlots.getOrNull(index) + private fun renderInputRow(ctx: UiMeasureContext, state: InputRowsRenderState, index: Int) { + val inputSlot = + state.layout.inputSlots + .getOrNull(index) val labelNode = inputLabelNodes[index] val inputNode = inputValueNodes[index] if (inputSlot == null) { @@ -393,7 +424,7 @@ internal class SystemColorPickerPopupBodyNode( state: InputRowsRenderState, labelNode: TextNode, inputNode: TextInputNode, - index: Int + index: Int, ) { inputSemanticKeys[index] = null if (FocusManager.isFocused(inputNode)) { @@ -404,7 +435,7 @@ internal class SystemColorPickerPopupBodyNode( syncTextNodeVisual( node = labelNode, text = "", - color = state.style.mutedTextColor + color = state.style.mutedTextColor, ) renderNode(ctx, labelNode, null) renderNode(ctx, inputNode, null) @@ -416,7 +447,7 @@ internal class SystemColorPickerPopupBodyNode( labelNode: TextNode, inputNode: TextInputNode, inputSlot: ColorPickerInputSlot, - index: Int + index: Int, ) { val key = inputSlot.key inputSemanticKeys[index] = key @@ -425,15 +456,16 @@ internal class SystemColorPickerPopupBodyNode( syncTextNodeVisual( node = labelNode, text = label, - color = state.style.mutedTextColor + color = state.style.mutedTextColor, ) val focused = FocusManager.isFocused(inputNode) - val borderColor = when { - focused -> state.style.inputActiveBorderColor - inputSlot.inputRect.contains(state.hoverX, state.hoverY) -> state.style.buttonHoverColor - else -> state.style.inputBorderColor - } + val borderColor = + when { + focused -> state.style.inputActiveBorderColor + inputSlot.inputRect.contains(state.hoverX, state.hoverY) -> state.style.buttonHoverColor + else -> state.style.inputBorderColor + } val value = if (focused) null else state.inputValues[key].orEmpty() syncTextInputVisual( node = inputNode, @@ -443,7 +475,7 @@ internal class SystemColorPickerPopupBodyNode( focusedBackground = state.style.inputBackgroundColor, textColor = state.style.textColor, placeholderColor = state.style.mutedTextColor, - fontSize = state.style.fontSize + fontSize = state.style.fontSize, ) renderNode(ctx, labelNode, inputSlot.labelRect) @@ -456,26 +488,25 @@ internal class SystemColorPickerPopupBodyNode( style: ColorPickerStyle, hoverX: Int, hoverY: Int, - recentColors: List + recentColors: List, ) { - val renderState = RecentSwatchRenderState( - layout = layout, - style = style, - recentColors = recentColors, - hoveredRecent = layout.recentRects.indexOfFirst { it.contains(hoverX, hoverY) } - ) + val renderState = + RecentSwatchRenderState( + layout = layout, + style = style, + recentColors = recentColors, + hoveredRecent = layout.recentRects.indexOfFirst { it.contains(hoverX, hoverY) }, + ) for (index in 0 until RECENT_SWATCH_COUNT) { renderRecentSwatch(ctx, renderState, index) } } - private fun renderRecentSwatch( - ctx: UiMeasureContext, - state: RecentSwatchRenderState, - index: Int - ) { + private fun renderRecentSwatch(ctx: UiMeasureContext, state: RecentSwatchRenderState, index: Int) { val swatchNode = recentSwatchNodes[index] - val swatchRect = state.layout.recentRects.getOrNull(index) + val swatchRect = + state.layout.recentRects + .getOrNull(index) if (swatchRect == null) { renderNode(ctx, swatchNode, null) return @@ -483,7 +514,7 @@ internal class SystemColorPickerPopupBodyNode( swatchNode.bind( style = state.style, color = state.recentColors.getOrNull(index), - highlighted = index == state.hoveredRecent + highlighted = index == state.hoveredRecent, ) renderNode(ctx, swatchNode, swatchRect) } @@ -531,16 +562,14 @@ internal class SystemColorPickerPopupBodyNode( } } - private fun resyncFocusedInputForModeOrOrderChange( - controller: ColorPickerController, - layout: ColorPickerLayout - ) { + private fun resyncFocusedInputForModeOrOrderChange(controller: ColorPickerController, layout: ColorPickerLayout) { val focusedIndex = inputValueNodes.indexOf(FocusManager.focusedNode()) val focusedSlotKey = if (focusedIndex >= 0) inputSemanticKeys.getOrNull(focusedIndex) else null - val resyncKey = controller.consumeDomInputFocusResyncKey() - ?: focusedSemanticInputKey - ?: focusedSlotKey - ?: return + val resyncKey = + controller.consumeDomInputFocusResyncKey() + ?: focusedSemanticInputKey + ?: focusedSlotKey + ?: return val targetIndex = layout.inputSlots.indexOfFirst { it.key == resyncKey } if (targetIndex >= 0) { FocusManager.requestFocus(inputValueNodes[targetIndex]) @@ -552,18 +581,22 @@ internal class SystemColorPickerPopupBodyNode( } private fun applyStaticStyle(style: ColorPickerStyle) { - val buttons = buildList { - add(modeSelectButton) - add(rgbaOrderButton) - add(argbOrderButton) - add(copyButton) - add(pasteButton) - add(pipetteButton) - } + val buttons = + buildList { + add(modeSelectButton) + add(rgbaOrderButton) + add(argbOrderButton) + add(copyButton) + add(pasteButton) + add(pipetteButton) + } buttons.forEach { button -> button.textColor = style.textColor button.applyStyle { - border { width = 1.px; color = style.inputBorderColor } + border { + width = 1.px + color = style.inputBorderColor + } fontSize = style.fontSize.px padding = 0.px textWrap = TextWrap.NoWrap @@ -588,13 +621,14 @@ internal class SystemColorPickerPopupBodyNode( text: String?, style: ColorPickerStyle, hovered: Boolean, - selected: Boolean + selected: Boolean, ) { - val nextBackground = when { - selected -> style.buttonActiveColor - hovered -> style.buttonHoverColor - else -> style.buttonBackgroundColor - } + val nextBackground = + when { + selected -> style.buttonActiveColor + hovered -> style.buttonHoverColor + else -> style.buttonBackgroundColor + } val nextBorder = Border.all(1, if (selected) style.inputActiveBorderColor else style.inputBorderColor) var changed = false if (text != null && button.text != text) { @@ -645,7 +679,7 @@ internal class SystemColorPickerPopupBodyNode( focusedBackground: Int, textColor: Int, placeholderColor: Int, - fontSize: Int + fontSize: Int, ) { var changed = false if (value != null && node.text != value) { @@ -681,7 +715,6 @@ internal class SystemColorPickerPopupBodyNode( } } - private fun renderNode(ctx: UiMeasureContext, node: DOMNode, rect: Rect?) { if (rect == null || rect.width <= 0 || rect.height <= 0) { node.display = Display.None @@ -707,7 +740,7 @@ internal class SystemColorPickerPopupBodyNode( internal class SystemColorPickerTransientOverlayNode( private val popupEngine: ColorPickerPopupEngine, - key: Any? = "dsgl-system-color-picker-native-transient" + key: Any? = "dsgl-system-color-picker-native-transient", ) : DOMNode(key) { override val styleType: String = "dsgl-system-color-picker-native-transient" @@ -716,11 +749,16 @@ internal class SystemColorPickerTransientOverlayNode( private val eyedropperOverlayNode: SystemColorPickerEyedropperOverlayNode = SystemColorPickerEyedropperOverlayNode(popupEngine).applyParent(this) - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } + override fun measure(ctx: UiMeasureContext): Size = + Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) modeDropdownOverlayNode.render(ctx, x, y, width, height) eyedropperOverlayNode.render(ctx, x, y, width, height) @@ -729,19 +767,24 @@ internal class SystemColorPickerTransientOverlayNode( internal class SystemColorPickerModeDropdownOverlayNode( private val popupEngine: ColorPickerPopupEngine, - key: Any? = "dsgl-system-color-picker-native-mode-dropdown-overlay" + key: Any? = "dsgl-system-color-picker-native-mode-dropdown-overlay", ) : DOMNode(key) { override val styleType: String = "dsgl-system-color-picker-native-mode-dropdown-overlay" private val scope = UiScope(this) - private val popupBackgroundNode: ContainerNode = ContainerNode( - key = "dsgl-system-color-picker-mode-dropdown-background" - ).applyParent(this) - private val modeOptionButtons: Map = ColorFormatMode.entries.associateWith { mode -> - scope.button(mode.name, { - this.key = "dsgl-system-color-picker-mode-option-${mode.name.lowercase()}" - }) - } + private val popupBackgroundNode: ContainerNode = + ContainerNode( + key = "dsgl-system-color-picker-mode-dropdown-background", + ).applyParent(this) + private val modeOptionButtons: Map = + ColorFormatMode.entries.associateWith { mode -> + scope.button( + mode.name, + { + this.key = "dsgl-system-color-picker-mode-option-${mode.name.lowercase()}" + }, + ) + } private var appliedStyle: ColorPickerStyle? = null private data class ModeDropdownRenderState( @@ -750,19 +793,25 @@ internal class SystemColorPickerModeDropdownOverlayNode( val popupRect: Rect, val hoverX: Int, val hoverY: Int, - val selectedMode: ColorFormatMode + val selectedMode: ColorFormatMode, ) - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } + override fun measure(ctx: UiMeasureContext): Size = + Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) - val renderState = resolveRenderState() ?: run { - hideAll(ctx) - return - } + val renderState = + resolveRenderState() ?: run { + hideAll(ctx) + return + } renderPopupBackground(ctx, renderState) renderModeOptions(ctx, renderState) } @@ -785,7 +834,7 @@ internal class SystemColorPickerModeDropdownOverlayNode( popupRect = popupRect, hoverX = hover.first, hoverY = hover.second, - selectedMode = controller.snapshot().mode + selectedMode = controller.snapshot().mode, ) } @@ -794,19 +843,21 @@ internal class SystemColorPickerModeDropdownOverlayNode( syncContainerVisual( node = popupBackgroundNode, backgroundColor = state.style.inputBackgroundColor, - border = Border.all(1, state.style.inputBorderColor) + border = Border.all(1, state.style.inputBorderColor), ) popupBackgroundNode.render( ctx, state.popupRect.x, state.popupRect.y, state.popupRect.width, - state.popupRect.height + state.popupRect.height, ) } private fun renderModeOptions(ctx: UiMeasureContext, state: ModeDropdownRenderState) { - val optionsByMode = state.layout.modeOptions.associateBy { it.mode } + val optionsByMode = + state.layout.modeOptions + .associateBy { it.mode } ColorFormatMode.entries.forEach { mode -> val button = modeOptionButtons[mode] ?: return@forEach val optionRect = optionsByMode[mode]?.rect @@ -820,7 +871,7 @@ internal class SystemColorPickerModeDropdownOverlayNode( text = null, style = state.style, hovered = optionRect.contains(state.hoverX, state.hoverY), - selected = state.selectedMode == mode + selected = state.selectedMode == mode, ) button.display = Display.Block button.render(ctx, optionRect.x, optionRect.y, optionRect.width, optionRect.height) @@ -830,7 +881,10 @@ internal class SystemColorPickerModeDropdownOverlayNode( private fun applyStaticStyle(style: ColorPickerStyle) { modeOptionButtons.values.forEach { button -> button.applyStyle { - border { width = 1.px; color = style.inputBorderColor } + border { + width = 1.px + color = style.inputBorderColor + } fontSize = style.fontSize.px padding = 0.px textWrap = TextWrap.NoWrap @@ -843,13 +897,14 @@ internal class SystemColorPickerModeDropdownOverlayNode( text: String?, style: ColorPickerStyle, hovered: Boolean, - selected: Boolean + selected: Boolean, ) { - val nextBackground = when { - selected -> style.buttonActiveColor - hovered -> style.buttonHoverColor - else -> style.buttonBackgroundColor - } + val nextBackground = + when { + selected -> style.buttonActiveColor + hovered -> style.buttonHoverColor + else -> style.buttonBackgroundColor + } val nextBorder = Border.all(1, if (selected) style.inputActiveBorderColor else style.inputBorderColor) var changed = false if (text != null && button.text != text) { @@ -904,60 +959,84 @@ internal class SystemColorPickerModeDropdownOverlayNode( internal class SystemColorPickerEyedropperOverlayNode( private val popupEngine: ColorPickerPopupEngine, - key: Any? = "dsgl-system-color-picker-native-eyedropper-overlay" + key: Any? = "dsgl-system-color-picker-native-eyedropper-overlay", ) : DOMNode(key) { override val styleType: String = "dsgl-system-color-picker-native-eyedropper-overlay" private val scope = UiScope(this) - private val captureNode: EyedropperCaptureNode = EyedropperCaptureNode( - key = "dsgl-system-color-picker-eyedropper-capture" - ).applyParent(this) - private val shadowNode: ContainerNode = scope.div({ - this.key = "dsgl-system-color-picker-eyedropper-shadow" - }) - private val panelNode: ContainerNode = scope.div({ - this.key = "dsgl-system-color-picker-eyedropper-panel" - }) - private val magnifierDrawNode: EyedropperMagnifierDrawNode = scope.eyedropperMagnifier({ - this.key = "dsgl-system-color-picker-eyedropper-magnifier" - }) - private val centerNode: ContainerNode = scope.div({ - this.key = "dsgl-system-color-picker-eyedropper-center" - }) - private val swatchNode: ColorSwatchSurfaceNode = scope.colorSwatch({ - allowEmpty = false - this.key = "dsgl-system-color-picker-eyedropper-swatch" - }) - private val modeTextNode: TextNode = createOverlayTextNode( - key = "dsgl-system-color-picker-eyedropper-mode", - text = "" - ) - private val valueTextNode: TextNode = createOverlayTextNode( - key = "dsgl-system-color-picker-eyedropper-value", - text = "" - ) + private val captureNode: EyedropperCaptureNode = + EyedropperCaptureNode( + key = "dsgl-system-color-picker-eyedropper-capture", + ).applyParent(this) + private val shadowNode: ContainerNode = + scope.div( + { + this.key = "dsgl-system-color-picker-eyedropper-shadow" + }, + ) + private val panelNode: ContainerNode = + scope.div( + { + this.key = "dsgl-system-color-picker-eyedropper-panel" + }, + ) + private val magnifierDrawNode: EyedropperMagnifierDrawNode = + scope.eyedropperMagnifier( + { + this.key = "dsgl-system-color-picker-eyedropper-magnifier" + }, + ) + private val centerNode: ContainerNode = + scope.div( + { + this.key = "dsgl-system-color-picker-eyedropper-center" + }, + ) + private val swatchNode: ColorSwatchSurfaceNode = + scope.colorSwatch( + { + allowEmpty = false + this.key = "dsgl-system-color-picker-eyedropper-swatch" + }, + ) + private val modeTextNode: TextNode = + createOverlayTextNode( + key = "dsgl-system-color-picker-eyedropper-mode", + text = "", + ) + private val valueTextNode: TextNode = + createOverlayTextNode( + key = "dsgl-system-color-picker-eyedropper-value", + text = "", + ) private data class EyedropperRenderState( val model: ColorPickerEyedropperOverlayModel, val style: ColorPickerStyle, - val color: RgbaColor + val color: RgbaColor, ) private data class EyedropperTextRects( val modeRect: Rect, - val valueRect: Rect + val valueRect: Rect, ) - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } + override fun measure(ctx: UiMeasureContext): Size = + Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) - val renderState = resolveRenderState() ?: run { - hideAll(ctx) - return - } + val renderState = + resolveRenderState() ?: run { + hideAll(ctx) + return + } syncVisuals(renderState) bindVisualNodes(renderState) @@ -966,15 +1045,16 @@ internal class SystemColorPickerEyedropperOverlayNode( private fun resolveRenderState(): EyedropperRenderState? { val controller = popupEngine.debugActiveController() ?: return null - val model = controller.resolveEyedropperOverlayModel( - viewportWidth = bounds.width.coerceAtLeast(1), - viewportHeight = bounds.height.coerceAtLeast(1) - ) ?: return null + val model = + controller.resolveEyedropperOverlayModel( + viewportWidth = bounds.width.coerceAtLeast(1), + viewportHeight = bounds.height.coerceAtLeast(1), + ) ?: return null val style = popupEngine.debugActiveStyle() ?: controller.style() return EyedropperRenderState( model = model, style = style, - color = controller.snapshot().color + color = controller.snapshot().color, ) } @@ -983,17 +1063,17 @@ internal class SystemColorPickerEyedropperOverlayNode( syncContainerVisual( node = shadowNode, backgroundColor = state.style.panelShadowColor, - border = Border.NONE + border = Border.NONE, ) syncContainerVisual( node = panelNode, backgroundColor = state.style.eyedropperOverlayBackgroundColor, - border = Border.all(1, state.style.eyedropperOverlayBorderColor) + border = Border.all(1, state.style.eyedropperOverlayBorderColor), ) syncContainerVisual( node = centerNode, backgroundColor = null, - border = Border.all(1, state.style.eyedropperCenterBorderColor) + border = Border.all(1, state.style.eyedropperCenterBorderColor), ) syncOverlayTextVisual(modeTextNode, state.style.mutedTextColor, state.style.fontSize) syncOverlayTextVisual(valueTextNode, state.style.textColor, state.style.fontSize) @@ -1002,28 +1082,31 @@ internal class SystemColorPickerEyedropperOverlayNode( private fun bindVisualNodes(state: EyedropperRenderState) { captureNode.bind( sourceRect = state.model.captureSourceRect, - fallbackColor = state.color.toArgbInt() + fallbackColor = state.color.toArgbInt(), ) swatchNode.bind(style = state.style, color = state.color, highlighted = false) magnifierDrawNode.bind( columns = state.model.captureSourceRect.width, rows = state.model.captureSourceRect.height, - magnification = ( - state.model.magnifierRect.width / - state.model.captureSourceRect.width.coerceAtLeast(1) + magnification = + ( + state.model.magnifierRect.width / + state.model.captureSourceRect.width + .coerceAtLeast(1) ).coerceAtLeast(1), gridEnabled = state.style.eyedropperGridOverlayEnabled, - gridColor = state.style.eyedropperGridOverlayColor + gridColor = state.style.eyedropperGridOverlayColor, ) } private fun renderOverlayNodes(ctx: UiMeasureContext, state: EyedropperRenderState) { - val shadowRect = Rect( - x = state.model.panelRect.x + 2, - y = state.model.panelRect.y + 2, - width = state.model.panelRect.width, - height = state.model.panelRect.height - ) + val shadowRect = + Rect( + x = state.model.panelRect.x + 2, + y = state.model.panelRect.y + 2, + width = state.model.panelRect.width, + height = state.model.panelRect.height, + ) val textRects = resolveTextRects(state) renderNode(ctx, captureNode, state.model.panelRect) @@ -1038,24 +1121,27 @@ internal class SystemColorPickerEyedropperOverlayNode( private fun resolveTextRects(state: EyedropperRenderState): EyedropperTextRects { val textX = state.model.swatchRect.x + state.model.swatchRect.width + 8 - val textWidth = ( - state.model.panelRect.x + state.model.panelRect.width - 6 - textX + val textWidth = + ( + state.model.panelRect.x + state.model.panelRect.width - 6 - textX ).coerceAtLeast(1) - val modeRect = Rect( - x = textX, - y = state.model.swatchRect.y + 1, - width = textWidth, - height = (state.style.fontSize + 2).coerceAtLeast(1) - ) - val valueRect = Rect( - x = textX, - y = modeRect.y + state.style.fontSize, - width = textWidth, - height = (state.style.fontSize + 2).coerceAtLeast(1) - ) + val modeRect = + Rect( + x = textX, + y = state.model.swatchRect.y + 1, + width = textWidth, + height = (state.style.fontSize + 2).coerceAtLeast(1), + ) + val valueRect = + Rect( + x = textX, + y = modeRect.y + state.style.fontSize, + width = textWidth, + height = (state.style.fontSize + 2).coerceAtLeast(1), + ) return EyedropperTextRects( modeRect = modeRect, - valueRect = valueRect + valueRect = valueRect, ) } @@ -1098,11 +1184,11 @@ internal class SystemColorPickerEyedropperOverlayNode( } } - private fun createOverlayTextNode(key: Any, text: String): TextNode { - return TextNode(TextSource.Static(text), key = key).apply { - textWrap = TextWrap.NoWrap - }.applyParent(this) - } + private fun createOverlayTextNode(key: Any, text: String): TextNode = + TextNode(TextSource.Static(text), key = key) + .apply { + textWrap = TextWrap.NoWrap + }.applyParent(this) private fun renderNode(ctx: UiMeasureContext, node: DOMNode, rect: Rect?) { if (rect == null || rect.width <= 0 || rect.height <= 0) { @@ -1121,4 +1207,3 @@ internal class SystemColorPickerEyedropperOverlayNode( } } } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt index acdbd7e..c780c54 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt @@ -13,11 +13,7 @@ import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection import org.dreamfinity.dsgl.core.style.JustifyContent -fun UiScope.modalHost( - modals: List, - modalKey: String = "modal.host", - content: UiScope.() -> Unit -) { +fun UiScope.modalHost(modals: List, modalKey: String = "modal.host", content: UiScope.() -> Unit) { ModalRuntime.onBuild(modalKey, modals) val hostNode = mount(ModalHostNode(modalKey)) hostNode.onKeyDown = { event -> @@ -47,10 +43,11 @@ fun UiScope.modalHost( modals.forEachIndexed { index, spec -> val isTopMost = index == modals.lastIndex val dialogKey = ModalRuntime.dialogKey(modalKey, spec.key) - val backdropColor = when (spec.backdrop) { - BackdropMode.True, BackdropMode.Static -> 0x88000000.toInt() - BackdropMode.False -> 0x00000000 - } + val backdropColor = + when (spec.backdrop) { + BackdropMode.True, BackdropMode.Static -> 0x88000000.toInt() + BackdropMode.False -> 0x00000000 + } hostScope.div({ key = "$modalKey.modal.${spec.key}.layer" onMouseDown = { event -> @@ -91,27 +88,28 @@ fun UiScope.modalHost( justifyContent = if (spec.centered) JustifyContent.Center else JustifyContent.Start padding { all((if (spec.centered) 6 else 10).px) } } - }) { modalFrame( spec = spec, dialogKey = dialogKey, - scope = ModalScope( - dismiss = spec.onHide, - isTopMost = isTopMost, - modalKey = spec.key - ) + scope = + ModalScope( + dismiss = spec.onHide, + isTopMost = isTopMost, + modalKey = spec.key, + ), ) } } hostScope.div({ key = "$modalKey.modal.lifecycle" - ref = RefTarget { handle -> - if (handle != null) { - ModalRuntime.onCommit(modalKey, modals) + ref = + RefTarget { handle -> + if (handle != null) { + ModalRuntime.onCommit(modalKey, modals) + } } - } style = { width = 0.px height = 0.px @@ -123,16 +121,17 @@ fun UiScope.modalHost( fun UiScope.modalFrame( spec: ModalSpec, dialogKey: String = "modal.dialog.${spec.key}", - scope: ModalScope = ModalScope( - dismiss = spec.onHide, - isTopMost = true, - modalKey = spec.key - ) + scope: ModalScope = + ModalScope( + dismiss = spec.onHide, + isTopMost = true, + modalKey = spec.key, + ), ) { modalDialog( centered = spec.centered, size = spec.size, - modalKey = dialogKey + modalKey = dialogKey, ) { spec.content(this, scope) } @@ -142,13 +141,14 @@ fun UiScope.modalDialog( centered: Boolean = false, size: ModalSize? = null, modalKey: Any? = null, - block: UiScope.() -> Unit + block: UiScope.() -> Unit, ) { - val presetWidth = when (size) { - ModalSize.Sm -> 132 - ModalSize.Lg -> 232 - null -> 184 - } + val presetWidth = + when (size) { + ModalSize.Sm -> 132 + ModalSize.Lg -> 232 + null -> 184 + } div({ key = modalKey style = { @@ -159,21 +159,24 @@ fun UiScope.modalDialog( gap = 0.px backgroundColor = 0xFF2F3A46.toInt() if (!centered) { - margin { top = 6.px; right = 0.px; bottom = 0.px; left = 0.px } + margin { + top = 6.px + right = 0.px + bottom = 0.px + left = 0.px + } + } + border { + width = 1.px + color = 0xFF6E7D8C.toInt() } - border { width = 1.px; color = 0xFF6E7D8C.toInt() } } - }) { block() } } -fun UiScope.modalHeader( - closeButton: Boolean = false, - onHide: (() -> Unit)? = null, - block: UiScope.() -> Unit = {} -) { +fun UiScope.modalHeader(closeButton: Boolean = false, onHide: (() -> Unit)? = null, block: UiScope.() -> Unit = {}) { div({ key = "modal.header" style = { @@ -204,10 +207,7 @@ fun UiScope.modalHeader( } } -fun UiScope.modalTitle( - text: String, - modalTitleKey: Any? = null -) { +fun UiScope.modalTitle(text: String, modalTitleKey: Any? = null) { div({ key = modalTitleKey style = { @@ -220,10 +220,7 @@ fun UiScope.modalTitle( } } -fun UiScope.modalBody( - modalBodyKey: Any? = null, - block: UiScope.() -> Unit -) { +fun UiScope.modalBody(modalBodyKey: Any? = null, block: UiScope.() -> Unit) { div({ key = modalBodyKey style = { @@ -238,10 +235,7 @@ fun UiScope.modalBody( } } -fun UiScope.modalFooter( - modalFooterKey: Any? = null, - block: UiScope.() -> Unit -) { +fun UiScope.modalFooter(modalFooterKey: Any? = null, block: UiScope.() -> Unit) { div({ key = modalFooterKey style = { @@ -255,7 +249,6 @@ fun UiScope.modalFooter( justifyContent = JustifyContent.End alignItems = AlignItems.Center } - }) { block() } @@ -265,11 +258,11 @@ fun alertModal( modalKey: String, title: String, message: String, - onClose: () -> Unit -): ModalSpec { - return ModalSpec( + onClose: () -> Unit, +): ModalSpec = + ModalSpec( key = modalKey, - onHide = onClose + onHide = onClose, ) { scope -> modalHeader(closeButton = true, onHide = scope.dismiss) { modalTitle(title) @@ -283,7 +276,6 @@ fun alertModal( }) } } -} fun confirmModal( modalKey: String, @@ -292,11 +284,11 @@ fun confirmModal( confirmText: String = "Confirm", cancelText: String = "Cancel", onConfirm: () -> Unit, - onCancel: () -> Unit -): ModalSpec { - return ModalSpec( + onCancel: () -> Unit, +): ModalSpec = + ModalSpec( key = modalKey, - onHide = onCancel + onHide = onCancel, ) { scope -> modalHeader(closeButton = true, onHide = scope.dismiss) { modalTitle(title) @@ -315,7 +307,6 @@ fun confirmModal( }) } } -} fun promptModal( modalKey: String, @@ -325,24 +316,25 @@ fun promptModal( confirmText: String = "Apply", cancelText: String = "Cancel", onConfirm: () -> Unit, - onCancel: () -> Unit -): ModalSpec { - return ModalSpec( + onCancel: () -> Unit, +): ModalSpec = + ModalSpec( key = modalKey, - onHide = onCancel + onHide = onCancel, ) { scope -> modalHeader(closeButton = true, onHide = scope.dismiss) { modalTitle(title) } modalBody { input( - InputType.Text(value = value, placeholder = "Enter value"), { + InputType.Text(value = value, placeholder = "Enter value"), + { this.key = "modal.prompt.input.$key" style = { width = 150.px } onInput = { event -> onValueInput(event.value) } - } + }, ) } modalFooter { @@ -354,7 +346,6 @@ fun promptModal( }) } } -} private fun isTargetInsideDialog(target: DOMNode?, dialogKey: String): Boolean { var node = target @@ -364,4 +355,3 @@ private fun isTargetInsideDialog(target: DOMNode?, dialogKey: String): Boolean { } return false } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalModels.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalModels.kt index 64ecff8..e00a94e 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalModels.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalModels.kt @@ -5,12 +5,12 @@ import org.dreamfinity.dsgl.core.dsl.UiScope enum class BackdropMode { True, False, - Static + Static, } enum class ModalSize { Sm, - Lg + Lg, } data class ModalSpec( @@ -22,11 +22,11 @@ data class ModalSpec( val trapFocus: Boolean = true, val restoreFocus: Boolean = true, val onHide: (() -> Unit)? = null, - val content: UiScope.(ModalScope) -> Unit + val content: UiScope.(ModalScope) -> Unit, ) data class ModalScope( val dismiss: (() -> Unit)?, val isTopMost: Boolean, - val modalKey: String + val modalKey: String, ) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalHostNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalHostNode.kt index 7b7565d..7c95764 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalHostNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalHostNode.kt @@ -12,7 +12,7 @@ import org.dreamfinity.dsgl.core.style.StyleEngine * child[0] is regular content and children[1..] are full-viewport modal layers. */ internal class ModalHostNode( - key: Any? + key: Any?, ) : DOMNode(key) { override val styleType: String = "modal-host" @@ -24,27 +24,43 @@ internal class ModalHostNode( return Size(totalWidth, totalHeight) } - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { val parentNode = parent - val resolvedBounds = if (parentNode != null && parentNode.parent == null) { - Rect( - x = 0, - y = 0, - width = StyleEngine.viewportWidthPx().coerceAtLeast(0), - height = StyleEngine.viewportHeightPx().coerceAtLeast(0) - ) - } else if (parentNode != null) { - Rect( - x = parentNode.bounds.x + parentNode.border.left + parentNode.padding.left, - y = parentNode.bounds.y + parentNode.border.top + parentNode.padding.top, - width = (parentNode.bounds.width - parentNode.border.horizontal - parentNode.padding.horizontal).coerceAtLeast(0), - height = (parentNode.bounds.height - parentNode.border.vertical - parentNode.padding.vertical).coerceAtLeast(0) - ) - } else { - Rect(x, y, width, height) - } + val resolvedBounds = + if (parentNode != null && parentNode.parent == null) { + Rect( + x = 0, + y = 0, + width = StyleEngine.viewportWidthPx().coerceAtLeast(0), + height = StyleEngine.viewportHeightPx().coerceAtLeast(0), + ) + } else if (parentNode != null) { + Rect( + x = parentNode.bounds.x + parentNode.border.left + parentNode.padding.left, + y = parentNode.bounds.y + parentNode.border.top + parentNode.padding.top, + width = + (parentNode.bounds.width - parentNode.border.horizontal - parentNode.padding.horizontal) + .coerceAtLeast( + 0, + ), + height = + (parentNode.bounds.height - parentNode.border.vertical - parentNode.padding.vertical) + .coerceAtLeast( + 0, + ), + ) + } else { + Rect(x, y, width, height) + } bounds = resolvedBounds - children.firstOrNull() + children + .firstOrNull() ?.render(ctx, resolvedBounds.x, resolvedBounds.y, resolvedBounds.width, resolvedBounds.height) if (children.size <= 1) return for (i in 1 until children.size) { @@ -54,4 +70,3 @@ internal class ModalHostNode( override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) = Unit } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalRuntime.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalRuntime.kt index 89a9d16..39ebf60 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalRuntime.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalRuntime.kt @@ -6,7 +6,7 @@ import java.util.concurrent.ConcurrentHashMap internal object ModalRuntime { private data class ModalMeta( - val restoreFocus: Boolean + val restoreFocus: Boolean, ) private class HostState { @@ -56,9 +56,10 @@ internal object ModalRuntime { } state.previousKeys = currentKeys - state.previousMetaByKey = modals.associate { spec -> - spec.key to ModalMeta(restoreFocus = spec.restoreFocus) - } + state.previousMetaByKey = + modals.associate { spec -> + spec.key to ModalMeta(restoreFocus = spec.restoreFocus) + } } fun onCommit(hostKey: String, modals: List) { @@ -100,4 +101,4 @@ internal object ModalRuntime { } fun dialogKey(hostKey: String, modalKey: String): String = "$hostKey.modal.$modalKey.dialog" -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuDsl.kt index 8005f5d..1af116c 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuDsl.kt @@ -7,7 +7,7 @@ fun contextMenu( id: String? = null, fontId: String? = null, fontSize: Int? = null, - block: ContextMenuBuilder.() -> Unit + block: ContextMenuBuilder.() -> Unit, ): ContextMenuModel { val builder = ContextMenuBuilder() builder.block() @@ -15,7 +15,7 @@ fun contextMenu( id = id, entries = builder.buildEntries(), fontId = fontId, - fontSize = fontSize + fontSize = fontSize, ) } @@ -23,56 +23,42 @@ fun contextMenu( class ContextMenuBuilder { private val entries: MutableList = ArrayList() - fun item( - label: String, - id: String? = null, - block: ContextMenuItemBuilder.() -> Unit = {} - ) { + fun item(label: String, id: String? = null, block: ContextMenuItemBuilder.() -> Unit = {}) { item(labelProvider = { label }, id = id, block = block) } - fun item( - labelProvider: () -> String, - id: String? = null, - block: ContextMenuItemBuilder.() -> Unit = {} - ) { + fun item(labelProvider: () -> String, id: String? = null, block: ContextMenuItemBuilder.() -> Unit = {}) { val builder = ContextMenuItemBuilder() builder.block() - entries += MenuEntry.Item( - id = id, - labelProvider = labelProvider, - iconProvider = builder.iconProvider, - hintProvider = builder.hintProvider, - enabledProvider = builder.enabledProvider, - checkedProvider = builder.checkedProvider, - closeOnAction = builder.closeOnAction, - onClick = builder.onClick - ) - } - - fun submenu( - label: String, - id: String? = null, - block: ContextMenuSubmenuBuilder.() -> Unit - ) { + entries += + MenuEntry.Item( + id = id, + labelProvider = labelProvider, + iconProvider = builder.iconProvider, + hintProvider = builder.hintProvider, + enabledProvider = builder.enabledProvider, + checkedProvider = builder.checkedProvider, + closeOnAction = builder.closeOnAction, + onClick = builder.onClick, + ) + } + + fun submenu(label: String, id: String? = null, block: ContextMenuSubmenuBuilder.() -> Unit) { submenu(labelProvider = { label }, id = id, block = block) } - fun submenu( - labelProvider: () -> String, - id: String? = null, - block: ContextMenuSubmenuBuilder.() -> Unit - ) { + fun submenu(labelProvider: () -> String, id: String? = null, block: ContextMenuSubmenuBuilder.() -> Unit) { val builder = ContextMenuSubmenuBuilder() builder.block() - entries += MenuEntry.Submenu( - id = id, - labelProvider = labelProvider, - iconProvider = builder.iconProvider, - hintProvider = builder.hintProvider, - enabledProvider = builder.enabledProvider, - entries = builder.buildEntries() - ) + entries += + MenuEntry.Submenu( + id = id, + labelProvider = labelProvider, + iconProvider = builder.iconProvider, + hintProvider = builder.hintProvider, + enabledProvider = builder.enabledProvider, + entries = builder.buildEntries(), + ) } fun separator(id: String? = null) { @@ -113,56 +99,42 @@ class ContextMenuSubmenuBuilder { enabledProvider = predicate } - fun item( - label: String, - id: String? = null, - block: ContextMenuItemBuilder.() -> Unit = {} - ) { + fun item(label: String, id: String? = null, block: ContextMenuItemBuilder.() -> Unit = {}) { item(labelProvider = { label }, id = id, block = block) } - fun item( - labelProvider: () -> String, - id: String? = null, - block: ContextMenuItemBuilder.() -> Unit = {} - ) { + fun item(labelProvider: () -> String, id: String? = null, block: ContextMenuItemBuilder.() -> Unit = {}) { val builder = ContextMenuItemBuilder() builder.block() - entries += MenuEntry.Item( - id = id, - labelProvider = labelProvider, - iconProvider = builder.iconProvider, - hintProvider = builder.hintProvider, - enabledProvider = builder.enabledProvider, - checkedProvider = builder.checkedProvider, - closeOnAction = builder.closeOnAction, - onClick = builder.onClick - ) - } - - fun submenu( - label: String, - id: String? = null, - block: ContextMenuSubmenuBuilder.() -> Unit - ) { + entries += + MenuEntry.Item( + id = id, + labelProvider = labelProvider, + iconProvider = builder.iconProvider, + hintProvider = builder.hintProvider, + enabledProvider = builder.enabledProvider, + checkedProvider = builder.checkedProvider, + closeOnAction = builder.closeOnAction, + onClick = builder.onClick, + ) + } + + fun submenu(label: String, id: String? = null, block: ContextMenuSubmenuBuilder.() -> Unit) { submenu(labelProvider = { label }, id = id, block = block) } - fun submenu( - labelProvider: () -> String, - id: String? = null, - block: ContextMenuSubmenuBuilder.() -> Unit - ) { + fun submenu(labelProvider: () -> String, id: String? = null, block: ContextMenuSubmenuBuilder.() -> Unit) { val builder = ContextMenuSubmenuBuilder() builder.block() - entries += MenuEntry.Submenu( - id = id, - labelProvider = labelProvider, - iconProvider = builder.iconProvider, - hintProvider = builder.hintProvider, - enabledProvider = builder.enabledProvider, - entries = builder.buildEntries() - ) + entries += + MenuEntry.Submenu( + id = id, + labelProvider = labelProvider, + iconProvider = builder.iconProvider, + hintProvider = builder.hintProvider, + enabledProvider = builder.enabledProvider, + entries = builder.buildEntries(), + ) } fun separator(id: String? = null) { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngine.kt index b9a8639..2a9ccb6 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngine.kt @@ -10,7 +10,7 @@ import org.dreamfinity.dsgl.core.style.StyleEngine class ContextMenuEngine( private val clock: ContextMenuClock = SystemContextMenuClock, - private val measurementCache: ContextMenuMeasurementCache = ContextMenuMeasurementCache() + private val measurementCache: ContextMenuMeasurementCache = ContextMenuMeasurementCache(), ) : ContextMenuHost { private data class OpenLevel( val token: Long, @@ -25,24 +25,24 @@ class ContextMenuEngine( var hoveredIndex: Int = -1, var selectedIndex: Int = -1, var scrollOffset: Int = 0, - var measurement: ContextMenuMeasurementCache.Measurement? = null + var measurement: ContextMenuMeasurementCache.Measurement? = null, ) private data class PendingOpen( val parentLevel: Int, val parentEntryIndex: Int, - val dueMs: Long + val dueMs: Long, ) private data class PendingTrim( val fromLevel: Int, - val dueMs: Long + val dueMs: Long, ) data class StackSnapshot( val levelCount: Int, val hoveredIndices: List, - val selectedIndices: List + val selectedIndices: List, ) private val levels: MutableList = ArrayList(4) @@ -68,17 +68,14 @@ class ContextMenuEngine( fun measurementComputeCount(): Long = measurementCache.computeCount - fun snapshot(): StackSnapshot { - return StackSnapshot( + fun snapshot(): StackSnapshot = + StackSnapshot( levelCount = levels.size, hoveredIndices = levels.map { it.hoveredIndex }, - selectedIndices = levels.map { it.selectedIndex } + selectedIndices = levels.map { it.selectedIndex }, ) - } - fun debugPanelRect(levelIndex: Int): Rect? { - return levels.getOrNull(levelIndex)?.panelRect - } + fun debugPanelRect(levelIndex: Int): Rect? = levels.getOrNull(levelIndex)?.panelRect fun debugEntryRect(levelIndex: Int, entryIndex: Int): Rect? { val level = levels.getOrNull(levelIndex) ?: return null @@ -91,16 +88,17 @@ class ContextMenuEngine( return } levels.clear() - levels += OpenLevel( - token = model.token, - entries = model.entries, - placementMode = PLACEMENT_CURSOR, - fontId = model.fontId, - fontSize = model.fontSize, - anchorRect = Rect(x, y, 0, 0), - parentLevelIndex = -1, - parentEntryIndex = -1 - ) + levels += + OpenLevel( + token = model.token, + entries = model.entries, + placementMode = PLACEMENT_CURSOR, + fontId = model.fontId, + fontSize = model.fontSize, + anchorRect = Rect(x, y, 0, 0), + parentLevelIndex = -1, + parentEntryIndex = -1, + ) pendingOpen = null pendingTrim = null layoutDirty = true @@ -112,16 +110,17 @@ class ContextMenuEngine( return } levels.clear() - levels += OpenLevel( - token = model.token, - entries = model.entries, - placementMode = PLACEMENT_ANCHORED, - fontId = model.fontId, - fontSize = model.fontSize, - anchorRect = anchorRect, - parentLevelIndex = -1, - parentEntryIndex = -1 - ) + levels += + OpenLevel( + token = model.token, + entries = model.entries, + placementMode = PLACEMENT_ANCHORED, + fontId = model.fontId, + fontSize = model.fontSize, + anchorRect = anchorRect, + parentLevelIndex = -1, + parentEntryIndex = -1, + ) pendingOpen = null pendingTrim = null layoutDirty = true @@ -140,7 +139,7 @@ class ContextMenuEngine( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, - viewportScale: Float = 1f + viewportScale: Float = 1f, ) { lastMeasureContext = measureContext if (this.viewportWidth != viewportWidth || this.viewportHeight != viewportHeight) { @@ -165,14 +164,14 @@ class ContextMenuEngine( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, - out: MutableList + out: MutableList, ) { if (!isOpen()) return onFrame( measureContext = measureContext, viewportWidth = viewportWidth, viewportHeight = viewportHeight, - viewportScale = viewportScale + viewportScale = viewportScale, ) if (!isOpen()) return @@ -212,18 +211,33 @@ class ContextMenuEngine( return@forEach } - val itemRect = Rect( - rowRect.x, - rowRect.y, - rowRect.width, - rowRect.height - ) + val itemRect = + Rect( + rowRect.x, + rowRect.y, + rowRect.width, + rowRect.height, + ) val isHovered = index == level.hoveredIndex val isSelected = index == level.selectedIndex if (isHovered) { - out += RenderCommand.DrawRect(itemRect.x, itemRect.y, itemRect.width, itemRect.height, style.itemHoverBackgroundColor) + out += + RenderCommand.DrawRect( + itemRect.x, + itemRect.y, + itemRect.width, + itemRect.height, + style.itemHoverBackgroundColor, + ) } else if (isSelected) { - out += RenderCommand.DrawRect(itemRect.x, itemRect.y, itemRect.width, itemRect.height, style.itemSelectedBackgroundColor) + out += + RenderCommand.DrawRect( + itemRect.x, + itemRect.y, + itemRect.width, + itemRect.height, + style.itemSelectedBackgroundColor, + ) } val textY = itemRect.y + ((itemRect.height - fontHeight).coerceAtLeast(0) / 2) @@ -232,54 +246,62 @@ class ContextMenuEngine( val labelX = indicatorX + measurement.indicatorWidth + style.contentSpacing val textColor = if (snapshot.enabled) style.itemTextColor else style.disabledTextColor - val indicatorText = when { - snapshot.checked -> ContextMenuGlyphs.CHECK_MARK - !snapshot.icon.isNullOrEmpty() -> snapshot.icon - else -> null - } + val indicatorText = + when { + snapshot.checked -> ContextMenuGlyphs.CHECK_MARK + !snapshot.icon.isNullOrEmpty() -> snapshot.icon + else -> null + } if (!indicatorText.isNullOrEmpty()) { val indicatorColor = if (snapshot.checked) style.checkMarkColor else textColor - out += RenderCommand.DrawText( - text = indicatorText, - x = indicatorX, + out += + RenderCommand.DrawText( + text = indicatorText, + x = indicatorX, + y = textY, + color = indicatorColor, + fontId = fontId, + fontSize = fontSize, + ) + } + + out += + RenderCommand.DrawText( + text = snapshot.label, + x = labelX, y = textY, - color = indicatorColor, + color = textColor, fontId = fontId, - fontSize = fontSize + fontSize = fontSize, ) - } - - out += RenderCommand.DrawText( - text = snapshot.label, - x = labelX, - y = textY, - color = textColor, - fontId = fontId, - fontSize = fontSize - ) - val hintText = when { - snapshot.kind == ContextMenuMeasurementCache.KIND_SUBMENU && snapshot.hint.isNullOrEmpty() -> ContextMenuGlyphs.SUBMENU_ARROW - else -> snapshot.hint - } + val hintText = + when { + snapshot.kind == ContextMenuMeasurementCache.KIND_SUBMENU && + snapshot.hint.isNullOrEmpty() -> ContextMenuGlyphs.SUBMENU_ARROW + else -> snapshot.hint + } if (!hintText.isNullOrEmpty()) { val hintWidth = measureContext.measureText(hintText, fontId, fontSize) val hintX = itemRect.x + itemRect.width - style.rowPaddingX - hintWidth - val hintColor = when { - !snapshot.enabled -> style.disabledTextColor - snapshot.kind == ContextMenuMeasurementCache.KIND_SUBMENU && snapshot.hint.isNullOrEmpty() -> - style.submenuArrowColor - - else -> style.hintTextColor - } - out += RenderCommand.DrawText( - text = hintText, - x = hintX, - y = textY, - color = hintColor, - fontId = fontId, - fontSize = fontSize - ) + val hintColor = + when { + !snapshot.enabled -> style.disabledTextColor + snapshot.kind == ContextMenuMeasurementCache.KIND_SUBMENU && + snapshot.hint.isNullOrEmpty() -> + style.submenuArrowColor + + else -> style.hintTextColor + } + out += + RenderCommand.DrawText( + text = hintText, + x = hintX, + y = textY, + color = hintColor, + fontId = fontId, + fontSize = fontSize, + ) } } @@ -311,7 +333,10 @@ class ContextMenuEngine( hitLevel.selectedIndex = hit.entryIndex } - val snapshot = hitLevel.measurement?.snapshots?.getOrNull(hit.entryIndex) + val snapshot = + hitLevel.measurement + ?.snapshots + ?.getOrNull(hit.entryIndex) if (snapshot != null && snapshot.kind == ContextMenuMeasurementCache.KIND_SUBMENU && snapshot.enabled) { scheduleSubmenuOpen(hit.levelIndex, hit.entryIndex) } else { @@ -405,7 +430,10 @@ class ContextMenuEngine( KeyCodes.RIGHT -> { val index = normalizedSelection(level) if (index >= 0) { - val snapshot = level.measurement?.snapshots?.getOrNull(index) + val snapshot = + level.measurement + ?.snapshots + ?.getOrNull(index) if (snapshot != null && snapshot.kind == ContextMenuMeasurementCache.KIND_SUBMENU && snapshot.enabled @@ -447,15 +475,16 @@ class ContextMenuEngine( var index = 0 while (index < levels.size) { val level = levels[index] - val measurement = measurementCache.measure( - menuToken = level.token, - entries = level.entries, - style = style, - fontId = level.fontId ?: style.fontId, - fontSize = level.fontSize ?: style.fontSize, - ctx = ctx, - dpiScale = viewportScale - ) + val measurement = + measurementCache.measure( + menuToken = level.token, + entries = level.entries, + style = style, + fontId = level.fontId ?: style.fontId, + fontSize = level.fontSize ?: style.fontSize, + ctx = ctx, + dpiScale = viewportScale, + ) level.measurement = measurement val panelWidth = measurement.panelWidth + style.panelPaddingX * 2 val maxAvailableHeight = (viewportHeight - style.viewportPadding * 2).coerceAtLeast(1) @@ -476,33 +505,36 @@ class ContextMenuEngine( level.anchorRect = parentEntryRect } - val preferredRect = when (level.placementMode) { - PLACEMENT_CURSOR -> { - Rect(level.anchorRect.x, level.anchorRect.y, panelWidth, panelHeight) - } + val preferredRect = + when (level.placementMode) { + PLACEMENT_CURSOR -> { + Rect(level.anchorRect.x, level.anchorRect.y, panelWidth, panelHeight) + } - PLACEMENT_ANCHORED -> { - Rect(level.anchorRect.x, level.anchorRect.y + level.anchorRect.height, panelWidth, panelHeight) - } + PLACEMENT_ANCHORED -> { + Rect(level.anchorRect.x, level.anchorRect.y + level.anchorRect.height, panelWidth, panelHeight) + } - else -> { - Rect(level.anchorRect.x + level.anchorRect.width, level.anchorRect.y, panelWidth, panelHeight) + else -> { + Rect(level.anchorRect.x + level.anchorRect.width, level.anchorRect.y, panelWidth, panelHeight) + } } - } - val flipCandidateX = if (level.placementMode == PLACEMENT_SUBMENU) { - level.anchorRect.x - panelWidth - } else { - null - } - val placement = PopupPlacement.resolve( - PopupPlacementRequest( - preferredRect = preferredRect, - popupSize = Size(panelWidth, panelHeight), - viewport = Rect(0, 0, viewportWidth.coerceAtLeast(1), viewportHeight.coerceAtLeast(1)), - padding = style.viewportPadding, - horizontalFlipX = flipCandidateX + val flipCandidateX = + if (level.placementMode == PLACEMENT_SUBMENU) { + level.anchorRect.x - panelWidth + } else { + null + } + val placement = + PopupPlacement.resolve( + PopupPlacementRequest( + preferredRect = preferredRect, + popupSize = Size(panelWidth, panelHeight), + viewport = Rect(0, 0, viewportWidth.coerceAtLeast(1), viewportHeight.coerceAtLeast(1)), + padding = style.viewportPadding, + horizontalFlipX = flipCandidateX, + ), ) - ) level.panelRect = placement.rect val maxScroll = maxScroll(level) if (level.scrollOffset > maxScroll) { @@ -522,9 +554,12 @@ class ContextMenuEngine( val measurement = level.measurement ?: return if (measurement.snapshots.isEmpty()) return - val start = if (level.selectedIndex >= 0) level.selectedIndex else { - if (direction >= 0) -1 else measurement.snapshots.size - } + val start = + if (level.selectedIndex >= 0) { + level.selectedIndex + } else { + if (direction >= 0) -1 else measurement.snapshots.size + } var index = start repeat(measurement.snapshots.size) { index += direction @@ -567,7 +602,10 @@ class ContextMenuEngine( private fun activate(levelIndex: Int, entryIndex: Int) { val level = levels.getOrNull(levelIndex) ?: return val entry = level.entries.getOrNull(entryIndex) ?: return - val snapshot = level.measurement?.snapshots?.getOrNull(entryIndex) ?: return + val snapshot = + level.measurement + ?.snapshots + ?.getOrNull(entryIndex) ?: return if (!snapshot.enabled || snapshot.kind == ContextMenuMeasurementCache.KIND_SEPARATOR) { return } @@ -604,16 +642,17 @@ class ContextMenuEngine( } trimLevels(parentLevelIndex + 1) } - levels += OpenLevel( - token = parentEntry.token, - entries = parentEntry.entries, - placementMode = PLACEMENT_SUBMENU, - fontId = parent.fontId, - fontSize = parent.fontSize, - anchorRect = parent.anchorRect, - parentLevelIndex = parentLevelIndex, - parentEntryIndex = parentEntryIndex - ) + levels += + OpenLevel( + token = parentEntry.token, + entries = parentEntry.entries, + placementMode = PLACEMENT_SUBMENU, + fontId = parent.fontId, + fontSize = parent.fontSize, + anchorRect = parent.anchorRect, + parentLevelIndex = parentLevelIndex, + parentEntryIndex = parentEntryIndex, + ) pendingOpen = null pendingTrim = null layoutDirty = true @@ -668,11 +707,12 @@ class ContextMenuEngine( } val due = clock.nowMs() + delayMs val current = pendingTrim - pendingTrim = if (current == null || fromLevel < current.fromLevel || due < current.dueMs) { - PendingTrim(fromLevel, due) - } else { - current - } + pendingTrim = + if (current == null || fromLevel < current.fromLevel || due < current.dueMs) { + PendingTrim(fromLevel, due) + } else { + current + } } private fun processTimers() { @@ -696,7 +736,7 @@ class ContextMenuEngine( private data class Hit( val levelIndex: Int, - val entryIndex: Int + val entryIndex: Int, ) private fun hitTest(mouseX: Int, mouseY: Int): Hit? { @@ -770,4 +810,3 @@ class ContextMenuEngine( private const val PLACEMENT_SUBMENU: Int = 3 } } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuHost.kt index e2e1712..29865e1 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuHost.kt @@ -4,8 +4,11 @@ import org.dreamfinity.dsgl.core.dom.layout.Rect interface ContextMenuHost { fun openAtCursor(model: ContextMenuModel, x: Int, y: Int) + fun openAnchored(model: ContextMenuModel, anchorRect: Rect) + fun closeAll() + fun isOpen(): Boolean } @@ -23,7 +26,7 @@ data class ContextMenuTriggerScope( val anchorRect: Rect?, private val inheritedFontId: String?, private val inheritedFontSize: Int?, - private val host: ContextMenuHost + private val host: ContextMenuHost, ) { fun openMenu(model: ContextMenuModel) { host.openAtCursor(resolveModel(model), mouseX, mouseY) @@ -41,7 +44,7 @@ data class ContextMenuTriggerScope( if (model.fontId != null && model.fontSize != null) return model return model.copy( fontId = model.fontId ?: inheritedFontId, - fontSize = model.fontSize ?: inheritedFontSize + fontSize = model.fontSize ?: inheritedFontSize, ) } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuMeasurementCache.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuMeasurementCache.kt index 5bb5716..e76370b 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuMeasurementCache.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuMeasurementCache.kt @@ -3,7 +3,7 @@ package org.dreamfinity.dsgl.core.contextmenu import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext class ContextMenuMeasurementCache( - private val maxEntries: Int = 192 + private val maxEntries: Int = 192, ) { data class EntrySnapshot( val id: String?, @@ -12,7 +12,7 @@ class ContextMenuMeasurementCache( val icon: String?, val hint: String?, val enabled: Boolean, - val checked: Boolean + val checked: Boolean, ) data class Measurement( @@ -25,7 +25,7 @@ class ContextMenuMeasurementCache( val maxLabelWidth: Int, val maxHintWidth: Int, val panelWidth: Int, - val indicatorWidth: Int + val indicatorWidth: Int, ) data class Key( @@ -33,14 +33,13 @@ class ContextMenuMeasurementCache( val styleHash: Int, val fontHash: Int, val dpiKey: Int, - val entriesHash: Int + val entriesHash: Int, ) private val cache: MutableMap = object : LinkedHashMap(64, 0.75f, true) { - override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean { - return size > maxEntries - } + override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean = + size > maxEntries } var computeCount: Long = 0L @@ -53,24 +52,26 @@ class ContextMenuMeasurementCache( fontId: String?, fontSize: Int?, ctx: UiMeasureContext, - dpiScale: Float + dpiScale: Float, ): Measurement { val snapshots = buildSnapshots(entries) val fingerprint = fingerprint(snapshots) - val key = Key( - menuToken = menuToken, - styleHash = style.hashCode(), - fontHash = 31 * (fontId?.hashCode() ?: 0) + (fontSize ?: 0), - dpiKey = (dpiScale * 1000f).toInt(), - entriesHash = fingerprint - ) + val key = + Key( + menuToken = menuToken, + styleHash = style.hashCode(), + fontHash = 31 * (fontId?.hashCode() ?: 0) + (fontSize ?: 0), + dpiKey = (dpiScale * 1000f).toInt(), + entriesHash = fingerprint, + ) synchronized(cache) { cache[key]?.let { return it } } computeCount += 1 - val rowHeight = (ctx.fontHeight(fontId, fontSize) + style.rowPaddingY * 2) - .coerceAtLeast(14) + val rowHeight = + (ctx.fontHeight(fontId, fontSize) + style.rowPaddingY * 2) + .coerceAtLeast(14) val separatorHeight = style.separatorHeight.coerceAtLeast(2) val entryHeights = IntArray(snapshots.size) @@ -94,11 +95,12 @@ class ContextMenuMeasurementCache( maxLabelWidth = labelWidth } - val indicatorText = when { - snapshot.checked -> ContextMenuGlyphs.CHECK_MARK - !snapshot.icon.isNullOrEmpty() -> snapshot.icon - else -> null - } + val indicatorText = + when { + snapshot.checked -> ContextMenuGlyphs.CHECK_MARK + !snapshot.icon.isNullOrEmpty() -> snapshot.icon + else -> null + } if (!indicatorText.isNullOrEmpty()) { val indicatorWidth = ctx.measureText(indicatorText, fontId, fontSize) if (indicatorWidth > maxIndicatorWidth) { @@ -106,11 +108,12 @@ class ContextMenuMeasurementCache( } } - val hintText = when { - !snapshot.hint.isNullOrEmpty() -> snapshot.hint - snapshot.kind == KIND_SUBMENU -> ContextMenuGlyphs.SUBMENU_ARROW - else -> null - } + val hintText = + when { + !snapshot.hint.isNullOrEmpty() -> snapshot.hint + snapshot.kind == KIND_SUBMENU -> ContextMenuGlyphs.SUBMENU_ARROW + else -> null + } if (!hintText.isNullOrEmpty()) { val hintWidth = ctx.measureText(hintText, fontId, fontSize) if (hintWidth > maxHintWidth) { @@ -133,18 +136,19 @@ class ContextMenuMeasurementCache( style.rowPaddingX val panelWidth = measuredWidth.coerceAtLeast(style.minPanelWidth) - val measured = Measurement( - snapshots = snapshots, - rowHeight = rowHeight, - separatorHeight = separatorHeight, - entryHeights = entryHeights, - entryOffsets = entryOffsets, - totalContentHeight = offset, - maxLabelWidth = maxLabelWidth, - maxHintWidth = maxHintWidth, - panelWidth = panelWidth, - indicatorWidth = measuredIndicatorWidth - ) + val measured = + Measurement( + snapshots = snapshots, + rowHeight = rowHeight, + separatorHeight = separatorHeight, + entryHeights = entryHeights, + entryOffsets = entryOffsets, + totalContentHeight = offset, + maxLabelWidth = maxLabelWidth, + maxHintWidth = maxHintWidth, + panelWidth = panelWidth, + indicatorWidth = measuredIndicatorWidth, + ) synchronized(cache) { cache[key] = measured } @@ -157,39 +161,42 @@ class ContextMenuMeasurementCache( entries.forEach { entry -> when (entry) { is MenuEntry.Item -> { - out += EntrySnapshot( - id = entry.id, - kind = KIND_ITEM, - label = entry.labelProvider.invoke(), - icon = entry.iconProvider?.invoke(), - hint = entry.hintProvider?.invoke(), - enabled = entry.enabledProvider.invoke(), - checked = entry.checkedProvider?.invoke() == true - ) + out += + EntrySnapshot( + id = entry.id, + kind = KIND_ITEM, + label = entry.labelProvider.invoke(), + icon = entry.iconProvider?.invoke(), + hint = entry.hintProvider?.invoke(), + enabled = entry.enabledProvider.invoke(), + checked = entry.checkedProvider?.invoke() == true, + ) } is MenuEntry.Submenu -> { - out += EntrySnapshot( - id = entry.id, - kind = KIND_SUBMENU, - label = entry.labelProvider.invoke(), - icon = entry.iconProvider?.invoke(), - hint = entry.hintProvider?.invoke(), - enabled = entry.enabledProvider.invoke(), - checked = false - ) + out += + EntrySnapshot( + id = entry.id, + kind = KIND_SUBMENU, + label = entry.labelProvider.invoke(), + icon = entry.iconProvider?.invoke(), + hint = entry.hintProvider?.invoke(), + enabled = entry.enabledProvider.invoke(), + checked = false, + ) } is MenuEntry.Separator -> { - out += EntrySnapshot( - id = entry.id, - kind = KIND_SEPARATOR, - label = "", - icon = null, - hint = null, - enabled = false, - checked = false - ) + out += + EntrySnapshot( + id = entry.id, + kind = KIND_SEPARATOR, + label = "", + icon = null, + hint = null, + enabled = false, + checked = false, + ) } } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuModel.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuModel.kt index b5a86fc..234a9a3 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuModel.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuModel.kt @@ -4,6 +4,7 @@ import java.util.concurrent.atomic.AtomicLong private object ContextMenuIds { private val nextToken = AtomicLong(1L) + fun next(): Long = nextToken.getAndIncrement() } @@ -12,11 +13,11 @@ data class ContextMenuModel( val entries: List, val fontId: String? = null, val fontSize: Int? = null, - internal val token: Long = ContextMenuIds.next() + internal val token: Long = ContextMenuIds.next(), ) sealed class MenuEntry( - open val id: String? + open val id: String?, ) { data class Item( override val id: String? = null, @@ -26,7 +27,7 @@ sealed class MenuEntry( val enabledProvider: () -> Boolean = { true }, val checkedProvider: (() -> Boolean)? = null, val closeOnAction: Boolean = true, - val onClick: (() -> Unit)? = null + val onClick: (() -> Unit)? = null, ) : MenuEntry(id) data class Submenu( @@ -36,10 +37,10 @@ sealed class MenuEntry( val hintProvider: (() -> String?)? = null, val enabledProvider: () -> Boolean = { true }, val entries: List, - internal val token: Long = ContextMenuIds.next() + internal val token: Long = ContextMenuIds.next(), ) : MenuEntry(id) data class Separator( - override val id: String? = null + override val id: String? = null, ) : MenuEntry(id) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuStyle.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuStyle.kt index e8e65d9..51b8fbb 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuStyle.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuStyle.kt @@ -31,5 +31,5 @@ data class ContextMenuStyle( val hintTextColor: Int = 0xFFB6C2CF.toInt(), val separatorColor: Int = 0xFF4D5D6E.toInt(), val checkMarkColor: Int = 0xFF8BE39A.toInt(), - val submenuArrowColor: Int = 0xFFC7D4E1.toInt() + val submenuArrowColor: Int = 0xFFC7D4E1.toInt(), ) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/PopupPlacement.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/PopupPlacement.kt index 44612b5..e33647b 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/PopupPlacement.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/PopupPlacement.kt @@ -8,13 +8,13 @@ data class PopupPlacementRequest( val popupSize: Size, val viewport: Rect, val padding: Int = 6, - val horizontalFlipX: Int? = null + val horizontalFlipX: Int? = null, ) data class PopupPlacementResult( val rect: Rect, val flippedHorizontally: Boolean, - val clampedVertically: Boolean + val clampedVertically: Boolean, ) object PopupPlacement { @@ -25,8 +25,12 @@ object PopupPlacement { val maxY = request.viewport.y + request.viewport.height - request.padding val widthLimit = (maxX - minX).coerceAtLeast(1) val heightLimit = (maxY - minY).coerceAtLeast(1) - val resolvedWidth = request.popupSize.width.coerceIn(1, widthLimit) - val resolvedHeight = request.popupSize.height.coerceIn(1, heightLimit) + val resolvedWidth = + request.popupSize.width + .coerceIn(1, widthLimit) + val resolvedHeight = + request.popupSize.height + .coerceIn(1, heightLimit) val maxPanelX = (maxX - resolvedWidth).coerceAtLeast(minX) val maxPanelY = (maxY - resolvedHeight).coerceAtLeast(minY) @@ -54,7 +58,7 @@ object PopupPlacement { return PopupPlacementResult( rect = Rect(x, clampedY, resolvedWidth, resolvedHeight), flippedHorizontally = flipped, - clampedVertically = verticallyClamped + clampedVertically = verticallyClamped, ) } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt index 4305232..c42b2f7 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt @@ -1,7 +1,6 @@ package org.dreamfinity.dsgl.core.debug import org.dreamfinity.dsgl.core.DomTree -import org.dreamfinity.dsgl.core.dsl.UiScope import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.elements.ButtonNode import org.dreamfinity.dsgl.core.dom.elements.ContainerNode @@ -11,6 +10,7 @@ import org.dreamfinity.dsgl.core.dom.layout.Border import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.Size import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.dsl.UiScope import org.dreamfinity.dsgl.core.dsl.button import org.dreamfinity.dsgl.core.dsl.div import org.dreamfinity.dsgl.core.dsl.text @@ -29,11 +29,11 @@ internal data class OverlayDebugControlLayout( val systemOverlayTintRect: Rect, val systemOverlayRenderRect: Rect, val systemOverlayInputRect: Rect, - val resetRect: Rect + val resetRect: Rect, ) class OverlayDebugControlHost( - private val state: OverlayLayerDebugState = OverlayLayerDebugState + private val state: OverlayLayerDebugState = OverlayLayerDebugState, ) { private data class ToggleSnapshot( val applicationOverlayRenderEnabled: Boolean, @@ -41,17 +41,18 @@ class OverlayDebugControlHost( val applicationOverlayInputEnabled: Boolean, val systemOverlayRenderEnabled: Boolean, val systemOverlayTintEnabled: Boolean, - val systemOverlayInputEnabled: Boolean + val systemOverlayInputEnabled: Boolean, ) private var viewportWidth: Int = 1 private var viewportHeight: Int = 1 private var layout: OverlayDebugControlLayout? = null private val rootNode: OverlayDebugControlRootNode = OverlayDebugControlRootNode() - private val tree: DomTree = DomTree( - root = rootNode, - styleScope = StyleApplicationScope.SystemOverlay - ) + private val tree: DomTree = + DomTree( + root = rootNode, + styleScope = StyleApplicationScope.SystemOverlay, + ) private var lastToggleSnapshot: ToggleSnapshot? = null fun render(viewportWidth: Int, viewportHeight: Int) { @@ -150,12 +151,13 @@ class OverlayDebugControlHost( val panelHeight = 176 + 56 val panelX = 8 val panelY = (viewportHeight - panelHeight - 8).coerceAtLeast(8) - val panelRect = Rect( - x = panelX, - y = panelY, - width = panelWidth.coerceAtMost((viewportWidth - 16).coerceAtLeast(120)), - height = panelHeight.coerceAtMost((viewportHeight - 16).coerceAtLeast(96)) - ) + val panelRect = + Rect( + x = panelX, + y = panelY, + width = panelWidth.coerceAtMost((viewportWidth - 16).coerceAtLeast(120)), + height = panelHeight.coerceAtMost((viewportHeight - 16).coerceAtLeast(96)), + ) val toggleWidth = 56 val toggleHeight = 18 val toggleX = panelRect.x + panelRect.width - toggleWidth - 10 @@ -170,53 +172,58 @@ class OverlayDebugControlHost( systemOverlayRenderRect = Rect(toggleX, firstY + rowStep * row++, toggleWidth, toggleHeight), systemOverlayTintRect = Rect(toggleX, firstY + rowStep * row++, toggleWidth, toggleHeight), systemOverlayInputRect = Rect(toggleX, firstY + rowStep * row++, toggleWidth, toggleHeight), - resetRect = Rect( - x = panelRect.x + 10, - y = panelRect.y + panelRect.height - 40, - width = panelRect.width - 20, - height = 20 - ) + resetRect = + Rect( + x = panelRect.x + 10, + y = panelRect.y + panelRect.height - 40, + width = panelRect.width - 20, + height = 20, + ), ) } - private fun OverlayLayerDebugSnapshot.toggleSnapshot(): ToggleSnapshot { - return ToggleSnapshot( + private fun OverlayLayerDebugSnapshot.toggleSnapshot(): ToggleSnapshot = + ToggleSnapshot( applicationOverlayRenderEnabled = applicationOverlayRenderEnabled, applicationOverlayTintEnabled = applicationOverlayTintEnabled, applicationOverlayInputEnabled = applicationOverlayInputEnabled, systemOverlayRenderEnabled = systemOverlayRenderEnabled, systemOverlayTintEnabled = systemOverlayTintEnabled, - systemOverlayInputEnabled = systemOverlayInputEnabled + systemOverlayInputEnabled = systemOverlayInputEnabled, ) - } } private class OverlayDebugControlRootNode( - key: Any? = "dsgl-overlay-debug-root" + key: Any? = "dsgl-overlay-debug-root", ) : DOMNode(key) { override val styleType: String = "dsgl-overlay-debug-root" private val scope = UiScope(this) - private val shadowNode: ContainerNode = scope.div({ - this.key = "dsgl-overlay-debug-shadow" - }) - private val panelNode: ContainerNode = scope.div({ - this.key = "dsgl-overlay-debug-panel" - }) - private val titleNode: TextNode = scope.text(props = { - this.key = "dsgl-overlay-debug-title" - source = TextSource.Static("Overlay Debug") - style = { - textWrap = TextWrap.NoWrap - } - }) + private val shadowNode: ContainerNode = + scope.div({ + this.key = "dsgl-overlay-debug-shadow" + }) + private val panelNode: ContainerNode = + scope.div({ + this.key = "dsgl-overlay-debug-panel" + }) + private val titleNode: TextNode = + scope.text(props = { + this.key = "dsgl-overlay-debug-title" + source = TextSource.Static("Overlay Debug") + style = { + textWrap = TextWrap.NoWrap + } + }) private val appRenderLabelNode: TextNode = labelNode("App Overlay Render", "dsgl-overlay-debug-label-app-render") private val appTintLabelNode: TextNode = labelNode("App Overlay Tint", "dsgl-overlay-debug-label-app-tint") private val appInputLabelNode: TextNode = labelNode("App Overlay Input", "dsgl-overlay-debug-label-app-input") - private val systemRenderLabelNode: TextNode = labelNode("System Overlay Render", "dsgl-overlay-debug-label-system-render") + private val systemRenderLabelNode: TextNode = + labelNode("System Overlay Render", "dsgl-overlay-debug-label-system-render") private val systemTintLabelNode: TextNode = labelNode("System Overlay Tint", "dsgl-overlay-debug-label-system-tint") - private val systemInputLabelNode: TextNode = labelNode("System Overlay Input", "dsgl-overlay-debug-label-system-input") + private val systemInputLabelNode: TextNode = + labelNode("System Overlay Input", "dsgl-overlay-debug-label-system-input") private val appRenderToggleNode: ButtonNode = toggleNode("dsgl-overlay-debug-toggle-app-render") private val appTintToggleNode: ButtonNode = toggleNode("dsgl-overlay-debug-toggle-app-tint") @@ -225,50 +232,59 @@ private class OverlayDebugControlRootNode( private val systemTintToggleNode: ButtonNode = toggleNode("dsgl-overlay-debug-toggle-system-tint") private val systemInputToggleNode: ButtonNode = toggleNode("dsgl-overlay-debug-toggle-system-input") - private val resetButtonNode: ButtonNode = scope.button("Reset All", { - this.key = "dsgl-overlay-debug-reset" - style = { - textWrap = TextWrap.NoWrap - } - }) + private val resetButtonNode: ButtonNode = + scope.button("Reset All", { + this.key = "dsgl-overlay-debug-reset" + style = { + textWrap = TextWrap.NoWrap + } + }) - private val statusNode: TextNode = scope.text(props = { - this.key = "dsgl-overlay-debug-status" - source = TextSource.Static("") - style = { - textWrap = TextWrap.NoWrap - } - }) + private val statusNode: TextNode = + scope.text(props = { + this.key = "dsgl-overlay-debug-status" + source = TextSource.Static("") + style = { + textWrap = TextWrap.NoWrap + } + }) private var layout: OverlayDebugControlLayout? = null - private var snapshot: OverlayLayerDebugSnapshot = OverlayLayerDebugSnapshot( - applicationOverlayRenderEnabled = true, - applicationOverlayTintEnabled = false, - applicationOverlayInputEnabled = true, - systemOverlayRenderEnabled = true, - systemOverlayTintEnabled = false, - systemOverlayInputEnabled = true, - frameFps = 0, - frameTimeMs = 0f, - frameFpsWindow = 0, - frameTimeWindowMs = 0f - ) + private var snapshot: OverlayLayerDebugSnapshot = + OverlayLayerDebugSnapshot( + applicationOverlayRenderEnabled = true, + applicationOverlayTintEnabled = false, + applicationOverlayInputEnabled = true, + systemOverlayRenderEnabled = true, + systemOverlayTintEnabled = false, + systemOverlayInputEnabled = true, + frameFps = 0, + frameTimeMs = 0f, + frameFpsWindow = 0, + frameTimeWindowMs = 0f, + ) fun bind(layout: OverlayDebugControlLayout, snapshot: OverlayLayerDebugSnapshot) { this.layout = layout this.snapshot = snapshot } - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } + override fun measure(ctx: UiMeasureContext): Size = + Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) - val localLayout = layout ?: run { - hideAll(ctx) - return - } + val localLayout = + layout ?: run { + hideAll(ctx) + return + } val panelRect = localLayout.panelRect val shadowRect = Rect(panelRect.x + 2, panelRect.y + 2, panelRect.width, panelRect.height) @@ -301,9 +317,12 @@ private class OverlayDebugControlRootNode( resetButtonNode.textColor = 0xFFFFFFFF.toInt() resetButtonNode.fontSize = 14 + val rApp = if (snapshot.applicationOverlayRenderEnabled) "A1" else "A0" + val rSys = if (snapshot.systemOverlayRenderEnabled) "S1" else "S0" + val iApp = if (snapshot.applicationOverlayInputEnabled) "A1" else "A0" + val iSys = if (snapshot.systemOverlayInputEnabled) "S1" else "S0" val statusTextValue = - "R:${if (snapshot.applicationOverlayRenderEnabled) "A1" else "A0"}/${if (snapshot.systemOverlayRenderEnabled) "S1" else "S0"} " + - "I:${if (snapshot.applicationOverlayInputEnabled) "A1" else "A0"}/${if (snapshot.systemOverlayInputEnabled) "S1" else "S0"} " + + "R:$rApp/$rSys I:$iApp/$iSys " + "FPS:${snapshot.frameFps} (${String.format(Locale.US, "%.1f", snapshot.frameTimeMs)}ms) " + "AvgFPS:${snapshot.frameFpsWindow} (${String.format(Locale.US, "%.1f", snapshot.frameTimeWindowMs)}ms)" statusNode.setText(statusTextValue) @@ -317,7 +336,13 @@ private class OverlayDebugControlRootNode( renderToggleRow(ctx, panelRect, localLayout.appOverlayRenderRect, appRenderLabelNode, appRenderToggleNode) renderToggleRow(ctx, panelRect, localLayout.appOverlayTintRect, appTintLabelNode, appTintToggleNode) renderToggleRow(ctx, panelRect, localLayout.appOverlayInputRect, appInputLabelNode, appInputToggleNode) - renderToggleRow(ctx, panelRect, localLayout.systemOverlayRenderRect, systemRenderLabelNode, systemRenderToggleNode) + renderToggleRow( + ctx, + panelRect, + localLayout.systemOverlayRenderRect, + systemRenderLabelNode, + systemRenderToggleNode, + ) renderToggleRow(ctx, panelRect, localLayout.systemOverlayTintRect, systemTintLabelNode, systemTintToggleNode) renderToggleRow(ctx, panelRect, localLayout.systemOverlayInputRect, systemInputLabelNode, systemInputToggleNode) @@ -329,29 +354,27 @@ private class OverlayDebugControlRootNode( x = panelRect.x + 10, y = panelRect.y + panelRect.height - 14, width = (panelRect.width - 20).coerceAtLeast(1), - height = 14 - ) + height = 14, + ), ) } - private fun labelNode(text: String, key: Any): TextNode { - return scope.text(props = { + private fun labelNode(text: String, key: Any): TextNode = + scope.text(props = { this.key = key source = TextSource.Static(text) style = { textWrap = TextWrap.NoWrap } }) - } - private fun toggleNode(key: Any): ButtonNode { - return scope.button("ON", { + private fun toggleNode(key: Any): ButtonNode = + scope.button("ON", { this.key = key style = { textWrap = TextWrap.NoWrap } }) - } private fun applyLabelStyle(node: TextNode) { node.color = 0xFFE0E9F2.toInt() @@ -371,14 +394,15 @@ private class OverlayDebugControlRootNode( panelRect: Rect, toggleRect: Rect, labelNode: TextNode, - toggleNode: ButtonNode + toggleNode: ButtonNode, ) { - val labelRect = Rect( - x = panelRect.x + 10, - y = toggleRect.y + 2, - width = (toggleRect.x - (panelRect.x + 16)).coerceAtLeast(1), - height = (toggleRect.height - 2).coerceAtLeast(1) - ) + val labelRect = + Rect( + x = panelRect.x + 10, + y = toggleRect.y + 2, + width = (toggleRect.x - (panelRect.x + 16)).coerceAtLeast(1), + height = (toggleRect.height - 2).coerceAtLeast(1), + ) renderNode(ctx, labelNode, labelRect) renderNode(ctx, toggleNode, toggleRect) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayLayerDebugState.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayLayerDebugState.kt index 57b1d15..4b007dd 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayLayerDebugState.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayLayerDebugState.kt @@ -12,7 +12,7 @@ data class OverlayLayerDebugSnapshot( val frameFps: Int, val frameTimeMs: Float, val frameFpsWindow: Int, - val frameTimeWindowMs: Float + val frameTimeWindowMs: Float, ) object OverlayLayerDebugState { @@ -24,6 +24,7 @@ object OverlayLayerDebugState { @Volatile var applicationOverlayRenderEnabled: Boolean = true + @Volatile var applicationOverlayTintEnabled: Boolean = false @@ -53,35 +54,33 @@ object OverlayLayerDebugState { val controlsEnabled: Boolean get() { - return controlsEnabledOverride ?: java.lang.Boolean.getBoolean("dsgl.overlay.controls") + return controlsEnabledOverride ?: java.lang.Boolean + .getBoolean("dsgl.overlay.controls") } - fun isRenderEnabled(layer: UiLayerId): Boolean { - return when (layer) { + fun isRenderEnabled(layer: UiLayerId): Boolean = + when (layer) { UiLayerId.ApplicationOverlay -> applicationOverlayRenderEnabled UiLayerId.SystemOverlay -> systemOverlayRenderEnabled UiLayerId.Debug -> true UiLayerId.ApplicationRoot -> true } - } - fun isTintEnabled(layer: UiLayerId): Boolean { - return when (layer) { + fun isTintEnabled(layer: UiLayerId): Boolean = + when (layer) { UiLayerId.ApplicationOverlay -> applicationOverlayTintEnabled UiLayerId.SystemOverlay -> systemOverlayTintEnabled UiLayerId.Debug -> true UiLayerId.ApplicationRoot -> true } - } - fun isInputEnabled(layer: UiLayerId): Boolean { - return when (layer) { + fun isInputEnabled(layer: UiLayerId): Boolean = + when (layer) { UiLayerId.ApplicationOverlay -> applicationOverlayInputEnabled UiLayerId.SystemOverlay -> systemOverlayInputEnabled UiLayerId.Debug -> true UiLayerId.ApplicationRoot -> true } - } fun resetAll() { applicationOverlayRenderEnabled = true @@ -97,14 +96,19 @@ object OverlayLayerDebugState { frameTimeWindowWriteIndex = 0 frameTimeWindowCount = 0 frameTimeWindowSumSeconds = 0.0 - java.util.Arrays.fill(frameTimeWindowSeconds, 0.0) + java.util.Arrays + .fill(frameTimeWindowSeconds, 0.0) } @Synchronized fun updateFrameTiming(dtSeconds: Double) { val safeDt = dtSeconds.coerceAtLeast(1.0 / 1000.0) frameTimeMs = (safeDt * 1000.0).toFloat() - frameFps = kotlin.math.round(1.0 / safeDt).toInt().coerceAtLeast(0) + frameFps = + kotlin.math + .round(1.0 / safeDt) + .toInt() + .coerceAtLeast(0) if (frameTimeWindowCount == FRAME_TIMING_WINDOW_SIZE) { frameTimeWindowSumSeconds -= frameTimeWindowSeconds[frameTimeWindowWriteIndex] @@ -115,17 +119,22 @@ object OverlayLayerDebugState { frameTimeWindowSumSeconds += safeDt frameTimeWindowWriteIndex = (frameTimeWindowWriteIndex + 1) % FRAME_TIMING_WINDOW_SIZE - val averageDt = if (frameTimeWindowCount > 0) { - frameTimeWindowSumSeconds / frameTimeWindowCount.toDouble() - } else { - safeDt - } + val averageDt = + if (frameTimeWindowCount > 0) { + frameTimeWindowSumSeconds / frameTimeWindowCount.toDouble() + } else { + safeDt + } frameTimeWindowMs = (averageDt * 1000.0).toFloat() - frameFpsWindow = kotlin.math.round(1.0 / averageDt).toInt().coerceAtLeast(0) + frameFpsWindow = + kotlin.math + .round(1.0 / averageDt) + .toInt() + .coerceAtLeast(0) } - fun snapshot(): OverlayLayerDebugSnapshot { - return OverlayLayerDebugSnapshot( + fun snapshot(): OverlayLayerDebugSnapshot = + OverlayLayerDebugSnapshot( applicationOverlayRenderEnabled = applicationOverlayRenderEnabled, applicationOverlayTintEnabled = applicationOverlayTintEnabled, applicationOverlayInputEnabled = applicationOverlayInputEnabled, @@ -135,9 +144,8 @@ object OverlayLayerDebugState { frameFps = frameFps, frameTimeMs = frameTimeMs, frameFpsWindow = frameFpsWindow, - frameTimeWindowMs = frameTimeWindowMs + frameTimeWindowMs = frameTimeWindowMs, ) - } private var controlsEnabledOverride: Boolean? = null diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/ScrollPerformanceCounters.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/ScrollPerformanceCounters.kt index 10bbaa4..9a99a1e 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/ScrollPerformanceCounters.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/ScrollPerformanceCounters.kt @@ -22,7 +22,7 @@ data class ScrollPerformanceSnapshot( val paintTotalNanos: Long, val styleApplyNanos: Long, val fullRerenderLayoutNanos: Long, - val chunkRebuildNanos: Long + val chunkRebuildNanos: Long, ) object ScrollPerformanceCounters { @@ -115,8 +115,8 @@ object ScrollPerformanceCounters { chunkRebuildNanos.addAndGet(durationNanos.coerceAtLeast(0L)) } - fun snapshot(): ScrollPerformanceSnapshot { - return ScrollPerformanceSnapshot( + fun snapshot(): ScrollPerformanceSnapshot = + ScrollPerformanceSnapshot( paintCalls = paintCalls.get(), guardedScrollVisualFastPathRuns = guardedScrollVisualFastPathRuns.get(), scrollVisualGeometryRefreshRuns = scrollVisualGeometryRefreshRuns.get(), @@ -136,9 +136,8 @@ object ScrollPerformanceCounters { paintTotalNanos = paintTotalNanos.get(), styleApplyNanos = styleApplyNanos.get(), fullRerenderLayoutNanos = fullRerenderLayoutNanos.get(), - chunkRebuildNanos = chunkRebuildNanos.get() + chunkRebuildNanos = chunkRebuildNanos.get(), ) - } fun resetForTests() { paintCalls.set(0L) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndBindings.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndBindings.kt index e90a80d..9975c53 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndBindings.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndBindings.kt @@ -9,7 +9,7 @@ data class DndListeners( val onDragEnter: ((DragEnterEvent) -> Unit)? = null, val onDragOver: ((DragOverEvent) -> Unit)? = null, val onDragLeave: ((DragLeaveEvent) -> Unit)? = null, - val onDrop: ((DropEvent) -> Unit)? = null + val onDrop: ((DropEvent) -> Unit)? = null, ) fun ComponentProps.applyDndListeners(listeners: DndListeners) { @@ -22,26 +22,22 @@ fun ComponentProps.applyDndListeners(listeners: DndListeners) { onDrop = chain(onDrop, listeners.onDrop) } -fun mergeDndListeners(first: DndListeners, second: DndListeners): DndListeners { - return DndListeners( +fun mergeDndListeners(first: DndListeners, second: DndListeners): DndListeners = + DndListeners( onDragStart = chain(first.onDragStart, second.onDragStart), onDrag = chain(first.onDrag, second.onDrag), onDragEnd = chain(first.onDragEnd, second.onDragEnd), onDragEnter = chain(first.onDragEnter, second.onDragEnter), onDragOver = chain(first.onDragOver, second.onDragOver), onDragLeave = chain(first.onDragLeave, second.onDragLeave), - onDrop = chain(first.onDrop, second.onDrop) + onDrop = chain(first.onDrop, second.onDrop), ) -} -private fun chain( - first: ((T) -> Unit)?, - second: ((T) -> Unit)? -): ((T) -> Unit)? { +private fun chain(first: ((T) -> Unit)?, second: ((T) -> Unit)?): ((T) -> Unit)? { if (first == null) return second if (second == null) return first return { event -> first(event) second(event) } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndDescriptors.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndDescriptors.kt index b6e15d2..fc98438 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndDescriptors.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndDescriptors.kt @@ -15,7 +15,7 @@ data class Draggable( val previewMode: DragPreviewMode, val hideSourceWhileDragging: Boolean, val renderPreview: (DragPreviewScope.() -> Unit)?, - val renderPlaceholder: (PlaceholderScope.() -> Unit)? + val renderPlaceholder: (PlaceholderScope.() -> Unit)?, ) data class Droppable( @@ -24,7 +24,7 @@ data class Droppable( val isOver: Boolean, val active: ActiveDrag?, val listeners: DndListeners, - val setNodeRef: RefTarget + val setNodeRef: RefTarget, ) data class Sortable( @@ -36,5 +36,5 @@ data class Sortable( val isOver: Boolean, val overId: String?, val projection: SortableProjection, - val listeners: DndListeners -) \ No newline at end of file + val listeners: DndListeners, +) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndHooks.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndHooks.kt index e6e6bc2..6c1e86a 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndHooks.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndHooks.kt @@ -2,42 +2,44 @@ package org.dreamfinity.dsgl.core.dnd import org.dreamfinity.dsgl.core.DsglWindow import org.dreamfinity.dsgl.core.dsl.UiScope -import org.dreamfinity.dsgl.core.hooks.useEffect import org.dreamfinity.dsgl.core.hooks.ref.ElementHandle import org.dreamfinity.dsgl.core.hooks.ref.useRef +import org.dreamfinity.dsgl.core.hooks.useEffect import java.util.WeakHashMap private data class SortableState( var activeId: String? = null, var overId: String? = null, - var insertPosition: InsertPosition = InsertPosition.APPEND + var insertPosition: InsertPosition = InsertPosition.APPEND, ) private data class SortableContainerRecord( val state: SortableState, - var activeHookCount: Int = 0 + var activeHookCount: Int = 0, ) private data class SortableWindowState( - val byContainerId: MutableMap = linkedMapOf() + val byContainerId: MutableMap = linkedMapOf(), ) private data class SortableBindingKey( val containerId: String, - val nodeKey: Any + val nodeKey: Any, ) private val sortableStateByWindow: WeakHashMap = WeakHashMap() -private fun sortableWindowState(window: DsglWindow): SortableWindowState { - return sortableStateByWindow.getOrPut(window) { SortableWindowState() } -} +private fun sortableWindowState(window: DsglWindow): SortableWindowState = + sortableStateByWindow.getOrPut(window) { + SortableWindowState() + } private fun sortableState(window: DsglWindow, containerId: String): SortableState { val state = sortableWindowState(window) - val record = state.byContainerId.getOrPut(containerId) { - SortableContainerRecord(state = SortableState()) - } + val record = + state.byContainerId.getOrPut(containerId) { + SortableContainerRecord(state = SortableState()) + } pruneUnboundSortableContainers(state = state, keepContainerId = containerId) return record.state } @@ -46,10 +48,11 @@ private fun retainSortableContainer(window: DsglWindow, containerId: String, ren val state = sortableWindowState(window) val existing = state.byContainerId[containerId] if (existing == null) { - state.byContainerId[containerId] = SortableContainerRecord( - state = renderState, - activeHookCount = 1 - ) + state.byContainerId[containerId] = + SortableContainerRecord( + state = renderState, + activeHookCount = 1, + ) } else { existing.activeHookCount += 1 } @@ -70,7 +73,9 @@ private fun releaseSortableContainer(window: DsglWindow, containerId: String) { } private fun pruneUnboundSortableContainers(state: SortableWindowState, keepContainerId: String?) { - val iterator = state.byContainerId.entries.iterator() + val iterator = + state.byContainerId.entries + .iterator() while (iterator.hasNext()) { val entry = iterator.next() if (entry.key == keepContainerId) { @@ -93,9 +98,9 @@ fun UiScope.useDraggable( renderPlaceholder: (PlaceholderScope.() -> Unit)? = null, onDragStart: ((DragStartEvent) -> Unit)? = null, onDrag: ((DragEvent) -> Unit)? = null, - onDragEnd: ((DragEndEvent) -> Unit)? = null -): Draggable { - return requireHookOwnerWindow().useDraggable( + onDragEnd: ((DragEndEvent) -> Unit)? = null, +): Draggable = + requireHookOwnerWindow().useDraggable( id = id, nodeKey = nodeKey, type = type, @@ -106,9 +111,8 @@ fun UiScope.useDraggable( renderPlaceholder = renderPlaceholder, onDragStart = onDragStart, onDrag = onDrag, - onDragEnd = onDragEnd + onDragEnd = onDragEnd, ) -} internal fun DsglWindow.useDraggable( id: String, @@ -121,39 +125,42 @@ internal fun DsglWindow.useDraggable( renderPlaceholder: (PlaceholderScope.() -> Unit)? = null, onDragStart: ((DragStartEvent) -> Unit)? = null, onDrag: ((DragEvent) -> Unit)? = null, - onDragEnd: ((DragEndEvent) -> Unit)? = null -): Draggable { - return hookRuntime().withComponentInstance(componentName = "useDraggable", key = nodeKey) { + onDragEnd: ((DragEndEvent) -> Unit)? = null, +): Draggable = + hookRuntime().withComponentInstance(componentName = "useDraggable", key = nodeKey) { DndSystem.registerPayload(id, data) val ref by useRef() val monitor = DndSystem.monitor(nodeKey) val isDragging = monitor.isDragging && monitor.sourceKey == nodeKey - val transform = if (isDragging) { - Transform( - x = monitor.previewX - monitor.cursorX.toDouble(), - y = monitor.previewY - monitor.cursorY.toDouble() - ) - } else { - null - } + val transform = + if (isDragging) { + Transform( + x = monitor.previewX - monitor.cursorX.toDouble(), + y = monitor.previewY - monitor.cursorY.toDouble(), + ) + } else { + null + } - val listeners = DndListeners( - onDragStart = { event -> - event.dataTransfer.setData(DND_DATA_ID_MIME, id) - event.dataTransfer.setData(DND_DATA_TYPE_MIME, type) - onDragStart?.invoke(event) - }, - onDrag = onDrag, - onDragEnd = onDragEnd - ) + val listeners = + DndListeners( + onDragStart = { event -> + event.dataTransfer.setData(DND_DATA_ID_MIME, id) + event.dataTransfer.setData(DND_DATA_TYPE_MIME, type) + onDragStart?.invoke(event) + }, + onDrag = onDrag, + onDragEnd = onDragEnd, + ) Draggable( id = id, nodeKey = nodeKey, - attributes = mapOf( - "data-dnd-id" to id, - "data-dnd-type" to type - ), + attributes = + mapOf( + "data-dnd-id" to id, + "data-dnd-type" to type, + ), listeners = listeners, isDragging = isDragging, activeTransform = transform, @@ -162,10 +169,9 @@ internal fun DsglWindow.useDraggable( previewMode = previewMode, hideSourceWhileDragging = hideSourceWhileDragging, renderPreview = renderPreview, - renderPlaceholder = renderPlaceholder + renderPlaceholder = renderPlaceholder, ) } -} fun UiScope.useDroppable( id: String, @@ -174,18 +180,17 @@ fun UiScope.useDroppable( onDragOver: ((DragOverEvent, ActiveDrag?) -> Unit)? = null, onDrop: ((DropEvent, ActiveDrag?) -> Unit)? = null, onDragEnter: ((DragEnterEvent, ActiveDrag?) -> Unit)? = null, - onDragLeave: ((DragLeaveEvent, ActiveDrag?) -> Unit)? = null -): Droppable { - return requireHookOwnerWindow().useDroppable( + onDragLeave: ((DragLeaveEvent, ActiveDrag?) -> Unit)? = null, +): Droppable = + requireHookOwnerWindow().useDroppable( id = id, nodeKey = nodeKey, accepts = accepts, onDragOver = onDragOver, onDrop = onDrop, onDragEnter = onDragEnter, - onDragLeave = onDragLeave + onDragLeave = onDragLeave, ) -} internal fun DsglWindow.useDroppable( id: String, @@ -194,43 +199,43 @@ internal fun DsglWindow.useDroppable( onDragOver: ((DragOverEvent, ActiveDrag?) -> Unit)? = null, onDrop: ((DropEvent, ActiveDrag?) -> Unit)? = null, onDragEnter: ((DragEnterEvent, ActiveDrag?) -> Unit)? = null, - onDragLeave: ((DragLeaveEvent, ActiveDrag?) -> Unit)? = null -): Droppable { - return hookRuntime().withComponentInstance(componentName = "useDroppable", key = nodeKey) { + onDragLeave: ((DragLeaveEvent, ActiveDrag?) -> Unit)? = null, +): Droppable = + hookRuntime().withComponentInstance(componentName = "useDroppable", key = nodeKey) { val ref by useRef() val active = DndSystem.activeDrag() val isOver = active?.overKey == nodeKey - val listeners = DndListeners( - onDragEnter = { event -> - val snapshot = activeFromEvent(event) - onDragEnter?.invoke(event, snapshot) - }, - onDragOver = { event -> - val snapshot = activeFromEvent(event) - if (accepts(snapshot)) { - event.acceptDrop(snapshot.dropEffect) - } - onDragOver?.invoke(event, snapshot) - }, - onDragLeave = { event -> - val snapshot = activeFromEvent(event) - onDragLeave?.invoke(event, snapshot) - }, - onDrop = { event -> - val snapshot = activeFromEvent(event) - onDrop?.invoke(event, snapshot) - } - ) + val listeners = + DndListeners( + onDragEnter = { event -> + val snapshot = activeFromEvent(event) + onDragEnter?.invoke(event, snapshot) + }, + onDragOver = { event -> + val snapshot = activeFromEvent(event) + if (accepts(snapshot)) { + event.acceptDrop(snapshot.dropEffect) + } + onDragOver?.invoke(event, snapshot) + }, + onDragLeave = { event -> + val snapshot = activeFromEvent(event) + onDragLeave?.invoke(event, snapshot) + }, + onDrop = { event -> + val snapshot = activeFromEvent(event) + onDrop?.invoke(event, snapshot) + }, + ) Droppable( id = id, nodeKey = nodeKey, isOver = isOver, active = active, listeners = listeners, - setNodeRef = { value -> ref.current = value } + setNodeRef = { value -> ref.current = value }, ) } -} fun UiScope.useSortable( id: String, @@ -239,18 +244,17 @@ fun UiScope.useSortable( items: List, data: Any? = null, previewMode: DragPreviewMode = DragPreviewMode.ORIGINAL, - hideSourceWhileDragging: Boolean = true -): Sortable { - return requireHookOwnerWindow().useSortable( + hideSourceWhileDragging: Boolean = true, +): Sortable = + requireHookOwnerWindow().useSortable( id = id, nodeKey = nodeKey, containerId = containerId, items = items, data = data, previewMode = previewMode, - hideSourceWhileDragging = hideSourceWhileDragging + hideSourceWhileDragging = hideSourceWhileDragging, ) -} internal fun DsglWindow.useSortable( id: String, @@ -259,18 +263,18 @@ internal fun DsglWindow.useSortable( items: List, data: Any? = null, previewMode: DragPreviewMode = DragPreviewMode.ORIGINAL, - hideSourceWhileDragging: Boolean = true + hideSourceWhileDragging: Boolean = true, ): Sortable { val state = sortableState(this, containerId) hookRuntime().withComponentInstance( componentName = "useSortableBinding", - key = SortableBindingKey(containerId = containerId, nodeKey = nodeKey) + key = SortableBindingKey(containerId = containerId, nodeKey = nodeKey), ) { useEffect { retainSortableContainer( window = this@useSortable, containerId = containerId, - renderState = state + renderState = state, ) onDispose { releaseSortableContainer(window = this@useSortable, containerId = containerId) @@ -278,54 +282,65 @@ internal fun DsglWindow.useSortable( } } val sortableType = "sortable:$containerId" - val draggable = useDraggable( - id = id, - nodeKey = nodeKey, - type = sortableType, - data = data, - previewMode = previewMode, - hideSourceWhileDragging = hideSourceWhileDragging, - onDragStart = { - state.activeId = id - state.overId = null - state.insertPosition = InsertPosition.APPEND - }, - onDragEnd = { - state.activeId = null - state.overId = null - state.insertPosition = InsertPosition.APPEND - } - ) - val droppable = useDroppable( - id = id, - nodeKey = nodeKey, - accepts = { active -> active.type == sortableType && active.id != id }, - onDragOver = { event, active -> - if (active == null || active.id == id) return@useDroppable - state.overId = id - state.insertPosition = if (event.mouseY >= event.target!!.bounds.y + event.target!!.bounds.height / 2) { - InsertPosition.AFTER - } else { - InsertPosition.BEFORE - } - }, - onDrop = { event, active -> - if (active == null || active.id == id) return@useDroppable - state.overId = id - state.insertPosition = if (event.mouseY >= event.target!!.bounds.y + event.target!!.bounds.height / 2) { - InsertPosition.AFTER - } else { - InsertPosition.BEFORE - } - } - ) + val draggable = + useDraggable( + id = id, + nodeKey = nodeKey, + type = sortableType, + data = data, + previewMode = previewMode, + hideSourceWhileDragging = hideSourceWhileDragging, + onDragStart = { + state.activeId = id + state.overId = null + state.insertPosition = InsertPosition.APPEND + }, + onDragEnd = { + state.activeId = null + state.overId = null + state.insertPosition = InsertPosition.APPEND + }, + ) + val droppable = + useDroppable( + id = id, + nodeKey = nodeKey, + accepts = { active -> active.type == sortableType && active.id != id }, + onDragOver = { event, active -> + if (active == null || active.id == id) return@useDroppable + state.overId = id + state.insertPosition = + if (event.mouseY >= event.target!! + .bounds.y + event.target!! + .bounds.height / 2 + ) { + InsertPosition.AFTER + } else { + InsertPosition.BEFORE + } + }, + onDrop = { event, active -> + if (active == null || active.id == id) return@useDroppable + state.overId = id + state.insertPosition = + if (event.mouseY >= event.target!! + .bounds.y + event.target!! + .bounds.height / 2 + ) { + InsertPosition.AFTER + } else { + InsertPosition.BEFORE + } + }, + ) - val projected = SortableProjection( - activeId = state.activeId, - overId = state.overId, - insertPosition = state.insertPosition, - newIndex = projectedIndex(items, state.activeId, state.overId, state.insertPosition) - ) + val projected = + SortableProjection( + activeId = state.activeId, + overId = state.overId, + insertPosition = state.insertPosition, + newIndex = projectedIndex(items, state.activeId, state.overId, state.insertPosition), + ) val mergedListeners = mergeDndListeners(draggable.listeners, droppable.listeners) return Sortable( id = id, @@ -336,7 +351,7 @@ internal fun DsglWindow.useSortable( isOver = droppable.isOver, overId = state.overId, projection = projected, - listeners = mergedListeners + listeners = mergedListeners, ) } @@ -345,27 +360,40 @@ fun UiScope.useDragDropMonitor(callbacks: DragDropMonitorCallbacks) { val callbackRef by useRef(callbacks) callbackRef.current = callbacks useEffect { - val subscription = DndRuntime.engine.subscribe(object : DndMonitorListener { - override fun onDragStart(active: ActiveDrag) { - callbackRef.current?.onDragStart?.invoke(active) - } + val subscription = + DndRuntime.engine.subscribe( + object : DndMonitorListener { + override fun onDragStart(active: ActiveDrag) { + callbackRef.current + ?.onDragStart + ?.invoke(active) + } - override fun onDragMove(active: ActiveDrag, over: Any?) { - callbackRef.current?.onDragMove?.invoke(active, over) - } + override fun onDragMove(active: ActiveDrag, over: Any?) { + callbackRef.current + ?.onDragMove + ?.invoke(active, over) + } - override fun onDragOver(active: ActiveDrag, over: Any?) { - callbackRef.current?.onDragOver?.invoke(active, over) - } + override fun onDragOver(active: ActiveDrag, over: Any?) { + callbackRef.current + ?.onDragOver + ?.invoke(active, over) + } - override fun onDragEnd(active: ActiveDrag, over: Any?, dropEffect: DropEffect) { - callbackRef.current?.onDragEnd?.invoke(active, over, dropEffect) - } + override fun onDragEnd(active: ActiveDrag, over: Any?, dropEffect: DropEffect) { + callbackRef.current + ?.onDragEnd + ?.invoke(active, over, dropEffect) + } - override fun onDragCancel(active: ActiveDrag) { - callbackRef.current?.onDragCancel?.invoke(active) - } - }) + override fun onDragCancel(active: ActiveDrag) { + callbackRef.current + ?.onDragCancel + ?.invoke(active) + } + }, + ) onDispose { subscription.close() } @@ -387,7 +415,7 @@ private fun activeFromEvent(event: DragDropEvent): ActiveDrag { cursorY = event.mouseY, transform = Transform(0.0, 0.0), dropEffect = event.dataTransfer.dropEffect, - dataTransfer = event.dataTransfer + dataTransfer = event.dataTransfer, ) } @@ -395,14 +423,15 @@ private fun projectedIndex( items: List, activeId: String?, overId: String?, - insertPosition: InsertPosition + insertPosition: InsertPosition, ): Int? { if (activeId == null) return null - val moved = reorderByDnD( - items = items, - activeId = activeId, - overId = overId, - insertPosition = insertPosition - ) { value -> value } + val moved = + reorderByDnD( + items = items, + activeId = activeId, + overId = overId, + insertPosition = insertPosition, + ) { value -> value } return moved.indexOf(activeId).takeIf { it >= 0 } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndInterfaces.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndInterfaces.kt index d70ef6e..8d2bc7e 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndInterfaces.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndInterfaces.kt @@ -16,7 +16,7 @@ data class DndMonitorState( val previewY: Double, val mode: DragPreviewMode?, val collisionCandidates: Int, - val sourceExcludedFromHitTest: Boolean + val sourceExcludedFromHitTest: Boolean, ) interface DndMonitorRegistry { @@ -27,12 +27,13 @@ interface DndHitTester interface DndOverlayRenderer { fun appendPlaceholderCommands(out: MutableList) + fun appendOverlayCommands( root: DOMNode, ctx: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, - out: MutableList + out: MutableList, ) } @@ -40,20 +41,30 @@ interface DndClock { fun onFrame(root: DOMNode, dtSeconds: Double) } -interface DndEngine : DndMonitorRegistry, DndOverlayRenderer, DndClock { +interface DndEngine : + DndMonitorRegistry, + DndOverlayRenderer, + DndClock { val isDragging: Boolean val isPointerCaptured: Boolean fun monitor(nodeKey: Any? = null): DndMonitorState + fun activeDrag(): ActiveDrag? fun setSmoothingFactor(value: Double) + fun getSmoothingFactor(): Double + fun isDraggingNode(nodeKey: Any?): Boolean fun onMouseDown(root: DOMNode, target: DOMNode?, event: MouseDownEvent) + fun onMouseMove(root: DOMNode, mouseX: Int, mouseY: Int) + fun onMouseUp(root: DOMNode, event: MouseUpEvent): Boolean + fun rebindAfterReconcile(root: DOMNode) + fun cancelActiveDrag() } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndModels.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndModels.kt index 5c946c2..c798f43 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndModels.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndModels.kt @@ -5,7 +5,7 @@ const val DND_DATA_TYPE_MIME: String = "application/x-dsgl-dnd-type" data class Transform( val x: Double, - val y: Double + val y: Double, ) data class ActiveDrag( @@ -18,18 +18,18 @@ data class ActiveDrag( val cursorY: Int, val transform: Transform, val dropEffect: DropEffect, - val dataTransfer: DataTransfer + val dataTransfer: DataTransfer, ) enum class InsertPosition { BEFORE, AFTER, - APPEND + APPEND, } data class SortableProjection( val activeId: String?, val overId: String?, val insertPosition: InsertPosition, - val newIndex: Int? -) \ No newline at end of file + val newIndex: Int?, +) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndMonitor.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndMonitor.kt index 7df91c9..b3801ce 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndMonitor.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndMonitor.kt @@ -2,9 +2,13 @@ package org.dreamfinity.dsgl.core.dnd interface DndMonitorListener { fun onDragStart(active: ActiveDrag) {} + fun onDragMove(active: ActiveDrag, over: Any?) {} + fun onDragOver(active: ActiveDrag, over: Any?) {} + fun onDragEnd(active: ActiveDrag, over: Any?, dropEffect: DropEffect) {} + fun onDragCancel(active: ActiveDrag) {} } @@ -13,5 +17,5 @@ data class DragDropMonitorCallbacks( val onDragMove: ((ActiveDrag, Any?) -> Unit)? = null, val onDragOver: ((ActiveDrag, Any?) -> Unit)? = null, val onDragEnd: ((ActiveDrag, Any?, DropEffect) -> Unit)? = null, - val onDragCancel: ((ActiveDrag) -> Unit)? = null -) \ No newline at end of file + val onDragCancel: ((ActiveDrag) -> Unit)? = null, +) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndProps.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndProps.kt index 0c4d61c..73d74f9 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndProps.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndProps.kt @@ -22,4 +22,4 @@ fun ComponentProps.applySortable(descriptor: Sortable) { applyDraggable(descriptor.draggable) applyDroppable(descriptor.droppable) applyDndListeners(descriptor.listeners) -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndReorder.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndReorder.kt index a7997f9..119a990 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndReorder.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndReorder.kt @@ -5,7 +5,7 @@ fun reorderByDnD( activeId: String?, overId: String?, insertPosition: InsertPosition, - idOf: (T) -> String + idOf: (T) -> String, ): List { if (activeId.isNullOrBlank()) return items val fromIndex = items.indexOfFirst { idOf(it) == activeId } @@ -13,19 +13,20 @@ fun reorderByDnD( val mutable = items.toMutableList() val moved = mutable.removeAt(fromIndex) - val targetIndex = when { - overId.isNullOrBlank() || insertPosition == InsertPosition.APPEND -> mutable.size - else -> { - val overIndex = mutable.indexOfFirst { idOf(it) == overId } - if (overIndex < 0) { - mutable.size - } else if (insertPosition == InsertPosition.AFTER) { - overIndex + 1 - } else { - overIndex + val targetIndex = + when { + overId.isNullOrBlank() || insertPosition == InsertPosition.APPEND -> mutable.size + else -> { + val overIndex = mutable.indexOfFirst { idOf(it) == overId } + if (overIndex < 0) { + mutable.size + } else if (insertPosition == InsertPosition.AFTER) { + overIndex + 1 + } else { + overIndex + } } - } - }.coerceIn(0, mutable.size) + }.coerceIn(0, mutable.size) mutable.add(targetIndex, moved) return if (items.sameOrderAs(mutable, idOf)) items else mutable diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndRuntime.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndRuntime.kt index 2ef4d80..2735ab3 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndRuntime.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndRuntime.kt @@ -32,4 +32,4 @@ object DndSystem { } fun getSmoothingFactor(): Double = DndRuntime.engine.getSmoothingFactor() -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DragDataTransfer.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DragDataTransfer.kt index 27394ba..d11bf98 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DragDataTransfer.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DragDataTransfer.kt @@ -8,20 +8,20 @@ enum class EffectAllowed { COPY_LINK, COPY_MOVE, LINK_MOVE, - ALL + ALL, } enum class DropEffect { NONE, COPY, MOVE, - LINK + LINK, } data class DragImageSpec( val nodeKey: Any, val offsetX: Int = 0, - val offsetY: Int = 0 + val offsetY: Int = 0, ) /** @@ -43,9 +43,7 @@ class DataTransfer { dataByType[normalized] = value } - fun getData(type: String): String? { - return dataByType[type.trim()] - } + fun getData(type: String): String? = dataByType[type.trim()] fun clearData(type: String? = null) { if (type == null) { @@ -72,4 +70,4 @@ class DataTransfer { } internal fun currentDragImageSpec(): DragImageSpec? = dragImageSpec -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DragDropEvents.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DragDropEvents.kt index cdf67e8..3eba835 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DragDropEvents.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DragDropEvents.kt @@ -7,14 +7,14 @@ abstract class DragDropEvent( override var mouseX: Int, override var mouseY: Int, val sourceKey: Any?, - val dataTransfer: DataTransfer + val dataTransfer: DataTransfer, ) : MouseEvent(mouseX, mouseY) data class DragStartEvent( private val x: Int, private val y: Int, private val dragSourceKey: Any?, - private val transfer: DataTransfer + private val transfer: DataTransfer, ) : DragDropEvent(x, y, dragSourceKey, transfer) { override val type: Events get() = Events.DRAGSTART @@ -24,7 +24,7 @@ data class DragEvent( private val x: Int, private val y: Int, private val dragSourceKey: Any?, - private val transfer: DataTransfer + private val transfer: DataTransfer, ) : DragDropEvent(x, y, dragSourceKey, transfer) { override val type: Events get() = Events.DRAGGING @@ -37,7 +37,7 @@ data class DragEndEvent( private val transfer: DataTransfer, val didDrop: Boolean, val finalDropEffect: DropEffect, - val dropTargetKey: Any? + val dropTargetKey: Any?, ) : DragDropEvent(x, y, dragSourceKey, transfer) { override val type: Events get() = Events.DRAGEND @@ -47,7 +47,7 @@ data class DragEnterEvent( private val x: Int, private val y: Int, private val dragSourceKey: Any?, - private val transfer: DataTransfer + private val transfer: DataTransfer, ) : DragDropEvent(x, y, dragSourceKey, transfer) { override val type: Events get() = Events.DRAGENTER @@ -57,7 +57,7 @@ class DragOverEvent( x: Int, y: Int, dragSourceKey: Any?, - transfer: DataTransfer + transfer: DataTransfer, ) : DragDropEvent(x, y, dragSourceKey, transfer) { override val type: Events get() = Events.DRAGOVER @@ -81,7 +81,7 @@ data class DragLeaveEvent( private val x: Int, private val y: Int, private val dragSourceKey: Any?, - private val transfer: DataTransfer + private val transfer: DataTransfer, ) : DragDropEvent(x, y, dragSourceKey, transfer) { override val type: Events get() = Events.DRAGLEAVE @@ -91,7 +91,7 @@ class DropEvent( x: Int, y: Int, dragSourceKey: Any?, - transfer: DataTransfer + transfer: DataTransfer, ) : DragDropEvent(x, y, dragSourceKey, transfer) { override val type: Events get() = Events.DROP @@ -109,4 +109,4 @@ class DropEvent( dataTransfer.dropEffect = effect } } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DragPresentation.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DragPresentation.kt index 687cfcc..8733fa1 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DragPresentation.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DragPresentation.kt @@ -6,7 +6,7 @@ import org.dreamfinity.dsgl.core.render.RenderCommand enum class DragPreviewMode { ORIGINAL, - GHOST + GHOST, } class PlaceholderScope internal constructor() { @@ -30,8 +30,8 @@ class PlaceholderScope internal constructor() { bounds.y, bounds.width, bounds.height, - color - ) + color, + ), ) } val border = borderColor @@ -44,8 +44,8 @@ class PlaceholderScope internal constructor() { bounds.y + bounds.height - width, bounds.width, width, - border - ) + border, + ), ) out.add(RenderCommand.DrawRect(bounds.x, bounds.y, width, bounds.height, border)) out.add( @@ -54,8 +54,8 @@ class PlaceholderScope internal constructor() { bounds.y, width, bounds.height, - border - ) + border, + ), ) } return out @@ -66,35 +66,52 @@ class DragPreviewScope internal constructor( val dataTransfer: DataTransfer, val sourceBounds: Rect, private val anchorX: Int, - private val anchorY: Int + private val anchorY: Int, ) { private val commands: MutableList = ArrayList(8) - fun rect(x: Int, y: Int, width: Int, height: Int, color: Int) { + fun rect( + x: Int, + y: Int, + width: Int, + height: Int, + color: Int, + ) { commands.add( RenderCommand.DrawRect( anchorX + x, anchorY + y, width, height, - color - ) + color, + ), ) } - fun text(value: String, x: Int, y: Int, color: Int = DsglColors.WHITE) { + fun text( + value: String, + x: Int, + y: Int, + color: Int = DsglColors.WHITE, + ) { commands.add(RenderCommand.DrawText(value, anchorX + x, anchorY + y, color)) } - fun image(resource: String, x: Int, y: Int, width: Int, height: Int) { + fun image( + resource: String, + x: Int, + y: Int, + width: Int, + height: Int, + ) { commands.add( RenderCommand.DrawImage( resource, anchorX + x, anchorY + y, width, - height - ) + height, + ), ) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngine.kt index 1e30088..f2cd2e7 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngine.kt @@ -25,7 +25,7 @@ object DefaultDndEngine : DndEngine { val sourceKey: Any?, val sourceClass: Class, val startX: Int, - val startY: Int + val startY: Int, ) private data class ActiveSession( @@ -52,7 +52,7 @@ object DefaultDndEngine : DndEngine { var collisionCandidateCount: Int = 0, var sourceExcludedFromHitTest: Boolean = true, var dragTickAccum: Double = 0.0, - var overTickAccum: Double = 0.0 + var overTickAccum: Double = 0.0, ) private var pendingDrag: PendingDrag? = null @@ -74,18 +74,19 @@ object DefaultDndEngine : DndEngine { override fun getSmoothingFactor(): Double = smoothingFactor override fun monitor(nodeKey: Any?): DndMonitorState { - val active = activeDrag ?: return DndMonitorState( - isDragging = false, - sourceKey = null, - cursorX = 0, - cursorY = 0, - previewX = 0.0, - previewY = 0.0, - mode = null, - overKey = null, - collisionCandidates = 0, - sourceExcludedFromHitTest = true - ) + val active = + activeDrag ?: return DndMonitorState( + isDragging = false, + sourceKey = null, + cursorX = 0, + cursorY = 0, + previewX = 0.0, + previewY = 0.0, + mode = null, + overKey = null, + collisionCandidates = 0, + sourceExcludedFromHitTest = true, + ) val draggingThisSource = nodeKey != null && nodeKey == active.sourceKey return DndMonitorState( isDragging = nodeKey == null || draggingThisSource, @@ -97,7 +98,7 @@ object DefaultDndEngine : DndEngine { mode = active.previewMode, overKey = active.dropTargetKey, collisionCandidates = active.collisionCandidateCount, - sourceExcludedFromHitTest = active.sourceExcludedFromHitTest + sourceExcludedFromHitTest = active.sourceExcludedFromHitTest, ) } @@ -126,15 +127,16 @@ object DefaultDndEngine : DndEngine { return } if (activeDrag != null) return - pendingDrag = resolveDraggableSource(target)?.let { source -> - PendingDrag( - sourceNode = source, - sourceKey = source.key, - sourceClass = source.javaClass, - startX = event.mouseX, - startY = event.mouseY - ) - } + pendingDrag = + resolveDraggableSource(target)?.let { source -> + PendingDrag( + sourceNode = source, + sourceKey = source.key, + sourceClass = source.javaClass, + startX = event.mouseX, + startY = event.mouseY, + ) + } } override fun onMouseMove(root: DOMNode, mouseX: Int, mouseY: Int) { @@ -175,12 +177,13 @@ object DefaultDndEngine : DndEngine { var didDrop = false var dropTargetKey: Any? = null if (acceptedTarget != null) { - val dropEvent = DropEvent( - x = active.cursorX, - y = active.cursorY, - dragSourceKey = active.sourceKey, - transfer = active.dataTransfer - ) + val dropEvent = + DropEvent( + x = active.cursorX, + y = active.cursorY, + dragSourceKey = active.sourceKey, + transfer = active.dataTransfer, + ) dropEvent.target = acceptedTarget EventBus.post(dropEvent) dropTargetKey = acceptedTarget.key @@ -199,11 +202,12 @@ object DefaultDndEngine : DndEngine { rebindAfterReconcile(root) val active = activeDrag ?: return - val alpha = if (smoothingFactor <= 0.0) { - 1.0 - } else { - 1.0 - exp(-smoothingFactor * safeDt) - } + val alpha = + if (smoothingFactor <= 0.0) { + 1.0 + } else { + 1.0 - exp(-smoothingFactor * safeDt) + } active.previewX += (active.cursorX.toDouble() - active.previewX) * alpha active.previewY += (active.cursorY.toDouble() - active.previewY) * alpha @@ -239,7 +243,7 @@ object DefaultDndEngine : DndEngine { ctx: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, - out: MutableList + out: MutableList, ) { val active = activeDrag ?: return if (viewportWidth <= 0 || viewportHeight <= 0) return @@ -271,11 +275,12 @@ object DefaultDndEngine : DndEngine { } val dropClass = active.dropTargetClass - val reboundTarget = if (active.dropTargetKey != null && dropClass != null) { - findByKeyAndClass(root, active.dropTargetKey, dropClass) - } else { - null - } + val reboundTarget = + if (active.dropTargetKey != null && dropClass != null) { + findByKeyAndClass(root, active.dropTargetKey, dropClass) + } else { + null + } active.dropTargetNode = reboundTarget if (reboundTarget == null) { active.dropTargetKey = null @@ -286,23 +291,30 @@ object DefaultDndEngine : DndEngine { } override fun cancelActiveDrag() { - val active = activeDrag ?: run { - pendingDrag = null - return - } + val active = + activeDrag ?: run { + pendingDrag = null + return + } notifyDragCancel(active) finishDrag(active, didDrop = false, dropTargetKey = null) } - private fun tryStartDrag(root: DOMNode, pending: PendingDrag, mouseX: Int, mouseY: Int) { + private fun tryStartDrag( + root: DOMNode, + pending: PendingDrag, + mouseX: Int, + mouseY: Int, + ) { val source = findByKeyAndClass(root, pending.sourceKey, pending.sourceClass) ?: pending.sourceNode val transfer = DataTransfer() - val startEvent = DragStartEvent( - x = mouseX, - y = mouseY, - dragSourceKey = source.key, - transfer = transfer - ) + val startEvent = + DragStartEvent( + x = mouseX, + y = mouseY, + dragSourceKey = source.key, + transfer = transfer, + ) startEvent.target = source EventBus.post(startEvent) if (startEvent.cancelled) { @@ -318,24 +330,25 @@ object DefaultDndEngine : DndEngine { val previewOffsetY = previewSpec?.offsetY ?: defaultOffsetY val hideSource = source.dragPreviewMode == DragPreviewMode.ORIGINAL || source.hideSourceWhileDragging - val startedDrag = ActiveSession( - sourceNode = source, - sourceKey = source.key, - sourceClass = source.javaClass, - dataTransfer = transfer, - previewMode = source.dragPreviewMode, - sourceHiddenDuringDrag = hideSource, - placeholderWidth = sourceBounds.width, - placeholderHeight = sourceBounds.height, - placeholderBuilder = source.dragPlaceholderBuilder, - previewBuilder = source.dragPreviewBuilder, - cursorX = mouseX, - cursorY = mouseY, - previewX = mouseX.toDouble(), - previewY = mouseY.toDouble(), - previewOffsetX = previewOffsetX, - previewOffsetY = previewOffsetY - ) + val startedDrag = + ActiveSession( + sourceNode = source, + sourceKey = source.key, + sourceClass = source.javaClass, + dataTransfer = transfer, + previewMode = source.dragPreviewMode, + sourceHiddenDuringDrag = hideSource, + placeholderWidth = sourceBounds.width, + placeholderHeight = sourceBounds.height, + placeholderBuilder = source.dragPlaceholderBuilder, + previewBuilder = source.dragPreviewBuilder, + cursorX = mouseX, + cursorY = mouseY, + previewX = mouseX.toDouble(), + previewY = mouseY.toDouble(), + previewOffsetX = previewOffsetX, + previewOffsetY = previewOffsetY, + ) activeDrag = startedDrag pendingDrag = null applySourceHiddenFlags(startedDrag) @@ -357,12 +370,13 @@ object DefaultDndEngine : DndEngine { val resolvedTarget = resolveDropTarget(root, active, active.cursorX, active.cursorY) if (!isSameNode(active.dropTargetNode, resolvedTarget)) { active.dropTargetNode?.let { prev -> - val leaveEvent = DragLeaveEvent( - x = active.cursorX, - y = active.cursorY, - dragSourceKey = active.sourceKey, - transfer = active.dataTransfer - ) + val leaveEvent = + DragLeaveEvent( + x = active.cursorX, + y = active.cursorY, + dragSourceKey = active.sourceKey, + transfer = active.dataTransfer, + ) leaveEvent.target = prev EventBus.post(leaveEvent) } @@ -372,47 +386,56 @@ object DefaultDndEngine : DndEngine { active.dropAccepted = false active.dataTransfer.dropEffect = DropEffect.NONE resolvedTarget?.let { next -> - val enterEvent = DragEnterEvent( - x = active.cursorX, - y = active.cursorY, - dragSourceKey = active.sourceKey, - transfer = active.dataTransfer - ) + val enterEvent = + DragEnterEvent( + x = active.cursorX, + y = active.cursorY, + dragSourceKey = active.sourceKey, + transfer = active.dataTransfer, + ) enterEvent.target = next EventBus.post(enterEvent) } } if (!dispatchOver) return - val currentTarget = active.dropTargetNode ?: run { - active.dropAccepted = false - active.dataTransfer.dropEffect = DropEffect.NONE - return - } + val currentTarget = + active.dropTargetNode ?: run { + active.dropAccepted = false + active.dataTransfer.dropEffect = DropEffect.NONE + return + } - val overEvent = DragOverEvent( - x = active.cursorX, - y = active.cursorY, - dragSourceKey = active.sourceKey, - transfer = active.dataTransfer - ) + val overEvent = + DragOverEvent( + x = active.cursorX, + y = active.cursorY, + dragSourceKey = active.sourceKey, + transfer = active.dataTransfer, + ) overEvent.target = currentTarget EventBus.post(overEvent) val accepted = overEvent.dropAccepted || overEvent.cancelled active.dropAccepted = accepted if (accepted) { - active.dataTransfer.dropEffect = normalizeDropEffect( - requested = active.dataTransfer.dropEffect, - allowed = active.dataTransfer.effectAllowed - ) + active.dataTransfer.dropEffect = + normalizeDropEffect( + requested = active.dataTransfer.dropEffect, + allowed = active.dataTransfer.effectAllowed, + ) } else { active.dataTransfer.dropEffect = DropEffect.NONE } notifyDragOver(active) } - private fun resolveDropTarget(root: DOMNode, active: ActiveSession, mouseX: Int, mouseY: Int): DOMNode? { + private fun resolveDropTarget( + root: DOMNode, + active: ActiveSession, + mouseX: Int, + mouseY: Int, + ): DOMNode? { val chain = collectHoverChain(root, mouseX, mouseY) val candidates = ArrayList(chain.size) var excludedSource = false @@ -447,27 +470,29 @@ object DefaultDndEngine : DndEngine { private fun dispatchDragEvent(active: ActiveSession) { val source = active.sourceNode - val dragEvent = DragEvent( - x = active.cursorX, - y = active.cursorY, - dragSourceKey = active.sourceKey, - transfer = active.dataTransfer - ) + val dragEvent = + DragEvent( + x = active.cursorX, + y = active.cursorY, + dragSourceKey = active.sourceKey, + transfer = active.dataTransfer, + ) dragEvent.target = source EventBus.post(dragEvent) } private fun dispatchDragEnd(active: ActiveSession, didDrop: Boolean, dropTargetKey: Any?) { val source = active.sourceNode - val event = DragEndEvent( - x = active.cursorX, - y = active.cursorY, - dragSourceKey = active.sourceKey, - transfer = active.dataTransfer, - didDrop = didDrop, - finalDropEffect = if (didDrop) active.dataTransfer.dropEffect else DropEffect.NONE, - dropTargetKey = dropTargetKey - ) + val event = + DragEndEvent( + x = active.cursorX, + y = active.cursorY, + dragSourceKey = active.sourceKey, + transfer = active.dataTransfer, + didDrop = didDrop, + finalDropEffect = if (didDrop) active.dataTransfer.dropEffect else DropEffect.NONE, + dropTargetKey = dropTargetKey, + ) event.target = source EventBus.post(event) } @@ -495,7 +520,7 @@ object DefaultDndEngine : DndEngine { private fun appendOriginalPreviewCommands( active: ActiveSession, ctx: UiMeasureContext, - out: MutableList + out: MutableList, ) { val source = active.sourceNode val dx = (active.previewX - active.previewOffsetX - source.bounds.x).toInt() @@ -511,7 +536,7 @@ object DefaultDndEngine : DndEngine { root: DOMNode, active: ActiveSession, ctx: UiMeasureContext, - out: MutableList + out: MutableList, ) { if (!active.dataTransfer.ghostVisible) return val anchorX = (active.previewX - active.previewOffsetX).toInt() @@ -520,21 +545,23 @@ object DefaultDndEngine : DndEngine { val customBuilder = active.previewBuilder if (customBuilder != null) { - val scope = DragPreviewScope( - dataTransfer = active.dataTransfer, - sourceBounds = sourceBounds, - anchorX = anchorX, - anchorY = anchorY - ) + val scope = + DragPreviewScope( + dataTransfer = active.dataTransfer, + sourceBounds = sourceBounds, + anchorX = anchorX, + anchorY = anchorY, + ) customBuilder.invoke(scope) out.addAll(scope.build()) return } val previewSpec = active.dataTransfer.currentDragImageSpec() - val previewNode = previewSpec?.let { spec -> - findByKey(root, spec.nodeKey) - } + val previewNode = + previewSpec?.let { spec -> + findByKey(root, spec.nodeKey) + } if (previewNode != null && previewSpec != null) { val dx = (active.previewX - previewSpec.offsetX - previewNode.bounds.x).toInt() val dy = (active.previewY - previewSpec.offsetY - previewNode.bounds.y).toInt() @@ -565,8 +592,8 @@ object DefaultDndEngine : DndEngine { } } - private fun isDropEffectAllowed(effect: DropEffect, allowed: EffectAllowed): Boolean { - return when (allowed) { + private fun isDropEffectAllowed(effect: DropEffect, allowed: EffectAllowed): Boolean = + when (allowed) { EffectAllowed.NONE -> false EffectAllowed.COPY -> effect == DropEffect.COPY EffectAllowed.MOVE -> effect == DropEffect.MOVE @@ -576,13 +603,8 @@ object DefaultDndEngine : DndEngine { EffectAllowed.LINK_MOVE -> effect == DropEffect.LINK || effect == DropEffect.MOVE EffectAllowed.ALL -> effect != DropEffect.NONE } - } - private fun drawDefaultGhost( - active: ActiveSession, - ctx: UiMeasureContext, - out: MutableList - ) { + private fun drawDefaultGhost(active: ActiveSession, ctx: UiMeasureContext, out: MutableList) { val label = active.dataTransfer.getData("text/plain") ?: "drag" val x = (active.previewX - active.previewOffsetX).toInt() val y = (active.previewY - active.previewOffsetY).toInt() @@ -596,73 +618,84 @@ object DefaultDndEngine : DndEngine { out.add(RenderCommand.DrawText(label, x + 6, y + 4, DsglColors.WHITE)) } - private fun shiftCommand(command: RenderCommand, dx: Int, dy: Int): RenderCommand { - return when (command) { - is RenderCommand.DrawRect -> command.copy( - x = command.x + dx, - y = command.y + dy - ) + private fun shiftCommand(command: RenderCommand, dx: Int, dy: Int): RenderCommand = + when (command) { + is RenderCommand.DrawRect -> + command.copy( + x = command.x + dx, + y = command.y + dy, + ) - is RenderCommand.DrawColorField -> command.copy( - x = command.x + dx, - y = command.y + dy - ) + is RenderCommand.DrawColorField -> + command.copy( + x = command.x + dx, + y = command.y + dy, + ) - is RenderCommand.DrawHueBar -> command.copy( - x = command.x + dx, - y = command.y + dy - ) + is RenderCommand.DrawHueBar -> + command.copy( + x = command.x + dx, + y = command.y + dy, + ) - is RenderCommand.DrawAlphaBar -> command.copy( - x = command.x + dx, - y = command.y + dy - ) + is RenderCommand.DrawAlphaBar -> + command.copy( + x = command.x + dx, + y = command.y + dy, + ) - is RenderCommand.DrawCheckerboard -> command.copy( - x = command.x + dx, - y = command.y + dy - ) + is RenderCommand.DrawCheckerboard -> + command.copy( + x = command.x + dx, + y = command.y + dy, + ) - is RenderCommand.DrawText -> command.copy( - x = command.x + dx, - y = command.y + dy - ) + is RenderCommand.DrawText -> + command.copy( + x = command.x + dx, + y = command.y + dy, + ) - is RenderCommand.DrawImage -> command.copy( - x = command.x + dx, - y = command.y + dy - ) + is RenderCommand.DrawImage -> + command.copy( + x = command.x + dx, + y = command.y + dy, + ) - is RenderCommand.CaptureScreenRegion -> command.copy( - sourceX = command.sourceX + dx, - sourceY = command.sourceY + dy - ) + is RenderCommand.CaptureScreenRegion -> + command.copy( + sourceX = command.sourceX + dx, + sourceY = command.sourceY + dy, + ) - is RenderCommand.DrawCapturedScreenRegion -> command.copy( - x = command.x + dx, - y = command.y + dy - ) + is RenderCommand.DrawCapturedScreenRegion -> + command.copy( + x = command.x + dx, + y = command.y + dy, + ) - is RenderCommand.DrawItemStack -> command.copy( - x = command.x + dx, - y = command.y + dy - ) + is RenderCommand.DrawItemStack -> + command.copy( + x = command.x + dx, + y = command.y + dy, + ) - is RenderCommand.PushClip -> command.copy( - x = command.x + dx, - y = command.y + dy - ) + is RenderCommand.PushClip -> + command.copy( + x = command.x + dx, + y = command.y + dy, + ) RenderCommand.PopClip -> RenderCommand.PopClip - is RenderCommand.PushTransform -> command.copy( - originX = command.originX + dx, - originY = command.originY + dy - ) + is RenderCommand.PushTransform -> + command.copy( + originX = command.originX + dx, + originY = command.originY + dy, + ) RenderCommand.PopTransform -> RenderCommand.PopTransform is RenderCommand.PushOpacity -> command RenderCommand.PopOpacity -> RenderCommand.PopOpacity } - } private fun isSameNode(prev: DOMNode?, current: DOMNode?): Boolean { if (prev === current) return true @@ -671,9 +704,9 @@ object DefaultDndEngine : DndEngine { val currKey = current.key if (prevKey != null || currKey != null) { return prevKey != null && - currKey != null && - prevKey == currKey && - prev.javaClass == current.javaClass + currKey != null && + prevKey == currKey && + prev.javaClass == current.javaClass } return false } @@ -748,13 +781,13 @@ object DefaultDndEngine : DndEngine { data = DndSystem.payload(id), cursorX = active.cursorX, cursorY = active.cursorY, - transform = Transform( - x = active.previewX - active.cursorX.toDouble(), - y = active.previewY - active.cursorY.toDouble() - ), + transform = + Transform( + x = active.previewX - active.cursorX.toDouble(), + y = active.previewY - active.cursorY.toDouble(), + ), dropEffect = active.dataTransfer.dropEffect, - dataTransfer = active.dataTransfer + dataTransfer = active.dataTransfer, ) } } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEvents.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEvents.kt index a13c30d..4d6722f 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEvents.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEvents.kt @@ -8,7 +8,7 @@ import org.dreamfinity.dsgl.core.event.MouseButton fun DOMNode.onContextMenu( host: ContextMenuHost = ContextMenuRuntime.host, - handler: ContextMenuTriggerScope.() -> Unit + handler: ContextMenuTriggerScope.() -> Unit, ) { val previous = onMouseDown onMouseDown = { event -> @@ -24,8 +24,8 @@ fun DOMNode.onContextMenu( anchorRect = anchor, inheritedFontId = sourceStyle?.fontId ?: sourceNode.fontId, inheritedFontSize = sourceStyle?.fontSize ?: sourceNode.fontSize, - host = host - ) + host = host, + ), ) event.cancelled = true } @@ -34,7 +34,7 @@ fun DOMNode.onContextMenu( fun DOMNode.onContextMenuModel( host: ContextMenuHost = ContextMenuRuntime.host, - modelProvider: () -> ContextMenuModel + modelProvider: () -> ContextMenuModel, ) { onContextMenu(host = host, handler = { openMenu(modelProvider()) }) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/DOMNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/DOMNode.kt index 26985a7..c0efbc9 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/DOMNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/DOMNode.kt @@ -1,14 +1,15 @@ package org.dreamfinity.dsgl.core.dom -import org.dreamfinity.dsgl.core.dsl.ComponentProps import org.dreamfinity.dsgl.core.DsglColors -import org.dreamfinity.dsgl.core.dsl.StyleScope import org.dreamfinity.dsgl.core.animation.AnimationSpec import org.dreamfinity.dsgl.core.animation.StyleAnimationEngine import org.dreamfinity.dsgl.core.animation.TransitionSpec import org.dreamfinity.dsgl.core.debug.ScrollPerformanceCounters import org.dreamfinity.dsgl.core.dnd.* import org.dreamfinity.dsgl.core.dom.layout.* +import org.dreamfinity.dsgl.core.dom.text.ResolvedTextMetrics +import org.dreamfinity.dsgl.core.dsl.ComponentProps +import org.dreamfinity.dsgl.core.dsl.StyleScope import org.dreamfinity.dsgl.core.event.* import org.dreamfinity.dsgl.core.font.FontRegistry import org.dreamfinity.dsgl.core.hooks.ref.ElementHandle @@ -19,12 +20,11 @@ import org.dreamfinity.dsgl.core.text.MinecraftFormattingParser import org.dreamfinity.dsgl.core.text.ParsedText import org.dreamfinity.dsgl.core.text.TextStyleFlags import org.dreamfinity.dsgl.core.text.TextStyleMetrics -import org.dreamfinity.dsgl.core.dom.text.ResolvedTextMetrics import kotlin.math.roundToInt data class NodeStyleApplyResult( val visualDirty: Boolean, - val layoutDirty: Boolean + val layoutDirty: Boolean, ) data class ScrollAxisState( @@ -32,7 +32,7 @@ data class ScrollAxisState( val scrollContainer: Boolean, val clipsToViewport: Boolean, val scrollbarPresent: Boolean, - val scrollbarGutter: Int + val scrollbarGutter: Int, ) data class ScrollContainerState( @@ -46,7 +46,7 @@ data class ScrollContainerState( val horizontalScrollbarGutter: Int, val verticalScrollbarGutter: Int, val axisX: ScrollAxisState, - val axisY: ScrollAxisState + val axisY: ScrollAxisState, ) data class ScrollAnimationDebugState( @@ -55,7 +55,7 @@ data class ScrollAnimationDebugState( val displayedX: Double, val displayedY: Double, val resolvedX: Int, - val resolvedY: Int + val resolvedY: Int, ) data class ScrollSessionSnapshot( @@ -65,20 +65,21 @@ data class ScrollSessionSnapshot( val displayedY: Double, val resolvedX: Int, val resolvedY: Int, - val dragSession: ScrollbarDragSessionDebugState? + val dragSession: ScrollbarDragSessionDebugState?, ) data class ScrollInvalidationState( val layoutDirty: Boolean, val visualDirty: Boolean, - val interactionDirty: Boolean + val interactionDirty: Boolean, ) { companion object { - val CLEAN = ScrollInvalidationState( - layoutDirty = false, - visualDirty = false, - interactionDirty = false - ) + val CLEAN = + ScrollInvalidationState( + layoutDirty = false, + visualDirty = false, + interactionDirty = false, + ) } val anyDirty: Boolean @@ -93,19 +94,19 @@ data class ScrollbarDragSessionDebugState( val maxThumbTravelPx: Int, val maxScroll: Int, val grabOffsetPx: Int, - val initialResolvedScroll: Int + val initialResolvedScroll: Int, ) data class ScrollbarVisualAxis( val trackRect: Rect, val thumbRect: Rect, val maxScroll: Int, - val scrollOffset: Int + val scrollOffset: Int, ) data class ScrollbarVisualState( val horizontal: ScrollbarVisualAxis?, - val vertical: ScrollbarVisualAxis? + val vertical: ScrollbarVisualAxis?, ) private data class ScrollbarResolution( @@ -114,13 +115,13 @@ private data class ScrollbarResolution( val horizontalGutter: Int, val verticalGutter: Int, val viewportWidth: Int, - val viewportHeight: Int + val viewportHeight: Int, ) private data class NativeFontMetricsPx( val lineHeightPx: Int, val ascenderPx: Float, - val descenderPx: Float + val descenderPx: Float, ) private data class ScrollbarDragSession( @@ -131,12 +132,12 @@ private data class ScrollbarDragSession( val maxThumbTravelPx: Int, val maxScroll: Int, val grabOffsetPx: Int, - val initialResolvedScroll: Int + val initialResolvedScroll: Int, ) private enum class ScrollbarAxis { Horizontal, - Vertical + Vertical, } /** @@ -145,7 +146,7 @@ private enum class ScrollbarAxis { * Nodes are measured, laid out, and then converted into [RenderCommand]s by the host. */ abstract class DOMNode( - var key: Any? = null + var key: Any? = null, ) { companion object { const val NORMAL_LINE_HEIGHT_MULTIPLIER: Float = 1.2f @@ -338,7 +339,7 @@ abstract class DOMNode( private var stickyVisualOffsetXPx: Int = 0 private var stickyVisualOffsetYPx: Int = 0 private var stickyVisualOffsetsDirty: Boolean = true - private var stickyVisualRefreshSubtreeDirty: Boolean = true + private var stickyVisualRefreshSubtreeDirty: Boolean = true private var gapStyleValue: CssLength = CssLength.px(gap) private var flexBasisStyleValue: CssLength? = null private var borderColorStyleValue: Int = border.color @@ -552,76 +553,84 @@ abstract class DOMNode( } /** Measures the node's desired size. */ - internal fun resolveLayoutStyleValues( - ctx: UiMeasureContext, - parentContentWidth: Int?, - parentContentHeight: Int? - ) { + internal fun resolveLayoutStyleValues(ctx: UiMeasureContext, parentContentWidth: Int?, parentContentHeight: Int?) { if (appliedComputedStyle == null) { return } val context = lengthResolveContext(ctx, parentContentWidth, parentContentHeight) margin = marginStyleValue.resolveToInsets(context) padding = paddingStyleValue.resolveToInsets(context) - val borderWidthPx = borderWidthStyleValue - .resolvePx(context, LengthPercentBase.ContainerWidth) - .roundToInt() - .coerceAtLeast(0) + val borderWidthPx = + borderWidthStyleValue + .resolvePx(context, LengthPercentBase.ContainerWidth) + .roundToInt() + .coerceAtLeast(0) border = Border.all(borderWidthPx, borderColorStyleValue) - borderRadius = borderRadiusStyleValue - .resolvePx(context, LengthPercentBase.ContainerWidth) - .roundToInt() - .coerceAtLeast(0) - val resolvedWidth = widthStyleValue - ?.resolvePx(context, LengthPercentBase.ContainerWidth) - ?.roundToInt() - ?.coerceAtLeast(0) - val resolvedHeight = heightStyleValue - ?.resolvePx(context, LengthPercentBase.ContainerHeight) - ?.roundToInt() - ?.coerceAtLeast(0) - val resolvedMinWidth = minWidthStyleValue - ?.resolvePx(context, LengthPercentBase.ContainerWidth) - ?.roundToInt() - ?.coerceAtLeast(0) - val resolvedMinHeight = minHeightStyleValue - ?.resolvePx(context, LengthPercentBase.ContainerHeight) - ?.roundToInt() - ?.coerceAtLeast(0) - val resolvedMaxWidth = maxWidthStyleValue - ?.resolvePx(context, LengthPercentBase.ContainerWidth) - ?.roundToInt() - ?.coerceAtLeast(0) - val resolvedMaxHeight = maxHeightStyleValue - ?.resolvePx(context, LengthPercentBase.ContainerHeight) - ?.roundToInt() - ?.coerceAtLeast(0) + borderRadius = + borderRadiusStyleValue + .resolvePx(context, LengthPercentBase.ContainerWidth) + .roundToInt() + .coerceAtLeast(0) + val resolvedWidth = + widthStyleValue + ?.resolvePx(context, LengthPercentBase.ContainerWidth) + ?.roundToInt() + ?.coerceAtLeast(0) + val resolvedHeight = + heightStyleValue + ?.resolvePx(context, LengthPercentBase.ContainerHeight) + ?.roundToInt() + ?.coerceAtLeast(0) + val resolvedMinWidth = + minWidthStyleValue + ?.resolvePx(context, LengthPercentBase.ContainerWidth) + ?.roundToInt() + ?.coerceAtLeast(0) + val resolvedMinHeight = + minHeightStyleValue + ?.resolvePx(context, LengthPercentBase.ContainerHeight) + ?.roundToInt() + ?.coerceAtLeast(0) + val resolvedMaxWidth = + maxWidthStyleValue + ?.resolvePx(context, LengthPercentBase.ContainerWidth) + ?.roundToInt() + ?.coerceAtLeast(0) + val resolvedMaxHeight = + maxHeightStyleValue + ?.resolvePx(context, LengthPercentBase.ContainerHeight) + ?.roundToInt() + ?.coerceAtLeast(0) minWidth = resolvedMinWidth minHeight = resolvedMinHeight maxWidth = resolvedMaxWidth maxHeight = resolvedMaxHeight - width = resolvedWidth?.let { - clampContentLengthToConstraints( - length = it, - minConstraint = resolvedMinWidth, - maxConstraint = resolvedMaxWidth - ) - } - height = resolvedHeight?.let { - clampContentLengthToConstraints( - length = it, - minConstraint = resolvedMinHeight, - maxConstraint = resolvedMaxHeight - ) - } - gap = gapStyleValue - .resolvePx(context, LengthPercentBase.ContainerWidth) - .roundToInt() - .coerceAtLeast(0) - flexBasis = flexBasisStyleValue - ?.resolvePx(context, LengthPercentBase.ContainerWidth) - ?.roundToInt() - ?.coerceAtLeast(0) + width = + resolvedWidth?.let { + clampContentLengthToConstraints( + length = it, + minConstraint = resolvedMinWidth, + maxConstraint = resolvedMaxWidth, + ) + } + height = + resolvedHeight?.let { + clampContentLengthToConstraints( + length = it, + minConstraint = resolvedMinHeight, + maxConstraint = resolvedMaxHeight, + ) + } + gap = + gapStyleValue + .resolvePx(context, LengthPercentBase.ContainerWidth) + .roundToInt() + .coerceAtLeast(0) + flexBasis = + flexBasisStyleValue + ?.resolvePx(context, LengthPercentBase.ContainerWidth) + ?.roundToInt() + ?.coerceAtLeast(0) val resolvedRelativeOffsetX = resolveRelativeVisualOffsetXPx(context) val resolvedRelativeOffsetY = resolveRelativeVisualOffsetYPx(context) @@ -668,55 +677,62 @@ abstract class DOMNode( val verticalActive = verticalInset.active if (!horizontalActive && !verticalActive) return 0 to 0 - val references = StickyLayoutModel.nearestStickyScrollContainers( - node = this, - resolveHorizontal = horizontalActive, - resolveVertical = verticalActive - ) + val references = + StickyLayoutModel.nearestStickyScrollContainers( + node = this, + resolveHorizontal = horizontalActive, + resolveVertical = verticalActive, + ) val containingBlockRect = stickyContainingBlockForPositioningRect() val baseRect = visualBoundsWithPendingScrollTranslation(this) val referenceStates = HashMap(2) + fun viewportRect(reference: DOMNode): Rect { - val state = referenceStates.getOrPut(reference) { - reference.scrollContainerState() - } + val state = + referenceStates.getOrPut(reference) { + reference.scrollContainerState() + } return stickyReferenceViewportRect(reference, state) } - val offsetX = if (horizontalActive) { - val viewportRect = viewportRect(references.horizontal) - val offsetContext = positioningOffsetResolveContext(viewportRect) - val insetLength = horizontalInset.value - ?: error("Sticky horizontal inset must be present when horizontal sticky axis is active") - val insetPx = insetLength.resolvePx(offsetContext, LengthPercentBase.ContainerWidth).roundToInt() - StickyLayoutModel.resolveHorizontalVisualOffsetPx( - baseX = baseRect.x, - nodeWidth = baseRect.width.coerceAtLeast(0), - viewportRect = viewportRect, - containingBlockRect = containingBlockRect, - insetResolution = horizontalInset, - insetPx = insetPx - ) - } else { - 0 - } - val offsetY = if (verticalActive) { - val viewportRect = viewportRect(references.vertical) - val offsetContext = positioningOffsetResolveContext(viewportRect) - val insetLength = verticalInset.value - ?: error("Sticky vertical inset must be present when vertical sticky axis is active") - val insetPx = insetLength.resolvePx(offsetContext, LengthPercentBase.ContainerHeight).roundToInt() - StickyLayoutModel.resolveVerticalVisualOffsetPx( - baseY = baseRect.y, - nodeHeight = baseRect.height.coerceAtLeast(0), - viewportRect = viewportRect, - containingBlockRect = containingBlockRect, - insetResolution = verticalInset, - insetPx = insetPx - ) - } else { - 0 - } + val offsetX = + if (horizontalActive) { + val viewportRect = viewportRect(references.horizontal) + val offsetContext = positioningOffsetResolveContext(viewportRect) + val insetLength = + horizontalInset.value + ?: error("Sticky horizontal inset must be present when horizontal sticky axis is active") + val insetPx = insetLength.resolvePx(offsetContext, LengthPercentBase.ContainerWidth).roundToInt() + StickyLayoutModel.resolveHorizontalVisualOffsetPx( + baseX = baseRect.x, + nodeWidth = baseRect.width.coerceAtLeast(0), + viewportRect = viewportRect, + containingBlockRect = containingBlockRect, + insetResolution = horizontalInset, + insetPx = insetPx, + ) + } else { + 0 + } + val offsetY = + if (verticalActive) { + val viewportRect = viewportRect(references.vertical) + val offsetContext = positioningOffsetResolveContext(viewportRect) + val insetLength = + verticalInset.value + ?: error("Sticky vertical inset must be present when vertical sticky axis is active") + val insetPx = insetLength.resolvePx(offsetContext, LengthPercentBase.ContainerHeight).roundToInt() + StickyLayoutModel.resolveVerticalVisualOffsetPx( + baseY = baseRect.y, + nodeHeight = baseRect.height.coerceAtLeast(0), + viewportRect = viewportRect, + containingBlockRect = containingBlockRect, + insetResolution = verticalInset, + insetPx = insetPx, + ) + } else { + 0 + } return offsetX to offsetY } @@ -738,7 +754,7 @@ abstract class DOMNode( private fun stickyReferenceViewportRect( referenceScrollContainer: DOMNode, - referenceState: ScrollContainerState? = null + referenceState: ScrollContainerState? = null, ): Rect { val viewportRect = (referenceState ?: referenceScrollContainer.scrollContainerState()).viewportRect val adjustedBounds = visualBoundsWithPendingScrollTranslation(referenceScrollContainer) @@ -749,7 +765,7 @@ abstract class DOMNode( x = viewportRect.x + shiftX, y = viewportRect.y + shiftY, width = viewportRect.width, - height = viewportRect.height + height = viewportRect.height, ) } @@ -760,7 +776,7 @@ abstract class DOMNode( x = adjustedBounds.x, y = adjustedBounds.y, width = adjustedBounds.width.coerceAtLeast(0), - height = adjustedBounds.height.coerceAtLeast(0) + height = adjustedBounds.height.coerceAtLeast(0), ) } @@ -773,7 +789,7 @@ abstract class DOMNode( x = node.bounds.x - pendingDelta.first, y = node.bounds.y - pendingDelta.second, width = node.bounds.width, - height = node.bounds.height + height = node.bounds.height, ) } @@ -800,14 +816,15 @@ abstract class DOMNode( ctx: UiMeasureContext, parentContentWidth: Int?, parentContentHeight: Int?, - axis: FlexDirection + axis: FlexDirection, ): Int? { val context = lengthResolveContext(ctx, parentContentWidth, parentContentHeight) - val percentBase = if (axis == FlexDirection.Row) { - LengthPercentBase.ContainerWidth - } else { - LengthPercentBase.ContainerHeight - } + val percentBase = + if (axis == FlexDirection.Row) { + LengthPercentBase.ContainerWidth + } else { + LengthPercentBase.ContainerHeight + } return flexBasisStyleValue ?.resolvePx(context, percentBase) ?.roundToInt() @@ -817,13 +834,14 @@ abstract class DOMNode( private fun lengthResolveContext( ctx: UiMeasureContext, parentContentWidth: Int?, - parentContentHeight: Int? + parentContentHeight: Int?, ): LengthResolveContext { val rootFontSizePx = rootNode().resolveComputedFontSizePx().toFloat() - val inheritedFontSizePx = ( + val inheritedFontSizePx = + ( parent?.resolveComputedFontSizePx() ?: resolveComputedFontSizePx() - ).toFloat() + ).toFloat() val currentFontSizePx = resolveComputedFontSizePx().toFloat() return LengthResolveContext( viewportWidthPx = StyleEngine.viewportWidthPx().toFloat(), @@ -832,7 +850,7 @@ abstract class DOMNode( containingBlockHeightPx = parentContentHeight?.toFloat(), rootFontSizePx = rootFontSizePx, currentFontSizePx = currentFontSizePx, - inheritedFontSizePx = inheritedFontSizePx + inheritedFontSizePx = inheritedFontSizePx, ) } @@ -844,104 +862,87 @@ abstract class DOMNode( return current } + internal fun participatesInPositionedOrderingModel(): Boolean = PositionedLayoutModel.isPositioned(this) - internal fun participatesInPositionedOrderingModel(): Boolean { - return PositionedLayoutModel.isPositioned(this) - } + internal fun rootStackingScopeForPositioning(): DOMNode = PositionedLayoutModel.rootStackingScope(this) - internal fun rootStackingScopeForPositioning(): DOMNode { - return PositionedLayoutModel.rootStackingScope(this) - } + internal fun sharesRootStackingScopeForPositioning(other: DOMNode): Boolean = + PositionedLayoutModel.sharesRootStackingScope(this, other) - internal fun sharesRootStackingScopeForPositioning(other: DOMNode): Boolean { - return PositionedLayoutModel.sharesRootStackingScope(this, other) - } + internal fun rootStackingContextIdentityForPositioning(): PositionedLayoutModel.RootStackingContextId = + PositionedLayoutModel.rootStackingContextId(this) - internal fun rootStackingContextIdentityForPositioning(): PositionedLayoutModel.RootStackingContextId { - return PositionedLayoutModel.rootStackingContextId(this) - } + internal fun stackingContextScaffoldForTraversalOwner(): PositionedLayoutModel.StackingContext = + PositionedLayoutModel.stackingContextScaffold(this) - internal fun stackingContextScaffoldForTraversalOwner(): PositionedLayoutModel.StackingContext { - return PositionedLayoutModel.stackingContextScaffold(this) - } + internal fun containingBlockForAbsolutePositioning(): DOMNode = + PositionedLayoutModel.containingBlockForAbsolute(this) - internal fun containingBlockForAbsolutePositioning(): DOMNode { - return PositionedLayoutModel.containingBlockForAbsolute(this) - } + internal fun fixedViewportRootForPositioning(): DOMNode = PositionedLayoutModel.fixedViewportRoot(this) - internal fun fixedViewportRootForPositioning(): DOMNode { - return PositionedLayoutModel.fixedViewportRoot(this) - } + internal fun stickyReferenceScrollContainerVertical(): DOMNode = + StickyLayoutModel.nearestStickyScrollContainerVertical(this) - internal fun stickyReferenceScrollContainerVertical(): DOMNode { - return StickyLayoutModel.nearestStickyScrollContainerVertical(this) - } + internal fun stickyReferenceScrollContainerHorizontal(): DOMNode = + StickyLayoutModel.nearestStickyScrollContainerHorizontal(this) - internal fun stickyReferenceScrollContainerHorizontal(): DOMNode { - return StickyLayoutModel.nearestStickyScrollContainerHorizontal(this) - } + internal fun stickyContainingBlockForPositioning(): DOMNode = StickyLayoutModel.stickyContainingBlock(this) - internal fun stickyContainingBlockForPositioning(): DOMNode { - return StickyLayoutModel.stickyContainingBlock(this) - } - - internal fun stickyHorizontalInsetResolutionContract(): StickyLayoutModel.StickyHorizontalInsetResolution { - return StickyLayoutModel.resolveHorizontalInsets( + internal fun stickyHorizontalInsetResolutionContract(): StickyLayoutModel.StickyHorizontalInsetResolution = + StickyLayoutModel.resolveHorizontalInsets( left = leftStyleValue, - right = rightStyleValue + right = rightStyleValue, ) - } - internal fun stickyVerticalInsetResolutionContract(): StickyLayoutModel.StickyInsetResolution { - return StickyLayoutModel.resolveVerticalInsets( + internal fun stickyVerticalInsetResolutionContract(): StickyLayoutModel.StickyInsetResolution = + StickyLayoutModel.resolveVerticalInsets( top = topStyleValue, - bottom = bottomStyleValue + bottom = bottomStyleValue, ) - } - internal fun stickyPositionedGeometryIntegrationPoint(): StickyLayoutModel.PositionedGeometryIntegrationPoint { - return StickyLayoutModel.positionedGeometryIntegrationPoint() - } + internal fun stickyPositionedGeometryIntegrationPoint(): StickyLayoutModel.PositionedGeometryIntegrationPoint = + StickyLayoutModel.positionedGeometryIntegrationPoint() - internal fun isRemovedFromNormalFlowForPositioning(): Boolean { - return position == PositionMode.Absolute || position == PositionMode.Fixed - } + internal fun isRemovedFromNormalFlowForPositioning(): Boolean = + position == PositionMode.Absolute || position == PositionMode.Fixed internal fun resolveAbsoluteLayoutRect( ctx: UiMeasureContext, desiredX: Int, desiredY: Int, desiredWidth: Int, - desiredHeight: Int + desiredHeight: Int, ): Rect { if (position != PositionMode.Absolute) { return Rect( x = desiredX, y = desiredY, width = desiredWidth.coerceAtLeast(0), - height = desiredHeight.coerceAtLeast(0) + height = desiredHeight.coerceAtLeast(0), ) } val containingBlockRect = absoluteContainingBlockRect() val offsetContext = positioningOffsetResolveContext(ctx, containingBlockRect) - val resolvedX = resolvePositionedX( - context = offsetContext, - containerRect = containingBlockRect, - desiredX = desiredX, - desiredWidth = desiredWidth - ) - val resolvedY = resolvePositionedY( - context = offsetContext, - containerRect = containingBlockRect, - desiredY = desiredY, - desiredHeight = desiredHeight - ) + val resolvedX = + resolvePositionedX( + context = offsetContext, + containerRect = containingBlockRect, + desiredX = desiredX, + desiredWidth = desiredWidth, + ) + val resolvedY = + resolvePositionedY( + context = offsetContext, + containerRect = containingBlockRect, + desiredY = desiredY, + desiredHeight = desiredHeight, + ) return Rect( x = resolvedX, y = resolvedY, width = desiredWidth.coerceAtLeast(0), - height = desiredHeight.coerceAtLeast(0) + height = desiredHeight.coerceAtLeast(0), ) } @@ -950,79 +951,77 @@ abstract class DOMNode( desiredX: Int, desiredY: Int, desiredWidth: Int, - desiredHeight: Int + desiredHeight: Int, ): Rect { if (position != PositionMode.Fixed) { return Rect( x = desiredX, y = desiredY, width = desiredWidth.coerceAtLeast(0), - height = desiredHeight.coerceAtLeast(0) + height = desiredHeight.coerceAtLeast(0), ) } val viewportRect = fixedViewportAnchorRect() val offsetContext = positioningOffsetResolveContext(ctx, viewportRect) - val resolvedX = resolvePositionedX( - context = offsetContext, - containerRect = viewportRect, - desiredX = desiredX, - desiredWidth = desiredWidth - ) - val resolvedY = resolvePositionedY( - context = offsetContext, - containerRect = viewportRect, - desiredY = desiredY, - desiredHeight = desiredHeight - ) + val resolvedX = + resolvePositionedX( + context = offsetContext, + containerRect = viewportRect, + desiredX = desiredX, + desiredWidth = desiredWidth, + ) + val resolvedY = + resolvePositionedY( + context = offsetContext, + containerRect = viewportRect, + desiredY = desiredY, + desiredHeight = desiredHeight, + ) return Rect( x = resolvedX, y = resolvedY, width = desiredWidth.coerceAtLeast(0), - height = desiredHeight.coerceAtLeast(0) + height = desiredHeight.coerceAtLeast(0), ) } - internal fun orderedChildrenForPaintTraversal(): List { - return PositionedLayoutModel.orderedChildrenForPaint(this) - } + internal fun orderedChildrenForPaintTraversal(): List = PositionedLayoutModel.orderedChildrenForPaint(this) - internal fun orderedChildrenForHitTestingTraversal(): List { - return PositionedLayoutModel.orderedChildrenForHitTesting(this) - } + internal fun orderedChildrenForHitTestingTraversal(): List = + PositionedLayoutModel.orderedChildrenForHitTesting(this) internal fun clampMeasuredOuterSize(size: Size): Size { val extrasWidth = (padding.horizontal + border.horizontal).coerceAtLeast(0) val extrasHeight = (padding.vertical + border.vertical).coerceAtLeast(0) val contentWidth = (size.width - extrasWidth).coerceAtLeast(0) val contentHeight = (size.height - extrasHeight).coerceAtLeast(0) - val clampedContentWidth = clampContentLengthToConstraints( - length = contentWidth, - minConstraint = minWidth, - maxConstraint = maxWidth - ) - val clampedContentHeight = clampContentLengthToConstraints( - length = contentHeight, - minConstraint = minHeight, - maxConstraint = maxHeight - ) + val clampedContentWidth = + clampContentLengthToConstraints( + length = contentWidth, + minConstraint = minWidth, + maxConstraint = maxWidth, + ) + val clampedContentHeight = + clampContentLengthToConstraints( + length = contentHeight, + minConstraint = minHeight, + maxConstraint = maxHeight, + ) return Size( width = clampedContentWidth + extrasWidth, - height = clampedContentHeight + extrasHeight + height = clampedContentHeight + extrasHeight, ) } - private fun clampContentLengthToConstraints( - length: Int, - minConstraint: Int?, - maxConstraint: Int? - ): Int { + private fun clampContentLengthToConstraints(length: Int, minConstraint: Int?, maxConstraint: Int?): Int { val normalizedMin = minConstraint?.coerceAtLeast(0) val normalizedMax = maxConstraint?.coerceAtLeast(0) - val effectiveMax = when { - normalizedMin != null && normalizedMax != null -> normalizedMax.coerceAtLeast(normalizedMin) - else -> normalizedMax - } + val effectiveMax = + when { + normalizedMin != null && normalizedMax != null -> normalizedMax.coerceAtLeast(normalizedMin) + else -> normalizedMax + } var result = length.coerceAtLeast(0) if (normalizedMin != null && result < normalizedMin) { result = normalizedMin @@ -1039,8 +1038,12 @@ abstract class DOMNode( return Rect( x = state.viewportRect.x - state.scrollX, y = state.viewportRect.y - state.scrollY, - width = state.viewportRect.width.coerceAtLeast(0), - height = state.viewportRect.height.coerceAtLeast(0) + width = + state.viewportRect.width + .coerceAtLeast(0), + height = + state.viewportRect.height + .coerceAtLeast(0), ) } @@ -1050,21 +1053,21 @@ abstract class DOMNode( return Rect( x = state.viewportRect.x, y = state.viewportRect.y, - width = state.viewportRect.width.coerceAtLeast(0), - height = state.viewportRect.height.coerceAtLeast(0) + width = + state.viewportRect.width + .coerceAtLeast(0), + height = + state.viewportRect.height + .coerceAtLeast(0), ) } - private fun fixedViewportAnchorRect(): Rect { - return fixedViewportClipRectForPromotedParticipation() - } + private fun fixedViewportAnchorRect(): Rect = fixedViewportClipRectForPromotedParticipation() private fun positioningOffsetResolveContext( @Suppress("UNUSED_PARAMETER") ctx: UiMeasureContext, - containerRect: Rect - ): LengthResolveContext { - return positioningOffsetResolveContext(containerRect) - } + containerRect: Rect, + ): LengthResolveContext = positioningOffsetResolveContext(containerRect) private fun positioningOffsetResolveContext(containerRect: Rect): LengthResolveContext { val rootFontSizePx = rootNode().resolveComputedFontSizePx().toFloat() @@ -1073,11 +1076,17 @@ abstract class DOMNode( return LengthResolveContext( viewportWidthPx = StyleEngine.viewportWidthPx().toFloat(), viewportHeightPx = StyleEngine.viewportHeightPx().toFloat(), - containingBlockWidthPx = containerRect.width.coerceAtLeast(0).toFloat(), - containingBlockHeightPx = containerRect.height.coerceAtLeast(0).toFloat(), + containingBlockWidthPx = + containerRect.width + .coerceAtLeast(0) + .toFloat(), + containingBlockHeightPx = + containerRect.height + .coerceAtLeast(0) + .toFloat(), rootFontSizePx = rootFontSizePx, currentFontSizePx = currentFontSizePx, - inheritedFontSizePx = inheritedFontSizePx + inheritedFontSizePx = inheritedFontSizePx, ) } @@ -1085,7 +1094,7 @@ abstract class DOMNode( context: LengthResolveContext, containerRect: Rect, desiredX: Int, - desiredWidth: Int + desiredWidth: Int, ): Int { val resolution = PositionedLayoutModel.resolveHorizontalOffset(left = leftStyleValue, right = rightStyleValue) val value = resolution.value ?: return desiredX @@ -1101,7 +1110,7 @@ abstract class DOMNode( context: LengthResolveContext, containerRect: Rect, desiredY: Int, - desiredHeight: Int + desiredHeight: Int, ): Int { val resolution = PositionedLayoutModel.resolveVerticalOffset(top = topStyleValue, bottom = bottomStyleValue) val value = resolution.value ?: return desiredY @@ -1114,9 +1123,7 @@ abstract class DOMNode( } /** Measures the node's desired size. */ - internal open fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { - return measure(ctx) - } + internal open fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size = measure(ctx) /** Measures the node's desired size. */ open fun measure(ctx: UiMeasureContext): Size { @@ -1131,7 +1138,13 @@ abstract class DOMNode( } /** Lays out this node and its children for the given bounds. */ - open fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + open fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { if (display == Display.None) { val next = Rect(x, y, 0, 0) if (bounds != next) { @@ -1159,7 +1172,7 @@ abstract class DOMNode( child.resolveLayoutStyleValues( ctx = ctx, parentContentWidth = availableOuterWidth, - parentContentHeight = availableOuterHeight + parentContentHeight = availableOuterHeight, ) val childSize = child.clampMeasuredOuterSize(child.measureForLayout(ctx, availableOuterWidth)) val childX = layoutContentX + child.margin.left @@ -1177,20 +1190,22 @@ abstract class DOMNode( val localClipRect = overflowViewportRect() val inheritedClipRect = currentInheritedChildRenderClipRect() if (localClipRect != null) { - val effectiveClipRect = if (inheritedClipRect != null) { - inheritedClipRect.intersection(localClipRect) ?: return - } else { - localClipRect - } + val effectiveClipRect = + if (inheritedClipRect != null) { + inheritedClipRect.intersection(localClipRect) ?: return + } else { + localClipRect + } if (effectiveClipRect.width <= 0 || effectiveClipRect.height <= 0) { return } - out += RenderCommand.PushClip( - x = effectiveClipRect.x, - y = effectiveClipRect.y, - width = effectiveClipRect.width.coerceAtLeast(0), - height = effectiveClipRect.height.coerceAtLeast(0) - ) + out += + RenderCommand.PushClip( + x = effectiveClipRect.x, + y = effectiveClipRect.y, + width = effectiveClipRect.width.coerceAtLeast(0), + height = effectiveClipRect.height.coerceAtLeast(0), + ) withInheritedChildRenderClipRect(effectiveClipRect) { orderedChildrenForPaintTraversal().forEach { child -> child.appendRenderCommands(ctx, out) @@ -1217,15 +1232,16 @@ abstract class DOMNode( if (transformPushed) { val ox = bounds.x + bounds.width * transformOrigin.originX val oy = bounds.y + bounds.height * transformOrigin.originY - out += RenderCommand.PushTransform( - originX = ox, - originY = oy, - translateX = activeTransform.translateX, - translateY = activeTransform.translateY, - scaleX = activeTransform.scaleX, - scaleY = activeTransform.scaleY, - rotateDeg = activeTransform.rotateDeg - ) + out += + RenderCommand.PushTransform( + originX = ox, + originY = oy, + translateX = activeTransform.translateX, + translateY = activeTransform.translateY, + scaleX = activeTransform.scaleX, + scaleY = activeTransform.scaleY, + rotateDeg = activeTransform.rotateDeg, + ) } if (opacityPushed) { out += RenderCommand.PushOpacity(activeOpacity) @@ -1243,23 +1259,18 @@ abstract class DOMNode( open fun handleClick(event: MouseClickEvent): Boolean = false /** Dispatches a click through this node and its subtree. */ - fun dispatchClick(event: MouseClickEvent): Boolean { - return dispatchClickInternal( + fun dispatchClick(event: MouseClickEvent): Boolean = + dispatchClickInternal( element = this, event = event, parentTransform = AffineTransform2D.IDENTITY, - parentInputClipRect = null + parentInputClipRect = null, ) - } /** Returns true if the mouse event is within current bounds. */ - fun hovered(event: MouseEvent): Boolean { - return isHitTestVisible() && containsGlobalPoint(event.mouseX, event.mouseY) - } + fun hovered(event: MouseEvent): Boolean = isHitTestVisible() && containsGlobalPoint(event.mouseX, event.mouseY) - fun isHitTestVisible(): Boolean { - return !dragHitTestHidden && display != Display.None - } + fun isHitTestVisible(): Boolean = !dragHitTestHidden && display != Display.None /** Applies event handlers from [ComponentProps] to this node. */ fun applyHandlers(props: ComponentProps) { @@ -1275,10 +1286,10 @@ abstract class DOMNode( this@DOMNode.styleDisabled = props.disabled this@DOMNode.draggable = props.draggable this@DOMNode.droppable = props.droppable || - props.onDragEnter != null || - props.onDragOver != null || - props.onDragLeave != null || - props.onDrop != null + props.onDragEnter != null || + props.onDragOver != null || + props.onDragLeave != null || + props.onDrop != null this@DOMNode.dragPreviewMode = props.dragPreviewMode this@DOMNode.hideSourceWhileDragging = props.hideSourceWhileDragging this@DOMNode.dragPreviewBuilder = props.dragPreview @@ -1379,7 +1390,7 @@ abstract class DOMNode( mouseY: Int, mouseDX: Int, mouseDY: Int, - button: MouseButton + button: MouseButton, ) { if (styleDisabled || button != MouseButton.LEFT) return updateScrollbarPointerDrag(mouseX, mouseY) @@ -1584,58 +1595,59 @@ abstract class DOMNode( val existing = styleDefaultsSnapshot if (existing != null) return existing val defaultFontSize = defaultFontSize() - val computed = ComputedStyleDefaults( - margin = LengthInsets.fromInsets(margin), - padding = LengthInsets.fromInsets(padding), - backgroundColor = defaultBackgroundColor(), - backgroundImage = defaultBackgroundImage(), - borderColor = border.color, - borderWidth = CssLength.px(maxOf(border.top, border.right, border.bottom, border.left)), - borderRadius = CssLength.px(borderRadius), - foregroundColor = defaultForegroundColor(), - fontId = defaultFontId(), - fontSize = defaultFontSize, - fontSizeValue = defaultFontSize?.let { CssLength.px(it) }, - fontWeight = fontWeight, - fontStyle = fontStyle, - textDecoration = textDecoration, - obfuscated = textObfuscated, - width = width?.let { CssLength.px(it) }, - height = height?.let { CssLength.px(it) }, - minWidth = minWidth?.let { CssLength.px(it) }, - minHeight = minHeight?.let { CssLength.px(it) }, - maxWidth = maxWidth?.let { CssLength.px(it) }, - maxHeight = maxHeight?.let { CssLength.px(it) }, - align = align, - display = display, - position = position, - left = leftStyleValue, - top = topStyleValue, - right = rightStyleValue, - bottom = bottomStyleValue, - zIndex = zIndex, - flexDirection = flexDirection, - justifyContent = justifyContent, - alignItems = alignItems, - justifyItems = justifyItems, - gap = CssLength.px(gap), - flexGrow = flexGrow, - flexShrink = flexShrink, - flexBasis = flexBasis?.let { CssLength.px(it) }, - gridColumns = gridColumns, - gridRows = gridRows, - gridAutoFlow = gridAutoFlow, - gridColumnSpan = gridColumnSpan, - gridRowSpan = gridRowSpan, - overflow = overflow, - overflowX = overflowX, - overflowY = overflowY, - textWrap = textWrap, - textFormatting = textFormatting, - transform = transform, - transformOrigin = transformOrigin, - opacity = opacity - ) + val computed = + ComputedStyleDefaults( + margin = LengthInsets.fromInsets(margin), + padding = LengthInsets.fromInsets(padding), + backgroundColor = defaultBackgroundColor(), + backgroundImage = defaultBackgroundImage(), + borderColor = border.color, + borderWidth = CssLength.px(maxOf(border.top, border.right, border.bottom, border.left)), + borderRadius = CssLength.px(borderRadius), + foregroundColor = defaultForegroundColor(), + fontId = defaultFontId(), + fontSize = defaultFontSize, + fontSizeValue = defaultFontSize?.let { CssLength.px(it) }, + fontWeight = fontWeight, + fontStyle = fontStyle, + textDecoration = textDecoration, + obfuscated = textObfuscated, + width = width?.let { CssLength.px(it) }, + height = height?.let { CssLength.px(it) }, + minWidth = minWidth?.let { CssLength.px(it) }, + minHeight = minHeight?.let { CssLength.px(it) }, + maxWidth = maxWidth?.let { CssLength.px(it) }, + maxHeight = maxHeight?.let { CssLength.px(it) }, + align = align, + display = display, + position = position, + left = leftStyleValue, + top = topStyleValue, + right = rightStyleValue, + bottom = bottomStyleValue, + zIndex = zIndex, + flexDirection = flexDirection, + justifyContent = justifyContent, + alignItems = alignItems, + justifyItems = justifyItems, + gap = CssLength.px(gap), + flexGrow = flexGrow, + flexShrink = flexShrink, + flexBasis = flexBasis?.let { CssLength.px(it) }, + gridColumns = gridColumns, + gridRows = gridRows, + gridAutoFlow = gridAutoFlow, + gridColumnSpan = gridColumnSpan, + gridRowSpan = gridRowSpan, + overflow = overflow, + overflowX = overflowX, + overflowY = overflowY, + textWrap = textWrap, + textFormatting = textFormatting, + transform = transform, + transformOrigin = transformOrigin, + opacity = opacity, + ) styleDefaultsSnapshot = computed return computed } @@ -1646,7 +1658,7 @@ abstract class DOMNode( StyleAnimationEngine.onComputedStyleApplied(this, previous, style) return NodeStyleApplyResult( visualDirty = false, - layoutDirty = false + layoutDirty = false, ) } marginStyleValue = style.margin @@ -1708,10 +1720,11 @@ abstract class DOMNode( if (previous == null) { return NodeStyleApplyResult( visualDirty = true, - layoutDirty = true + layoutDirty = true, ) } - val layoutDirty = previous.margin != style.margin || + val layoutDirty = + previous.margin != style.margin || previous.padding != style.padding || previous.borderWidth != style.borderWidth || previous.borderColor != style.borderColor || @@ -1755,7 +1768,7 @@ abstract class DOMNode( previous.lineHeight != style.lineHeight return NodeStyleApplyResult( visualDirty = true, - layoutDirty = layoutDirty + layoutDirty = layoutDirty, ) } @@ -1765,8 +1778,8 @@ abstract class DOMNode( val normalizedOpacity = opacity?.coerceIn(0f, 1f) val changed = animatedTransform != transform || - animatedOpacity != normalizedOpacity || - animatedColor != color + animatedOpacity != normalizedOpacity || + animatedColor != color animatedTransform = transform animatedOpacity = normalizedOpacity animatedColor = color @@ -1792,7 +1805,10 @@ abstract class DOMNode( result = 31L * result + scrollOffsetResolvedX.toLong() result = 31L * result + scrollOffsetResolvedY.toLong() result = 31L * result + effectiveTransform().hashCode().toLong() - result = 31L * result + java.lang.Float.floatToIntBits(effectiveOpacity()).toLong() + result = 31L * result + + java.lang.Float + .floatToIntBits(effectiveOpacity()) + .toLong() result = 31L * result + volatileRenderCommandsSignature(nowMs) return result } @@ -1826,7 +1842,7 @@ abstract class DOMNode( } return base.copy( translateX = base.translateX + relativeOffsetX.toFloat() + stickyOffsetX.toFloat(), - translateY = base.translateY + relativeOffsetY.toFloat() + stickyOffsetY.toFloat() + translateY = base.translateY + relativeOffsetY.toFloat() + stickyOffsetY.toFloat(), ) } @@ -1845,7 +1861,11 @@ abstract class DOMNode( val translate = AffineTransform2D.translation(active.translateX, active.translateY) val rotate = AffineTransform2D.rotation(active.rotateDeg) val scale = AffineTransform2D.scale(active.scaleX, active.scaleY) - return translate.times(origin).times(rotate).times(scale).times(originBack) + return translate + .times(origin) + .times(rotate) + .times(scale) + .times(originBack) } fun worldTransformMatrix(): AffineTransform2D { @@ -1894,54 +1914,50 @@ abstract class DOMNode( protected open fun applyFontSize(value: Int?) {} - protected fun resolveFontSize(ctx: UiMeasureContext): Int { - return ctx.fontHeight(fontId, fontSize).coerceAtLeast(1) - } + protected fun resolveFontSize(ctx: UiMeasureContext): Int = ctx.fontHeight(fontId, fontSize).coerceAtLeast(1) - protected fun resolveComputedFontSizePx(): Int { - return (appliedComputedStyleSnapshot()?.fontSize ?: fontSize ?: 16).coerceAtLeast(1) - } + protected fun resolveComputedFontSizePx(): Int = + (appliedComputedStyleSnapshot()?.fontSize ?: fontSize ?: 16).coerceAtLeast(1) - protected fun resolveEffectiveLineHeight(ctx: UiMeasureContext): Int { - return resolveTextMetrics(ctx).lineHeightPx - } + protected fun resolveEffectiveLineHeight(ctx: UiMeasureContext): Int = resolveTextMetrics(ctx).lineHeightPx - protected fun resolveEffectiveLineTopLeading(ctx: UiMeasureContext): Int { - return resolveTextMetrics(ctx).topLeadingPx.roundToInt().coerceAtLeast(0) - } + protected fun resolveEffectiveLineTopLeading(ctx: UiMeasureContext): Int = + resolveTextMetrics(ctx) + .topLeadingPx + .roundToInt() + .coerceAtLeast(0) - protected fun resolveEffectiveAscenderPx(ctx: UiMeasureContext): Float { - return resolveTextMetrics(ctx).ascenderPx - } + protected fun resolveEffectiveAscenderPx(ctx: UiMeasureContext): Float = resolveTextMetrics(ctx).ascenderPx - protected fun resolveEffectiveDescenderPx(ctx: UiMeasureContext): Float { - return resolveTextMetrics(ctx).descenderPx - } + protected fun resolveEffectiveDescenderPx(ctx: UiMeasureContext): Float = resolveTextMetrics(ctx).descenderPx protected fun resolveTextMetrics(ctx: UiMeasureContext): ResolvedTextMetrics { val fontSizePx = resolveComputedFontSizePx().coerceAtLeast(1) val nativeMetrics = resolveNativeFontMetrics(ctx, fontSizePx) val fallbackFontHeightPx = resolveFontSize(ctx).coerceAtLeast(1) - val fallbackNormalLineHeightPx = (fallbackFontHeightPx * NORMAL_LINE_HEIGHT_MULTIPLIER) - .roundToInt() - .coerceAtLeast(fallbackFontHeightPx) - .coerceAtLeast(1) + val fallbackNormalLineHeightPx = + (fallbackFontHeightPx * NORMAL_LINE_HEIGHT_MULTIPLIER) + .roundToInt() + .coerceAtLeast(fallbackFontHeightPx) + .coerceAtLeast(1) val nativeLineHeightPx = nativeMetrics?.lineHeightPx ?: fallbackNormalLineHeightPx val ascenderPx = nativeMetrics?.ascenderPx ?: (fallbackFontHeightPx * 0.8f) - val descenderPx = nativeMetrics?.descenderPx - ?: (nativeLineHeightPx - ascenderPx).coerceAtLeast(0f) + val descenderPx = + nativeMetrics?.descenderPx + ?: (nativeLineHeightPx - ascenderPx).coerceAtLeast(0f) val computedLineHeight = when (val computedLineHeight = appliedComputedStyleSnapshot()?.lineHeight ?: LineHeightValue.Normal) { LineHeightValue.Normal -> nativeLineHeightPx is LineHeightValue.Length -> { val currentFontSizePx = fontSizePx.toFloat() - val context = LengthResolveContext( - rootFontSizePx = currentFontSizePx, - currentFontSizePx = currentFontSizePx, - inheritedFontSizePx = currentFontSizePx - ) + val context = + LengthResolveContext( + rootFontSizePx = currentFontSizePx, + currentFontSizePx = currentFontSizePx, + inheritedFontSizePx = currentFontSizePx, + ) computedLineHeight.value .resolvePx(context, LengthPercentBase.CurrentFontSize) .roundToInt() @@ -1959,7 +1975,7 @@ abstract class DOMNode( ascenderPx = ascenderPx, descenderPx = descenderPx, topLeadingPx = topLeadingPx, - bottomLeadingPx = bottomLeadingPx + bottomLeadingPx = bottomLeadingPx, ) } @@ -1967,42 +1983,47 @@ abstract class DOMNode( val metrics = ctx.fontLineMetrics(fontId, fontSizePx) ?: return null if (metrics.emSize <= 0f || metrics.lineHeightEm <= 0f) return null val scalePx = fontSizePx / metrics.emSize - val lineHeightPx = kotlin.math.ceil(metrics.lineHeightEm * scalePx).toInt().coerceAtLeast(1) + val lineHeightPx = + kotlin.math + .ceil(metrics.lineHeightEm * scalePx) + .toInt() + .coerceAtLeast(1) val ascenderPx = (metrics.ascenderEm * scalePx).coerceAtLeast(0f) val descenderPx = kotlin.math.abs(metrics.descenderEm * scalePx) return NativeFontMetricsPx( lineHeightPx = lineHeightPx, ascenderPx = ascenderPx, - descenderPx = descenderPx + descenderPx = descenderPx, ) } - protected fun parseTextForFormatting(rawText: String): ParsedText { - return MinecraftFormattingParser.parse(rawText, textFormatting) - } + protected fun parseTextForFormatting(rawText: String): ParsedText = + MinecraftFormattingParser.parse(rawText, textFormatting) - protected fun baseTextStyleFlags(): TextStyleFlags { - return TextStyleFlags( + protected fun baseTextStyleFlags(): TextStyleFlags = + TextStyleFlags( bold = fontWeight == FontWeight.Bold, italic = fontStyle == FontStyle.Italic, - underline = textDecoration == TextDecoration.Underline || + underline = + textDecoration == TextDecoration.Underline || textDecoration == TextDecoration.UnderlineStrikethrough, - strikethrough = textDecoration == TextDecoration.Strikethrough || + strikethrough = + textDecoration == TextDecoration.Strikethrough || textDecoration == TextDecoration.UnderlineStrikethrough, - obfuscated = textObfuscated + obfuscated = textObfuscated, ) - } protected fun measureText(ctx: UiMeasureContext, text: String): Int { val metrics = resolveTextMetrics(ctx) val parsed = parseTextForFormatting(text) val plainText = parsed.plainText val base = ctx.measureText(plainText, fontId, metrics.fontSizePx) - val extraBold = TextStyleMetrics.boldExtraPxForRange( - plainText = plainText, - spans = parsed.spans, - baseFlags = baseTextStyleFlags() - ) + val extraBold = + TextStyleMetrics.boldExtraPxForRange( + plainText = plainText, + spans = parsed.spans, + baseFlags = baseTextStyleFlags(), + ) return base + extraBold } @@ -2012,7 +2033,7 @@ abstract class DOMNode( x: Int, y: Int, color: Int, - styleSpans: List = emptyList() + styleSpans: List = emptyList(), ): RenderCommand.DrawText { val metrics = resolveTextMetrics(ctx) val baseFlags = baseTextStyleFlags() @@ -2030,7 +2051,7 @@ abstract class DOMNode( strikethrough = baseFlags.strikethrough, obfuscated = baseFlags.obfuscated, textStyleSpans = styleSpans, - sourceKey = key?.toString() + sourceKey = key?.toString(), ) } @@ -2038,27 +2059,17 @@ abstract class DOMNode( protected fun contentY(): Int = bounds.y + border.top + padding.top - protected fun contentWidth(): Int = - (bounds.width - border.horizontal - padding.horizontal).coerceAtLeast(0) + protected fun contentWidth(): Int = (bounds.width - border.horizontal - padding.horizontal).coerceAtLeast(0) - protected fun contentHeight(): Int = - (bounds.height - border.vertical - padding.vertical).coerceAtLeast(0) + protected fun contentHeight(): Int = (bounds.height - border.vertical - padding.vertical).coerceAtLeast(0) - protected fun viewportContentX(): Int { - return scrollContainerState().viewportRect.x - } + protected fun viewportContentX(): Int = scrollContainerState().viewportRect.x - protected fun viewportContentY(): Int { - return scrollContainerState().viewportRect.y - } + protected fun viewportContentY(): Int = scrollContainerState().viewportRect.y - protected fun viewportContentWidth(): Int { - return scrollContainerState().viewportRect.width - } + protected fun viewportContentWidth(): Int = scrollContainerState().viewportRect.width - protected fun viewportContentHeight(): Int { - return scrollContainerState().viewportRect.height - } + protected fun viewportContentHeight(): Int = scrollContainerState().viewportRect.height protected fun setContentLayoutScroll(scrollX: Int, scrollY: Int) { contentLayoutScrollX = scrollX @@ -2114,13 +2125,11 @@ abstract class DOMNode( return ScrollInvalidationState( layoutDirty = layoutDirty, visualDirty = visualDirty, - interactionDirty = interactionDirty + interactionDirty = interactionDirty, ) } - internal fun consumeScrollLayoutDirtyRecursively(): Boolean { - return consumeScrollInvalidationRecursively().layoutDirty - } + internal fun consumeScrollLayoutDirtyRecursively(): Boolean = consumeScrollInvalidationRecursively().layoutDirty internal fun invalidateStickyVisualOffsetsRecursively() { ScrollPerformanceCounters.incrementStickyVisualInvalidateRuns() @@ -2214,12 +2223,13 @@ abstract class DOMNode( return 0 } var translated = 0 - val next = Rect( - x = bounds.x + deltaX, - y = bounds.y + deltaY, - width = bounds.width, - height = bounds.height - ) + val next = + Rect( + x = bounds.x + deltaX, + y = bounds.y + deltaY, + width = bounds.width, + height = bounds.height, + ) if (next != bounds) { bounds = next markRenderCommandsDirty() @@ -2231,16 +2241,15 @@ abstract class DOMNode( return translated } - internal fun debugScrollAnimationState(): ScrollAnimationDebugState { - return ScrollAnimationDebugState( + internal fun debugScrollAnimationState(): ScrollAnimationDebugState = + ScrollAnimationDebugState( targetX = scrollOffsetTargetX, targetY = scrollOffsetTargetY, displayedX = scrollOffsetDisplayedX, displayedY = scrollOffsetDisplayedY, resolvedX = scrollOffsetResolvedX, - resolvedY = scrollOffsetResolvedY + resolvedY = scrollOffsetResolvedY, ) - } internal fun captureScrollSessionSnapshot(): ScrollSessionSnapshot { val animation = debugScrollAnimationState() @@ -2251,7 +2260,7 @@ abstract class DOMNode( displayedY = if (animation.displayedY.isFinite()) animation.displayedY.coerceAtLeast(0.0) else 0.0, resolvedX = animation.resolvedX.coerceAtLeast(0), resolvedY = animation.resolvedY.coerceAtLeast(0), - dragSession = debugScrollbarDragSession() + dragSession = debugScrollbarDragSession(), ) } @@ -2311,16 +2320,17 @@ abstract class DOMNode( } } else { val nextAxis = if (drag.verticalAxis) ScrollbarAxis.Vertical else ScrollbarAxis.Horizontal - val nextSession = ScrollbarDragSession( - axis = nextAxis, - trackStartPx = drag.trackStartPx, - trackLengthPx = drag.trackLengthPx, - thumbLengthPx = drag.thumbLengthPx, - maxThumbTravelPx = drag.maxThumbTravelPx, - maxScroll = drag.maxScroll.coerceAtLeast(0), - grabOffsetPx = drag.grabOffsetPx.coerceAtLeast(0), - initialResolvedScroll = drag.initialResolvedScroll.coerceAtLeast(0) - ) + val nextSession = + ScrollbarDragSession( + axis = nextAxis, + trackStartPx = drag.trackStartPx, + trackLengthPx = drag.trackLengthPx, + thumbLengthPx = drag.thumbLengthPx, + maxThumbTravelPx = drag.maxThumbTravelPx, + maxScroll = drag.maxScroll.coerceAtLeast(0), + grabOffsetPx = drag.grabOffsetPx.coerceAtLeast(0), + initialResolvedScroll = drag.initialResolvedScroll.coerceAtLeast(0), + ) if (activeScrollbarDragAxis != nextAxis || scrollbarDragSession != nextSession) { activeScrollbarDragAxis = nextAxis scrollbarDragSession = nextSession @@ -2332,17 +2342,13 @@ abstract class DOMNode( markScrollInvalidation( layoutDirty = layoutChanged, visualDirty = true, - interactionDirty = true + interactionDirty = true, ) markRenderCommandsDirty() } } - private fun markScrollInvalidation( - layoutDirty: Boolean, - visualDirty: Boolean, - interactionDirty: Boolean - ) { + private fun markScrollInvalidation(layoutDirty: Boolean, visualDirty: Boolean, interactionDirty: Boolean) { if (layoutDirty) { scrollLayoutDirty = true } @@ -2383,7 +2389,7 @@ abstract class DOMNode( markScrollInvalidation( layoutDirty = false, visualDirty = true, - interactionDirty = true + interactionDirty = true, ) markRenderCommandsDirty() } @@ -2397,14 +2403,16 @@ abstract class DOMNode( scrollContainer = state.axisX.scrollContainer, maxScroll = state.maxScrollX, vertical = false, - dtSeconds = normalizedDt - ) || changed + dtSeconds = normalizedDt, + ) || + changed changed = advanceScrollAnimationAxis( scrollContainer = state.axisY.scrollContainer, maxScroll = state.maxScrollY, vertical = true, - dtSeconds = normalizedDt - ) || changed + dtSeconds = normalizedDt, + ) || + changed return changed } @@ -2412,7 +2420,7 @@ abstract class DOMNode( scrollContainer: Boolean, maxScroll: Int, vertical: Boolean, - dtSeconds: Double + dtSeconds: Double, ): Boolean { if (!scrollContainer) { return normalizeScrollAxisForNonScrollable(vertical) @@ -2441,7 +2449,7 @@ abstract class DOMNode( markScrollInvalidation( layoutDirty = false, visualDirty = true, - interactionDirty = true + interactionDirty = true, ) } return changed @@ -2456,18 +2464,19 @@ abstract class DOMNode( } val target = clampedTarget.toDouble() - val nextDisplayed = if (dtSeconds <= 0.0) { - normalizedDisplayed - } else { - val delta = target - normalizedDisplayed - if (kotlin.math.abs(delta) <= wheelScrollSnapThresholdPx()) { - target + val nextDisplayed = + if (dtSeconds <= 0.0) { + normalizedDisplayed } else { - val alpha = 1.0 - kotlin.math.exp(-wheelScrollSmoothingPerSecond() * dtSeconds) - val eased = normalizedDisplayed + delta * alpha.coerceIn(0.0, 1.0) - if (kotlin.math.abs(target - eased) <= wheelScrollSnapThresholdPx()) target else eased - } - }.coerceIn(0.0, maxScroll.toDouble()) + val delta = target - normalizedDisplayed + if (kotlin.math.abs(delta) <= wheelScrollSnapThresholdPx()) { + target + } else { + val alpha = 1.0 - kotlin.math.exp(-wheelScrollSmoothingPerSecond() * dtSeconds) + val eased = normalizedDisplayed + delta * alpha.coerceIn(0.0, 1.0) + if (kotlin.math.abs(target - eased) <= wheelScrollSnapThresholdPx()) target else eased + } + }.coerceIn(0.0, maxScroll.toDouble()) if (nextDisplayed != displayedScrollAxis(vertical)) { setDisplayedScrollAxis(vertical, nextDisplayed) changed = true @@ -2483,7 +2492,7 @@ abstract class DOMNode( markScrollInvalidation( layoutDirty = false, visualDirty = true, - interactionDirty = true + interactionDirty = true, ) } return changed @@ -2507,32 +2516,27 @@ abstract class DOMNode( markScrollInvalidation( layoutDirty = false, visualDirty = true, - interactionDirty = true + interactionDirty = true, ) markRenderCommandsDirty() } return changed } - private fun targetScrollAxis(vertical: Boolean): Int { - return if (vertical) scrollOffsetTargetY else scrollOffsetTargetX - } + private fun targetScrollAxis(vertical: Boolean): Int = if (vertical) scrollOffsetTargetY else scrollOffsetTargetX - private fun isScrollbarAxisActivelyDragged(vertical: Boolean): Boolean { - return when (activeScrollbarDragAxis) { + private fun isScrollbarAxisActivelyDragged(vertical: Boolean): Boolean = + when (activeScrollbarDragAxis) { ScrollbarAxis.Vertical -> vertical ScrollbarAxis.Horizontal -> !vertical null -> false } - } - private fun displayedScrollAxis(vertical: Boolean): Double { - return if (vertical) scrollOffsetDisplayedY else scrollOffsetDisplayedX - } + private fun displayedScrollAxis(vertical: Boolean): Double = + if (vertical) scrollOffsetDisplayedY else scrollOffsetDisplayedX - private fun resolvedScrollAxis(vertical: Boolean): Int { - return if (vertical) scrollOffsetResolvedY else scrollOffsetResolvedX - } + private fun resolvedScrollAxis(vertical: Boolean): Int = + if (vertical) scrollOffsetResolvedY else scrollOffsetResolvedX private fun setTargetScrollAxis(vertical: Boolean, value: Int) { if (vertical) { @@ -2560,51 +2564,59 @@ abstract class DOMNode( fun scrollContainerState(): ScrollContainerState { ScrollPerformanceCounters.incrementScrollContainerStateCalls() - val baseViewportRect = Rect( - x = contentX(), - y = contentY(), - width = contentWidth(), - height = contentHeight() - ) - val contentExtent = computeContentExtent( - contentOriginX = baseViewportRect.x, - contentOriginY = baseViewportRect.y, - layoutScrollX = contentLayoutScrollX, - layoutScrollY = contentLayoutScrollY - ) - val scrollbarResolution = resolveScrollbarResolution( - overflowX = overflowX, - overflowY = overflowY, - contentExtent = contentExtent, - baseViewportWidth = baseViewportRect.width, - baseViewportHeight = baseViewportRect.height - ) - val viewportRect = Rect( - x = baseViewportRect.x, - y = baseViewportRect.y, - width = scrollbarResolution.viewportWidth, - height = scrollbarResolution.viewportHeight - ) - val axisX = axisStateForOverflow( - overflowMode = overflowX, - scrollbarPresent = scrollbarResolution.horizontalPresent, - scrollbarGutter = scrollbarResolution.horizontalGutter - ) - val axisY = axisStateForOverflow( - overflowMode = overflowY, - scrollbarPresent = scrollbarResolution.verticalPresent, - scrollbarGutter = scrollbarResolution.verticalGutter - ) - val maxScrollX = if (axisX.scrollContainer) { - (contentExtent.width - viewportRect.width).coerceAtLeast(0) - } else { - 0 - } - val maxScrollY = if (axisY.scrollContainer) { - (contentExtent.height - viewportRect.height).coerceAtLeast(0) - } else { - 0 - } + val baseViewportRect = + Rect( + x = contentX(), + y = contentY(), + width = contentWidth(), + height = contentHeight(), + ) + val contentExtent = + computeContentExtent( + contentOriginX = baseViewportRect.x, + contentOriginY = baseViewportRect.y, + layoutScrollX = contentLayoutScrollX, + layoutScrollY = contentLayoutScrollY, + ) + val scrollbarResolution = + resolveScrollbarResolution( + overflowX = overflowX, + overflowY = overflowY, + contentExtent = contentExtent, + baseViewportWidth = baseViewportRect.width, + baseViewportHeight = baseViewportRect.height, + ) + val viewportRect = + Rect( + x = baseViewportRect.x, + y = baseViewportRect.y, + width = scrollbarResolution.viewportWidth, + height = scrollbarResolution.viewportHeight, + ) + val axisX = + axisStateForOverflow( + overflowMode = overflowX, + scrollbarPresent = scrollbarResolution.horizontalPresent, + scrollbarGutter = scrollbarResolution.horizontalGutter, + ) + val axisY = + axisStateForOverflow( + overflowMode = overflowY, + scrollbarPresent = scrollbarResolution.verticalPresent, + scrollbarGutter = scrollbarResolution.verticalGutter, + ) + val maxScrollX = + if (axisX.scrollContainer) { + (contentExtent.width - viewportRect.width).coerceAtLeast(0) + } else { + 0 + } + val maxScrollY = + if (axisY.scrollContainer) { + (contentExtent.height - viewportRect.height).coerceAtLeast(0) + } else { + 0 + } if (axisX.scrollContainer) { var axisChanged = false val axisDragged = isScrollbarAxisActivelyDragged(vertical = false) @@ -2629,7 +2641,7 @@ abstract class DOMNode( markScrollInvalidation( layoutDirty = false, visualDirty = true, - interactionDirty = true + interactionDirty = true, ) markRenderCommandsDirty() } @@ -2660,7 +2672,7 @@ abstract class DOMNode( markScrollInvalidation( layoutDirty = false, visualDirty = true, - interactionDirty = true + interactionDirty = true, ) markRenderCommandsDirty() } @@ -2678,14 +2690,14 @@ abstract class DOMNode( horizontalScrollbarGutter = scrollbarResolution.horizontalGutter, verticalScrollbarGutter = scrollbarResolution.verticalGutter, axisX = axisX, - axisY = axisY + axisY = axisY, ) } private fun axisStateForOverflow( overflowMode: Overflow, scrollbarPresent: Boolean, - scrollbarGutter: Int + scrollbarGutter: Int, ): ScrollAxisState { val scrollContainer = overflowMode != Overflow.Visible val supportsScrollbar = overflowMode == Overflow.Scroll || overflowMode == Overflow.Auto @@ -2694,7 +2706,7 @@ abstract class DOMNode( scrollContainer = scrollContainer, clipsToViewport = scrollContainer, scrollbarPresent = supportsScrollbar && scrollbarPresent, - scrollbarGutter = if (supportsScrollbar && scrollbarPresent) scrollbarGutter.coerceAtLeast(0) else 0 + scrollbarGutter = if (supportsScrollbar && scrollbarPresent) scrollbarGutter.coerceAtLeast(0) else 0, ) } @@ -2719,7 +2731,7 @@ abstract class DOMNode( overflowY: Overflow, contentExtent: Size, baseViewportWidth: Int, - baseViewportHeight: Int + baseViewportHeight: Int, ): ScrollbarResolution { val thickness = scrollbarThicknessPx().coerceAtLeast(0) var horizontalPresent = overflowX == Overflow.Scroll @@ -2727,16 +2739,18 @@ abstract class DOMNode( repeat(3) { val viewportWidth = (baseViewportWidth - if (verticalPresent) thickness else 0).coerceAtLeast(0) val viewportHeight = (baseViewportHeight - if (horizontalPresent) thickness else 0).coerceAtLeast(0) - val nextHorizontal = when (overflowX) { - Overflow.Visible, Overflow.Hidden -> false - Overflow.Scroll -> true - Overflow.Auto -> contentExtent.width > viewportWidth - } - val nextVertical = when (overflowY) { - Overflow.Visible, Overflow.Hidden -> false - Overflow.Scroll -> true - Overflow.Auto -> contentExtent.height > viewportHeight - } + val nextHorizontal = + when (overflowX) { + Overflow.Visible, Overflow.Hidden -> false + Overflow.Scroll -> true + Overflow.Auto -> contentExtent.width > viewportWidth + } + val nextVertical = + when (overflowY) { + Overflow.Visible, Overflow.Hidden -> false + Overflow.Scroll -> true + Overflow.Auto -> contentExtent.height > viewportHeight + } if (nextHorizontal == horizontalPresent && nextVertical == verticalPresent) { return ScrollbarResolution( horizontalPresent = nextHorizontal, @@ -2744,7 +2758,7 @@ abstract class DOMNode( horizontalGutter = if (nextHorizontal) thickness else 0, verticalGutter = if (nextVertical) thickness else 0, viewportWidth = viewportWidth, - viewportHeight = viewportHeight + viewportHeight = viewportHeight, ) } horizontalPresent = nextHorizontal @@ -2758,50 +2772,58 @@ abstract class DOMNode( horizontalGutter = if (horizontalPresent) thickness else 0, verticalGutter = if (verticalPresent) thickness else 0, viewportWidth = viewportWidth, - viewportHeight = viewportHeight + viewportHeight = viewportHeight, ) } private fun scrollbarVisualState(state: ScrollContainerState = scrollContainerState()): ScrollbarVisualState { - val verticalTrack = if (state.axisY.scrollbarPresent && state.verticalScrollbarGutter > 0) { - Rect( - x = state.viewportRect.x + state.viewportRect.width, - y = state.viewportRect.y, - width = state.verticalScrollbarGutter.coerceAtLeast(1), - height = state.viewportRect.height.coerceAtLeast(0) + val verticalTrack = + if (state.axisY.scrollbarPresent && state.verticalScrollbarGutter > 0) { + Rect( + x = state.viewportRect.x + state.viewportRect.width, + y = state.viewportRect.y, + width = state.verticalScrollbarGutter.coerceAtLeast(1), + height = + state.viewportRect.height + .coerceAtLeast(0), + ) + } else { + null + } + val horizontalTrack = + if (state.axisX.scrollbarPresent && state.horizontalScrollbarGutter > 0) { + Rect( + x = state.viewportRect.x, + y = state.viewportRect.y + state.viewportRect.height, + width = + state.viewportRect.width + .coerceAtLeast(0), + height = state.horizontalScrollbarGutter.coerceAtLeast(1), + ) + } else { + null + } + val vertical = + buildScrollbarVisualAxis( + trackRect = verticalTrack, + viewportExtent = state.viewportRect.height, + contentExtent = state.contentExtent.height, + scrollOffset = displayedScrollAxis(vertical = true).coerceIn(0.0, state.maxScrollY.toDouble()), + maxScroll = state.maxScrollY, + verticalAxis = true, ) - } else { - null - } - val horizontalTrack = if (state.axisX.scrollbarPresent && state.horizontalScrollbarGutter > 0) { - Rect( - x = state.viewportRect.x, - y = state.viewportRect.y + state.viewportRect.height, - width = state.viewportRect.width.coerceAtLeast(0), - height = state.horizontalScrollbarGutter.coerceAtLeast(1) + val horizontal = + buildScrollbarVisualAxis( + trackRect = horizontalTrack, + viewportExtent = state.viewportRect.width, + contentExtent = state.contentExtent.width, + scrollOffset = displayedScrollAxis(vertical = false).coerceIn(0.0, state.maxScrollX.toDouble()), + maxScroll = state.maxScrollX, + verticalAxis = false, ) - } else { - null - } - val vertical = buildScrollbarVisualAxis( - trackRect = verticalTrack, - viewportExtent = state.viewportRect.height, - contentExtent = state.contentExtent.height, - scrollOffset = displayedScrollAxis(vertical = true).coerceIn(0.0, state.maxScrollY.toDouble()), - maxScroll = state.maxScrollY, - verticalAxis = true - ) - val horizontal = buildScrollbarVisualAxis( - trackRect = horizontalTrack, - viewportExtent = state.viewportRect.width, - contentExtent = state.contentExtent.width, - scrollOffset = displayedScrollAxis(vertical = false).coerceIn(0.0, state.maxScrollX.toDouble()), - maxScroll = state.maxScrollX, - verticalAxis = false - ) return ScrollbarVisualState( horizontal = horizontal, - vertical = vertical + vertical = vertical, ) } @@ -2811,93 +2833,108 @@ abstract class DOMNode( contentExtent: Int, scrollOffset: Double, maxScroll: Int, - verticalAxis: Boolean + verticalAxis: Boolean, ): ScrollbarVisualAxis? { val track = trackRect ?: return null val trackExtent = if (verticalAxis) track.height else track.width if (trackExtent <= 0) return null val minThumb = minScrollbarThumbSizePx().coerceAtLeast(1) - val rawThumbExtent = if (contentExtent <= 0 || viewportExtent <= 0) { - trackExtent - } else { - val ratio = viewportExtent.toFloat() / contentExtent.toFloat() - (trackExtent.toFloat() * ratio).roundToInt() - } + val rawThumbExtent = + if (contentExtent <= 0 || viewportExtent <= 0) { + trackExtent + } else { + val ratio = viewportExtent.toFloat() / contentExtent.toFloat() + (trackExtent.toFloat() * ratio).roundToInt() + } val thumbExtent = rawThumbExtent.coerceIn(minThumb.coerceAtMost(trackExtent), trackExtent) val thumbTravel = (trackExtent - thumbExtent).coerceAtLeast(0) - val thumbOffset = if (maxScroll <= 0 || thumbTravel <= 0) { - 0 - } else { - val ratio = (scrollOffset.coerceIn(0.0, maxScroll.toDouble()) / maxScroll.toDouble()).toFloat() - (ratio * thumbTravel.toFloat()).roundToInt().coerceIn(0, thumbTravel) - } - val thumbRect = if (verticalAxis) { - Rect(track.x, track.y + thumbOffset, track.width, thumbExtent) - } else { - Rect(track.x + thumbOffset, track.y, thumbExtent, track.height) - } + val thumbOffset = + if (maxScroll <= 0 || thumbTravel <= 0) { + 0 + } else { + val ratio = (scrollOffset.coerceIn(0.0, maxScroll.toDouble()) / maxScroll.toDouble()).toFloat() + (ratio * thumbTravel.toFloat()).roundToInt().coerceIn(0, thumbTravel) + } + val thumbRect = + if (verticalAxis) { + Rect(track.x, track.y + thumbOffset, track.width, thumbExtent) + } else { + Rect(track.x + thumbOffset, track.y, thumbExtent, track.height) + } return ScrollbarVisualAxis( trackRect = track, thumbRect = thumbRect, maxScroll = maxScroll.coerceAtLeast(0), - scrollOffset = scrollOffset.roundToInt().coerceAtLeast(0) + scrollOffset = scrollOffset.roundToInt().coerceAtLeast(0), ) } private fun appendScrollbarCommands(out: MutableList, state: ScrollContainerState) { val visuals = scrollbarVisualState(state) visuals.vertical?.let { axis -> - out += RenderCommand.DrawRect( - x = axis.trackRect.x, - y = axis.trackRect.y, - width = axis.trackRect.width, - height = axis.trackRect.height, - color = scrollbarTrackColor() - ) - val thumbColor = if (activeScrollbarDragAxis == ScrollbarAxis.Vertical) { - scrollbarThumbActiveColor() - } else { - scrollbarThumbColor() - } - out += RenderCommand.DrawRect( - x = axis.thumbRect.x, - y = axis.thumbRect.y, - width = axis.thumbRect.width, - height = axis.thumbRect.height, - color = thumbColor - ) + out += + RenderCommand.DrawRect( + x = axis.trackRect.x, + y = axis.trackRect.y, + width = axis.trackRect.width, + height = axis.trackRect.height, + color = scrollbarTrackColor(), + ) + val thumbColor = + if (activeScrollbarDragAxis == ScrollbarAxis.Vertical) { + scrollbarThumbActiveColor() + } else { + scrollbarThumbColor() + } + out += + RenderCommand.DrawRect( + x = axis.thumbRect.x, + y = axis.thumbRect.y, + width = axis.thumbRect.width, + height = axis.thumbRect.height, + color = thumbColor, + ) } visuals.horizontal?.let { axis -> - out += RenderCommand.DrawRect( - x = axis.trackRect.x, - y = axis.trackRect.y, - width = axis.trackRect.width, - height = axis.trackRect.height, - color = scrollbarTrackColor() - ) - val thumbColor = if (activeScrollbarDragAxis == ScrollbarAxis.Horizontal) { - scrollbarThumbActiveColor() - } else { - scrollbarThumbColor() - } - out += RenderCommand.DrawRect( - x = axis.thumbRect.x, - y = axis.thumbRect.y, - width = axis.thumbRect.width, - height = axis.thumbRect.height, - color = thumbColor - ) + out += + RenderCommand.DrawRect( + x = axis.trackRect.x, + y = axis.trackRect.y, + width = axis.trackRect.width, + height = axis.trackRect.height, + color = scrollbarTrackColor(), + ) + val thumbColor = + if (activeScrollbarDragAxis == ScrollbarAxis.Horizontal) { + scrollbarThumbActiveColor() + } else { + scrollbarThumbColor() + } + out += + RenderCommand.DrawRect( + x = axis.thumbRect.x, + y = axis.thumbRect.y, + width = axis.thumbRect.width, + height = axis.thumbRect.height, + color = thumbColor, + ) } } private fun resolveScrollbarDragAxisAt(mouseX: Int, mouseY: Int): ScrollbarAxis? { if (!containsGlobalPoint(mouseX, mouseY)) return null val visuals = scrollbarVisualState() - if (visuals.vertical?.trackRect?.contains(mouseX, mouseY) == true) { + if (visuals.vertical + ?.trackRect + ?.contains(mouseX, mouseY) == true + ) { return ScrollbarAxis.Vertical } - if (visuals.horizontal?.trackRect?.contains(mouseX, mouseY) == true) { + if (visuals.horizontal + ?.trackRect + ?.contains(mouseX, mouseY) == true + ) { return ScrollbarAxis.Horizontal } return null @@ -2908,12 +2945,13 @@ abstract class DOMNode( val vertical = visuals.vertical if (vertical != null && vertical.trackRect.contains(mouseX, mouseY)) { activeScrollbarDragAxis = ScrollbarAxis.Vertical - scrollbarDragSession = beginScrollbarDragSession( - axis = ScrollbarAxis.Vertical, - visual = vertical, - pointerPx = mouseY, - pointerInsideThumb = vertical.thumbRect.contains(mouseX, mouseY) - ) + scrollbarDragSession = + beginScrollbarDragSession( + axis = ScrollbarAxis.Vertical, + visual = vertical, + pointerPx = mouseY, + pointerInsideThumb = vertical.thumbRect.contains(mouseX, mouseY), + ) markRenderCommandsDirty() updateScrollbarPointerDrag(mouseX, mouseY) return true @@ -2922,12 +2960,13 @@ abstract class DOMNode( val horizontal = visuals.horizontal if (horizontal != null && horizontal.trackRect.contains(mouseX, mouseY)) { activeScrollbarDragAxis = ScrollbarAxis.Horizontal - scrollbarDragSession = beginScrollbarDragSession( - axis = ScrollbarAxis.Horizontal, - visual = horizontal, - pointerPx = mouseX, - pointerInsideThumb = horizontal.thumbRect.contains(mouseX, mouseY) - ) + scrollbarDragSession = + beginScrollbarDragSession( + axis = ScrollbarAxis.Horizontal, + visual = horizontal, + pointerPx = mouseX, + pointerInsideThumb = horizontal.thumbRect.contains(mouseX, mouseY), + ) markRenderCommandsDirty() updateScrollbarPointerDrag(mouseX, mouseY) return true @@ -2939,33 +2978,37 @@ abstract class DOMNode( axis: ScrollbarAxis, visual: ScrollbarVisualAxis, pointerPx: Int, - pointerInsideThumb: Boolean + pointerInsideThumb: Boolean, ): ScrollbarDragSession { val track = visual.trackRect val thumb = visual.thumbRect - val trackLengthPx = if (axis == ScrollbarAxis.Vertical) { - track.height.coerceAtLeast(0) - } else { - track.width.coerceAtLeast(0) - } - val thumbLengthPx = if (axis == ScrollbarAxis.Vertical) { - thumb.height.coerceAtLeast(1) - } else { - thumb.width.coerceAtLeast(1) - } + val trackLengthPx = + if (axis == ScrollbarAxis.Vertical) { + track.height.coerceAtLeast(0) + } else { + track.width.coerceAtLeast(0) + } + val thumbLengthPx = + if (axis == ScrollbarAxis.Vertical) { + thumb.height.coerceAtLeast(1) + } else { + thumb.width.coerceAtLeast(1) + } val trackStartPx = if (axis == ScrollbarAxis.Vertical) track.y else track.x val thumbStartPx = if (axis == ScrollbarAxis.Vertical) thumb.y else thumb.x val maxThumbTravelPx = (trackLengthPx - thumbLengthPx).coerceAtLeast(0) - val grabOffsetPx = if (pointerInsideThumb) { - (pointerPx - thumbStartPx).coerceIn(0, (thumbLengthPx - 1).coerceAtLeast(0)) - } else { - (thumbLengthPx / 2).coerceAtLeast(0) - } - val initialResolvedScroll = if (axis == ScrollbarAxis.Vertical) { - resolvedScrollAxis(vertical = true) - } else { - resolvedScrollAxis(vertical = false) - } + val grabOffsetPx = + if (pointerInsideThumb) { + (pointerPx - thumbStartPx).coerceIn(0, (thumbLengthPx - 1).coerceAtLeast(0)) + } else { + (thumbLengthPx / 2).coerceAtLeast(0) + } + val initialResolvedScroll = + if (axis == ScrollbarAxis.Vertical) { + resolvedScrollAxis(vertical = true) + } else { + resolvedScrollAxis(vertical = false) + } return ScrollbarDragSession( axis = axis, trackStartPx = trackStartPx, @@ -2974,24 +3017,26 @@ abstract class DOMNode( maxThumbTravelPx = maxThumbTravelPx, maxScroll = visual.maxScroll.coerceAtLeast(0), grabOffsetPx = grabOffsetPx, - initialResolvedScroll = initialResolvedScroll + initialResolvedScroll = initialResolvedScroll, ) } private fun updateScrollbarPointerDrag(mouseX: Int, mouseY: Int) { val session = scrollbarDragSession ?: return val pointerAxisPx = if (session.axis == ScrollbarAxis.Vertical) mouseY else mouseX - val desiredThumbStartPx = if (session.maxThumbTravelPx <= 0) { - 0 - } else { - (pointerAxisPx - session.trackStartPx - session.grabOffsetPx).coerceIn(0, session.maxThumbTravelPx) - } - val desiredScroll = if (session.maxThumbTravelPx <= 0 || session.maxScroll <= 0) { - 0 - } else { - val ratio = desiredThumbStartPx.toDouble() / session.maxThumbTravelPx.toDouble() - (ratio * session.maxScroll.toDouble()).roundToInt().coerceIn(0, session.maxScroll) - } + val desiredThumbStartPx = + if (session.maxThumbTravelPx <= 0) { + 0 + } else { + (pointerAxisPx - session.trackStartPx - session.grabOffsetPx).coerceIn(0, session.maxThumbTravelPx) + } + val desiredScroll = + if (session.maxThumbTravelPx <= 0 || session.maxScroll <= 0) { + 0 + } else { + val ratio = desiredThumbStartPx.toDouble() / session.maxThumbTravelPx.toDouble() + (ratio * session.maxScroll.toDouble()).roundToInt().coerceIn(0, session.maxScroll) + } var nextScrollX = resolvedScrollAxis(vertical = false) var nextScrollY = resolvedScrollAxis(vertical = true) @@ -3002,14 +3047,11 @@ abstract class DOMNode( } applyDragScrollOffsets( scrollX = nextScrollX, - scrollY = nextScrollY + scrollY = nextScrollY, ) } - private fun applyDragScrollOffsets( - scrollX: Int, - scrollY: Int - ) { + private fun applyDragScrollOffsets(scrollX: Int, scrollY: Int) { val resolvedX = scrollX.coerceAtLeast(0) val resolvedY = scrollY.coerceAtLeast(0) val clampedX = resolvedX.toDouble() @@ -3043,7 +3085,7 @@ abstract class DOMNode( markScrollInvalidation( layoutDirty = false, visualDirty = true, - interactionDirty = true + interactionDirty = true, ) markRenderCommandsDirty() } @@ -3053,7 +3095,7 @@ abstract class DOMNode( contentOriginX: Int, contentOriginY: Int, layoutScrollX: Int, - layoutScrollY: Int + layoutScrollY: Int, ): Size { var maxWidth = 0 var maxHeight = 0 @@ -3081,8 +3123,22 @@ abstract class DOMNode( return Rect( x = if (clipX) state.viewportRect.x else root.bounds.x, y = if (clipY) state.viewportRect.y else root.bounds.y, - width = if (clipX) state.viewportRect.width.coerceAtLeast(0) else root.bounds.width.coerceAtLeast(0), - height = if (clipY) state.viewportRect.height.coerceAtLeast(0) else root.bounds.height.coerceAtLeast(0) + width = + if (clipX) { + state.viewportRect.width + .coerceAtLeast(0) + } else { + root.bounds.width + .coerceAtLeast(0) + }, + height = + if (clipY) { + state.viewportRect.height + .coerceAtLeast(0) + } else { + root.bounds.height + .coerceAtLeast(0) + }, ) } @@ -3126,11 +3182,12 @@ abstract class DOMNode( while (current != null) { val clipRect = current.overflowViewportRect() if (clipRect != null) { - effectiveRect = if (effectiveRect == null) { - clipRect - } else { - effectiveRect.intersection(clipRect) ?: return Rect(0, 0, 0, 0) - } + effectiveRect = + if (effectiveRect == null) { + clipRect + } else { + effectiveRect.intersection(clipRect) ?: return Rect(0, 0, 0, 0) + } } current = current.parent } @@ -3146,9 +3203,7 @@ abstract class DOMNode( } } - internal fun debugScrollbarVisualState(): ScrollbarVisualState { - return scrollbarVisualState() - } + internal fun debugScrollbarVisualState(): ScrollbarVisualState = scrollbarVisualState() internal fun debugScrollbarDragSession(): ScrollbarDragSessionDebugState? { val session = scrollbarDragSession ?: return null @@ -3160,7 +3215,7 @@ abstract class DOMNode( maxThumbTravelPx = session.maxThumbTravelPx, maxScroll = session.maxScroll, grabOffsetPx = session.grabOffsetPx, - initialResolvedScroll = session.initialResolvedScroll + initialResolvedScroll = session.initialResolvedScroll, ) } @@ -3265,21 +3320,23 @@ abstract class DOMNode( } } - private fun copyStyleDecls(source: StyleDeclarations): StyleDeclarations { - return StyleDeclarations( - values = linkedMapOf().apply { - putAll(source.values) - }, - importantProperties = linkedSetOf().apply { - addAll(source.importantProperties) - } + private fun copyStyleDecls(source: StyleDeclarations): StyleDeclarations = + StyleDeclarations( + values = + linkedMapOf().apply { + putAll(source.values) + }, + importantProperties = + linkedSetOf().apply { + addAll(source.importantProperties) + }, ) - } } private fun parseClassNames(value: String): Set { if (value.isBlank()) return emptySet() - return value.trim() + return value + .trim() .split(Regex("\\s+")) .filter { it.isNotBlank() } .toSet() @@ -3295,4 +3352,3 @@ fun T.applyParent(parent: DOMNode?): T { } return this } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/DomElementEvents.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/DomElementEvents.kt index 237496a..562a6ec 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/DomElementEvents.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/DomElementEvents.kt @@ -117,4 +117,4 @@ fun DOMNode.onDragLeave(handler: (DragLeaveEvent) -> Unit) { /** Registers a drop handler for this node. */ fun DOMNode.onDrop(handler: (DropEvent) -> Unit) { EventBus.run { this@onDrop.addEventListener(Events.DROP, handler) } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutModel.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutModel.kt index 1c7825f..525f00c 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutModel.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutModel.kt @@ -5,33 +5,34 @@ import org.dreamfinity.dsgl.core.style.PositionMode import org.dreamfinity.dsgl.core.style.StyleProperty internal object PositionedLayoutModel { - private fun isLayoutRuntimePositionedMode(mode: PositionMode): Boolean { - return when (mode) { + private fun isLayoutRuntimePositionedMode(mode: PositionMode): Boolean = + when (mode) { PositionMode.Relative, PositionMode.Absolute, - PositionMode.Fixed -> true + PositionMode.Fixed, + -> true PositionMode.Static, - PositionMode.Sticky -> false + PositionMode.Sticky, + -> false } - } - private fun isOrderingPositionedMode(mode: PositionMode): Boolean { - return when (mode) { + private fun isOrderingPositionedMode(mode: PositionMode): Boolean = + when (mode) { PositionMode.Static -> false PositionMode.Relative, PositionMode.Absolute, PositionMode.Fixed, - PositionMode.Sticky -> true + PositionMode.Sticky, + -> true } - } data class RootStackingContextId( - val rootNode: DOMNode + val rootNode: DOMNode, ) enum class StackingParticipantKind { LocalNode, - ChildContext + ChildContext, } data class StackingParticipant( @@ -41,47 +42,42 @@ internal object PositionedLayoutModel { val priority: OrderingPriority, val kind: StackingParticipantKind, val createsChildContextHint: Boolean, - val rootContextPromotionTarget: RootStackingContextId? + val rootContextPromotionTarget: RootStackingContextId?, ) data class StackingContext( val id: RootStackingContextId, val ownerNode: DOMNode, val rootNode: DOMNode, - val participants: List + val participants: List, ) data class OffsetPrecedenceResolution( val sourceProperty: StyleProperty?, - val value: CssLength? + val value: CssLength?, ) data class OrderingPriority( val positionedBucket: Int, val zIndex: Int, - val domOrder: Int + val domOrder: Int, ) data class ChildEntry( val node: DOMNode, - val priority: OrderingPriority + val priority: OrderingPriority, ) - fun isPositioned(node: DOMNode): Boolean { - return isOrderingPositionedMode(node.position) - } + fun isPositioned(node: DOMNode): Boolean = isOrderingPositionedMode(node.position) - private fun effectiveOrderingZIndex(node: DOMNode): Int { - return if (isPositioned(node)) node.zIndex else 0 - } + private fun effectiveOrderingZIndex(node: DOMNode): Int = if (isPositioned(node)) node.zIndex else 0 - fun orderingPriority(node: DOMNode, domOrder: Int): OrderingPriority { - return OrderingPriority( + fun orderingPriority(node: DOMNode, domOrder: Int): OrderingPriority = + OrderingPriority( positionedBucket = if (isPositioned(node)) 1 else 0, zIndex = effectiveOrderingZIndex(node), - domOrder = domOrder + domOrder = domOrder, ) - } fun rootStackingScope(node: DOMNode): DOMNode { var current = node @@ -91,31 +87,28 @@ internal object PositionedLayoutModel { return current } - fun sharesRootStackingScope(first: DOMNode, second: DOMNode): Boolean { - return rootStackingScope(first) === rootStackingScope(second) - } + fun sharesRootStackingScope(first: DOMNode, second: DOMNode): Boolean = + rootStackingScope(first) === rootStackingScope(second) - fun rootStackingContextId(node: DOMNode): RootStackingContextId { - return RootStackingContextId(rootNode = rootStackingScope(node)) - } + fun rootStackingContextId(node: DOMNode): RootStackingContextId = + RootStackingContextId(rootNode = rootStackingScope(node)) - fun matchesChildContextTrigger(node: DOMNode): Boolean { - return isOrderingPositionedMode(node.position) && node.zIndex != 0 - } + fun matchesChildContextTrigger(node: DOMNode): Boolean = isOrderingPositionedMode(node.position) && node.zIndex != 0 fun stackingContextScaffold(owner: DOMNode): StackingContext { val root = rootStackingScope(owner) val contextId = RootStackingContextId(rootNode = root) - val participants = if (owner.parent == null) { - rootContextParticipants(owner, contextId) - } else { - localContextParticipants(owner) - } + val participants = + if (owner.parent == null) { + rootContextParticipants(owner, contextId) + } else { + localContextParticipants(owner) + } return StackingContext( id = contextId, ownerNode = owner, rootNode = root, - participants = participants + participants = participants, ) } @@ -130,9 +123,7 @@ internal object PositionedLayoutModel { return rootStackingScope(node) } - fun fixedViewportRoot(node: DOMNode): DOMNode { - return rootStackingScope(node) - } + fun fixedViewportRoot(node: DOMNode): DOMNode = rootStackingScope(node) private fun createsChildContextForLocalParticipation(node: DOMNode): Boolean { if (node.position == PositionMode.Fixed) { @@ -141,8 +132,9 @@ internal object PositionedLayoutModel { return matchesChildContextTrigger(node) } - private fun localContextParticipants(owner: DOMNode): List { - return owner.children.withIndex() + private fun localContextParticipants(owner: DOMNode): List = + owner.children + .withIndex() .filter { indexed -> indexed.value.position != PositionMode.Fixed } .map { indexed -> val child = indexed.value @@ -152,57 +144,62 @@ internal object PositionedLayoutModel { logicalParent = owner, sourceDomOrder = indexed.index, priority = orderingPriority(child, indexed.index), - kind = if (createsChildContextHint) { - StackingParticipantKind.ChildContext - } else { - StackingParticipantKind.LocalNode - }, + kind = + if (createsChildContextHint) { + StackingParticipantKind.ChildContext + } else { + StackingParticipantKind.LocalNode + }, createsChildContextHint = createsChildContextHint, - rootContextPromotionTarget = null + rootContextPromotionTarget = null, ) } - } private fun rootContextParticipants(root: DOMNode, contextId: RootStackingContextId): List { val globalDomOrder = buildGlobalDomOrderMap(root) - val localParticipants = root.children.withIndex() - .filter { indexed -> indexed.value.position != PositionMode.Fixed } - .map { indexed -> - val child = indexed.value - val domOrder = globalDomOrder[child] ?: indexed.index - val createsChildContextHint = createsChildContextForLocalParticipation(child) - StackingParticipant( - node = child, - logicalParent = root, - sourceDomOrder = domOrder, - priority = orderingPriority(child, domOrder), - kind = if (createsChildContextHint) { - StackingParticipantKind.ChildContext - } else { - StackingParticipantKind.LocalNode - }, - createsChildContextHint = createsChildContextHint, - rootContextPromotionTarget = null - ) - } - val promotedFixedParticipants = collectPromotedFixedNodes(root) - .map { fixed -> - val domOrder = globalDomOrder[fixed] ?: Int.MAX_VALUE - StackingParticipant( - node = fixed, - logicalParent = fixed.parent ?: root, - sourceDomOrder = domOrder, - priority = orderingPriority(fixed, domOrder), - kind = StackingParticipantKind.ChildContext, - createsChildContextHint = true, - rootContextPromotionTarget = contextId - ) - } + val localParticipants = + root.children + .withIndex() + .filter { indexed -> indexed.value.position != PositionMode.Fixed } + .map { indexed -> + val child = indexed.value + val domOrder = globalDomOrder[child] ?: indexed.index + val createsChildContextHint = createsChildContextForLocalParticipation(child) + StackingParticipant( + node = child, + logicalParent = root, + sourceDomOrder = domOrder, + priority = orderingPriority(child, domOrder), + kind = + if (createsChildContextHint) { + StackingParticipantKind.ChildContext + } else { + StackingParticipantKind.LocalNode + }, + createsChildContextHint = createsChildContextHint, + rootContextPromotionTarget = null, + ) + } + val promotedFixedParticipants = + collectPromotedFixedNodes(root) + .map { fixed -> + val domOrder = globalDomOrder[fixed] ?: Int.MAX_VALUE + StackingParticipant( + node = fixed, + logicalParent = fixed.parent ?: root, + sourceDomOrder = domOrder, + priority = orderingPriority(fixed, domOrder), + kind = StackingParticipantKind.ChildContext, + createsChildContextHint = true, + rootContextPromotionTarget = contextId, + ) + } return localParticipants + promotedFixedParticipants } private fun collectPromotedFixedNodes(root: DOMNode): List { val out = ArrayList() + fun visit(node: DOMNode) { node.children.forEach { child -> if (child.position == PositionMode.Fixed) { @@ -218,6 +215,7 @@ internal object PositionedLayoutModel { private fun buildGlobalDomOrderMap(root: DOMNode): Map { val order = LinkedHashMap() var cursor = 0 + fun visit(node: DOMNode) { node.children.forEach { child -> order[child] = cursor @@ -229,21 +227,19 @@ internal object PositionedLayoutModel { return order } - fun resolveHorizontalOffset(left: CssLength?, right: CssLength?): OffsetPrecedenceResolution { - return when { + fun resolveHorizontalOffset(left: CssLength?, right: CssLength?): OffsetPrecedenceResolution = + when { left != null -> OffsetPrecedenceResolution(StyleProperty.LEFT, left) right != null -> OffsetPrecedenceResolution(StyleProperty.RIGHT, right) else -> OffsetPrecedenceResolution(null, null) } - } - fun resolveVerticalOffset(top: CssLength?, bottom: CssLength?): OffsetPrecedenceResolution { - return when { + fun resolveVerticalOffset(top: CssLength?, bottom: CssLength?): OffsetPrecedenceResolution = + when { top != null -> OffsetPrecedenceResolution(StyleProperty.TOP, top) bottom != null -> OffsetPrecedenceResolution(StyleProperty.BOTTOM, bottom) else -> OffsetPrecedenceResolution(null, null) } - } fun orderedParticipantsForPaint(owner: DOMNode): List { val participants = stackingContextScaffold(owner).participants @@ -259,20 +255,18 @@ internal object PositionedLayoutModel { compareBy( { it.priority.positionedBucket }, { it.priority.zIndex }, - { it.priority.domOrder } - ) + { it.priority.domOrder }, + ), ) } - fun orderedParticipantsForHitTesting(owner: DOMNode): List { - return orderedParticipantsForPaint(owner).asReversed() - } + fun orderedParticipantsForHitTesting(owner: DOMNode): List = + orderedParticipantsForPaint(owner).asReversed() - fun orderedChildrenForPaint(parent: DOMNode): List { - return orderedParticipantsForPaint(parent).map { it.node } - } + fun orderedChildrenForPaint(parent: DOMNode): List = orderedParticipantsForPaint(parent).map { it.node } - fun orderedChildrenForHitTesting(parent: DOMNode): List { - return orderedParticipantsForHitTesting(parent).map { it.node } - } + fun orderedChildrenForHitTesting(parent: DOMNode): List = + orderedParticipantsForHitTesting(parent).map { + it.node + } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/StickyLayoutModel.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/StickyLayoutModel.kt index c90136b..b43e265 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/StickyLayoutModel.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/StickyLayoutModel.kt @@ -6,25 +6,25 @@ import org.dreamfinity.dsgl.core.style.StyleProperty internal object StickyLayoutModel { enum class PositionedGeometryIntegrationPoint { - SharedUsedGeometryTransform + SharedUsedGeometryTransform, } enum class StickyInsetAxisMode { Inactive, Top, - Bottom + Bottom, } enum class StickyHorizontalInsetAxisMode { Inactive, Left, - Right + Right, } data class StickyInsetResolution( val mode: StickyInsetAxisMode, val sourceProperty: StyleProperty?, - val value: CssLength? + val value: CssLength?, ) { val active: Boolean get() = mode != StickyInsetAxisMode.Inactive && sourceProperty != null && value != null @@ -33,7 +33,7 @@ internal object StickyLayoutModel { data class StickyHorizontalInsetResolution( val mode: StickyHorizontalInsetAxisMode, val sourceProperty: StyleProperty?, - val value: CssLength? + val value: CssLength?, ) { val active: Boolean get() = mode != StickyHorizontalInsetAxisMode.Inactive && sourceProperty != null && value != null @@ -41,13 +41,13 @@ internal object StickyLayoutModel { data class StickyReferenceScrollContainers( val horizontal: DOMNode, - val vertical: DOMNode + val vertical: DOMNode, ) fun nearestStickyScrollContainers( node: DOMNode, resolveHorizontal: Boolean = true, - resolveVertical: Boolean = true + resolveVertical: Boolean = true, ): StickyReferenceScrollContainers { val root = PositionedLayoutModel.rootStackingScope(node) var horizontal: DOMNode? = if (resolveHorizontal) null else root @@ -65,77 +65,76 @@ internal object StickyLayoutModel { } return StickyReferenceScrollContainers( horizontal = horizontal ?: root, - vertical = vertical ?: root + vertical = vertical ?: root, ) } - fun nearestStickyScrollContainerHorizontal(node: DOMNode): DOMNode { - return nearestStickyScrollContainers( + fun nearestStickyScrollContainerHorizontal(node: DOMNode): DOMNode = + nearestStickyScrollContainers( node = node, resolveHorizontal = true, - resolveVertical = false + resolveVertical = false, ).horizontal - } - fun nearestStickyScrollContainerVertical(node: DOMNode): DOMNode { - return nearestStickyScrollContainers( + fun nearestStickyScrollContainerVertical(node: DOMNode): DOMNode = + nearestStickyScrollContainers( node = node, resolveHorizontal = false, - resolveVertical = true + resolveVertical = true, ).vertical - } - - fun stickyContainingBlock(node: DOMNode): DOMNode { - return node.parent ?: PositionedLayoutModel.rootStackingScope(node) - } - fun resolveHorizontalInsets(left: CssLength?, right: CssLength?): StickyHorizontalInsetResolution { - return when { - left != null -> StickyHorizontalInsetResolution( - mode = StickyHorizontalInsetAxisMode.Left, - sourceProperty = StyleProperty.LEFT, - value = left - ) - - right != null -> StickyHorizontalInsetResolution( - mode = StickyHorizontalInsetAxisMode.Right, - sourceProperty = StyleProperty.RIGHT, - value = right - ) - - else -> StickyHorizontalInsetResolution( - mode = StickyHorizontalInsetAxisMode.Inactive, - sourceProperty = null, - value = null - ) + fun stickyContainingBlock(node: DOMNode): DOMNode = node.parent ?: PositionedLayoutModel.rootStackingScope(node) + + fun resolveHorizontalInsets(left: CssLength?, right: CssLength?): StickyHorizontalInsetResolution = + when { + left != null -> + StickyHorizontalInsetResolution( + mode = StickyHorizontalInsetAxisMode.Left, + sourceProperty = StyleProperty.LEFT, + value = left, + ) + + right != null -> + StickyHorizontalInsetResolution( + mode = StickyHorizontalInsetAxisMode.Right, + sourceProperty = StyleProperty.RIGHT, + value = right, + ) + + else -> + StickyHorizontalInsetResolution( + mode = StickyHorizontalInsetAxisMode.Inactive, + sourceProperty = null, + value = null, + ) } - } - fun resolveVerticalInsets(top: CssLength?, bottom: CssLength?): StickyInsetResolution { - return when { - top != null -> StickyInsetResolution( - mode = StickyInsetAxisMode.Top, - sourceProperty = StyleProperty.TOP, - value = top - ) - - bottom != null -> StickyInsetResolution( - mode = StickyInsetAxisMode.Bottom, - sourceProperty = StyleProperty.BOTTOM, - value = bottom - ) - - else -> StickyInsetResolution( - mode = StickyInsetAxisMode.Inactive, - sourceProperty = null, - value = null - ) + fun resolveVerticalInsets(top: CssLength?, bottom: CssLength?): StickyInsetResolution = + when { + top != null -> + StickyInsetResolution( + mode = StickyInsetAxisMode.Top, + sourceProperty = StyleProperty.TOP, + value = top, + ) + + bottom != null -> + StickyInsetResolution( + mode = StickyInsetAxisMode.Bottom, + sourceProperty = StyleProperty.BOTTOM, + value = bottom, + ) + + else -> + StickyInsetResolution( + mode = StickyInsetAxisMode.Inactive, + sourceProperty = null, + value = null, + ) } - } - fun positionedGeometryIntegrationPoint(): PositionedGeometryIntegrationPoint { - return PositionedGeometryIntegrationPoint.SharedUsedGeometryTransform - } + fun positionedGeometryIntegrationPoint(): PositionedGeometryIntegrationPoint = + PositionedGeometryIntegrationPoint.SharedUsedGeometryTransform fun resolveVerticalVisualOffsetPx( baseY: Int, @@ -143,25 +142,26 @@ internal object StickyLayoutModel { viewportRect: Rect, containingBlockRect: Rect, insetResolution: StickyInsetResolution, - insetPx: Int + insetPx: Int, ): Int { if (!insetResolution.active) return 0 val minY = containingBlockRect.y val maxY = (containingBlockRect.y + containingBlockRect.height - nodeHeight).coerceAtLeast(minY) - val targetY = when (insetResolution.mode) { - StickyInsetAxisMode.Top -> { - val thresholdY = viewportRect.y + insetPx - maxOf(baseY, thresholdY) + val targetY = + when (insetResolution.mode) { + StickyInsetAxisMode.Top -> { + val thresholdY = viewportRect.y + insetPx + maxOf(baseY, thresholdY) + } + + StickyInsetAxisMode.Bottom -> { + val thresholdY = viewportRect.y + viewportRect.height - insetPx - nodeHeight + minOf(baseY, thresholdY) + } + + StickyInsetAxisMode.Inactive -> baseY } - - StickyInsetAxisMode.Bottom -> { - val thresholdY = viewportRect.y + viewportRect.height - insetPx - nodeHeight - minOf(baseY, thresholdY) - } - - StickyInsetAxisMode.Inactive -> baseY - } val finalY = targetY.coerceIn(minY, maxY) return finalY - baseY } @@ -172,25 +172,26 @@ internal object StickyLayoutModel { viewportRect: Rect, containingBlockRect: Rect, insetResolution: StickyHorizontalInsetResolution, - insetPx: Int + insetPx: Int, ): Int { if (!insetResolution.active) return 0 val minX = containingBlockRect.x val maxX = (containingBlockRect.x + containingBlockRect.width - nodeWidth).coerceAtLeast(minX) - val targetX = when (insetResolution.mode) { - StickyHorizontalInsetAxisMode.Left -> { - val thresholdX = viewportRect.x + insetPx - maxOf(baseX, thresholdX) + val targetX = + when (insetResolution.mode) { + StickyHorizontalInsetAxisMode.Left -> { + val thresholdX = viewportRect.x + insetPx + maxOf(baseX, thresholdX) + } + + StickyHorizontalInsetAxisMode.Right -> { + val thresholdX = viewportRect.x + viewportRect.width - insetPx - nodeWidth + minOf(baseX, thresholdX) + } + + StickyHorizontalInsetAxisMode.Inactive -> baseX } - - StickyHorizontalInsetAxisMode.Right -> { - val thresholdX = viewportRect.x + viewportRect.width - insetPx - nodeWidth - minOf(baseX, thresholdX) - } - - StickyHorizontalInsetAxisMode.Inactive -> baseX - } val finalX = targetX.coerceIn(minX, maxX) return finalX - baseX } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/UsedInteractionGeometryResolver.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/UsedInteractionGeometryResolver.kt index d41c4ae..6af99d5 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/UsedInteractionGeometryResolver.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/UsedInteractionGeometryResolver.kt @@ -8,13 +8,13 @@ internal data class UsedInteractionProjection( val worldTransform: AffineTransform2D, val childInputClipRect: Rect?, val canTraverseChildren: Boolean, - val selfContainsPoint: Boolean + val selfContainsPoint: Boolean, ) internal data class UsedInteractionNodeGeometry( val usedBorderRect: Rect, val usedClipRect: Rect?, - val visibleBorderRect: Rect? + val visibleBorderRect: Rect?, ) internal object UsedInteractionGeometryResolver { @@ -23,7 +23,7 @@ internal object UsedInteractionGeometryResolver { mouseX: Int, mouseY: Int, parentTransform: AffineTransform2D, - parentInputClipRect: Rect? + parentInputClipRect: Rect?, ): UsedInteractionProjection? { val worldTransform = parentTransform.times(node.localTransformMatrix()) val inverse = worldTransform.inverseOrNull() ?: return null @@ -42,38 +42,33 @@ internal object UsedInteractionGeometryResolver { worldTransform = worldTransform, childInputClipRect = childInputClipRect, canTraverseChildren = canTraverseChildren, - selfContainsPoint = selfContainsPoint + selfContainsPoint = selfContainsPoint, ) } - fun orderedChildrenForHitTraversal(node: DOMNode): List { - return node.orderedChildrenForHitTestingTraversal() - } + fun orderedChildrenForHitTraversal(node: DOMNode): List = node.orderedChildrenForHitTestingTraversal() fun resolveNodeGeometry(node: DOMNode): UsedInteractionNodeGeometry { val usedClipRect = resolveNodeSelfInputClipRect(node) val usedBorderRect = resolveUsedBorderRect(node) - val visibleBorderRect = when (usedClipRect) { - null -> usedBorderRect - else -> usedBorderRect.intersection(usedClipRect) - } + val visibleBorderRect = + when (usedClipRect) { + null -> usedBorderRect + else -> usedBorderRect.intersection(usedClipRect) + } return UsedInteractionNodeGeometry( usedBorderRect = usedBorderRect, usedClipRect = usedClipRect, - visibleBorderRect = visibleBorderRect + visibleBorderRect = visibleBorderRect, ) } - private fun resolveSelfInputClipRect( - node: DOMNode, - parentInputClipRect: Rect? - ): Rect? { - return if (node.position == PositionMode.Fixed) { + private fun resolveSelfInputClipRect(node: DOMNode, parentInputClipRect: Rect?): Rect? = + if (node.position == PositionMode.Fixed) { node.fixedViewportClipRectForPromotedParticipation() } else { parentInputClipRect } - } private fun resolveNodeSelfInputClipRect(node: DOMNode): Rect? { val chain = ArrayList(8) @@ -109,10 +104,24 @@ internal object UsedInteractionGeometryResolver { val minY = minOf(topLeft.second, topRight.second, bottomLeft.second, bottomRight.second) val maxY = maxOf(topLeft.second, topRight.second, bottomLeft.second, bottomRight.second) - val x = kotlin.math.floor(minX.toDouble()).toInt() - val y = kotlin.math.floor(minY.toDouble()).toInt() - val width = kotlin.math.ceil((maxX - minX).toDouble()).toInt().coerceAtLeast(0) - val height = kotlin.math.ceil((maxY - minY).toDouble()).toInt().coerceAtLeast(0) + val x = + kotlin.math + .floor(minX.toDouble()) + .toInt() + val y = + kotlin.math + .floor(minY.toDouble()) + .toInt() + val width = + kotlin.math + .ceil((maxX - minX).toDouble()) + .toInt() + .coerceAtLeast(0) + val height = + kotlin.math + .ceil((maxY - minY).toDouble()) + .toInt() + .coerceAtLeast(0) return Rect(x, y, width, height) } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/debug/LayoutValidator.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/debug/LayoutValidator.kt index af1b318..4434549 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/debug/LayoutValidator.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/debug/LayoutValidator.kt @@ -16,7 +16,7 @@ data class LayoutViolation( val parentKey: Any?, val message: String, val nodeBounds: Rect, - val parentContentBounds: Rect? + val parentContentBounds: Rect?, ) object LayoutValidator { @@ -29,19 +29,19 @@ object LayoutValidator { out.forEach { violation -> println( "[DSGL-Layout] ${violation.code} key=${violation.nodeKey} parent=${violation.parentKey} " + - "node=${rectLabel(violation.nodeBounds)} parentContent=${rectLabel(violation.parentContentBounds)} :: " + - violation.message + "node=${ + rectLabel( + violation.nodeBounds, + ) + } parentContent=${rectLabel(violation.parentContentBounds)} :: " + + violation.message, ) } } return out } - fun appendDebugCommands( - root: DOMNode, - violations: List, - out: MutableList - ) { + fun appendDebugCommands(root: DOMNode, violations: List, out: MutableList) { if (!LayoutDebug.drawBounds) return val violatingKeys = violations.mapNotNull { it.nodeKey }.toSet() walkDraw(root, violatingKeys, out) @@ -52,7 +52,7 @@ object LayoutValidator { parent: DOMNode?, parentContent: Rect?, ctx: UiMeasureContext, - out: MutableList + out: MutableList, ) { if (node.display == Display.None) return validateRectShape(node = node, parent = parent, out = out) @@ -68,36 +68,46 @@ object LayoutValidator { parent = node, parentContent = content, ctx = ctx, - out = out + out = out, ) } } private fun validateRectShape(node: DOMNode, parent: DOMNode?, out: MutableList) { if (node.bounds.width < 0 || node.bounds.height < 0) { - out += LayoutViolation( - code = "NEGATIVE_SIZE", - nodeKey = node.key, - parentKey = parent?.key, - message = "Negative bounds size is invalid.", - nodeBounds = node.bounds, - parentContentBounds = parent?.let { contentRect(it) } - ) + out += + LayoutViolation( + code = "NEGATIVE_SIZE", + nodeKey = node.key, + parentKey = parent?.key, + message = "Negative bounds size is invalid.", + nodeBounds = node.bounds, + parentContentBounds = parent?.let { contentRect(it) }, + ) } - val right = node.bounds.x.toLong() + node.bounds.width.toLong() - val bottom = node.bounds.y.toLong() + node.bounds.height.toLong() + val right = + node.bounds.x + .toLong() + + node.bounds.width + .toLong() + val bottom = + node.bounds.y + .toLong() + + node.bounds.height + .toLong() if (right !in Int.MIN_VALUE.toLong()..Int.MAX_VALUE.toLong() || bottom !in Int.MIN_VALUE.toLong()..Int.MAX_VALUE.toLong() ) { - out += LayoutViolation( - code = "INT_OVERFLOW", - nodeKey = node.key, - parentKey = parent?.key, - message = "Bounds exceed Int range.", - nodeBounds = node.bounds, - parentContentBounds = parent?.let { contentRect(it) } - ) + out += + LayoutViolation( + code = "INT_OVERFLOW", + nodeKey = node.key, + parentKey = parent?.key, + message = "Bounds exceed Int range.", + nodeBounds = node.bounds, + parentContentBounds = parent?.let { contentRect(it) }, + ) } } @@ -105,18 +115,19 @@ object LayoutValidator { node: DOMNode, parent: DOMNode?, parentContent: Rect, - out: MutableList + out: MutableList, ) { val outer = outerRect(node) if (!containsRect(parentContent, outer)) { - out += LayoutViolation( - code = "CHILD_OUTSIDE_PARENT_CONTENT", - nodeKey = node.key, - parentKey = parent?.key, - message = "Child outer rect escapes parent content box.", - nodeBounds = outer, - parentContentBounds = parentContent - ) + out += + LayoutViolation( + code = "CHILD_OUTSIDE_PARENT_CONTENT", + nodeKey = node.key, + parentKey = parent?.key, + message = "Child outer rect escapes parent content box.", + nodeBounds = outer, + parentContentBounds = parentContent, + ) } } @@ -124,28 +135,30 @@ object LayoutValidator { node: DOMNode, ctx: UiMeasureContext, parent: DOMNode?, - out: MutableList + out: MutableList, ) { when (node) { - is TextNode -> validateLineStack( - text = node.text, - wrap = node.textWrap, - rect = contentRect(node), - ctx = ctx, - node = node, - parent = parent, - out = out - ) + is TextNode -> + validateLineStack( + text = node.text, + wrap = node.textWrap, + rect = contentRect(node), + ctx = ctx, + node = node, + parent = parent, + out = out, + ) - is ButtonNode -> validateLineStack( - text = node.text, - wrap = node.textWrap, - rect = contentRect(node), - ctx = ctx, - node = node, - parent = parent, - out = out - ) + is ButtonNode -> + validateLineStack( + text = node.text, + wrap = node.textWrap, + rect = contentRect(node), + ctx = ctx, + node = node, + parent = parent, + out = out, + ) } } @@ -156,44 +169,49 @@ object LayoutValidator { ctx: UiMeasureContext, node: DOMNode, parent: DOMNode?, - out: MutableList + out: MutableList, ) { val maxWidth = if (wrap == TextWrap.Wrap) rect.width.coerceAtLeast(0) else null val lineHeight = ctx.fontHeight(node.fontId, node.fontSize).coerceAtLeast(1) - val layout = TextLayoutEngine.layout( - text = text, - maxWidth = maxWidth, - wrap = wrap, - fontHeight = lineHeight, - measureText = { value -> ctx.measureText(value, node.fontId, node.fontSize) } - ) + val layout = + TextLayoutEngine.layout( + text = text, + maxWidth = maxWidth, + wrap = wrap, + fontHeight = lineHeight, + measureText = { value -> ctx.measureText(value, node.fontId, node.fontSize) }, + ) var previousBottom = 0 layout.lines.forEachIndexed { index, _ -> val top = index * layout.lineHeight if (top < previousBottom) { - out += LayoutViolation( - code = "TEXT_LINE_COLLISION", - nodeKey = node.key, - parentKey = parent?.key, - message = "Wrapped line boxes overlap.", - nodeBounds = node.bounds, - parentContentBounds = parent?.let { contentRect(it) } - ) + out += + LayoutViolation( + code = "TEXT_LINE_COLLISION", + nodeKey = node.key, + parentKey = parent?.key, + message = "Wrapped line boxes overlap.", + nodeBounds = node.bounds, + parentContentBounds = parent?.let { contentRect(it) }, + ) return } previousBottom = top + layout.lineHeight } if (rect.height < layout.totalHeight) { - out += LayoutViolation( - code = "TEXT_HEIGHT_UNDERSIZED", - nodeKey = node.key, - parentKey = parent?.key, - message = "Measured content height (${rect.height}) is smaller than line stack (${layout.totalHeight}).", - nodeBounds = node.bounds, - parentContentBounds = parent?.let { contentRect(it) } - ) + out += + LayoutViolation( + code = "TEXT_HEIGHT_UNDERSIZED", + nodeKey = node.key, + parentKey = parent?.key, + message = + "Measured content height (${rect.height}) is smaller " + + "than line stack (${layout.totalHeight}).", + nodeBounds = node.bounds, + parentContentBounds = parent?.let { contentRect(it) }, + ) } } @@ -255,4 +273,3 @@ object LayoutValidator { return "(${rect.x},${rect.y},${rect.width},${rect.height})" } } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ButtonNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ButtonNode.kt index 045fc43..88f3196 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ButtonNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ButtonNode.kt @@ -22,7 +22,7 @@ class ButtonNode( var textColor: Int = DsglColors.TEXT, var backgroundColor: Int = DsglColors.BUTTON, padding: Int = 4, - key: Any? = null + key: Any? = null, ) : DOMNode(key) { override val styleType: String = "button" private var onClickHandler: ((MouseClickEvent) -> Unit)? = null @@ -37,13 +37,10 @@ class ButtonNode( } } - internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { - return measureWithConstraint(ctx, availableOuterWidth) - } + internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size = + measureWithConstraint(ctx, availableOuterWidth) - override fun measure(ctx: UiMeasureContext): Size { - return measureWithConstraint(ctx, null) - } + override fun measure(ctx: UiMeasureContext): Size = measureWithConstraint(ctx, null) private fun measureWithConstraint(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { val textMetrics = resolveTextMetrics(ctx) @@ -51,25 +48,27 @@ class ButtonNode( val parsed = parseTextForFormatting(text) val plainText = parsed.plainText val baseFlags = baseTextStyleFlags() - val measuredRanges = MeasuredTextRangeWidthSource( - plainText = plainText, - fontId = fontId, - fontSizePx = textMetrics.fontSizePx, - baseFlags = baseFlags, - spans = parsed.spans, - ctx = ctx - ) + val measuredRanges = + MeasuredTextRangeWidthSource( + plainText = plainText, + fontId = fontId, + fontSizePx = textMetrics.fontSizePx, + baseFlags = baseFlags, + spans = parsed.spans, + ctx = ctx, + ) val contentLimit = resolvedContentLimit(availableOuterWidth) val wrapWidth = if (textWrap == TextWrap.Wrap) contentLimit else null - val layout = TextLayoutEngine.layout( - text = plainText, - maxWidth = wrapWidth, - wrap = textWrap, - fontHeight = lineHeight, - measureText = { value -> ctx.measureText(value, fontId, textMetrics.fontSizePx) }, - measureRange = measuredRanges::measureRange, - measureRangeCacheKey = measuredRanges.cacheKey - ) + val layout = + TextLayoutEngine.layout( + text = plainText, + maxWidth = wrapWidth, + wrap = textWrap, + fontHeight = lineHeight, + measureText = { value -> ctx.measureText(value, fontId, textMetrics.fontSizePx) }, + measureRange = measuredRanges::measureRange, + measureRangeCacheKey = measuredRanges.cacheKey, + ) val naturalContentWidth = width ?: layout.maxLineWidth val contentWidth = contentLimit?.let { minOf(it, naturalContentWidth) } ?: naturalContentWidth val contentHeight = height ?: layout.totalHeight @@ -96,52 +95,56 @@ class ButtonNode( val parsed = parseTextForFormatting(text) val plainText = parsed.plainText val baseFlags = baseTextStyleFlags() - val measuredRanges = MeasuredTextRangeWidthSource( - plainText = plainText, - fontId = fontId, - fontSizePx = textMetrics.fontSizePx, - baseFlags = baseFlags, - spans = parsed.spans, - ctx = ctx - ) + val measuredRanges = + MeasuredTextRangeWidthSource( + plainText = plainText, + fontId = fontId, + fontSizePx = textMetrics.fontSizePx, + baseFlags = baseFlags, + spans = parsed.spans, + ctx = ctx, + ) out.add(RenderCommand.DrawRect(bounds.x, bounds.y, bounds.width, bounds.height, backgroundColor)) addBackgroundImageCommand(out) addBorderCommands(out) val contentWidth = contentWidth() val contentHeight = contentHeight() val wrapWidth = if (textWrap == TextWrap.Wrap) contentWidth else null - val layout = TextLayoutEngine.layout( - text = plainText, - maxWidth = wrapWidth, - wrap = textWrap, - fontHeight = lineHeight, - measureText = { value -> ctx.measureText(value, fontId, textMetrics.fontSizePx) }, - measureRange = measuredRanges::measureRange, - measureRangeCacheKey = measuredRanges.cacheKey - ) + val layout = + TextLayoutEngine.layout( + text = plainText, + maxWidth = wrapWidth, + wrap = textWrap, + fontHeight = lineHeight, + measureText = { value -> ctx.measureText(value, fontId, textMetrics.fontSizePx) }, + measureRange = measuredRanges::measureRange, + measureRangeCacheKey = measuredRanges.cacheKey, + ) val textBlockHeight = layout.totalHeight val blockY = contentY() + (contentHeight - textBlockHeight) / 2 layout.lines.forEachIndexed { index, line -> val lineX = contentX() + (contentWidth - line.width) / 2 val lineY = blockY + index * layout.lineHeight + lineTopLeading - val spans = MinecraftFormattingParser.resolveStyleSpans( - parsed = parsed, - baseColor = textColor, - baseFlags = baseFlags, - rangeStart = line.startIndex, - rangeEnd = line.endIndexExclusive - ).map { span -> - RenderCommand.TextStyleSpan( - start = span.start, - end = span.end, - color = span.color, - bold = span.flags.bold, - italic = span.flags.italic, - underline = span.flags.underline, - strikethrough = span.flags.strikethrough, - obfuscated = span.flags.obfuscated - ) - } + val spans = + MinecraftFormattingParser + .resolveStyleSpans( + parsed = parsed, + baseColor = textColor, + baseFlags = baseFlags, + rangeStart = line.startIndex, + rangeEnd = line.endIndexExclusive, + ).map { span -> + RenderCommand.TextStyleSpan( + start = span.start, + end = span.end, + color = span.color, + bold = span.flags.bold, + italic = span.flags.italic, + underline = span.flags.underline, + strikethrough = span.flags.strikethrough, + obfuscated = span.flags.obfuscated, + ) + } out.add(drawTextCommand(ctx, line.text, lineX, lineY, textColor, spans)) } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/CheckboxGroupNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/CheckboxGroupNode.kt index ee5676c..01be7b4 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/CheckboxGroupNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/CheckboxGroupNode.kt @@ -16,7 +16,7 @@ class CheckboxGroupNode( selected: Set = emptySet(), var minSelected: Int? = null, var maxSelected: Int? = null, - key: Any? = null + key: Any? = null, ) : DOMNode(key) { override val styleType: String = "input" override val focusable: Boolean = true @@ -48,13 +48,10 @@ class CheckboxGroupNode( fun selected(): Set = selectedIds.toSet() - internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { - return measureWithConstraint(ctx, availableOuterWidth) - } + internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size = + measureWithConstraint(ctx, availableOuterWidth) - override fun measure(ctx: UiMeasureContext): Size { - return measureWithConstraint(ctx, null) - } + override fun measure(ctx: UiMeasureContext): Size = measureWithConstraint(ctx, null) private fun measureWithConstraint(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { val fontHeight = resolveFontSize(ctx) @@ -81,7 +78,13 @@ class CheckboxGroupNode( } } - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) val fontHeight = resolveFontSize(ctx) boxSize = maxOf(10, fontHeight - 2) @@ -132,9 +135,11 @@ class CheckboxGroupNode( } } - private fun valueString(): String { - return selectedIds.toList().sorted().joinToString(",") - } + private fun valueString(): String = + selectedIds + .toList() + .sorted() + .joinToString(",") override fun defaultForegroundColor(): Int = textColor diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerInlineNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerInlineNode.kt index 6e8960e..83f7dae 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerInlineNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerInlineNode.kt @@ -15,7 +15,7 @@ class ColorPickerInlineNode( previousValue: RgbaColor? = null, mode: ColorFormatMode = ColorFormatMode.HEX, alphaEnabled: Boolean = true, - key: Any? = null + key: Any? = null, ) : DOMNode(key) { override val styleType: String = "color-picker" override val focusable: Boolean = true @@ -36,16 +36,18 @@ class ColorPickerInlineNode( private var uncontrolledValue: RgbaColor = value ?: defaultValue private var uncontrolledPrevious: RgbaColor = previousValue ?: uncontrolledValue private val recentHistory: ColorRecentHistory = ColorRecentHistory(64) - private val controller: ColorPickerController = ColorPickerController( - initial = ColorPickerState( - color = effectiveColor(), - previous = effectivePreviousColor(), - mode = mode, - alphaEnabled = alphaEnabled, - closeOnSelect = closeOnSelect - ), - recentHistory = recentHistory - ) + private val controller: ColorPickerController = + ColorPickerController( + initial = + ColorPickerState( + color = effectiveColor(), + previous = effectivePreviousColor(), + mode = mode, + alphaEnabled = alphaEnabled, + closeOnSelect = closeOnSelect, + ), + recentHistory = recentHistory, + ) private var layout: ColorPickerLayout? = null private var dragCaptured: Boolean = false private var syncedColorArgb: Int = Int.MIN_VALUE @@ -120,21 +122,16 @@ class ColorPickerInlineNode( } } - override fun shouldCapturePointerDrag(mouseX: Int, mouseY: Int): Boolean { - return dragCaptured || containsGlobalPoint(mouseX, mouseY) - } + override fun shouldCapturePointerDrag(mouseX: Int, mouseY: Int): Boolean = + dragCaptured || containsGlobalPoint(mouseX, mouseY) fun wantsGlobalPointerInput(): Boolean = controller.isEyedropperActive() - fun appendEyedropperOverlayCommands( - viewportWidth: Int, - viewportHeight: Int, - out: MutableList - ) { + fun appendEyedropperOverlayCommands(viewportWidth: Int, viewportHeight: Int, out: MutableList) { controller.appendEyedropperOverlay( viewportWidth = viewportWidth.coerceAtLeast(1), viewportHeight = viewportHeight.coerceAtLeast(1), - out = out + out = out, ) } @@ -142,13 +139,10 @@ class ColorPickerInlineNode( controller.sampleEyedropperAtHover() } - internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { - return measureWithConstraint(availableOuterWidth) - } + internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size = + measureWithConstraint(availableOuterWidth) - override fun measure(ctx: UiMeasureContext): Size { - return measureWithConstraint(null) - } + override fun measure(ctx: UiMeasureContext): Size = measureWithConstraint(null) private fun measureWithConstraint(availableOuterWidth: Int?): Size { val minWidth = controller.style().minWidth @@ -171,7 +165,13 @@ class ColorPickerInlineNode( } } - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) syncControllerStateIfNeeded() refreshLayout() @@ -234,8 +234,8 @@ class ColorPickerInlineNode( previous = previous, mode = mode, alphaEnabled = alphaEnabled, - closeOnSelect = closeOnSelect - ) + closeOnSelect = closeOnSelect, + ), ) bindController() } @@ -268,31 +268,30 @@ class ColorPickerInlineNode( } } - private fun effectiveColor(): RgbaColor { - return if (controlled) { + private fun effectiveColor(): RgbaColor = + if (controlled) { (controlledValue ?: defaultValue).normalized() } else { uncontrolledValue.normalized() } - } - private fun effectivePreviousColor(): RgbaColor { - return if (controlled) { + private fun effectivePreviousColor(): RgbaColor = + if (controlled) { (previousValue ?: controlledValue ?: defaultValue).normalized() } else { (previousValue ?: uncontrolledPrevious).normalized() } - } private fun refreshLayout() { if (bounds.width <= 0 || bounds.height <= 0) return - layout = controller.buildLayout( - Rect( - contentX(), - contentY(), - contentWidth().coerceAtLeast(1), - contentHeight().coerceAtLeast(1) + layout = + controller.buildLayout( + Rect( + contentX(), + contentY(), + contentWidth().coerceAtLeast(1), + contentHeight().coerceAtLeast(1), + ), ) - ) } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerPopupPaneNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerPopupPaneNode.kt index b5413d6..48d396a 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerPopupPaneNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerPopupPaneNode.kt @@ -16,7 +16,7 @@ class ColorPickerPopupPaneNode( previousValue: RgbaColor? = null, mode: ColorFormatMode = ColorFormatMode.HEX, alphaEnabled: Boolean = true, - key: Any? = null + key: Any? = null, ) : DOMNode(key) { override val styleType: String = "color-picker-popup" override val focusable: Boolean = true @@ -50,7 +50,13 @@ class ColorPickerPopupPaneNode( this@ColorPickerPopupPaneNode.addEventListener(Events.MOUSEDOWN) { event: MouseDownEvent -> if (this@ColorPickerPopupPaneNode.styleDisabled) return@addEventListener if (event.mouseButton != MouseButton.LEFT) return@addEventListener - if (!this@ColorPickerPopupPaneNode.containsGlobalPoint(event.mouseX, event.mouseY)) return@addEventListener + if (!this@ColorPickerPopupPaneNode.containsGlobalPoint( + event.mouseX, + event.mouseY, + ) + ) { + return@addEventListener + } FocusManager.requestFocus(this@ColorPickerPopupPaneNode) if (ColorPickerRuntime.host.isOpenFor(ownerToken)) { ColorPickerRuntime.host.close(ownerToken) @@ -62,13 +68,10 @@ class ColorPickerPopupPaneNode( } } - internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { - return measureWithConstraint(ctx, availableOuterWidth) - } + internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size = + measureWithConstraint(ctx, availableOuterWidth) - override fun measure(ctx: UiMeasureContext): Size { - return measureWithConstraint(ctx, null) - } + override fun measure(ctx: UiMeasureContext): Size = measureWithConstraint(ctx, null) private fun measureWithConstraint(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { val label = ColorTextCodec.format(effectiveColor(), mode, alphaEnabled) @@ -99,35 +102,46 @@ class ColorPickerPopupPaneNode( addBorderCommands(out) val swatchSize = (rect.height - 8).coerceAtLeast(10) val swatchRect = Rect(rect.x + 4, rect.y + ((rect.height - swatchSize) / 2), swatchSize, swatchSize) - out += RenderCommand.DrawRect(swatchRect.x, swatchRect.y, swatchRect.width, swatchRect.height, effectiveColor().toArgbInt()) + out += + RenderCommand.DrawRect( + swatchRect.x, + swatchRect.y, + swatchRect.width, + swatchRect.height, + effectiveColor().toArgbInt(), + ) out += RenderCommand.DrawRect(swatchRect.x, swatchRect.y, swatchRect.width, 1, borderColor) - out += RenderCommand.DrawRect(swatchRect.x, swatchRect.y + swatchRect.height - 1, swatchRect.width, 1, borderColor) + out += + RenderCommand.DrawRect(swatchRect.x, swatchRect.y + swatchRect.height - 1, swatchRect.width, 1, borderColor) out += RenderCommand.DrawRect(swatchRect.x, swatchRect.y, 1, swatchRect.height, borderColor) - out += RenderCommand.DrawRect(swatchRect.x + swatchRect.width - 1, swatchRect.y, 1, swatchRect.height, borderColor) - out += drawTextCommand( - ctx, - text = ColorTextCodec.format(effectiveColor(), mode, alphaEnabled), - x = swatchRect.x + swatchRect.width + 6, - y = rect.y + 3, - color = textColor - ) - out += if (ColorPickerRuntime.host.isOpenFor(ownerToken)) { + out += + RenderCommand.DrawRect(swatchRect.x + swatchRect.width - 1, swatchRect.y, 1, swatchRect.height, borderColor) + out += drawTextCommand( ctx, - text = "^", - x = rect.x + rect.width - 14, + text = ColorTextCodec.format(effectiveColor(), mode, alphaEnabled), + x = swatchRect.x + swatchRect.width + 6, y = rect.y + 3, - color = textColor + color = textColor, ) - } else { - drawTextCommand( - ctx, - text = "v", - x = rect.x + rect.width - 14, - y = rect.y + 3, - color = textColor - ) - } + out += + if (ColorPickerRuntime.host.isOpenFor(ownerToken)) { + drawTextCommand( + ctx, + text = "^", + x = rect.x + rect.width - 14, + y = rect.y + 3, + color = textColor, + ) + } else { + drawTextCommand( + ctx, + text = "v", + x = rect.x + rect.width - 14, + y = rect.y + 3, + color = textColor, + ) + } } internal fun syncFrom(template: ColorPickerPopupPaneNode) { @@ -179,18 +193,19 @@ class ColorPickerPopupPaneNode( setOpenState(true) } - private fun openRequest(): ColorPickerPopupRequest { - return ColorPickerPopupRequest( + private fun openRequest(): ColorPickerPopupRequest = + ColorPickerPopupRequest( owner = ownerToken, anchorRect = bounds, title = popupTitle, - state = ColorPickerState( - color = effectiveColor(), - previous = effectivePreviousColor(), - mode = mode, - alphaEnabled = alphaEnabled, - closeOnSelect = closeOnSelect - ), + state = + ColorPickerState( + color = effectiveColor(), + previous = effectivePreviousColor(), + mode = mode, + alphaEnabled = alphaEnabled, + closeOnSelect = closeOnSelect, + ), width = popupWidth, draggable = popupDraggable, closeOnOutsideClick = popupCloseOnOutsideClick, @@ -218,23 +233,20 @@ class ColorPickerPopupPaneNode( }, onClose = { setOpenState(false) - } + }, ) - } - private fun effectiveColor(): RgbaColor { - return if (controlled) { + private fun effectiveColor(): RgbaColor = + if (controlled) { (controlledValue ?: defaultValue).normalized() } else { uncontrolledValue.normalized() } - } - private fun effectivePreviousColor(): RgbaColor { - return if (controlled) { + private fun effectivePreviousColor(): RgbaColor = + if (controlled) { (previousValue ?: controlledValue ?: defaultValue).normalized() } else { (previousValue ?: uncontrolledPrevious).normalized() } - } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ContainerNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ContainerNode.kt index 9b822f9..eb5ff96 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ContainerNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ContainerNode.kt @@ -18,7 +18,7 @@ class ContainerNode( gap: Int = 0, var backgroundColor: Int? = null, var stackLayout: Boolean = false, - key: Any? = null + key: Any? = null, ) : DOMNode(key) { override val styleType: String = "div" @@ -27,16 +27,15 @@ class ContainerNode( this.gap = gap.coerceAtLeast(0) } - override fun measure(ctx: UiMeasureContext): Size { - return measureWithConstraint(ctx, null) - } + override fun measure(ctx: UiMeasureContext): Size = measureWithConstraint(ctx, null) override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { - val constrainedContentWidth = if (availableOuterWidth != null) { - constrainContentWidthForOuterLimit(availableOuterWidth, this@ContainerNode) - } else { - null - } + val constrainedContentWidth = + if (availableOuterWidth != null) { + constrainContentWidthForOuterLimit(availableOuterWidth, this@ContainerNode) + } else { + null + } return measureWithConstraint(ctx, constrainedContentWidth) } @@ -47,38 +46,47 @@ class ContainerNode( val visibleChildren = visibleChildren() val inFlowChildren = inFlowChildren(visibleChildren) val resolvedWrapWidth = resolvedContentLimit(constrainedContentWidth) - val boundedExplicitWidth = width?.let { explicit -> - resolvedWrapWidth?.let { minOf(explicit, it) } ?: explicit - } + val boundedExplicitWidth = + width?.let { explicit -> + resolvedWrapWidth?.let { minOf(explicit, it) } ?: explicit + } if (inFlowChildren.isEmpty()) { val contentWidth = boundedExplicitWidth ?: 0 val contentHeight = height ?: 0 return Size( contentWidth + padding.horizontal + border.horizontal, - contentHeight + padding.vertical + border.vertical + contentHeight + padding.vertical + border.vertical, ) } - val contentSize = when { - stackLayout -> measureStack(ctx, inFlowChildren) - display == Display.Flex -> measureFlex(ctx, inFlowChildren, resolvedWrapWidth) - display == Display.Grid -> measureGrid(ctx, inFlowChildren, resolvedWrapWidth) - display == Display.Inline -> measureInline(ctx, inFlowChildren, resolvedWrapWidth) - else -> measureBlock(ctx, inFlowChildren, resolvedWrapWidth) - } - val contentWidth = when { - boundedExplicitWidth != null -> boundedExplicitWidth - resolvedWrapWidth != null -> minOf(contentSize.width, resolvedWrapWidth) - else -> contentSize.width - } + val contentSize = + when { + stackLayout -> measureStack(ctx, inFlowChildren) + display == Display.Flex -> measureFlex(ctx, inFlowChildren, resolvedWrapWidth) + display == Display.Grid -> measureGrid(ctx, inFlowChildren, resolvedWrapWidth) + display == Display.Inline -> measureInline(ctx, inFlowChildren, resolvedWrapWidth) + else -> measureBlock(ctx, inFlowChildren, resolvedWrapWidth) + } + val contentWidth = + when { + boundedExplicitWidth != null -> boundedExplicitWidth + resolvedWrapWidth != null -> minOf(contentSize.width, resolvedWrapWidth) + else -> contentSize.width + } val contentHeight = height ?: contentSize.height return Size( contentWidth + padding.horizontal + border.horizontal, - contentHeight + padding.vertical + border.vertical + contentHeight + padding.vertical + border.vertical, ) } - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { if (display == Display.None) { bounds = Rect(x, y, 0, 0) resetContentLayoutScroll() @@ -121,17 +129,17 @@ class ContainerNode( backgroundColor = value } - private fun visibleChildren(): List { - return children.filter { it.display != Display.None } - } + private fun visibleChildren(): List = children.filter { it.display != Display.None } - private fun inFlowChildren(children: List): List { - return children.filter { !it.isRemovedFromNormalFlowForPositioning() } - } + private fun inFlowChildren(children: List): List = + children.filter { + !it.isRemovedFromNormalFlowForPositioning() + } - private fun outOfFlowChildren(children: List): List { - return children.filter { it.isRemovedFromNormalFlowForPositioning() } - } + private fun outOfFlowChildren(children: List): List = + children.filter { + it.isRemovedFromNormalFlowForPositioning() + } private fun renderOutOfFlowChildren(ctx: UiMeasureContext, children: List) { val cx = childContentOriginX() @@ -139,12 +147,13 @@ class ContainerNode( val cw = viewportContentWidth() val ch = viewportContentHeight() children.forEach { child -> - val measured = measureChildForLayout( - ctx = ctx, - child = child, - availableOuterWidth = cw, - availableOuterHeight = ch - ) + val measured = + measureChildForLayout( + ctx = ctx, + child = child, + availableOuterWidth = cw, + availableOuterHeight = ch, + ) val childX = cx + child.margin.left val childY = cy + child.margin.top renderContainedChild( @@ -157,7 +166,7 @@ class ContainerNode( desiredX = childX, desiredY = childY, desiredWidth = measured.width, - desiredHeight = measured.height + desiredHeight = measured.height, ) } } @@ -166,12 +175,13 @@ class ContainerNode( var maxWidth = 0 var maxHeight = 0 children.forEach { child -> - val size = measureChildForLayout( - ctx = ctx, - child = child, - availableOuterWidth = null, - availableOuterHeight = null - ) + val size = + measureChildForLayout( + ctx = ctx, + child = child, + availableOuterWidth = null, + availableOuterHeight = null, + ) maxWidth = maxOf(maxWidth, size.width + child.margin.horizontal) maxHeight = maxOf(maxHeight, size.height + child.margin.vertical) } @@ -184,12 +194,13 @@ class ContainerNode( val cw = viewportContentWidth() val ch = viewportContentHeight() children.forEach { child -> - val size = measureChildForLayout( - ctx = ctx, - child = child, - availableOuterWidth = cw, - availableOuterHeight = ch - ) + val size = + measureChildForLayout( + ctx = ctx, + child = child, + availableOuterWidth = cw, + availableOuterHeight = ch, + ) val childWidth = size.width val childHeight = size.height val childX = alignedChildX(child, cx, cw, childWidth) @@ -219,11 +230,12 @@ class ContainerNode( } children.forEach { child -> - val measured = measureChildForLayout( - ctx = ctx, - child = child, - availableOuterWidth = wrapWidth - ) + val measured = + measureChildForLayout( + ctx = ctx, + child = child, + availableOuterWidth = wrapWidth, + ) val outerWidth = measured.width + child.margin.horizontal val outerHeight = measured.height + child.margin.vertical if (isInlineFormattingChild(child)) { @@ -240,12 +252,14 @@ class ContainerNode( } else { flushInlineLine() if (hasRows) totalHeight += gap - val blockWidth = if (wrapWidth != null && child.width == null) { - val stretchedOuterWidth = (wrapWidth - child.margin.horizontal).coerceAtLeast(0) - child.clampMeasuredOuterSize(Size(stretchedOuterWidth, measured.height)).width + child.margin.horizontal - } else { - outerWidth - } + val blockWidth = + if (wrapWidth != null && child.width == null) { + val stretchedOuterWidth = (wrapWidth - child.margin.horizontal).coerceAtLeast(0) + child.clampMeasuredOuterSize(Size(stretchedOuterWidth, measured.height)).width + + child.margin.horizontal + } else { + outerWidth + } maxWidth = maxOf(maxWidth, blockWidth) totalHeight += outerHeight hasRows = true @@ -262,7 +276,7 @@ class ContainerNode( val width: Int, val height: Int, val relX: Int, - val outerHeight: Int + val outerHeight: Int, ) private fun renderBlock(ctx: UiMeasureContext, children: List) { @@ -295,12 +309,13 @@ class ContainerNode( } children.forEach { child -> - val measured = measureChildForLayout( - ctx = ctx, - child = child, - availableOuterWidth = cw, - availableOuterHeight = ch - ) + val measured = + measureChildForLayout( + ctx = ctx, + child = child, + availableOuterWidth = cw, + availableOuterHeight = ch, + ) if (isInlineFormattingChild(child)) { val outerWidth = measured.width + child.margin.horizontal val outerHeight = measured.height + child.margin.vertical @@ -316,20 +331,21 @@ class ContainerNode( width = measured.width, height = measured.height, relX = relX, - outerHeight = outerHeight - ) + outerHeight = outerHeight, + ), ) lineWidth = relX + outerWidth lineHeight = maxOf(lineHeight, outerHeight, inlineLineBoxHeight) } else { flushInlineLine() if (hasRows) cursorY += gap - val widthToRender = if (child.width == null) { - val stretchedOuterWidth = (cw - child.margin.horizontal).coerceAtLeast(0) - child.clampMeasuredOuterSize(Size(stretchedOuterWidth, measured.height)).width - } else { - measured.width - } + val widthToRender = + if (child.width == null) { + val stretchedOuterWidth = (cw - child.margin.horizontal).coerceAtLeast(0) + child.clampMeasuredOuterSize(Size(stretchedOuterWidth, measured.height)).width + } else { + measured.width + } val heightToRender = measured.height val childX = alignedChildX(child, cx, cw, widthToRender) val childY = cursorY + child.margin.top @@ -360,11 +376,12 @@ class ContainerNode( } children.forEach { child -> - val measured = measureChildForLayout( - ctx = ctx, - child = child, - availableOuterWidth = wrapWidth - ) + val measured = + measureChildForLayout( + ctx = ctx, + child = child, + availableOuterWidth = wrapWidth, + ) val outerWidth = measured.width + child.margin.horizontal val outerHeight = measured.height + child.margin.vertical val spacing = if (lineHasItems) gap else 0 @@ -379,11 +396,12 @@ class ContainerNode( } flushLine() - val finalWidth = if (wrapWidth != null) { - minOf(maxLineWidth, wrapWidth.coerceAtLeast(0)) - } else { - maxLineWidth - } + val finalWidth = + if (wrapWidth != null) { + minOf(maxLineWidth, wrapWidth.coerceAtLeast(0)) + } else { + maxLineWidth + } return Size(finalWidth.coerceAtLeast(0), totalHeight.coerceAtLeast(0)) } @@ -399,12 +417,13 @@ class ContainerNode( val inlineLineBoxHeight = resolveEffectiveLineHeight(ctx) children.forEach { child -> - val measured = measureChildForLayout( - ctx = ctx, - child = child, - availableOuterWidth = cw, - availableOuterHeight = ch - ) + val measured = + measureChildForLayout( + ctx = ctx, + child = child, + availableOuterWidth = cw, + availableOuterHeight = ch, + ) val outerWidth = measured.width + child.margin.horizontal val outerHeight = measured.height + child.margin.vertical if (lineHasItems && cursorX + gap + outerWidth > cx + cw) { @@ -423,9 +442,7 @@ class ContainerNode( } } - private fun isInlineFormattingChild(child: DOMNode): Boolean { - return child.display == Display.Inline || child is TextNode - } + private fun isInlineFormattingChild(child: DOMNode): Boolean = child.display == Display.Inline || child is TextNode private data class FlexItem( val child: DOMNode, @@ -437,7 +454,7 @@ class ContainerNode( val crossMarginEnd: Int, val explicitMain: Int?, val explicitCross: Int?, - val resolvedFlexBasis: Int? + val resolvedFlexBasis: Int?, ) private fun measureFlex(ctx: UiMeasureContext, children: List, wrapWidth: Int?): Size { @@ -472,31 +489,34 @@ class ContainerNode( if (children.isEmpty()) return - val items = children.map { child -> - val measured = measureChildForLayout( - ctx = ctx, - child = child, - availableOuterWidth = availableOuterWidth, - availableOuterHeight = availableOuterHeight - ) - FlexItem( - child = child, - measuredMain = if (isRow) measured.width else measured.height, - measuredCross = if (isRow) measured.height else measured.width, - mainMarginStart = if (isRow) child.margin.left else child.margin.top, - mainMarginEnd = if (isRow) child.margin.right else child.margin.bottom, - crossMarginStart = if (isRow) child.margin.top else child.margin.left, - crossMarginEnd = if (isRow) child.margin.bottom else child.margin.right, - explicitMain = if (isRow) child.width else child.height, - explicitCross = if (isRow) child.height else child.width, - resolvedFlexBasis = child.resolveFlexBasisForAxis( - ctx = ctx, - parentContentWidth = availableOuterWidth, - parentContentHeight = availableOuterHeight, - axis = flexDirection - ), - ) - } + val items = + children.map { child -> + val measured = + measureChildForLayout( + ctx = ctx, + child = child, + availableOuterWidth = availableOuterWidth, + availableOuterHeight = availableOuterHeight, + ) + FlexItem( + child = child, + measuredMain = if (isRow) measured.width else measured.height, + measuredCross = if (isRow) measured.height else measured.width, + mainMarginStart = if (isRow) child.margin.left else child.margin.top, + mainMarginEnd = if (isRow) child.margin.right else child.margin.bottom, + crossMarginStart = if (isRow) child.margin.top else child.margin.left, + crossMarginEnd = if (isRow) child.margin.bottom else child.margin.right, + explicitMain = if (isRow) child.width else child.height, + explicitCross = if (isRow) child.height else child.width, + resolvedFlexBasis = + child.resolveFlexBasisForAxis( + ctx = ctx, + parentContentWidth = availableOuterWidth, + parentContentHeight = availableOuterHeight, + axis = flexDirection, + ), + ) + } val baseMain = DoubleArray(items.size) var totalOuterBaseMain = 0.0 @@ -506,8 +526,15 @@ class ContainerNode( val base = (item.explicitMain ?: item.resolvedFlexBasis ?: item.measuredMain).coerceAtLeast(0) baseMain[index] = base.toDouble() totalOuterBaseMain += base + item.mainMarginStart + item.mainMarginEnd - totalGrow += item.child.flexGrow.coerceAtLeast(0f).toDouble() - totalShrinkWeight += (item.child.flexShrink.coerceAtLeast(0f) * base).toDouble() + totalGrow += + item.child.flexGrow + .coerceAtLeast(0f) + .toDouble() + totalShrinkWeight += + ( + item.child.flexShrink + .coerceAtLeast(0f) * base + ).toDouble() } val gapTotal = gap * (items.size - 1).coerceAtLeast(0) val freeSpace = availableMain - totalOuterBaseMain - gapTotal @@ -517,22 +544,30 @@ class ContainerNode( val finalMain = DoubleArray(items.size) { baseMain[it] } if (freeSpace > 0.0 && totalGrow > 0.0) { items.forEachIndexed { index, item -> - val grow = item.child.flexGrow.coerceAtLeast(0f).toDouble() + val grow = + item.child.flexGrow + .coerceAtLeast(0f) + .toDouble() finalMain[index] += freeSpace * (grow / totalGrow) } } else if (allowMainAxisShrink && freeSpace < 0.0 && totalShrinkWeight > 0.0) { items.forEachIndexed { index, item -> - val weight = (item.child.flexShrink.coerceAtLeast(0f) * baseMain[index].toFloat()).toDouble() + val weight = + ( + item.child.flexShrink + .coerceAtLeast(0f) * baseMain[index].toFloat() + ).toDouble() finalMain[index] += freeSpace * (weight / totalShrinkWeight) if (finalMain[index] < 0.0) finalMain[index] = 0.0 } } - val usedMain = finalMain.indices.sumOf { index -> - finalMain[index] + + val usedMain = + finalMain.indices.sumOf { index -> + finalMain[index] + items[index].mainMarginStart + items[index].mainMarginEnd - } + gapTotal + } + gapTotal val extra = (availableMain - usedMain).coerceAtLeast(0.0) val (mainStartOffset, spacing) = justifyOffsets(justifyContent, items.size, extra, gap.toDouble()) @@ -542,40 +577,49 @@ class ContainerNode( cursorMain += item.mainMarginStart val mainSize = finalMain[index].roundToInt().coerceAtLeast(0) val crossAvailable = (availableCross - item.crossMarginStart - item.crossMarginEnd).coerceAtLeast(0) - val crossSize = when { - item.explicitCross != null -> item.explicitCross - alignItems == AlignItems.Stretch -> crossAvailable - else -> item.measuredCross.coerceAtMost(crossAvailable) - }.coerceAtLeast(0) + val crossSize = + when { + item.explicitCross != null -> item.explicitCross + alignItems == AlignItems.Stretch -> crossAvailable + else -> item.measuredCross.coerceAtMost(crossAvailable) + }.coerceAtLeast(0) val candidateWidth = if (isRow) mainSize else crossSize val candidateHeight = if (isRow) crossSize else mainSize - val resolvedSize = item.child.clampMeasuredOuterSize( - Size( - width = candidateWidth, - height = candidateHeight + val resolvedSize = + item.child.clampMeasuredOuterSize( + Size( + width = candidateWidth, + height = candidateHeight, + ), ) - ) val childWidth = resolvedSize.width val childHeight = resolvedSize.height val resolvedCross = if (isRow) childHeight else childWidth val resolvedMain = if (isRow) childWidth else childHeight - val crossRoom = (availableCross - resolvedCross - item.crossMarginStart - item.crossMarginEnd).coerceAtLeast(0) - val crossOffset = when (alignItems) { - AlignItems.Start, AlignItems.Stretch -> 0 - AlignItems.Center -> crossRoom / 2 - AlignItems.End -> crossRoom - } + val crossRoom = + (availableCross - resolvedCross - item.crossMarginStart - item.crossMarginEnd) + .coerceAtLeast( + 0, + ) + val crossOffset = + when (alignItems) { + AlignItems.Start, AlignItems.Stretch -> 0 + AlignItems.Center -> crossRoom / 2 + AlignItems.End -> crossRoom + } - val childX = if (isRow) { - cx + cursorMain.roundToInt() - } else { - cx + item.crossMarginStart + crossOffset - } - val childY = if (isRow) { - cy + item.crossMarginStart + crossOffset - } else { - cy + cursorMain.roundToInt() - } + val childX = + if (isRow) { + cx + cursorMain.roundToInt() + } else { + cx + item.crossMarginStart + crossOffset + } + val childY = + if (isRow) { + cy + item.crossMarginStart + crossOffset + } else { + cy + cursorMain.roundToInt() + } renderContainedChild( ctx = ctx, child = item.child, @@ -586,7 +630,7 @@ class ContainerNode( desiredX = childX, desiredY = childY, desiredWidth = childWidth, - desiredHeight = childHeight + desiredHeight = childHeight, ) cursorMain += resolvedMain + item.mainMarginEnd @@ -597,7 +641,7 @@ class ContainerNode( justify: JustifyContent, count: Int, extra: Double, - baseGap: Double + baseGap: Double, ): Pair { if (count <= 0) return 0.0 to baseGap return when (justify) { @@ -626,7 +670,7 @@ class ContainerNode( val row: Int, val column: Int, val rowSpan: Int, - val columnSpan: Int + val columnSpan: Int, ) private fun measureGrid(ctx: UiMeasureContext, children: List, fixedWidth: Int?): Size { @@ -635,16 +679,18 @@ class ContainerNode( val placements = computeGridPlacements(children, columns) val rowCount = resolveGridRowCount(placements) - val colWidth = when { - fixedWidth != null -> ((fixedWidth - gap * (columns - 1)).coerceAtLeast(0)) / columns - else -> placements.maxOfOrNull { placement -> - val child = placement.child - val outerWidth = measureChildForLayout(ctx, child, null).width + child.margin.horizontal - val totalGapWithinSpan = gap * (placement.columnSpan - 1).coerceAtLeast(0) - ((outerWidth - totalGapWithinSpan).coerceAtLeast(0) + placement.columnSpan - 1) / - placement.columnSpan.coerceAtLeast(1) - } ?: 0 - } + val colWidth = + when { + fixedWidth != null -> ((fixedWidth - gap * (columns - 1)).coerceAtLeast(0)) / columns + else -> + placements.maxOfOrNull { placement -> + val child = placement.child + val outerWidth = measureChildForLayout(ctx, child, null).width + child.margin.horizontal + val totalGapWithinSpan = gap * (placement.columnSpan - 1).coerceAtLeast(0) + ((outerWidth - totalGapWithinSpan).coerceAtLeast(0) + placement.columnSpan - 1) / + placement.columnSpan.coerceAtLeast(1) + } ?: 0 + } val rowHeights = computeGridRowHeights(ctx, placements, rowCount, colWidth) val measuredWidth = fixedWidth ?: (columns * colWidth + gap * (columns - 1)) val measuredHeight = rowHeights.sum() + gap * (rowHeights.size - 1).coerceAtLeast(0) @@ -678,44 +724,50 @@ class ContainerNode( val availableCellWidth = (cellWidth - child.margin.horizontal).coerceAtLeast(0) val availableCellHeight = (cellHeight - child.margin.vertical).coerceAtLeast(0) - val measured = measureChildForLayout( - ctx = ctx, - child = child, - availableOuterWidth = cellWidth.coerceAtLeast(0), - availableOuterHeight = cellHeight.coerceAtLeast(0) - ) + val measured = + measureChildForLayout( + ctx = ctx, + child = child, + availableOuterWidth = cellWidth.coerceAtLeast(0), + availableOuterHeight = cellHeight.coerceAtLeast(0), + ) - val requestedChildWidth = when { - child.width != null -> child.width!!.coerceAtMost(availableCellWidth) - justifyItems == JustifyItems.Stretch -> availableCellWidth - else -> measured.width.coerceAtMost(availableCellWidth) - }.coerceAtLeast(0) - val requestedChildHeight = when { - child.height != null -> child.height!!.coerceAtMost(availableCellHeight) - alignItems == AlignItems.Stretch -> availableCellHeight - else -> measured.height.coerceAtMost(availableCellHeight) - }.coerceAtLeast(0) - val resolvedSize = child.clampMeasuredOuterSize( - Size( - width = requestedChildWidth, - height = requestedChildHeight + val requestedChildWidth = + when { + child.width != null -> child.width!!.coerceAtMost(availableCellWidth) + justifyItems == JustifyItems.Stretch -> availableCellWidth + else -> measured.width.coerceAtMost(availableCellWidth) + }.coerceAtLeast(0) + val requestedChildHeight = + when { + child.height != null -> child.height!!.coerceAtMost(availableCellHeight) + alignItems == AlignItems.Stretch -> availableCellHeight + else -> measured.height.coerceAtMost(availableCellHeight) + }.coerceAtLeast(0) + val resolvedSize = + child.clampMeasuredOuterSize( + Size( + width = requestedChildWidth, + height = requestedChildHeight, + ), ) - ) val childWidth = resolvedSize.width val childHeight = resolvedSize.height val xSpace = (availableCellWidth - childWidth).coerceAtLeast(0) val ySpace = (availableCellHeight - childHeight).coerceAtLeast(0) - val xOffset = when (justifyItems) { - JustifyItems.Start, JustifyItems.Stretch -> 0 - JustifyItems.Center -> xSpace / 2 - JustifyItems.End -> xSpace - } - val yOffset = when (alignItems) { - AlignItems.Start, AlignItems.Stretch -> 0 - AlignItems.Center -> ySpace / 2 - AlignItems.End -> ySpace - } + val xOffset = + when (justifyItems) { + JustifyItems.Start, JustifyItems.Stretch -> 0 + JustifyItems.Center -> xSpace / 2 + JustifyItems.End -> xSpace + } + val yOffset = + when (alignItems) { + AlignItems.Start, AlignItems.Stretch -> 0 + AlignItems.Center -> ySpace / 2 + AlignItems.End -> ySpace + } val childX = cellX + child.margin.left + xOffset val childY = cellY + child.margin.top + yOffset @@ -729,7 +781,7 @@ class ContainerNode( childX, childY, childWidth, - childHeight + childHeight, ) } } @@ -741,7 +793,10 @@ class ContainerNode( var searchRows = (gridRows ?: 1).coerceAtLeast(1) children.forEach { child -> - val colSpan = child.gridColumnSpan.coerceAtLeast(1).coerceAtMost(columns) + val colSpan = + child.gridColumnSpan + .coerceAtLeast(1) + .coerceAtMost(columns) val rowSpan = child.gridRowSpan.coerceAtLeast(1) var placed: GridPlacement? = null var safety = 0 @@ -750,13 +805,14 @@ class ContainerNode( for ((row, col) in candidates) { if (canPlaceGridCell(row, col, rowSpan, colSpan, columns, occupied)) { markGridCell(row, col, rowSpan, colSpan, occupied) - placed = GridPlacement( - child = child, - row = row, - column = col, - rowSpan = rowSpan, - columnSpan = colSpan - ) + placed = + GridPlacement( + child = child, + row = row, + column = col, + rowSpan = rowSpan, + columnSpan = colSpan, + ) break } } @@ -771,14 +827,14 @@ class ContainerNode( row = placements.size / columns, column = placements.size % columns, rowSpan = rowSpan, - columnSpan = colSpan + columnSpan = colSpan, ) } return placements } - private fun candidateGridCells(columns: Int, rows: Int): Sequence> { - return sequence { + private fun candidateGridCells(columns: Int, rows: Int): Sequence> = + sequence { val maxRows = rows.coerceAtLeast(1) if (gridAutoFlow == GridAutoFlow.Column) { for (col in 0 until columns) { @@ -794,7 +850,6 @@ class ContainerNode( } } } - } private fun canPlaceGridCell( row: Int, @@ -802,7 +857,7 @@ class ContainerNode( rowSpan: Int, colSpan: Int, columns: Int, - occupied: Set + occupied: Set, ): Boolean { if (column + colSpan > columns) return false for (r in row until row + rowSpan) { @@ -818,7 +873,7 @@ class ContainerNode( column: Int, rowSpan: Int, colSpan: Int, - occupied: MutableSet + occupied: MutableSet, ) { for (r in row until row + rowSpan) { for (c in column until column + colSpan) { @@ -827,9 +882,8 @@ class ContainerNode( } } - private fun encodeGridCell(row: Int, column: Int): Long { - return (row.toLong() shl 32) or (column.toLong() and 0xFFFF_FFFFL) - } + private fun encodeGridCell(row: Int, column: Int): Long = + (row.toLong() shl 32) or (column.toLong() and 0xFFFF_FFFFL) private fun resolveGridRowCount(placements: List): Int { val placedRows = placements.maxOfOrNull { it.row + it.rowSpan } ?: 0 @@ -840,7 +894,7 @@ class ContainerNode( ctx: UiMeasureContext, placements: List, rowCount: Int, - colWidth: Int + colWidth: Int, ): IntArray { val rowHeights = IntArray(rowCount) placements.forEach { placement -> @@ -869,13 +923,13 @@ class ContainerNode( ctx: UiMeasureContext, child: DOMNode, availableOuterWidth: Int?, - availableOuterHeight: Int? = null + availableOuterHeight: Int? = null, ): Size { ScrollPerformanceCounters.incrementMeasureChildForLayoutCalls() child.resolveLayoutStyleValues( ctx = ctx, parentContentWidth = availableOuterWidth, - parentContentHeight = availableOuterHeight + parentContentHeight = availableOuterHeight, ) return child.clampMeasuredOuterSize(child.measureForLayout(ctx, availableOuterWidth)) } @@ -908,23 +962,35 @@ class ContainerNode( return total } - private fun alignedChildX(child: DOMNode, contentX: Int, availableWidth: Int, childWidth: Int): Int { + private fun alignedChildX( + child: DOMNode, + contentX: Int, + availableWidth: Int, + childWidth: Int, + ): Int { val interactableWidth = (availableWidth - child.margin.horizontal).coerceAtLeast(0) - val horizontalOffset = when (child.align) { - StyleAlign.START -> 0 - StyleAlign.CENTER -> (interactableWidth - childWidth) / 2 - StyleAlign.END -> interactableWidth - childWidth - } + val horizontalOffset = + when (child.align) { + StyleAlign.START -> 0 + StyleAlign.CENTER -> (interactableWidth - childWidth) / 2 + StyleAlign.END -> interactableWidth - childWidth + } return contentX + child.margin.left + horizontalOffset.coerceAtLeast(0) } - private fun alignedChildY(child: DOMNode, contentY: Int, availableHeight: Int, childHeight: Int): Int { + private fun alignedChildY( + child: DOMNode, + contentY: Int, + availableHeight: Int, + childHeight: Int, + ): Int { val interactableHeight = (availableHeight - child.margin.vertical).coerceAtLeast(0) - val verticalOffset = when (child.align) { - StyleAlign.START -> 0 - StyleAlign.CENTER -> (interactableHeight - childHeight) / 2 - StyleAlign.END -> interactableHeight - childHeight - } + val verticalOffset = + when (child.align) { + StyleAlign.START -> 0 + StyleAlign.CENTER -> (interactableHeight - childHeight) / 2 + StyleAlign.END -> interactableHeight - childHeight + } return contentY + child.margin.top + verticalOffset.coerceAtLeast(0) } @@ -938,39 +1004,42 @@ class ContainerNode( desiredX: Int, desiredY: Int, desiredWidth: Int, - desiredHeight: Int + desiredHeight: Int, ) { - val positionedRect = when (child.position) { - PositionMode.Absolute -> child.resolveAbsoluteLayoutRect( - ctx = ctx, - desiredX = desiredX, - desiredY = desiredY, - desiredWidth = desiredWidth, - desiredHeight = desiredHeight - ) + val positionedRect = + when (child.position) { + PositionMode.Absolute -> + child.resolveAbsoluteLayoutRect( + ctx = ctx, + desiredX = desiredX, + desiredY = desiredY, + desiredWidth = desiredWidth, + desiredHeight = desiredHeight, + ) - PositionMode.Fixed -> child.resolveFixedLayoutRect( - ctx = ctx, - desiredX = desiredX, - desiredY = desiredY, - desiredWidth = desiredWidth, - desiredHeight = desiredHeight - ) + PositionMode.Fixed -> + child.resolveFixedLayoutRect( + ctx = ctx, + desiredX = desiredX, + desiredY = desiredY, + desiredWidth = desiredWidth, + desiredHeight = desiredHeight, + ) - else -> Rect( - x = desiredX, - y = desiredY, - width = desiredWidth.coerceAtLeast(0), - height = desiredHeight.coerceAtLeast(0) - ) - } + else -> + Rect( + x = desiredX, + y = desiredY, + width = desiredWidth.coerceAtLeast(0), + height = desiredHeight.coerceAtLeast(0), + ) + } child.render( ctx = ctx, x = positionedRect.x, y = positionedRect.y, width = positionedRect.width.coerceAtLeast(0), - height = positionedRect.height.coerceAtLeast(0) + height = positionedRect.height.coerceAtLeast(0), ) } } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/DateInputNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/DateInputNode.kt index 81fff09..15831b6 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/DateInputNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/DateInputNode.kt @@ -14,7 +14,7 @@ class DateInputNode( value: Instant, zoneId: ZoneId, placeholder: String = "dd.MM.yyyy HH:mm", - key: Any? = null + key: Any? = null, ) : SingleLineInputNode("", placeholder, key) { private val initialValue: Instant = value private val initialZoneId: ZoneId = zoneId @@ -63,25 +63,25 @@ class DateInputNode( return formatter.format(dateTime) } - private fun parseInstant(text: String): Instant? { - return try { + private fun parseInstant(text: String): Instant? = + try { val dateTime = LocalDateTime.parse(text, formatter) dateTime.atZone(this.zoneId).toInstant() } catch (ex: Exception) { null } - } private fun isValidPrefix(text: String): Boolean { if (text.isEmpty()) return true for (i in text.indices) { val ch = text[i] - val expected = when (i) { - 2, 5 -> '.' - 10 -> ' ' - 13 -> ':' - else -> null - } + val expected = + when (i) { + 2, 5 -> '.' + 10 -> ' ' + 13 -> ':' + else -> null + } if (expected != null) { if (ch != expected) return false } else if (!ch.isDigit()) { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ImageNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ImageNode.kt index 0fbdf5b..ca61920 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ImageNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ImageNode.kt @@ -12,17 +12,14 @@ class ImageNode( var url: String, var imageWidth: Int = 0, var imageHeight: Int = 0, - key: Any? = null + key: Any? = null, ) : DOMNode(key) { override val styleType: String = "img" - internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { - return measureWithConstraint(availableOuterWidth) - } + internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size = + measureWithConstraint(availableOuterWidth) - override fun measure(ctx: UiMeasureContext): Size { - return measureWithConstraint(null) - } + override fun measure(ctx: UiMeasureContext): Size = measureWithConstraint(null) private fun measureWithConstraint(availableOuterWidth: Int?): Size { val contentLimit = resolvedContentLimit(availableOuterWidth) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/InputOption.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/InputOption.kt index 401749c..d2c32a3 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/InputOption.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/InputOption.kt @@ -5,5 +5,5 @@ package org.dreamfinity.dsgl.core.dom.elements */ data class InputOption( val id: String, - val label: String = id -) \ No newline at end of file + val label: String = id, +) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/InputType.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/InputType.kt index 105e9e3..1b77234 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/InputType.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/InputType.kt @@ -13,7 +13,7 @@ sealed class InputType { val placeholder: String = "", val allowedChars: String? = null, val minLength: Int? = null, - val maxLength: Int? = null + val maxLength: Int? = null, ) : InputType() /** Password input rendered with masking. */ @@ -21,7 +21,7 @@ sealed class InputType { val value: String = "", val placeholder: String = "", val minLength: Int? = null, - val maxLength: Int? = null + val maxLength: Int? = null, ) : InputType() /** Numeric input. */ @@ -29,7 +29,7 @@ sealed class InputType { val value: Long = 0L, val placeholder: String = "", val min: Long? = null, - val max: Long? = null + val max: Long? = null, ) : InputType() /** Range slider input. */ @@ -37,7 +37,7 @@ sealed class InputType { val value: Long = 0L, val min: Long = 0L, val max: Long = 100L, - val step: Long? = null + val step: Long? = null, ) : InputType() /** Checkbox group input. */ @@ -45,19 +45,19 @@ sealed class InputType { val variants: List, val selected: Set = emptySet(), val minSelected: Int? = null, - val maxSelected: Int? = null + val maxSelected: Int? = null, ) : InputType() /** Radio group input. */ data class Radio( val variants: List, - val selected: String? = null + val selected: String? = null, ) : InputType() /** Date/time input. */ data class Date( val value: Instant? = null, val zoneId: ZoneId? = null, - val placeholder: String = "dd.MM.yyyy HH:mm" + val placeholder: String = "dd.MM.yyyy HH:mm", ) : InputType() -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ItemStackNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ItemStackNode.kt index 0742db3..01bfb09 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ItemStackNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ItemStackNode.kt @@ -14,17 +14,14 @@ class ItemStackNode( var size: Int = 18, var rotYDeg: Double = 160.0, var rotXDeg: Double = -11.0, - key: Any? = null + key: Any? = null, ) : DOMNode(key) { override val styleType: String = "itemstack" - internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { - return measureWithConstraint(ctx, availableOuterWidth) - } + internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size = + measureWithConstraint(ctx, availableOuterWidth) - override fun measure(ctx: UiMeasureContext): Size { - return measureWithConstraint(ctx, null) - } + override fun measure(ctx: UiMeasureContext): Size = measureWithConstraint(ctx, null) private fun measureWithConstraint(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { val contentLimit = resolvedContentLimit(availableOuterWidth) @@ -58,8 +55,8 @@ class ItemStackNode( contentWidth(), size, rotYDeg, - rotXDeg - ) + rotXDeg, + ), ) } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/NumberInputNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/NumberInputNode.kt index badf5bd..97b740f 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/NumberInputNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/NumberInputNode.kt @@ -13,7 +13,7 @@ class NumberInputNode( placeholder: String = "", var min: Long? = null, var max: Long? = null, - key: Any? = null + key: Any? = null, ) : SingleLineInputNode(value.toString(), placeholder, key) { private val initialValue: Long = value var value: Long = initialValue diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/PasswordInputNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/PasswordInputNode.kt index 320c87a..b00ea3b 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/PasswordInputNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/PasswordInputNode.kt @@ -8,18 +8,16 @@ class PasswordInputNode( placeholder: String = "", minLength: Int? = null, maxLength: Int? = null, - key: Any? = null + key: Any? = null, ) : SingleLineInputNode(text, placeholder, key) { init { this.minLength = minLength this.maxLength = maxLength } - override fun displayText(): String { - return if (text.isEmpty()) "" else "*".repeat(text.length) - } + override fun displayText(): String = if (text.isEmpty()) "" else "*".repeat(text.length) override fun allowClipboardCopy(): Boolean = false override fun allowClipboardCut(): Boolean = false -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/RadioGroupNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/RadioGroupNode.kt index 3b028cb..be54fe8 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/RadioGroupNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/RadioGroupNode.kt @@ -14,7 +14,7 @@ import org.dreamfinity.dsgl.core.render.RenderCommand class RadioGroupNode( var variants: List, var selectedId: String? = null, - key: Any? = null + key: Any? = null, ) : DOMNode(key) { override val styleType: String = "input" override val focusable: Boolean = true @@ -42,13 +42,10 @@ class RadioGroupNode( } } - internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { - return measureWithConstraint(ctx, availableOuterWidth) - } + internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size = + measureWithConstraint(ctx, availableOuterWidth) - override fun measure(ctx: UiMeasureContext): Size { - return measureWithConstraint(ctx, null) - } + override fun measure(ctx: UiMeasureContext): Size = measureWithConstraint(ctx, null) private fun measureWithConstraint(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { val fontHeight = resolveFontSize(ctx) @@ -75,7 +72,13 @@ class RadioGroupNode( } } - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) val fontHeight = resolveFontSize(ctx) boxSize = maxOf(10, fontHeight - 2) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/RangeInputNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/RangeInputNode.kt index 9d1d996..29f49e5 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/RangeInputNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/RangeInputNode.kt @@ -17,11 +17,11 @@ class RangeInputNode( var min: Long = 0L, var max: Long = 100L, var step: Long? = null, - key: Any? = null + key: Any? = null, ) : DOMNode(key) { private data class SliderGeometry( val trackRect: Rect, - val knobSize: Int + val knobSize: Int, ) companion object { @@ -57,7 +57,7 @@ class RangeInputNode( postInput( this@RangeInputNode, this@RangeInputNode.value.toString(), - this@RangeInputNode.value + this@RangeInputNode.value, ) } } @@ -70,7 +70,7 @@ class RangeInputNode( postChange( this@RangeInputNode, this@RangeInputNode.value.toString(), - this@RangeInputNode.value + this@RangeInputNode.value, ) } clearActiveDrag() @@ -85,20 +85,17 @@ class RangeInputNode( postInput( this@RangeInputNode, this@RangeInputNode.value.toString(), - this@RangeInputNode.value + this@RangeInputNode.value, ) } } } } - internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { - return measureWithConstraint(availableOuterWidth) - } + internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size = + measureWithConstraint(availableOuterWidth) - override fun measure(ctx: UiMeasureContext): Size { - return measureWithConstraint(null) - } + override fun measure(ctx: UiMeasureContext): Size = measureWithConstraint(null) private fun measureWithConstraint(availableOuterWidth: Int?): Size { val contentLimit = resolvedContentLimit(availableOuterWidth) @@ -121,7 +118,13 @@ class RangeInputNode( } } - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) } @@ -135,8 +138,8 @@ class RangeInputNode( geometry.trackRect.y, geometry.trackRect.width, geometry.trackRect.height, - trackColor - ) + trackColor, + ), ) val knobX = valueToX(geometry) val knobY = geometry.trackRect.y + (geometry.trackRect.height - geometry.knobSize) / 2 @@ -166,7 +169,8 @@ class RangeInputNode( val trackWidth = trackRect.width.coerceAtLeast(1) if (max == min) return trackRect.x val ratio = (value - min).toDouble() / (max - min).toDouble() - return (trackRect.x + ratio * trackWidth - knobSize / 2.0).toInt() + return (trackRect.x + ratio * trackWidth - knobSize / 2.0) + .toInt() .coerceIn(trackRect.x, trackRect.x + trackWidth - knobSize) } @@ -174,15 +178,16 @@ class RangeInputNode( val contentHeight = contentHeight() val resolvedTrackHeight = maxOf(2, contentHeight / 3) val resolvedKnobSize = maxOf(resolvedTrackHeight * 2, 8) - val trackRect = Rect( - x = contentX(), - y = contentY() + (contentHeight - resolvedTrackHeight) / 2, - width = contentWidth(), - height = resolvedTrackHeight - ) + val trackRect = + Rect( + x = contentX(), + y = contentY() + (contentHeight - resolvedTrackHeight) / 2, + width = contentWidth(), + height = resolvedTrackHeight, + ) return SliderGeometry( trackRect = trackRect, - knobSize = resolvedKnobSize + knobSize = resolvedKnobSize, ) } @@ -193,9 +198,7 @@ class RangeInputNode( value = clamped } - private fun dragIdentity(): Any { - return key ?: this - } + private fun dragIdentity(): Any = key ?: this private fun isActiveDragTarget(): Boolean { val active = activeDragIdentity ?: return false diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt index 13f75bd..206a3c9 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt @@ -21,7 +21,7 @@ class SelectNode( defaultValue: String? = null, closeOnSelect: Boolean = true, ownerScope: OverlayOwnerScope = OverlayOwnerScope.Application, - key: Any? = null + key: Any? = null, ) : DOMNode(key) { override val styleType: String = "select" override val focusable: Boolean = true @@ -64,7 +64,10 @@ class SelectNode( var backgroundColor: Int = 0xFF2E2E33.toInt() var disabledTextColor: Int = 0xFF8E8E8E.toInt() var minContentWidth: Int = 92 - var arrowGlyph: String = SelectRuntime.engine.currentStyle().arrowGlyph + var arrowGlyph: String = + SelectRuntime.engine + .currentStyle() + .arrowGlyph var arrowSpacing: Int = 8 private var uncontrolledValue: String? = defaultValue @@ -116,13 +119,10 @@ class SelectNode( } } - internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { - return measureWithConstraint(ctx, availableOuterWidth) - } + internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size = + measureWithConstraint(ctx, availableOuterWidth) - override fun measure(ctx: UiMeasureContext): Size { - return measureWithConstraint(ctx, null) - } + override fun measure(ctx: UiMeasureContext): Size = measureWithConstraint(ctx, null) private fun measureWithConstraint(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { val lineHeight = resolveFontSize(ctx) @@ -156,11 +156,12 @@ class SelectNode( syncPopup() val isFocused = FocusManager.isFocused(this) && !styleDisabled val textValue = selectedLabelOrPlaceholder() - val drawColor = when { - styleDisabled -> disabledTextColor - selectedOptionId() == null -> placeholderColor - else -> textColor - } + val drawColor = + when { + styleDisabled -> disabledTextColor + selectedOptionId() == null -> placeholderColor + else -> textColor + } val arrowWidth = if (arrowGlyph.isEmpty()) 0 else measureText(ctx, arrowGlyph) val lineHeight = resolveFontSize(ctx) out += RenderCommand.DrawRect(bounds.x, bounds.y, bounds.width, bounds.height, backgroundColor) @@ -177,27 +178,28 @@ class SelectNode( out += RenderCommand.PushClip(innerX, innerY, textClipWidth, innerHeight.coerceAtLeast(1)) if (textValue.isNotEmpty()) { - out += drawTextCommand( - ctx, - text = textValue, - x = innerX, - y = textY, - color = drawColor - ) + out += + drawTextCommand( + ctx, + text = textValue, + x = innerX, + y = textY, + color = drawColor, + ) } out += RenderCommand.PopClip if (arrowGlyph.isNotEmpty()) { val arrowColor = if (styleDisabled) disabledTextColor else textColor - out += drawTextCommand( - ctx, - text = arrowGlyph, - x = arrowX, - y = textY, - color = arrowColor - ) + out += + drawTextCommand( + ctx, + text = arrowGlyph, + x = arrowX, + y = textY, + color = arrowColor, + ) } - } override fun volatileRenderCommandsSignature(nowMs: Long): Long { @@ -270,7 +272,7 @@ class SelectNode( onClose = { setOpenState(false) }, fontId = fontId, fontSize = fontSize, - ownerScope = ownerScope + ownerScope = ownerScope, ) } @@ -299,7 +301,9 @@ class SelectNode( return option.labelProvider.invoke() } } - return model.placeholderProvider?.invoke().orEmpty() + return model.placeholderProvider + ?.invoke() + .orEmpty() } private fun reconcileSelection() { @@ -310,17 +314,16 @@ class SelectNode( if (controlled) { return } - uncontrolledValue = when { - !defaultValue.isNullOrEmpty() && optionExists(defaultValue!!) -> defaultValue - else -> firstEnabledOptionId() - } + uncontrolledValue = + when { + !defaultValue.isNullOrEmpty() && optionExists(defaultValue!!) -> defaultValue + else -> firstEnabledOptionId() + } } private fun hasEnabledOption(): Boolean = firstEnabledOptionId() != null - private fun firstEnabledOptionId(): String? { - return firstEnabledOptionId(model.entries) - } + private fun firstEnabledOptionId(): String? = firstEnabledOptionId(model.entries) private fun firstEnabledOptionId(entries: List): String? { entries.forEach { entry -> @@ -337,13 +340,9 @@ class SelectNode( return null } - private fun optionExists(optionId: String): Boolean { - return findOption(optionId) != null - } + private fun optionExists(optionId: String): Boolean = findOption(optionId) != null - private fun findOption(optionId: String): SelectEntry.Option? { - return findOption(model.entries, optionId) - } + private fun findOption(optionId: String): SelectEntry.Option? = findOption(model.entries, optionId) private fun findOption(entries: List, optionId: String): SelectEntry.Option? { entries.forEach { entry -> diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SingleLineInputNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SingleLineInputNode.kt index cc73e4c..1fb2d11 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SingleLineInputNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SingleLineInputNode.kt @@ -16,20 +16,20 @@ import org.dreamfinity.dsgl.core.style.TextWrap open class SingleLineInputNode( text: String = "", var placeholder: String = "", - key: Any? = null + key: Any? = null, ) : DOMNode(key) { companion object { private data class UndoSnapshot( val text: String, val caretIndex: Int, - val selectionAnchor: Int? + val selectionAnchor: Int?, ) private data class PersistedState( val caretIndex: Int, val selectionAnchor: Int?, val undoHistory: List, - val redoHistory: List + val redoHistory: List, ) private val persistedByKey: KeyedStateStore = KeyedStateStore() @@ -37,7 +37,6 @@ open class SingleLineInputNode( fun clearActiveDrag() { activeSelectionDragIdentity = null - } } @@ -110,15 +109,18 @@ open class SingleLineInputNode( } protected open fun displayText(): String = this.text + protected open fun currentEventValue(): String = this.text + protected open fun currentParsedValue(): Any? = this.text + protected open fun allowClipboardCopy(): Boolean = true + protected open fun allowClipboardCut(): Boolean = true + protected open fun allowClipboardPaste(): Boolean = true - protected open fun sanitizePastedText(raw: String): String { - return raw.replace("\r", "").replace("\n", "") - } + protected open fun sanitizePastedText(raw: String): String = raw.replace("\r", "").replace("\n", "") protected open fun handleKey(event: KeyboardKeyDownEvent) { editState.clampToLength(text.length) @@ -148,13 +150,9 @@ open class SingleLineInputNode( } } - protected fun isPrintable(ch: Char): Boolean { - return TextEditOps.isPrintable(ch) - } + protected fun isPrintable(ch: Char): Boolean = TextEditOps.isPrintable(ch) - protected open fun canAcceptText(next: String): Boolean { - return !(maxLength != null && next.length > maxLength!!) - } + protected open fun canAcceptText(next: String): Boolean = !(maxLength != null && next.length > maxLength!!) protected open fun applyText(next: String) { text = next @@ -173,13 +171,10 @@ open class SingleLineInputNode( postInput(this, current, currentParsedValue()) } - internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { - return measureWithConstraint(ctx, availableOuterWidth) - } + internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size = + measureWithConstraint(ctx, availableOuterWidth) - override fun measure(ctx: UiMeasureContext): Size { - return measureWithConstraint(ctx, null) - } + override fun measure(ctx: UiMeasureContext): Size = measureWithConstraint(ctx, null) private fun measureWithConstraint(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { val lineHeight = resolveFontSize(ctx) @@ -252,7 +247,6 @@ open class SingleLineInputNode( if (innerWidth > 0 && innerHeight > 0) { out.add(RenderCommand.PopClip) } - } fun shouldCaptureTextSelectionDrag(mouseX: Int, mouseY: Int): Boolean { @@ -345,7 +339,12 @@ open class SingleLineInputNode( return replaceRange(start, end, insert, recordUndo) } - private fun replaceRange(start: Int, end: Int, insert: String, recordUndo: Boolean = false): Boolean { + private fun replaceRange( + start: Int, + end: Int, + insert: String, + recordUndo: Boolean = false, + ): Boolean { val safeStart = start.coerceIn(0, text.length) val safeEnd = end.coerceIn(safeStart, text.length) val previous = currentEventValue() @@ -365,8 +364,8 @@ open class SingleLineInputNode( return true } - private fun handleClipboardShortcut(event: KeyboardKeyDownEvent): Boolean { - return TextEditShortcutDispatcher.dispatch( + private fun handleClipboardShortcut(event: KeyboardKeyDownEvent): Boolean = + TextEditShortcutDispatcher.dispatch( event, TextShortcutCallbacks( canCopy = allowClipboardCopy(), @@ -391,28 +390,26 @@ open class SingleLineInputNode( if (action != TextShortcutAction.COPY) { persistState() } - } - ) + }, + ), ) - } private fun pushUndoSnapshot() { history.pushUndo( UndoSnapshot( text = text, caretIndex = editState.caretIndex, - selectionAnchor = editState.selectionAnchor - ) + selectionAnchor = editState.selectionAnchor, + ), ) } - private fun currentSnapshot(): UndoSnapshot { - return UndoSnapshot( + private fun currentSnapshot(): UndoSnapshot = + UndoSnapshot( text = text, caretIndex = editState.caretIndex, - selectionAnchor = editState.selectionAnchor + selectionAnchor = editState.selectionAnchor, ) - } private fun undoLastEdit(): Boolean { val snapshot = history.undo(currentSnapshot()) ?: return false @@ -436,17 +433,14 @@ open class SingleLineInputNode( return true } - private fun shouldRecordTypingUndo(ch: Char): Boolean { - return typingUndoGrouping.shouldRecord(ch, editState.hasSelection()) - } + private fun shouldRecordTypingUndo(ch: Char): Boolean = + typingUndoGrouping.shouldRecord(ch, editState.hasSelection()) private fun resetTypingUndoGroup() { typingUndoGrouping.reset() } - private fun selectedText(): String { - return TextEditOps.selectedText(text, editState) - } + private fun selectedText(): String = TextEditOps.selectedText(text, editState) private fun caretIndexFromMouseX(mouseX: Int): Int { val rendered = displayText() @@ -469,9 +463,7 @@ open class SingleLineInputNode( return rendered.length } - private fun dragIdentity(): Any { - return TextEditOps.dragIdentity(key, this) - } + private fun dragIdentity(): Any = TextEditOps.dragIdentity(key, this) private fun isActiveSelectionDragTarget(): Boolean { val active = activeSelectionDragIdentity ?: return false @@ -486,8 +478,8 @@ open class SingleLineInputNode( caretIndex = editState.caretIndex, selectionAnchor = editState.selectionAnchor, undoHistory = history.undoHistory(), - redoHistory = history.redoHistory() - ) + redoHistory = history.redoHistory(), + ), ) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextAreaNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextAreaNode.kt index a77a235..e8d4340 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextAreaNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextAreaNode.kt @@ -17,7 +17,7 @@ import org.dreamfinity.dsgl.core.style.TextWrap class TextAreaNode( var text: String = "", var placeholder: String = "", - key: Any? = null + key: Any? = null, ) : DOMNode(key) { companion object { private data class UndoSnapshot( @@ -25,7 +25,7 @@ class TextAreaNode( val caretIndex: Int, val selectionAnchor: Int?, val scrollY: Int, - val preferredColumn: Int? + val preferredColumn: Int?, ) private data class PersistedState( @@ -34,7 +34,7 @@ class TextAreaNode( val preferredColumn: Int?, val selectionAnchor: Int?, val undoHistory: List, - val redoHistory: List + val redoHistory: List, ) private val persistedByKey: KeyedStateStore = KeyedStateStore() @@ -165,13 +165,10 @@ class TextAreaNode( } } - override fun measure(ctx: UiMeasureContext): Size { - return measureWithConstraint(ctx, null) - } + override fun measure(ctx: UiMeasureContext): Size = measureWithConstraint(ctx, null) - internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { - return measureWithConstraint(ctx, availableOuterWidth) - } + internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size = + measureWithConstraint(ctx, availableOuterWidth) private fun measureWithConstraint(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { val lineHeight = resolveFontSize(ctx) @@ -180,17 +177,19 @@ class TextAreaNode( lastLineHeight = lineHeight val display = text.ifEmpty { placeholder } val contentLimit = resolvedContentLimit(availableOuterWidth) - val naturalContentWidth = width ?: maxOf( - layoutForText(display, null, lineHeight, measure).maxLineWidth, - minContentWidth - ) + val naturalContentWidth = + width ?: maxOf( + layoutForText(display, null, lineHeight, measure).maxLineWidth, + minContentWidth, + ) val contentWidth = contentLimit?.let { minOf(it, naturalContentWidth) } ?: naturalContentWidth - val textLayout = layoutForText( - source = display, - contentWidth = if (textWrap == TextWrap.Wrap) contentWidth else null, - fontHeight = lineHeight, - measureText = measure - ) + val textLayout = + layoutForText( + source = display, + contentWidth = if (textWrap == TextWrap.Wrap) contentWidth else null, + fontHeight = lineHeight, + measureText = measure, + ) val contentHeight = height ?: maxOf(textLayout.totalHeight, minContentHeight) val totalWidth = contentWidth + padding.horizontal + border.horizontal val totalHeight = contentHeight + padding.vertical + border.vertical @@ -243,16 +242,18 @@ class TextAreaNode( clampScroll() val showPlaceholder = text.isEmpty() && !focused && placeholder.isNotEmpty() - val drawLayout = if (showPlaceholder) { - layoutForText(placeholder, textInnerWidth, lineHeight, measure) - } else { - textLayout - } + val drawLayout = + if (showPlaceholder) { + layoutForText(placeholder, textInnerWidth, lineHeight, measure) + } else { + textLayout + } val color = if (showPlaceholder) placeholderColor else textColor val effectiveScroll = if (showPlaceholder) 0 else editState.scrollY.coerceIn(0, maxScroll) val firstVisibleLine = (effectiveScroll / lastLineHeight).coerceIn(0, drawLayout.lines.lastIndex) - val lastVisibleLine = ((effectiveScroll + innerHeight) / lastLineHeight + 1) - .coerceIn(0, drawLayout.lines.lastIndex) + val lastVisibleLine = + ((effectiveScroll + innerHeight) / lastLineHeight + 1) + .coerceIn(0, drawLayout.lines.lastIndex) if (textInnerWidth > 0 && innerHeight > 0) { out.add(RenderCommand.PushClip(innerX, innerY, textInnerWidth, innerHeight)) @@ -332,13 +333,10 @@ class TextAreaNode( return containsGlobalPoint(mouseX, mouseY) } - fun shouldCaptureAnyDrag(mouseX: Int, mouseY: Int): Boolean { - return shouldCaptureScrollbarDrag(mouseX, mouseY) || shouldCaptureTextSelectionDrag(mouseX, mouseY) - } + fun shouldCaptureAnyDrag(mouseX: Int, mouseY: Int): Boolean = + shouldCaptureScrollbarDrag(mouseX, mouseY) || shouldCaptureTextSelectionDrag(mouseX, mouseY) - override fun inspectorScrollOffset(): Pair? { - return 0 to editState.scrollY - } + override fun inspectorScrollOffset(): Pair? = 0 to editState.scrollY private fun handleKey(event: KeyboardKeyDownEvent) { editState.clampToLength(text.length) @@ -370,8 +368,8 @@ class TextAreaNode( } } - private fun handleClipboardShortcut(event: KeyboardKeyDownEvent): Boolean { - return TextEditShortcutDispatcher.dispatch( + private fun handleClipboardShortcut(event: KeyboardKeyDownEvent): Boolean = + TextEditShortcutDispatcher.dispatch( event, TextShortcutCallbacks( hasSelection = { editState.hasSelection() }, @@ -393,10 +391,9 @@ class TextAreaNode( if (action != TextShortcutAction.COPY) { persistState() } - } - ) + }, + ), ) - } private fun pushUndoSnapshot() { history.pushUndo( @@ -405,20 +402,19 @@ class TextAreaNode( caretIndex = editState.caretIndex, selectionAnchor = editState.selectionAnchor, scrollY = editState.scrollY, - preferredColumn = preferredColumn - ) + preferredColumn = preferredColumn, + ), ) } - private fun currentSnapshot(): UndoSnapshot { - return UndoSnapshot( + private fun currentSnapshot(): UndoSnapshot = + UndoSnapshot( text = text, caretIndex = editState.caretIndex, selectionAnchor = editState.selectionAnchor, scrollY = editState.scrollY, - preferredColumn = preferredColumn + preferredColumn = preferredColumn, ) - } private fun undoLastEdit(): Boolean { val snapshot = history.undo(currentSnapshot()) ?: return false @@ -454,9 +450,8 @@ class TextAreaNode( return true } - private fun shouldRecordTypingUndo(ch: Char): Boolean { - return typingUndoGrouping.shouldRecord(ch, editState.hasSelection()) - } + private fun shouldRecordTypingUndo(ch: Char): Boolean = + typingUndoGrouping.shouldRecord(ch, editState.hasSelection()) private fun resetTypingUndoGroup() { typingUndoGrouping.reset() @@ -534,11 +529,12 @@ class TextAreaNode( val layout = currentTextLayout() val current = caretLineAndColumn(editState.caretIndex, layout) val line = layout.lines[current.first] - val next = if (start) { - line.startIndex - } else { - line.endIndexExclusive - } + val next = + if (start) { + line.startIndex + } else { + line.endIndexExclusive + } moveCaretTo(next, extend) preferredColumn = null ensureCaretVisible() @@ -574,7 +570,12 @@ class TextAreaNode( return replaceRange(start, end, insert, recordUndo) } - private fun replaceRange(start: Int, end: Int, insert: String, recordUndo: Boolean = false): Boolean { + private fun replaceRange( + start: Int, + end: Int, + insert: String, + recordUndo: Boolean = false, + ): Boolean { val safeStart = start.coerceIn(0, text.length) val safeEnd = end.coerceIn(safeStart, text.length) val previous = text @@ -636,9 +637,12 @@ class TextAreaNode( private fun maxScrollFor(source: String): Int { val visibleHeight = lastVisibleHeight.coerceAtLeast(lastLineHeight) - val layout = if (source == text) currentTextLayout() else { - layoutForText(source, textContentWrapWidth(), lastLineHeight, currentMeasure()) - } + val layout = + if (source == text) { + currentTextLayout() + } else { + layoutForText(source, textContentWrapWidth(), lastLineHeight, currentMeasure()) + } val totalHeight = layout.totalHeight return (totalHeight - visibleHeight).coerceAtLeast(0) } @@ -649,7 +653,7 @@ class TextAreaNode( innerY: Int, innerWidth: Int, innerHeight: Int, - focused: Boolean + focused: Boolean, ) { val trackX = innerX + innerWidth - scrollbarWidth val trackWidth = scrollbarWidth.coerceAtLeast(1) @@ -662,17 +666,18 @@ class TextAreaNode( trackRect.y, trackRect.width, trackRect.height, - scrollbarTrackColor - ) + scrollbarTrackColor, + ), ) val thumbHeight = computeThumbHeight(trackHeight) val thumbTravel = (trackHeight - thumbHeight).coerceAtLeast(0) - val thumbOffset = if (lastMaxScroll <= 0 || thumbTravel <= 0) { - 0 - } else { - ((editState.scrollY.toDouble() / lastMaxScroll.toDouble()) * thumbTravel.toDouble()).toInt() - } + val thumbOffset = + if (lastMaxScroll <= 0 || thumbTravel <= 0) { + 0 + } else { + ((editState.scrollY.toDouble() / lastMaxScroll.toDouble()) * thumbTravel.toDouble()).toInt() + } val thumbY = innerY + thumbOffset val thumbRect = Rect(trackX, thumbY, trackWidth, thumbHeight) scrollbarThumbRect = thumbRect @@ -687,7 +692,7 @@ class TextAreaNode( lastVisibleLine: Int, innerX: Int, innerY: Int, - effectiveScroll: Int + effectiveScroll: Int, ) { if (!editState.hasSelection()) return val startIndex = editState.selectionStart().coerceIn(0, text.length) @@ -732,11 +737,12 @@ class TextAreaNode( FocusManager.requestFocus(this) activeScrollbarDragIdentity = dragIdentity() val thumb = scrollbarThumbRect - scrollbarDragAnchorY = if (thumb != null && thumb.contains(mouseX, mouseY)) { - (mouseY - thumb.y).coerceIn(0, thumb.height.coerceAtLeast(1)) - } else { - computeThumbHeight(scrollbarTrackRect?.height ?: 0) / 2 - } + scrollbarDragAnchorY = + if (thumb != null && thumb.contains(mouseX, mouseY)) { + (mouseY - thumb.y).coerceIn(0, thumb.height.coerceAtLeast(1)) + } else { + computeThumbHeight(scrollbarTrackRect?.height ?: 0) / 2 + } updateScrollbarFromDrag(mouseY) editState.resetBlinkClock() persistState() @@ -756,8 +762,10 @@ class TextAreaNode( return } val desiredTop = (mouseY - track.y - scrollbarDragAnchorY).coerceIn(0, thumbTravel) - editState.scrollY = ((desiredTop.toDouble() / thumbTravel.toDouble()) * lastMaxScroll.toDouble()).toInt() - .coerceIn(0, lastMaxScroll) + editState.scrollY = + ((desiredTop.toDouble() / thumbTravel.toDouble()) * lastMaxScroll.toDouble()) + .toInt() + .coerceIn(0, lastMaxScroll) } private fun caretIndexFromClick(mouseX: Int, mouseY: Int, scrollOffsetY: Int): Int { @@ -792,17 +800,11 @@ class TextAreaNode( return lineText.length } - private fun selectedText(): String { - return TextEditOps.selectedText(text, editState) - } + private fun selectedText(): String = TextEditOps.selectedText(text, editState) - private fun isPrintable(ch: Char): Boolean { - return TextEditOps.isPrintable(ch) - } + private fun isPrintable(ch: Char): Boolean = TextEditOps.isPrintable(ch) - private fun dragIdentity(): Any { - return TextEditOps.dragIdentity(key, this) - } + private fun dragIdentity(): Any = TextEditOps.dragIdentity(key, this) private fun isActiveScrollbarDragTarget(): Boolean { val active = activeScrollbarDragIdentity ?: return false @@ -829,8 +831,8 @@ class TextAreaNode( preferredColumn = preferredColumn, selectionAnchor = editState.selectionAnchor, undoHistory = history.undoHistory(), - redoHistory = history.redoHistory() - ) + redoHistory = history.redoHistory(), + ), ) } @@ -865,15 +867,13 @@ class TextAreaNode( } } - private fun currentMeasure(): (String) -> Int { - return lastMeasureText ?: { value: String -> value.length * 6 } - } + private fun currentMeasure(): (String) -> Int = lastMeasureText ?: { value: String -> value.length * 6 } private fun layoutForText( source: String, contentWidth: Int?, fontHeight: Int, - measureText: (String) -> Int + measureText: (String) -> Int, ): TextLayoutEngine.Layout { val wrapMode = if (this@TextAreaNode.textWrap == TextWrap.Wrap) TextWrap.Wrap else TextWrap.NoWrap val maxWidth = if (wrapMode == TextWrap.Wrap) contentWidth else null @@ -882,7 +882,7 @@ class TextAreaNode( maxWidth = maxWidth, wrap = wrapMode, fontHeight = fontHeight.coerceAtLeast(1), - measureText = measureText + measureText = measureText, ) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextEditState.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextEditState.kt index 0831743..74a0917 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextEditState.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextEditState.kt @@ -7,7 +7,7 @@ data class TextEditState( var caretIndex: Int = 0, var selectionAnchor: Int? = null, var scrollY: Int = 0, - var lastInteractionAtMs: Long = System.currentTimeMillis() + var lastInteractionAtMs: Long = System.currentTimeMillis(), ) { fun clampToLength(length: Int) { caretIndex = caretIndex.coerceIn(0, length) @@ -42,4 +42,4 @@ data class TextEditState( val ticks = ((now - lastInteractionAtMs).coerceAtLeast(0L) / blinkPeriodMs) return ticks % 2L == 0L } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextInputNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextInputNode.kt index 7c08699..f149f04 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextInputNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextInputNode.kt @@ -9,11 +9,11 @@ class TextInputNode( allowedChars: String? = null, minLength: Int? = null, maxLength: Int? = null, - key: Any? = null + key: Any? = null, ) : SingleLineInputNode(text, placeholder, key) { init { this.allowedChars = allowedChars this.minLength = minLength this.maxLength = maxLength } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextNode.kt index 71d0d54..fd36e47 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextNode.kt @@ -16,7 +16,7 @@ import org.dreamfinity.dsgl.core.text.MinecraftFormattingParser class TextNode( private var textSource: TextSource, var color: Int = DsglColors.TEXT, - key: Any? = null + key: Any? = null, ) : DOMNode(key) { companion object { const val NORMAL_LINE_HEIGHT_MULTIPLIER: Float = DOMNode.NORMAL_LINE_HEIGHT_MULTIPLIER @@ -27,13 +27,10 @@ class TextNode( var text: String = textSource.resolve() private set - internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { - return measureWithConstraint(ctx, availableOuterWidth) - } + internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size = + measureWithConstraint(ctx, availableOuterWidth) - override fun measure(ctx: UiMeasureContext): Size { - return measureWithConstraint(ctx, null) - } + override fun measure(ctx: UiMeasureContext): Size = measureWithConstraint(ctx, null) private fun measureWithConstraint(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { val textMetrics = resolveTextMetrics(ctx) @@ -41,25 +38,27 @@ class TextNode( val parsed = parseTextForFormatting(this@TextNode.text) val plainText = parsed.plainText val baseFlags = baseTextStyleFlags() - val measuredRanges = MeasuredTextRangeWidthSource( - plainText = plainText, - fontId = fontId, - fontSizePx = textMetrics.fontSizePx, - baseFlags = baseFlags, - spans = parsed.spans, - ctx = ctx - ) + val measuredRanges = + MeasuredTextRangeWidthSource( + plainText = plainText, + fontId = fontId, + fontSizePx = textMetrics.fontSizePx, + baseFlags = baseFlags, + spans = parsed.spans, + ctx = ctx, + ) val contentLimit = resolvedContentLimit(availableOuterWidth) val wrapWidth = if (textWrap == TextWrap.Wrap) contentLimit else null - val layout = TextLayoutEngine.layout( - text = plainText, - maxWidth = wrapWidth, - wrap = textWrap, - fontHeight = lineHeight, - measureText = { value -> ctx.measureText(value, fontId, textMetrics.fontSizePx) }, - measureRange = measuredRanges::measureRange, - measureRangeCacheKey = measuredRanges.cacheKey - ) + val layout = + TextLayoutEngine.layout( + text = plainText, + maxWidth = wrapWidth, + wrap = textWrap, + fontHeight = lineHeight, + measureText = { value -> ctx.measureText(value, fontId, textMetrics.fontSizePx) }, + measureRange = measuredRanges::measureRange, + measureRangeCacheKey = measuredRanges.cacheKey, + ) val naturalContentWidth = width ?: layout.maxLineWidth val contentWidth = contentLimit?.let { minOf(it, naturalContentWidth) } ?: naturalContentWidth val contentHeight = height ?: layout.totalHeight @@ -86,46 +85,50 @@ class TextNode( val parsed = parseTextForFormatting(this@TextNode.text) val plainText = parsed.plainText val baseFlags = baseTextStyleFlags() - val measuredRanges = MeasuredTextRangeWidthSource( - plainText = plainText, - fontId = fontId, - fontSizePx = textMetrics.fontSizePx, - baseFlags = baseFlags, - spans = parsed.spans, - ctx = ctx - ) + val measuredRanges = + MeasuredTextRangeWidthSource( + plainText = plainText, + fontId = fontId, + fontSizePx = textMetrics.fontSizePx, + baseFlags = baseFlags, + spans = parsed.spans, + ctx = ctx, + ) addBorderCommands(out) val wrapWidth = if (textWrap == TextWrap.Wrap) contentWidth() else null - val layout = TextLayoutEngine.layout( - text = plainText, - maxWidth = wrapWidth, - wrap = textWrap, - fontHeight = lineHeight, - measureText = { value -> ctx.measureText(value, fontId, textMetrics.fontSizePx) }, - measureRange = measuredRanges::measureRange, - measureRangeCacheKey = measuredRanges.cacheKey - ) + val layout = + TextLayoutEngine.layout( + text = plainText, + maxWidth = wrapWidth, + wrap = textWrap, + fontHeight = lineHeight, + measureText = { value -> ctx.measureText(value, fontId, textMetrics.fontSizePx) }, + measureRange = measuredRanges::measureRange, + measureRangeCacheKey = measuredRanges.cacheKey, + ) val baseX = contentX() var lineY = contentY() layout.lines.forEach { line -> - val spans = MinecraftFormattingParser.resolveStyleSpans( - parsed = parsed, - baseColor = color, - baseFlags = baseFlags, - rangeStart = line.startIndex, - rangeEnd = line.endIndexExclusive - ).map { span -> - RenderCommand.TextStyleSpan( - start = span.start, - end = span.end, - color = span.color, - bold = span.flags.bold, - italic = span.flags.italic, - underline = span.flags.underline, - strikethrough = span.flags.strikethrough, - obfuscated = span.flags.obfuscated - ) - } + val spans = + MinecraftFormattingParser + .resolveStyleSpans( + parsed = parsed, + baseColor = color, + baseFlags = baseFlags, + rangeStart = line.startIndex, + rangeEnd = line.endIndexExclusive, + ).map { span -> + RenderCommand.TextStyleSpan( + start = span.start, + end = span.end, + color = span.color, + bold = span.flags.bold, + italic = span.flags.italic, + underline = span.flags.underline, + strikethrough = span.flags.strikethrough, + obfuscated = span.flags.obfuscated, + ) + } out.add(drawTextCommand(ctx, line.text, baseX, lineY + lineTopLeading, color, spans)) lineY += layout.lineHeight } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextSource.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextSource.kt index 71fec66..5d4b3a5 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextSource.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextSource.kt @@ -3,11 +3,15 @@ package org.dreamfinity.dsgl.core.dom.elements sealed interface TextSource { fun resolve(): String - data class Static(private val value: String) : TextSource { + data class Static( + private val value: String, + ) : TextSource { override fun resolve(): String = value } - data class Dynamic(private val supplier: () -> String) : TextSource { + data class Dynamic( + private val supplier: () -> String, + ) : TextSource { override fun resolve(): String = supplier() } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ToggleNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ToggleNode.kt index 68493ed..60fc9ba 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ToggleNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ToggleNode.kt @@ -20,7 +20,7 @@ class ToggleNode( controlled: Boolean = false, checked: Boolean = false, defaultChecked: Boolean = false, - key: Any? = null + key: Any? = null, ) : DOMNode(key) { override val styleType: String = "toggle" override val focusable: Boolean = true @@ -87,9 +87,7 @@ class ToggleNode( } } - fun isChecked(): Boolean { - return if (controlled) controlledChecked else uncontrolledChecked - } + fun isChecked(): Boolean = if (controlled) controlledChecked else uncontrolledChecked internal fun syncFrom(template: ToggleNode) { val wasControlled = controlled @@ -111,13 +109,10 @@ class ToggleNode( markRenderCommandsDirty() } - internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size { - return measureWithConstraint(availableOuterWidth) - } + internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size = + measureWithConstraint(availableOuterWidth) - override fun measure(ctx: UiMeasureContext): Size { - return measureWithConstraint(null) - } + override fun measure(ctx: UiMeasureContext): Size = measureWithConstraint(null) private fun measureWithConstraint(availableOuterWidth: Int?): Size { val contentLimit = resolvedContentLimit(availableOuterWidth) @@ -140,7 +135,13 @@ class ToggleNode( } } - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) val tx = contentX() val ty = contentY() @@ -158,11 +159,12 @@ class ToggleNode( override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) { addBackgroundImageCommand(out) - val trackColor = when { - styleDisabled -> trackDisabledColor - isChecked() -> trackOnColor - else -> trackOffColor - } + val trackColor = + when { + styleDisabled -> trackDisabledColor + isChecked() -> trackOnColor + else -> trackOffColor + } out += RenderCommand.DrawRect(trackRect.x, trackRect.y, trackRect.width, trackRect.height, trackColor) addBorderCommands(out) val thumb = if (styleDisabled) thumbDisabledColor else thumbColor diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/KeyedStateStore.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/KeyedStateStore.kt index 35a9cbb..4c6d716 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/KeyedStateStore.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/KeyedStateStore.kt @@ -4,7 +4,7 @@ package org.dreamfinity.dsgl.core.dom.elements.support * Small keyed state store with bounded growth for node-persisted editor state. */ internal class KeyedStateStore( - private val maxEntries: Int = 1024 + private val maxEntries: Int = 1024, ) { private val states: LinkedHashMap = LinkedHashMap() diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/MeasuredTextRangeWidthSource.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/MeasuredTextRangeWidthSource.kt index 0fe8281..bbc39aa 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/MeasuredTextRangeWidthSource.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/MeasuredTextRangeWidthSource.kt @@ -12,12 +12,12 @@ internal class MeasuredTextRangeWidthSource( private val fontSizePx: Int, private val baseFlags: TextStyleFlags, private val spans: List, - private val ctx: UiMeasureContext + private val ctx: UiMeasureContext, ) { data class SpanWidthKey( val start: Int, val end: Int, - val flagsMask: Int + val flagsMask: Int, ) data class CacheKey( @@ -25,25 +25,27 @@ internal class MeasuredTextRangeWidthSource( val fontId: String?, val fontSizePx: Int, val baseFlagsMask: Int, - val spanWidthKey: List + val spanWidthKey: List, ) private val rangeWidthCache: MutableMap = HashMap() private val hasBoldContribution: Boolean = baseFlags.bold || spans.any { it.flags.bold } - val cacheKey: CacheKey = CacheKey( - backendFingerprint = backendFingerprint(ctx, fontId, fontSizePx), - fontId = fontId, - fontSizePx = fontSizePx, - baseFlagsMask = baseFlags.mask(), - spanWidthKey = spans.map { span -> - SpanWidthKey( - start = span.start, - end = span.end, - flagsMask = span.flags.mask() - ) - } - ) + val cacheKey: CacheKey = + CacheKey( + backendFingerprint = backendFingerprint(ctx, fontId, fontSizePx), + fontId = fontId, + fontSizePx = fontSizePx, + baseFlagsMask = baseFlags.mask(), + spanWidthKey = + spans.map { span -> + SpanWidthKey( + start = span.start, + end = span.end, + flagsMask = span.flags.mask(), + ) + }, + ) fun measureRange(startIndex: Int, endIndexExclusive: Int): Int { val safeStart = startIndex.coerceIn(0, plainText.length) @@ -51,31 +53,32 @@ internal class MeasuredTextRangeWidthSource( if (safeEnd <= safeStart) return 0 val key = packRange(safeStart, safeEnd) return rangeWidthCache.getOrPut(key) { - val baseWidth = ctx.measureTextRange( - text = plainText, - startIndex = safeStart, - endIndexExclusive = safeEnd, - fontId = fontId, - fontSize = fontSizePx - ) + val baseWidth = + ctx.measureTextRange( + text = plainText, + startIndex = safeStart, + endIndexExclusive = safeEnd, + fontId = fontId, + fontSize = fontSizePx, + ) if (!hasBoldContribution) { baseWidth } else { - val boldExtra = TextStyleMetrics.boldExtraPxForRangeInText( - plainText = plainText, - spans = spans, - baseFlags = baseFlags, - rangeStart = safeStart, - rangeEnd = safeEnd - ) + val boldExtra = + TextStyleMetrics.boldExtraPxForRangeInText( + plainText = plainText, + spans = spans, + baseFlags = baseFlags, + rangeStart = safeStart, + rangeEnd = safeEnd, + ) baseWidth + boldExtra } } } - private fun packRange(start: Int, endExclusive: Int): Long { - return (start.toLong() shl 32) or (endExclusive.toLong() and 0xFFFF_FFFFL) - } + private fun packRange(start: Int, endExclusive: Int): Long = + (start.toLong() shl 32) or (endExclusive.toLong() and 0xFFFF_FFFFL) private fun TextStyleFlags.mask(): Int { var mask = 0 diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/TextChangeTracker.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/TextChangeTracker.kt index 0d8a0a4..be38f37 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/TextChangeTracker.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/TextChangeTracker.kt @@ -4,7 +4,7 @@ package org.dreamfinity.dsgl.core.dom.elements.support * Tracks "dirty since focus" state and commit baseline for change events. */ internal class TextChangeTracker( - initialValue: String + initialValue: String, ) { private var valueAtFocusStart: String = initialValue private var dirtySinceFocus: Boolean = false diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/TextEditOps.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/TextEditOps.kt index deb00d0..b5b39b9 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/TextEditOps.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/TextEditOps.kt @@ -3,13 +3,9 @@ package org.dreamfinity.dsgl.core.dom.elements.support import org.dreamfinity.dsgl.core.dom.elements.TextEditState internal object TextEditOps { - fun isPrintable(ch: Char): Boolean { - return ch >= ' ' && ch.code != 127 - } + fun isPrintable(ch: Char): Boolean = ch >= ' ' && ch.code != 127 - fun dragIdentity(key: Any?, node: Any): Any { - return key ?: node - } + fun dragIdentity(key: Any?, node: Any): Any = key ?: node fun selectedText(source: String, editState: TextEditState): String { if (!editState.hasSelection()) return "" @@ -36,7 +32,12 @@ internal object TextEditOps { return start to end } - fun moveCaretWithSelection(editState: TextEditState, next: Int, textLength: Int, extend: Boolean) { + fun moveCaretWithSelection( + editState: TextEditState, + next: Int, + textLength: Int, + extend: Boolean, + ) { if (extend) { if (editState.selectionAnchor == null) { editState.selectionAnchor = editState.caretIndex @@ -50,9 +51,14 @@ internal object TextEditOps { } } - fun replaceRange(source: String, start: Int, end: Int, insert: String): String { + fun replaceRange( + source: String, + start: Int, + end: Int, + insert: String, + ): String { val safeStart = start.coerceIn(0, source.length) val safeEnd = end.coerceIn(safeStart, source.length) return source.substring(0, safeStart) + insert + source.substring(safeEnd) } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/TextEditShortcutDispatcher.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/TextEditShortcutDispatcher.kt index 0258c97..eb7815b 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/TextEditShortcutDispatcher.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/TextEditShortcutDispatcher.kt @@ -11,7 +11,7 @@ internal enum class TextShortcutAction { CUT, PASTE, UNDO, - REDO + REDO, } internal data class TextShortcutCallbacks( @@ -27,7 +27,7 @@ internal data class TextShortcutCallbacks( val undo: () -> Unit, val redo: () -> Unit, val beforeHandled: () -> Unit, - val afterHandled: (TextShortcutAction) -> Unit + val afterHandled: (TextShortcutAction) -> Unit, ) internal object TextEditShortcutDispatcher { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/TextLayoutEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/TextLayoutEngine.kt index 5df5b71..83e2250 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/TextLayoutEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/TextLayoutEngine.kt @@ -8,7 +8,7 @@ object TextLayoutEngine { val text: String, val startIndex: Int, val endIndexExclusive: Int, - val width: Int + val width: Int, ) { val length: Int get() = endIndexExclusive - startIndex @@ -18,7 +18,7 @@ object TextLayoutEngine { val lines: List, val maxLineWidth: Int, val totalHeight: Int, - val lineHeight: Int + val lineHeight: Int, ) { fun lineForCaret(caretIndex: Int): Int { if (lines.isEmpty()) return 0 @@ -38,7 +38,7 @@ object TextLayoutEngine { val fontHeight: Int, val fontFingerprint: Int, val usesRangeMeasurement: Boolean, - val rangeMeasureCacheKey: Any? + val rangeMeasureCacheKey: Any?, ) data class HotPathStats( @@ -54,15 +54,15 @@ object TextLayoutEngine { val temporarySegmentSubstringCalls: Long, val probeMeasureSubstringCalls: Long, val finalLineTextSubstringCalls: Long, - val substringSliceCalls: Long + val substringSliceCalls: Long, ) private const val MAX_CACHE_SIZE: Int = 512 - private val cache: MutableMap = object : LinkedHashMap(128, 0.75f, true) { - override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean { - return size > MAX_CACHE_SIZE + private val cache: MutableMap = + object : LinkedHashMap(128, 0.75f, true) { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean = + size > MAX_CACHE_SIZE } - } private val layoutCalls = AtomicLong(0L) private val cacheHits = AtomicLong(0L) private val cacheMisses = AtomicLong(0L) @@ -90,7 +90,7 @@ object TextLayoutEngine { fontHeight: Int, measureText: (String) -> Int, measureRange: ((startIndex: Int, endIndexExclusive: Int) -> Int)? = null, - measureRangeCacheKey: Any? = null + measureRangeCacheKey: Any? = null, ): Layout { layoutCalls.incrementAndGet() val resolvedHeight = fontHeight.coerceAtLeast(1) @@ -104,23 +104,25 @@ object TextLayoutEngine { lines = lines, maxLineWidth = maxLineWidth, totalHeight = totalHeight, - lineHeight = resolvedHeight + lineHeight = resolvedHeight, ) } - val fingerprint = if (measureRange != null) { - measureRangeCacheKey.hashCode() - } else { - measureText("M") * 31 + measureText("i") - } - val key = CacheKey( - text = text, - maxWidth = constrainedWidth, - wrap = wrap, - fontHeight = resolvedHeight, - fontFingerprint = fingerprint, - usesRangeMeasurement = measureRange != null, - rangeMeasureCacheKey = measureRangeCacheKey - ) + val fingerprint = + if (measureRange != null) { + measureRangeCacheKey.hashCode() + } else { + measureText("M") * 31 + measureText("i") + } + val key = + CacheKey( + text = text, + maxWidth = constrainedWidth, + wrap = wrap, + fontHeight = resolvedHeight, + fontFingerprint = fingerprint, + usesRangeMeasurement = measureRange != null, + rangeMeasureCacheKey = measureRangeCacheKey, + ) synchronized(cache) { cache[key]?.let { cacheHits.incrementAndGet() @@ -141,8 +143,8 @@ object TextLayoutEngine { return result } - fun hotPathStats(): HotPathStats { - return HotPathStats( + fun hotPathStats(): HotPathStats = + HotPathStats( layoutCalls = layoutCalls.get(), cacheHits = cacheHits.get(), cacheMisses = cacheMisses.get(), @@ -155,9 +157,8 @@ object TextLayoutEngine { temporarySegmentSubstringCalls = temporarySegmentSubstringCalls.get(), probeMeasureSubstringCalls = probeMeasureSubstringCalls.get(), finalLineTextSubstringCalls = finalLineTextSubstringCalls.get(), - substringSliceCalls = substringSliceCalls.get() + substringSliceCalls = substringSliceCalls.get(), ) - } fun resetHotPathStats() { layoutCalls.set(0L) @@ -180,7 +181,7 @@ object TextLayoutEngine { maxWidth: Int?, wrap: TextWrap, measureText: (String) -> Int, - measureRange: ((startIndex: Int, endIndexExclusive: Int) -> Int)? + measureRange: ((startIndex: Int, endIndexExclusive: Int) -> Int)?, ): List { buildLinesCalls.incrementAndGet() if (text.isEmpty()) { @@ -198,13 +199,14 @@ object TextLayoutEngine { text = lineText, startIndex = logicalStart, endIndexExclusive = newlineIndex, - width = measureWidth( - measureRange = measureRange, - startIndex = logicalStart, - endIndexExclusive = newlineIndex, - fallbackMeasure = { measureText(lineText) } - ) - ) + width = + measureWidth( + measureRange = measureRange, + startIndex = logicalStart, + endIndexExclusive = newlineIndex, + fallbackMeasure = { measureText(lineText) }, + ), + ), ) } else { appendWrappedSegment( @@ -214,7 +216,7 @@ object TextLayoutEngine { segmentEndExclusive = newlineIndex, maxWidth = maxWidth, measureText = measureText, - measureRange = measureRange + measureRange = measureRange, ) } @@ -242,7 +244,7 @@ object TextLayoutEngine { segmentEndExclusive: Int, maxWidth: Int, measureText: (String) -> Int, - measureRange: ((startIndex: Int, endIndexExclusive: Int) -> Int)? + measureRange: ((startIndex: Int, endIndexExclusive: Int) -> Int)?, ) { appendWrappedSegmentCalls.incrementAndGet() if (segmentStart >= segmentEndExclusive) { @@ -252,14 +254,15 @@ object TextLayoutEngine { var lineStart = segmentStart while (lineStart < segmentEndExclusive) { - var lineEndExclusive = findMaxFittingEnd( - text = text, - start = lineStart, - segmentEndExclusive = segmentEndExclusive, - maxWidth = maxWidth, - measureText = measureText, - measureRange = measureRange - ) + var lineEndExclusive = + findMaxFittingEnd( + text = text, + start = lineStart, + segmentEndExclusive = segmentEndExclusive, + maxWidth = maxWidth, + measureText = measureText, + measureRange = measureRange, + ) if (lineEndExclusive <= lineStart) { lineEndExclusive = (lineStart + 1).coerceAtMost(segmentEndExclusive) } else if (lineEndExclusive < segmentEndExclusive) { @@ -275,13 +278,14 @@ object TextLayoutEngine { text = lineText, startIndex = lineStart, endIndexExclusive = lineEndExclusive, - width = measureWidth( - measureRange = measureRange, - startIndex = lineStart, - endIndexExclusive = lineEndExclusive, - fallbackMeasure = { measureText(lineText) } - ) - ) + width = + measureWidth( + measureRange = measureRange, + startIndex = lineStart, + endIndexExclusive = lineEndExclusive, + fallbackMeasure = { measureText(lineText) }, + ), + ), ) lineStart = lineEndExclusive } @@ -293,7 +297,7 @@ object TextLayoutEngine { segmentEndExclusive: Int, maxWidth: Int, measureText: (String) -> Int, - measureRange: ((startIndex: Int, endIndexExclusive: Int) -> Int)? + measureRange: ((startIndex: Int, endIndexExclusive: Int) -> Int)?, ): Int { findMaxFittingCalls.incrementAndGet() var low = (start + 1).coerceAtMost(segmentEndExclusive) @@ -301,16 +305,17 @@ object TextLayoutEngine { var best = start while (low <= high) { val mid = (low + high) ushr 1 - val width = measureWidth( - measureRange = measureRange, - startIndex = start, - endIndexExclusive = mid, - fallbackMeasure = { - probeMeasureSubstringCalls.incrementAndGet() - substringSliceCalls.incrementAndGet() - measureText(text.substring(start, mid)) - } - ) + val width = + measureWidth( + measureRange = measureRange, + startIndex = start, + endIndexExclusive = mid, + fallbackMeasure = { + probeMeasureSubstringCalls.incrementAndGet() + substringSliceCalls.incrementAndGet() + measureText(text.substring(start, mid)) + }, + ) if (width <= maxWidth) { best = mid low = mid + 1 @@ -325,16 +330,15 @@ object TextLayoutEngine { measureRange: ((startIndex: Int, endIndexExclusive: Int) -> Int)?, startIndex: Int, endIndexExclusive: Int, - fallbackMeasure: () -> Int - ): Int { - return if (measureRange != null) { + fallbackMeasure: () -> Int, + ): Int = + if (measureRange != null) { rangeMeasureCalls.incrementAndGet() measureRange.invoke(startIndex, endIndexExclusive) } else { plainMeasureCalls.incrementAndGet() fallbackMeasure() } - } private fun lastWhitespaceBreak(text: String, start: Int, endExclusive: Int): Int? { var index = endExclusive diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/UndoRedoHistory.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/UndoRedoHistory.kt index 98cf8e9..5ed2c56 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/UndoRedoHistory.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/UndoRedoHistory.kt @@ -1,12 +1,12 @@ package org.dreamfinity.dsgl.core.dom.elements.support -import java.util.* +import java.util.ArrayDeque /** * Undo/redo stack pair with a fixed snapshot capacity. */ internal class UndoRedoHistory( - private val limit: Int + private val limit: Int, ) { private val undoStack: ArrayDeque = ArrayDeque() private val redoStack: ArrayDeque = ArrayDeque() diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/layout/AffineTransform2D.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/layout/AffineTransform2D.kt index defaf27..a671020 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/layout/AffineTransform2D.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/layout/AffineTransform2D.kt @@ -14,25 +14,23 @@ data class AffineTransform2D( val c: Float, val d: Float, val tx: Float, - val ty: Float + val ty: Float, ) { - fun transform(x: Float, y: Float): Pair { - return Pair( + fun transform(x: Float, y: Float): Pair = + Pair( a * x + c * y + tx, - b * x + d * y + ty + b * x + d * y + ty, ) - } - fun times(other: AffineTransform2D): AffineTransform2D { - return AffineTransform2D( + fun times(other: AffineTransform2D): AffineTransform2D = + AffineTransform2D( a = a * other.a + c * other.b, b = b * other.a + d * other.b, c = a * other.c + c * other.d, d = b * other.c + d * other.d, tx = a * other.tx + c * other.ty + tx, - ty = b * other.tx + d * other.ty + ty + ty = b * other.tx + d * other.ty + ty, ) - } fun inverseOrNull(): AffineTransform2D? { val det = a * d - b * c @@ -48,22 +46,19 @@ data class AffineTransform2D( } companion object { - val IDENTITY: AffineTransform2D = AffineTransform2D( - a = 1f, - b = 0f, - c = 0f, - d = 1f, - tx = 0f, - ty = 0f - ) + val IDENTITY: AffineTransform2D = + AffineTransform2D( + a = 1f, + b = 0f, + c = 0f, + d = 1f, + tx = 0f, + ty = 0f, + ) - fun translation(x: Float, y: Float): AffineTransform2D { - return AffineTransform2D(1f, 0f, 0f, 1f, x, y) - } + fun translation(x: Float, y: Float): AffineTransform2D = AffineTransform2D(1f, 0f, 0f, 1f, x, y) - fun scale(x: Float, y: Float): AffineTransform2D { - return AffineTransform2D(x, 0f, 0f, y, 0f, 0f) - } + fun scale(x: Float, y: Float): AffineTransform2D = AffineTransform2D(x, 0f, 0f, y, 0f, 0f) fun rotation(deg: Float): AffineTransform2D { val rad = Math.toRadians(deg.toDouble()) @@ -73,4 +68,3 @@ data class AffineTransform2D( } } } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/layout/Border.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/layout/Border.kt index fe74fc1..3a3b602 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/layout/Border.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/layout/Border.kt @@ -8,7 +8,7 @@ data class Border( val right: Int, val bottom: Int, val left: Int, - val color: Int + val color: Int, ) { /** Combined left/right thickness. */ val horizontal: Int @@ -23,11 +23,10 @@ data class Border( val NONE: Border = Border(0, 0, 0, 0, 0) /** Same border on all sides. */ - fun all(width: Int, color: Int): Border = - Border(width, width, width, width, color) + fun all(width: Int, color: Int): Border = Border(width, width, width, width, color) /** Border with separate horizontal and vertical thicknesses. */ fun horizontalVertical(horizontal: Int, vertical: Int, color: Int): Border = Border(vertical, horizontal, vertical, horizontal, color) } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/layout/Insets.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/layout/Insets.kt index f78f0e0..28f9c4c 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/layout/Insets.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/layout/Insets.kt @@ -7,7 +7,7 @@ data class Insets( val top: Int, val right: Int, val bottom: Int, - val left: Int + val left: Int, ) { /** Combined left/right value. */ val horizontal: Int @@ -28,4 +28,4 @@ data class Insets( fun horizontalVertical(horizontal: Int, vertical: Int): Insets = Insets(vertical, horizontal, vertical, horizontal) } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/layout/Size.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/layout/Size.kt index 686d431..7083d66 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/layout/Size.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/layout/Size.kt @@ -1,20 +1,25 @@ package org.dreamfinity.dsgl.core.dom.layout /** Size in pixels. */ -data class Size(val width: Int, val height: Int) +data class Size( + val width: Int, + val height: Int, +) /** Rectangle bounds in pixels. */ -data class Rect(val x: Int, val y: Int, val width: Int, val height: Int) { - fun contains(px: Int, py: Int): Boolean { - return px >= x && py >= y && px < x + width && py < y + height - } +data class Rect( + val x: Int, + val y: Int, + val width: Int, + val height: Int, +) { + fun contains(px: Int, py: Int): Boolean = px >= x && py >= y && px < x + width && py < y + height - fun contains(px: Float, py: Float): Boolean { - return px >= x.toFloat() && - py >= y.toFloat() && - px < (x + width).toFloat() && - py < (y + height).toFloat() - } + fun contains(px: Float, py: Float): Boolean = + px >= x.toFloat() && + py >= y.toFloat() && + px < (x + width).toFloat() && + py < (y + height).toFloat() fun intersection(other: Rect): Rect? { val left = maxOf(x, other.x) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/layout/UiMeasureContext.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/layout/UiMeasureContext.kt index 118f874..e92bdc2 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/layout/UiMeasureContext.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/layout/UiMeasureContext.kt @@ -6,7 +6,7 @@ data class FontLineMetrics( val emSize: Float, val lineHeightEm: Float, val ascenderEm: Float, - val descenderEm: Float + val descenderEm: Float, ) /** @@ -39,7 +39,7 @@ interface UiMeasureContext { startIndex: Int, endIndexExclusive: Int, fontId: String?, - fontSize: Int? + fontSize: Int?, ): Int { val safeStart = startIndex.coerceIn(0, text.length) val safeEnd = endIndexExclusive.coerceIn(safeStart, text.length) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/DomReconciler.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/DomReconciler.kt index c129038..0e3ecc1 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/DomReconciler.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/DomReconciler.kt @@ -9,12 +9,12 @@ data class DomReconcileResult( val reusedNodes: Int, val insertedNodes: Int, val removedNodes: Int, - val replacedSubtrees: Int + val replacedSubtrees: Int, ) private data class ChildIdentity( val key: Any, - val type: Class + val type: Class, ) private class Counters { @@ -28,19 +28,20 @@ object DomReconciler { fun reconcile(currentRoot: DOMNode, nextRoot: DOMNode): DomReconcileResult { val detached = ArrayList(8) val counters = Counters() - val mergedRoot = reconcileNodeOrReplace( - current = currentRoot, - template = nextRoot, - detached = detached, - counters = counters - ) + val mergedRoot = + reconcileNodeOrReplace( + current = currentRoot, + template = nextRoot, + detached = detached, + counters = counters, + ) return DomReconcileResult( root = mergedRoot, detachedRoots = detached, reusedNodes = counters.reused, insertedNodes = counters.inserted, removedNodes = counters.removed, - replacedSubtrees = counters.replaced + replacedSubtrees = counters.replaced, ) } @@ -48,7 +49,7 @@ object DomReconciler { current: DOMNode, template: DOMNode, detached: MutableList, - counters: Counters + counters: Counters, ): DOMNode { if (!canReuse(current, template)) { counters.replaced += 1 @@ -68,7 +69,7 @@ object DomReconciler { current: DOMNode, template: DOMNode, detached: MutableList, - counters: Counters + counters: Counters, ) { if (current.children.isEmpty() && template.children.isEmpty()) return @@ -92,12 +93,13 @@ object DomReconciler { counters.inserted += countSubtree(templateChild) } else { consumed[matchIndex] = true - val merged = reconcileNodeOrReplace( - current = oldChildren[matchIndex], - template = templateChild, - detached = detached, - counters = counters - ) + val merged = + reconcileNodeOrReplace( + current = oldChildren[matchIndex], + template = templateChild, + detached = detached, + counters = counters, + ) merged.parent = current reconciledChildren.add(merged) } @@ -119,7 +121,7 @@ object DomReconciler { template: DOMNode, oldChildren: List, consumed: BooleanArray, - keyed: Map> + keyed: Map>, ): Int? { val templateKey = template.key if (templateKey != null) { @@ -158,12 +160,11 @@ object DomReconciler { return !isReplaceOnlyType(current) } - private fun isReplaceOnlyType(node: DOMNode): Boolean { - return node is CheckboxGroupNode || - node is NumberInputNode || - node is DateInputNode || - node is RangeInputNode - } + private fun isReplaceOnlyType(node: DOMNode): Boolean = + node is CheckboxGroupNode || + node is NumberInputNode || + node is DateInputNode || + node is RangeInputNode private fun syncNode(current: DOMNode, template: DOMNode) { current.syncBaseFrom(template) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/text/ResolvedTextMetrics.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/text/ResolvedTextMetrics.kt index 19f43e0..2a0bbc6 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/text/ResolvedTextMetrics.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/text/ResolvedTextMetrics.kt @@ -13,5 +13,5 @@ data class ResolvedTextMetrics( val ascenderPx: Float, val descenderPx: Float, val topLeadingPx: Float, - val bottomLeadingPx: Float + val bottomLeadingPx: Float, ) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ButtonDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ButtonDsl.kt index b9b2552..8cd1a97 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ButtonDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ButtonDsl.kt @@ -6,13 +6,17 @@ import org.dreamfinity.dsgl.core.hooks.ref.ElementHandle import org.dreamfinity.dsgl.core.hooks.ref.RefTarget /** Button node props. */ -open class ButtonProps(var text: String) : TextProps() +open class ButtonProps( + var text: String, +) : TextProps() /** * Button-specific DSL scope. */ @DsglDsl -class ButtonScope internal constructor(private val node: ButtonNode) { +class ButtonScope internal constructor( + private val node: ButtonNode, +) { fun onClick(handler: (MouseClickEvent) -> Unit) { node.onClick(handler) } @@ -24,11 +28,11 @@ fun UiScope.button( text: String, props: ButtonProps.() -> Unit = {}, ref: RefTarget? = null, - block: ButtonScope.() -> Unit = {} + block: ButtonScope.() -> Unit = {}, ) = withProps(ButtonProps(text).apply(props)) { props -> ButtonNode( props.text, - key = props.key + key = props.key, ).apply { applyStyle(this, props.style) applyHandlers(this, props) @@ -36,4 +40,4 @@ fun UiScope.button( add(this) ButtonScope(this).block() } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorPickerDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorPickerDsl.kt index eea7f7b..6585e99 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorPickerDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorPickerDsl.kt @@ -36,67 +36,63 @@ open class ColorPickerProps : ComponentProps() { } internal fun hasControlledValue(): Boolean = valueSpecified + internal fun controlledValue(): RgbaColor? = valueInternal private var valueSpecified: Boolean = false private var valueInternal: RgbaColor? = null } - @DsglDsl -fun UiScope.colorPicker( - props: ColorPickerProps.() -> Unit = {}, - ref: RefTarget? = null -) = withProps(ColorPickerProps().apply(props)) { props -> - val controlled = props.hasControlledValue() - ColorPickerInlineNode( - controlled = controlled, - value = if (controlled) props.controlledValue() else null, - defaultValue = props.defaultValue, - previousValue = props.previousValue, - mode = props.mode, - alphaEnabled = props.alphaEnabled, - key = props.key - ).apply { - closeOnSelect = props.closeOnSelect - eyedropperEnabled = props.eyedropperEnabled - onPreviewColor = props.onPreviewColor - onChangeColor = props.onChangeColor - onCommitColor = props.onCommitColor - onRequestClose = props.onRequestClose - applyStyle(this, props.style) - applyHandlers(this, props) - applyRef(this, ref) - add(this) +fun UiScope.colorPicker(props: ColorPickerProps.() -> Unit = {}, ref: RefTarget? = null) = + withProps(ColorPickerProps().apply(props)) { props -> + val controlled = props.hasControlledValue() + ColorPickerInlineNode( + controlled = controlled, + value = if (controlled) props.controlledValue() else null, + defaultValue = props.defaultValue, + previousValue = props.previousValue, + mode = props.mode, + alphaEnabled = props.alphaEnabled, + key = props.key, + ).apply { + closeOnSelect = props.closeOnSelect + eyedropperEnabled = props.eyedropperEnabled + onPreviewColor = props.onPreviewColor + onChangeColor = props.onChangeColor + onCommitColor = props.onCommitColor + onRequestClose = props.onRequestClose + applyStyle(this, props.style) + applyHandlers(this, props) + applyRef(this, ref) + add(this) + } } -} @DsglDsl -fun UiScope.colorPickerPopup( - props: ColorPickerPopupProps.() -> Unit = {}, - ref: RefTarget? = null -) = withProps(ColorPickerPopupProps().apply(props)) { props -> - val controlled = props.hasControlledValue() - ColorPickerPopupPaneNode( - controlled = controlled, - value = if (controlled) props.controlledValue() else null, - defaultValue = props.defaultValue, - previousValue = props.previousValue, - mode = props.mode, - alphaEnabled = props.alphaEnabled, - key = props.key - ).apply { - closeOnSelect = props.closeOnSelect - popupTitle = props.popupTitle - popupWidth = props.popupWidth - popupDraggable = props.popupDraggable - popupCloseOnOutsideClick = props.popupCloseOnOutsideClick - onPreviewColor = props.onPreviewColor - onChangeColor = props.onChangeColor - onCommitColor = props.onCommitColor - applyStyle(this, props.style) - applyHandlers(this, props) - applyRef(this, ref) - add(this) +fun UiScope.colorPickerPopup(props: ColorPickerPopupProps.() -> Unit = {}, ref: RefTarget? = null) = + withProps(ColorPickerPopupProps().apply(props)) { props -> + val controlled = props.hasControlledValue() + ColorPickerPopupPaneNode( + controlled = controlled, + value = if (controlled) props.controlledValue() else null, + defaultValue = props.defaultValue, + previousValue = props.previousValue, + mode = props.mode, + alphaEnabled = props.alphaEnabled, + key = props.key, + ).apply { + closeOnSelect = props.closeOnSelect + popupTitle = props.popupTitle + popupWidth = props.popupWidth + popupDraggable = props.popupDraggable + popupCloseOnOutsideClick = props.popupCloseOnOutsideClick + onPreviewColor = props.onPreviewColor + onChangeColor = props.onChangeColor + onCommitColor = props.onCommitColor + applyStyle(this, props.style) + applyHandlers(this, props) + applyRef(this, ref) + add(this) + } } -} \ No newline at end of file diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt index 37f287c..59baf91 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ColorSurfaceDsl.kt @@ -42,84 +42,76 @@ internal open class EyedropperMagnifierProps : ComponentProps() { } @DsglDsl -internal fun UiScope.colorSwatch( - props: ColorSwatchProps.() -> Unit = {}, - ref: RefTarget? = null -) = withProps(ColorSwatchProps().apply(props)) { props -> - ColorSwatchSurfaceNode( - allowEmpty = props.allowEmpty, - key = props.key - ).apply { - bind(style = props.palette, color = props.color, highlighted = props.highlighted) - applyStyle(this, props.style) - applyHandlers(this, props) - applyRef(this, ref) - add(this) +internal fun UiScope.colorSwatch(props: ColorSwatchProps.() -> Unit = {}, ref: RefTarget? = null) = + withProps(ColorSwatchProps().apply(props)) { props -> + ColorSwatchSurfaceNode( + allowEmpty = props.allowEmpty, + key = props.key, + ).apply { + bind(style = props.palette, color = props.color, highlighted = props.highlighted) + applyStyle(this, props.style) + applyHandlers(this, props) + applyRef(this, ref) + add(this) + } } -} @DsglDsl -internal fun UiScope.hueSlider( - props: HueSliderProps.() -> Unit = {}, - ref: RefTarget? = null -) = withProps(HueSliderProps().apply(props)) { props -> - HueSurfaceNode( - key = props.key - ).apply { - bind(style = props.palette, hueDeg = props.hueDeg) - applyStyle(this, props.style) - applyHandlers(this, props) - applyRef(this, ref) - add(this) +internal fun UiScope.hueSlider(props: HueSliderProps.() -> Unit = {}, ref: RefTarget? = null) = + withProps(HueSliderProps().apply(props)) { props -> + HueSurfaceNode( + key = props.key, + ).apply { + bind(style = props.palette, hueDeg = props.hueDeg) + applyStyle(this, props.style) + applyHandlers(this, props) + applyRef(this, ref) + add(this) + } } -} @DsglDsl -internal fun UiScope.alphaSlider( - props: AlphaSliderProps.() -> Unit = {}, - ref: RefTarget? = null -) = withProps(AlphaSliderProps().apply(props)) { props -> - AlphaSurfaceNode( - key = props.key - ).apply { - bind(style = props.palette, color = props.color) - applyStyle(this, props.style) - applyHandlers(this, props) - applyRef(this, ref) - add(this) +internal fun UiScope.alphaSlider(props: AlphaSliderProps.() -> Unit = {}, ref: RefTarget? = null) = + withProps(AlphaSliderProps().apply(props)) { props -> + AlphaSurfaceNode( + key = props.key, + ).apply { + bind(style = props.palette, color = props.color) + applyStyle(this, props.style) + applyHandlers(this, props) + applyRef(this, ref) + add(this) + } } -} @DsglDsl -internal fun UiScope.colorField( - props: ColorFieldProps.() -> Unit = {}, - ref: RefTarget? = null -) = withProps(ColorFieldProps().apply(props)) { props -> - ColorFieldSurfaceNode( - key = props.key - ).apply { - bind(style = props.palette, color = props.color, hueDeg = props.hueDeg) - applyStyle(this, props.style) - applyHandlers(this, props) - applyRef(this, ref) - add(this) +internal fun UiScope.colorField(props: ColorFieldProps.() -> Unit = {}, ref: RefTarget? = null) = + withProps(ColorFieldProps().apply(props)) { props -> + ColorFieldSurfaceNode( + key = props.key, + ).apply { + bind(style = props.palette, color = props.color, hueDeg = props.hueDeg) + applyStyle(this, props.style) + applyHandlers(this, props) + applyRef(this, ref) + add(this) + } } -} @DsglDsl internal fun UiScope.eyedropperMagnifier( props: EyedropperMagnifierProps.() -> Unit = {}, - ref: RefTarget? = null + ref: RefTarget? = null, ) = withProps(EyedropperMagnifierProps().apply(props)) { props -> EyedropperMagnifierDrawNode( - key = props.key + key = props.key, ).apply { bind( columns = props.sourceColumns, rows = props.sourceRows, magnification = props.magnification, gridEnabled = props.showGrid, - gridColor = props.gridColor + gridColor = props.gridColor, ) applyStyle(this, props.style) applyHandlers(this, props) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ComponentProps.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ComponentProps.kt index 9ce9e9f..f32a200 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ComponentProps.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ComponentProps.kt @@ -70,7 +70,7 @@ open class ComponentProps( var onDragEnter: ((DragEnterEvent) -> Unit)? = null, var onDragOver: ((DragOverEvent) -> Unit)? = null, var onDragLeave: ((DragLeaveEvent) -> Unit)? = null, - var onDrop: ((DropEvent) -> Unit)? = null + var onDrop: ((DropEvent) -> Unit)? = null, ) { fun style(block: StyleScope.() -> Unit) { style = block @@ -89,4 +89,4 @@ open class ComponentProps( // } // return this // } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ContainerDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ContainerDsl.kt index 38aa2b6..e0f40d9 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ContainerDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ContainerDsl.kt @@ -8,11 +8,11 @@ import org.dreamfinity.dsgl.core.hooks.ref.RefTarget fun UiScope.div( props: ComponentProps.() -> Unit, ref: RefTarget? = null, - block: UiScope.() -> Unit = {} + block: UiScope.() -> Unit = {}, ) = withProps(ComponentProps().apply(props)) { props -> ContainerNode( stackLayout = false, - key = props.key + key = props.key, ).apply { applyStyle(this, props.style) applyHandlers(this, props) @@ -26,11 +26,11 @@ fun UiScope.div( fun UiScope.overlay( props: ComponentProps.() -> Unit, ref: RefTarget? = null, - block: UiScope.() -> Unit = {} + block: UiScope.() -> Unit = {}, ) = withProps(ComponentProps().apply(props)) { props -> ContainerNode( stackLayout = true, - key = props.key + key = props.key, ).apply { applyStyle(this, props.style) applyHandlers(this, props) @@ -38,4 +38,4 @@ fun UiScope.overlay( add(this) childScope(this).block() } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/DsglDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/DsglDsl.kt index 417698a..71bc5c0 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/DsglDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/DsglDsl.kt @@ -4,4 +4,4 @@ package org.dreamfinity.dsgl.core.dsl * Marks the DSGL UI DSL to keep nested scopes safe. */ @DslMarker -annotation class DsglDsl \ No newline at end of file +annotation class DsglDsl diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/InputDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/InputDsl.kt index 2b8e807..3a6185f 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/InputDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/InputDsl.kt @@ -10,7 +10,9 @@ import java.time.Instant import java.time.ZoneId /** Multiline text area props. */ -open class TextAreaProps(var placeholder: String = "") : TextProps() +open class TextAreaProps( + var placeholder: String = "", +) : TextProps() /** Select input props. */ open class SelectProps : ComponentProps() { @@ -26,6 +28,7 @@ open class SelectProps : ComponentProps() { } internal fun hasControlledValue(): Boolean = valueSpecified + internal fun controlledValue(): String? = valueInternal private var valueSpecified: Boolean = false @@ -33,7 +36,9 @@ open class SelectProps : ComponentProps() { } /** Input node props, driven by [org.dreamfinity.dsgl.core.dom.elements.InputType]. */ -open class InputProps(val type: InputType) : TextProps() +open class InputProps( + val type: InputType, +) : TextProps() /** Toggle/switch input props. */ open class ToggleProps : ComponentProps() { @@ -55,105 +60,107 @@ open class ToggleProps : ComponentProps() { } internal fun hasControlledChecked(): Boolean = checkedSpecified + internal fun controlledChecked(): Boolean = checkedInternal private var checkedSpecified: Boolean = false private var checkedInternal: Boolean = false } - /** Input node backed by an [org.dreamfinity.dsgl.core.dom.elements.InputType]. */ @DsglDsl -fun UiScope.input( - type: InputType, - props: InputProps.() -> Unit = {}, - ref: RefTarget? = null, -) = withProps(InputProps(type).apply(props)) { props -> - when (props.type) { - is InputType.Text -> TextInputNode( - text = props.type.value, - placeholder = props.type.placeholder, - allowedChars = props.type.allowedChars, - minLength = props.type.minLength, - maxLength = props.type.maxLength, - key = props.key - ) - - is InputType.Password -> PasswordInputNode( - text = props.type.value, - placeholder = props.type.placeholder, - minLength = props.type.minLength, - maxLength = props.type.maxLength, - key = props.key - ) - - is InputType.Number -> NumberInputNode( - value = props.type.value, - placeholder = props.type.placeholder, - min = props.type.min, - max = props.type.max, - key = props.key - ) - - is InputType.Range -> RangeInputNode( - value = props.type.value, - min = props.type.min, - max = props.type.max, - step = props.type.step, - key = props.key - ) - - is InputType.Checkbox -> CheckboxGroupNode( - variants = props.type.variants, - selected = props.type.selected, - minSelected = props.type.minSelected, - maxSelected = props.type.maxSelected, - key = props.key - ) - - is InputType.Radio -> RadioGroupNode( - variants = props.type.variants, - selectedId = props.type.selected, - key = props.key - ) - - is InputType.Date -> DateInputNode( - value = props.type.value ?: Instant.now(), - zoneId = props.type.zoneId ?: ZoneId.systemDefault(), - placeholder = props.type.placeholder, - key = props.key - ) - }.apply { - applyStyle(this, props.style) - applyHandlers(this, props) - applyRef(this, ref) - add(this) +fun UiScope.input(type: InputType, props: InputProps.() -> Unit = {}, ref: RefTarget? = null) = + withProps(InputProps(type).apply(props)) { props -> + when (props.type) { + is InputType.Text -> + TextInputNode( + text = props.type.value, + placeholder = props.type.placeholder, + allowedChars = props.type.allowedChars, + minLength = props.type.minLength, + maxLength = props.type.maxLength, + key = props.key, + ) + + is InputType.Password -> + PasswordInputNode( + text = props.type.value, + placeholder = props.type.placeholder, + minLength = props.type.minLength, + maxLength = props.type.maxLength, + key = props.key, + ) + + is InputType.Number -> + NumberInputNode( + value = props.type.value, + placeholder = props.type.placeholder, + min = props.type.min, + max = props.type.max, + key = props.key, + ) + + is InputType.Range -> + RangeInputNode( + value = props.type.value, + min = props.type.min, + max = props.type.max, + step = props.type.step, + key = props.key, + ) + + is InputType.Checkbox -> + CheckboxGroupNode( + variants = props.type.variants, + selected = props.type.selected, + minSelected = props.type.minSelected, + maxSelected = props.type.maxSelected, + key = props.key, + ) + + is InputType.Radio -> + RadioGroupNode( + variants = props.type.variants, + selectedId = props.type.selected, + key = props.key, + ) + + is InputType.Date -> + DateInputNode( + value = props.type.value ?: Instant.now(), + zoneId = props.type.zoneId ?: ZoneId.systemDefault(), + placeholder = props.type.placeholder, + key = props.key, + ) + }.apply { + applyStyle(this, props.style) + applyHandlers(this, props) + applyRef(this, ref) + add(this) + } } -} /** Multiline text input area. */ @DsglDsl -fun UiScope.textarea( - props: TextAreaProps.() -> Unit = {}, - ref: RefTarget? = null -) = withProps(TextAreaProps().apply(props)) { props -> - TextAreaNode( - props.value, - props.placeholder, - props.key - ).apply { - applyStyle(this, props.style) - applyHandlers(this, props) - applyRef(this, ref) - add(this) +fun UiScope.textarea(props: TextAreaProps.() -> Unit = {}, ref: RefTarget? = null) = + withProps(TextAreaProps().apply(props)) { props -> + TextAreaNode( + props.value, + props.placeholder, + props.key, + ).apply { + applyStyle(this, props.style) + applyHandlers(this, props) + applyRef(this, ref) + add(this) + } } -} @DsglDsl fun UiScope.select( props: SelectProps.() -> Unit = {}, ref: RefTarget? = null, - block: SelectModelBuilder.() -> Unit + block: SelectModelBuilder.() -> Unit, ) = withProps(SelectProps().apply(props)) { props -> val model = selectModel(block = block) val controlled = props.hasControlledValue() @@ -164,7 +171,7 @@ fun UiScope.select( defaultValue = props.defaultValue, closeOnSelect = props.closeOnSelect, ownerScope = props.ownerScope, - key = props.key + key = props.key, ).apply { applyStyle(this, props.style) applyHandlers(this, props) @@ -174,28 +181,26 @@ fun UiScope.select( } @DsglDsl -fun UiScope.toggle( - props: ToggleProps.() -> Unit = {}, - ref: RefTarget? = null -) = withProps(ToggleProps().apply(props)) { props -> - val controlled = props.hasControlledChecked() - ToggleNode( - controlled = controlled, - checked = if (controlled) props.controlledChecked() else false, - defaultChecked = props.defaultChecked, - key = props.key - ).apply { - trackOnColor = props.trackOnColor - trackOffColor = props.trackOffColor - trackDisabledColor = props.trackDisabledColor - thumbColor = props.thumbColor - thumbDisabledColor = props.thumbDisabledColor - focusOutlineColor = props.focusOutlineColor - switchWidthPx = props.switchWidthPx - switchHeightPx = props.switchHeightPx - applyStyle(this, props.style) - applyHandlers(this, props) - applyRef(this, ref) - add(this) +fun UiScope.toggle(props: ToggleProps.() -> Unit = {}, ref: RefTarget? = null) = + withProps(ToggleProps().apply(props)) { props -> + val controlled = props.hasControlledChecked() + ToggleNode( + controlled = controlled, + checked = if (controlled) props.controlledChecked() else false, + defaultChecked = props.defaultChecked, + key = props.key, + ).apply { + trackOnColor = props.trackOnColor + trackOffColor = props.trackOffColor + trackDisabledColor = props.trackDisabledColor + thumbColor = props.thumbColor + thumbDisabledColor = props.thumbDisabledColor + focusOutlineColor = props.focusOutlineColor + switchWidthPx = props.switchWidthPx + switchHeightPx = props.switchHeightPx + applyStyle(this, props.style) + applyHandlers(this, props) + applyRef(this, ref) + add(this) + } } -} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/MediaDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/MediaDsl.kt index efc7a5f..f203f32 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/MediaDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/MediaDsl.kt @@ -7,34 +7,32 @@ import org.dreamfinity.dsgl.core.hooks.ref.ElementHandle import org.dreamfinity.dsgl.core.hooks.ref.RefTarget /** Image node props; accepts resource id, file://, or http(s) URLs in MC host. */ -open class ImageProps(var url: String) : ComponentProps() +open class ImageProps( + var url: String, +) : ComponentProps() /** Item stack node props for platform-specific stacks. */ open class ItemStackProps( var stack: ItemStackRef, var size: Int = 18, var rotYDeg: Double = 160.0, - var rotXDeg: Double = -11.0 + var rotXDeg: Double = -11.0, ) : ComponentProps() - /** Image node from resource, file, or URL (host-dependent). */ @DsglDsl -fun UiScope.img( - url: String, - props: ImageProps.() -> Unit = {}, - ref: RefTarget? = null, -) = withProps(ImageProps(url).apply(props)) { props -> - ImageNode( - props.url, - key = props.key - ).apply { - applyStyle(props.style) - applyHandlers(props) - applyRef(this, ref) - add(this) +fun UiScope.img(url: String, props: ImageProps.() -> Unit = {}, ref: RefTarget? = null) = + withProps(ImageProps(url).apply(props)) { props -> + ImageNode( + props.url, + key = props.key, + ).apply { + applyStyle(props.style) + applyHandlers(props) + applyRef(this, ref) + add(this) + } } -} /** Item stack node for platform-specific stack types. */ @DsglDsl @@ -48,11 +46,11 @@ fun UiScope.itemStack( props.size, props.rotYDeg, props.rotXDeg, - props.key + props.key, ).apply { applyStyle(this, props.style) applyHandlers(this, props) applyRef(this, ref) add(this) } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/StyleScope.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/StyleScope.kt index fc4d7d8..eac6b30 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/StyleScope.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/StyleScope.kt @@ -7,19 +7,19 @@ import org.dreamfinity.dsgl.core.style.* data class StyleBorder( val width: CssLength, - val color: Int = DsglColors.BORDER + val color: Int = DsglColors.BORDER, ) data class StyleSpacing( val top: CssLength, val right: CssLength, val bottom: CssLength, - val left: CssLength + val left: CssLength, ) data class StyleOverflowAxes( val x: Overflow, - val y: Overflow + val y: Overflow, ) interface CssLengthUnitsDsl { @@ -62,12 +62,11 @@ class StyleTransformOriginBuilder : CssLengthUnitsDsl { y = value } - internal fun build(): TransformOrigin { - return TransformOrigin( + internal fun build(): TransformOrigin = + TransformOrigin( originX = x.coerceIn(0f, 1f), - originY = y.coerceIn(0f, 1f) + originY = y.coerceIn(0f, 1f), ) - } } @DsglDsl @@ -94,14 +93,13 @@ class StyleSpacingBuilder : CssLengthUnitsDsl { bottom = value } - internal fun build(): StyleSpacing { - return StyleSpacing( + internal fun build(): StyleSpacing = + StyleSpacing( top = top, right = right, bottom = bottom, - left = left + left = left, ) - } } @DsglDsl @@ -184,17 +182,21 @@ class StyleInsetBuilder : CssLengthUnitsDsl { } internal fun hasLeft(): Boolean = leftAssigned + internal fun hasTop(): Boolean = topAssigned + internal fun hasRight(): Boolean = rightAssigned + internal fun hasBottom(): Boolean = bottomAssigned } -@DsglDsl /** * Styling DSL attached to a [DOMNode]. */ -class StyleScope internal constructor(private val node: DOMNode) : CssLengthUnitsDsl { - +@DsglDsl +class StyleScope internal constructor( + private val node: DOMNode, +) : CssLengthUnitsDsl { var display: Display get() = Display.Block set(value) { @@ -593,10 +595,11 @@ class StyleScope internal constructor(private val node: DOMNode) : CssLengthUnit } fun transformOrigin(originX: Float, originY: Float) { - transformOrigin = TransformOrigin( - originX = originX.coerceIn(0f, 1f), - originY = originY.coerceIn(0f, 1f) - ) + transformOrigin = + TransformOrigin( + originX = originX.coerceIn(0f, 1f), + originY = originY.coerceIn(0f, 1f), + ) } fun transformOrigin(block: StyleTransformOriginBuilder.() -> Unit) { @@ -619,7 +622,12 @@ class StyleScope internal constructor(private val node: DOMNode) : CssLengthUnit setSpacing(StyleProperty.MARGIN, vertical, horizontal, vertical, horizontal) } - fun margin(top: CssLength, right: CssLength, bottom: CssLength, left: CssLength) { + fun margin( + top: CssLength, + right: CssLength, + bottom: CssLength, + left: CssLength, + ) { setSpacing(StyleProperty.MARGIN, top, right, bottom, left) } @@ -639,7 +647,12 @@ class StyleScope internal constructor(private val node: DOMNode) : CssLengthUnit setSpacing(StyleProperty.PADDING, vertical, horizontal, vertical, horizontal) } - fun padding(top: CssLength, right: CssLength, bottom: CssLength, left: CssLength) { + fun padding( + top: CssLength, + right: CssLength, + bottom: CssLength, + left: CssLength, + ) { requireNonNegative(top, "padding") requireNonNegative(right, "padding") requireNonNegative(bottom, "padding") @@ -676,7 +689,13 @@ class StyleScope internal constructor(private val node: DOMNode) : CssLengthUnit border(maxLength(horizontal, vertical), color) } - fun border(top: CssLength, right: CssLength, bottom: CssLength, left: CssLength, color: Int = DsglColors.BORDER) { + fun border( + top: CssLength, + right: CssLength, + bottom: CssLength, + left: CssLength, + color: Int = DsglColors.BORDER, + ) { border(maxLength(top, right, bottom, left), color) } @@ -764,11 +783,11 @@ class StyleScope internal constructor(private val node: DOMNode) : CssLengthUnit top: CssLength, right: CssLength, bottom: CssLength, - left: CssLength + left: CssLength, ) { setLiteral( property, - "${top.toCssLiteral()} ${right.toCssLiteral()} ${bottom.toCssLiteral()} ${left.toCssLiteral()}" + "${top.toCssLiteral()} ${right.toCssLiteral()} ${bottom.toCssLiteral()} ${left.toCssLiteral()}", ) } @@ -783,7 +802,12 @@ class StyleScope internal constructor(private val node: DOMNode) : CssLengthUnit return if (first.value >= second.value) first else second } - private fun maxLength(first: CssLength, second: CssLength, third: CssLength, fourth: CssLength): CssLength { + private fun maxLength( + first: CssLength, + second: CssLength, + third: CssLength, + fourth: CssLength, + ): CssLength { val firstMax = maxLength(first, second) val secondMax = maxLength(third, fourth) return maxLength(firstMax, secondMax) @@ -799,91 +823,108 @@ class StyleScope internal constructor(private val node: DOMNode) : CssLengthUnit private fun toColorLiteral(value: Int): String { val unsigned = value.toLong() and 0xFFFFFFFFL - return "#" + unsigned.toString(16).padStart(8, '0').uppercase() + return "#" + + unsigned + .toString(16) + .padStart(8, '0') + .uppercase() } - private fun Display.toCssLiteral(): String = when (this) { - Display.Block -> "block" - Display.Inline -> "inline" - Display.None -> "none" - Display.Flex -> "flex" - Display.Grid -> "grid" - } + private fun Display.toCssLiteral(): String = + when (this) { + Display.Block -> "block" + Display.Inline -> "inline" + Display.None -> "none" + Display.Flex -> "flex" + Display.Grid -> "grid" + } - private fun PositionMode.toCssLiteral(): String = when (this) { - PositionMode.Static -> "static" - PositionMode.Relative -> "relative" - PositionMode.Absolute -> "absolute" - PositionMode.Fixed -> "fixed" - PositionMode.Sticky -> "sticky" - } + private fun PositionMode.toCssLiteral(): String = + when (this) { + PositionMode.Static -> "static" + PositionMode.Relative -> "relative" + PositionMode.Absolute -> "absolute" + PositionMode.Fixed -> "fixed" + PositionMode.Sticky -> "sticky" + } - private fun Overflow.toCssLiteral(): String = when (this) { - Overflow.Visible -> "visible" - Overflow.Hidden -> "hidden" - Overflow.Scroll -> "scroll" - Overflow.Auto -> "auto" - } + private fun Overflow.toCssLiteral(): String = + when (this) { + Overflow.Visible -> "visible" + Overflow.Hidden -> "hidden" + Overflow.Scroll -> "scroll" + Overflow.Auto -> "auto" + } - private fun FlexDirection.toCssLiteral(): String = when (this) { - FlexDirection.Row -> "row" - FlexDirection.Column -> "column" - } + private fun FlexDirection.toCssLiteral(): String = + when (this) { + FlexDirection.Row -> "row" + FlexDirection.Column -> "column" + } - private fun JustifyContent.toCssLiteral(): String = when (this) { - JustifyContent.Start -> "start" - JustifyContent.Center -> "center" - JustifyContent.End -> "end" - JustifyContent.SpaceBetween -> "space-between" - JustifyContent.SpaceAround -> "space-around" - JustifyContent.SpaceEvenly -> "space-evenly" - } + private fun JustifyContent.toCssLiteral(): String = + when (this) { + JustifyContent.Start -> "start" + JustifyContent.Center -> "center" + JustifyContent.End -> "end" + JustifyContent.SpaceBetween -> "space-between" + JustifyContent.SpaceAround -> "space-around" + JustifyContent.SpaceEvenly -> "space-evenly" + } - private fun AlignItems.toCssLiteral(): String = when (this) { - AlignItems.Start -> "start" - AlignItems.Center -> "center" - AlignItems.End -> "end" - AlignItems.Stretch -> "stretch" - } + private fun AlignItems.toCssLiteral(): String = + when (this) { + AlignItems.Start -> "start" + AlignItems.Center -> "center" + AlignItems.End -> "end" + AlignItems.Stretch -> "stretch" + } - private fun JustifyItems.toCssLiteral(): String = when (this) { - JustifyItems.Start -> "start" - JustifyItems.Center -> "center" - JustifyItems.End -> "end" - JustifyItems.Stretch -> "stretch" - } + private fun JustifyItems.toCssLiteral(): String = + when (this) { + JustifyItems.Start -> "start" + JustifyItems.Center -> "center" + JustifyItems.End -> "end" + JustifyItems.Stretch -> "stretch" + } - private fun GridAutoFlow.toCssLiteral(): String = when (this) { - GridAutoFlow.Row -> "row" - GridAutoFlow.Column -> "column" - } + private fun GridAutoFlow.toCssLiteral(): String = + when (this) { + GridAutoFlow.Row -> "row" + GridAutoFlow.Column -> "column" + } - private fun TextWrap.toCssLiteral(): String = when (this) { - TextWrap.Wrap -> "wrap" - TextWrap.NoWrap -> "nowrap" - } + private fun TextWrap.toCssLiteral(): String = + when (this) { + TextWrap.Wrap -> "wrap" + TextWrap.NoWrap -> "nowrap" + } - private fun TextFormatting.toCssLiteral(): String = when (this) { - TextFormatting.None -> "none" - TextFormatting.Minecraft -> "minecraft" - } + private fun TextFormatting.toCssLiteral(): String = + when (this) { + TextFormatting.None -> "none" + TextFormatting.Minecraft -> "minecraft" + } - private fun FontWeight.toCssLiteral(): String = when (this) { - FontWeight.Normal -> "normal" - FontWeight.Bold -> "bold" - } + private fun FontWeight.toCssLiteral(): String = + when (this) { + FontWeight.Normal -> "normal" + FontWeight.Bold -> "bold" + } - private fun FontStyle.toCssLiteral(): String = when (this) { - FontStyle.Normal -> "normal" - FontStyle.Italic -> "italic" - } + private fun FontStyle.toCssLiteral(): String = + when (this) { + FontStyle.Normal -> "normal" + FontStyle.Italic -> "italic" + } - private fun TextDecoration.toCssLiteral(): String = when (this) { - TextDecoration.None -> "none" - TextDecoration.Underline -> "underline" - TextDecoration.Strikethrough -> "strikethrough" - TextDecoration.UnderlineStrikethrough -> "underline-strikethrough" - } + private fun TextDecoration.toCssLiteral(): String = + when (this) { + TextDecoration.None -> "none" + TextDecoration.Underline -> "underline" + TextDecoration.Strikethrough -> "strikethrough" + TextDecoration.UnderlineStrikethrough -> "underline-strikethrough" + } private fun UiTransform.toCssLiteral(): String { if (isIdentity()) return "none" diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/TextDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/TextDsl.kt index 14051f9..0aa3d9b 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/TextDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/TextDsl.kt @@ -7,7 +7,9 @@ import org.dreamfinity.dsgl.core.hooks.ref.RefTarget import org.dreamfinity.dsgl.core.style.TextFormatting /** Static text props. */ -open class TextProps(value: String = "") : ComponentProps() { +open class TextProps( + value: String = "", +) : ComponentProps() { var source: TextSource = TextSource.Static(value) constructor(valueProvider: () -> String) : this() { @@ -21,34 +23,27 @@ open class TextProps(value: String = "") : ComponentProps() { } } - /** Text node. Supports static and rebuild-driven dynamic text. */ -fun UiScope.text( - props: TextProps.() -> Unit, - ref: RefTarget? = null -) = withProps(TextProps().apply(props)) { props -> - TextNode( - props.source, - key = props.key - ).apply { - applyStyle(this, props.style) - applyHandlers(this, props) - applyRef(this, ref) - add(this) +fun UiScope.text(props: TextProps.() -> Unit, ref: RefTarget? = null) = + withProps(TextProps().apply(props)) { props -> + TextNode( + props.source, + key = props.key, + ).apply { + applyStyle(this, props.style) + applyHandlers(this, props) + applyRef(this, ref) + add(this) + } } -} -fun UiScope.text( - value: String, - props: (TextProps.() -> Unit) = {}, - ref: RefTarget? = null, -) { +fun UiScope.text(value: String, props: (TextProps.() -> Unit) = {}, ref: RefTarget? = null) { text( props = { this.value = value props() }, - ref = ref + ref = ref, ) } @@ -71,27 +66,21 @@ fun UiScope.text( } } }, - ref = ref + ref = ref, ) } -fun UiScope.dynamicText( - value: () -> String -) { +fun UiScope.dynamicText(value: () -> String) { dynamicText(value = value, ref = null) } -fun UiScope.dynamicText( - value: () -> String, - props: TextProps.() -> Unit = {}, - ref: RefTarget? = null, -) { +fun UiScope.dynamicText(value: () -> String, props: TextProps.() -> Unit = {}, ref: RefTarget? = null) { text( props = { source = TextSource.Dynamic(value) props() }, - ref = ref + ref = ref, ) } @@ -114,6 +103,6 @@ fun UiScope.dynamicText( } } }, - ref = ref + ref = ref, ) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/UiScope.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/UiScope.kt index c347204..122f5ec 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/UiScope.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/UiScope.kt @@ -14,21 +14,16 @@ import org.dreamfinity.dsgl.core.hooks.ref.RefTarget * * Call this from [DsglWindow.render] to define the UI hierarchy. */ -fun ui(block: UiScope.() -> Unit): DomTree { - return buildUiTree(ownerWindow = null, root = ContainerNode(stackLayout = true), block = block) -} +fun ui(block: UiScope.() -> Unit): DomTree = + buildUiTree(ownerWindow = null, root = ContainerNode(stackLayout = true), block = block) -fun ui(root: DOMNode, block: UiScope.() -> Unit): DomTree { - return buildUiTree(ownerWindow = null, root = root, block = block) -} +fun ui(root: DOMNode, block: UiScope.() -> Unit): DomTree = buildUiTree(ownerWindow = null, root = root, block = block) -fun ui(window: DsglWindow, block: UiScope.() -> Unit): DomTree { - return buildUiTree(ownerWindow = window, root = ContainerNode(stackLayout = true), block = block) -} +fun ui(window: DsglWindow, block: UiScope.() -> Unit): DomTree = + buildUiTree(ownerWindow = window, root = ContainerNode(stackLayout = true), block = block) -fun ui(window: DsglWindow, root: DOMNode, block: UiScope.() -> Unit): DomTree { - return buildUiTree(ownerWindow = window, root = root, block = block) -} +fun ui(window: DsglWindow, root: DOMNode, block: UiScope.() -> Unit): DomTree = + buildUiTree(ownerWindow = window, root = root, block = block) private fun buildUiTree(ownerWindow: DsglWindow?, root: DOMNode, block: UiScope.() -> Unit): DomTree { val scope = UiScope(root, ownerWindow) @@ -43,33 +38,28 @@ private fun buildUiTree(ownerWindow: DsglWindow?, root: DOMNode, block: UiScope. class UiScope internal constructor( private val parent: DOMNode, private val ownerWindow: DsglWindow? = null, - private val providedContexts: Map, Any?> = emptyMap() + private val providedContexts: Map, Any?> = emptyMap(), ) { @PublishedApi - internal fun requireHookOwnerWindow(): DsglWindow { - return ownerWindow ?: throw IllegalStateException( - "Hook APIs require a UiScope owned by a DsglWindow render session." + internal fun requireHookOwnerWindow(): DsglWindow = + ownerWindow ?: throw IllegalStateException( + "Hook APIs require a UiScope owned by a DsglWindow render session.", ) - } - internal fun childScope(childParent: DOMNode): UiScope { - return UiScope( + internal fun childScope(childParent: DOMNode): UiScope = + UiScope( parent = childParent, ownerWindow = ownerWindow, - providedContexts = providedContexts + providedContexts = providedContexts, ) - } - internal fun withProvidedContext( - context: DsglContext, - value: T, - block: UiScope.() -> R - ): R { - val nextScope = UiScope( - parent = parent, - ownerWindow = ownerWindow, - providedContexts = providedContexts + (context to value) - ) + internal fun withProvidedContext(context: DsglContext, value: T, block: UiScope.() -> R): R { + val nextScope = + UiScope( + parent = parent, + ownerWindow = ownerWindow, + providedContexts = providedContexts + (context to value), + ) return nextScope.block() } @@ -81,14 +71,9 @@ class UiScope internal constructor( return providedContexts[context] as T } + internal fun add(node: T): T = node.applyParent(parent) - internal fun add(node: T): T { - return node.applyParent(parent) - } - - internal fun mount(node: T): T { - return add(node) - } + internal fun mount(node: T): T = add(node) internal fun applyHandlers(node: DOMNode, props: ComponentProps) { node.applyHandlers(props) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/ClickDispatch.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/ClickDispatch.kt index 927d663..82a1f80 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/ClickDispatch.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/ClickDispatch.kt @@ -8,31 +8,31 @@ import org.dreamfinity.dsgl.core.dom.layout.Rect /** * Dispatches a click through the DOM tree; returns true if handled. */ -fun dispatchClick(root: DOMNode, event: MouseClickEvent): Boolean { - return dispatchClickInternal( +fun dispatchClick(root: DOMNode, event: MouseClickEvent): Boolean = + dispatchClickInternal( element = root, event = event, parentTransform = AffineTransform2D.IDENTITY, - parentInputClipRect = null + parentInputClipRect = null, ) -} internal fun dispatchClickInternal( element: DOMNode, event: MouseClickEvent, parentTransform: AffineTransform2D, - parentInputClipRect: Rect? + parentInputClipRect: Rect?, ): Boolean { if (!element.isHitTestVisible()) { return false } - val projection = UsedInteractionGeometryResolver.projectNodeAtPoint( - node = element, - mouseX = event.mouseX, - mouseY = event.mouseY, - parentTransform = parentTransform, - parentInputClipRect = parentInputClipRect - ) ?: return false + val projection = + UsedInteractionGeometryResolver.projectNodeAtPoint( + node = element, + mouseX = event.mouseX, + mouseY = event.mouseY, + parentTransform = parentTransform, + parentInputClipRect = parentInputClipRect, + ) ?: return false val childInputClipRect = projection.childInputClipRect if (projection.canTraverseChildren) { @@ -42,7 +42,7 @@ internal fun dispatchClickInternal( element = child, event = event, parentTransform = projection.worldTransform, - parentInputClipRect = childInputClipRect + parentInputClipRect = childInputClipRect, ) ) { return true @@ -56,4 +56,3 @@ internal fun dispatchClickInternal( return element.handleClick(event) } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/EventBus.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/EventBus.kt index c4d9442..245765c 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/EventBus.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/EventBus.kt @@ -1,24 +1,29 @@ package org.dreamfinity.dsgl.core.event import org.dreamfinity.dsgl.core.dom.DOMNode -import java.util.* +import java.util.Collections +import java.util.EnumMap +import java.util.IdentityHashMap +import java.util.WeakHashMap typealias EventCallback = (Event) -> Unit object EventBus { private val listeners: MutableMap>> = EnumMap(Events::class.java) - private val nonBubblingEvents: Set = setOf( - Events.MOUSEENTER, - Events.MOUSELEAVE, - Events.MOUSEOVER, - Events.FOCUS, - Events.BLUR - ) + private val nonBubblingEvents: Set = + setOf( + Events.MOUSEENTER, + Events.MOUSELEAVE, + Events.MOUSEOVER, + Events.FOCUS, + Events.BLUR, + ) - private fun getEventMap(eventType: Events): MutableMap> { - return listeners.getOrPut(eventType) { WeakHashMap() } - } + private fun getEventMap(eventType: Events): MutableMap> = + listeners.getOrPut(eventType) { + WeakHashMap() + } fun DOMNode.addEventListener(eventType: Events, callback: (E) -> Unit) { getEventMap(eventType).getOrPut(this) { arrayListOf() }.add(callback as EventCallback) @@ -60,7 +65,7 @@ object EventBus { data class DebugListenerSnapshot( val registeredNodes: Int, - val registeredCallbacks: Int + val registeredCallbacks: Int, ) fun debugListenerSnapshot(): DebugListenerSnapshot { @@ -74,12 +79,12 @@ object EventBus { } return DebugListenerSnapshot( registeredNodes = nodes.size, - registeredCallbacks = callbacks + registeredCallbacks = callbacks, ) } - internal fun hasInputListeners(node: DOMNode): Boolean { - return listOf( + internal fun hasInputListeners(node: DOMNode): Boolean = + listOf( Events.MOUSEDOWN, Events.MOUSEUP, Events.CLICK, @@ -87,12 +92,11 @@ object EventBus { Events.WHEEL, Events.MOUSEMOVE, Events.KEYDOWN, - Events.KEYUP + Events.KEYUP, ).any { eventType -> val callbacks = listeners[eventType]?.get(node) callbacks != null && callbacks.isNotEmpty() } - } fun post(event: Event) { val allListeners = listeners[event.type] ?: return @@ -129,27 +133,29 @@ object EventBus { return } - val validListeners = when (event.type) { - Events.KEYUP, Events.KEYDOWN -> allListeners - Events.MOUSEDOWN, Events.CLICK, Events.MOUSEUP, Events.WHEEL, Events.MOUSEOVER -> - allListeners.filter { it.key.hovered(event as MouseEvent) } - - Events.MOUSEOUT, Events.MOUSELEAVE -> - allListeners.filter { !it.key.hovered(event as MouseEvent) } - - Events.MOUSEMOVE -> allListeners - Events.DRAG -> allListeners - Events.MOUSEENTER -> allListeners - Events.DRAGSTART, - Events.DRAGGING, - Events.DRAGEND, - Events.DRAGENTER, - Events.DRAGOVER, - Events.DRAGLEAVE, - Events.DROP -> allListeners - - Events.FOCUS, Events.BLUR, Events.INPUT, Events.CHANGE -> allListeners - } + val validListeners = + when (event.type) { + Events.KEYUP, Events.KEYDOWN -> allListeners + Events.MOUSEDOWN, Events.CLICK, Events.MOUSEUP, Events.WHEEL, Events.MOUSEOVER -> + allListeners.filter { it.key.hovered(event as MouseEvent) } + + Events.MOUSEOUT, Events.MOUSELEAVE -> + allListeners.filter { !it.key.hovered(event as MouseEvent) } + + Events.MOUSEMOVE -> allListeners + Events.DRAG -> allListeners + Events.MOUSEENTER -> allListeners + Events.DRAGSTART, + Events.DRAGGING, + Events.DRAGEND, + Events.DRAGENTER, + Events.DRAGOVER, + Events.DRAGLEAVE, + Events.DROP, + -> allListeners + + Events.FOCUS, Events.BLUR, Events.INPUT, Events.CHANGE -> allListeners + } validListeners.forEach { (_, callbacks) -> callbacks.forEach { callback -> diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/Events.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/Events.kt index 7b97a63..85b78b3 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/Events.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/Events.kt @@ -29,5 +29,5 @@ enum class Events { FOCUS, BLUR, INPUT, - CHANGE + CHANGE, } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/FocusGainEvent.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/FocusGainEvent.kt index acd4026..7636ab4 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/FocusGainEvent.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/FocusGainEvent.kt @@ -4,7 +4,7 @@ package org.dreamfinity.dsgl.core.event * Focus event fired when an element receives focus. */ data class FocusGainEvent( - val previousTargetKey: Any? = null + val previousTargetKey: Any? = null, ) : Event() { override val type: Events get() = Events.FOCUS diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/FocusLoseEvent.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/FocusLoseEvent.kt index bf03505..ea1a861 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/FocusLoseEvent.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/FocusLoseEvent.kt @@ -4,7 +4,7 @@ package org.dreamfinity.dsgl.core.event * Blur event fired when an element loses focus. */ data class FocusLoseEvent( - val nextTargetKey: Any? = null + val nextTargetKey: Any? = null, ) : Event() { override val type: Events get() = Events.BLUR diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/HoverDispatcher.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/HoverDispatcher.kt index 7bacf07..2c62f29 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/HoverDispatcher.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/HoverDispatcher.kt @@ -18,7 +18,7 @@ fun collectHoverChain(root: DOMNode, mouseX: Int, mouseY: Int): List { mouseY = mouseY, parentTransform = AffineTransform2D.IDENTITY, parentInputClipRect = null, - out = out + out = out, ) return out } @@ -29,18 +29,19 @@ internal fun collectHoverChain( mouseY: Int, parentTransform: AffineTransform2D, parentInputClipRect: Rect?, - out: MutableList + out: MutableList, ): Boolean { if (root.styleDisabled) return false if (!root.isHitTestVisible()) return false - val projection = UsedInteractionGeometryResolver.projectNodeAtPoint( - node = root, - mouseX = mouseX, - mouseY = mouseY, - parentTransform = parentTransform, - parentInputClipRect = parentInputClipRect - ) ?: return false + val projection = + UsedInteractionGeometryResolver.projectNodeAtPoint( + node = root, + mouseX = mouseX, + mouseY = mouseY, + parentTransform = parentTransform, + parentInputClipRect = parentInputClipRect, + ) ?: return false out.add(root) @@ -54,7 +55,7 @@ internal fun collectHoverChain( mouseY = mouseY, parentTransform = projection.worldTransform, parentInputClipRect = childInputClipRect, - out = out + out = out, ) ) { return true @@ -78,7 +79,7 @@ fun updateHover( mouseX: Int, mouseY: Int, mouseDX: Int, - mouseDY: Int + mouseDY: Int, ) { val currHoverChain = ArrayList(prevHoverChain.size + 4) collectHoverChain( @@ -87,7 +88,7 @@ fun updateHover( mouseY = mouseY, parentTransform = AffineTransform2D.IDENTITY, parentInputClipRect = null, - out = currHoverChain + out = currHoverChain, ) val minSize = minOf(prevHoverChain.size, currHoverChain.size) @@ -134,9 +135,9 @@ private fun isSameHoverNode(prev: DOMNode, curr: DOMNode): Boolean { val currKey = curr.key if (prevKey != null || currKey != null) { return prevKey != null && - currKey != null && - prevKey == currKey && - prev.javaClass == curr.javaClass + currKey != null && + prevKey == currKey && + prev.javaClass == curr.javaClass } // Root is recreated on every rebuild but is conceptually the same hover scope. @@ -181,4 +182,3 @@ private fun label(element: DOMNode): String { val keyPart = element.key?.let { " key=$it" } ?: "" return element.javaClass.simpleName + keyPart } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/InputEvent.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/InputEvent.kt index dd48392..8c895ab 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/InputEvent.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/InputEvent.kt @@ -5,7 +5,7 @@ package org.dreamfinity.dsgl.core.event */ data class InputEvent( val value: String, - val parsedValue: Any? = null + val parsedValue: Any? = null, ) : Event() { override val type: Events get() = Events.INPUT diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/KeyInput.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/KeyInput.kt index 74a824c..3421aaf 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/KeyInput.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/KeyInput.kt @@ -34,4 +34,4 @@ object KeyInput { else -> ch } } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/KeyModifiers.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/KeyModifiers.kt index e68a76d..07921e0 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/KeyModifiers.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/KeyModifiers.kt @@ -7,17 +7,20 @@ object KeyModifiers { @Volatile var shiftDown: Boolean = false private set + @Volatile var controlDown: Boolean = false private set + @Volatile var metaDown: Boolean = false private set - private val macOs: Boolean = run { - val osName = System.getProperty("os.name") ?: return@run false - osName.lowercase().contains("mac") - } + private val macOs: Boolean = + run { + val osName = System.getProperty("os.name") ?: return@run false + osName.lowercase().contains("mac") + } val shortcutDown: Boolean get() = if (macOs) metaDown else controlDown @@ -37,4 +40,4 @@ object KeyModifiers { controlDown = control metaDown = meta } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/KeyboardKeyDownEvent.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/KeyboardKeyDownEvent.kt index 2eb8617..dceeb5d 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/KeyboardKeyDownEvent.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/KeyboardKeyDownEvent.kt @@ -3,7 +3,10 @@ package org.dreamfinity.dsgl.core.event /** * Key down event. */ -data class KeyboardKeyDownEvent(var keyChar: Char, var keyCode: Int) : Event() { +data class KeyboardKeyDownEvent( + var keyChar: Char, + var keyCode: Int, +) : Event() { override val type: Events get() = Events.KEYDOWN -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/KeyboardKeyUpEvent.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/KeyboardKeyUpEvent.kt index 67180f2..12246fb 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/KeyboardKeyUpEvent.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/KeyboardKeyUpEvent.kt @@ -3,7 +3,10 @@ package org.dreamfinity.dsgl.core.event /** * Key up event. */ -data class KeyboardKeyUpEvent(var keyChar: Char, var keyCode: Int) : Event() { +data class KeyboardKeyUpEvent( + var keyChar: Char, + var keyCode: Int, +) : Event() { override val type: Events get() = Events.KEYUP -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseButton.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseButton.kt index 1625eaa..4eb83a1 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseButton.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseButton.kt @@ -6,5 +6,5 @@ package org.dreamfinity.dsgl.core.event enum class MouseButton { LEFT, RIGHT, - MIDDLE -} \ No newline at end of file + MIDDLE, +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseClickEvent.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseClickEvent.kt index a5459cf..fee7a9f 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseClickEvent.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseClickEvent.kt @@ -6,8 +6,8 @@ package org.dreamfinity.dsgl.core.event data class MouseClickEvent( override var mouseX: Int, override var mouseY: Int, - var mouseButton: MouseButton + var mouseButton: MouseButton, ) : MouseEvent(mouseX, mouseY) { override val type: Events get() = Events.CLICK -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseDownEvent.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseDownEvent.kt index 1a82fde..d1827cb 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseDownEvent.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseDownEvent.kt @@ -6,8 +6,8 @@ package org.dreamfinity.dsgl.core.event data class MouseDownEvent( override var mouseX: Int, override var mouseY: Int, - var mouseButton: MouseButton + var mouseButton: MouseButton, ) : MouseEvent(mouseX, mouseY) { override val type: Events get() = Events.MOUSEDOWN -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseDragEvent.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseDragEvent.kt index a298067..cc54e8b 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseDragEvent.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseDragEvent.kt @@ -8,8 +8,8 @@ data class MouseDragEvent( var lastMouseY: Int, var dx: Int, var dy: Int, - var mouseButton: MouseButton + var mouseButton: MouseButton, ) : MouseEvent(lastMouseX, lastMouseY) { override val type: Events get() = Events.DRAG -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseEnterEvent.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseEnterEvent.kt index 0bb62b9..1b9a443 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseEnterEvent.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseEnterEvent.kt @@ -3,7 +3,10 @@ package org.dreamfinity.dsgl.core.event /** * Mouse enter event for a node's bounds. */ -class MouseEnterEvent(mouseX: Int, mouseY: Int) : MouseEvent(mouseX, mouseY) { +class MouseEnterEvent( + mouseX: Int, + mouseY: Int, +) : MouseEvent(mouseX, mouseY) { override val type: Events get() = Events.MOUSEENTER -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseEvent.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseEvent.kt index da113ff..b555ce4 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseEvent.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseEvent.kt @@ -3,4 +3,7 @@ package org.dreamfinity.dsgl.core.event /** * Base mouse event with cursor position in UI coordinates. */ -abstract class MouseEvent(open var mouseX: Int, open var mouseY: Int) : Event() \ No newline at end of file +abstract class MouseEvent( + open var mouseX: Int, + open var mouseY: Int, +) : Event() diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseLeaveEvent.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseLeaveEvent.kt index baaa576..b1f50df 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseLeaveEvent.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseLeaveEvent.kt @@ -3,7 +3,10 @@ package org.dreamfinity.dsgl.core.event /** * Mouse leave event for a node's bounds. */ -class MouseLeaveEvent(mouseX: Int, mouseY: Int) : MouseEvent(mouseX, mouseY) { +class MouseLeaveEvent( + mouseX: Int, + mouseY: Int, +) : MouseEvent(mouseX, mouseY) { override val type: Events get() = Events.MOUSELEAVE -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseMoveEvent.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseMoveEvent.kt index 859e87e..89a5994 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseMoveEvent.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseMoveEvent.kt @@ -7,8 +7,8 @@ class MouseMoveEvent( mouseX: Int, mouseY: Int, val prevX: Int = mouseX, - val prevY: Int = mouseY + val prevY: Int = mouseY, ) : MouseEvent(mouseX, mouseY) { override val type: Events get() = Events.MOUSEMOVE -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseOverEvent.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseOverEvent.kt index b9fcae6..a14228f 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseOverEvent.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseOverEvent.kt @@ -3,7 +3,10 @@ package org.dreamfinity.dsgl.core.event /** * Mouse over (move within bounds) event. */ -class MouseOverEvent(mouseX: Int, mouseY: Int) : MouseEvent(mouseX, mouseY) { +class MouseOverEvent( + mouseX: Int, + mouseY: Int, +) : MouseEvent(mouseX, mouseY) { override val type: Events get() = Events.MOUSEOVER -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseUpEvent.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseUpEvent.kt index e9ee672..55dc6d4 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseUpEvent.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseUpEvent.kt @@ -6,8 +6,8 @@ package org.dreamfinity.dsgl.core.event data class MouseUpEvent( override var mouseX: Int, override var mouseY: Int, - var mouseButton: MouseButton + var mouseButton: MouseButton, ) : MouseEvent(mouseX, mouseY) { override val type: Events get() = Events.MOUSEUP -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseWheelEvent.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseWheelEvent.kt index f423540..2ed7878 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseWheelEvent.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/MouseWheelEvent.kt @@ -6,8 +6,8 @@ package org.dreamfinity.dsgl.core.event data class MouseWheelEvent( override var mouseX: Int, override var mouseY: Int, - var dWheel: Int + var dWheel: Int, ) : MouseEvent(mouseX, mouseY) { override val type: Events get() = Events.WHEEL -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/ValueChangedEvent.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/ValueChangedEvent.kt index 75b1618..aa3b4a1 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/ValueChangedEvent.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/ValueChangedEvent.kt @@ -5,7 +5,7 @@ package org.dreamfinity.dsgl.core.event */ data class ValueChangedEvent( val value: String, - val parsedValue: Any? = null + val parsedValue: Any? = null, ) : Event() { override val type: Events get() = Events.CHANGE diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/FontDiscovery.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/FontDiscovery.kt index 9ad3ab4..45c8792 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/FontDiscovery.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/FontDiscovery.kt @@ -8,7 +8,7 @@ import kotlin.io.path.isRegularFile enum class FontAssetSource { Jar, - External + External, } internal data class FontPackageCandidate( @@ -16,7 +16,7 @@ internal data class FontPackageCandidate( val ttfPath: Path, val metaPath: Path, val atlasPath: Path, - val missing: List + val missing: List, ) { val isValid: Boolean get() = missing.isEmpty() @@ -31,16 +31,16 @@ internal object FontDiscovery { return normalized.removeSuffix(".ttf") } - fun parseGeneratedFontIndex(content: String): List { - return content.lineSequence() + fun parseGeneratedFontIndex(content: String): List = + content + .lineSequence() .map { it.trim() } .filter { it.isNotBlank() && it.endsWith(".ttf", ignoreCase = true) } .toList() - } fun resolveSourcePriority( jarFontIds: Collection, - externalFontIds: Collection + externalFontIds: Collection, ): Map { val result = linkedMapOf() jarFontIds.forEach { result[it] = FontAssetSource.Jar } @@ -53,8 +53,12 @@ internal object FontDiscovery { val candidates = ArrayList(16) Files.walk(root).use { stream -> stream - .filter { it.isRegularFile() && it.fileName.toString().endsWith(".ttf", ignoreCase = true) } - .forEach { ttfPath -> + .filter { + it.isRegularFile() && + it.fileName + .toString() + .endsWith(".ttf", ignoreCase = true) + }.forEach { ttfPath -> val relative = normalizeRelativePath(root.relativize(ttfPath).toString()) val fontId = fontIdFromRelativeTtfPath(relative) val baseRelative = relative.removeSuffix(".ttf") @@ -63,21 +67,22 @@ internal object FontDiscovery { val missing = mutableListOf() if (!meta.isRegularFile()) missing += "$fontId-meta.json" if (!atlas.isRegularFile()) missing += "$fontId-mtsdf.rgba.deflate" - candidates += FontPackageCandidate( - fontId = fontId, - ttfPath = ttfPath.toAbsolutePath().normalize(), - metaPath = meta.toAbsolutePath().normalize(), - atlasPath = atlas.toAbsolutePath().normalize(), - missing = missing - ) + candidates += + FontPackageCandidate( + fontId = fontId, + ttfPath = ttfPath.toAbsolutePath().normalize(), + metaPath = meta.toAbsolutePath().normalize(), + atlasPath = atlas.toAbsolutePath().normalize(), + missing = missing, + ) } } return candidates.sortedBy { it.fontId.lowercase() } } - internal fun normalizeRelativePath(path: String): String { - return path.replace('\\', '/') + internal fun normalizeRelativePath(path: String): String = + path + .replace('\\', '/') .trimStart('/', '\\') .replace(File.separatorChar, '/') - } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/FontRegistry.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/FontRegistry.kt index 44a8b15..3bb18d5 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/FontRegistry.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/FontRegistry.kt @@ -10,7 +10,7 @@ import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths -import java.util.* +import java.util.Collections import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicLong import java.util.zip.Inflater @@ -19,7 +19,7 @@ import javax.imageio.ImageIO enum class FontPathMode { Resource, - FileSystem + FileSystem, } data class MsdfFontResource( @@ -28,13 +28,13 @@ data class MsdfFontResource( val pathMode: FontPathMode, val metaPath: String, val atlasPath: String, - val ttfPath: String? + val ttfPath: String?, ) data class FontTextureHandle( val textureId: Int, val width: Int, - val height: Int + val height: Int, ) data class LoadedMsdfFont( @@ -44,17 +44,17 @@ data class LoadedMsdfFont( val preferredMissingGlyphIndex: Int?, val preferredQuestionGlyphIndex: Int?, val atlasPayload: AtlasPayload, - var handle: FontTextureHandle? = null + var handle: FontTextureHandle? = null, ) data class AtlasBitmap( val width: Int, val height: Int, - var rgbaBytes: ByteArray + var rgbaBytes: ByteArray, ) class AtlasPayload internal constructor( - private var encodedPngBytes: ByteArray? + private var encodedPngBytes: ByteArray?, ) { @Volatile private var decodedBitmap: AtlasBitmap? = null @@ -109,9 +109,10 @@ class AtlasPayload internal constructor( } private fun decodePng(bytes: ByteArray): AtlasBitmap { - val image = ByteArrayInputStream(bytes).use { input -> - ImageIO.read(input) - } ?: throw IllegalStateException("Atlas payload is neither deflated rgba nor PNG") + val image = + ByteArrayInputStream(bytes).use { input -> + ImageIO.read(input) + } ?: throw IllegalStateException("Atlas payload is neither deflated rgba nor PNG") val width = image.width.coerceAtLeast(1) val height = image.height.coerceAtLeast(1) @@ -139,7 +140,7 @@ data class RegisteredFontInfo( val source: FontAssetSource, val metaPath: String, val atlasPath: String, - val ttfPath: String? + val ttfPath: String?, ) data class FontPreloadSummary( @@ -151,7 +152,7 @@ data class FontPreloadSummary( val failedFonts: Int, val totalFonts: Int, val durationMs: Long, - val fallbackReady: Boolean + val fallbackReady: Boolean, ) data class ShapedGlyph( @@ -162,7 +163,7 @@ data class ShapedGlyph( val advance: Float, val charStart: Int, val charEnd: Int, - val sourceCodepoint: Int + val sourceCodepoint: Int, ) data class ShapedTextRun( @@ -170,13 +171,13 @@ data class ShapedTextRun( val charStart: Int, val charEnd: Int, val glyphs: List, - val advance: Float + val advance: Float, ) data class ShapedText( val glyphs: List, val runs: List, - val width: Float + val width: Float, ) object FontRegistry { @@ -198,12 +199,12 @@ object FontRegistry { val rangeStart: Int, val rangeEnd: Int, val directionFlags: Int, - val formattingMode: String + val formattingMode: String, ) private data class FontCodepointKey( val fontId: FontId, - val codepoint: Int + val codepoint: Int, ) private data class MutableShapingSegment( @@ -212,7 +213,7 @@ object FontRegistry { val sourceStartByChar: MutableList = ArrayList(), val sourceEndByChar: MutableList = ArrayList(), var charStart: Int = Int.MAX_VALUE, - var charEnd: Int = 0 + var charEnd: Int = 0, ) private val descriptors: MutableMap = linkedMapOf() @@ -221,9 +222,8 @@ object FontRegistry { private val parseErrorLogTimes: MutableMap = ConcurrentHashMap() private val shapeCache: MutableMap = object : LinkedHashMap(128, 0.75f, true) { - override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean { - return size > 1024 - } + override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean = + size > 1024 } private val derivedFontCache: MutableMap = ConcurrentHashMap() private val fontRenderContext: FontRenderContext = FontRenderContext(AffineTransform(), true, true) @@ -256,7 +256,7 @@ object FontRegistry { val hits: Long, val misses: Long, val entries: Int, - val maxEntries: Int + val maxEntries: Int, ) data class TextHotPathStats( @@ -273,23 +273,24 @@ object FontRegistry { val glyphIndexForCodepointCalls: Long, val glyphIndexCacheHits: Long, val glyphIndexCacheMisses: Long, - val glyphIndexVectorBuildCalls: Long + val glyphIndexVectorBuildCalls: Long, ) @Synchronized fun discoverAndPreloadFonts( externalFontsDir: File?, - classLoader: ClassLoader = javaClass.classLoader + classLoader: ClassLoader = javaClass.classLoader, ): FontPreloadSummary { val startedAt = System.nanoTime() resourceClassLoader = classLoader ensureDefaults() val jarDiscovered = descriptors.values.count { it.source == FontAssetSource.Jar } - val externalPackages = externalFontsDir - ?.takeIf { it.exists() && it.isDirectory } - ?.let { FontDiscovery.discoverExternalPackages(it.toPath()) } - .orEmpty() + val externalPackages = + externalFontsDir + ?.takeIf { it.exists() && it.isDirectory } + ?.let { FontDiscovery.discoverExternalPackages(it.toPath()) } + .orEmpty() var externalDiscovered = 0 var externalOverrodeJar = 0 @@ -302,9 +303,9 @@ object FontRegistry { key = "external-missing:${candidate.fontId}", message = "[DSGL-MSDF] Skipping external font '${candidate.fontId}', missing: ${ candidate.missing.joinToString( - ", " + ", ", ) - }" + }", ) return@forEach } @@ -318,7 +319,7 @@ object FontRegistry { fontId = candidate.fontId, metaFile = candidate.metaPath, atlasFile = candidate.atlasPath, - ttfFile = candidate.ttfPath + ttfFile = candidate.ttfPath, ) } @@ -329,14 +330,14 @@ object FontRegistry { if (!fallbackReady) { logParseErrorRateLimited( key = "fallback-missing", - message = "[DSGL-MSDF] Fallback font '$FALLBACK_FONT_ID' is not available after preload." + message = "[DSGL-MSDF] Fallback font '$FALLBACK_FONT_ID' is not available after preload.", ) } val durationMs = ((System.nanoTime() - startedAt) / 1_000_000L).coerceAtLeast(0L) println( "[DSGL-MSDF] preload summary: jar=$jarDiscovered external=$externalDiscovered " + - "override=$externalOverrodeJar invalidExternal=$invalidExternalPackages loaded=$loadedFonts/$totalFonts in ${durationMs}ms" + "override=$externalOverrodeJar invalidExternal=$invalidExternalPackages loaded=$loadedFonts/$totalFonts in ${durationMs}ms", ) return FontPreloadSummary( @@ -348,7 +349,7 @@ object FontRegistry { failedFonts = failedFonts, totalFonts = totalFonts, durationMs = durationMs, - fallbackReady = fallbackReady + fallbackReady = fallbackReady, ) } @@ -368,29 +369,28 @@ object FontRegistry { metaResourcePath: String, atlasResourcePath: String, ttfResourcePath: String? = null, - source: FontAssetSource = FontAssetSource.Jar + source: FontAssetSource = FontAssetSource.Jar, ) { val normalizedMeta = normalizeRelativePath(metaResourcePath) val normalizedAtlas = normalizeRelativePath(atlasResourcePath) - val normalizedTtf = ttfResourcePath?.let(::normalizeRelativePath) - ?: (normalizedMeta.removeSuffix("-meta.json") + ".ttf") - - descriptors[fontId] = MsdfFontResource( - fontId = fontId, - source = source, - pathMode = FontPathMode.Resource, - metaPath = normalizedMeta, - atlasPath = normalizedAtlas, - ttfPath = normalizedTtf - ) + val normalizedTtf = + ttfResourcePath?.let(::normalizeRelativePath) + ?: (normalizedMeta.removeSuffix("-meta.json") + ".ttf") + + descriptors[fontId] = + MsdfFontResource( + fontId = fontId, + source = source, + pathMode = FontPathMode.Resource, + metaPath = normalizedMeta, + atlasPath = normalizedAtlas, + ttfPath = normalizedTtf, + ) onDescriptorChanged(fontId) } @Synchronized - fun registerGeneratedFromTtfPath( - relativeTtfPath: String, - fontId: FontId = fontIdFromTtfPath(relativeTtfPath) - ) { + fun registerGeneratedFromTtfPath(relativeTtfPath: String, fontId: FontId = fontIdFromTtfPath(relativeTtfPath)) { val normalized = normalizeRelativePath(relativeTtfPath) require(normalized.endsWith(".ttf", ignoreCase = true)) { "Expected .ttf path, got '$relativeTtfPath'" @@ -401,7 +401,7 @@ object FontRegistry { metaResourcePath = "fonts/$base-meta.json", atlasResourcePath = "fonts/$base-mtsdf.rgba.deflate", ttfResourcePath = "fonts/$normalized", - source = FontAssetSource.Jar + source = FontAssetSource.Jar, ) } @@ -435,7 +435,7 @@ object FontRegistry { source = descriptor.source, metaPath = descriptor.metaPath, atlasPath = descriptor.atlasPath, - ttfPath = descriptor.ttfPath + ttfPath = descriptor.ttfPath, ) } } @@ -460,24 +460,26 @@ object FontRegistry { if (text.contains('\n')) { return text.lineSequence().maxOfOrNull { line -> measureText(line, fontId, fontSize) } ?: 0 } - return shapeText(text, fontId, fontSize).width.toInt().coerceAtLeast(0) + return shapeText(text, fontId, fontSize) + .width + .toInt() + .coerceAtLeast(0) } fun shapeText( text: String, fontId: FontId?, fontSize: Int?, - formattingMode: String = "plain" - ): ShapedText { - return shapeTextRange( + formattingMode: String = "plain", + ): ShapedText = + shapeTextRange( text = text, startIndex = 0, endIndexExclusive = text.length, fontId = fontId, fontSize = fontSize, - formattingMode = formattingMode + formattingMode = formattingMode, ) - } fun shapeTextRange( text: String, @@ -485,7 +487,7 @@ object FontRegistry { endIndexExclusive: Int, fontId: FontId?, fontSize: Int?, - formattingMode: String = "plain" + formattingMode: String = "plain", ): ShapedText { shapeTextRangeCalls.incrementAndGet() val (safeStart, safeEnd) = normalizeRange(text, startIndex, endIndexExclusive) @@ -499,21 +501,23 @@ object FontRegistry { return ShapedText( glyphs = emptyList(), runs = emptyList(), - width = fallbackMeasureText(text, fontSize).toFloat() + width = fallbackMeasureText(text, fontSize).toFloat(), ) } - val fallback = getExact(FALLBACK_FONT_ID) - ?.takeIf { it.descriptor.fontId != primary.descriptor.fontId && it.awtBaseFont != null } - val cacheKey = ShapeCacheKey( - primaryFontId = primary.descriptor.fontId, - fallbackFontId = fallback?.descriptor?.fontId, - fontPx = fontPx, - text = text, - rangeStart = safeStart, - rangeEnd = safeEnd, - directionFlags = Font.LAYOUT_LEFT_TO_RIGHT, - formattingMode = formattingMode - ) + val fallback = + getExact(FALLBACK_FONT_ID) + ?.takeIf { it.descriptor.fontId != primary.descriptor.fontId && it.awtBaseFont != null } + val cacheKey = + ShapeCacheKey( + primaryFontId = primary.descriptor.fontId, + fallbackFontId = fallback?.descriptor?.fontId, + fontPx = fontPx, + text = text, + rangeStart = safeStart, + rangeEnd = safeEnd, + directionFlags = Font.LAYOUT_LEFT_TO_RIGHT, + formattingMode = formattingMode, + ) synchronized(shapeCache) { shapeCacheRequests.incrementAndGet() shapeCache[cacheKey]?.let { @@ -522,14 +526,15 @@ object FontRegistry { } } shapeCacheMisses.incrementAndGet() - val shaped = shapeSingleLine( - text = text, - startIndex = safeStart, - endIndexExclusive = safeEnd, - primary = primary, - fallback = fallback, - fontPx = fontPx - ) + val shaped = + shapeSingleLine( + text = text, + startIndex = safeStart, + endIndexExclusive = safeEnd, + primary = primary, + fallback = fallback, + fontPx = fontPx, + ) synchronized(shapeCache) { shapeCache[cacheKey] = shaped } @@ -541,13 +546,9 @@ object FontRegistry { return font.meta.lineHeightPx(resolveFontSize(fontSize)) } - fun resolveFontSize(fontSize: Int?): Int { - return (fontSize ?: DEFAULT_FONT_SIZE).coerceAtLeast(1) - } + fun resolveFontSize(fontSize: Int?): Int = (fontSize ?: DEFAULT_FONT_SIZE).coerceAtLeast(1) - fun fontIdFromTtfPath(relativeTtfPath: String): FontId { - return FontDiscovery.fontIdFromRelativeTtfPath(relativeTtfPath) - } + fun fontIdFromTtfPath(relativeTtfPath: String): FontId = FontDiscovery.fontIdFromRelativeTtfPath(relativeTtfPath) fun predecodeAtlases(fontIds: Collection): Int { ensureDefaults() @@ -559,7 +560,9 @@ object FontRegistry { .onFailure { error -> logParseErrorRateLimited( key = "atlas-predecode:$fontId", - message = "[DSGL-MSDF] Failed atlas predecode for '$fontId': ${error.message ?: error.javaClass.simpleName}" + message = + "[DSGL-MSDF] Failed atlas predecode for '$fontId': " + + "${error.message ?: error.javaClass.simpleName}", ) } } @@ -573,7 +576,7 @@ object FontRegistry { hits = shapeCacheHits.get(), misses = shapeCacheMisses.get(), entries = entries, - maxEntries = 1024 + maxEntries = 1024, ) } @@ -583,8 +586,8 @@ object FontRegistry { shapeCacheMisses.set(0L) } - fun textHotPathStats(): TextHotPathStats { - return TextHotPathStats( + fun textHotPathStats(): TextHotPathStats = + TextHotPathStats( shapeTextRangeCalls = shapeTextRangeCalls.get(), shapeSegmentCalls = shapeSegmentCalls.get(), requiresReplacementGlyphCalls = requiresReplacementGlyphCalls.get(), @@ -598,9 +601,8 @@ object FontRegistry { glyphIndexForCodepointCalls = glyphIndexForCodepointCalls.get(), glyphIndexCacheHits = glyphIndexCacheHits.get(), glyphIndexCacheMisses = glyphIndexCacheMisses.get(), - glyphIndexVectorBuildCalls = glyphIndexVectorBuildCalls.get() + glyphIndexVectorBuildCalls = glyphIndexVectorBuildCalls.get(), ) - } fun resetTextHotPathStats() { shapeTextRangeCalls.set(0L) @@ -632,35 +634,35 @@ object FontRegistry { metaResourcePath = "fonts/minecraft/MinecraftDefault-Regular-meta.json", atlasResourcePath = "fonts/minecraft/MinecraftDefault-Regular-mtsdf.rgba.deflate", ttfResourcePath = "fonts/minecraft/MinecraftDefault-Regular.ttf", - source = FontAssetSource.Jar + source = FontAssetSource.Jar, ) registerMsdf( fontId = FONT_UBUNTU, metaResourcePath = "fonts/ubuntu/Ubuntu-Regular-meta.json", atlasResourcePath = "fonts/ubuntu/Ubuntu-Regular-mtsdf.rgba.deflate", ttfResourcePath = "fonts/ubuntu/Ubuntu-Regular.ttf", - source = FontAssetSource.Jar + source = FontAssetSource.Jar, ) registerMsdf( fontId = FONT_NOTO_SANS, metaResourcePath = "fonts/noto/Noto_Sans/NotoSans-Regular-meta.json", atlasResourcePath = "fonts/noto/Noto_Sans/NotoSans-Regular-mtsdf.rgba.deflate", ttfResourcePath = "fonts/noto/Noto_Sans/NotoSans-Regular.ttf", - source = FontAssetSource.Jar + source = FontAssetSource.Jar, ) registerMsdf( fontId = FONT_JB_MONO, metaResourcePath = "fonts/jetbrains_mono/JetBrainsMono-Regular-meta.json", atlasResourcePath = "fonts/jetbrains_mono/JetBrainsMono-Regular-mtsdf.rgba.deflate", ttfResourcePath = "fonts/jetbrains_mono/JetBrainsMono-Regular.ttf", - source = FontAssetSource.Jar + source = FontAssetSource.Jar, ) registerMsdf( fontId = TELEGRAFICO, metaResourcePath = "fonts/telegrafico/telegrafico-meta.json", atlasResourcePath = "fonts/telegrafico/telegrafico-mtsdf.rgba.deflate", ttfResourcePath = "fonts/telegrafico/telegrafico.ttf", - source = FontAssetSource.Jar + source = FontAssetSource.Jar, ) loadGeneratedFontManifest() defaultsRegistered = true @@ -671,16 +673,29 @@ object FontRegistry { fontId: FontId, metaFile: Path, atlasFile: Path, - ttfFile: Path + ttfFile: Path, ) { - descriptors[fontId] = MsdfFontResource( - fontId = fontId, - source = FontAssetSource.External, - pathMode = FontPathMode.FileSystem, - metaPath = metaFile.toAbsolutePath().normalize().toString(), - atlasPath = atlasFile.toAbsolutePath().normalize().toString(), - ttfPath = ttfFile.toAbsolutePath().normalize().toString() - ) + descriptors[fontId] = + MsdfFontResource( + fontId = fontId, + source = FontAssetSource.External, + pathMode = FontPathMode.FileSystem, + metaPath = + metaFile + .toAbsolutePath() + .normalize() + .toString(), + atlasPath = + atlasFile + .toAbsolutePath() + .normalize() + .toString(), + ttfPath = + ttfFile + .toAbsolutePath() + .normalize() + .toString(), + ) onDescriptorChanged(fontId) } @@ -714,39 +729,40 @@ object FontRegistry { } val metaRaw = String(metaBytes, StandardCharsets.UTF_8) - val meta = runCatching { MsdfFontMetaParser.parse(metaRaw) } - .onFailure { error -> - failFontLoad( - descriptor, - "Failed to parse metadata '${descriptor.metaPath}': ${error.message ?: error.javaClass.simpleName}" - ) - } - .getOrNull() ?: return null - - val awtBase = descriptor.ttfPath?.let { ttfPath -> - val ttfBytes = readBytes(ttfPath, descriptor.pathMode) - if (ttfBytes == null) { - failFontLoad(descriptor, "Missing TTF '$ttfPath'") - return null - } - runCatching { loadAwtFont(ttfBytes) } + val meta = + runCatching { MsdfFontMetaParser.parse(metaRaw) } .onFailure { error -> failFontLoad( descriptor, - "Failed to load TTF '$ttfPath': ${error.message ?: error.javaClass.simpleName}" + "Failed to parse metadata '${descriptor.metaPath}': ${error.message ?: error.javaClass.simpleName}", ) + }.getOrNull() ?: return null + + val awtBase = + descriptor.ttfPath?.let { ttfPath -> + val ttfBytes = readBytes(ttfPath, descriptor.pathMode) + if (ttfBytes == null) { + failFontLoad(descriptor, "Missing TTF '$ttfPath'") + return null } - .getOrNull() ?: return null - } + runCatching { loadAwtFont(ttfBytes) } + .onFailure { error -> + failFontLoad( + descriptor, + "Failed to load TTF '$ttfPath': ${error.message ?: error.javaClass.simpleName}", + ) + }.getOrNull() ?: return null + } - val loadedFont = LoadedMsdfFont( - descriptor = descriptor, - meta = meta, - awtBaseFont = awtBase, - preferredMissingGlyphIndex = computeGlyphIndexForCodepoint(awtBase, 0xFFFD), - preferredQuestionGlyphIndex = computeGlyphIndexForCodepoint(awtBase, '?'.code), - atlasPayload = AtlasPayload(atlasBytes) - ) + val loadedFont = + LoadedMsdfFont( + descriptor = descriptor, + meta = meta, + awtBaseFont = awtBase, + preferredMissingGlyphIndex = computeGlyphIndexForCodepoint(awtBase, 0xFFFD), + preferredQuestionGlyphIndex = computeGlyphIndexForCodepoint(awtBase, '?'.code), + atlasPayload = AtlasPayload(atlasBytes), + ) loaded[descriptor.fontId] = loadedFont failedLoads.remove(descriptor.fontId) return loadedFont @@ -756,7 +772,9 @@ object FontRegistry { failedLoads.add(descriptor.fontId) logParseErrorRateLimited( key = "font-load:${descriptor.fontId}", - message = "[DSGL-MSDF] Failed to load font '${descriptor.fontId}' (${descriptor.source.name.lowercase()}): $reason" + message = + "[DSGL-MSDF] Failed to load font '${descriptor.fontId}' " + + "(${descriptor.source.name.lowercase()}): $reason", ) } @@ -773,9 +791,7 @@ object FontRegistry { } } - private fun normalizeRelativePath(path: String): String { - return FontDiscovery.normalizeRelativePath(path) - } + private fun normalizeRelativePath(path: String): String = FontDiscovery.normalizeRelativePath(path) private fun loadGeneratedFontManifest() { val manifestPath = "fonts/generated-fonts.txt" @@ -797,15 +813,16 @@ object FontRegistry { endIndexExclusive: Int, primary: LoadedMsdfFont, fallback: LoadedMsdfFont?, - fontPx: Int + fontPx: Int, ): ShapedText { - val segments = buildShapingSegments( - text = text, - startIndex = startIndex, - endIndexExclusive = endIndexExclusive, - primary = primary, - fallback = fallback - ) + val segments = + buildShapingSegments( + text = text, + startIndex = startIndex, + endIndexExclusive = endIndexExclusive, + primary = primary, + fallback = fallback, + ) if (segments.isEmpty()) { return ShapedText(glyphs = emptyList(), runs = emptyList(), width = 0f) } @@ -814,13 +831,14 @@ object FontRegistry { var penX = 0f segments.forEach { segment -> - val shapedRun = shapeSegment( - sourceText = text, - sourceRangeStart = startIndex, - segment = segment, - fontPx = fontPx, - penX = penX - ) + val shapedRun = + shapeSegment( + sourceText = text, + sourceRangeStart = startIndex, + segment = segment, + fontPx = fontPx, + penX = penX, + ) allGlyphs += shapedRun.glyphs runs += shapedRun penX += shapedRun.advance @@ -828,7 +846,7 @@ object FontRegistry { return ShapedText( glyphs = allGlyphs, runs = runs, - width = penX.coerceAtLeast(0f) + width = penX.coerceAtLeast(0f), ) } @@ -837,7 +855,7 @@ object FontRegistry { startIndex: Int, endIndexExclusive: Int, primary: LoadedMsdfFont, - fallback: LoadedMsdfFont? + fallback: LoadedMsdfFont?, ): List { val out = ArrayList(4) var segment: MutableShapingSegment? = null @@ -852,16 +870,18 @@ object FontRegistry { val primaryNeedsReplacement = requiresReplacementGlyph(primary, codepoint) val fallbackNeedsReplacement = fallback?.let { requiresReplacementGlyph(it, codepoint) } ?: true - val selectedFont = when { - !primaryNeedsReplacement -> primary - fallback != null && !fallbackNeedsReplacement -> fallback - fallback != null -> fallback - else -> primary - } - val replacementNeeded = when (selectedFont.descriptor.fontId) { - primary.descriptor.fontId -> primaryNeedsReplacement - else -> fallbackNeedsReplacement - } + val selectedFont = + when { + !primaryNeedsReplacement -> primary + fallback != null && !fallbackNeedsReplacement -> fallback + fallback != null -> fallback + else -> primary + } + val replacementNeeded = + when (selectedFont.descriptor.fontId) { + primary.descriptor.fontId -> primaryNeedsReplacement + else -> fallbackNeedsReplacement + } if (segment == null || segment.font.descriptor.fontId != selectedFont.descriptor.fontId) { segment = MutableShapingSegment(font = selectedFont) @@ -890,7 +910,7 @@ object FontRegistry { sourceRangeStart: Int, segment: MutableShapingSegment, fontPx: Int, - penX: Float + penX: Float, ): ShapedTextRun { shapeSegmentCalls.incrementAndGet() val font = deriveAwtFont(segment.font, fontPx) @@ -900,50 +920,58 @@ object FontRegistry { charStart = if (segment.charStart == Int.MAX_VALUE) 0 else segment.charStart, charEnd = segment.charEnd, glyphs = emptyList(), - advance = 0f + advance = 0f, ) } - val chars = segment.text.toString().toCharArray() - val glyphVector = runCatching { - font.layoutGlyphVector( - fontRenderContext, - chars, - 0, - chars.size, - Font.LAYOUT_LEFT_TO_RIGHT - ) - }.getOrElse { - font.createGlyphVector(fontRenderContext, segment.text.toString()) - } + val chars = + segment.text + .toString() + .toCharArray() + val glyphVector = + runCatching { + font.layoutGlyphVector( + fontRenderContext, + chars, + 0, + chars.size, + Font.LAYOUT_LEFT_TO_RIGHT, + ) + }.getOrElse { + font.createGlyphVector(fontRenderContext, segment.text.toString()) + } val glyphCount = glyphVector.numGlyphs val positions = glyphVector.getGlyphPositions(0, glyphCount + 1, null) val runGlyphs = ArrayList(glyphCount) for (i in 0 until glyphCount) { - val charIndex = glyphVector.getGlyphCharIndex(i) - .coerceIn(0, (segment.sourceStartByChar.size - 1).coerceAtLeast(0)) + val charIndex = + glyphVector + .getGlyphCharIndex(i) + .coerceIn(0, (segment.sourceStartByChar.size - 1).coerceAtLeast(0)) val sourceStart = segment.sourceStartByChar.getOrElse(charIndex) { 0 } val sourceEnd = segment.sourceEndByChar.getOrElse(charIndex) { sourceStart + 1 } val sourceGlobalStart = sourceRangeStart + sourceStart - val sourceCodepoint = if (charIndex in chars.indices) { - Character.codePointAt(chars, charIndex, chars.size) - } else if (sourceGlobalStart in sourceText.indices) { - Character.codePointAt(sourceText, sourceGlobalStart) - } else { - '?'.code - } + val sourceCodepoint = + if (charIndex in chars.indices) { + Character.codePointAt(chars, charIndex, chars.size) + } else if (sourceGlobalStart in sourceText.indices) { + Character.codePointAt(sourceText, sourceGlobalStart) + } else { + '?'.code + } val x = (penX + positions[i * 2]) val y = positions[i * 2 + 1] val advance = (positions[(i + 1) * 2] - positions[i * 2]) - runGlyphs += ShapedGlyph( - fontId = segment.font.descriptor.fontId, - glyphIndex = glyphVector.getGlyphCode(i), - x = x, - y = y, - advance = advance, - charStart = sourceStart, - charEnd = sourceEnd, - sourceCodepoint = sourceCodepoint - ) + runGlyphs += + ShapedGlyph( + fontId = segment.font.descriptor.fontId, + glyphIndex = glyphVector.getGlyphCode(i), + x = x, + y = y, + advance = advance, + charStart = sourceStart, + charEnd = sourceEnd, + sourceCodepoint = sourceCodepoint, + ) } val runAdvance = positions[glyphCount * 2].coerceAtLeast(0f) return ShapedTextRun( @@ -951,7 +979,7 @@ object FontRegistry { charStart = if (segment.charStart == Int.MAX_VALUE) 0 else segment.charStart, charEnd = segment.charEnd, glyphs = runGlyphs, - advance = runAdvance + advance = runAdvance, ) } @@ -987,23 +1015,24 @@ object FontRegistry { } requiresReplacementGlyphCacheMisses.incrementAndGet() requiresReplacementGlyphEvaluations.incrementAndGet() - val resolved = if (!Character.isValidCodePoint(codepoint)) { - true - } else if (!canDisplay(font, codepoint)) { - true - } else { - val glyphIndex = glyphIndexForCodepoint(font, codepoint) - if (glyphIndex == null || glyphIndex < 0) { + val resolved = + if (!Character.isValidCodePoint(codepoint)) { + true + } else if (!canDisplay(font, codepoint)) { true } else { - val missingIndex = font.preferredMissingGlyphIndex - if (missingIndex != null && glyphIndex == missingIndex) { + val glyphIndex = glyphIndexForCodepoint(font, codepoint) + if (glyphIndex == null || glyphIndex < 0) { true } else { - glyphIndex == 0 && (missingIndex == null || missingIndex == 0) + val missingIndex = font.preferredMissingGlyphIndex + if (missingIndex != null && glyphIndex == missingIndex) { + true + } else { + glyphIndex == 0 && (missingIndex == null || missingIndex == 0) + } } } - } requiresReplacementGlyphByFontCodepoint[key] = resolved return resolved } @@ -1036,11 +1065,10 @@ object FontRegistry { }.getOrNull() } - private fun loadAwtFont(ttfBytes: ByteArray): Font { - return ByteArrayInputStream(ttfBytes).use { input -> + private fun loadAwtFont(ttfBytes: ByteArray): Font = + ByteArrayInputStream(ttfBytes).use { input -> Font.createFont(Font.TRUETYPE_FONT, input) } - } private fun normalizeRange(text: String, startIndex: Int, endIndexExclusive: Int): Pair { var start = startIndex.coerceIn(0, text.length) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/MsdfFontMeta.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/MsdfFontMeta.kt index 21e720c..34cd873 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/MsdfFontMeta.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/MsdfFontMeta.kt @@ -11,7 +11,7 @@ data class MsdfAtlasInfo( val size: Float, val width: Int, val height: Int, - val yOrigin: String + val yOrigin: String, ) data class MsdfMetrics( @@ -20,21 +20,21 @@ data class MsdfMetrics( val ascender: Float, val descender: Float, val underlineY: Float? = null, - val underlineThickness: Float? = null + val underlineThickness: Float? = null, ) data class MsdfPlaneBounds( val left: Float, val bottom: Float, val right: Float, - val top: Float + val top: Float, ) data class MsdfAtlasBounds( val left: Float, val bottom: Float, val right: Float, - val top: Float + val top: Float, ) data class MsdfGlyph( @@ -42,7 +42,7 @@ data class MsdfGlyph( val codepoint: Int?, val advance: Float, val planeBounds: MsdfPlaneBounds?, - val atlasBounds: MsdfAtlasBounds? + val atlasBounds: MsdfAtlasBounds?, ) { val drawable: Boolean get() = planeBounds != null && atlasBounds != null @@ -51,7 +51,7 @@ data class MsdfGlyph( data class MsdfKerningPair( val leftGlyphIndex: Int, val rightGlyphIndex: Int, - val advance: Float + val advance: Float, ) data class MsdfGlyphRunItem( @@ -60,7 +60,7 @@ data class MsdfGlyphRunItem( val glyph: MsdfGlyph?, val advancePx: Float, val draw: Boolean, - val usedFallback: Boolean + val usedFallback: Boolean, ) data class MsdfFontMeta( @@ -71,32 +71,35 @@ data class MsdfFontMeta( val kerningPairsByIndex: Map, val kerningPairsByCodepoint: Map, val replacementGlyphIndex: Int?, - val replacementCodepoint: Int? + val replacementCodepoint: Int?, ) { private val tabWidthInSpaces: Int = 4 - private val denseGlyphsByIndex: Array = run { - val maxIndex = glyphsByIndex.keys.maxOrNull() ?: -1 - if (maxIndex < 0) { - emptyArray() - } else { - arrayOfNulls(maxIndex + 1).also { dense -> - glyphsByIndex.forEach { (index, glyph) -> - if (index >= 0 && index < dense.size) { - dense[index] = glyph + private val denseGlyphsByIndex: Array = + run { + val maxIndex = glyphsByIndex.keys.maxOrNull() ?: -1 + if (maxIndex < 0) { + emptyArray() + } else { + arrayOfNulls(maxIndex + 1).also { dense -> + glyphsByIndex.forEach { (index, glyph) -> + if (index >= 0 && index < dense.size) { + dense[index] = glyph + } } } } } - } - private val cachedFallbackGlyph: MsdfGlyph? = run { - val replacementByIndex = replacementGlyphIndex - ?.takeIf { it >= 0 && it < denseGlyphsByIndex.size } - ?.let { denseGlyphsByIndex[it] } - if (replacementByIndex != null) return@run replacementByIndex - val replacementByCodepoint = replacementCodepoint?.let(glyphsByCodepoint::get) - if (replacementByCodepoint != null) return@run replacementByCodepoint - glyphsByIndex.values.firstOrNull() ?: glyphsByCodepoint.values.firstOrNull() - } + private val cachedFallbackGlyph: MsdfGlyph? = + run { + val replacementByIndex = + replacementGlyphIndex + ?.takeIf { it >= 0 && it < denseGlyphsByIndex.size } + ?.let { denseGlyphsByIndex[it] } + if (replacementByIndex != null) return@run replacementByIndex + val replacementByCodepoint = replacementCodepoint?.let(glyphsByCodepoint::get) + if (replacementByCodepoint != null) return@run replacementByCodepoint + glyphsByIndex.values.firstOrNull() ?: glyphsByCodepoint.values.firstOrNull() + } fun glyphByIndex(glyphIndex: Int): MsdfGlyph? { if (glyphIndex >= 0 && glyphIndex < denseGlyphsByIndex.size) { @@ -107,13 +110,9 @@ data class MsdfFontMeta( fun glyph(codepoint: Int): MsdfGlyph? = glyphsByCodepoint[codepoint] - fun fallbackGlyph(): MsdfGlyph? { - return cachedFallbackGlyph - } + fun fallbackGlyph(): MsdfGlyph? = cachedFallbackGlyph - fun glyphOrFallback(codepoint: Int): MsdfGlyph? { - return glyph(codepoint) ?: fallbackGlyph() - } + fun glyphOrFallback(codepoint: Int): MsdfGlyph? = glyph(codepoint) ?: fallbackGlyph() fun resolveGlyphRun(codepoint: Int, fontSizePx: Int): MsdfGlyphRunItem? { val size = fontSizePx.coerceAtLeast(1) @@ -126,7 +125,7 @@ data class MsdfFontMeta( glyph = explicitSpaceGlyph, advancePx = spaceAdvancePx(size), draw = false, - usedFallback = false + usedFallback = false, ) } @@ -137,7 +136,7 @@ data class MsdfFontMeta( glyph = null, advancePx = spaceAdvancePx(size) * tabWidthInSpaces, draw = false, - usedFallback = false + usedFallback = false, ) } @@ -150,7 +149,7 @@ data class MsdfFontMeta( glyph = direct, advancePx = advancePx(direct, size), draw = direct.drawable, - usedFallback = false + usedFallback = false, ) } val fallback = fallbackGlyph() ?: return null @@ -160,7 +159,7 @@ data class MsdfFontMeta( glyph = fallback, advancePx = advancePx(fallback, size), draw = fallback.drawable, - usedFallback = true + usedFallback = true, ) } } @@ -189,17 +188,13 @@ data class MsdfFontMeta( return ceil(metrics.lineHeight * scale).toInt().coerceAtLeast(1) } - fun advancePx(glyph: MsdfGlyph, fontSizePx: Int): Float { - return glyph.advance * scalePx(fontSizePx) - } + fun advancePx(glyph: MsdfGlyph, fontSizePx: Int): Float = glyph.advance * scalePx(fontSizePx) - fun kerningPx(leftCodepoint: Int, rightCodepoint: Int, fontSizePx: Int): Float { - return kerning(leftCodepoint, rightCodepoint) * scalePx(fontSizePx) - } + fun kerningPx(leftCodepoint: Int, rightCodepoint: Int, fontSizePx: Int): Float = + kerning(leftCodepoint, rightCodepoint) * scalePx(fontSizePx) - fun kerningPxByIndex(leftGlyphIndex: Int, rightGlyphIndex: Int, fontSizePx: Int): Float { - return kerningByIndex(leftGlyphIndex, rightGlyphIndex) * scalePx(fontSizePx) - } + fun kerningPxByIndex(leftGlyphIndex: Int, rightGlyphIndex: Int, fontSizePx: Int): Float = + kerningByIndex(leftGlyphIndex, rightGlyphIndex) * scalePx(fontSizePx) fun measureTextWidth(text: String, fontSizePx: Int): Int { if (text.isEmpty()) return 0 @@ -258,9 +253,6 @@ data class MsdfFontMeta( } companion object { - fun kerningKey(left: Int, right: Int): Long { - return (left.toLong() shl 32) or (right.toLong() and 0xFFFF_FFFFL) - } + fun kerningKey(left: Int, right: Int): Long = (left.toLong() shl 32) or (right.toLong() and 0xFFFF_FFFFL) } } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/MsdfFontMetaParser.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/MsdfFontMetaParser.kt index e9fe4d1..479f804 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/MsdfFontMetaParser.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/MsdfFontMetaParser.kt @@ -3,55 +3,60 @@ import kotlinx.serialization.json.Json object MsdfFontMetaParser { - val json: Json = Json { - ignoreUnknownKeys = true - isLenient = true - explicitNulls = false - } + val json: Json = + Json { + ignoreUnknownKeys = true + isLenient = true + explicitNulls = false + } fun parse(rawJson: String): MsdfFontMeta { val parsed = json.decodeFromString(rawJson) validate(parsed) - val atlas = MsdfAtlasInfo( - type = parsed.atlas.type, - distanceRange = parsed.atlas.distanceRange, - size = parsed.atlas.size, - width = parsed.atlas.width, - height = parsed.atlas.height, - yOrigin = parsed.atlas.yOrigin - ) + val atlas = + MsdfAtlasInfo( + type = parsed.atlas.type, + distanceRange = parsed.atlas.distanceRange, + size = parsed.atlas.size, + width = parsed.atlas.width, + height = parsed.atlas.height, + yOrigin = parsed.atlas.yOrigin, + ) - val metrics = MsdfMetrics( - emSize = parsed.metrics.emSize, - lineHeight = parsed.metrics.lineHeight, - ascender = parsed.metrics.ascender, - descender = parsed.metrics.descender, - underlineY = parsed.metrics.underlineY, - underlineThickness = parsed.metrics.underlineThickness - ) + val metrics = + MsdfMetrics( + emSize = parsed.metrics.emSize, + lineHeight = parsed.metrics.lineHeight, + ascender = parsed.metrics.ascender, + descender = parsed.metrics.descender, + underlineY = parsed.metrics.underlineY, + underlineThickness = parsed.metrics.underlineThickness, + ) val anyExplicitGlyphIndex = parsed.glyphs.any { it.glyphIndex >= 0 } val glyphsByIndex = linkedMapOf() val glyphsByCodepoint = linkedMapOf() parsed.glyphs.forEachIndexed { listIndex, glyph -> - val resolvedGlyphIndex = when { - glyph.glyphIndex >= 0 -> glyph.glyphIndex - !anyExplicitGlyphIndex -> listIndex - else -> throw IllegalArgumentException( - "Glyph metadata has mixed explicit/implicit glyph indices. " + - "Provide a stable glyph index field for every glyph entry." + val resolvedGlyphIndex = + when { + glyph.glyphIndex >= 0 -> glyph.glyphIndex + !anyExplicitGlyphIndex -> listIndex + else -> throw IllegalArgumentException( + "Glyph metadata has mixed explicit/implicit glyph indices. " + + "Provide a stable glyph index field for every glyph entry.", + ) + } + + val runtimeGlyph = + MsdfGlyph( + glyphIndex = resolvedGlyphIndex, + codepoint = glyph.unicodeCodepoint, + advance = glyph.advance, + planeBounds = glyph.planeBounds?.toRuntime(), + atlasBounds = glyph.atlasBounds?.toRuntime(), ) - } - - val runtimeGlyph = MsdfGlyph( - glyphIndex = resolvedGlyphIndex, - codepoint = glyph.unicodeCodepoint, - advance = glyph.advance, - planeBounds = glyph.planeBounds?.toRuntime(), - atlasBounds = glyph.atlasBounds?.toRuntime() - ) val previous = glyphsByIndex.putIfAbsent(resolvedGlyphIndex, runtimeGlyph) if (previous != null) { @@ -78,16 +83,18 @@ object MsdfFontMetaParser { } } - val replacementCodepoint = when { - glyphsByCodepoint.containsKey(0xFFFD) -> 0xFFFD - glyphsByCodepoint.containsKey('?'.code) -> '?'.code - else -> glyphsByCodepoint.keys.firstOrNull() - } - val replacementGlyphIndex = when { - replacementCodepoint != null -> glyphsByCodepoint[replacementCodepoint]?.glyphIndex - glyphsByIndex.containsKey(0) -> 0 - else -> glyphsByIndex.keys.firstOrNull() - } + val replacementCodepoint = + when { + glyphsByCodepoint.containsKey(0xFFFD) -> 0xFFFD + glyphsByCodepoint.containsKey('?'.code) -> '?'.code + else -> glyphsByCodepoint.keys.firstOrNull() + } + val replacementGlyphIndex = + when { + replacementCodepoint != null -> glyphsByCodepoint[replacementCodepoint]?.glyphIndex + glyphsByIndex.containsKey(0) -> 0 + else -> glyphsByIndex.keys.firstOrNull() + } return MsdfFontMeta( atlas = atlas, @@ -97,7 +104,7 @@ object MsdfFontMetaParser { kerningPairsByIndex = kerningByIndex, kerningPairsByCodepoint = kerningByCodepoint, replacementGlyphIndex = replacementGlyphIndex, - replacementCodepoint = replacementCodepoint + replacementCodepoint = replacementCodepoint, ) } @@ -113,22 +120,19 @@ object MsdfFontMetaParser { } } - private fun MsdfPlaneBoundsJson.toRuntime(): MsdfPlaneBounds { - return MsdfPlaneBounds( + private fun MsdfPlaneBoundsJson.toRuntime(): MsdfPlaneBounds = + MsdfPlaneBounds( left = left, bottom = bottom, right = right, - top = top + top = top, ) - } - private fun MsdfAtlasBoundsJson.toRuntime(): MsdfAtlasBounds { - return MsdfAtlasBounds( + private fun MsdfAtlasBoundsJson.toRuntime(): MsdfAtlasBounds = + MsdfAtlasBounds( left = left, bottom = bottom, right = right, - top = top + top = top, ) - } } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/MsdfMetaJson.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/MsdfMetaJson.kt index f9ca831..5a8e467 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/MsdfMetaJson.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/MsdfMetaJson.kt @@ -1,8 +1,8 @@ package org.dreamfinity.dsgl.core.font +import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor @@ -11,8 +11,8 @@ import kotlinx.serialization.descriptors.element import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonDecoder -import kotlinx.serialization.json.JsonEncoder import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonEncoder import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.doubleOrNull @@ -25,7 +25,7 @@ internal data class MsdfMetaJson( val atlas: MsdfAtlasJson = MsdfAtlasJson(), val metrics: MsdfMetricsJson = MsdfMetricsJson(), val glyphs: List = emptyList(), - val kerning: List = emptyList() + val kerning: List = emptyList(), ) @Serializable @@ -39,7 +39,7 @@ internal data class MsdfAtlasJson( val width: Int = 0, @Serializable(with = NumberAsIntSerializer::class) val height: Int = 0, - val yOrigin: String = "bottom" + val yOrigin: String = "bottom", ) @Serializable @@ -53,7 +53,7 @@ internal data class MsdfMetricsJson( @Serializable(with = NumberAsFloatSerializer::class) val descender: Float = 0f, val underlineY: Float? = null, - val underlineThickness: Float? = null + val underlineThickness: Float? = null, ) @Serializable(with = MsdfGlyphJsonSerializer::class) @@ -63,7 +63,7 @@ internal data class MsdfGlyphJson( @Serializable(with = NumberAsFloatSerializer::class) val advance: Float = 0f, val planeBounds: MsdfPlaneBoundsJson? = null, - val atlasBounds: MsdfAtlasBoundsJson? = null + val atlasBounds: MsdfAtlasBoundsJson? = null, ) @Serializable @@ -75,7 +75,7 @@ internal data class MsdfPlaneBoundsJson( @Serializable(with = NumberAsFloatSerializer::class) val right: Float = 0f, @Serializable(with = NumberAsFloatSerializer::class) - val top: Float = 0f + val top: Float = 0f, ) @Serializable @@ -87,7 +87,7 @@ internal data class MsdfAtlasBoundsJson( @Serializable(with = NumberAsFloatSerializer::class) val right: Float = 0f, @Serializable(with = NumberAsFloatSerializer::class) - val top: Float = 0f + val top: Float = 0f, ) @Serializable @@ -99,7 +99,7 @@ internal data class MsdfKerningJson( @Serializable(with = NumberAsIntSerializer::class) val rightGlyphIndex: Int = 0, @Serializable(with = NumberAsFloatSerializer::class) - val advance: Float = 0f + val advance: Float = 0f, ) internal object NumberAsFloatSerializer : KSerializer { @@ -107,8 +107,9 @@ internal object NumberAsFloatSerializer : KSerializer { PrimitiveSerialDescriptor("NumberAsFloat", PrimitiveKind.FLOAT) override fun deserialize(decoder: Decoder): Float { - val primitive = (decoder as? JsonDecoder)?.decodeJsonElement()?.jsonPrimitive - ?: return decoder.decodeFloat() + val primitive = + (decoder as? JsonDecoder)?.decodeJsonElement()?.jsonPrimitive + ?: return decoder.decodeFloat() return primitive.toFloatStrict() } @@ -122,8 +123,9 @@ internal object NumberAsIntSerializer : KSerializer { PrimitiveSerialDescriptor("NumberAsInt", PrimitiveKind.INT) override fun deserialize(decoder: Decoder): Int { - val primitive = (decoder as? JsonDecoder)?.decodeJsonElement()?.jsonPrimitive - ?: return decoder.decodeInt() + val primitive = + (decoder as? JsonDecoder)?.decodeJsonElement()?.jsonPrimitive + ?: return decoder.decodeInt() return primitive.toIntStrict() } @@ -141,39 +143,44 @@ private fun JsonPrimitive.toFloatStrict(): Float { } internal object MsdfGlyphJsonSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("MsdfGlyphJson") { - element("glyphIndex", isOptional = true) - element("unicodeCodepoint", isOptional = true) - element("advance", isOptional = true) - element("planeBounds", isOptional = true) - element("atlasBounds", isOptional = true) - } + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("MsdfGlyphJson") { + element("glyphIndex", isOptional = true) + element("unicodeCodepoint", isOptional = true) + element("advance", isOptional = true) + element("planeBounds", isOptional = true) + element("atlasBounds", isOptional = true) + } override fun deserialize(decoder: Decoder): MsdfGlyphJson { - val jsonDecoder = decoder as? JsonDecoder - ?: throw IllegalArgumentException("MsdfGlyphJsonSerializer requires JsonDecoder") + val jsonDecoder = + decoder as? JsonDecoder + ?: throw IllegalArgumentException("MsdfGlyphJsonSerializer requires JsonDecoder") val obj = jsonDecoder.decodeJsonElement().jsonObject val glyphIndex = resolveGlyphIndex(obj) val unicodeCodepoint = resolveUnicodeCodepoint(obj) val advance = obj["advance"]?.jsonPrimitive?.toFloatStrict() ?: 0f - val plane = obj["planeBounds"]?.let { - jsonDecoder.json.decodeFromJsonElement(MsdfPlaneBoundsJson.serializer(), it) - } - val atlas = obj["atlasBounds"]?.let { - jsonDecoder.json.decodeFromJsonElement(MsdfAtlasBoundsJson.serializer(), it) - } + val plane = + obj["planeBounds"]?.let { + jsonDecoder.json.decodeFromJsonElement(MsdfPlaneBoundsJson.serializer(), it) + } + val atlas = + obj["atlasBounds"]?.let { + jsonDecoder.json.decodeFromJsonElement(MsdfAtlasBoundsJson.serializer(), it) + } return MsdfGlyphJson( glyphIndex = glyphIndex, unicodeCodepoint = unicodeCodepoint, advance = advance, planeBounds = plane, - atlasBounds = atlas + atlasBounds = atlas, ) } override fun serialize(encoder: Encoder, value: MsdfGlyphJson) { - val jsonEncoder = encoder as? JsonEncoder - ?: throw IllegalArgumentException("MsdfGlyphJsonSerializer requires JsonEncoder") + val jsonEncoder = + encoder as? JsonEncoder + ?: throw IllegalArgumentException("MsdfGlyphJsonSerializer requires JsonEncoder") val map = linkedMapOf() if (value.glyphIndex >= 0) { map["index"] = JsonPrimitive(value.glyphIndex) @@ -231,4 +238,3 @@ private fun JsonPrimitive.floatOrNull(): Float? { if (direct != null) return direct return doubleOrNull?.toFloat() } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/UnicodeCodepoints.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/UnicodeCodepoints.kt index 0a1c4b8..7c7cd22 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/UnicodeCodepoints.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/UnicodeCodepoints.kt @@ -9,7 +9,10 @@ inline fun forEachCodepoint(text: CharSequence, action: (Int) -> Unit) { } } -inline fun forEachCodepointIndexed(text: CharSequence, action: (codepoint: Int, startIndex: Int, endIndex: Int) -> Unit) { +inline fun forEachCodepointIndexed( + text: CharSequence, + action: (codepoint: Int, startIndex: Int, endIndex: Int) -> Unit, +) { var index = 0 while (index < text.length) { val start = index diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ComponentHookRuntime.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ComponentHookRuntime.kt index e348abb..981d8e4 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ComponentHookRuntime.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ComponentHookRuntime.kt @@ -1,9 +1,9 @@ package org.dreamfinity.dsgl.core.hooks import org.dreamfinity.dsgl.core.DsglWindow -import kotlin.reflect.KType -import kotlin.reflect.KClass import java.util.ArrayDeque +import kotlin.reflect.KClass +import kotlin.reflect.KType internal enum class HookEntryKind { Ref, @@ -12,19 +12,21 @@ internal enum class HookEntryKind { Memo, Effect, CustomScope, - Custom + Custom, } enum class HookRenderSessionMode { Normal, - HotReload + HotReload, } class HookHotReloadRemountException( - message: String + message: String, ) : RuntimeException(message) -internal class HookUsageException(message: String) : IllegalStateException(message) +internal class HookUsageException( + message: String, +) : IllegalStateException(message) @PublishedApi internal sealed interface HookSignature { @@ -32,32 +34,32 @@ internal sealed interface HookSignature { } internal data class KindOnlyHookSignature( - val kind: HookEntryKind + val kind: HookEntryKind, ) : HookSignature { override fun diagnosticLabel(): String = "KindOnly($kind)" } internal data class StateHookSignature( - val valueType: KType + val valueType: KType, ) : HookSignature { override fun diagnosticLabel(): String = "State<$valueType>" } internal data class RefHookSignature( - val valueType: KType + val valueType: KType, ) : HookSignature { override fun diagnosticLabel(): String = "Ref<$valueType>" } internal data class MemoHookSignature( - val valueType: KType + val valueType: KType, ) : HookSignature { override fun diagnosticLabel(): String = "Memo<$valueType>" } internal data class ReducerHookSignature( val stateClass: KClass<*>?, - val initialWasNull: Boolean + val initialWasNull: Boolean, ) : HookSignature { override fun diagnosticLabel(): String { val classLabel = stateClass?.qualifiedName ?: "unknown" @@ -68,11 +70,11 @@ internal data class ReducerHookSignature( internal enum class EffectHookRunMode { OnDependencyChange, - EveryCommit + EveryCommit, } internal data class EffectHookSignature( - val runMode: EffectHookRunMode + val runMode: EffectHookRunMode, ) : HookSignature { override fun diagnosticLabel(): String = "Effect<$runMode>" } @@ -92,19 +94,18 @@ internal object HookSignatures { internal fun memo(valueType: KType): HookSignature = MemoHookSignature(valueType) @PublishedApi - internal fun reducer(stateClass: KClass<*>?, initialWasNull: Boolean): HookSignature { - return ReducerHookSignature( + internal fun reducer(stateClass: KClass<*>?, initialWasNull: Boolean): HookSignature = + ReducerHookSignature( stateClass = stateClass, - initialWasNull = initialWasNull + initialWasNull = initialWasNull, ) - } @PublishedApi internal fun effect(runMode: EffectHookRunMode): HookSignature = EffectHookSignature(runMode) } internal data class HookPath( - val segments: List + val segments: List, ) { init { require(segments.isNotEmpty()) { "Hook path must contain at least one segment." } @@ -114,11 +115,9 @@ internal data class HookPath( } internal class OwnerIdentity( - private val owner: Any + private val owner: Any, ) { - override fun equals(other: Any?): Boolean { - return other is OwnerIdentity && owner === other.owner - } + override fun equals(other: Any?): Boolean = other is OwnerIdentity && owner === other.owner override fun hashCode(): Int = System.identityHashCode(owner) } @@ -128,46 +127,46 @@ internal sealed interface ComponentDiscriminator { } private data class ExplicitKeyDiscriminator( - val key: Any + val key: Any, ) : ComponentDiscriminator { override fun debugLabel(): String = "key=$key" } private data class PositionalIndexDiscriminator( - val index: Int + val index: Int, ) : ComponentDiscriminator { override fun debugLabel(): String = "#$index" } internal data class ComponentIdentitySegment( val name: String, - private val discriminator: ComponentDiscriminator + private val discriminator: ComponentDiscriminator, ) { fun debugLabel(): String = "$name[${discriminator.debugLabel()}]" } internal data class ComponentInstanceId( private val ownerIdentity: OwnerIdentity, - val segments: List + val segments: List, ) { companion object { - fun root(owner: Any): ComponentInstanceId { - return ComponentInstanceId( + fun root(owner: Any): ComponentInstanceId = + ComponentInstanceId( ownerIdentity = OwnerIdentity(owner), - segments = emptyList() + segments = emptyList(), ) - } } fun child(name: String, explicitKey: Any?, positionalIndex: Int): ComponentInstanceId { - val discriminator = if (explicitKey != null) { - ExplicitKeyDiscriminator(explicitKey) - } else { - PositionalIndexDiscriminator(positionalIndex) - } + val discriminator = + if (explicitKey != null) { + ExplicitKeyDiscriminator(explicitKey) + } else { + PositionalIndexDiscriminator(positionalIndex) + } return ComponentInstanceId( ownerIdentity = ownerIdentity, - segments = segments + ComponentIdentitySegment(name, discriminator) + segments = segments + ComponentIdentitySegment(name, discriminator), ) } @@ -192,7 +191,7 @@ internal data class HookEntry( val signature: HookSignature, var value: Any?, val synthetic: Boolean, - val createdRenderEpoch: Long + val createdRenderEpoch: Long, ) { var lastVisitedRenderEpoch: Long = createdRenderEpoch } @@ -201,24 +200,24 @@ internal data class ResolvedHookEntry( val path: HookPath, val entry: HookEntry, val created: Boolean, - val synthetic: Boolean + val synthetic: Boolean, ) internal data class ResolvedTypedHookEntry( val path: HookPath, val value: T, val created: Boolean, - val synthetic: Boolean + val synthetic: Boolean, ) private data class SyntheticCounterKey( val scopeSegments: List, - val hookName: String + val hookName: String, ) private enum class HookCompatibilityMismatchReason { Kind, - Signature + Signature, } private data class HookCompatibilityMismatch( @@ -228,7 +227,7 @@ private data class HookCompatibilityMismatch( val requestedKind: HookEntryKind, val existingSignature: HookSignature, val requestedSignature: HookSignature, - val reason: HookCompatibilityMismatchReason + val reason: HookCompatibilityMismatchReason, ) { fun runtimeErrorMessage(): String { val guidance = "Use distinct delegated property names/scopes for semantically different hooks." @@ -247,22 +246,21 @@ private data class HookCompatibilityMismatch( } } - fun hotReloadWarningMessage(): String { - return "[DSGL][Hooks] Hot-reload remount/reset for component '${componentId.debugPath()}' due to " + + fun hotReloadWarningMessage(): String = + "[DSGL][Hooks] Hot-reload remount/reset for component '${componentId.debugPath()}' due to " + "incompatible hook at path '$path': " + "previous=${existingSignature.diagnosticLabel()} ($existingKind), " + "next=${requestedSignature.diagnosticLabel()} ($requestedKind). " + "Local hook state for this component subtree was reset. " + "Use distinct delegated property names/scopes for semantically different hooks." - } } private class HookCompatibilityMismatchException( - val mismatch: HookCompatibilityMismatch + val mismatch: HookCompatibilityMismatch, ) : RuntimeException() internal class ComponentHookContext( - val componentId: ComponentInstanceId + val componentId: ComponentInstanceId, ) { private val entriesByPath: MutableMap = linkedMapOf() private val claimedPathsThisRender: MutableSet = linkedSetOf() @@ -283,7 +281,7 @@ internal class ComponentHookContext( kind: HookEntryKind, signature: HookSignature, renderEpoch: Long, - initializer: () -> Any? + initializer: () -> Any?, ): ResolvedHookEntry { val path = HookPath(scopeSegments + delegateName) return resolvePath( @@ -292,7 +290,7 @@ internal class ComponentHookContext( signature = signature, synthetic = false, renderEpoch = renderEpoch, - initializer = initializer + initializer = initializer, ) } @@ -302,7 +300,7 @@ internal class ComponentHookContext( kind: HookEntryKind, signature: HookSignature, renderEpoch: Long, - initializer: () -> Any? + initializer: () -> Any?, ): ResolvedHookEntry { val counterKey = SyntheticCounterKey(scopeSegments = scopeSegments, hookName = hookName) val index = syntheticCounterByScope[counterKey] ?: 0 @@ -315,7 +313,7 @@ internal class ComponentHookContext( signature = signature, synthetic = true, renderEpoch = renderEpoch, - initializer = initializer + initializer = initializer, ) } @@ -337,16 +335,16 @@ internal class ComponentHookContext( signature: HookSignature, synthetic: Boolean, renderEpoch: Long, - initializer: () -> Any? + initializer: () -> Any?, ): ResolvedHookEntry { if (!claimedPathsThisRender.add(path)) { if (synthetic) { throw HookUsageException( - "Synthetic hook key collision at path '$path' in component '${componentId.debugPath()}'." + "Synthetic hook key collision at path '$path' in component '${componentId.debugPath()}'.", ) } throw HookUsageException( - "Duplicate hook path '$path' in component '${componentId.debugPath()}'." + "Duplicate hook path '$path' in component '${componentId.debugPath()}'.", ) } @@ -354,7 +352,7 @@ internal class ComponentHookContext( if (existing != null) { if (existing.synthetic != synthetic) { throw HookUsageException( - "Synthetic hook key collision at path '$path' in component '${componentId.debugPath()}'." + "Synthetic hook key collision at path '$path' in component '${componentId.debugPath()}'.", ) } if (existing.kind != kind) { @@ -366,8 +364,8 @@ internal class ComponentHookContext( requestedKind = kind, existingSignature = existing.signature, requestedSignature = signature, - reason = HookCompatibilityMismatchReason.Kind - ) + reason = HookCompatibilityMismatchReason.Kind, + ), ) } if (existing.signature != signature) { @@ -379,21 +377,22 @@ internal class ComponentHookContext( requestedKind = kind, existingSignature = existing.signature, requestedSignature = signature, - reason = HookCompatibilityMismatchReason.Signature - ) + reason = HookCompatibilityMismatchReason.Signature, + ), ) } existing.lastVisitedRenderEpoch = renderEpoch return ResolvedHookEntry(path = path, entry = existing, created = false, synthetic = synthetic) } - val created = HookEntry( - kind = kind, - signature = signature, - value = initializer(), - synthetic = synthetic, - createdRenderEpoch = renderEpoch - ) + val created = + HookEntry( + kind = kind, + signature = signature, + value = initializer(), + synthetic = synthetic, + createdRenderEpoch = renderEpoch, + ) created.lastVisitedRenderEpoch = renderEpoch entriesByPath[path] = created return ResolvedHookEntry(path = path, entry = created, created = true, synthetic = synthetic) @@ -404,30 +403,30 @@ internal class ComponentHookRuntime { internal data class StorageHookBindingToken( val hookName: String, val renderEpoch: Long, - var bound: Boolean = false + var bound: Boolean = false, ) private enum class ComponentFrameOrigin { Root, Explicit, - Inferred + Inferred, } private data class InferredComponentDescriptor( val ownerClassName: String, val methodName: String, - val componentName: String + val componentName: String, ) private data class HookCallSite( val className: String, val methodName: String, - val lineNumber: Int + val lineNumber: Int, ) private data class InferenceSnapshot( val descriptors: List, - val leafCallSite: HookCallSite? + val leafCallSite: HookCallSite?, ) private data class ComponentFrame( @@ -437,12 +436,12 @@ internal class ComponentHookRuntime { val positionalChildCounters: MutableMap = linkedMapOf(), val origin: ComponentFrameOrigin, val inferredDescriptor: InferredComponentDescriptor? = null, - val seenHookCallSitesThisRender: MutableSet = linkedSetOf() + val seenHookCallSitesThisRender: MutableSet = linkedSetOf(), ) private data class EffectRegistrationKey( val componentId: ComponentInstanceId, - val path: HookPath + val path: HookPath, ) private data class EffectRenderRegistration( @@ -450,18 +449,18 @@ internal class ComponentHookRuntime { val path: HookPath, val runMode: EffectHookRunMode, val deps: List, - val effect: (registerCleanup: (() -> Unit) -> Unit) -> Unit + val effect: (registerCleanup: (() -> Unit) -> Unit) -> Unit, ) private data class CommittedEffectState( val runMode: EffectHookRunMode, val deps: List, - val cleanup: (() -> Unit)? + val cleanup: (() -> Unit)?, ) private data class PendingEffectCommitBatch( val visitedComponents: Set, - val registrationsInOrder: List + val registrationsInOrder: List, ) private val contextsByComponent: MutableMap = linkedMapOf() @@ -469,6 +468,8 @@ internal class ComponentHookRuntime { private val componentStack: ArrayDeque = ArrayDeque() private val pendingStorageHookBindings: MutableList = arrayListOf() private val renderEffectRegistrations: MutableMap = linkedMapOf() + + @Suppress("ktlint:standard:max-line-length", "ktlint:standard:property-wrapping") private val committedEffectsByComponent: MutableMap> = linkedMapOf() private var pendingEffectCommitBatch: PendingEffectCommitBatch? = null @@ -500,8 +501,8 @@ internal class ComponentHookRuntime { ComponentFrame( componentId = rootId, context = rootContext, - origin = ComponentFrameOrigin.Root - ) + origin = ComponentFrameOrigin.Root, + ), ) } @@ -514,22 +515,23 @@ internal class ComponentHookRuntime { unwindInferredFramesForRenderEnd() - val unboundStorageHook = pendingStorageHookBindings.firstOrNull { token -> - token.renderEpoch == renderEpoch && !token.bound - } + val unboundStorageHook = + pendingStorageHookBindings.firstOrNull { token -> + token.renderEpoch == renderEpoch && !token.bound + } if (unboundStorageHook != null) { failStorageBackedHookWithoutDelegate(unboundStorageHook.hookName) } val current = componentStack.lastOrNull() if (componentStack.size != 1 || current == null) { throw HookUsageException( - "Invalid nested component scope behavior: render ended with unbalanced component scopes." + "Invalid nested component scope behavior: render ended with unbalanced component scopes.", ) } if (current.scopeSegments.isNotEmpty()) { throw HookUsageException( "Invalid nested hook scope behavior: render ended with unclosed custom hook scope " + - "'${current.scopeSegments.last()}'." + "'${current.scopeSegments.last()}'.", ) } @@ -547,32 +549,28 @@ internal class ComponentHookRuntime { } } - pendingEffectCommitBatch = PendingEffectCommitBatch( - visitedComponents = enteredComponentIdsThisRender.toSet(), - registrationsInOrder = renderEffectRegistrations.values.toList() - ) + pendingEffectCommitBatch = + PendingEffectCommitBatch( + visitedComponents = enteredComponentIdsThisRender.toSet(), + registrationsInOrder = renderEffectRegistrations.values.toList(), + ) clearRenderSessionStateOnly() } - fun registerEffectOnDependencyChange( - deps: List, - effect: (registerCleanup: (() -> Unit) -> Unit) -> Unit - ) { + fun registerEffectOnDependencyChange(deps: List, effect: (registerCleanup: (() -> Unit) -> Unit) -> Unit) { registerEffectInvocation( runMode = EffectHookRunMode.OnDependencyChange, deps = deps, - effect = effect + effect = effect, ) } - fun registerEffectEveryCommit( - effect: (registerCleanup: (() -> Unit) -> Unit) -> Unit - ) { + fun registerEffectEveryCommit(effect: (registerCleanup: (() -> Unit) -> Unit) -> Unit) { registerEffectInvocation( runMode = EffectHookRunMode.EveryCommit, deps = emptyList(), - effect = effect + effect = effect, ) } @@ -595,7 +593,7 @@ internal class ComponentHookRuntime { pendingEffectCommitBatch = null runCommittedEffectCleanupForSubtree( root = null, - reason = "component runtime disposal" + reason = "component runtime disposal", ) committedEffectsByComponent.clear() contextsByComponent.clear() @@ -613,7 +611,7 @@ internal class ComponentHookRuntime { componentName = normalizedName, key = key, origin = ComponentFrameOrigin.Explicit, - inferredDescriptor = null + inferredDescriptor = null, ) } @@ -625,13 +623,13 @@ internal class ComponentHookRuntime { val frame = componentStack.last() if (frame.origin != ComponentFrameOrigin.Explicit) { throw HookUsageException( - "Cannot leave inferred/root component hook scope with explicit leave call." + "Cannot leave inferred/root component hook scope with explicit leave call.", ) } if (frame.scopeSegments.isNotEmpty()) { throw HookUsageException( "Invalid nested hook scope behavior: component '${frame.componentId.debugPath()}' " + - "ended with unclosed custom hook scope '${frame.scopeSegments.last()}'." + "ended with unclosed custom hook scope '${frame.scopeSegments.last()}'.", ) } componentStack.removeLast() @@ -654,7 +652,7 @@ internal class ComponentHookRuntime { delegateName = normalizedName, kind = HookEntryKind.CustomScope, signature = HookSignatures.kindOnly(HookEntryKind.CustomScope), - renderEpoch = renderEpoch + renderEpoch = renderEpoch, ) { Unit } recordInferredHookCallSite(frame) frame.scopeSegments.addLast(normalizedName) @@ -664,7 +662,7 @@ internal class ComponentHookRuntime { val frame = currentFrame() if (frame.scopeSegments.isEmpty()) { throw HookUsageException( - "Invalid nested hook scope behavior: no active custom hook scope to leave." + "Invalid nested hook scope behavior: no active custom hook scope to leave.", ) } frame.scopeSegments.removeLast() @@ -679,22 +677,14 @@ internal class ComponentHookRuntime { } } - fun resolveNamedEntry( - kind: HookEntryKind, - delegateName: String, - initializer: () -> Any? - ): ResolvedHookEntry { + fun resolveNamedEntry(kind: HookEntryKind, delegateName: String, initializer: () -> Any?): ResolvedHookEntry { val frame = currentFrameForHookResolution() val normalizedName = validateSegment(delegateName, "delegated property") val signature = HookSignatures.kindOnly(kind) return resolveNamedEntryWithSignature(frame, kind, signature, normalizedName, initializer) } - fun resolveUnnamedEntry( - kind: HookEntryKind, - hookName: String, - initializer: () -> Any? - ): ResolvedHookEntry { + fun resolveUnnamedEntry(kind: HookEntryKind, hookName: String, initializer: () -> Any?): ResolvedHookEntry { val frame = currentFrameForHookResolution() val normalizedHookName = validateSegment(hookName, "hook name") val signature = HookSignatures.kindOnly(kind) @@ -706,22 +696,23 @@ internal class ComponentHookRuntime { delegateName: String, signature: HookSignature, expectedRawType: Class<*>, - initializer: () -> T + initializer: () -> T, ): ResolvedTypedHookEntry { val frame = currentFrameForHookResolution() val normalizedName = validateSegment(delegateName, "delegated property") val resolved = resolveNamedEntryWithSignature(frame, kind, signature, normalizedName, initializer) - val typedValue: T = castResolvedEntryValue( - value = resolved.entry.value, - path = resolved.path, - componentLabel = frame.componentId.debugPath(), - expectedRawType = expectedRawType - ) + val typedValue: T = + castResolvedEntryValue( + value = resolved.entry.value, + path = resolved.path, + componentLabel = frame.componentId.debugPath(), + expectedRawType = expectedRawType, + ) return ResolvedTypedHookEntry( path = resolved.path, value = typedValue, created = resolved.created, - synthetic = resolved.synthetic + synthetic = resolved.synthetic, ) } @@ -729,38 +720,38 @@ internal class ComponentHookRuntime { kind: HookEntryKind, delegateName: String, signature: HookSignature, - noinline initializer: () -> T - ): ResolvedTypedHookEntry { - return resolveNamedTypedEntry( + noinline initializer: () -> T, + ): ResolvedTypedHookEntry = + resolveNamedTypedEntry( kind = kind, delegateName = delegateName, signature = signature, expectedRawType = T::class.java, - initializer = initializer + initializer = initializer, ) - } fun resolveUnnamedTypedEntry( kind: HookEntryKind, hookName: String, signature: HookSignature, expectedRawType: Class<*>, - initializer: () -> T + initializer: () -> T, ): ResolvedTypedHookEntry { val frame = currentFrameForHookResolution() val normalizedHookName = validateSegment(hookName, "hook name") val resolved = resolveUnnamedEntryWithSignature(frame, kind, signature, normalizedHookName, initializer) - val typedValue: T = castResolvedEntryValue( - value = resolved.entry.value, - path = resolved.path, - componentLabel = frame.componentId.debugPath(), - expectedRawType = expectedRawType - ) + val typedValue: T = + castResolvedEntryValue( + value = resolved.entry.value, + path = resolved.path, + componentLabel = frame.componentId.debugPath(), + expectedRawType = expectedRawType, + ) return ResolvedTypedHookEntry( path = resolved.path, value = typedValue, created = resolved.created, - synthetic = resolved.synthetic + synthetic = resolved.synthetic, ) } @@ -768,30 +759,29 @@ internal class ComponentHookRuntime { kind: HookEntryKind, hookName: String, signature: HookSignature, - noinline initializer: () -> T - ): ResolvedTypedHookEntry { - return resolveUnnamedTypedEntry( + noinline initializer: () -> T, + ): ResolvedTypedHookEntry = + resolveUnnamedTypedEntry( kind = kind, hookName = hookName, signature = signature, expectedRawType = T::class.java, - initializer = initializer + initializer = initializer, ) - } - fun failStorageBackedHookWithoutDelegate(hookName: String): Nothing { + fun failStorageBackedHookWithoutDelegate(hookName: String): Nothing = throw HookUsageException( "Storage-backed hook '$hookName' must be bound via delegated property syntax (`by $hookName(...)`). " + - "Direct assignment is not supported." + "Direct assignment is not supported.", ) - } fun registerStorageBackedHookCandidate(hookName: String): StorageHookBindingToken { ensureActiveRender() - val token = StorageHookBindingToken( - hookName = hookName, - renderEpoch = renderEpoch - ) + val token = + StorageHookBindingToken( + hookName = hookName, + renderEpoch = renderEpoch, + ) pendingStorageHookBindings.add(token) return token } @@ -800,7 +790,7 @@ internal class ComponentHookRuntime { ensureActiveRender() if (token.renderEpoch != renderEpoch) { throw HookUsageException( - "Storage-backed hook '${token.hookName}' binding token does not belong to current render session." + "Storage-backed hook '${token.hookName}' binding token does not belong to current render session.", ) } token.bound = true @@ -815,46 +805,46 @@ internal class ComponentHookRuntime { kind: HookEntryKind, signature: HookSignature, delegateName: String, - initializer: () -> Any? - ): ResolvedHookEntry { - return try { - val resolved = frame.context.resolveNamedEntry( - scopeSegments = frame.scopeSegments.toList(), - delegateName = delegateName, - kind = kind, - signature = signature, - renderEpoch = renderEpoch, - initializer = initializer - ) + initializer: () -> Any?, + ): ResolvedHookEntry = + try { + val resolved = + frame.context.resolveNamedEntry( + scopeSegments = frame.scopeSegments.toList(), + delegateName = delegateName, + kind = kind, + signature = signature, + renderEpoch = renderEpoch, + initializer = initializer, + ) recordInferredHookCallSite(frame) resolved } catch (mismatch: HookCompatibilityMismatchException) { handleCompatibilityMismatch(mismatch.mismatch) } - } private fun resolveUnnamedEntryWithSignature( frame: ComponentFrame, kind: HookEntryKind, signature: HookSignature, hookName: String, - initializer: () -> Any? - ): ResolvedHookEntry { - return try { - val resolved = frame.context.resolveUnnamedEntry( - scopeSegments = frame.scopeSegments.toList(), - hookName = hookName, - kind = kind, - signature = signature, - renderEpoch = renderEpoch, - initializer = initializer - ) + initializer: () -> Any?, + ): ResolvedHookEntry = + try { + val resolved = + frame.context.resolveUnnamedEntry( + scopeSegments = frame.scopeSegments.toList(), + hookName = hookName, + kind = kind, + signature = signature, + renderEpoch = renderEpoch, + initializer = initializer, + ) recordInferredHookCallSite(frame) resolved } catch (mismatch: HookCompatibilityMismatchException) { handleCompatibilityMismatch(mismatch.mismatch) } - } private fun handleCompatibilityMismatch(mismatch: HookCompatibilityMismatch): Nothing { if (renderSessionMode != HookRenderSessionMode.HotReload) { @@ -866,9 +856,10 @@ internal class ComponentHookRuntime { } private fun resetComponentSubtree(root: ComponentInstanceId) { - val idsToRemove = contextsByComponent.keys - .filter { componentId -> componentId.isSameOrDescendantOf(root) } - .toSet() + val idsToRemove = + contextsByComponent.keys + .filter { componentId -> componentId.isSameOrDescendantOf(root) } + .toSet() if (idsToRemove.isEmpty()) return val iterator = contextsByComponent.entries.iterator() while (iterator.hasNext()) { @@ -879,7 +870,7 @@ internal class ComponentHookRuntime { } runCommittedEffectCleanupForSubtree( root = root, - reason = "hot-reload remount/reset" + reason = "hot-reload remount/reset", ) committedEffectsByComponent.keys .filter { componentId -> componentId.isSameOrDescendantOf(root) } @@ -902,26 +893,29 @@ internal class ComponentHookRuntime { private fun registerEffectInvocation( runMode: EffectHookRunMode, deps: List, - effect: (registerCleanup: (() -> Unit) -> Unit) -> Unit + effect: (registerCleanup: (() -> Unit) -> Unit) -> Unit, ) { val frame = currentFrameForHookResolution() - val resolved = resolveUnnamedEntryWithSignature( - frame = frame, - kind = HookEntryKind.Effect, - signature = HookSignatures.effect(runMode), - hookName = "useEffect" - ) { } - val key = EffectRegistrationKey( - componentId = frame.componentId, - path = resolved.path - ) - renderEffectRegistrations[key] = EffectRenderRegistration( - componentId = frame.componentId, - path = resolved.path, - runMode = runMode, - deps = deps, - effect = effect - ) + val resolved = + resolveUnnamedEntryWithSignature( + frame = frame, + kind = HookEntryKind.Effect, + signature = HookSignatures.effect(runMode), + hookName = "useEffect", + ) { } + val key = + EffectRegistrationKey( + componentId = frame.componentId, + path = resolved.path, + ) + renderEffectRegistrations[key] = + EffectRenderRegistration( + componentId = frame.componentId, + path = resolved.path, + runMode = runMode, + deps = deps, + effect = effect, + ) } private fun applyEffectCommitBatch(batch: PendingEffectCommitBatch) { @@ -930,9 +924,11 @@ internal class ComponentHookRuntime { val runQueue: MutableList = arrayListOf() val visitedComponents = batch.visitedComponents - val registrationsByComponent: MutableMap> = linkedMapOf() + val registrationsByComponent: MutableMap> = + linkedMapOf() batch.registrationsInOrder.forEach { registration -> - registrationsByComponent.getOrPut(registration.componentId) { arrayListOf() } + registrationsByComponent + .getOrPut(registration.componentId) { arrayListOf() } .add(registration) } @@ -955,15 +951,16 @@ internal class ComponentHookRuntime { runQueue.add(registration) return@forEach } - val shouldRerun = when (registration.runMode) { - EffectHookRunMode.EveryCommit -> true - EffectHookRunMode.OnDependencyChange -> existing.deps != registration.deps - } + val shouldRerun = + when (registration.runMode) { + EffectHookRunMode.EveryCommit -> true + EffectHookRunMode.OnDependencyChange -> existing.deps != registration.deps + } if (shouldRerun) { val cleanup = existing.cleanup if (cleanup != null) { cleanupQueue.add( - EffectRegistrationKey(registration.componentId, registration.path) to cleanup + EffectRegistrationKey(registration.componentId, registration.path) to cleanup, ) } runQueue.add(registration) @@ -992,7 +989,7 @@ internal class ComponentHookRuntime { componentId = key.componentId, path = key.path, cleanup = cleanup, - reason = "effect cleanup" + reason = "effect cleanup", ) } @@ -1005,32 +1002,35 @@ internal class ComponentHookRuntime { } runQueue.forEach { registration -> - val committedByPath = committedEffectsByComponent - .getOrPut(registration.componentId) { linkedMapOf() } - val cleanup = runCommittedEffect( - componentId = registration.componentId, - path = registration.path, - effect = registration.effect - ) - committedByPath[registration.path] = CommittedEffectState( - runMode = registration.runMode, - deps = registration.deps, - cleanup = cleanup - ) + val committedByPath = + committedEffectsByComponent + .getOrPut(registration.componentId) { linkedMapOf() } + val cleanup = + runCommittedEffect( + componentId = registration.componentId, + path = registration.path, + effect = registration.effect, + ) + committedByPath[registration.path] = + CommittedEffectState( + runMode = registration.runMode, + deps = registration.deps, + cleanup = cleanup, + ) } } private fun runCommittedEffect( componentId: ComponentInstanceId, path: HookPath, - effect: (registerCleanup: (() -> Unit) -> Unit) -> Unit + effect: (registerCleanup: (() -> Unit) -> Unit) -> Unit, ): (() -> Unit)? { var cleanupRegistration: (() -> Unit)? = null effect { cleanup -> if (cleanupRegistration != null) { throw HookUsageException( "Effect at path '$path' in component '${componentId.debugPath()}' " + - "registered multiple cleanup handlers via onDispose." + "registered multiple cleanup handlers via onDispose.", ) } cleanupRegistration = cleanup @@ -1042,22 +1042,19 @@ internal class ComponentHookRuntime { componentId: ComponentInstanceId, path: HookPath, cleanup: () -> Unit, - reason: String + reason: String, ) { try { cleanup() } catch (error: Throwable) { println( "[DSGL][Hooks] Effect cleanup failed at path '$path' in component '${componentId.debugPath()}' " + - "during $reason: ${error.message}" + "during $reason: ${error.message}", ) } } - private fun runCommittedEffectCleanupForSubtree( - root: ComponentInstanceId?, - reason: String - ) { + private fun runCommittedEffectCleanupForSubtree(root: ComponentInstanceId?, reason: String) { val entries = committedEffectsByComponent.entries.toList() entries.forEach { (componentId, byPath) -> if (root != null && !componentId.isSameOrDescendantOf(root)) { @@ -1069,7 +1066,7 @@ internal class ComponentHookRuntime { componentId = componentId, path = path, cleanup = cleanup, - reason = reason + reason = reason, ) } } @@ -1094,8 +1091,9 @@ internal class ComponentHookRuntime { private fun prepareInferredComponentFramesForHookResolution() { ensureActiveRender() - val top = componentStack.lastOrNull() - ?: throw HookUsageException("Hook runtime has no active component frame.") + val top = + componentStack.lastOrNull() + ?: throw HookUsageException("Hook runtime has no active component frame.") if (top.origin == ComponentFrameOrigin.Explicit) { currentInferenceLeafCallSite = null return @@ -1112,7 +1110,9 @@ internal class ComponentHookRuntime { while (commonPrefixLength < existingCount && commonPrefixLength < desiredPath.size) { val existingFrame = componentStack.elementAt(baseIndex + 1 + commonPrefixLength) val existingDescriptor = existingFrame.inferredDescriptor - if (existingFrame.origin != ComponentFrameOrigin.Inferred || existingDescriptor != desiredPath[commonPrefixLength]) { + if (existingFrame.origin != ComponentFrameOrigin.Inferred || + existingDescriptor != desiredPath[commonPrefixLength] + ) { break } commonPrefixLength += 1 @@ -1122,18 +1122,20 @@ internal class ComponentHookRuntime { popInferredFrameForTransition() } - var parent = componentStack.lastOrNull() - ?: throw HookUsageException("Hook runtime has no active component frame.") + var parent = + componentStack.lastOrNull() + ?: throw HookUsageException("Hook runtime has no active component frame.") var index = commonPrefixLength while (index < desiredPath.size) { val descriptor = desiredPath[index] - parent = pushComponentFrame( - parent = parent, - componentName = descriptor.componentName, - key = null, - origin = ComponentFrameOrigin.Inferred, - inferredDescriptor = descriptor - ) + parent = + pushComponentFrame( + parent = parent, + componentName = descriptor.componentName, + key = null, + origin = ComponentFrameOrigin.Inferred, + inferredDescriptor = descriptor, + ) index += 1 } } @@ -1150,15 +1152,18 @@ internal class ComponentHookRuntime { } private fun popInferredFrameForTransition() { - val frame = componentStack.lastOrNull() - ?: throw HookUsageException("Hook runtime has no active component frame.") + val frame = + componentStack.lastOrNull() + ?: throw HookUsageException("Hook runtime has no active component frame.") if (frame.origin != ComponentFrameOrigin.Inferred) { - throw HookUsageException("Hook runtime internal error: attempted to pop non-inferred frame during inference transition.") + throw HookUsageException( + "Hook runtime internal error: attempted to pop non-inferred frame during inference transition.", + ) } if (frame.scopeSegments.isNotEmpty()) { throw HookUsageException( "Invalid nested hook scope behavior: component '${frame.componentId.debugPath()}' " + - "ended with unclosed custom hook scope '${frame.scopeSegments.last()}'." + "ended with unclosed custom hook scope '${frame.scopeSegments.last()}'.", ) } componentStack.removeLast() @@ -1171,13 +1176,13 @@ internal class ComponentHookRuntime { ComponentFrameOrigin.Inferred -> popInferredFrameForTransition() ComponentFrameOrigin.Explicit -> { throw HookUsageException( - "Invalid nested component scope behavior: render ended with unbalanced explicit component scopes." + "Invalid nested component scope behavior: render ended with unbalanced explicit component scopes.", ) } ComponentFrameOrigin.Root -> { throw HookUsageException( - "Hook runtime internal error: root frame encountered before stack unwind completed." + "Hook runtime internal error: root frame encountered before stack unwind completed.", ) } } @@ -1189,34 +1194,37 @@ internal class ComponentHookRuntime { componentName: String, key: Any?, origin: ComponentFrameOrigin, - inferredDescriptor: InferredComponentDescriptor? + inferredDescriptor: InferredComponentDescriptor?, ): ComponentFrame { - val positionalIndex = if (key == null) { - val current = parent.positionalChildCounters[componentName] ?: 0 - parent.positionalChildCounters[componentName] = current + 1 - current - } else { - -1 - } + val positionalIndex = + if (key == null) { + val current = parent.positionalChildCounters[componentName] ?: 0 + parent.positionalChildCounters[componentName] = current + 1 + current + } else { + -1 + } - val childId = parent.componentId.child( - name = componentName, - explicitKey = key, - positionalIndex = positionalIndex - ) + val childId = + parent.componentId.child( + name = componentName, + explicitKey = key, + positionalIndex = positionalIndex, + ) if (!enteredComponentIdsThisRender.add(childId)) { throw HookUsageException( - "Duplicate component identity '${childId.debugPath()}' in a single render pass." + "Duplicate component identity '${childId.debugPath()}' in a single render pass.", ) } val childContext = contextsByComponent.getOrPut(childId) { ComponentHookContext(childId) } childContext.beginRender(renderEpoch) - val childFrame = ComponentFrame( - componentId = childId, - context = childContext, - origin = origin, - inferredDescriptor = inferredDescriptor - ) + val childFrame = + ComponentFrame( + componentId = childId, + context = childContext, + origin = origin, + inferredDescriptor = inferredDescriptor, + ) componentStack.addLast(childFrame) return childFrame } @@ -1235,17 +1243,19 @@ internal class ComponentHookRuntime { foundRenderBoundary = true break } - val descriptor = InferredComponentDescriptor( - ownerClassName = frame.className, - methodName = methodName, - componentName = inferredComponentName(frame.className, methodName) - ) - if (leafCallSite == null) { - leafCallSite = HookCallSite( - className = frame.className, + val descriptor = + InferredComponentDescriptor( + ownerClassName = frame.className, methodName = methodName, - lineNumber = frame.lineNumber + componentName = inferredComponentName(frame.className, methodName), ) + if (leafCallSite == null) { + leafCallSite = + HookCallSite( + className = frame.className, + methodName = methodName, + lineNumber = frame.lineNumber, + ) } if (innerToOuter.lastOrNull() == descriptor) { continue @@ -1255,14 +1265,14 @@ internal class ComponentHookRuntime { if (!foundRenderBoundary || innerToOuter.isEmpty()) { return InferenceSnapshot( descriptors = emptyList(), - leafCallSite = null + leafCallSite = null, ) } val result = innerToOuter.toMutableList() result.reverse() return InferenceSnapshot( descriptors = result, - leafCallSite = leafCallSite + leafCallSite = leafCallSite, ) } @@ -1281,10 +1291,11 @@ internal class ComponentHookRuntime { } private fun normalizeInferenceMethodName(rawMethodName: String): String? { - val method = when { - rawMethodName.endsWith("\$default") -> rawMethodName.removeSuffix("\$default") - else -> rawMethodName - } + val method = + when { + rawMethodName.endsWith("\$default") -> rawMethodName.removeSuffix("\$default") + else -> rawMethodName + } if (method.isBlank()) return null if (method == "getStackTrace") return null if (method.startsWith("invoke")) return null @@ -1312,7 +1323,7 @@ internal class ComponentHookRuntime { val raw = "inferred_${ownerSimple}_$methodName" return validateSegment( value = raw.replace('.', '_'), - label = "inferred component name" + label = "inferred component name", ) } @@ -1324,23 +1335,25 @@ internal class ComponentHookRuntime { if (!frame.seenHookCallSitesThisRender.contains(callSite)) { return frame } - val descriptor = frame.inferredDescriptor - ?: throw HookUsageException("Hook runtime internal error: inferred frame without descriptor.") + val descriptor = + frame.inferredDescriptor + ?: throw HookUsageException("Hook runtime internal error: inferred frame without descriptor.") if (frame.scopeSegments.isNotEmpty()) { throw HookUsageException( "Invalid nested hook scope behavior: component '${frame.componentId.debugPath()}' " + - "ended with unclosed custom hook scope '${frame.scopeSegments.last()}'." + "ended with unclosed custom hook scope '${frame.scopeSegments.last()}'.", ) } componentStack.removeLast() - val parent = componentStack.lastOrNull() - ?: throw HookUsageException("Hook runtime has no parent frame for inferred sibling transition.") + val parent = + componentStack.lastOrNull() + ?: throw HookUsageException("Hook runtime has no parent frame for inferred sibling transition.") return pushComponentFrame( parent = parent, componentName = descriptor.componentName, key = null, origin = ComponentFrameOrigin.Inferred, - inferredDescriptor = descriptor + inferredDescriptor = descriptor, ) } @@ -1379,13 +1392,13 @@ internal class ComponentHookRuntime { value: Any?, path: HookPath, componentLabel: String, - expectedRawType: Class<*> + expectedRawType: Class<*>, ): T { if (value == null || !expectedRawType.isInstance(value)) { val actualType = if (value == null) "null" else value.javaClass.name throw HookUsageException( "Hook value type mismatch at path '$path' in component '$componentLabel': " + - "expected=${expectedRawType.name}, actual=$actualType." + "expected=${expectedRawType.name}, actual=$actualType.", ) } @Suppress("UNCHECKED_CAST") diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseContext.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseContext.kt index a8168e0..69344c6 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseContext.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseContext.kt @@ -4,24 +4,16 @@ import org.dreamfinity.dsgl.core.dsl.UiScope class DsglContext( val name: String, - val defaultValue: T + val defaultValue: T, ) -fun createContext(defaultValue: T, name: String = "Context"): DsglContext { - return DsglContext( +fun createContext(defaultValue: T, name: String = "Context"): DsglContext = + DsglContext( name = name, - defaultValue = defaultValue + defaultValue = defaultValue, ) -} -fun UiScope.useContext(context: DsglContext): T { - return readContextValue(context) -} +fun UiScope.useContext(context: DsglContext): T = readContextValue(context) -fun UiScope.provideContext( - context: DsglContext, - value: T, - block: UiScope.() -> R -): R { - return withProvidedContext(context = context, value = value, block = block) -} +fun UiScope.provideContext(context: DsglContext, value: T, block: UiScope.() -> R): R = + withProvidedContext(context = context, value = value, block = block) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseEffect.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseEffect.kt index 5fc49d6..7abba1d 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseEffect.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseEffect.kt @@ -18,11 +18,12 @@ fun UiScope.useEffectEveryCommit(effect: EffectScope.() -> Unit) { internal fun DsglWindow.useEffect(vararg deps: Any?, effect: EffectScope.() -> Unit) { val runtime = hookRuntime() runtime.registerEffectOnDependencyChange(deps.toList()) { registerCleanup -> - val scope = object : EffectScope { - override fun onDispose(cleanup: () -> Unit) { - registerCleanup(cleanup) + val scope = + object : EffectScope { + override fun onDispose(cleanup: () -> Unit) { + registerCleanup(cleanup) + } } - } effect(scope) } } @@ -30,11 +31,12 @@ internal fun DsglWindow.useEffect(vararg deps: Any?, effect: EffectScope.() -> U internal fun DsglWindow.useEffectEveryCommit(effect: EffectScope.() -> Unit) { val runtime = hookRuntime() runtime.registerEffectEveryCommit { registerCleanup -> - val scope = object : EffectScope { - override fun onDispose(cleanup: () -> Unit) { - registerCleanup(cleanup) + val scope = + object : EffectScope { + override fun onDispose(cleanup: () -> Unit) { + registerCleanup(cleanup) + } } - } effect(scope) } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseMemo.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseMemo.kt index b2f3233..69a621c 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseMemo.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseMemo.kt @@ -8,7 +8,7 @@ import kotlin.reflect.typeOf internal class MemoHookCell( deps: List, - value: T + value: T, ) { private var depsSnapshot: List = deps private var cachedValue: T = value @@ -24,112 +24,109 @@ internal class MemoHookCell( fun read(): T = cachedValue } -class MemoHookDelegate @PublishedApi internal constructor( - private val window: DsglWindow, - private val hookName: String, - private val deps: List, - private val compute: () -> T, - private val signature: HookSignature -) { - private val runtime = window.hookRuntime() - private val bindingToken = runtime.registerStorageBackedHookCandidate(hookName) - private var boundCell: MemoHookCell? = null +class MemoHookDelegate + @PublishedApi + internal constructor( + private val window: DsglWindow, + private val hookName: String, + private val deps: List, + private val compute: () -> T, + private val signature: HookSignature, + ) { + private val runtime = window.hookRuntime() + private val bindingToken = runtime.registerStorageBackedHookCandidate(hookName) + private var boundCell: MemoHookCell? = null - operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): MemoHookDelegate { - runtime.markStorageBackedHookBound(bindingToken) - val resolved = runtime.resolveNamedTypedEntry( - kind = HookEntryKind.Memo, - delegateName = property.name, - signature = signature, - expectedRawType = MemoHookCell::class.java - ) { - MemoHookCell(deps = deps, value = compute()) + operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): MemoHookDelegate { + runtime.markStorageBackedHookBound(bindingToken) + val resolved = + runtime.resolveNamedTypedEntry( + kind = HookEntryKind.Memo, + delegateName = property.name, + signature = signature, + expectedRawType = MemoHookCell::class.java, + ) { + MemoHookCell(deps = deps, value = compute()) + } + resolved.value.sync(nextDeps = deps, compute = compute) + boundCell = resolved.value + return this } - resolved.value.sync(nextDeps = deps, compute = compute) - boundCell = resolved.value - return this - } - operator fun getValue(thisRef: Any?, property: KProperty<*>): T { - val resolved = boundCell - if (resolved != null) { - return resolved.read() + operator fun getValue(thisRef: Any?, property: KProperty<*>): T { + val resolved = boundCell + if (resolved != null) { + return resolved.read() + } + runtime.failStorageBackedHookWithoutDelegate(hookName) } - runtime.failStorageBackedHookWithoutDelegate(hookName) } -} @OptIn(ExperimentalStdlibApi::class) -inline fun UiScope.useMemo(vararg deps: Any?, noinline compute: () -> T): MemoHookDelegate { - return createMemoHookDelegate( +inline fun UiScope.useMemo(vararg deps: Any?, noinline compute: () -> T): MemoHookDelegate = + createMemoHookDelegate( window = requireHookOwnerWindow(), hookName = "useMemo", deps = deps.toList(), compute = compute, - signature = HookSignatures.memo(typeOf()) + signature = HookSignatures.memo(typeOf()), ) -} @OptIn(ExperimentalStdlibApi::class) -inline fun UiScope.useMemo(noinline compute: () -> T): MemoHookDelegate { - return createMemoHookDelegate( +inline fun UiScope.useMemo(noinline compute: () -> T): MemoHookDelegate = + createMemoHookDelegate( window = requireHookOwnerWindow(), hookName = "useMemo", deps = emptyList(), compute = compute, - signature = HookSignatures.memo(typeOf()) + signature = HookSignatures.memo(typeOf()), ) -} @OptIn(ExperimentalStdlibApi::class) -inline fun UiScope.useCallback(vararg deps: Any?, noinline factory: () -> F): MemoHookDelegate { - return createMemoHookDelegate( +inline fun UiScope.useCallback(vararg deps: Any?, noinline factory: () -> F): MemoHookDelegate = + createMemoHookDelegate( window = requireHookOwnerWindow(), hookName = "useCallback", deps = deps.toList(), compute = factory, - signature = HookSignatures.memo(typeOf()) + signature = HookSignatures.memo(typeOf()), ) -} @PublishedApi @OptIn(ExperimentalStdlibApi::class) -internal inline fun DsglWindow.useMemo(vararg deps: Any?, noinline compute: () -> T): MemoHookDelegate { - return createMemoHookDelegate( +internal inline fun DsglWindow.useMemo(vararg deps: Any?, noinline compute: () -> T): MemoHookDelegate = + createMemoHookDelegate( window = this, hookName = "useMemo", deps = deps.toList(), compute = compute, - signature = HookSignatures.memo(typeOf()) + signature = HookSignatures.memo(typeOf()), ) -} @PublishedApi @OptIn(ExperimentalStdlibApi::class) -internal inline fun DsglWindow.useMemo(noinline compute: () -> T): MemoHookDelegate { - return createMemoHookDelegate( +internal inline fun DsglWindow.useMemo(noinline compute: () -> T): MemoHookDelegate = + createMemoHookDelegate( window = this, hookName = "useMemo", deps = emptyList(), compute = compute, - signature = HookSignatures.memo(typeOf()) + signature = HookSignatures.memo(typeOf()), ) -} @PublishedApi @OptIn(ExperimentalStdlibApi::class) internal inline fun DsglWindow.useCallback( vararg deps: Any?, - noinline factory: () -> F -): MemoHookDelegate { - return createMemoHookDelegate( + noinline factory: () -> F, +): MemoHookDelegate = + createMemoHookDelegate( window = this, hookName = "useCallback", deps = deps.toList(), compute = factory, - signature = HookSignatures.memo(typeOf()) + signature = HookSignatures.memo(typeOf()), ) -} @PublishedApi internal fun createMemoHookDelegate( @@ -137,13 +134,12 @@ internal fun createMemoHookDelegate( hookName: String, deps: List, compute: () -> T, - signature: HookSignature -): MemoHookDelegate { - return MemoHookDelegate( + signature: HookSignature, +): MemoHookDelegate = + MemoHookDelegate( window = window, hookName = hookName, deps = deps, compute = compute, - signature = signature + signature = signature, ) -} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseReducer.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseReducer.kt index 74a6d44..e55e2da 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseReducer.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseReducer.kt @@ -4,44 +4,38 @@ import org.dreamfinity.dsgl.core.DsglWindow import org.dreamfinity.dsgl.core.dsl.UiScope import kotlin.reflect.KClass -fun UiScope.useReducer( - initialState: S, - reducer: (S, A) -> S -): Pair Unit> { - return requireHookOwnerWindow().useReducer(initialState = initialState, reducer = reducer) -} +fun UiScope.useReducer(initialState: S, reducer: (S, A) -> S): Pair Unit> = + requireHookOwnerWindow().useReducer(initialState = initialState, reducer = reducer) -internal fun DsglWindow.useReducer( - initialState: S, - reducer: (S, A) -> S -): Pair Unit> { - return createReducerHookPair( +internal fun DsglWindow.useReducer(initialState: S, reducer: (S, A) -> S): Pair Unit> = + createReducerHookPair( window = this, initialState = initialState, - reducer = reducer + reducer = reducer, ) -} internal fun createReducerHookPair( window: DsglWindow, initialState: S, - reducer: (S, A) -> S + reducer: (S, A) -> S, ): Pair Unit> { val runtime = window.hookRuntime() - val signature = HookSignatures.reducer( - stateClass = reducerStateClassOf(initialState), - initialWasNull = initialState == null - ) - val resolved = runtime.resolveUnnamedTypedEntry( - kind = HookEntryKind.Reducer, - hookName = "useReducer", - signature = signature, - expectedRawType = HookStateCell::class.java - ) { - HookStateCell(initialState) { - window.onHookStateChanged() + val signature = + HookSignatures.reducer( + stateClass = reducerStateClassOf(initialState), + initialWasNull = initialState == null, + ) + val resolved = + runtime.resolveUnnamedTypedEntry( + kind = HookEntryKind.Reducer, + hookName = "useReducer", + signature = signature, + expectedRawType = HookStateCell::class.java, + ) { + HookStateCell(initialState) { + window.onHookStateChanged() + } } - } val stateCell = resolved.value val dispatch: (A) -> Unit = { action -> stateCell.write(reducer(stateCell.read(), action)) @@ -49,6 +43,4 @@ internal fun createReducerHookPair( return stateCell.read() to dispatch } -private fun reducerStateClassOf(value: Any?): KClass<*>? { - return value?.let { current -> current::class } -} +private fun reducerStateClassOf(value: Any?): KClass<*>? = value?.let { current -> current::class } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseState.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseState.kt index 4826130..f81474d 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseState.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseState.kt @@ -8,7 +8,7 @@ import kotlin.reflect.typeOf internal class HookStateCell( initial: T, - private val onChange: () -> Unit + private val onChange: () -> Unit, ) { private var stored: T = initial @@ -22,68 +22,63 @@ internal class HookStateCell( } } -class StateHookDelegate @PublishedApi internal constructor( - private val window: DsglWindow, - private val initial: T, - private val signature: HookSignature -) { - private val runtime = window.hookRuntime() - private val bindingToken = runtime.registerStorageBackedHookCandidate("useState") - private var boundCell: HookStateCell? = null +class StateHookDelegate + @PublishedApi + internal constructor( + private val window: DsglWindow, + private val initial: T, + private val signature: HookSignature, + ) { + private val runtime = window.hookRuntime() + private val bindingToken = runtime.registerStorageBackedHookCandidate("useState") + private var boundCell: HookStateCell? = null - operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): StateHookDelegate { - runtime.markStorageBackedHookBound(bindingToken) - val resolved = runtime.resolveNamedTypedEntry( - kind = HookEntryKind.State, - delegateName = property.name, - signature = signature, - expectedRawType = HookStateCell::class.java - ) { - HookStateCell(initial) { - window.onHookStateChanged() - } + operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): StateHookDelegate { + runtime.markStorageBackedHookBound(bindingToken) + val resolved = + runtime.resolveNamedTypedEntry( + kind = HookEntryKind.State, + delegateName = property.name, + signature = signature, + expectedRawType = HookStateCell::class.java, + ) { + HookStateCell(initial) { + window.onHookStateChanged() + } + } + boundCell = resolved.value + return this } - boundCell = resolved.value - return this - } - operator fun getValue(thisRef: Any?, property: KProperty<*>): T { - return requireBoundCell().read() - } + operator fun getValue(thisRef: Any?, property: KProperty<*>): T = requireBoundCell().read() - operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { - requireBoundCell().write(value) - } + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + requireBoundCell().write(value) + } - private fun requireBoundCell(): HookStateCell { - val resolved = boundCell - if (resolved != null) { - return resolved + private fun requireBoundCell(): HookStateCell { + val resolved = boundCell + if (resolved != null) { + return resolved + } + runtime.failStorageBackedHookWithoutDelegate("useState") } - runtime.failStorageBackedHookWithoutDelegate("useState") } -} @OptIn(ExperimentalStdlibApi::class) -inline fun UiScope.useState(initial: T): StateHookDelegate { - return createStateHookDelegate(window = requireHookOwnerWindow(), initial = initial) -} +inline fun UiScope.useState(initial: T): StateHookDelegate = + createStateHookDelegate(window = requireHookOwnerWindow(), initial = initial) @PublishedApi @OptIn(ExperimentalStdlibApi::class) -internal inline fun DsglWindow.useState(initial: T): StateHookDelegate { - return createStateHookDelegate(window = this, initial = initial) -} +internal inline fun DsglWindow.useState(initial: T): StateHookDelegate = + createStateHookDelegate(window = this, initial = initial) @PublishedApi @OptIn(ExperimentalStdlibApi::class) -internal inline fun createStateHookDelegate( - window: DsglWindow, - initial: T -): StateHookDelegate { - return StateHookDelegate( +internal inline fun createStateHookDelegate(window: DsglWindow, initial: T): StateHookDelegate = + StateHookDelegate( window = window, initial = initial, - signature = HookSignatures.state(typeOf()) + signature = HookSignatures.state(typeOf()), ) -} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ref/ElementHandle.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ref/ElementHandle.kt index bc90d33..cffc455 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ref/ElementHandle.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ref/ElementHandle.kt @@ -14,7 +14,7 @@ interface ElementHandle { } internal class NodeElementHandle( - node: DOMNode + node: DOMNode, ) : ElementHandle { private var nodeRef: DOMNode? = node diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ref/RefManager.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ref/RefManager.kt index 8bcd3c1..6526470 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ref/RefManager.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ref/RefManager.kt @@ -1,12 +1,12 @@ package org.dreamfinity.dsgl.core.hooks.ref import org.dreamfinity.dsgl.core.dom.DOMNode -import java.util.* +import java.util.IdentityHashMap internal class RefManager { private data class AttachedRef( val target: RefTarget, - val handle: NodeElementHandle + val handle: NodeElementHandle, ) private val attachedByNode: MutableMap = IdentityHashMap() @@ -42,9 +42,10 @@ internal class RefManager { } } - val staleNodes = attachedByNode.keys.filter { node -> - !visited.containsKey(node) - } + val staleNodes = + attachedByNode.keys.filter { node -> + !visited.containsKey(node) + } staleNodes.forEach { node -> val attached = attachedByNode.remove(node) ?: return@forEach attached.target.set(null) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ref/Refs.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ref/Refs.kt index 9448306..06c98fc 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ref/Refs.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ref/Refs.kt @@ -24,63 +24,63 @@ interface Ref : RefTarget { } class RefObject( - override var current: T? = null + override var current: T? = null, ) : Ref fun createRef(initial: T? = null): Ref = RefObject(initial) -class RefHookDelegate @PublishedApi internal constructor( - private val window: DsglWindow, - private val initial: T?, - private val signature: HookSignature -) { - private val runtime = window.hookRuntime() - private val bindingToken = runtime.registerStorageBackedHookCandidate("useRef") - private var boundRef: Ref? = null +class RefHookDelegate + @PublishedApi + internal constructor( + private val window: DsglWindow, + private val initial: T?, + private val signature: HookSignature, + ) { + private val runtime = window.hookRuntime() + private val bindingToken = runtime.registerStorageBackedHookCandidate("useRef") + private var boundRef: Ref? = null - operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): RefHookDelegate { - runtime.markStorageBackedHookBound(bindingToken) - val resolved = runtime.resolveNamedTypedEntry( - kind = HookEntryKind.Ref, - delegateName = property.name, - signature = signature, - expectedRawType = Ref::class.java - ) { - RefObject(initial) + operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): RefHookDelegate { + runtime.markStorageBackedHookBound(bindingToken) + val resolved = + runtime.resolveNamedTypedEntry( + kind = HookEntryKind.Ref, + delegateName = property.name, + signature = signature, + expectedRawType = Ref::class.java, + ) { + RefObject(initial) + } + boundRef = resolved.value + return this } - boundRef = resolved.value - return this - } - operator fun getValue(thisRef: Any?, property: KProperty<*>): Ref { - val resolved = boundRef - if (resolved != null) { - return resolved + operator fun getValue(thisRef: Any?, property: KProperty<*>): Ref { + val resolved = boundRef + if (resolved != null) { + return resolved + } + runtime.failStorageBackedHookWithoutDelegate("useRef") } - runtime.failStorageBackedHookWithoutDelegate("useRef") } -} @OptIn(ExperimentalStdlibApi::class) -inline fun UiScope.useRef(initial: T? = null): RefHookDelegate { - return createRefHookDelegate(window = requireHookOwnerWindow(), initial = initial) -} +inline fun UiScope.useRef(initial: T? = null): RefHookDelegate = + createRefHookDelegate(window = requireHookOwnerWindow(), initial = initial) @PublishedApi @OptIn(ExperimentalStdlibApi::class) -internal inline fun DsglWindow.useRef(initial: T? = null): RefHookDelegate { - return createRefHookDelegate(window = this, initial = initial) -} +internal inline fun DsglWindow.useRef(initial: T? = null): RefHookDelegate = + createRefHookDelegate(window = this, initial = initial) @PublishedApi @OptIn(ExperimentalStdlibApi::class) internal inline fun createRefHookDelegate( window: DsglWindow, - initial: T? = null -): RefHookDelegate { - return RefHookDelegate( + initial: T? = null, +): RefHookDelegate = + RefHookDelegate( window = window, initial = initial, - signature = HookSignatures.ref(typeOf()) + signature = HookSignatures.ref(typeOf()), ) -} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/host/DsglWindowHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/host/DsglWindowHost.kt index f39a2e3..0305d71 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/host/DsglWindowHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/host/DsglWindowHost.kt @@ -10,27 +10,26 @@ data class Viewport( val height: Int, val scale: Float = 1f, val x: Int = 0, - val y: Int = 0 + val y: Int = 0, ) data class ViewportPoint( val x: Int, - val y: Int + val y: Int, ) data class GlScissorRect( val x: Int, val y: Int, val width: Int, - val height: Int + val height: Int, ) -fun Viewport.rawMouseToDsgl(rawX: Int, rawY: Int): ViewportPoint { - return ViewportPoint( +fun Viewport.rawMouseToDsgl(rawX: Int, rawY: Int): ViewportPoint = + ViewportPoint( x = rawX - x, - y = (y + height) - rawY - 1 + y = (y + height) - rawY - 1, ) -} fun Viewport.rawMouseToDsglX(rawX: Int): Int = rawX - x @@ -40,7 +39,7 @@ fun Viewport.dsglRectToGlScissor( dsglX: Int, dsglY: Int, dsglWidth: Int, - dsglHeight: Int + dsglHeight: Int, ): GlScissorRect { val widthPx = dsglWidth.coerceAtLeast(0) val heightPx = dsglHeight.coerceAtLeast(0) @@ -48,7 +47,7 @@ fun Viewport.dsglRectToGlScissor( x = x + dsglX, y = y + height - (dsglY + heightPx), width = widthPx, - height = heightPx + height = heightPx, ) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/input/ClipboardBridge.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/input/ClipboardBridge.kt index ebaa537..8fd9964 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/input/ClipboardBridge.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/input/ClipboardBridge.kt @@ -5,6 +5,7 @@ package org.dreamfinity.dsgl.core.input */ interface ClipboardAccess { fun readText(): String + fun writeText(value: String) } @@ -16,11 +17,9 @@ object ClipboardBridge { provider = access } - fun readText(): String { - return provider?.readText() ?: "" - } + fun readText(): String = provider?.readText() ?: "" fun writeText(value: String) { provider?.writeText(value) } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt index f8ef4bb..18b0962 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt @@ -9,8 +9,8 @@ import org.dreamfinity.dsgl.core.colorpicker.internal.SystemColorPickerPanelMana import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.elements.TextEditState import org.dreamfinity.dsgl.core.dom.elements.support.TextEditOps -import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.Insets +import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.KeyInput import org.dreamfinity.dsgl.core.event.KeyModifiers @@ -22,16 +22,16 @@ import org.dreamfinity.dsgl.core.style.* enum class InspectorMode { Pick, - Locked + Locked, } enum class InspectorPanelState { Expanded, - Minimized + Minimized, } class InspectorController( - colorPickerManager: InspectorColorPickerHost = SystemColorPickerPanelManager() + colorPickerManager: InspectorColorPickerHost = SystemColorPickerPanelManager(), ) { private var colorPickerManager: InspectorColorPickerHost = colorPickerManager @@ -44,7 +44,7 @@ class InspectorController( ToggleValueSelect, SelectValueOption, ToggleUnitSelect, - SelectUnitOption + SelectUnitOption, } private enum class ActionKind { @@ -54,7 +54,7 @@ class InspectorController( Child, EditProperty, ResetSelectedOverrides, - ClearAllOverrides + ClearAllOverrides, } private data class PanelAction( @@ -64,7 +64,7 @@ class InspectorController( val property: StyleProperty? = null, val editOperation: EditOperation? = null, val step: Float = 1f, - val payload: String? = null + val payload: String? = null, ) private data class DropdownLayout( @@ -72,14 +72,14 @@ class InspectorController( val property: StyleProperty, val isUnit: Boolean, val totalOptions: Int, - val visibleRows: Int + val visibleRows: Int, ) private data class SelectionStyleCache( val key: Any?, val nodeClass: Class, val layoutVersion: Long, - val inspection: StyleInspection + val inspection: StyleInspection, ) private enum class DragMode { @@ -94,7 +94,7 @@ class InspectorController( ResizeBottomLeft, ResizeBottomRight, MinimizedMove, - ScrollbarThumb + ScrollbarThumb, } var active: Boolean = false @@ -226,7 +226,7 @@ class InspectorController( private val styleEditorSnapshotBuilder: InspectorStyleEditorSnapshotBuilder = InspectorStyleEditorSnapshotBuilder( resolveLiteralFromComputed = ::literalFromComputed, - renderExpressionLabel = ::expressionLabel + renderExpressionLabel = ::expressionLabel, ) private val minPanelWidth: Int = 240 @@ -240,6 +240,7 @@ class InspectorController( private val secondaryFontSizePx: Int = parseLengthPxInt("24px", allowNegative = false) private val lineHeightPx: Int = (textFontSizePx + 8).coerceAtLeast(28) private val rowHeightPx: Int = (textFontSizePx + 10).coerceAtLeast(32) + fun toggle() { active = !active if (!active) deactivateInternal() @@ -298,11 +299,13 @@ class InspectorController( dragMoved = false } - fun blocksUnderlyingInput(): Boolean = active && ( - mode == InspectorMode.Pick || - dragMode != DragMode.None || - overlayPanelPointerCapture - ) + fun blocksUnderlyingInput(): Boolean = + active && + ( + mode == InspectorMode.Pick || + dragMode != DragMode.None || + overlayPanelPointerCapture + ) fun shouldConsumePointer(mouseX: Int, mouseY: Int): Boolean { if (!active) return false @@ -311,6 +314,7 @@ class InspectorController( if (mode == InspectorMode.Pick) return true return hitTestUi(mouseX, mouseY) } + fun shouldConsumeWheel(mouseX: Int, mouseY: Int): Boolean { if (!active) return false if (dragMode != DragMode.None || overlayPanelPointerCapture) return true @@ -329,9 +333,7 @@ class InspectorController( lastHandledPointerEvent = reason } - fun hitTestUi(mouseX: Int, mouseY: Int): Boolean { - return isInsideInspectorUi(mouseX, mouseY) - } + fun hitTestUi(mouseX: Int, mouseY: Int): Boolean = isInsideInspectorUi(mouseX, mouseY) fun onLayoutCommitted(root: DOMNode, layoutVersion: Long) { val layoutChanged = this.layoutVersion != layoutVersion @@ -367,6 +369,7 @@ class InspectorController( } refreshHoverIfNeeded() } + fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean { if (!active || delta == 0) return false this.mouseX = mouseX @@ -386,11 +389,12 @@ class InspectorController( val maxScroll = maxOf(0, panelContentHeight - contentBounds.height) val steps = (kotlin.math.abs(delta) / 120).coerceAtLeast(1) val amount = steps * 18 - val next = if (delta < 0) { - (panelScrollY + amount).coerceAtMost(maxScroll) - } else { - (panelScrollY - amount).coerceAtLeast(0) - } + val next = + if (delta < 0) { + (panelScrollY + amount).coerceAtMost(maxScroll) + } else { + (panelScrollY - amount).coerceAtLeast(0) + } panelScrollY = next return true } @@ -402,17 +406,19 @@ class InspectorController( val maxFirst = (target.totalOptions - target.visibleRows).coerceAtLeast(0) if (maxFirst <= 0) return true if (target.isUnit) { - openUnitSelectScrollIndex = if (delta < 0) { - (openUnitSelectScrollIndex + steps).coerceAtMost(maxFirst) - } else { - (openUnitSelectScrollIndex - steps).coerceAtLeast(0) - } + openUnitSelectScrollIndex = + if (delta < 0) { + (openUnitSelectScrollIndex + steps).coerceAtMost(maxFirst) + } else { + (openUnitSelectScrollIndex - steps).coerceAtLeast(0) + } } else { - openValueSelectScrollIndex = if (delta < 0) { - (openValueSelectScrollIndex + steps).coerceAtMost(maxFirst) - } else { - (openValueSelectScrollIndex - steps).coerceAtLeast(0) - } + openValueSelectScrollIndex = + if (delta < 0) { + (openValueSelectScrollIndex + steps).coerceAtMost(maxFirst) + } else { + (openValueSelectScrollIndex - steps).coerceAtLeast(0) + } } return true } @@ -496,7 +502,11 @@ class InspectorController( return true } KeyCodes.V -> { - val paste = ClipboardBridge.readText().replace("\r", "").replace("\n", "") + val paste = + ClipboardBridge + .readText() + .replace("\r", "") + .replace("\n", "") if (paste.isNotEmpty()) { replaceActiveSelection(paste) } else { @@ -584,6 +594,7 @@ class InspectorController( TextEditOps.moveCaretWithSelection(activeEditState, next, activeEditBuffer.length, extend) activeEditState.resetBlinkClock() } + fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { if (!active) return false if (button != MouseButton.LEFT) { @@ -687,7 +698,13 @@ class InspectorController( } return true } - fun onCapturedPointerMove(mouseX: Int, mouseY: Int, viewportWidth: Int, viewportHeight: Int) { + + fun onCapturedPointerMove( + mouseX: Int, + mouseY: Int, + viewportWidth: Int, + viewportHeight: Int, + ) { if (!active || dragMode == DragMode.None) return this.mouseX = mouseX this.mouseY = mouseY @@ -705,7 +722,8 @@ class InspectorController( DragMode.ResizeTopLeft, DragMode.ResizeTopRight, DragMode.ResizeBottomLeft, - DragMode.ResizeBottomRight -> updateExpandedDrag(mouseX, mouseY, viewportWidth, viewportHeight) + DragMode.ResizeBottomRight, + -> updateExpandedDrag(mouseX, mouseY, viewportWidth, viewportHeight) DragMode.ScrollbarThumb -> updateScrollbarDrag(mouseY) DragMode.None -> Unit @@ -721,10 +739,11 @@ class InspectorController( } viewportW = viewportWidth viewportH = viewportHeight - val currentRoot = root ?: run { - resetNativePresentation() - return null - } + val currentRoot = + root ?: run { + resetNativePresentation() + return null + } rebindSelection() expandedRect = clampExpandedRect(expandedRect, viewportWidth, viewportHeight) clampMinimizedPosition(viewportWidth, viewportHeight) @@ -745,7 +764,7 @@ class InspectorController( private fun buildExpandedDomSnapshot( root: DOMNode, viewportWidth: Int, - viewportHeight: Int + viewportHeight: Int, ): InspectorDomSnapshot { val clamped = clampExpandedRect(expandedRect, viewportWidth, viewportHeight) expandedRect = clamped @@ -759,7 +778,11 @@ class InspectorController( Rect(clamped.x + 6, clamped.y + 5, clamped.width - 12, (titleFontSizePx + 16).coerceAtLeast(44)) headerBounds = headerRect val pickOn = mode == InspectorMode.Pick - val selectedShort = selectedNode?.key?.toString()?.take(18) ?: "none" + val selectedShort = + selectedNode + ?.key + ?.toString() + ?.take(18) ?: "none" val headerText = "Inspector Pick:${if (pickOn) "ON" else "OFF"} Sel:$selectedShort" val headerButtonHeight = (secondaryFontSizePx + 10).coerceAtLeast(26) @@ -770,12 +793,13 @@ class InspectorController( panelActions += PanelAction(minimizeRect, ActionKind.Minimize) val bodyTop = headerRect.y + headerRect.height + 6 - val bodyRect = Rect( - clamped.x + 6, - bodyTop, - clamped.width - 12, - (clamped.height - (bodyTop - clamped.y) - 4).coerceAtLeast(24) - ) + val bodyRect = + Rect( + clamped.x + 6, + bodyTop, + clamped.width - 12, + (clamped.height - (bodyTop - clamped.y) - 4).coerceAtLeast(24), + ) contentBounds = bodyRect val infoLines = ArrayList(128) @@ -785,23 +809,26 @@ class InspectorController( var y = bodyRect.y val maxChars = InspectorPresentationSupport.estimateMaxChars(bodyRect.width - 12, textFontSizePx) - val buttonLabelMaxChars = InspectorPresentationSupport.estimateMaxChars( - (clamped.width - 32).coerceAtLeast(40), - secondaryFontSizePx - ) + val buttonLabelMaxChars = + InspectorPresentationSupport.estimateMaxChars( + (clamped.width - 32).coerceAtLeast(40), + secondaryFontSizePx, + ) y = appendDomLine(infoLines, y, "F12 toggle, F9 mode, Esc cancel pick", maxChars) - y = appendDomLine( - infoLines, - y, - "Hovered: ${hoveredNode?.let { InspectorPresentationSupport.nodeLabel(it) } ?: "none"}", - maxChars - ) - y = appendDomLine( - infoLines, - y, - "Selected: ${selectedNode?.let { InspectorPresentationSupport.nodeLabel(it) } ?: "none"}", - maxChars - ) + y = + appendDomLine( + infoLines, + y, + "Hovered: ${hoveredNode?.let { InspectorPresentationSupport.nodeLabel(it) } ?: "none"}", + maxChars, + ) + y = + appendDomLine( + infoLines, + y, + "Selected: ${selectedNode?.let { InspectorPresentationSupport.nodeLabel(it) } ?: "none"}", + maxChars, + ) y = appendDomLine(infoLines, y, "Inspector handled last: $lastHandledPointerEvent", maxChars) y = appendDomLine(infoLines, y, "Pointer over Inspector: $pointerOverInspectorUi", maxChars) y = appendDomLine(infoLines, y, "Hover pick enabled: $hoverPickEnabled", maxChars) @@ -824,45 +851,50 @@ class InspectorController( parentLabel = null, childLabels = emptyList(), styleEditorHeight = 0, - styleLines = emptyList() + styleLines = emptyList(), ) } - val pathLines = InspectorPresentationSupport.wrapPathLines( - InspectorPresentationSupport.pathToNode(root, selected), - maxChars - ) + val pathLines = + InspectorPresentationSupport.wrapPathLines( + InspectorPresentationSupport.pathToNode(root, selected), + maxChars, + ) pathLines.forEach { line -> infoLines += line y += lineHeightPx } val boxes = InspectorGeometrySupport.computeBoxes(selected) - y = appendDomLine( - infoLines, - y, - "Border box: ${InspectorPresentationSupport.rectLabel(boxes.border)}", - maxChars - ) - y = appendDomLine( - infoLines, - y, - "Content box: ${InspectorPresentationSupport.rectLabel(boxes.content)}", - maxChars - ) - y = appendDomLine( - infoLines, - y, - "Margin box: ${InspectorPresentationSupport.rectLabel(boxes.margin)}", - maxChars - ) + y = + appendDomLine( + infoLines, + y, + "Border box: ${InspectorPresentationSupport.rectLabel(boxes.border)}", + maxChars, + ) + y = + appendDomLine( + infoLines, + y, + "Content box: ${InspectorPresentationSupport.rectLabel(boxes.content)}", + maxChars, + ) + y = + appendDomLine( + infoLines, + y, + "Margin box: ${InspectorPresentationSupport.rectLabel(boxes.margin)}", + maxChars, + ) boxes.parentContent?.let { y = appendDomLine(infoLines, y, "Parent content: ${InspectorPresentationSupport.rectLabel(it)}", maxChars) } - val localPos = selected.parent?.let { parent -> - val parentContent = InspectorGeometrySupport.contentRect(parent) - "${selected.bounds.x - parentContent.x},${selected.bounds.y - parentContent.y}" - } ?: "${selected.bounds.x},${selected.bounds.y}" + val localPos = + selected.parent?.let { parent -> + val parentContent = InspectorGeometrySupport.contentRect(parent) + "${selected.bounds.x - parentContent.x},${selected.bounds.y - parentContent.y}" + } ?: "${selected.bounds.x},${selected.bounds.y}" y = appendDomLine(infoLines, y, "Local pos: $localPos", maxChars) selected.inspectorScrollOffset()?.let { (sx, sy) -> y = appendDomLine(infoLines, y, "Scroll: x=$sx y=$sy", maxChars) @@ -881,7 +913,8 @@ class InspectorController( y = appendDomLine(infoLines, y, "Children:", maxChars) for (index in children.indices) { val child = children[index] - childLabels += ellipsize("[$index] ${InspectorPresentationSupport.nodeLabel(child)}", buttonLabelMaxChars) + childLabels += + ellipsize("[$index] ${InspectorPresentationSupport.nodeLabel(child)}", buttonLabelMaxChars) val row = Rect(clamped.x + 10, y - panelScrollY, clamped.width - 20, rowHeightPx) panelActions += PanelAction(row, ActionKind.Child, index) y += rowHeightPx + 2 @@ -890,40 +923,42 @@ class InspectorController( val inspection = selectionStyle(selected) val styleEditorStartY = y + 2 - val styleEditorSnapshots = styleEditorSnapshotBuilder.build( - InspectorStyleEditorSnapshotBuildContext( - panelRect = clamped, - panelBounds = panelBounds, - selected = selected, - inspection = inspection, - editableProperties = editablePropertiesFor(selected), - startY = styleEditorStartY, - lineHeightPx = lineHeightPx, - rowHeightPx = rowHeightPx, - secondaryFontSizePx = secondaryFontSizePx, - pointerProjectionScrollY = resolvedNativePointerProjectionScrollY(), - mouseX = mouseX, - mouseY = mouseY, - viewportWidth = viewportW, - viewportHeight = viewportH, - openValueSelectProperty = openValueSelectProperty, - openUnitSelectProperty = openUnitSelectProperty, - openValueSelectScrollIndex = openValueSelectScrollIndex, - openUnitSelectScrollIndex = openUnitSelectScrollIndex + val styleEditorSnapshots = + styleEditorSnapshotBuilder.build( + InspectorStyleEditorSnapshotBuildContext( + panelRect = clamped, + panelBounds = panelBounds, + selected = selected, + inspection = inspection, + editableProperties = editablePropertiesFor(selected), + startY = styleEditorStartY, + lineHeightPx = lineHeightPx, + rowHeightPx = rowHeightPx, + secondaryFontSizePx = secondaryFontSizePx, + pointerProjectionScrollY = resolvedNativePointerProjectionScrollY(), + mouseX = mouseX, + mouseY = mouseY, + viewportWidth = viewportW, + viewportHeight = viewportH, + openValueSelectProperty = openValueSelectProperty, + openUnitSelectProperty = openUnitSelectProperty, + openValueSelectScrollIndex = openValueSelectScrollIndex, + openUnitSelectScrollIndex = openUnitSelectScrollIndex, + ), ) - ) y = styleEditorSnapshots.endY nativeVariableTooltip = styleEditorSnapshots.variableTooltip nativeStyleEditorRows.addAll(styleEditorSnapshots.rows) nativeDropdowns.addAll(styleEditorSnapshots.dropdowns) styleEditorSnapshots.dropdownLayouts.forEach { layout -> - dropdownLayouts += DropdownLayout( - rect = layout.rect, - property = layout.property, - isUnit = layout.unitSelect, - totalOptions = layout.totalOptions, - visibleRows = layout.visibleRows - ) + dropdownLayouts += + DropdownLayout( + rect = layout.rect, + property = layout.property, + isUnit = layout.unitSelect, + totalOptions = layout.totalOptions, + visibleRows = layout.visibleRows, + ) } styleEditorSnapshots.actionSpecs.forEach { action -> panelActions += toPanelAction(action) @@ -935,9 +970,10 @@ class InspectorController( val styleEditorHeight = (y - styleEditorStartY).coerceAtLeast(0) y = appendDomLine(infoLines, y, "Computed styles:", maxChars) - styleLines = styleRows(inspection).flatMap { line -> - InspectorPresentationSupport.wrapText(line, maxChars) - } + styleLines = + styleRows(inspection).flatMap { line -> + InspectorPresentationSupport.wrapText(line, maxChars) + } y += styleLines.size * lineHeightPx panelContentHeight = (y - bodyRect.y).coerceAtLeast(0) @@ -956,14 +992,11 @@ class InspectorController( parentLabel = parentLabel, childLabels = childLabels, styleEditorHeight = styleEditorHeight, - styleLines = styleLines + styleLines = styleLines, ) } - private fun buildMinimizedDomSnapshot( - viewportWidth: Int, - viewportHeight: Int - ): InspectorDomSnapshot { + private fun buildMinimizedDomSnapshot(viewportWidth: Int, viewportHeight: Int): InspectorDomSnapshot { val chipWidth = minimizedWidth().coerceAtLeast(minChipWidth) val chipHeight = minimizedHeight() clampMinimizedPosition(viewportWidth, viewportHeight) @@ -982,9 +1015,18 @@ class InspectorController( scrollbarThumbRect = Rect(0, 0, 0, 0) val badge = if (mode == InspectorMode.Pick) "[Pick]" else "[Locked]" - val selectedShort = selectedNode?.key?.toString()?.let { " $it" } ?: "" + val selectedShort = + selectedNode + ?.key + ?.toString() + ?.let { " $it" } ?: "" val maxChars = InspectorPresentationSupport.estimateMaxChars(chipRect.width - 12, secondaryFontSizePx) - val lines = InspectorPresentationSupport.wrapMinimizedLabel("Inspector $badge$selectedShort", maxChars, maxLines = 2) + val lines = + InspectorPresentationSupport.wrapMinimizedLabel( + "Inspector $badge$selectedShort", + maxChars, + maxLines = 2, + ) return InspectorDomSnapshot( panelState = InspectorPanelState.Minimized, panelRect = chipRect, @@ -996,7 +1038,7 @@ class InspectorController( parentLabel = null, childLabels = emptyList(), styleEditorHeight = 0, - styleLines = emptyList() + styleLines = emptyList(), ) } @@ -1004,40 +1046,38 @@ class InspectorController( lines: MutableList, y: Int, text: String, - maxChars: Int + maxChars: Int, ): Int { val wrapped = InspectorPresentationSupport.wrapText(text, maxChars) lines += wrapped return y + wrapped.size * lineHeightPx } - private fun captureNativeHighlightsAndTooltips( - selected: DOMNode?, - viewportWidth: Int, - viewportHeight: Int - ) { - nativeSelectedHighlight = selected?.let { node -> - val boxes = InspectorGeometrySupport.computeHighlightBoxes(node) - InspectorHighlightSnapshot( - marginRect = boxes.margin, - borderRect = boxes.border, - paddingRect = boxes.padding, - contentRect = boxes.content, - parentContentRect = boxes.parentContent - ) - } - if (hoverPickEnabled) { - val hovered = hoveredNode - nativeHoveredHighlight = hovered?.let { node -> + private fun captureNativeHighlightsAndTooltips(selected: DOMNode?, viewportWidth: Int, viewportHeight: Int) { + nativeSelectedHighlight = + selected?.let { node -> val boxes = InspectorGeometrySupport.computeHighlightBoxes(node) InspectorHighlightSnapshot( marginRect = boxes.margin, borderRect = boxes.border, paddingRect = boxes.padding, contentRect = boxes.content, - parentContentRect = boxes.parentContent + parentContentRect = boxes.parentContent, ) } + if (hoverPickEnabled) { + val hovered = hoveredNode + nativeHoveredHighlight = + hovered?.let { node -> + val boxes = InspectorGeometrySupport.computeHighlightBoxes(node) + InspectorHighlightSnapshot( + marginRect = boxes.margin, + borderRect = boxes.border, + paddingRect = boxes.padding, + contentRect = boxes.content, + parentContentRect = boxes.parentContent, + ) + } if (hovered != null) { val label = resolveTooltipLabel(hovered) val boxW = (label.length * (secondaryFontSizePx / 2) + 18).coerceIn(140, viewportWidth - 8) @@ -1049,16 +1089,15 @@ class InspectorController( } private fun toPanelAction(action: InspectorStyleEditorActionSpec): PanelAction { - fun editAction(operation: EditOperation): PanelAction { - return PanelAction( + fun editAction(operation: EditOperation): PanelAction = + PanelAction( bounds = action.bounds, kind = ActionKind.EditProperty, property = requireNotNull(action.property), editOperation = operation, step = action.step, - payload = action.payload + payload = action.payload, ) - } return when (action.type) { InspectorStyleEditorActionType.ResetProperty -> editAction(EditOperation.ResetProperty) InspectorStyleEditorActionType.ToggleValueSelect -> editAction(EditOperation.ToggleValueSelect) @@ -1068,15 +1107,17 @@ class InspectorController( InspectorStyleEditorActionType.Increment -> editAction(EditOperation.Increment) InspectorStyleEditorActionType.ToggleUnitSelect -> editAction(EditOperation.ToggleUnitSelect) InspectorStyleEditorActionType.SelectUnitOption -> editAction(EditOperation.SelectUnitOption) - InspectorStyleEditorActionType.ResetSelectedOverrides -> PanelAction( - bounds = action.bounds, - kind = ActionKind.ResetSelectedOverrides - ) + InspectorStyleEditorActionType.ResetSelectedOverrides -> + PanelAction( + bounds = action.bounds, + kind = ActionKind.ResetSelectedOverrides, + ) - InspectorStyleEditorActionType.ClearAllOverrides -> PanelAction( - bounds = action.bounds, - kind = ActionKind.ClearAllOverrides - ) + InspectorStyleEditorActionType.ClearAllOverrides -> + PanelAction( + bounds = action.bounds, + kind = ActionKind.ClearAllOverrides, + ) } } @@ -1087,44 +1128,44 @@ class InspectorController( return } val trackWidth = 4 - val track = Rect( - bodyRect.x + bodyRect.width - trackWidth - 2, - bodyRect.y + 2, - trackWidth, - (bodyRect.height - 4).coerceAtLeast(8) - ) + val track = + Rect( + bodyRect.x + bodyRect.width - trackWidth - 2, + bodyRect.y + 2, + trackWidth, + (bodyRect.height - 4).coerceAtLeast(8), + ) val maxScroll = (panelContentHeight - bodyRect.height).coerceAtLeast(1) val thumbHeight = ((track.height.toFloat() * bodyRect.height.toFloat() / panelContentHeight.toFloat()).toInt()).coerceIn( 10, - track.height + track.height, ) val travel = (track.height - thumbHeight).coerceAtLeast(0) val thumbY = track.y + ((panelScrollY.toFloat() / maxScroll.toFloat()) * travel.toFloat()).toInt() scrollbarTrackRect = track scrollbarThumbRect = Rect(track.x, thumbY, track.width, thumbHeight) } - internal fun overlayPickToggleBounds(): Rect? { - return panelActions.lastOrNull { it.kind == ActionKind.TogglePick }?.bounds - } - internal fun overlayMinimizeBounds(): Rect? { - return panelActions.lastOrNull { it.kind == ActionKind.Minimize }?.bounds - } + internal fun overlayPickToggleBounds(): Rect? = panelActions.lastOrNull { it.kind == ActionKind.TogglePick }?.bounds + + internal fun overlayMinimizeBounds(): Rect? = panelActions.lastOrNull { it.kind == ActionKind.Minimize }?.bounds internal fun overlayContentRect(): Rect = contentBounds - internal fun overlayScrollbarThumbRect(): Rect = if (nativeDomBodyScrollStateActive) { - nativeDomScrollbarThumbRectOverride ?: Rect(0, 0, 0, 0) - } else { - scrollbarThumbRect - } + internal fun overlayScrollbarThumbRect(): Rect = + if (nativeDomBodyScrollStateActive) { + nativeDomScrollbarThumbRectOverride ?: Rect(0, 0, 0, 0) + } else { + scrollbarThumbRect + } - internal fun overlayScrollbarTrackRect(): Rect = if (nativeDomBodyScrollStateActive) { - nativeDomScrollbarTrackRectOverride ?: Rect(0, 0, 0, 0) - } else { - scrollbarTrackRect - } + internal fun overlayScrollbarTrackRect(): Rect = + if (nativeDomBodyScrollStateActive) { + nativeDomScrollbarTrackRectOverride ?: Rect(0, 0, 0, 0) + } else { + scrollbarTrackRect + } internal fun onNativeDomBodyScrollState(scrollY: Int, trackRect: Rect?, thumbRect: Rect?) { nativeDomBodyScrollStateActive = true @@ -1156,7 +1197,12 @@ class InspectorController( } } - internal fun onNativeDomMinimizedPanelPosition(x: Int, y: Int, viewportWidth: Int, viewportHeight: Int) { + internal fun onNativeDomMinimizedPanelPosition( + x: Int, + y: Int, + viewportWidth: Int, + viewportHeight: Int, + ) { minimizedPosX = x minimizedPosY = y clampMinimizedPosition(viewportWidth, viewportHeight) @@ -1190,20 +1236,19 @@ class InspectorController( } val selected = selectedNode ?: return emptyList() val literal = literalForEdit(selected, property) - val descriptor = InspectorEditorRegistry.describe( - property = property, - literal = literal, - expression = StyleEngine.inspectorOverrideFor(selected, property) - ) + val descriptor = + InspectorEditorRegistry.describe( + property = property, + literal = literal, + expression = StyleEngine.inspectorOverrideFor(selected, property), + ) if (descriptor.kind != InspectorEditorKind.EnumSelect && descriptor.kind != InspectorEditorKind.FontSelect) { return emptyList() } return descriptor.options } - internal fun hasOpenStyleDropdown(): Boolean { - return openValueSelectProperty != null || openUnitSelectProperty != null - } + internal fun hasOpenStyleDropdown(): Boolean = openValueSelectProperty != null || openUnitSelectProperty != null internal fun closeOpenStyleDropdowns(): Boolean { if (!hasOpenStyleDropdown()) return false @@ -1221,11 +1266,12 @@ class InspectorController( val steps = (kotlin.math.abs(delta) / 120).coerceAtLeast(1) val current = if (openDropdown.unitSelect) openUnitSelectScrollIndex else openValueSelectScrollIndex - val next = if (delta < 0) { - (current + steps).coerceAtMost(maxFirst) - } else { - (current - steps).coerceAtLeast(0) - } + val next = + if (delta < 0) { + (current + steps).coerceAtMost(maxFirst) + } else { + (current - steps).coerceAtLeast(0) + } if (next == current) return true if (openDropdown.unitSelect) { @@ -1238,15 +1284,17 @@ class InspectorController( internal fun debugActiveEditBuffer(): String? = activeEditProperty?.let { activeEditBuffer } - internal fun debugActiveEditCaret(): Int? = activeEditProperty?.let { - activeEditState.clampToLength(activeEditBuffer.length) - activeEditState.caretIndex - } + internal fun debugActiveEditCaret(): Int? = + activeEditProperty?.let { + activeEditState.clampToLength(activeEditBuffer.length) + activeEditState.caretIndex + } - internal fun debugActiveEditSelectionRange(): Pair? = activeEditProperty?.let { - activeEditState.clampToLength(activeEditBuffer.length) - activeEditState.selectionStart() to activeEditState.selectionEnd() - } + internal fun debugActiveEditSelectionRange(): Pair? = + activeEditProperty?.let { + activeEditState.clampToLength(activeEditBuffer.length) + activeEditState.selectionStart() to activeEditState.selectionEnd() + } internal fun onPickTogglePressed() { setPickMode(mode != InspectorMode.Pick) @@ -1278,7 +1326,7 @@ class InspectorController( operation = EditOperation.ResetProperty, step = 1f, payload = null, - actionBounds = Rect(0, 0, 0, 0) + actionBounds = Rect(0, 0, 0, 0), ) mode = InspectorMode.Locked return true @@ -1292,7 +1340,7 @@ class InspectorController( operation = EditOperation.ToggleValueSelect, step = 1f, payload = null, - actionBounds = Rect(0, 0, 0, 0) + actionBounds = Rect(0, 0, 0, 0), ) mode = InspectorMode.Locked return true @@ -1306,7 +1354,7 @@ class InspectorController( operation = EditOperation.SelectValueOption, step = 1f, payload = option, - actionBounds = Rect(0, 0, 0, 0) + actionBounds = Rect(0, 0, 0, 0), ) mode = InspectorMode.Locked return true @@ -1320,7 +1368,7 @@ class InspectorController( operation = EditOperation.Increment, step = StylePropertyRegistry.descriptor(property).numericStep, payload = null, - actionBounds = Rect(0, 0, 0, 0) + actionBounds = Rect(0, 0, 0, 0), ) mode = InspectorMode.Locked return true @@ -1334,7 +1382,7 @@ class InspectorController( operation = EditOperation.Decrement, step = StylePropertyRegistry.descriptor(property).numericStep, payload = null, - actionBounds = Rect(0, 0, 0, 0) + actionBounds = Rect(0, 0, 0, 0), ) mode = InspectorMode.Locked return true @@ -1348,7 +1396,7 @@ class InspectorController( operation = EditOperation.ToggleUnitSelect, step = 1f, payload = null, - actionBounds = Rect(0, 0, 0, 0) + actionBounds = Rect(0, 0, 0, 0), ) mode = InspectorMode.Locked return true @@ -1362,7 +1410,7 @@ class InspectorController( operation = EditOperation.SelectUnitOption, step = 1f, payload = option, - actionBounds = Rect(0, 0, 0, 0) + actionBounds = Rect(0, 0, 0, 0), ) mode = InspectorMode.Locked return true @@ -1391,7 +1439,7 @@ class InspectorController( private data class OpenStyleDropdown( val unitSelect: Boolean, - val optionCount: Int + val optionCount: Int, ) private fun resolveOpenStyleDropdown(): OpenStyleDropdown? { @@ -1405,11 +1453,12 @@ class InspectorController( val valueProperty = openValueSelectProperty ?: return null val selected = selectedNode ?: return null val literal = literalForEdit(selected, valueProperty) - val editor = InspectorEditorRegistry.describe( - property = valueProperty, - literal = literal, - expression = StyleEngine.inspectorOverrideFor(selected, valueProperty) - ) + val editor = + InspectorEditorRegistry.describe( + property = valueProperty, + literal = literal, + expression = StyleEngine.inspectorOverrideFor(selected, valueProperty), + ) val optionCount = editor.options.size if (optionCount <= 0) return null return OpenStyleDropdown(unitSelect = false, optionCount = optionCount) @@ -1429,7 +1478,11 @@ class InspectorController( } } - internal fun overlayApplyNumericOverride(property: StyleProperty, numericLiteral: String, unitToken: String?): Boolean { + internal fun overlayApplyNumericOverride( + property: StyleProperty, + numericLiteral: String, + unitToken: String?, + ): Boolean { val selected = selectedNode ?: return false val numberText = numericLiteral.trim() if (numberText.isEmpty() || numberText == "-" || numberText == "." || numberText == "-.") { @@ -1461,6 +1514,7 @@ class InspectorController( nativeDomScrollbarTrackRectOverride = null nativeDomScrollbarThumbRectOverride = null } + private fun resolvedNativePointerProjectionScrollY(): Int { val current = nativeDomPanelScrollYOverride return when { @@ -1515,13 +1569,14 @@ class InspectorController( } private fun rebindSelection() { - val currentRoot = root ?: run { - selectedNode = null - selectedKeyToken = null - selectedClass = null - cachedStyle = null - return - } + val currentRoot = + root ?: run { + selectedNode = null + selectedKeyToken = null + selectedClass = null + cachedStyle = null + return + } val key = selectedKeyToken val klass = selectedClass if (key == null || klass == null) { @@ -1555,12 +1610,13 @@ class InspectorController( ) { return } - val currentRoot = root ?: run { - hoveredPath = emptyList() - hoveredNode = null - hoverDirty = false - return - } + val currentRoot = + root ?: run { + hoveredPath = emptyList() + hoveredNode = null + hoverDirty = false + return + } hoveredPath = collectHoverChain(currentRoot, mouseX, mouseY) hoveredNode = resolveInspectorHoverCandidate(hoveredPath) lastHoverMouseX = mouseX @@ -1569,13 +1625,10 @@ class InspectorController( hoverDirty = false } - private fun resolveInspectorHoverCandidate(path: List): DOMNode? { - return path.lastOrNull { node -> shouldInspectorPickNode(node) } - } + private fun resolveInspectorHoverCandidate(path: List): DOMNode? = + path.lastOrNull { node -> shouldInspectorPickNode(node) } - private fun shouldInspectorPickNode(node: DOMNode): Boolean { - return node.display != Display.None - } + private fun shouldInspectorPickNode(node: DOMNode): Boolean = node.display != Display.None private fun resolveTooltipLabel(node: DOMNode): String { val bounds = node.bounds @@ -1601,12 +1654,13 @@ class InspectorController( return cached.inspection } val inspection = StyleEngine.inspect(node) - cachedStyle = SelectionStyleCache( - key = key, - nodeClass = klass, - layoutVersion = layoutVersion, - inspection = inspection - ) + cachedStyle = + SelectionStyleCache( + key = key, + nodeClass = klass, + layoutVersion = layoutVersion, + inspection = inspection, + ) return inspection } @@ -1627,11 +1681,11 @@ class InspectorController( } return rows } - private fun findPanelAction(mouseX: Int, mouseY: Int): PanelAction? { - return panelActions.lastOrNull { action -> + + private fun findPanelAction(mouseX: Int, mouseY: Int): PanelAction? = + panelActions.lastOrNull { action -> isPanelActionVisibleHit(action, mouseX, mouseY) } - } private fun isPanelActionVisibleHit(action: PanelAction, mouseX: Int, mouseY: Int): Boolean { if (!action.bounds.contains(mouseX, mouseY)) return false @@ -1646,19 +1700,19 @@ class InspectorController( val expectedOp = if (layout.isUnit) EditOperation.SelectUnitOption else EditOperation.SelectValueOption return panelActions.lastOrNull { action -> action.kind == ActionKind.EditProperty && - action.property == layout.property && - action.editOperation == expectedOp && - action.bounds.contains(mouseX, mouseY) + action.property == layout.property && + action.editOperation == expectedOp && + action.bounds.contains(mouseX, mouseY) } } - internal fun overlayColorPickerActionBounds(property: StyleProperty): Rect? { - return panelActions.lastOrNull { - it.kind == ActionKind.EditProperty && + internal fun overlayColorPickerActionBounds(property: StyleProperty): Rect? = + panelActions + .lastOrNull { + it.kind == ActionKind.EditProperty && it.property == property && it.editOperation == EditOperation.OpenColorPicker - }?.bounds - } + }?.bounds internal fun debugOpenColorPickerForSelection(property: StyleProperty, anchorRect: Rect): Boolean { val selected = selectedNode ?: return false @@ -1757,13 +1811,14 @@ class InspectorController( val isTextLike = selected.styleType == "text" || selected.styleType.contains("text") if (!isTextLike) return all - val priority = listOf( - StyleProperty.FOREGROUND_COLOR, - StyleProperty.FONT_ID, - StyleProperty.FONT_SIZE, - StyleProperty.TEXT_WRAP, - StyleProperty.ALIGN - ) + val priority = + listOf( + StyleProperty.FOREGROUND_COLOR, + StyleProperty.FONT_ID, + StyleProperty.FONT_SIZE, + StyleProperty.TEXT_WRAP, + StyleProperty.ALIGN, + ) val ordered = ArrayList(all.size) priority.forEach { property -> if (property in all) ordered += property @@ -1780,7 +1835,7 @@ class InspectorController( operation: EditOperation, step: Float, payload: String?, - actionBounds: Rect + actionBounds: Rect, ) { runCatching { when (operation) { @@ -1826,11 +1881,12 @@ class InspectorController( val current = literalForEdit(selected, property) val parsed = InspectorEditorRegistry.parseNumericLiteral(property, current) val numberText = parsed?.numberText ?: "0" - val nextLiteral = InspectorEditorRegistry.formatNumericLiteral( - property = property, - numberText = numberText, - unitToken = payload - ) + val nextLiteral = + InspectorEditorRegistry.formatNumericLiteral( + property = property, + numberText = numberText, + unitToken = payload, + ) StyleEngine.setInspectorOverrideLiteral(selected, property, nextLiteral).getOrThrow() openUnitSelectProperty = null openUnitSelectScrollIndex = 0 @@ -1846,25 +1902,26 @@ class InspectorController( private fun beginTextEdit(selected: DOMNode, property: StyleProperty, actionBounds: Rect) { if (activeEditProperty != property) { val current = literalForEdit(selected, property) - val descriptor = InspectorEditorRegistry.describe( - property = property, - literal = current, - expression = StyleEngine.inspectorOverrideFor(selected, property) - ) + val descriptor = + InspectorEditorRegistry.describe( + property = property, + literal = current, + expression = StyleEngine.inspectorOverrideFor(selected, property), + ) if (descriptor.kind == InspectorEditorKind.NumericInput) { val parsed = InspectorEditorRegistry.parseNumericLiteral(property, current) editSession.begin( property = property, initialBuffer = parsed?.numberText ?: "0", initialUnit = parsed?.unit ?: InspectorEditorRegistry.defaultNumericUnit(property), - isNumeric = true + isNumeric = true, ) } else { editSession.begin( property = property, initialBuffer = current, initialUnit = null, - isNumeric = false + isNumeric = false, ) } } else { @@ -1881,10 +1938,11 @@ class InspectorController( private fun updateActiveTextSelectionFromPointer(): Boolean { if (!editSession.textSelectionDragActive) return false - val property = editSession.textSelectionDragProperty ?: run { - stopActiveTextSelectionDrag() - return false - } + val property = + editSession.textSelectionDragProperty ?: run { + stopActiveTextSelectionDrag() + return false + } if (activeEditProperty != property) { stopActiveTextSelectionDrag() return false @@ -1915,6 +1973,7 @@ class InspectorController( val rawIndex = ((local + charWidth / 2) / charWidth) return rawIndex.coerceIn(0, text.length) } + private fun openColorPicker(selected: DOMNode, property: StyleProperty, anchorRect: Rect) { val literal = literalForEdit(selected, property) val parsedByStyle = runCatching { RgbaColor.fromArgbInt(parseColor(literal)) }.getOrNull() @@ -1924,13 +1983,14 @@ class InspectorController( colorPickerManager.open( anchorRect = anchorRect, title = "Edit ${property.key}", - state = ColorPickerState( - color = initialColor, - previous = initialColor, - mode = initialMode, - alphaEnabled = true, - closeOnSelect = false - ), + state = + ColorPickerState( + color = initialColor, + previous = initialColor, + mode = initialMode, + alphaEnabled = true, + closeOnSelect = false, + ), closeOnOutsideClick = false, onPreview = { color -> applyInspectorColorLiteral(selected, property, color) @@ -1940,13 +2000,19 @@ class InspectorController( }, onCommit = { color -> applyInspectorColorLiteral(selected, property, color) - } + }, ) } private fun applyInspectorColorLiteral(selected: DOMNode, property: StyleProperty, color: RgbaColor) { runCatching { - val argb = color.toArgbInt().toUInt().toString(16).uppercase().padStart(8, '0') + val argb = + color + .toArgbInt() + .toUInt() + .toString(16) + .uppercase() + .padStart(8, '0') StyleEngine.setInspectorOverrideLiteral(selected, property, "#$argb").getOrThrow() cachedStyle = null styleEditorError = null @@ -1967,20 +2033,22 @@ class InspectorController( val selected = selectedNode ?: return val property = activeEditProperty ?: return runCatching { - val literal = if (activeEditIsNumeric) { - InspectorEditorRegistry.formatNumericLiteral( - property = property, - numberText = activeEditBuffer, - unitToken = activeEditUnit?.token - ) - } else { - activeEditBuffer.trim() - } - val normalized = if (literal.isEmpty()) { - if (activeEditIsNumeric) InspectorEditorRegistry.defaultNumericLiteral(property) else "" - } else { - literal - } + val literal = + if (activeEditIsNumeric) { + InspectorEditorRegistry.formatNumericLiteral( + property = property, + numberText = activeEditBuffer, + unitToken = activeEditUnit?.token, + ) + } else { + activeEditBuffer.trim() + } + val normalized = + if (literal.isEmpty()) { + if (activeEditIsNumeric) InspectorEditorRegistry.defaultNumericLiteral(property) else "" + } else { + literal + } StyleEngine.setInspectorOverrideLiteral(selected, property, normalized).getOrThrow() cachedStyle = null styleEditorError = null @@ -2007,12 +2075,13 @@ class InspectorController( } StyleEditorValueType.LengthPx -> { - val base = runCatching { - parseLengthPxInt( - raw = current, - allowNegative = false - ) - }.getOrElse { descriptor.minInt } + val base = + runCatching { + parseLengthPxInt( + raw = current, + allowNegative = false, + ) + }.getOrElse { descriptor.minInt } val next = (base + delta.toInt()).coerceAtLeast(descriptor.minInt) pxLiteral(next) } @@ -2022,8 +2091,9 @@ class InspectorController( if (normalized == "normal") { CssLength(delta.coerceAtLeast(0f), CssUnit.Px).toCssLiteral() } else { - val base = runCatching { parseCssLength(current, allowUnitlessZero = true) } - .getOrElse { CssLength.ZERO_PX } + val base = + runCatching { parseCssLength(current, allowUnitlessZero = true) } + .getOrElse { CssLength.ZERO_PX } val next = (base.value + delta).coerceAtLeast(0f) CssLength(next, base.unit).toCssLiteral() } @@ -2036,10 +2106,11 @@ class InspectorController( } StyleEditorValueType.OptionalLengthPx -> { - val base = parseOptionalLengthPxInt( - raw = current, - allowNegative = false - ) ?: descriptor.minInt + val base = + parseOptionalLengthPxInt( + raw = current, + allowNegative = false, + ) ?: descriptor.minInt val next = (base + delta.toInt()).coerceAtLeast(descriptor.minInt) pxLiteral(next) } @@ -2052,10 +2123,11 @@ class InspectorController( StyleEditorValueType.SpacingLengthPx -> { val allowNegative = property == StyleProperty.MARGIN - val currentInsets = parseSpacingShorthand( - raw = current, - allowNegative = allowNegative - ) + val currentInsets = + parseSpacingShorthand( + raw = current, + allowNegative = allowNegative, + ) val rawNext = currentInsets.top + delta.toInt() val next = if (allowNegative) rawNext else rawNext.coerceAtLeast(0) pxLiteral(next) @@ -2071,15 +2143,14 @@ class InspectorController( } } - private fun expressionLabel(expression: StyleExpression): String { - return when (expression) { + private fun expressionLabel(expression: StyleExpression): String = + when (expression) { is StyleExpression.Literal -> expression.value is StyleExpression.VariableRef -> "var(${expression.name})" } - } - private fun literalFromComputed(style: ComputedStyle, property: StyleProperty): String { - return when (property) { + private fun literalFromComputed(style: ComputedStyle, property: StyleProperty): String = + when (property) { StyleProperty.MARGIN -> spacingLiteral(style.margin) StyleProperty.PADDING -> spacingLiteral(style.padding) StyleProperty.BACKGROUND_COLOR -> style.backgroundColor?.let(::colorLabel) ?: "none" @@ -2089,21 +2160,30 @@ class InspectorController( StyleProperty.BORDER_RADIUS -> style.borderRadius.toCssLiteral() StyleProperty.FOREGROUND_COLOR -> colorLabel(style.foregroundColor) StyleProperty.FONT_ID -> style.fontId ?: "minecraft" - StyleProperty.FONT_SIZE -> style.fontSizeValue?.toCssLiteral() ?: (style.fontSize?.let(::pxLiteral) - ?: "auto") - StyleProperty.LINE_HEIGHT -> when (val lineHeightValue = style.lineHeight) { - is LineHeightValue.Length -> lineHeightValue.value.toCssLiteral() - LineHeightValue.Normal -> "normal" - } + StyleProperty.FONT_SIZE -> + style.fontSizeValue?.toCssLiteral() ?: ( + style.fontSize?.let(::pxLiteral) + ?: "auto" + ) + StyleProperty.LINE_HEIGHT -> + when (val lineHeightValue = style.lineHeight) { + is LineHeightValue.Length -> lineHeightValue.value.toCssLiteral() + LineHeightValue.Normal -> "normal" + } - StyleProperty.FONT_WEIGHT -> style.fontWeight.name.lowercase() - StyleProperty.FONT_STYLE -> style.fontStyle.name.lowercase() - StyleProperty.TEXT_DECORATION -> when (style.textDecoration) { - TextDecoration.None -> "none" - TextDecoration.Underline -> "underline" - TextDecoration.Strikethrough -> "strikethrough" - TextDecoration.UnderlineStrikethrough -> "underline-strikethrough" - } + StyleProperty.FONT_WEIGHT -> + style.fontWeight.name + .lowercase() + StyleProperty.FONT_STYLE -> + style.fontStyle.name + .lowercase() + StyleProperty.TEXT_DECORATION -> + when (style.textDecoration) { + TextDecoration.None -> "none" + TextDecoration.Underline -> "underline" + TextDecoration.Strikethrough -> "strikethrough" + TextDecoration.UnderlineStrikethrough -> "underline-strikethrough" + } StyleProperty.OBFUSCATED -> style.obfuscated.toString() StyleProperty.WIDTH -> style.width?.toCssLiteral() ?: "auto" @@ -2112,65 +2192,86 @@ class InspectorController( StyleProperty.MIN_HEIGHT -> style.minHeight?.toCssLiteral() ?: "auto" StyleProperty.MAX_WIDTH -> style.maxWidth?.toCssLiteral() ?: "auto" StyleProperty.MAX_HEIGHT -> style.maxHeight?.toCssLiteral() ?: "auto" - StyleProperty.ALIGN -> style.align.name.lowercase() - StyleProperty.DISPLAY -> style.display.name.lowercase() - StyleProperty.POSITION -> style.position.name.lowercase() + StyleProperty.ALIGN -> + style.align.name + .lowercase() + StyleProperty.DISPLAY -> + style.display.name + .lowercase() + StyleProperty.POSITION -> + style.position.name + .lowercase() StyleProperty.LEFT -> style.left?.toCssLiteral() ?: "auto" StyleProperty.TOP -> style.top?.toCssLiteral() ?: "auto" StyleProperty.RIGHT -> style.right?.toCssLiteral() ?: "auto" StyleProperty.BOTTOM -> style.bottom?.toCssLiteral() ?: "auto" StyleProperty.Z_INDEX -> style.zIndex.toString() - StyleProperty.OVERFLOW -> if (style.overflowX == style.overflowY) { - style.overflowX.name.lowercase() - } else { - "${style.overflowX.name.lowercase()} ${style.overflowY.name.lowercase()}" - } - StyleProperty.OVERFLOW_X -> style.overflowX.name.lowercase() - StyleProperty.OVERFLOW_Y -> style.overflowY.name.lowercase() - StyleProperty.FLEX_DIRECTION -> style.flexDirection.name.lowercase() - StyleProperty.JUSTIFY_CONTENT -> style.justifyContent.name - .replace(Regex("([a-z])([A-Z])"), "$1-$2") - .lowercase() - - StyleProperty.ALIGN_ITEMS -> style.alignItems.name.lowercase() - StyleProperty.JUSTIFY_ITEMS -> style.justifyItems.name.lowercase() + StyleProperty.OVERFLOW -> + if (style.overflowX == style.overflowY) { + style.overflowX.name + .lowercase() + } else { + "${style.overflowX.name.lowercase()} ${style.overflowY.name.lowercase()}" + } + StyleProperty.OVERFLOW_X -> + style.overflowX.name + .lowercase() + StyleProperty.OVERFLOW_Y -> + style.overflowY.name + .lowercase() + StyleProperty.FLEX_DIRECTION -> + style.flexDirection.name + .lowercase() + StyleProperty.JUSTIFY_CONTENT -> + style.justifyContent.name + .replace(Regex("([a-z])([A-Z])"), "$1-$2") + .lowercase() + + StyleProperty.ALIGN_ITEMS -> + style.alignItems.name + .lowercase() + StyleProperty.JUSTIFY_ITEMS -> + style.justifyItems.name + .lowercase() StyleProperty.GAP -> style.gap.toCssLiteral() StyleProperty.FLEX_GROW -> formatFloatLiteral(style.flexGrow) StyleProperty.FLEX_SHRINK -> formatFloatLiteral(style.flexShrink) StyleProperty.FLEX_BASIS -> style.flexBasis?.toCssLiteral() ?: "auto" StyleProperty.GRID_COLUMNS -> style.gridColumns.toString() StyleProperty.GRID_ROWS -> style.gridRows?.toString() ?: "auto" - StyleProperty.GRID_AUTO_FLOW -> style.gridAutoFlow.name.lowercase() + StyleProperty.GRID_AUTO_FLOW -> + style.gridAutoFlow.name + .lowercase() StyleProperty.GRID_COLUMN_SPAN -> style.gridColumnSpan.toString() StyleProperty.GRID_ROW_SPAN -> style.gridRowSpan.toString() StyleProperty.TEXT_WRAP -> if (style.textWrap == TextWrap.Wrap) "wrap" else "nowrap" - StyleProperty.TEXT_FORMATTING -> when (style.textFormatting) { - TextFormatting.None -> "none" - TextFormatting.Minecraft -> "minecraft" - } + StyleProperty.TEXT_FORMATTING -> + when (style.textFormatting) { + TextFormatting.None -> "none" + TextFormatting.Minecraft -> "minecraft" + } - StyleProperty.TRANSFORM -> buildString { - append("translate(") - append(style.transform.translateX) - append(",") - append(style.transform.translateY) - append(") scale(") - append(style.transform.scaleX) - append(",") - append(style.transform.scaleY) - append(") rotate(") - append(style.transform.rotateDeg) - append("deg)") - } + StyleProperty.TRANSFORM -> + buildString { + append("translate(") + append(style.transform.translateX) + append(",") + append(style.transform.translateY) + append(") scale(") + append(style.transform.scaleX) + append(",") + append(style.transform.scaleY) + append(") rotate(") + append(style.transform.rotateDeg) + append("deg)") + } StyleProperty.TRANSFORM_ORIGIN -> "${style.transformOrigin.originX} ${style.transformOrigin.originY}" StyleProperty.OPACITY -> formatFloatLiteral(style.opacity) } - } - private fun spacingLiteral(value: LengthInsets): String { - return "${value.top.toCssLiteral()} ${value.right.toCssLiteral()} ${value.bottom.toCssLiteral()} ${value.left.toCssLiteral()}" - } + private fun spacingLiteral(value: LengthInsets): String = + "${value.top.toCssLiteral()} ${value.right.toCssLiteral()} ${value.bottom.toCssLiteral()} ${value.left.toCssLiteral()}" private fun pxLiteral(value: Int): String = "${value}px" @@ -2249,11 +2350,13 @@ class InspectorController( mouseX: Int, mouseY: Int, viewportWidth: Int, - viewportHeight: Int + viewportHeight: Int, ) { val nextX = mouseX - dragStartOffsetX val nextY = mouseY - dragStartOffsetY - if (!dragMoved && (kotlin.math.abs(nextX - minimizedPosX) >= 2 || kotlin.math.abs(nextY - minimizedPosY) >= 2)) { + if (!dragMoved && + (kotlin.math.abs(nextX - minimizedPosX) >= 2 || kotlin.math.abs(nextY - minimizedPosY) >= 2) + ) { dragMoved = true } minimizedPosX = nextX @@ -2265,7 +2368,7 @@ class InspectorController( mouseX: Int, mouseY: Int, viewportWidth: Int, - viewportHeight: Int + viewportHeight: Int, ) { val dx = mouseX - dragStartMouseX val dy = mouseY - dragStartMouseY @@ -2276,13 +2379,14 @@ class InspectorController( when (dragMode) { DragMode.Move -> { - val movedRect = paneMoveDrag.update( - mouseX = mouseX, - mouseY = mouseY, - viewportWidth = viewportWidth, - viewportHeight = viewportHeight, - clamp = ::clampExpandedRect - ) + val movedRect = + paneMoveDrag.update( + mouseX = mouseX, + mouseY = mouseY, + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + clamp = ::clampExpandedRect, + ) expandedRect = movedRect if (paneMoveDrag.moved) { dragMoved = true @@ -2303,8 +2407,9 @@ class InspectorController( if (right - left < minPanelWidth) { when (dragMode) { DragMode.ResizeLeft, DragMode.ResizeTopLeft, DragMode.ResizeBottomLeft -> left = right - minPanelWidth - DragMode.ResizeRight, DragMode.ResizeTopRight, DragMode.ResizeBottomRight -> right = - left + minPanelWidth + DragMode.ResizeRight, DragMode.ResizeTopRight, DragMode.ResizeBottomRight -> + right = + left + minPanelWidth else -> Unit } @@ -2312,20 +2417,32 @@ class InspectorController( if (bottom - top < minPanelHeight) { when (dragMode) { DragMode.ResizeTop, DragMode.ResizeTopLeft, DragMode.ResizeTopRight -> top = bottom - minPanelHeight - DragMode.ResizeBottom, DragMode.ResizeBottomLeft, DragMode.ResizeBottomRight -> bottom = - top + minPanelHeight + DragMode.ResizeBottom, DragMode.ResizeBottomLeft, DragMode.ResizeBottomRight -> + bottom = + top + minPanelHeight else -> Unit } } - val resized = clampExpandedRect( - Rect(left, top, (right - left).coerceAtLeast(minPanelWidth), (bottom - top).coerceAtLeast(minPanelHeight)), - viewportWidth, - viewportHeight - ) - if (!dragMoved && (kotlin.math.abs(resized.width - dragStartRect.width) >= 2 || kotlin.math.abs(resized.height - dragStartRect.height) >= 2 || - kotlin.math.abs(resized.x - dragStartRect.x) >= 2 || kotlin.math.abs(resized.y - dragStartRect.y) >= 2) + val resized = + clampExpandedRect( + Rect( + left, + top, + (right - left).coerceAtLeast(minPanelWidth), + (bottom - top).coerceAtLeast(minPanelHeight), + ), + viewportWidth, + viewportHeight, + ) + if (!dragMoved && + ( + kotlin.math.abs(resized.width - dragStartRect.width) >= 2 || + kotlin.math.abs(resized.height - dragStartRect.height) >= 2 || + kotlin.math.abs(resized.x - dragStartRect.x) >= 2 || + kotlin.math.abs(resized.y - dragStartRect.y) >= 2 + ) ) { dragMoved = true } @@ -2368,16 +2485,17 @@ class InspectorController( viewportWidth: Int, viewportHeight: Int, boxWidth: Int, - boxHeight: Int + boxHeight: Int, ): Rect { val inspectorRect = currentInspectorRect() - val candidates = listOf( - mouseX + 12 to mouseY + 12, - mouseX + 12 to mouseY - boxHeight - 12, - mouseX - boxWidth - 12 to mouseY + 12, - mouseX - boxWidth - 12 to mouseY - boxHeight - 12, - mouseX + 16 to mouseY - boxHeight / 2 - ) + val candidates = + listOf( + mouseX + 12 to mouseY + 12, + mouseX + 12 to mouseY - boxHeight - 12, + mouseX - boxWidth - 12 to mouseY + 12, + mouseX - boxWidth - 12 to mouseY - boxHeight - 12, + mouseX + 16 to mouseY - boxHeight / 2, + ) candidates.forEach { (rawX, rawY) -> val candidate = clampTooltipRect(rawX, rawY, boxWidth, boxHeight, viewportWidth, viewportHeight) if (!rectIntersects(candidate, inspectorRect)) { @@ -2403,7 +2521,7 @@ class InspectorController( width: Int, height: Int, viewportWidth: Int, - viewportHeight: Int + viewportHeight: Int, ): Rect { val clampedX = x.coerceIn(2, (viewportWidth - width - 2).coerceAtLeast(2)) val clampedY = y.coerceIn(2, (viewportHeight - height - 2).coerceAtLeast(2)) @@ -2413,9 +2531,9 @@ class InspectorController( private fun rectIntersects(a: Rect, b: Rect): Boolean { if (a.width <= 0 || a.height <= 0 || b.width <= 0 || b.height <= 0) return false return a.x < b.x + b.width && - a.x + a.width > b.x && - a.y < b.y + b.height && - a.y + a.height > b.y + a.x + a.width > b.x && + a.y < b.y + b.height && + a.y + a.height > b.y } private fun isInsideInspectorUi(mouseX: Int, mouseY: Int): Boolean { @@ -2425,12 +2543,14 @@ class InspectorController( if (panelActions.any { action -> isPanelActionVisibleHit(action, mouseX, mouseY) }) { return true } - val bounds = when (panelState) { - InspectorPanelState.Expanded -> expandedRect - InspectorPanelState.Minimized -> Rect(minimizedPosX, minimizedPosY, minimizedWidth(), minimizedHeight()) - } + val bounds = + when (panelState) { + InspectorPanelState.Expanded -> expandedRect + InspectorPanelState.Minimized -> Rect(minimizedPosX, minimizedPosY, minimizedWidth(), minimizedHeight()) + } return bounds.contains(mouseX, mouseY) } + private fun clampExpandedRect(rect: Rect, viewportWidth: Int, viewportHeight: Int): Rect { val safeViewportW = viewportWidth.coerceAtLeast(minPanelWidth + viewportMargin * 2) val safeViewportH = viewportHeight.coerceAtLeast(minPanelHeight + viewportMargin * 2) @@ -2466,11 +2586,7 @@ class InspectorController( return false } - private fun findByKeyAndClass( - node: DOMNode, - key: Any, - klass: Class - ): DOMNode? { + private fun findByKeyAndClass(node: DOMNode, key: Any, klass: Class): DOMNode? { if (node.key == key && node.javaClass == klass) return node node.children.forEach { child -> val found = findByKeyAndClass(child, key, klass) @@ -2489,17 +2605,17 @@ class InspectorController( return "${node.styleType}:$key" } - private fun rectLabel(rect: Rect): String { - return "${rect.x},${rect.y},${rect.width}x${rect.height}" - } + private fun rectLabel(rect: Rect): String = "${rect.x},${rect.y},${rect.width}x${rect.height}" - private fun spacingLabel(value: Insets): String { - return "${value.top}/${value.right}/${value.bottom}/${value.left}" - } + private fun spacingLabel(value: Insets): String = "${value.top}/${value.right}/${value.bottom}/${value.left}" private fun colorLabel(color: Int): String { - val hex = color.toUInt().toString(16).uppercase().padStart(8, '0') + val hex = + color + .toUInt() + .toString(16) + .uppercase() + .padStart(8, '0') return "#$hex" } - } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorDomSnapshot.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorDomSnapshot.kt index a5a618f..d198b80 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorDomSnapshot.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorDomSnapshot.kt @@ -13,5 +13,5 @@ internal data class InspectorDomSnapshot( val parentLabel: String?, val childLabels: List, val styleEditorHeight: Int, - val styleLines: List + val styleLines: List, ) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorEditSession.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorEditSession.kt index 357051d..7444add 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorEditSession.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorEditSession.kt @@ -23,7 +23,7 @@ internal class InspectorEditSession { property: StyleProperty, initialBuffer: String, initialUnit: CssUnit?, - isNumeric: Boolean + isNumeric: Boolean, ) { activeProperty = property activeBuffer = initialBuffer diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorEditorRegistry.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorEditorRegistry.kt index 4e01fa4..818ebf7 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorEditorRegistry.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorEditorRegistry.kt @@ -18,31 +18,32 @@ enum class InspectorEditorKind { EnumSelect, FontSelect, StringInput, - NumericInput + NumericInput, } data class InspectorEditorDescriptor( val kind: InspectorEditorKind, val options: List = emptyList(), val supportsUnits: Boolean = false, - val showColorPreview: Boolean = false + val showColorPreview: Boolean = false, ) data class InspectorNumberUnitValue( val numberText: String, val unit: CssUnit?, - val isAuto: Boolean + val isAuto: Boolean, ) object InspectorEditorRegistry { - private val unitOptions: List = listOf( - CssUnit.Px, - CssUnit.Em, - CssUnit.Rem, - CssUnit.Vw, - CssUnit.Vh, - CssUnit.Percent - ) + private val unitOptions: List = + listOf( + CssUnit.Px, + CssUnit.Em, + CssUnit.Rem, + CssUnit.Vw, + CssUnit.Vh, + CssUnit.Percent, + ) fun describe(property: StyleProperty, literal: String, expression: StyleExpression?): InspectorEditorDescriptor { val descriptor = StylePropertyRegistry.descriptor(property) @@ -50,14 +51,14 @@ object InspectorEditorRegistry { StyleInspectorEditorKind.FontSelect -> { InspectorEditorDescriptor( kind = InspectorEditorKind.FontSelect, - options = fontOptions() + options = fontOptions(), ) } StyleInspectorEditorKind.EnumSelect -> { InspectorEditorDescriptor( kind = InspectorEditorKind.EnumSelect, - options = descriptor.enumOptions + options = descriptor.enumOptions, ) } @@ -65,17 +66,19 @@ object InspectorEditorRegistry { InspectorEditorDescriptor( kind = InspectorEditorKind.NumericInput, options = descriptor.enumOptions, - supportsUnits = descriptor.grammarKind == StyleValueGrammarKind.LengthLike || - descriptor.grammarKind == StyleValueGrammarKind.LineHeight + supportsUnits = + descriptor.grammarKind == StyleValueGrammarKind.LengthLike || + descriptor.grammarKind == StyleValueGrammarKind.LineHeight, ) } StyleInspectorEditorKind.StringInput -> { InspectorEditorDescriptor( kind = InspectorEditorKind.StringInput, - showColorPreview = isColorProperty(property) || - looksLikeColorLiteral(literal) || - expression is StyleExpression.VariableRef + showColorPreview = + isColorProperty(property) || + looksLikeColorLiteral(literal) || + expression is StyleExpression.VariableRef, ) } } @@ -86,29 +89,28 @@ object InspectorEditorRegistry { return when (descriptor.valueType) { StyleEditorValueType.LengthPx, StyleEditorValueType.OptionalLengthPx, - StyleEditorValueType.SpacingLengthPx -> parseLengthLikeNumberUnit(rawLiteral) + StyleEditorValueType.SpacingLengthPx, + -> parseLengthLikeNumberUnit(rawLiteral) StyleEditorValueType.LineHeight -> parseLineHeightNumberUnit(rawLiteral) StyleEditorValueType.IntNumber -> parseUnitlessInt(rawLiteral, allowAuto = false) StyleEditorValueType.OptionalIntNumber -> parseUnitlessInt(rawLiteral, allowAuto = true) StyleEditorValueType.FloatNumber, - StyleEditorValueType.Spacing -> parseUnitlessFloat(rawLiteral) + StyleEditorValueType.Spacing, + -> parseUnitlessFloat(rawLiteral) else -> null } } - fun formatNumericLiteral( - property: StyleProperty, - numberText: String, - unitToken: String? - ): String { + fun formatNumericLiteral(property: StyleProperty, numberText: String, unitToken: String?): String { val descriptor = StylePropertyRegistry.descriptor(property) return when (descriptor.valueType) { StyleEditorValueType.LengthPx, StyleEditorValueType.OptionalLengthPx, - StyleEditorValueType.SpacingLengthPx -> { + StyleEditorValueType.SpacingLengthPx, + -> { val unit = parseCssUnitToken(unitToken) ?: CssUnit.Px formatNumberUnit(numberText, unit) } @@ -138,7 +140,8 @@ object InspectorEditorRegistry { } StyleEditorValueType.FloatNumber, - StyleEditorValueType.Spacing -> { + StyleEditorValueType.Spacing, + -> { val normalized = numberText.trim() if (normalized.isEmpty()) { "0" @@ -165,14 +168,13 @@ object InspectorEditorRegistry { val descriptor = StylePropertyRegistry.descriptor(property) return when (descriptor.grammarKind) { StyleValueGrammarKind.LengthLike, - StyleValueGrammarKind.LineHeight -> CssUnit.Px + StyleValueGrammarKind.LineHeight, + -> CssUnit.Px else -> null } } - fun parseNumberUnit(rawLiteral: String): InspectorNumberUnitValue? { - return parseLengthLikeNumberUnit(rawLiteral) - } + fun parseNumberUnit(rawLiteral: String): InspectorNumberUnitValue? = parseLengthLikeNumberUnit(rawLiteral) fun formatNumberUnit(numberText: String, unit: CssUnit?): String { val trimmed = numberText.trim() @@ -185,11 +187,10 @@ object InspectorEditorRegistry { fun unitOptions(): List = unitOptions - fun isColorProperty(property: StyleProperty): Boolean { - return property == StyleProperty.BACKGROUND_COLOR || + fun isColorProperty(property: StyleProperty): Boolean = + property == StyleProperty.BACKGROUND_COLOR || property == StyleProperty.BORDER_COLOR || property == StyleProperty.FOREGROUND_COLOR - } fun looksLikeColorLiteral(literal: String): Boolean { val value = literal.trim() @@ -198,9 +199,7 @@ object InspectorEditorRegistry { return hex.length == 3 || hex.length == 6 || hex.length == 8 } - private fun fontOptions(): List { - return FontRegistry.allFontIds().sortedBy { it.lowercase() } - } + private fun fontOptions(): List = FontRegistry.allFontIds().sortedBy { it.lowercase() } private fun parseLengthLikeNumberUnit(rawLiteral: String): InspectorNumberUnitValue? { val normalized = rawLiteral.trim() @@ -208,14 +207,19 @@ object InspectorEditorRegistry { if (normalized.equals("auto", ignoreCase = true)) { return InspectorNumberUnitValue(numberText = "0", unit = CssUnit.Px, isAuto = true) } - val token = normalized.split(Regex("\\s+")).firstOrNull()?.trim().orEmpty() + val token = + normalized + .split(Regex("\\s+")) + .firstOrNull() + ?.trim() + .orEmpty() if (token.isEmpty()) return null return runCatching { val parsed = parseCssLength(token, allowUnitlessZero = true) InspectorNumberUnitValue( numberText = stripTrailingZeros(parsed.value), unit = parsed.unit, - isAuto = false + isAuto = false, ) }.getOrNull() } @@ -232,7 +236,7 @@ object InspectorEditorRegistry { InspectorNumberUnitValue( numberText = stripTrailingZeros(parsed.value), unit = parsed.unit, - isAuto = false + isAuto = false, ) }.getOrNull() } @@ -248,7 +252,7 @@ object InspectorEditorRegistry { InspectorNumberUnitValue( numberText = parsed.toString(), unit = null, - isAuto = false + isAuto = false, ) }.getOrNull() } @@ -261,7 +265,7 @@ object InspectorEditorRegistry { InspectorNumberUnitValue( numberText = stripTrailingZeros(parsed), unit = null, - isAuto = false + isAuto = false, ) }.getOrNull() } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorGeometrySupport.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorGeometrySupport.kt index 9338ae8..50b76da 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorGeometrySupport.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorGeometrySupport.kt @@ -9,37 +9,40 @@ internal data class InspectorNodeBoxes( val border: Rect, val padding: Rect, val content: Rect, - val parentContent: Rect? + val parentContent: Rect?, ) internal object InspectorGeometrySupport { fun computeBoxes(node: DOMNode): InspectorNodeBoxes { val borderRect = node.bounds - val marginRect = Rect( - x = borderRect.x - node.margin.left, - y = borderRect.y - node.margin.top, - width = (borderRect.width + node.margin.horizontal).coerceAtLeast(0), - height = (borderRect.height + node.margin.vertical).coerceAtLeast(0) - ) - val paddingRect = Rect( - x = borderRect.x + node.border.left, - y = borderRect.y + node.border.top, - width = (borderRect.width - node.border.horizontal).coerceAtLeast(0), - height = (borderRect.height - node.border.vertical).coerceAtLeast(0) - ) - val contentRect = Rect( - x = paddingRect.x + node.padding.left, - y = paddingRect.y + node.padding.top, - width = (paddingRect.width - node.padding.horizontal).coerceAtLeast(0), - height = (paddingRect.height - node.padding.vertical).coerceAtLeast(0) - ) + val marginRect = + Rect( + x = borderRect.x - node.margin.left, + y = borderRect.y - node.margin.top, + width = (borderRect.width + node.margin.horizontal).coerceAtLeast(0), + height = (borderRect.height + node.margin.vertical).coerceAtLeast(0), + ) + val paddingRect = + Rect( + x = borderRect.x + node.border.left, + y = borderRect.y + node.border.top, + width = (borderRect.width - node.border.horizontal).coerceAtLeast(0), + height = (borderRect.height - node.border.vertical).coerceAtLeast(0), + ) + val contentRect = + Rect( + x = paddingRect.x + node.padding.left, + y = paddingRect.y + node.padding.top, + width = (paddingRect.width - node.padding.horizontal).coerceAtLeast(0), + height = (paddingRect.height - node.padding.vertical).coerceAtLeast(0), + ) val parentContent = node.parent?.let(::contentRect) return InspectorNodeBoxes( margin = marginRect, border = borderRect, padding = paddingRect, content = contentRect, - parentContent = parentContent + parentContent = parentContent, ) } @@ -47,42 +50,46 @@ internal object InspectorGeometrySupport { val geometry = UsedInteractionGeometryResolver.resolveNodeGeometry(node) val usedClip = geometry.usedClipRect val borderRect = clipRectToUsedClip(geometry.usedBorderRect, usedClip) - val marginRect = clipRectToUsedClip( - Rect( - x = geometry.usedBorderRect.x - node.margin.left, - y = geometry.usedBorderRect.y - node.margin.top, - width = (geometry.usedBorderRect.width + node.margin.horizontal).coerceAtLeast(0), - height = (geometry.usedBorderRect.height + node.margin.vertical).coerceAtLeast(0) - ), - usedClip - ) - val paddingRect = clipRectToUsedClip( - Rect( - x = geometry.usedBorderRect.x + node.border.left, - y = geometry.usedBorderRect.y + node.border.top, - width = (geometry.usedBorderRect.width - node.border.horizontal).coerceAtLeast(0), - height = (geometry.usedBorderRect.height - node.border.vertical).coerceAtLeast(0) - ), - usedClip - ) - val contentRect = clipRectToUsedClip( - Rect( - x = paddingRect.x + node.padding.left, - y = paddingRect.y + node.padding.top, - width = (paddingRect.width - node.padding.horizontal).coerceAtLeast(0), - height = (paddingRect.height - node.padding.vertical).coerceAtLeast(0) - ), - usedClip - ) - val parentContent = node.parent?.let { parent -> - clipRectToUsedClip(contentRect(parent), usedClip) - } + val marginRect = + clipRectToUsedClip( + Rect( + x = geometry.usedBorderRect.x - node.margin.left, + y = geometry.usedBorderRect.y - node.margin.top, + width = (geometry.usedBorderRect.width + node.margin.horizontal).coerceAtLeast(0), + height = (geometry.usedBorderRect.height + node.margin.vertical).coerceAtLeast(0), + ), + usedClip, + ) + val paddingRect = + clipRectToUsedClip( + Rect( + x = geometry.usedBorderRect.x + node.border.left, + y = geometry.usedBorderRect.y + node.border.top, + width = (geometry.usedBorderRect.width - node.border.horizontal).coerceAtLeast(0), + height = (geometry.usedBorderRect.height - node.border.vertical).coerceAtLeast(0), + ), + usedClip, + ) + val contentRect = + clipRectToUsedClip( + Rect( + x = paddingRect.x + node.padding.left, + y = paddingRect.y + node.padding.top, + width = (paddingRect.width - node.padding.horizontal).coerceAtLeast(0), + height = (paddingRect.height - node.padding.vertical).coerceAtLeast(0), + ), + usedClip, + ) + val parentContent = + node.parent?.let { parent -> + clipRectToUsedClip(contentRect(parent), usedClip) + } return InspectorNodeBoxes( margin = marginRect, border = borderRect, padding = paddingRect, content = contentRect, - parentContent = parentContent + parentContent = parentContent, ) } @@ -94,12 +101,11 @@ internal object InspectorGeometrySupport { return rect.intersection(clip) ?: Rect(0, 0, 0, 0) } - fun contentRect(node: DOMNode): Rect { - return Rect( + fun contentRect(node: DOMNode): Rect = + Rect( x = node.bounds.x + node.border.left + node.padding.left, y = node.bounds.y + node.border.top + node.padding.top, width = (node.bounds.width - node.border.horizontal - node.padding.horizontal).coerceAtLeast(0), - height = (node.bounds.height - node.border.vertical - node.padding.vertical).coerceAtLeast(0) + height = (node.bounds.height - node.border.vertical - node.padding.vertical).coerceAtLeast(0), ) - } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorNativePresentation.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorNativePresentation.kt index b4919fc..a2ad830 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorNativePresentation.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorNativePresentation.kt @@ -8,12 +8,12 @@ internal data class InspectorHighlightSnapshot( val borderRect: Rect, val paddingRect: Rect, val contentRect: Rect, - val parentContentRect: Rect? + val parentContentRect: Rect?, ) internal data class InspectorTooltipSnapshot( val text: String, - val rect: Rect + val rect: Rect, ) internal data class InspectorStyleEditorRowSnapshot( @@ -35,14 +35,14 @@ internal data class InspectorStyleEditorRowSnapshot( val unitValue: String?, val unitOpen: Boolean, val colorPreviewRect: Rect?, - val colorPreviewColor: Int? + val colorPreviewColor: Int?, ) internal data class InspectorDropdownOptionSnapshot( val rect: Rect, val text: String, val value: String, - val hovered: Boolean + val hovered: Boolean, ) internal data class InspectorDropdownSnapshot( @@ -50,5 +50,5 @@ internal data class InspectorDropdownSnapshot( val property: StyleProperty, val unitSelect: Boolean, val options: List, - val footerText: String? + val footerText: String?, ) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorPresentationSupport.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorPresentationSupport.kt index 4772fa2..375f59a 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorPresentationSupport.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorPresentationSupport.kt @@ -9,9 +9,7 @@ internal object InspectorPresentationSupport { return "${node.styleType}[$key]" } - fun rectLabel(rect: Rect): String { - return "${rect.x},${rect.y},${rect.width}x${rect.height}" - } + fun rectLabel(rect: Rect): String = "${rect.x},${rect.y},${rect.width}x${rect.height}" fun estimateMaxChars(pixelWidth: Int, fontSize: Int): Int { val approxCharWidth = (fontSize * 0.56f).toInt().coerceAtLeast(6) @@ -77,11 +75,7 @@ internal object InspectorPresentationSupport { return lines } - fun wrapMinimizedLabel( - text: String, - maxCharsPerLine: Int, - maxLines: Int - ): List { + fun wrapMinimizedLabel(text: String, maxCharsPerLine: Int, maxLines: Int): List { val source = text.trim() if (source.isEmpty()) return listOf("") if (maxCharsPerLine <= 0 || maxLines <= 0) return listOf("") diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorStyleEditorSnapshotBuilder.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorStyleEditorSnapshotBuilder.kt index 9d2e05a..d53f8ce 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorStyleEditorSnapshotBuilder.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorStyleEditorSnapshotBuilder.kt @@ -20,7 +20,7 @@ internal enum class InspectorStyleEditorActionType { ToggleUnitSelect, SelectUnitOption, ResetSelectedOverrides, - ClearAllOverrides + ClearAllOverrides, } internal data class InspectorStyleEditorActionSpec( @@ -28,7 +28,7 @@ internal data class InspectorStyleEditorActionSpec( val type: InspectorStyleEditorActionType, val property: StyleProperty? = null, val step: Float = 1f, - val payload: String? = null + val payload: String? = null, ) internal data class InspectorStyleEditorDropdownLayout( @@ -36,7 +36,7 @@ internal data class InspectorStyleEditorDropdownLayout( val property: StyleProperty, val unitSelect: Boolean, val totalOptions: Int, - val visibleRows: Int + val visibleRows: Int, ) internal data class InspectorStyleEditorSnapshotBuildContext( @@ -57,7 +57,7 @@ internal data class InspectorStyleEditorSnapshotBuildContext( val openValueSelectProperty: StyleProperty?, val openUnitSelectProperty: StyleProperty?, val openValueSelectScrollIndex: Int, - val openUnitSelectScrollIndex: Int + val openUnitSelectScrollIndex: Int, ) internal data class InspectorStyleEditorSnapshotBuildResult( @@ -70,14 +70,13 @@ internal data class InspectorStyleEditorSnapshotBuildResult( val resetRect: Rect, val clearRect: Rect, val openValueSelectScrollIndex: Int, - val openUnitSelectScrollIndex: Int + val openUnitSelectScrollIndex: Int, ) internal class InspectorStyleEditorSnapshotBuilder( private val resolveLiteralFromComputed: (ComputedStyle, StyleProperty) -> String, - private val renderExpressionLabel: (StyleExpression) -> String + private val renderExpressionLabel: (StyleExpression) -> String, ) { - fun build(context: InspectorStyleEditorSnapshotBuildContext): InspectorStyleEditorSnapshotBuildResult { var y = context.startY + context.lineHeightPx var variableTooltip: InspectorTooltipSnapshot? = null @@ -98,13 +97,16 @@ internal class InspectorStyleEditorSnapshotBuilder( context.editableProperties.forEach { property -> val overrideExpr = StyleEngine.inspectorOverrideFor(context.selected, property) - val effectiveValue = overrideExpr?.let(renderExpressionLabel) - ?: resolveLiteralFromComputed(context.inspection.computed, property) - val sourceTag = if (overrideExpr != null) { - "ins" - } else { - context.inspection.propertySources[property]?.source ?: "default" - } + val effectiveValue = + overrideExpr?.let(renderExpressionLabel) + ?: resolveLiteralFromComputed(context.inspection.computed, property) + val sourceTag = + if (overrideExpr != null) { + "ins" + } else { + context.inspection.propertySources[property] + ?.source ?: "default" + } val labelText = "${property.key} [$sourceTag]" val buttonsRight = rowLeft + rowWidth - 8 @@ -112,89 +114,103 @@ internal class InspectorStyleEditorSnapshotBuilder( val labelWidth = (rowWidth * 0.40f).toInt().coerceIn(80, maxLabelWidth) val labelMaxChars = InspectorPresentationSupport.estimateMaxChars((labelWidth - 12).coerceAtLeast(24), labelLineHeight) - val labelLineCount = InspectorPresentationSupport.wrapText(labelText, labelMaxChars).size.coerceAtLeast(1) + val labelLineCount = + InspectorPresentationSupport + .wrapText(labelText, labelMaxChars) + .size + .coerceAtLeast(1) val rowHeight = maxOf(context.rowHeightPx, labelLineCount * labelLineHeight + 10, controlHeight + 8) val rowRect = Rect(rowLeft, y, rowWidth, rowHeight) val controlX = rowRect.x + labelWidth val controlWidth = (buttonsRight - controlX - btnWidth - gap).coerceAtLeast(36) val controlY = rowRect.y + ((rowRect.height - controlHeight) / 2) val resetRect = Rect(buttonsRight - btnWidth, controlY, btnWidth, controlHeight) - actionSpecs += InspectorStyleEditorActionSpec( - bounds = resetRect, - type = InspectorStyleEditorActionType.ResetProperty, - property = property - ) + actionSpecs += + InspectorStyleEditorActionSpec( + bounds = resetRect, + type = InspectorStyleEditorActionType.ResetProperty, + property = property, + ) - val editor = InspectorEditorRegistry.describe( - property = property, - literal = effectiveValue, - expression = overrideExpr - ) + val editor = + InspectorEditorRegistry.describe( + property = property, + literal = effectiveValue, + expression = overrideExpr, + ) val controlRect = Rect(controlX, controlY, controlWidth, controlHeight) - var rowSnapshot = InspectorStyleEditorRowSnapshot( - property = property, - sourceTag = sourceTag, - rowRect = rowRect, - labelText = labelText, - resetRect = resetRect, - editorKind = editor.kind, - controlRect = controlRect, - controlValue = effectiveValue, - controlOpen = false, - controlHovered = false, - inputActive = false, - decrementRect = null, - inputRect = null, - incrementRect = null, - unitRect = null, - unitValue = null, - unitOpen = false, - colorPreviewRect = null, - colorPreviewColor = null - ) + var rowSnapshot = + InspectorStyleEditorRowSnapshot( + property = property, + sourceTag = sourceTag, + rowRect = rowRect, + labelText = labelText, + resetRect = resetRect, + editorKind = editor.kind, + controlRect = controlRect, + controlValue = effectiveValue, + controlOpen = false, + controlHovered = false, + inputActive = false, + decrementRect = null, + inputRect = null, + incrementRect = null, + unitRect = null, + unitValue = null, + unitOpen = false, + colorPreviewRect = null, + colorPreviewColor = null, + ) when (editor.kind) { InspectorEditorKind.EnumSelect, - InspectorEditorKind.FontSelect -> { + InspectorEditorKind.FontSelect, + -> { val isOpen = context.openValueSelectProperty == property - actionSpecs += InspectorStyleEditorActionSpec( - bounds = controlRect, - type = InspectorStyleEditorActionType.ToggleValueSelect, - property = property - ) - rowSnapshot = rowSnapshot.copy( - controlOpen = isOpen, - controlHovered = projectRectForPointer(controlRect, context.pointerProjectionScrollY).contains( - context.mouseX, - context.mouseY + actionSpecs += + InspectorStyleEditorActionSpec( + bounds = controlRect, + type = InspectorStyleEditorActionType.ToggleValueSelect, + property = property, + ) + rowSnapshot = + rowSnapshot.copy( + controlOpen = isOpen, + controlHovered = + projectRectForPointer(controlRect, context.pointerProjectionScrollY).contains( + context.mouseX, + context.mouseY, + ), ) - ) } InspectorEditorKind.StringInput -> { var previewRect: Rect? = null var previewColor: Int? = null if (editor.showColorPreview) { - previewRect = Rect( - x = controlRect.x + controlRect.width - (controlRect.height - 8).coerceAtLeast(10) - 6, - y = controlRect.y + 4, - width = (controlRect.height - 8).coerceAtLeast(10), - height = (controlRect.height - 8).coerceAtLeast(10) - ) + previewRect = + Rect( + x = controlRect.x + controlRect.width - (controlRect.height - 8).coerceAtLeast(10) - 6, + y = controlRect.y + 4, + width = (controlRect.height - 8).coerceAtLeast(10), + height = (controlRect.height - 8).coerceAtLeast(10), + ) previewColor = runCatching { parseColor(effectiveValue) }.getOrNull() - actionSpecs += InspectorStyleEditorActionSpec( - bounds = previewRect, - type = InspectorStyleEditorActionType.OpenColorPicker, - property = property - ) + actionSpecs += + InspectorStyleEditorActionSpec( + bounds = previewRect, + type = InspectorStyleEditorActionType.OpenColorPicker, + property = property, + ) } - rowSnapshot = rowSnapshot.copy( - controlValue = effectiveValue, - inputActive = false, - colorPreviewRect = previewRect, - colorPreviewColor = previewColor - ) + rowSnapshot = + rowSnapshot.copy( + controlValue = effectiveValue, + inputActive = false, + colorPreviewRect = previewRect, + colorPreviewColor = previewColor, + ) } InspectorEditorKind.NumericInput -> { @@ -209,73 +225,89 @@ internal class InspectorStyleEditorSnapshotBuilder( .coerceAtLeast(64) val decRect = Rect(controlRect.x, controlRect.y, buttonWidth, controlRect.height) val inputRect = Rect(decRect.x + decRect.width + 4, controlRect.y, inputWidth, controlRect.height) - val incRect = Rect(inputRect.x + inputRect.width + 4, controlRect.y, buttonWidth, controlRect.height) - actionSpecs += InspectorStyleEditorActionSpec( - bounds = decRect, - type = InspectorStyleEditorActionType.Decrement, - property = property, - step = step - ) - actionSpecs += InspectorStyleEditorActionSpec( - bounds = incRect, - type = InspectorStyleEditorActionType.Increment, - property = property, - step = step - ) + val incRect = + Rect(inputRect.x + inputRect.width + 4, controlRect.y, buttonWidth, controlRect.height) + actionSpecs += + InspectorStyleEditorActionSpec( + bounds = decRect, + type = InspectorStyleEditorActionType.Decrement, + property = property, + step = step, + ) + actionSpecs += + InspectorStyleEditorActionSpec( + bounds = incRect, + type = InspectorStyleEditorActionType.Increment, + property = property, + step = step, + ) var unitRect: Rect? = null if (editor.supportsUnits) { unitRect = Rect(incRect.x + incRect.width + 4, controlRect.y, unitWidth, controlRect.height) - actionSpecs += InspectorStyleEditorActionSpec( - bounds = unitRect, - type = InspectorStyleEditorActionType.ToggleUnitSelect, - property = property - ) + actionSpecs += + InspectorStyleEditorActionSpec( + bounds = unitRect, + type = InspectorStyleEditorActionType.ToggleUnitSelect, + property = property, + ) } - rowSnapshot = rowSnapshot.copy( - controlValue = numericValue, - inputActive = false, - decrementRect = decRect, - inputRect = inputRect, - incrementRect = incRect, - unitRect = unitRect, - unitValue = if (editor.supportsUnits) unit?.token else null, - unitOpen = context.openUnitSelectProperty == property, - controlOpen = false - ) + rowSnapshot = + rowSnapshot.copy( + controlValue = numericValue, + inputActive = false, + decrementRect = decRect, + inputRect = inputRect, + incrementRect = incRect, + unitRect = unitRect, + unitValue = if (editor.supportsUnits) unit?.token else null, + unitOpen = context.openUnitSelectProperty == property, + controlOpen = false, + ) } } val projectedRowRect = projectRectForPointer(rowRect, context.pointerProjectionScrollY) - if (overrideExpr is StyleExpression.VariableRef && projectedRowRect.contains(context.mouseX, context.mouseY)) { + if (overrideExpr is StyleExpression.VariableRef && + projectedRowRect.contains(context.mouseX, context.mouseY) + ) { val resolved = StyleEngine.resolveInspectorVariable(overrideExpr.name) val body = resolved.getOrElse { "unresolved (${it.message ?: "unknown error"})" } - variableTooltip = InspectorTooltipSnapshot( - text = "${overrideExpr.name} = $body", - rect = Rect( - x = (projectedRowRect.x + projectedRowRect.width - 360).coerceAtLeast(context.panelBounds.x + 8), - y = (projectedRowRect.y - context.lineHeightPx - 8).coerceAtLeast(context.panelBounds.y + 8), - width = 352, - height = context.lineHeightPx + 10 + variableTooltip = + InspectorTooltipSnapshot( + text = "${overrideExpr.name} = $body", + rect = + Rect( + x = + (projectedRowRect.x + projectedRowRect.width - 360).coerceAtLeast( + context.panelBounds.x + 8, + ), + y = + (projectedRowRect.y - context.lineHeightPx - 8).coerceAtLeast( + context.panelBounds.y + 8, + ), + width = 352, + height = context.lineHeightPx + 10, + ), ) - ) } if (context.openValueSelectProperty == property && editor.options.isNotEmpty()) { - val dropdown = buildDropdownSnapshot( - x = controlRect.x, - y = controlRect.y + controlRect.height + 2, - width = controlRect.width, - options = editor.options, - property = property, - unitSelect = false, - pointerProjectionScrollY = context.pointerProjectionScrollY, - rowHeightPx = context.rowHeightPx, - viewportWidth = context.viewportWidth, - viewportHeight = context.viewportHeight, - mouseX = context.mouseX, - mouseY = context.mouseY, - currentScrollIndex = openValueSelectScrollIndex - ) + val dropdown = + buildDropdownSnapshot( + x = controlRect.x, + y = controlRect.y + controlRect.height + 2, + width = controlRect.width, + options = editor.options, + property = property, + unitSelect = false, + pointerProjectionScrollY = context.pointerProjectionScrollY, + rowHeightPx = context.rowHeightPx, + viewportWidth = context.viewportWidth, + viewportHeight = context.viewportHeight, + mouseX = context.mouseX, + mouseY = context.mouseY, + currentScrollIndex = openValueSelectScrollIndex, + ) openValueSelectScrollIndex = dropdown.nextScrollIndex dropdownLayouts += dropdown.layout dropdownSnapshots += dropdown.snapshot @@ -284,21 +316,22 @@ internal class InspectorStyleEditorSnapshotBuilder( } if (context.openUnitSelectProperty == property && editor.supportsUnits) { val units = InspectorEditorRegistry.unitOptions().map { it.token } - val dropdown = buildDropdownSnapshot( - x = controlRect.x + controlRect.width - 90, - y = controlRect.y + controlRect.height + 2, - width = 90, - options = units, - property = property, - unitSelect = true, - pointerProjectionScrollY = context.pointerProjectionScrollY, - rowHeightPx = context.rowHeightPx, - viewportWidth = context.viewportWidth, - viewportHeight = context.viewportHeight, - mouseX = context.mouseX, - mouseY = context.mouseY, - currentScrollIndex = openUnitSelectScrollIndex - ) + val dropdown = + buildDropdownSnapshot( + x = controlRect.x + controlRect.width - 90, + y = controlRect.y + controlRect.height + 2, + width = 90, + options = units, + property = property, + unitSelect = true, + pointerProjectionScrollY = context.pointerProjectionScrollY, + rowHeightPx = context.rowHeightPx, + viewportWidth = context.viewportWidth, + viewportHeight = context.viewportHeight, + mouseX = context.mouseX, + mouseY = context.mouseY, + currentScrollIndex = openUnitSelectScrollIndex, + ) openUnitSelectScrollIndex = dropdown.nextScrollIndex dropdownLayouts += dropdown.layout dropdownSnapshots += dropdown.snapshot @@ -327,7 +360,7 @@ internal class InspectorStyleEditorSnapshotBuilder( resetRect = resetRect, clearRect = clearRect, openValueSelectScrollIndex = openValueSelectScrollIndex, - openUnitSelectScrollIndex = openUnitSelectScrollIndex + openUnitSelectScrollIndex = openUnitSelectScrollIndex, ) } @@ -349,7 +382,7 @@ internal class InspectorStyleEditorSnapshotBuilder( viewportHeight: Int, mouseX: Int, mouseY: Int, - currentScrollIndex: Int + currentScrollIndex: Int, ): BuiltDropdownSnapshot { val maxRows = 8 val visibleRows = minOf(maxRows, options.size) @@ -364,13 +397,14 @@ internal class InspectorStyleEditorSnapshotBuilder( val clampedY = y.coerceIn(2, (safeViewportH - popupHeight - 2).coerceAtLeast(2)) val popupRect = Rect(clampedX, clampedY, width, popupHeight) - val layout = InspectorStyleEditorDropdownLayout( - rect = popupRect, - property = property, - unitSelect = unitSelect, - totalOptions = options.size, - visibleRows = visibleRows - ) + val layout = + InspectorStyleEditorDropdownLayout( + rect = popupRect, + property = property, + unitSelect = unitSelect, + totalOptions = options.size, + visibleRows = visibleRows, + ) var optionY = popupRect.y + 3 val optionSnapshots = ArrayList(shown.size) @@ -378,42 +412,47 @@ internal class InspectorStyleEditorSnapshotBuilder( shown.forEach { option -> val optionRect = Rect(popupRect.x + 3, optionY, popupRect.width - 6, optionHeight - 2) val hovered = projectRectForPointer(optionRect, pointerProjectionScrollY).contains(mouseX, mouseY) - optionSnapshots += InspectorDropdownOptionSnapshot( - rect = optionRect, - text = ellipsize(option, 30), - value = option, - hovered = hovered - ) - optionActionSpecs += InspectorStyleEditorActionSpec( - bounds = optionRect, - type = if (unitSelect) { - InspectorStyleEditorActionType.SelectUnitOption - } else { - InspectorStyleEditorActionType.SelectValueOption - }, - property = property, - payload = option - ) + optionSnapshots += + InspectorDropdownOptionSnapshot( + rect = optionRect, + text = ellipsize(option, 30), + value = option, + hovered = hovered, + ) + optionActionSpecs += + InspectorStyleEditorActionSpec( + bounds = optionRect, + type = + if (unitSelect) { + InspectorStyleEditorActionType.SelectUnitOption + } else { + InspectorStyleEditorActionType.SelectValueOption + }, + property = property, + payload = option, + ) optionY += optionHeight } - val footer = if (options.size > visibleRows) { - "${first + 1}-${first + visibleRows}/${options.size}" - } else { - null - } - val snapshot = InspectorDropdownSnapshot( - popupRect = popupRect, - property = property, - unitSelect = unitSelect, - options = optionSnapshots, - footerText = footer - ) + val footer = + if (options.size > visibleRows) { + "${first + 1}-${first + visibleRows}/${options.size}" + } else { + null + } + val snapshot = + InspectorDropdownSnapshot( + popupRect = popupRect, + property = property, + unitSelect = unitSelect, + options = optionSnapshots, + footerText = footer, + ) return BuiltDropdownSnapshot( snapshot = snapshot, layout = layout, optionActionSpecs = optionActionSpecs, - nextScrollIndex = first + nextScrollIndex = first, ) } @@ -428,6 +467,6 @@ internal class InspectorStyleEditorSnapshotBuilder( val snapshot: InspectorDropdownSnapshot, val layout: InspectorStyleEditorDropdownLayout, val optionActionSpecs: List, - val nextScrollIndex: Int + val nextScrollIndex: Int, ) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt index 51077ab..2fbda9a 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt @@ -23,22 +23,23 @@ import org.dreamfinity.dsgl.core.style.TextWrap internal class SystemInspectorOverlayNode( private val controller: InspectorController, private val overlayPanel: OverlayPanel, - key: Any? = "dsgl-system-inspector" + key: Any? = "dsgl-system-inspector", ) : DOMNode(key) { override val styleType: String = "dsgl-system-inspector" override val focusable: Boolean = true internal constructor( controller: InspectorController, - key: Any? = "dsgl-system-inspector" + key: Any? = "dsgl-system-inspector", ) : this( controller = controller, - overlayPanel = OverlayPanel( - ownerId = "standalone-system-inspector", - panelState = OverlayPanelState(), - dragSession = OverlayPanelDragSession() - ), - key = key + overlayPanel = + OverlayPanel( + ownerId = "standalone-system-inspector", + panelState = OverlayPanelState(), + dragSession = OverlayPanelDragSession(), + ), + key = key, ) private var inspectedRoot: DOMNode? = null @@ -58,7 +59,7 @@ internal class SystemInspectorOverlayNode( val startRect: Rect, var currentPointerX: Int, var currentPointerY: Int, - var moved: Boolean = false + var moved: Boolean = false, ) init { @@ -103,16 +104,18 @@ internal class SystemInspectorOverlayNode( private fun handleOverlayPanelMouseDown(event: MouseDownEvent): Boolean { if (event.mouseButton != MouseButton.LEFT) return false val bodyRect = controller.overlayContentRect() - val pointerInsideBody = bodyRect.width > 0 && + val pointerInsideBody = + bodyRect.width > 0 && bodyRect.height > 0 && bodyRect.contains(event.mouseX, event.mouseY) if (pointerInsideBody) return false - val handled = overlayPanel.handleMouseDown( - mouseX = event.mouseX, - mouseY = event.mouseY, - button = event.mouseButton, - includeCloseButton = false - ) + val handled = + overlayPanel.handleMouseDown( + mouseX = event.mouseX, + mouseY = event.mouseY, + button = event.mouseButton, + includeCloseButton = false, + ) if (handled) { controller.onOverlayPanelPointerCaptureChanged(true) } @@ -120,14 +123,15 @@ internal class SystemInspectorOverlayNode( } private fun handleOverlayPanelDrag(mouseX: Int, mouseY: Int): Boolean { - val handled = overlayPanel.handleMouseMove( - mouseX = mouseX, - mouseY = mouseY, - viewportWidth = lastViewportWidth, - viewportHeight = lastViewportHeight - ) { rect -> - controller.onOverlayPanelRectChanged(rect, lastViewportWidth, lastViewportHeight) - } + val handled = + overlayPanel.handleMouseMove( + mouseX = mouseX, + mouseY = mouseY, + viewportWidth = lastViewportWidth, + viewportHeight = lastViewportHeight, + ) { rect -> + controller.onOverlayPanelRectChanged(rect, lastViewportWidth, lastViewportHeight) + } if (handled) { overlayPanelDragUpdatedByDomInput = true controller.onOverlayPanelPointerCaptureChanged(true) @@ -136,15 +140,16 @@ internal class SystemInspectorOverlayNode( } private fun handleOverlayPanelMouseUp(event: MouseUpEvent): Boolean { - val handled = overlayPanel.handleMouseUp( - mouseX = event.mouseX, - mouseY = event.mouseY, - button = event.mouseButton, - viewportWidth = lastViewportWidth, - viewportHeight = lastViewportHeight - ) { rect -> - controller.onOverlayPanelRectChanged(rect, lastViewportWidth, lastViewportHeight) - } + val handled = + overlayPanel.handleMouseUp( + mouseX = event.mouseX, + mouseY = event.mouseY, + button = event.mouseButton, + viewportWidth = lastViewportWidth, + viewportHeight = lastViewportHeight, + ) { rect -> + controller.onOverlayPanelRectChanged(rect, lastViewportWidth, lastViewportHeight) + } if (handled) { overlayPanelDragUpdatedByDomInput = false controller.onOverlayPanelPointerCaptureChanged(false) @@ -175,11 +180,16 @@ internal class SystemInspectorOverlayNode( bounds = resolveInputBounds(viewportRect, controller.overlayPanelRect()) } - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } + override fun measure(ctx: UiMeasureContext): Size = + Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { val viewportRect = Rect(x, y, width, height) bounds = resolveInputBounds(viewportRect, controller.overlayPanelRect()) inspectedRoot?.let { root -> @@ -248,7 +258,7 @@ internal class SystemInspectorOverlayNode( x = left, y = top, width = (right - left).coerceAtLeast(0), - height = (bottom - top).coerceAtLeast(0) + height = (bottom - top).coerceAtLeast(0), ) } @@ -277,9 +287,15 @@ internal class SystemInspectorOverlayNode( private fun renderExpanded(ctx: UiMeasureContext, snapshot: InspectorDomSnapshot, viewportRect: Rect) { val panelRect = snapshot.panelRect - val bodyRect = snapshot.bodyRect - ?: overlayPanel.bodyRect() - ?: Rect(panelRect.x + 6, panelRect.y + 58, panelRect.width - 12, (panelRect.height - 64).coerceAtLeast(24)) + val bodyRect = + snapshot.bodyRect + ?: overlayPanel.bodyRect() + ?: Rect( + panelRect.x + 6, + panelRect.y + 58, + panelRect.width - 12, + (panelRect.height - 64).coerceAtLeast(24), + ) val scope = UiScope(this) renderHighlights(scope, ctx) @@ -288,12 +304,13 @@ internal class SystemInspectorOverlayNode( renderExpandedChrome(scope, ctx, panelRect) - val body = scope.div({ - key = "dsgl-system-inspector-body" - style = { - display = Display.Block - } - }) + val body = + scope.div({ + key = "dsgl-system-inspector-body" + style = { + display = Display.Block + } + }) body.backgroundColor = 0x18212C39 body.overflow = Overflow.Hidden body.overflowX = Overflow.Hidden @@ -308,56 +325,60 @@ internal class SystemInspectorOverlayNode( val bodyScrollY = persistedBodyScrollSession?.resolvedY?.coerceAtLeast(0) ?: 0 var y = bodyRect.y + 2 - bodyScrollY - y = renderBodyInfoLines( - scope = bodyScope, - ctx = ctx, - infoLines = snapshot.infoLines, - contentX = contentX, - contentW = contentW, - startY = y, - lineHeightPx = lineHeightPx - ) + y = + renderBodyInfoLines( + scope = bodyScope, + ctx = ctx, + infoLines = snapshot.infoLines, + contentX = contentX, + contentW = contentW, + startY = y, + lineHeightPx = lineHeightPx, + ) - y = renderParentRow( - scope = bodyScope, - ctx = ctx, - parentLabel = snapshot.parentLabel, - contentX = contentX, - contentW = contentW, - startY = y, - rowHeightPx = rowHeightPx - ) - y = renderChildRows( - scope = bodyScope, - ctx = ctx, - childLabels = snapshot.childLabels, - contentX = contentX, - contentW = contentW, - startY = y, - rowHeightPx = rowHeightPx - ) + y = + renderParentRow( + scope = bodyScope, + ctx = ctx, + parentLabel = snapshot.parentLabel, + contentX = contentX, + contentW = contentW, + startY = y, + rowHeightPx = rowHeightPx, + ) + y = + renderChildRows( + scope = bodyScope, + ctx = ctx, + childLabels = snapshot.childLabels, + contentX = contentX, + contentW = contentW, + startY = y, + rowHeightPx = rowHeightPx, + ) renderStyleEditorHeading( scope = bodyScope, ctx = ctx, contentX = contentX, contentW = contentW, y = y, - lineHeightPx = lineHeightPx + lineHeightPx = lineHeightPx, ) val styleRows = controller.overlayStyleEditorRows() renderStyleEditorRows(bodyScope, body, ctx, bodyScrollY, styleRows) y += snapshot.styleEditorHeight - y = renderComputedStyleLines( - scope = bodyScope, - ctx = ctx, - styleLines = snapshot.styleLines, - contentX = contentX, - contentW = contentW, - startY = y, - lineHeightPx = lineHeightPx - ) + y = + renderComputedStyleLines( + scope = bodyScope, + ctx = ctx, + styleLines = snapshot.styleLines, + contentX = contentX, + contentW = contentW, + startY = y, + lineHeightPx = lineHeightPx, + ) controller.onNativeDomDropdownSnapshots(emptyList()) body.restoreScrollSessionSnapshot(persistedBodyScrollSession) val bodyState = body.scrollContainerState() @@ -366,7 +387,7 @@ internal class SystemInspectorOverlayNode( controller.onNativeDomBodyScrollState( scrollY = bodyState.scrollY, trackRect = bodyScrollbarVisual?.trackRect, - thumbRect = bodyScrollbarVisual?.thumbRect + thumbRect = bodyScrollbarVisual?.thumbRect, ) renderTooltip( scope, @@ -374,7 +395,7 @@ internal class SystemInspectorOverlayNode( "dsgl-system-inspector-variable-tooltip", controller.overlayVariableTooltip(), 0xEE141A22.toInt(), - 0xCC60758F.toInt() + 0xCC60758F.toInt(), ) renderTooltip( scope, @@ -382,21 +403,18 @@ internal class SystemInspectorOverlayNode( "dsgl-system-inspector-cursor-tooltip", controller.overlayCursorTooltip(), 0xDD11151A.toInt(), - 0xCC3F4A57.toInt() + 0xCC3F4A57.toInt(), ) } - private fun renderMinimizedChip( - scope: UiScope, - ctx: UiMeasureContext, - snapshot: InspectorDomSnapshot - ) { - val chip = scope.div({ - key = "dsgl-system-inspector-chip" - style = { - display = Display.Block - } - }) + private fun renderMinimizedChip(scope: UiScope, ctx: UiMeasureContext, snapshot: InspectorDomSnapshot) { + val chip = + scope.div({ + key = "dsgl-system-inspector-chip" + style = { + display = Display.Block + } + }) chip.backgroundColor = 0xDD1A202A.toInt() chip.border = Border.all(1, 0xCC4F6076.toInt()) chip.onMouseDown = { event -> @@ -423,13 +441,14 @@ internal class SystemInspectorOverlayNode( var lineY = snapshot.panelRect.y + ((snapshot.panelRect.height - compactLineHeight * snapshot.minimizedLines.size) / 2) snapshot.minimizedLines.forEachIndexed { index, line -> - val lineNode = scope.text(props = { - key = "dsgl-system-inspector-chip-line-$index" - value = line - style = { - textWrap = TextWrap.NoWrap - } - }) + val lineNode = + scope.text(props = { + key = "dsgl-system-inspector-chip-line-$index" + value = line + style = { + textWrap = TextWrap.NoWrap + } + }) lineNode.color = 0xFFE6EDF6.toInt() lineNode.fontSize = 14 renderNode( @@ -439,30 +458,29 @@ internal class SystemInspectorOverlayNode( snapshot.panelRect.x + 8, lineY, (snapshot.panelRect.width - 16).coerceAtLeast(1), - compactLineHeight - ) + compactLineHeight, + ), ) lineY += compactLineHeight } } - private fun renderExpandedChrome( - scope: UiScope, - ctx: UiMeasureContext, - panelRect: Rect - ) { - val pickRect = controller.overlayPickToggleBounds() - ?: Rect(panelRect.x + panelRect.width - 264, panelRect.y + 8, 160, 36) - val minimizeRect = controller.overlayMinimizeBounds() - ?: Rect(panelRect.x + panelRect.width - 96, panelRect.y + 8, 86, 36) + private fun renderExpandedChrome(scope: UiScope, ctx: UiMeasureContext, panelRect: Rect) { + val pickRect = + controller.overlayPickToggleBounds() + ?: Rect(panelRect.x + panelRect.width - 264, panelRect.y + 8, 160, 36) + val minimizeRect = + controller.overlayMinimizeBounds() + ?: Rect(panelRect.x + panelRect.width - 96, panelRect.y + 8, 86, 36) renderPickToggleButton(scope, ctx, pickRect) renderMinimizeButton(scope, ctx, minimizeRect) } private fun renderPickToggleButton(scope: UiScope, ctx: UiMeasureContext, rect: Rect) { - val pickButton = scope.button("Select Element", { - key = "dsgl-system-inspector-pick-toggle" - }) + val pickButton = + scope.button("Select Element", { + key = "dsgl-system-inspector-pick-toggle" + }) pickButton.backgroundColor = 0x3346596E pickButton.border = Border.all(1, 0x775E738C) pickButton.textColor = 0xFFE6EDF6.toInt() @@ -474,9 +492,10 @@ internal class SystemInspectorOverlayNode( } private fun renderMinimizeButton(scope: UiScope, ctx: UiMeasureContext, rect: Rect) { - val minimizeButton = scope.button("Minimize", { - key = "dsgl-system-inspector-minimize" - }) + val minimizeButton = + scope.button("Minimize", { + key = "dsgl-system-inspector-minimize" + }) minimizeButton.backgroundColor = 0x3346596E minimizeButton.border = Border.all(1, 0x775E738C) minimizeButton.textColor = 0xFFE6EDF6.toInt() @@ -494,17 +513,18 @@ internal class SystemInspectorOverlayNode( contentX: Int, contentW: Int, startY: Int, - lineHeightPx: Int + lineHeightPx: Int, ): Int { var y = startY infoLines.forEachIndexed { index, line -> - val lineNode = scope.text(props = { - key = "dsgl-system-inspector-info-line-$index" - value = line - style = { - textWrap = TextWrap.NoWrap - } - }) + val lineNode = + scope.text(props = { + key = "dsgl-system-inspector-info-line-$index" + value = line + style = { + textWrap = TextWrap.NoWrap + } + }) lineNode.color = 0xFFDCE5EF.toInt() lineNode.fontSize = 24 renderNode( @@ -524,13 +544,14 @@ internal class SystemInspectorOverlayNode( contentX: Int, contentW: Int, startY: Int, - rowHeightPx: Int + rowHeightPx: Int, ): Int { var y = startY parentLabel?.let { label -> - val parentButton = scope.button(label, { - key = "dsgl-system-inspector-parent-row" - }) + val parentButton = + scope.button(label, { + key = "dsgl-system-inspector-parent-row" + }) parentButton.backgroundColor = 0x1E263241 parentButton.border = Border.all(1, 0x55394654) parentButton.textColor = 0xFFDCE5EF.toInt() @@ -555,13 +576,14 @@ internal class SystemInspectorOverlayNode( contentX: Int, contentW: Int, startY: Int, - rowHeightPx: Int + rowHeightPx: Int, ): Int { var y = startY childLabels.forEachIndexed { index, label -> - val childButton = scope.button(label, { - key = "dsgl-system-inspector-child-row-$index" - }) + val childButton = + scope.button(label, { + key = "dsgl-system-inspector-child-row-$index" + }) childButton.backgroundColor = 0x1E263241 childButton.border = Border.all(1, 0x55394654) childButton.textColor = 0xFFDCE5EF.toInt() @@ -585,15 +607,16 @@ internal class SystemInspectorOverlayNode( contentX: Int, contentW: Int, y: Int, - lineHeightPx: Int + lineHeightPx: Int, ) { - val styleEditorHeader = scope.text(props = { - key = "dsgl-system-inspector-editor-header" - value = "Style editor (live overrides):" - style = { - textWrap = TextWrap.NoWrap - } - }) + val styleEditorHeader = + scope.text(props = { + key = "dsgl-system-inspector-editor-header" + value = "Style editor (live overrides):" + style = { + textWrap = TextWrap.NoWrap + } + }) styleEditorHeader.color = 0xFFDCE5EF.toInt() styleEditorHeader.fontSize = 24 renderNode( @@ -610,17 +633,18 @@ internal class SystemInspectorOverlayNode( contentX: Int, contentW: Int, startY: Int, - lineHeightPx: Int + lineHeightPx: Int, ): Int { var y = startY styleLines.forEachIndexed { index, line -> - val lineNode = scope.text(props = { - key = "dsgl-system-inspector-style-line-$index" - value = line - style = { - textWrap = TextWrap.NoWrap - } - }) + val lineNode = + scope.text(props = { + key = "dsgl-system-inspector-style-line-$index" + value = line + style = { + textWrap = TextWrap.NoWrap + } + }) lineNode.color = 0xFFDCE5EF.toInt() lineNode.fontSize = 24 renderNode( @@ -634,12 +658,13 @@ internal class SystemInspectorOverlayNode( } private fun renderPanelOccluder(scope: UiScope, ctx: UiMeasureContext, panelRect: Rect) { - val occluder = scope.div({ - key = "dsgl-system-inspector-panel-occluder" - style = { - display = Display.Block - } - }) + val occluder = + scope.div({ + key = "dsgl-system-inspector-panel-occluder" + style = { + display = Display.Block + } + }) occluder.backgroundColor = 0xFF141820.toInt() occluder.border = Border.NONE renderNode(ctx, occluder, panelRect) @@ -653,7 +678,7 @@ internal class SystemInspectorOverlayNode( "dsgl-system-inspector-selected-margin-fill", highlight.marginRect, 0x44F3B33D, - null + null, ) renderHighlightRect( scope, @@ -661,7 +686,7 @@ internal class SystemInspectorOverlayNode( "dsgl-system-inspector-selected-padding-fill", highlight.paddingRect, 0x4426A69A, - null + null, ) renderHighlightRect( scope, @@ -669,7 +694,7 @@ internal class SystemInspectorOverlayNode( "dsgl-system-inspector-selected-content-fill", highlight.contentRect, 0x444285F4, - null + null, ) renderHighlightRect( scope, @@ -677,7 +702,7 @@ internal class SystemInspectorOverlayNode( "dsgl-system-inspector-selected-margin-outline", highlight.marginRect, null, - 0x99F3B33D.toInt() + 0x99F3B33D.toInt(), ) renderHighlightRect( scope, @@ -685,7 +710,7 @@ internal class SystemInspectorOverlayNode( "dsgl-system-inspector-selected-border-outline", highlight.borderRect, null, - 0xCCFF9800.toInt() + 0xCCFF9800.toInt(), ) renderHighlightRect( scope, @@ -693,7 +718,7 @@ internal class SystemInspectorOverlayNode( "dsgl-system-inspector-selected-padding-outline", highlight.paddingRect, null, - 0x9926A69A.toInt() + 0x9926A69A.toInt(), ) renderHighlightRect( scope, @@ -701,7 +726,7 @@ internal class SystemInspectorOverlayNode( "dsgl-system-inspector-selected-content-outline", highlight.contentRect, null, - 0x994285F4.toInt() + 0x994285F4.toInt(), ) highlight.parentContentRect?.let { parentRect -> renderHighlightRect( @@ -710,7 +735,7 @@ internal class SystemInspectorOverlayNode( "dsgl-system-inspector-selected-parent-outline", parentRect, null, - 0x66FF5252 + 0x66FF5252, ) } } @@ -721,7 +746,7 @@ internal class SystemInspectorOverlayNode( "dsgl-system-inspector-hovered-content-fill", highlight.contentRect, 0x3A47A0FF, - null + null, ) renderHighlightRect( scope, @@ -729,7 +754,7 @@ internal class SystemInspectorOverlayNode( "dsgl-system-inspector-hovered-border-outline", highlight.borderRect, null, - 0xCC47A0FF.toInt() + 0xCC47A0FF.toInt(), ) } } @@ -740,15 +765,16 @@ internal class SystemInspectorOverlayNode( key: String, rect: Rect, fillColor: Int?, - borderColor: Int? + borderColor: Int?, ) { if (rect.width <= 0 || rect.height <= 0) return - val layer = scope.div({ - this.key = key - style = { - display = Display.Block - } - }) + val layer = + scope.div({ + this.key = key + style = { + display = Display.Block + } + }) layer.backgroundColor = fillColor ?: 0 layer.border = if (borderColor != null) Border.all(1, borderColor) else Border.NONE renderNode(ctx, layer, rect) @@ -759,7 +785,7 @@ internal class SystemInspectorOverlayNode( parentNode: DOMNode, ctx: UiMeasureContext, bodyScrollY: Int, - rows: List + rows: List, ) { rows.forEachIndexed { index, row -> val rowRect = translateRectY(row.rowRect, -bodyScrollY) @@ -769,7 +795,8 @@ internal class SystemInspectorOverlayNode( when (row.editorKind) { InspectorEditorKind.EnumSelect, - InspectorEditorKind.FontSelect -> { + InspectorEditorKind.FontSelect, + -> { renderStyleEditorSelectButton(scope, ctx, bodyScrollY, row, index) } @@ -787,13 +814,19 @@ internal class SystemInspectorOverlayNode( renderStyleEditorFooterActions(scope, ctx, bodyScrollY) } - private fun renderStyleEditorRowContainer(scope: UiScope, ctx: UiMeasureContext, rowRect: Rect, index: Int) { - val rowNode = scope.div({ - key = "dsgl-system-inspector-editor-row-$index" - style = { - display = Display.Block - } - }) + private fun renderStyleEditorRowContainer( + scope: UiScope, + ctx: UiMeasureContext, + rowRect: Rect, + index: Int, + ) { + val rowNode = + scope.div({ + key = "dsgl-system-inspector-editor-row-$index" + style = { + display = Display.Block + } + }) rowNode.backgroundColor = 0x1B293746 rowNode.border = Border.all(1, 0x553F4A57) renderNode(ctx, rowNode, rowRect) @@ -804,15 +837,16 @@ internal class SystemInspectorOverlayNode( ctx: UiMeasureContext, rowRect: Rect, row: InspectorStyleEditorRowSnapshot, - index: Int + index: Int, ) { - val labelNode = scope.text(props = { - key = "dsgl-system-inspector-editor-label-$index" - value = row.labelText - style = { - textWrap = TextWrap.Wrap - } - }) + val labelNode = + scope.text(props = { + key = "dsgl-system-inspector-editor-label-$index" + value = row.labelText + style = { + textWrap = TextWrap.Wrap + } + }) labelNode.color = 0xFFDCE5EF.toInt() labelNode.fontSize = 18 renderNode( @@ -822,7 +856,7 @@ internal class SystemInspectorOverlayNode( rowRect.x + 8, rowRect.y + 5, (row.controlRect.x - row.rowRect.x - 14).coerceAtLeast(40), - rowRect.height - 10 + rowRect.height - 10, ), ) } @@ -832,11 +866,12 @@ internal class SystemInspectorOverlayNode( ctx: UiMeasureContext, bodyScrollY: Int, row: InspectorStyleEditorRowSnapshot, - index: Int + index: Int, ) { - val resetButton = scope.button("x", { - key = "dsgl-system-inspector-editor-reset-$index" - }) + val resetButton = + scope.button("x", { + key = "dsgl-system-inspector-editor-reset-$index" + }) resetButton.backgroundColor = 0x3346596E resetButton.border = Border.all(1, 0x775E738C) resetButton.textColor = 0xFFDCE5EF.toInt() @@ -852,17 +887,18 @@ internal class SystemInspectorOverlayNode( ctx: UiMeasureContext, bodyScrollY: Int, row: InspectorStyleEditorRowSnapshot, - index: Int + index: Int, ) { - val selector = buildSystemOwnedSelectControl( - scope = scope, - key = "dsgl-system-inspector-editor-select-$index", - selectedValue = row.controlValue, - options = controller.resolveDropdownOptionsForProperty(row.property, unitSelect = false), - hovered = row.controlHovered - ) { selected -> - controller.onSelectValueOptionPressed(row.property, selected) - } + val selector = + buildSystemOwnedSelectControl( + scope = scope, + key = "dsgl-system-inspector-editor-select-$index", + selectedValue = row.controlValue, + options = controller.resolveDropdownOptionsForProperty(row.property, unitSelect = false), + hovered = row.controlHovered, + ) { selected -> + controller.onSelectValueOptionPressed(row.property, selected) + } renderNode(ctx, selector, translateRectY(row.controlRect, -bodyScrollY)) } @@ -871,12 +907,13 @@ internal class SystemInspectorOverlayNode( parentNode: DOMNode, ctx: UiMeasureContext, bodyScrollY: Int, - row: InspectorStyleEditorRowSnapshot + row: InspectorStyleEditorRowSnapshot, ) { - val input = TextInputNode( - text = row.controlValue.replace("|", ""), - key = "dsgl-system-inspector-editor-input-${row.property.key}" - ) + val input = + TextInputNode( + text = row.controlValue.replace("|", ""), + key = "dsgl-system-inspector-editor-input-${row.property.key}", + ) input.backgroundColor = if (row.inputActive) 0x334D5D70 else 0x22313D4B input.focusedBackgroundColor = input.backgroundColor input.border = Border.all(1, if (row.inputActive) 0xFFA8C6E6.toInt() else 0x77607084) @@ -898,13 +935,14 @@ internal class SystemInspectorOverlayNode( ctx: UiMeasureContext, bodyScrollY: Int, row: InspectorStyleEditorRowSnapshot, - index: Int + index: Int, ) { row.colorPreviewRect?.let { previewRect -> val shiftedPreviewRect = translateRectY(previewRect, -bodyScrollY) - val preview = scope.button("", { - key = "dsgl-system-inspector-editor-color-preview-$index" - }) + val preview = + scope.button("", { + key = "dsgl-system-inspector-editor-color-preview-$index" + }) preview.backgroundColor = row.colorPreviewColor ?: 0x663F4A57 preview.border = Border.all(1, 0xCC9BB2C9.toInt()) preview.onClick { @@ -920,12 +958,13 @@ internal class SystemInspectorOverlayNode( ctx: UiMeasureContext, bodyScrollY: Int, row: InspectorStyleEditorRowSnapshot, - index: Int + index: Int, ) { row.decrementRect?.let { rect -> - val dec = scope.button("-", { - key = "dsgl-system-inspector-editor-dec-$index" - }) + val dec = + scope.button("-", { + key = "dsgl-system-inspector-editor-dec-$index" + }) dec.backgroundColor = 0x3346596E dec.border = Border.all(1, 0x775E738C) dec.textColor = 0xFFDCE5EF.toInt() @@ -936,10 +975,11 @@ internal class SystemInspectorOverlayNode( renderNode(ctx, dec, translateRectY(rect, -bodyScrollY)) } row.inputRect?.let { rect -> - val input = TextInputNode( - text = row.controlValue.replace("|", ""), - key = "dsgl-system-inspector-editor-numeric-input-${row.property.key}" - ) + val input = + TextInputNode( + text = row.controlValue.replace("|", ""), + key = "dsgl-system-inspector-editor-numeric-input-${row.property.key}", + ) input.allowedChars = "-0123456789." input.backgroundColor = if (row.inputActive) 0x334D5D70 else 0x22313D4B input.focusedBackgroundColor = input.backgroundColor @@ -958,9 +998,10 @@ internal class SystemInspectorOverlayNode( } row.incrementRect?.let { rect -> - val inc = scope.button("+", { - key = "dsgl-system-inspector-editor-inc-$index" - }) + val inc = + scope.button("+", { + key = "dsgl-system-inspector-editor-inc-$index" + }) inc.backgroundColor = 0x3346596E inc.border = Border.all(1, 0x775E738C) inc.textColor = 0xFFDCE5EF.toInt() @@ -972,15 +1013,16 @@ internal class SystemInspectorOverlayNode( } row.unitRect?.let { rect -> val unitValue = row.unitValue ?: "px" - val unit = buildSystemOwnedSelectControl( - scope = scope, - key = "dsgl-system-inspector-editor-unit-$index", - selectedValue = unitValue, - options = controller.resolveDropdownOptionsForProperty(row.property, unitSelect = true), - hovered = false - ) { selected -> - controller.onSelectUnitOptionPressed(row.property, selected) - } + val unit = + buildSystemOwnedSelectControl( + scope = scope, + key = "dsgl-system-inspector-editor-unit-$index", + selectedValue = unitValue, + options = controller.resolveDropdownOptionsForProperty(row.property, unitSelect = true), + hovered = false, + ) { selected -> + controller.onSelectUnitOptionPressed(row.property, selected) + } renderNode(ctx, unit, translateRectY(rect, -bodyScrollY)) } } @@ -991,22 +1033,30 @@ internal class SystemInspectorOverlayNode( selectedValue: String, options: List, hovered: Boolean, - onSelected: (String) -> Unit + onSelected: (String) -> Unit, ): DOMNode { val open = SelectRuntime.host.isOpenFor(key) - val selectNode = scope.select( - props = { - this.key = key - ownerScope = OverlayOwnerScope.System - value = selectedValue - onInput = { onSelected(it.value) } + val selectNode = + scope.select( + props = { + this.key = key + ownerScope = OverlayOwnerScope.System + value = selectedValue + onInput = { onSelected(it.value) } + }, + ) { + options.forEach { option -> + option(id = option, label = option) + } } - ) { - options.forEach { option -> - option(id = option, label = option) + selectNode.backgroundColor = + if (open) { + 0x334D5D70 + } else if (hovered) { + 0x2A425164 + } else { + 0x22313D4B } - } - selectNode.backgroundColor = if (open) 0x334D5D70 else if (hovered) 0x2A425164 else 0x22313D4B selectNode.border = Border.all(1, if (open) 0xFFA8C6E6.toInt() else 0x77607084) selectNode.textColor = 0xFFE6EDF6.toInt() selectNode.fontSize = 18 @@ -1017,9 +1067,10 @@ internal class SystemInspectorOverlayNode( private fun renderStyleEditorFooterActions(scope: UiScope, ctx: UiMeasureContext, bodyScrollY: Int) { val resetRect = controller.overlayStyleEditorResetRect() if (resetRect.width > 0 && resetRect.height > 0) { - val resetButton = scope.button("Reset node", { - key = "dsgl-system-inspector-reset-node" - }) + val resetButton = + scope.button("Reset node", { + key = "dsgl-system-inspector-reset-node" + }) resetButton.backgroundColor = 0x2A465968 resetButton.border = Border.all(1, 0x775E738C) resetButton.textColor = 0xFFDCE5EF.toInt() @@ -1032,9 +1083,10 @@ internal class SystemInspectorOverlayNode( val clearRect = controller.overlayStyleEditorClearRect() if (clearRect.width > 0 && clearRect.height > 0) { - val clearButton = scope.button("Clear all", { - key = "dsgl-system-inspector-clear-all" - }) + val clearButton = + scope.button("Clear all", { + key = "dsgl-system-inspector-clear-all" + }) clearButton.backgroundColor = 0x2A4E3F56 clearButton.border = Border.all(1, 0x777A5C84) clearButton.textColor = 0xFFDCE5EF.toInt() @@ -1047,14 +1099,15 @@ internal class SystemInspectorOverlayNode( } private fun startMinimizedChipDrag(panelRect: Rect, mouseX: Int, mouseY: Int) { - minimizedChipDragSession = MinimizedChipDragSession( - startPointerX = mouseX, - startPointerY = mouseY, - startRect = panelRect, - currentPointerX = mouseX, - currentPointerY = mouseY, - moved = false - ) + minimizedChipDragSession = + MinimizedChipDragSession( + startPointerX = mouseX, + startPointerY = mouseY, + startRect = panelRect, + currentPointerX = mouseX, + currentPointerY = mouseY, + moved = false, + ) controller.onOverlayPanelPointerCaptureChanged(true) } @@ -1075,7 +1128,7 @@ internal class SystemInspectorOverlayNode( x = session.startRect.x + dx, y = session.startRect.y + dy, viewportWidth = lastViewportWidth, - viewportHeight = lastViewportHeight + viewportHeight = lastViewportHeight, ) } @@ -1105,26 +1158,28 @@ internal class SystemInspectorOverlayNode( keyPrefix: String, tooltip: InspectorTooltipSnapshot?, backgroundColor: Int, - borderColor: Int + borderColor: Int, ) { if (tooltip == null) return - val box = scope.div({ - key = "$keyPrefix-box" - style = { - display = Display.Block - } - }) + val box = + scope.div({ + key = "$keyPrefix-box" + style = { + display = Display.Block + } + }) box.backgroundColor = backgroundColor box.border = Border.all(1, borderColor) renderNode(ctx, box, tooltip.rect) - val textNode = scope.text(props = { - key = "$keyPrefix-text" - value = tooltip.text - style = { - textWrap = TextWrap.NoWrap - } - }) + val textNode = + scope.text(props = { + key = "$keyPrefix-text" + value = tooltip.text + style = { + textWrap = TextWrap.NoWrap + } + }) textNode.color = 0xFFE6EDF6.toInt() textNode.fontSize = 18 renderNode( @@ -1134,8 +1189,8 @@ internal class SystemInspectorOverlayNode( tooltip.rect.x + 6, tooltip.rect.y + 4, (tooltip.rect.width - 10).coerceAtLeast(20), - (tooltip.rect.height - 8).coerceAtLeast(16) - ) + (tooltip.rect.height - 8).coerceAtLeast(16), + ), ) } @@ -1163,12 +1218,11 @@ internal class SystemInspectorOverlayNode( } } - private fun findNodeByKey(targetKey: String): DOMNode? { - return collectNodes(this).firstOrNull { it.key == targetKey } - } + private fun findNodeByKey(targetKey: String): DOMNode? = collectNodes(this).firstOrNull { it.key == targetKey } private fun collectNodes(root: DOMNode): List { val out = ArrayList() + fun walk(node: DOMNode) { out += node node.children.forEach(::walk) @@ -1191,15 +1245,9 @@ internal class SystemInspectorOverlayNode( return false } - private fun translateRectY(rect: Rect, deltaY: Int): Rect { - return Rect(rect.x, rect.y + deltaY, rect.width, rect.height) - } + private fun translateRectY(rect: Rect, deltaY: Int): Rect = Rect(rect.x, rect.y + deltaY, rect.width, rect.height) - private fun renderNode( - ctx: UiMeasureContext, - node: DOMNode, - rect: Rect - ) { + private fun renderNode(ctx: UiMeasureContext, node: DOMNode, rect: Rect) { if (rect.width <= 0 || rect.height <= 0) { node.display = Display.None node.render(ctx, 0, 0, 0, 0) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt index 421a773..19786f6 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt @@ -1,29 +1,28 @@ package org.dreamfinity.dsgl.core.overlay import org.dreamfinity.dsgl.core.DomTree -import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.dom.layout.Rect -import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.StyleApplicationScope class ApplicationOverlayHost : OverlayLayerHost { override val layerId: UiLayerId = UiLayerId.ApplicationOverlay private val rootNode: ApplicationOverlayRootNode = ApplicationOverlayRootNode() - private val tree: DomTree = DomTree( - root = rootNode, - styleScope = StyleApplicationScope.Application - ) + private val tree: DomTree = + DomTree( + root = rootNode, + styleScope = StyleApplicationScope.Application, + ) override fun render(ctx: UiMeasureContext, width: Int, height: Int) { rootNode.setViewportBounds(width, height) tree.render(ctx, width, height) } - override fun paint(ctx: UiMeasureContext): List { - return tree.paint(ctx, applyStyles = true) - } + override fun paint(ctx: UiMeasureContext): List = tree.paint(ctx, applyStyles = true) override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = false @@ -39,7 +38,5 @@ class ApplicationOverlayHost : OverlayLayerHost { tree.clearRefs() } - internal fun debugRootBounds(): Rect { - return rootNode.bounds - } + internal fun debugRootBounds(): Rect = rootNode.bounds } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayRootNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayRootNode.kt index c071746..5669606 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayRootNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayRootNode.kt @@ -1,7 +1,6 @@ package org.dreamfinity.dsgl.core.overlay import org.dreamfinity.dsgl.core.DsglColors -import org.dreamfinity.dsgl.core.dsl.UiScope import org.dreamfinity.dsgl.core.debug.OverlayLayerDebugState.isTintEnabled import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.elements.ContainerNode @@ -9,23 +8,25 @@ import org.dreamfinity.dsgl.core.dom.layout.Border import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.Size import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.dsl.UiScope import org.dreamfinity.dsgl.core.dsl.div import org.dreamfinity.dsgl.core.font.FontRegistry import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.StyleEngine class ApplicationOverlayRootNode( - key: Any? = "dsgl-application-overlay-root" + key: Any? = "dsgl-application-overlay-root", ) : DOMNode(key) { override val styleType: String = "dsgl-application-overlay-root" private var viewportWidth: Int = 0 private var viewportHeight: Int = 0 - private val debugTintNode: ContainerNode = UiScope(this).div({ - this.key = "dsgl-application-overlay-debug-tint" - style = { - display = Display.None - } - }) + private val debugTintNode: ContainerNode = + UiScope(this).div({ + this.key = "dsgl-application-overlay-debug-tint" + style = { + display = Display.None + } + }) internal fun setViewportBounds(width: Int, height: Int) { viewportWidth = width.coerceAtLeast(0) @@ -37,11 +38,17 @@ class ApplicationOverlayRootNode( val resolvedHeight = if (viewportHeight > 0) viewportHeight else StyleEngine.viewportHeightPx().coerceAtLeast(0) return Size( width = resolvedWidth, - height = resolvedHeight + height = resolvedHeight, ) } - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { setViewportBounds(width, height) bounds = Rect(0, 0, viewportWidth, viewportHeight) val tintEnabled = OverlayDebugVisualization.enabled && isTintEnabled(UiLayerId.ApplicationOverlay) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ColorPickerPopupOverlayOwnership.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ColorPickerPopupOverlayOwnership.kt index 43e3c8b..905ab52 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ColorPickerPopupOverlayOwnership.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ColorPickerPopupOverlayOwnership.kt @@ -3,7 +3,6 @@ package org.dreamfinity.dsgl.core.overlay import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupRequest object ColorPickerPopupOverlayOwnership { - fun resolveLayer(request: ColorPickerPopupRequest): UiLayerId { - return OverlayLayerContracts.resolveTransientLayer(request.ownerScope) - } + fun resolveLayer(request: ColorPickerPopupRequest): UiLayerId = + OverlayLayerContracts.resolveTransientLayer(request.ownerScope) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayDebugVisualization.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayDebugVisualization.kt index 43bde73..c9d802c 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayDebugVisualization.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayDebugVisualization.kt @@ -7,10 +7,10 @@ object OverlayDebugVisualization { var systemOverlayBorderColor: Int = 0xAAE18BFF.toInt() val enabled: Boolean get() { - return testOverride ?: java.lang.Boolean.getBoolean("dsgl.overlay.debug") + return testOverride ?: java.lang.Boolean + .getBoolean("dsgl.overlay.debug") } - private var testOverride: Boolean? = null internal fun setTestOverride(value: Boolean?) { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContracts.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContracts.kt index 27828b3..a4d581d 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContracts.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContracts.kt @@ -6,43 +6,43 @@ enum class UiLayerId { Debug, ApplicationRoot, ApplicationOverlay, - SystemOverlay + SystemOverlay, } enum class OverlayOwnerScope { Application, - System + System, } object OverlayLayerContracts { - val paintOrder: List = listOf( - UiLayerId.ApplicationRoot, - UiLayerId.ApplicationOverlay, - UiLayerId.SystemOverlay, - UiLayerId.Debug - ) + val paintOrder: List = + listOf( + UiLayerId.ApplicationRoot, + UiLayerId.ApplicationOverlay, + UiLayerId.SystemOverlay, + UiLayerId.Debug, + ) - val inputPriority: List = listOf( - UiLayerId.Debug, - UiLayerId.SystemOverlay, - UiLayerId.ApplicationOverlay, - UiLayerId.ApplicationRoot - ) + val inputPriority: List = + listOf( + UiLayerId.Debug, + UiLayerId.SystemOverlay, + UiLayerId.ApplicationOverlay, + UiLayerId.ApplicationRoot, + ) - fun resolveTransientLayer(ownerScope: OverlayOwnerScope): UiLayerId { - return when (ownerScope) { + fun resolveTransientLayer(ownerScope: OverlayOwnerScope): UiLayerId = + when (ownerScope) { OverlayOwnerScope.Application -> UiLayerId.ApplicationOverlay OverlayOwnerScope.System -> UiLayerId.SystemOverlay } - } - fun resolveTransientLayer(ownerScope: OverlayOwnerScope, cursorX: Int, cursorY: Int): UiLayerId { - return resolveTransientLayer(ownerScope) - } + fun resolveTransientLayer(ownerScope: OverlayOwnerScope, cursorX: Int, cursorY: Int): UiLayerId = + resolveTransientLayer(ownerScope) fun firstInputConsumer( canConsume: (UiLayerId) -> Boolean, - isLayerInputEnabled: (UiLayerId) -> Boolean = { true } + isLayerInputEnabled: (UiLayerId) -> Boolean = { true }, ): UiLayerId? { inputPriority.forEach { layer -> if (!isLayerInputEnabled(layer)) return@forEach @@ -57,7 +57,7 @@ object OverlayLayerContracts { systemOverlay: List, debug: List, out: MutableList, - shouldRenderLayer: (UiLayerId) -> Boolean = { true } + shouldRenderLayer: (UiLayerId) -> Boolean = { true }, ) { out.clear() paintOrder.forEach { layer -> diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouter.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouter.kt index 895e24f..f27f5e4 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouter.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouter.kt @@ -8,20 +8,20 @@ import org.dreamfinity.dsgl.core.dom.layout.AffineTransform2D import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.event.EventBus import org.dreamfinity.dsgl.core.event.FocusManager +import org.dreamfinity.dsgl.core.event.KeyboardKeyDownEvent import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.event.MouseClickEvent -import org.dreamfinity.dsgl.core.event.MouseDragEvent import org.dreamfinity.dsgl.core.event.MouseDownEvent +import org.dreamfinity.dsgl.core.event.MouseDragEvent import org.dreamfinity.dsgl.core.event.MouseEnterEvent import org.dreamfinity.dsgl.core.event.MouseLeaveEvent import org.dreamfinity.dsgl.core.event.MouseMoveEvent import org.dreamfinity.dsgl.core.event.MouseOverEvent import org.dreamfinity.dsgl.core.event.MouseUpEvent import org.dreamfinity.dsgl.core.event.MouseWheelEvent -import org.dreamfinity.dsgl.core.event.KeyboardKeyDownEvent class LayerDomInputRouter( - private val rootProvider: () -> DOMNode? + private val rootProvider: () -> DOMNode?, ) { private val hoverChain: MutableList = ArrayList() private var hoverTarget: DOMNode? = null @@ -36,10 +36,11 @@ class LayerDomInputRouter( private var lastMoveY: Int = Int.MIN_VALUE fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean { - val root = rootProvider() ?: run { - clear() - return false - } + val root = + rootProvider() ?: run { + clear() + return false + } restoreDragCapture(root) val prevX = if (lastMoveX == Int.MIN_VALUE) mouseX else lastMoveX val prevY = if (lastMoveY == Int.MIN_VALUE) mouseY else lastMoveY @@ -67,7 +68,7 @@ class LayerDomInputRouter( mouseY = mouseY, mouseDX = dx, mouseDY = dy, - button = button + button = button, ) } } @@ -77,10 +78,11 @@ class LayerDomInputRouter( } fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { - val root = rootProvider() ?: run { - clear() - return false - } + val root = + rootProvider() ?: run { + clear() + return false + } restoreDragCapture(root) updateHoverLocal(root, hoverChain, mouseX, mouseY, 0, 0) hoverTarget = hoverChain.lastOrNull() @@ -108,10 +110,11 @@ class LayerDomInputRouter( } fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { - val root = rootProvider() ?: run { - clear() - return false - } + val root = + rootProvider() ?: run { + clear() + return false + } restoreDragCapture(root) updateHoverLocal(root, hoverChain, mouseX, mouseY, 0, 0) hoverTarget = hoverChain.lastOrNull() @@ -143,10 +146,11 @@ class LayerDomInputRouter( fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean { if (delta == 0) return false - val root = rootProvider() ?: run { - clear() - return false - } + val root = + rootProvider() ?: run { + clear() + return false + } restoreDragCapture(root) updateHoverLocal(root, hoverChain, mouseX, mouseY, 0, 0) hoverTarget = hoverChain.lastOrNull() @@ -159,10 +163,11 @@ class LayerDomInputRouter( } fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean { - val root = rootProvider() ?: run { - clear() - return false - } + val root = + rootProvider() ?: run { + clear() + return false + } val focused = FocusManager.focusedNode() ?: return false if (!isSameOrAncestor(root, focused)) return false val event = KeyboardKeyDownEvent(keyChar = keyChar, keyCode = keyCode) @@ -280,7 +285,12 @@ class LayerDomInputRouter( return if (focused is TextAreaNode) focused else null } - private fun bubbleGenericWheel(target: DOMNode, mouseX: Int, mouseY: Int, delta: Int): Boolean { + private fun bubbleGenericWheel( + target: DOMNode, + mouseX: Int, + mouseY: Int, + delta: Int, + ): Boolean { var current: DOMNode? = target while (current != null) { if (current.handleGenericWheel(mouseX, mouseY, delta)) { @@ -313,7 +323,7 @@ class LayerDomInputRouter( mouseY: Int, parentTransform: AffineTransform2D, parentInputClipRect: Rect?, - out: MutableList + out: MutableList, ): Boolean { if (root.styleDisabled) return false if (!root.isHitTestVisible()) return false @@ -332,7 +342,7 @@ class LayerDomInputRouter( mouseY = mouseY, parentTransform = worldTransform, parentInputClipRect = childInputClipRect, - out = out + out = out, ) ) { return true @@ -368,7 +378,7 @@ class LayerDomInputRouter( mouseX: Int, mouseY: Int, mouseDX: Int, - mouseDY: Int + mouseDY: Int, ) { val currHoverChain = ArrayList(prevHoverChain.size + 4) collectHoverChainLocal( @@ -377,7 +387,7 @@ class LayerDomInputRouter( mouseY = mouseY, parentTransform = AffineTransform2D.IDENTITY, parentInputClipRect = null, - out = currHoverChain + out = currHoverChain, ) val minSize = minOf(prevHoverChain.size, currHoverChain.size) var commonPrefixLen = 0 @@ -444,4 +454,3 @@ class LayerDomInputRouter( target.onmouseover?.invoke(event) } } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerInputDispatch.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerInputDispatch.kt index 0b81fa1..78df55f 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerInputDispatch.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerInputDispatch.kt @@ -2,7 +2,7 @@ package org.dreamfinity.dsgl.core.overlay.input internal inline fun dispatchManualThenDomFallback( manualDispatch: () -> Boolean, - domFallbackDispatch: () -> Boolean + domFallbackDispatch: () -> Boolean, ): Boolean { if (manualDispatch()) { return true diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanel.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanel.kt index f4555d6..f0449f5 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanel.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanel.kt @@ -1,6 +1,5 @@ package org.dreamfinity.dsgl.core.overlay.panel -import org.dreamfinity.dsgl.core.dsl.UiScope import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ButtonNode @@ -10,6 +9,7 @@ import org.dreamfinity.dsgl.core.dom.elements.TextSource import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.Size import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.dsl.UiScope import org.dreamfinity.dsgl.core.dsl.button import org.dreamfinity.dsgl.core.dsl.div import org.dreamfinity.dsgl.core.dsl.text @@ -35,26 +35,27 @@ data class OverlayPanelStyle( val closeButtonBorderColor: Int = 0xFF607A95.toInt(), val textColor: Int = 0xFFFFFFFF.toInt(), val fontSize: Int = 20, - val closeGlyph: String = "x" + val closeGlyph: String = "x", ) data class OverlayPanelFrame( val panelRect: Rect, val headerRect: Rect, val bodyRect: Rect, - val closeRect: Rect + val closeRect: Rect, ) class OverlayPanel( private val ownerId: Any, private val panelState: OverlayPanelState, private val dragSession: OverlayPanelDragSession, - private var style: OverlayPanelStyle = OverlayPanelStyle() + private var style: OverlayPanelStyle = OverlayPanelStyle(), ) { - private val rootNode: OverlayPanelRootNode = OverlayPanelRootNode( - owner = this, - key = "dsgl-overlay-panel-$ownerId" - ) + private val rootNode: OverlayPanelRootNode = + OverlayPanelRootNode( + owner = this, + key = "dsgl-overlay-panel-$ownerId", + ) private val shadowNode: ContainerNode private val panelNode: ContainerNode private val headerNode: ContainerNode @@ -77,39 +78,44 @@ class OverlayPanel( init { val scope = UiScope(rootNode) - shadowNode = scope.div({ - key = "dsgl-overlay-panel-shadow-$ownerId" - style = { - display = Display.Block - } - }) - panelNode = scope.div({ - key = "dsgl-overlay-panel-frame-$ownerId" - style = { - display = Display.Block - } - }) - headerNode = scope.div({ - key = "dsgl-overlay-panel-header-$ownerId" - style = { - display = Display.Flex - flexDirection = FlexDirection.Row - } - }) - titleNode = scope.text(props = { - key = "dsgl-overlay-panel-title-$ownerId" - value = "" - style = { - textWrap = TextWrap.NoWrap - } - }) - closeButtonNode = scope.button(style.closeGlyph, { - key = "dsgl-overlay-panel-close-$ownerId" - onMouseClick = { - onClose?.invoke() - it.cancelled = true - } - }) + shadowNode = + scope.div({ + key = "dsgl-overlay-panel-shadow-$ownerId" + style = { + display = Display.Block + } + }) + panelNode = + scope.div({ + key = "dsgl-overlay-panel-frame-$ownerId" + style = { + display = Display.Block + } + }) + headerNode = + scope.div({ + key = "dsgl-overlay-panel-header-$ownerId" + style = { + display = Display.Flex + flexDirection = FlexDirection.Row + } + }) + titleNode = + scope.text(props = { + key = "dsgl-overlay-panel-title-$ownerId" + value = "" + style = { + textWrap = TextWrap.NoWrap + } + }) + closeButtonNode = + scope.button(style.closeGlyph, { + key = "dsgl-overlay-panel-close-$ownerId" + onMouseClick = { + onClose?.invoke() + it.cancelled = true + } + }) applyStyleToNodes(style) rebuildFrameFromState() } @@ -137,7 +143,7 @@ class OverlayPanel( minWidth: Int = this.minWidth, minHeight: Int = this.minHeight, style: OverlayPanelStyle = this.style, - onClose: (() -> Unit)? = this.onClose + onClose: (() -> Unit)? = this.onClose, ) { val titleChanged = this.title != title val styleChanged = this.style != style @@ -191,7 +197,7 @@ class OverlayPanel( mouseX: Int, mouseY: Int, button: MouseButton, - includeCloseButton: Boolean = true + includeCloseButton: Boolean = true, ): Boolean { val localFrame = frame ?: return false if (button != MouseButton.LEFT) return false @@ -208,7 +214,7 @@ class OverlayPanel( pointerX = mouseX, pointerY = mouseY, panelState = panelState, - resizeHandle = resizeHandle + resizeHandle = resizeHandle, ) return true } @@ -224,7 +230,7 @@ class OverlayPanel( mouseY: Int, viewportWidth: Int, viewportHeight: Int, - onDragRectChanged: (Rect) -> Unit + onDragRectChanged: (Rect) -> Unit, ): Boolean { if (!dragSession.active) return false dragSession.update(mouseX, mouseY) @@ -241,7 +247,7 @@ class OverlayPanel( button: MouseButton, viewportWidth: Int, viewportHeight: Int, - onDragRectChanged: (Rect) -> Unit + onDragRectChanged: (Rect) -> Unit, ): Boolean { if (button != MouseButton.LEFT || !dragSession.active) return false dragSession.update(mouseX, mouseY) @@ -253,12 +259,11 @@ class OverlayPanel( return true } - private fun buildDraggedRect(viewportWidth: Int, viewportHeight: Int): Rect { - return when (dragSession.type) { + private fun buildDraggedRect(viewportWidth: Int, viewportHeight: Int): Rect = + when (dragSession.type) { OverlayPanelDragType.PanelResize -> buildResizedRect(viewportWidth, viewportHeight) else -> buildMovedRect(viewportWidth, viewportHeight) } - } private fun beginMoveDrag(mouseX: Int, mouseY: Int) { dragSession.begin( @@ -267,19 +272,20 @@ class OverlayPanel( pointerX = mouseX, pointerY = mouseY, panelState = panelState, - resizeHandle = null + resizeHandle = null, ) } private fun buildMovedRect(viewportWidth: Int, viewportHeight: Int): Rect { val dx = dragSession.currentPointerX - dragSession.startPointerX val dy = dragSession.currentPointerY - dragSession.startPointerY - val raw = Rect( - x = dragSession.startPanelX + dx, - y = dragSession.startPanelY + dy, - width = dragSession.startPanelWidth, - height = dragSession.startPanelHeight - ) + val raw = + Rect( + x = dragSession.startPanelX + dx, + y = dragSession.startPanelY + dy, + width = dragSession.startPanelWidth, + height = dragSession.startPanelHeight, + ) return clampPanel(raw, viewportWidth, viewportHeight) } @@ -296,22 +302,26 @@ class OverlayPanel( when (handle) { OverlayPanelResizeHandle.Left, OverlayPanelResizeHandle.TopLeft, - OverlayPanelResizeHandle.BottomLeft -> left += dx + OverlayPanelResizeHandle.BottomLeft, + -> left += dx OverlayPanelResizeHandle.Right, OverlayPanelResizeHandle.TopRight, - OverlayPanelResizeHandle.BottomRight -> right += dx + OverlayPanelResizeHandle.BottomRight, + -> right += dx else -> Unit } when (handle) { OverlayPanelResizeHandle.Top, OverlayPanelResizeHandle.TopLeft, - OverlayPanelResizeHandle.TopRight -> top += dy + OverlayPanelResizeHandle.TopRight, + -> top += dy OverlayPanelResizeHandle.Bottom, OverlayPanelResizeHandle.BottomLeft, - OverlayPanelResizeHandle.BottomRight -> bottom += dy + OverlayPanelResizeHandle.BottomRight, + -> bottom += dy else -> Unit } @@ -320,11 +330,13 @@ class OverlayPanel( when (handle) { OverlayPanelResizeHandle.Left, OverlayPanelResizeHandle.TopLeft, - OverlayPanelResizeHandle.BottomLeft -> left = right - minWidth + OverlayPanelResizeHandle.BottomLeft, + -> left = right - minWidth OverlayPanelResizeHandle.Right, OverlayPanelResizeHandle.TopRight, - OverlayPanelResizeHandle.BottomRight -> right = left + minWidth + OverlayPanelResizeHandle.BottomRight, + -> right = left + minWidth else -> right = left + minWidth } @@ -333,11 +345,13 @@ class OverlayPanel( when (handle) { OverlayPanelResizeHandle.Top, OverlayPanelResizeHandle.TopLeft, - OverlayPanelResizeHandle.TopRight -> top = bottom - minHeight + OverlayPanelResizeHandle.TopRight, + -> top = bottom - minHeight OverlayPanelResizeHandle.Bottom, OverlayPanelResizeHandle.BottomLeft, - OverlayPanelResizeHandle.BottomRight -> bottom = top + minHeight + OverlayPanelResizeHandle.BottomRight, + -> bottom = top + minHeight else -> bottom = top + minHeight } @@ -351,69 +365,77 @@ class OverlayPanel( when (handle) { OverlayPanelResizeHandle.Left, OverlayPanelResizeHandle.TopLeft, - OverlayPanelResizeHandle.BottomLeft -> left = left.coerceIn(minX, right - minWidth) + OverlayPanelResizeHandle.BottomLeft, + -> left = left.coerceIn(minX, right - minWidth) OverlayPanelResizeHandle.Right, OverlayPanelResizeHandle.TopRight, - OverlayPanelResizeHandle.BottomRight -> right = right.coerceIn(left + minWidth, maxRight) + OverlayPanelResizeHandle.BottomRight, + -> right = right.coerceIn(left + minWidth, maxRight) else -> Unit } when (handle) { OverlayPanelResizeHandle.Top, OverlayPanelResizeHandle.TopLeft, - OverlayPanelResizeHandle.TopRight -> top = top.coerceIn(minY, bottom - minHeight) + OverlayPanelResizeHandle.TopRight, + -> top = top.coerceIn(minY, bottom - minHeight) OverlayPanelResizeHandle.Bottom, OverlayPanelResizeHandle.BottomLeft, - OverlayPanelResizeHandle.BottomRight -> bottom = bottom.coerceIn(top + minHeight, maxBottom) + OverlayPanelResizeHandle.BottomRight, + -> bottom = bottom.coerceIn(top + minHeight, maxBottom) else -> Unit } val width = (right - left).coerceAtLeast(minWidth) val height = (bottom - top).coerceAtLeast(minHeight) - val clamped = clampPanel( - Rect(left, top, width, height), - viewportWidth = viewportWidth, - viewportHeight = viewportHeight - ) + val clamped = + clampPanel( + Rect(left, top, width, height), + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + ) return Rect( x = clamped.x, y = clamped.y, width = clamped.width.coerceAtLeast(minWidth), - height = clamped.height.coerceAtLeast(minHeight) + height = clamped.height.coerceAtLeast(minHeight), ) } private fun rebuildFrameFromState() { val panelRect = panelState.currentRectOrNull() - frame = if (panelRect == null) { - null - } else { - buildFrame(panelRect) - } + frame = + if (panelRect == null) { + null + } else { + buildFrame(panelRect) + } } private fun buildFrame(panelRect: Rect): OverlayPanelFrame { val headerRect = Rect(panelRect.x, panelRect.y, panelRect.width, style.headerHeight) - val bodyRect = Rect( - x = panelRect.x + style.panelPadding, - y = panelRect.y + style.headerHeight + style.panelPadding, - width = (panelRect.width - style.panelPadding * 2).coerceAtLeast(1), - height = (panelRect.height - style.headerHeight - style.panelPadding * 2).coerceAtLeast(1) - ) - val closeRect = Rect( - x = panelRect.x + panelRect.width - style.closeButtonMarginRight - style.closeButtonWidth, - y = panelRect.y + style.closeButtonMarginTop, - width = style.closeButtonWidth, - height = style.closeButtonHeight - ) + val bodyRect = + Rect( + x = panelRect.x + style.panelPadding, + y = panelRect.y + style.headerHeight + style.panelPadding, + width = (panelRect.width - style.panelPadding * 2).coerceAtLeast(1), + height = (panelRect.height - style.headerHeight - style.panelPadding * 2).coerceAtLeast(1), + ) + val closeRect = + Rect( + x = panelRect.x + panelRect.width - style.closeButtonMarginRight - style.closeButtonWidth, + y = panelRect.y + style.closeButtonMarginTop, + width = style.closeButtonWidth, + height = style.closeButtonHeight, + ) return OverlayPanelFrame( panelRect = panelRect, headerRect = headerRect, bodyRect = bodyRect, - closeRect = closeRect + closeRect = closeRect, ) } @@ -426,7 +448,7 @@ class OverlayPanel( x = rect.x.coerceIn(minX, maxX), y = rect.y.coerceIn(minY, maxY), width = rect.width, - height = rect.height + height = rect.height, ) } @@ -456,11 +478,17 @@ class OverlayPanel( } panelNode.applyStyle { backgroundColor = style.panelBackgroundColor - border { width = 1.px; color = style.panelBorderColor } + border { + width = 1.px + color = style.panelBorderColor + } } headerNode.applyStyle { backgroundColor = style.headerBackgroundColor - border { width = 1.px; color = style.headerBorderColor } + border { + width = 1.px + color = style.headerBorderColor + } } titleNode.applyStyle { color = style.textColor @@ -469,7 +497,10 @@ class OverlayPanel( } closeButtonNode.applyStyle { backgroundColor = style.closeButtonBackgroundColor - border { width = 1.px; color = style.closeButtonBorderColor } + border { + width = 1.px + color = style.closeButtonBorderColor + } color = style.textColor fontSize = style.fontSize.px width = style.closeButtonWidth.px @@ -482,10 +513,11 @@ class OverlayPanel( val oldNode = titleNode val oldIndex = rootNode.children.indexOf(oldNode) detachNode(oldNode) - val newNode = TextNode( - textSource = TextSource.Static(title), - key = "dsgl-overlay-panel-title-$ownerId" - ) + val newNode = + TextNode( + textSource = TextSource.Static(title), + key = "dsgl-overlay-panel-title-$ownerId", + ) newNode.applyStyle { textWrap = TextWrap.NoWrap } @@ -556,63 +588,68 @@ class OverlayPanel( panelRect.x + 2, panelRect.y + 2, panelRect.width, - panelRect.height + panelRect.height, ) panelNode.render( ctx, panelRect.x, panelRect.y, panelRect.width, - panelRect.height + panelRect.height, ) headerNode.render( ctx, headerRect.x, headerRect.y, headerRect.width, - headerRect.height + headerRect.height, ) titleNode.render( ctx, titleX, titleY, titleWidth, - titleHeight + titleHeight, ) closeButtonNode.render( ctx, closeRect.x, closeRect.y, closeRect.width, - closeRect.height + closeRect.height, ) bodyContentNode?.render( ctx, bodyRect.x, bodyRect.y, bodyRect.width, - bodyRect.height + bodyRect.height, ) overlayContentNode?.render( ctx, viewportRect.x, viewportRect.y, viewportRect.width, - viewportRect.height + viewportRect.height, ) } private class OverlayPanelRootNode( private val owner: OverlayPanel, - key: Any? + key: Any?, ) : DOMNode(key) { override val styleType: String = "dsgl-overlay-panel" - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } + override fun measure(ctx: UiMeasureContext): Size = + Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) owner.renderInto(ctx, bounds) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanelDragSession.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanelDragSession.kt index fdd27bc..9143c51 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanelDragSession.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanelDragSession.kt @@ -3,7 +3,7 @@ package org.dreamfinity.dsgl.core.overlay.panel enum class OverlayPanelDragType { PanelMove, PanelResize, - Transient + Transient, } enum class OverlayPanelResizeHandle { @@ -14,7 +14,7 @@ enum class OverlayPanelResizeHandle { TopLeft, TopRight, BottomLeft, - BottomRight + BottomRight, } class OverlayPanelDragSession { @@ -49,7 +49,7 @@ class OverlayPanelDragSession { pointerX: Int, pointerY: Int, panelState: OverlayPanelState, - resizeHandle: OverlayPanelResizeHandle? = null + resizeHandle: OverlayPanelResizeHandle? = null, ) { active = true this.ownerId = ownerId diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayCommandDslRenderer.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayCommandDslRenderer.kt index 166643d..ab62af3 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayCommandDslRenderer.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayCommandDslRenderer.kt @@ -18,10 +18,11 @@ internal object SystemOverlayCommandDslRenderer { return@forEachIndexed } - val replacement = SystemOverlayRawRenderCommandNode( - renderCommand = command, - key = "$keyPrefix-$index" - ) + val replacement = + SystemOverlayRawRenderCommandNode( + renderCommand = command, + key = "$keyPrefix-$index", + ) replacement.parent = parent if (existing == null) { children += replacement diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayDebugCounters.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayDebugCounters.kt index 043c076..81a3dc5 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayDebugCounters.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayDebugCounters.kt @@ -2,7 +2,9 @@ package org.dreamfinity.dsgl.core.overlay.system internal object SystemOverlayDebugCounters { @Volatile - private var enabled: Boolean = java.lang.Boolean.getBoolean("dsgl.systemOverlay.debugCounters") + private var enabled: Boolean = + java.lang.Boolean + .getBoolean("dsgl.systemOverlay.debugCounters") @Volatile private var rawNodeCreated: Long = 0L @@ -24,7 +26,7 @@ internal object SystemOverlayDebugCounters { val rawNodeReused: Long, val rawNodeRemoved: Long, val rawNodeActive: Long, - val rawNodePeakActive: Long + val rawNodePeakActive: Long, ) fun setEnabled(value: Boolean) { @@ -39,15 +41,14 @@ internal object SystemOverlayDebugCounters { rawNodePeakActive = 0L } - fun snapshot(): Snapshot { - return Snapshot( + fun snapshot(): Snapshot = + Snapshot( rawNodeCreated = rawNodeCreated, rawNodeReused = rawNodeReused, rawNodeRemoved = rawNodeRemoved, rawNodeActive = rawNodeActive, - rawNodePeakActive = rawNodePeakActive + rawNodePeakActive = rawNodePeakActive, ) - } fun onRawNodeCreated() { if (!enabled) return diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt index f1c7fe7..37a62e3 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt @@ -11,14 +11,14 @@ enum class SystemOverlayEntryId { ColorPickerPopup, ColorPickerTransient, PanelDemo, - TransientSession + TransientSession, } enum class SystemOverlayLane( - val zOrder: Int + val zOrder: Int, ) { PanelContent(0), - Transient(1) + Transient(1), } class SystemOverlayEntryState( @@ -26,7 +26,7 @@ class SystemOverlayEntryState( val order: Int, val lane: SystemOverlayLane = SystemOverlayLane.PanelContent, val panelState: OverlayPanelState = OverlayPanelState(), - val dragSession: OverlayPanelDragSession = OverlayPanelDragSession() + val dragSession: OverlayPanelDragSession = OverlayPanelDragSession(), ) { var active: Boolean = false internal set @@ -37,7 +37,7 @@ internal data class SystemOverlayFrameContext( val inspectedLayoutRevision: Long, val cursorX: Int, val cursorY: Int, - val inspectorPointerCaptured: Boolean + val inspectorPointerCaptured: Boolean, ) internal interface SystemOverlayEntry { @@ -65,40 +65,34 @@ internal interface SystemOverlayEntry { class SystemOverlayTransientSession( val ownerToken: Any, - val entryState: SystemOverlayEntryState = SystemOverlayEntryState( - id = SystemOverlayEntryId.TransientSession, - order = Int.MAX_VALUE - ) + val entryState: SystemOverlayEntryState = + SystemOverlayEntryState( + id = SystemOverlayEntryId.TransientSession, + order = Int.MAX_VALUE, + ), ) class SystemOverlayTransientOwnershipRegistry { private val sessions: IdentityHashMap = IdentityHashMap() - fun resolve(ownerToken: Any): SystemOverlayTransientSession { - return sessions.getOrPut(ownerToken) { + fun resolve(ownerToken: Any): SystemOverlayTransientSession = + sessions.getOrPut(ownerToken) { SystemOverlayTransientSession(ownerToken = ownerToken) } - } - fun resolve(ownerToken: Any, cursorX: Int, cursorY: Int): SystemOverlayTransientSession { - return resolve(ownerToken) - } + fun resolve(ownerToken: Any, cursorX: Int, cursorY: Int): SystemOverlayTransientSession = resolve(ownerToken) - fun release(ownerToken: Any): Boolean { - return sessions.remove(ownerToken) != null - } + fun release(ownerToken: Any): Boolean = sessions.remove(ownerToken) != null fun clear() { sessions.clear() } - fun activeSessions(): List { - return sessions.values.toList() - } + fun activeSessions(): List = sessions.values.toList() } internal class SystemOverlayEntryRegistry( - entries: List + entries: List, ) { private val orderedEntries: List = entries.sortedBy { it.state.order } private val byId: Map = orderedEntries.associateBy { it.state.id } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt index 5fa337c..1ccc386 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt @@ -17,15 +17,15 @@ import org.dreamfinity.dsgl.core.inspector.internal.SystemInspectorOverlayNode import org.dreamfinity.dsgl.core.overlay.OverlayLayerHost import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.overlay.UiLayerId +import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter +import org.dreamfinity.dsgl.core.overlay.input.dispatchManualThenDomFallback import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanel import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelStyle -import org.dreamfinity.dsgl.core.overlay.input.dispatchManualThenDomFallback -import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.StyleApplicationScope class SystemOverlayHost( - private val inspectorController: InspectorController + private val inspectorController: InspectorController, ) : OverlayLayerHost { override val layerId: UiLayerId = UiLayerId.SystemOverlay @@ -34,37 +34,37 @@ class SystemOverlayHost( private val colorPickerEntry: ColorPickerOverlayEntry = ColorPickerOverlayEntry() private val colorPickerTransientEntry: SystemOverlayEntry = ColorPickerTransientOverlayEntry(colorPickerEntry) private val overlayPanelDemoEntry: OverlayPanelDemoOverlayEntry = OverlayPanelDemoOverlayEntry() - private val entryRegistry: SystemOverlayEntryRegistry = SystemOverlayEntryRegistry( - listOf(inspectorEntry, colorPickerEntry, colorPickerTransientEntry, overlayPanelDemoEntry) - ) + private val entryRegistry: SystemOverlayEntryRegistry = + SystemOverlayEntryRegistry( + listOf(inspectorEntry, colorPickerEntry, colorPickerTransientEntry, overlayPanelDemoEntry), + ) private val transientOwnershipRegistry: SystemOverlayTransientOwnershipRegistry = SystemOverlayTransientOwnershipRegistry() - private val tree: DomTree = DomTree( - root = rootNode, - styleScope = StyleApplicationScope.SystemOverlay - ) - private var frameContext: SystemOverlayFrameContext = SystemOverlayFrameContext( - inspectedRoot = null, - inspectedLayoutRevision = 0L, - cursorX = 0, - cursorY = 0, - inspectorPointerCaptured = false - ) + private val tree: DomTree = + DomTree( + root = rootNode, + styleScope = StyleApplicationScope.SystemOverlay, + ) + private var frameContext: SystemOverlayFrameContext = + SystemOverlayFrameContext( + inspectedRoot = null, + inspectedLayoutRevision = 0L, + cursorX = 0, + cursorY = 0, + inspectorPointerCaptured = false, + ) private var knownViewportWidth: Int = 1 private var knownViewportHeight: Int = 1 - private val domInputRouter: LayerDomInputRouter = LayerDomInputRouter( - rootProvider = { - if (activeEntriesTopFirst().any { it.enablesDomInputFallbackRouting() }) rootNode else null - } - ) + private val domInputRouter: LayerDomInputRouter = + LayerDomInputRouter( + rootProvider = { + if (activeEntriesTopFirst().any { it.enablesDomInputFallbackRouting() }) rootNode else null + }, + ) - fun systemInspectorColorPickerPopupHost(): InspectorColorPickerHost { - return colorPickerEntry - } + fun systemInspectorColorPickerPopupHost(): InspectorColorPickerHost = colorPickerEntry - fun isSystemColorPickerOpen(): Boolean { - return colorPickerEntry.isOpen() - } + fun isSystemColorPickerOpen(): Boolean = colorPickerEntry.isOpen() fun captureSystemColorPickerEyedropperSample() { colorPickerEntry.captureEyedropperSample() @@ -74,9 +74,7 @@ class SystemOverlayHost( overlayPanelDemoEntry.toggle(anchorX, anchorY, knownViewportWidth, knownViewportHeight) } - fun isOverlayPanelDemoOpen(): Boolean { - return overlayPanelDemoEntry.isOpen() - } + fun isOverlayPanelDemoOpen(): Boolean = overlayPanelDemoEntry.isOpen() fun onInputFrame(viewportWidth: Int, viewportHeight: Int) { knownViewportWidth = viewportWidth.coerceAtLeast(1) @@ -92,15 +90,16 @@ class SystemOverlayHost( inspectedLayoutRevision: Long, cursorX: Int, cursorY: Int, - inspectorPointerCaptured: Boolean + inspectorPointerCaptured: Boolean, ) { - frameContext = SystemOverlayFrameContext( - inspectedRoot = inspectedRoot, - inspectedLayoutRevision = inspectedLayoutRevision, - cursorX = cursorX, - cursorY = cursorY, - inspectorPointerCaptured = inspectorPointerCaptured - ) + frameContext = + SystemOverlayFrameContext( + inspectedRoot = inspectedRoot, + inspectedLayoutRevision = inspectedLayoutRevision, + cursorX = cursorX, + cursorY = cursorY, + inspectorPointerCaptured = inspectorPointerCaptured, + ) rootNode.setViewportBounds(knownViewportWidth, knownViewportHeight) entryRegistry.allEntries().forEach { entry -> entry.sync(frameContext) @@ -115,44 +114,37 @@ class SystemOverlayHost( tree.render(ctx, width, height) } - override fun paint(ctx: UiMeasureContext): List { - return tree.paint(ctx, applyStyles = true) - } + override fun paint(ctx: UiMeasureContext): List = tree.paint(ctx, applyStyles = true) - override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean { - return dispatchManualThenDomFallback( + override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = + dispatchManualThenDomFallback( manualDispatch = { dispatchManualInput { entry -> entry.handleMouseMove(mouseX, mouseY) } }, - domFallbackDispatch = { domInputRouter.handleMouseMove(mouseX, mouseY) } + domFallbackDispatch = { domInputRouter.handleMouseMove(mouseX, mouseY) }, ) - } - override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { - return dispatchManualThenDomFallback( + override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + dispatchManualThenDomFallback( manualDispatch = { dispatchManualInput { entry -> entry.handleMouseDown(mouseX, mouseY, button) } }, - domFallbackDispatch = { domInputRouter.handleMouseDown(mouseX, mouseY, button) } + domFallbackDispatch = { domInputRouter.handleMouseDown(mouseX, mouseY, button) }, ) - } - override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { - return dispatchManualThenDomFallback( + override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + dispatchManualThenDomFallback( manualDispatch = { dispatchManualInput { entry -> entry.handleMouseUp(mouseX, mouseY, button) } }, - domFallbackDispatch = { domInputRouter.handleMouseUp(mouseX, mouseY, button) } + domFallbackDispatch = { domInputRouter.handleMouseUp(mouseX, mouseY, button) }, ) - } - override fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean { - return dispatchManualThenDomFallback( + override fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = + dispatchManualThenDomFallback( manualDispatch = { dispatchManualInput { entry -> entry.handleMouseWheel(mouseX, mouseY, delta) } }, - domFallbackDispatch = { domInputRouter.handleMouseWheel(mouseX, mouseY, delta) } + domFallbackDispatch = { domInputRouter.handleMouseWheel(mouseX, mouseY, delta) }, ) - } - override fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean { - return dispatchManualThenDomFallback( + override fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = + dispatchManualThenDomFallback( manualDispatch = { dispatchManualInput { entry -> entry.handleKeyDown(keyCode, keyChar) } }, - domFallbackDispatch = { domInputRouter.handleKeyDown(keyCode, keyChar) } + domFallbackDispatch = { domInputRouter.handleKeyDown(keyCode, keyChar) }, ) - } override fun clearRefs() { tree.clearRefs() @@ -162,117 +154,97 @@ class SystemOverlayHost( domInputRouter.clear() } - internal fun debugEntryState(id: SystemOverlayEntryId): SystemOverlayEntryState? { - return entryRegistry.entry(id)?.state - } + internal fun debugEntryState(id: SystemOverlayEntryId): SystemOverlayEntryState? = entryRegistry.entry(id)?.state - internal fun debugEntryNode(id: SystemOverlayEntryId): DOMNode? { - return entryRegistry.entry(id)?.node - } + internal fun debugEntryNode(id: SystemOverlayEntryId): DOMNode? = entryRegistry.entry(id)?.node - internal fun debugRegisteredEntryIds(): List { - return entryRegistry.allEntries().map { it.state.id } - } + internal fun debugRegisteredEntryIds(): List = entryRegistry.allEntries().map { it.state.id } internal fun debugMountedEntryIds(): List { val entriesByNode = entryRegistry.allEntries().associateBy { it.node } - val mountedNodes = buildList { - addAll(rootNode.mountedLaneNodes(SystemOverlayLane.PanelContent)) - addAll(rootNode.mountedLaneNodes(SystemOverlayLane.Transient)) - } + val mountedNodes = + buildList { + addAll(rootNode.mountedLaneNodes(SystemOverlayLane.PanelContent)) + addAll(rootNode.mountedLaneNodes(SystemOverlayLane.Transient)) + } return mountedNodes.mapNotNull { node -> entriesByNode[node]?.state?.id } } - internal fun resolveTransientSession(ownerToken: Any): SystemOverlayTransientSession { - return transientOwnershipRegistry.resolve(ownerToken) - } + internal fun resolveTransientSession(ownerToken: Any): SystemOverlayTransientSession = + transientOwnershipRegistry.resolve(ownerToken) - internal fun resolveTransientSession(ownerToken: Any, cursorX: Int, cursorY: Int): SystemOverlayTransientSession { - return transientOwnershipRegistry.resolve(ownerToken, cursorX, cursorY) - } + internal fun resolveTransientSession(ownerToken: Any, cursorX: Int, cursorY: Int): SystemOverlayTransientSession = + transientOwnershipRegistry.resolve(ownerToken, cursorX, cursorY) - internal fun releaseTransientSession(ownerToken: Any): Boolean { - return transientOwnershipRegistry.release(ownerToken) - } + internal fun releaseTransientSession(ownerToken: Any): Boolean = transientOwnershipRegistry.release(ownerToken) - internal fun debugTransientSessionCount(): Int { - return transientOwnershipRegistry.activeSessions().size - } + internal fun debugTransientSessionCount(): Int = transientOwnershipRegistry.activeSessions().size - internal fun debugSystemColorPickerHeaderRect(): Rect? { - return colorPickerEntry.debugHeaderRect() - } + internal fun debugSystemColorPickerHeaderRect(): Rect? = colorPickerEntry.debugHeaderRect() - internal fun debugSystemColorPickerCloseRect(): Rect? { - return colorPickerEntry.debugCloseRect() - } + internal fun debugSystemColorPickerCloseRect(): Rect? = colorPickerEntry.debugCloseRect() - internal fun debugSystemColorPickerBodyLayout(): ColorPickerLayout? { - return colorPickerEntry.debugBodyLayout() - } + internal fun debugSystemColorPickerBodyLayout(): ColorPickerLayout? = colorPickerEntry.debugBodyLayout() - internal fun debugSystemColorPickerState(): ColorPickerState? { - return colorPickerEntry.debugState() - } + internal fun debugSystemColorPickerState(): ColorPickerState? = colorPickerEntry.debugState() - internal fun debugSystemColorPickerPopupOwnerScope(): OverlayOwnerScope? { - return colorPickerEntry.debugOwnerScope() - } + internal fun debugSystemColorPickerPopupOwnerScope(): OverlayOwnerScope? = colorPickerEntry.debugOwnerScope() - internal fun debugRootBounds(): Rect { - return rootNode.bounds - } + internal fun debugRootBounds(): Rect = rootNode.bounds private fun reconcileMountedEntries() { val activeEntries = entryRegistry.allEntries().filter { it.state.active } - val panelNodes = activeEntries - .filter { it.state.lane == SystemOverlayLane.PanelContent } - .map { it.node } - val transientNodes = activeEntries - .filter { it.state.lane == SystemOverlayLane.Transient } - .map { it.node } + val panelNodes = + activeEntries + .filter { it.state.lane == SystemOverlayLane.PanelContent } + .map { it.node } + val transientNodes = + activeEntries + .filter { it.state.lane == SystemOverlayLane.Transient } + .map { it.node } rootNode.setLaneChildren( panelNodes = panelNodes, - transientNodes = transientNodes + transientNodes = transientNodes, ) } - private fun activeEntriesTopFirst(): List { - return entryRegistry.allEntries() + private fun activeEntriesTopFirst(): List = + entryRegistry + .allEntries() .filter { it.state.active } .sortedWith( compareBy { it.state.lane.zOrder } - .thenBy { it.state.order } - ) - .asReversed() - } + .thenBy { it.state.order }, + ).asReversed() - private inline fun dispatchManualInput(handler: (SystemOverlayEntry) -> Boolean): Boolean { - return activeEntriesTopFirst() + private inline fun dispatchManualInput(handler: (SystemOverlayEntry) -> Boolean): Boolean = + activeEntriesTopFirst() .asSequence() .filter { entry -> !entry.participatesInDomInput() } .any(handler) - } private class InspectorOverlayEntry( - private val inspectorController: InspectorController + private val inspectorController: InspectorController, ) : SystemOverlayEntry { - override val state: SystemOverlayEntryState = SystemOverlayEntryState( - id = SystemOverlayEntryId.Inspector, - order = 100, - lane = SystemOverlayLane.PanelContent - ) - private val overlayPanel: OverlayPanel = OverlayPanel( - ownerId = state.id, - panelState = state.panelState, - dragSession = state.dragSession - ) - override val node: SystemInspectorOverlayNode = SystemInspectorOverlayNode( - controller = inspectorController, - overlayPanel = overlayPanel - ) + override val state: SystemOverlayEntryState = + SystemOverlayEntryState( + id = SystemOverlayEntryId.Inspector, + order = 100, + lane = SystemOverlayLane.PanelContent, + ) + private val overlayPanel: OverlayPanel = + OverlayPanel( + ownerId = state.id, + panelState = state.panelState, + dragSession = state.dragSession, + ) + override val node: SystemInspectorOverlayNode = + SystemInspectorOverlayNode( + controller = inspectorController, + overlayPanel = overlayPanel, + ) private var viewportWidth: Int = 1 private var viewportHeight: Int = 1 @@ -307,7 +279,7 @@ class SystemOverlayHost( minWidth = 240, minHeight = 160, style = inspectorPanelStyle(), - onClose = inspectorController::onPanelMinimizeTogglePressed + onClose = inspectorController::onPanelMinimizeTogglePressed, ) val panelRect = inspectorController.overlayExpandedPanelRect() if (panelRect != null) { @@ -323,7 +295,7 @@ class SystemOverlayHost( mouseX = frame.cursorX, mouseY = frame.cursorY, viewportWidth = viewportWidth, - viewportHeight = viewportHeight + viewportHeight = viewportHeight, ) { rect -> inspectorController.onOverlayPanelRectChanged(rect, viewportWidth, viewportHeight) } @@ -337,23 +309,28 @@ class SystemOverlayHost( } } - private class ColorPickerOverlayEntry : SystemOverlayEntry, InspectorColorPickerHost { - override val state: SystemOverlayEntryState = SystemOverlayEntryState( - id = SystemOverlayEntryId.ColorPickerPopup, - order = 200, - lane = SystemOverlayLane.PanelContent - ) + private class ColorPickerOverlayEntry : + SystemOverlayEntry, + InspectorColorPickerHost { + override val state: SystemOverlayEntryState = + SystemOverlayEntryState( + id = SystemOverlayEntryId.ColorPickerPopup, + order = 200, + lane = SystemOverlayLane.PanelContent, + ) private val ownerToken: Any = Any() private val popupEngine: ColorPickerPopupEngine = ColorPickerPopupEngine() - private val overlayPanel: OverlayPanel = OverlayPanel( - ownerId = state.id, - panelState = state.panelState, - dragSession = state.dragSession - ) - override val node: SystemColorPickerOverlayNode = SystemColorPickerOverlayNode( - popupEngine = popupEngine, - overlayPanel = overlayPanel - ) + private val overlayPanel: OverlayPanel = + OverlayPanel( + ownerId = state.id, + panelState = state.panelState, + dragSession = state.dragSession, + ) + override val node: SystemColorPickerOverlayNode = + SystemColorPickerOverlayNode( + popupEngine = popupEngine, + overlayPanel = overlayPanel, + ) private val transientNode: SystemColorPickerTransientOverlayNode = SystemColorPickerTransientOverlayNode(popupEngine = popupEngine) private var draggable: Boolean = true @@ -373,10 +350,12 @@ class SystemOverlayHost( overlayPanel.configure( title = popupEngine.debugTitle(ownerToken) ?: "Color Picker", draggable = draggable, - style = popupEngine.debugStyle(ownerToken) - ?.let { toOverlayPanelStyle(it) } - ?: OverlayPanelStyle(), - onClose = ::close + style = + popupEngine + .debugStyle(ownerToken) + ?.let { toOverlayPanelStyle(it) } + ?: OverlayPanelStyle(), + onClose = ::close, ) val panelRect = popupEngine.debugPanelRect(ownerToken) if (panelRect != null) { @@ -389,7 +368,7 @@ class SystemOverlayHost( mouseX = frame.cursorX, mouseY = frame.cursorY, viewportWidth = viewportWidth, - viewportHeight = viewportHeight + viewportHeight = viewportHeight, ) { rect -> popupEngine.forcePanelRect(ownerToken, rect) } @@ -412,7 +391,7 @@ class SystemOverlayHost( mouseX = mouseX, mouseY = mouseY, viewportWidth = viewportWidth, - viewportHeight = viewportHeight + viewportHeight = viewportHeight, ) { rect -> popupEngine.forcePanelRect(ownerToken, rect) } @@ -443,7 +422,7 @@ class SystemOverlayHost( mouseY = mouseY, button = button, viewportWidth = viewportWidth, - viewportHeight = viewportHeight + viewportHeight = viewportHeight, ) { rect -> popupEngine.forcePanelRect(ownerToken, rect) } @@ -485,7 +464,7 @@ class SystemOverlayHost( onPreview: ((RgbaColor) -> Unit)?, onChange: ((RgbaColor) -> Unit)?, onCommit: ((RgbaColor) -> Unit)?, - onClose: (() -> Unit)? + onClose: (() -> Unit)?, ) { this.draggable = draggable popupEngine.open( @@ -502,8 +481,8 @@ class SystemOverlayHost( onPreview = onPreview, onChange = onChange, onCommit = onCommit, - onClose = onClose - ) + onClose = onClose, + ), ) } @@ -514,53 +493,39 @@ class SystemOverlayHost( state.active = false } - override fun isOpen(): Boolean { - return popupEngine.isOpenFor(ownerToken) - } + override fun isOpen(): Boolean = popupEngine.isOpenFor(ownerToken) - fun transientOverlayNode(): DOMNode { - return transientNode - } + fun transientOverlayNode(): DOMNode = transientNode fun isTransientActive(): Boolean { val controller = popupEngine.debugController(ownerToken) ?: return false return controller.viewModeDropdownOpen() || controller.isEyedropperActive() } - fun debugHeaderRect(): Rect? { - return overlayPanel.headerRect() - } + fun debugHeaderRect(): Rect? = overlayPanel.headerRect() - fun debugCloseRect(): Rect? { - return overlayPanel.closeRect() - } + fun debugCloseRect(): Rect? = overlayPanel.closeRect() - fun debugBodyLayout(): ColorPickerLayout? { - return popupEngine.debugBodyLayout(ownerToken) - } + fun debugBodyLayout(): ColorPickerLayout? = popupEngine.debugBodyLayout(ownerToken) - fun debugState(): ColorPickerState? { - return popupEngine.debugController(ownerToken)?.snapshot() - } + fun debugState(): ColorPickerState? = popupEngine.debugController(ownerToken)?.snapshot() fun captureEyedropperSample() { popupEngine.captureEyedropperSample() } - fun debugOwnerScope(): OverlayOwnerScope? { - return popupEngine.debugOwnerScope(ownerToken) - } + fun debugOwnerScope(): OverlayOwnerScope? = popupEngine.debugOwnerScope(ownerToken) } - private class ColorPickerTransientOverlayEntry( - private val panelEntry: ColorPickerOverlayEntry + private val panelEntry: ColorPickerOverlayEntry, ) : SystemOverlayEntry { - override val state: SystemOverlayEntryState = SystemOverlayEntryState( - id = SystemOverlayEntryId.ColorPickerTransient, - order = 210, - lane = SystemOverlayLane.Transient - ) + override val state: SystemOverlayEntryState = + SystemOverlayEntryState( + id = SystemOverlayEntryId.ColorPickerTransient, + order = 210, + lane = SystemOverlayLane.Transient, + ) override val node: DOMNode = panelEntry.transientOverlayNode() override fun sync(frame: SystemOverlayFrameContext) { @@ -569,16 +534,18 @@ class SystemOverlayHost( } private class OverlayPanelDemoOverlayEntry : SystemOverlayEntry { - override val state: SystemOverlayEntryState = SystemOverlayEntryState( - id = SystemOverlayEntryId.PanelDemo, - order = 300, - lane = SystemOverlayLane.PanelContent - ) - private val overlayPanel: OverlayPanel = OverlayPanel( - ownerId = state.id, - panelState = state.panelState, - dragSession = state.dragSession - ) + override val state: SystemOverlayEntryState = + SystemOverlayEntryState( + id = SystemOverlayEntryId.PanelDemo, + order = 300, + lane = SystemOverlayLane.PanelContent, + ) + private val overlayPanel: OverlayPanel = + OverlayPanel( + ownerId = state.id, + panelState = state.panelState, + dragSession = state.dragSession, + ) private val demoNode: SystemOverlayPanelDemoNode = SystemOverlayPanelDemoNode(overlayPanel) override val node: DOMNode = demoNode private var opened: Boolean = false @@ -586,7 +553,12 @@ class SystemOverlayHost( private var viewportHeight: Int = 1 private var buttonClicks: Int = 0 - fun toggle(anchorX: Int, anchorY: Int, viewportWidth: Int, viewportHeight: Int) { + fun toggle( + anchorX: Int, + anchorY: Int, + viewportWidth: Int, + viewportHeight: Int, + ) { if (opened) { close() return @@ -624,7 +596,7 @@ class SystemOverlayHost( title = "Overlay PanelF", draggable = true, style = OverlayPanelStyle(fontSize = 16), - onClose = ::close + onClose = ::close, ) overlayPanel.syncPanelRect(state.panelState.currentRectOrNull()) demoNode.setButtonClicks(buttonClicks) @@ -632,7 +604,7 @@ class SystemOverlayHost( mouseX = frame.cursorX, mouseY = frame.cursorY, viewportWidth = viewportWidth, - viewportHeight = viewportHeight + viewportHeight = viewportHeight, ) { rect -> state.panelState.updateFromRect(rect) } @@ -649,7 +621,7 @@ class SystemOverlayHost( mouseX = mouseX, mouseY = mouseY, viewportWidth = viewportWidth, - viewportHeight = viewportHeight + viewportHeight = viewportHeight, ) { rect -> state.panelState.updateFromRect(rect) } @@ -685,7 +657,7 @@ class SystemOverlayHost( mouseY = mouseY, button = button, viewportWidth = viewportWidth, - viewportHeight = viewportHeight + viewportHeight = viewportHeight, ) { rect -> state.panelState.updateFromRect(rect) } @@ -698,8 +670,8 @@ class SystemOverlayHost( } private companion object { - private fun inspectorPanelStyle(): OverlayPanelStyle { - return OverlayPanelStyle( + private fun inspectorPanelStyle(): OverlayPanelStyle = + OverlayPanelStyle( headerHeight = 52, panelPadding = 6, resizeHandleSize = 8, @@ -712,12 +684,11 @@ class SystemOverlayHost( closeButtonBorderColor = 0x775E738C, textColor = 0xFFE6EDF6.toInt(), fontSize = 24, - closeGlyph = "-" + closeGlyph = "-", ) - } - private fun toOverlayPanelStyle(style: ColorPickerStyle): OverlayPanelStyle { - return OverlayPanelStyle( + private fun toOverlayPanelStyle(style: ColorPickerStyle): OverlayPanelStyle = + OverlayPanelStyle( panelBackgroundColor = style.panelBackgroundColor, panelBorderColor = style.panelBorderColor, panelShadowColor = style.panelShadowColor, @@ -726,8 +697,7 @@ class SystemOverlayHost( closeButtonBackgroundColor = style.buttonBackgroundColor, closeButtonBorderColor = style.inputBorderColor, textColor = style.textColor, - fontSize = style.fontSize + fontSize = style.fontSize, ) - } } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayPanelDemoNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayPanelDemoNode.kt index bf44dd9..3a01398 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayPanelDemoNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayPanelDemoNode.kt @@ -10,7 +10,7 @@ import org.dreamfinity.dsgl.core.render.RenderCommand internal class SystemOverlayPanelDemoNode( private val overlayPanel: OverlayPanel, - key: Any? = "dsgl-system-panel-panel-demo" + key: Any? = "dsgl-system-panel-panel-demo", ) : DOMNode(key) { override val styleType: String = "dsgl-system-panel-panel-demo" @@ -25,17 +25,22 @@ internal class SystemOverlayPanelDemoNode( fun buttonRect(): Rect? = bodyNode.buttonRect() - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } + override fun measure(ctx: UiMeasureContext): Size = + Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) panelNode.render(ctx, x, y, width, height) } private class DemoBodyNode( - key: Any? = "dsgl-system-panel-demo-body" + key: Any? = "dsgl-system-panel-demo-body", ) : DOMNode(key) { override val styleType: String = "dsgl-system-panel-demo-body" @@ -52,59 +57,70 @@ internal class SystemOverlayPanelDemoNode( fun buttonRect(): Rect? = actionButtonRect - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } + override fun measure(ctx: UiMeasureContext): Size = + Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) commandBuffer.clear() actionButtonRect = null - commandBuffer += RenderCommand.DrawText( - text = "Reusable panel demo", - x = bounds.x + 6, - y = bounds.y + 4, - color = 0xFFFFFFFF.toInt(), - fontSize = 16 - ) - commandBuffer += RenderCommand.DrawText( - text = "Button clicks: $buttonClicks", - x = bounds.x + 6, - y = bounds.y + 22, - color = 0xFFB8C9DA.toInt(), - fontSize = 16 - ) + commandBuffer += + RenderCommand.DrawText( + text = "Reusable panel demo", + x = bounds.x + 6, + y = bounds.y + 4, + color = 0xFFFFFFFF.toInt(), + fontSize = 16, + ) + commandBuffer += + RenderCommand.DrawText( + text = "Button clicks: $buttonClicks", + x = bounds.x + 6, + y = bounds.y + 22, + color = 0xFFB8C9DA.toInt(), + fontSize = 16, + ) val buttonRect = Rect(bounds.x + 6, bounds.y + 44, 120, 24) actionButtonRect = buttonRect - commandBuffer += RenderCommand.DrawRect( - buttonRect.x, - buttonRect.y, - buttonRect.width, - buttonRect.height, - 0xFF314154.toInt() - ) + commandBuffer += + RenderCommand.DrawRect( + buttonRect.x, + buttonRect.y, + buttonRect.width, + buttonRect.height, + 0xFF314154.toInt(), + ) drawBorder(commandBuffer, buttonRect, 0xFF6F879E.toInt()) - commandBuffer += RenderCommand.DrawText( - text = "Click me", - x = buttonRect.x + 8, - y = buttonRect.y + 4, - color = 0xFFFFFFFF.toInt(), - fontSize = 16 - ) - commandBuffer += RenderCommand.DrawImage( - resource = "minecraft:textures/gui/options_background.png", - x = bounds.x + bounds.width - 52, - y = bounds.y + 6, - width = 44, - height = 44 - ) - commandBuffer += RenderCommand.DrawText( - text = "Drag the title bar to move.", - x = bounds.x + 6, - y = bounds.y + 78, - color = 0xFFB8C9DA.toInt(), - fontSize = 16 - ) + commandBuffer += + RenderCommand.DrawText( + text = "Click me", + x = buttonRect.x + 8, + y = buttonRect.y + 4, + color = 0xFFFFFFFF.toInt(), + fontSize = 16, + ) + commandBuffer += + RenderCommand.DrawImage( + resource = "minecraft:textures/gui/options_background.png", + x = bounds.x + bounds.width - 52, + y = bounds.y + 6, + width = 44, + height = 44, + ) + commandBuffer += + RenderCommand.DrawText( + text = "Drag the title bar to move.", + x = bounds.x + 6, + y = bounds.y + 78, + color = 0xFFB8C9DA.toInt(), + fontSize = 16, + ) if (SystemOverlayCommandDslRenderer.rebuildInto(this, commandBuffer, "system-panel-panel-demo-body")) { renderCommandsRevision += 1L markRenderCommandsDirty() @@ -114,9 +130,7 @@ internal class SystemOverlayPanelDemoNode( } } - override fun volatileRenderCommandsSignature(nowMs: Long): Long { - return renderCommandsRevision - } + override fun volatileRenderCommandsSignature(nowMs: Long): Long = renderCommandsRevision private fun drawBorder(out: MutableList, rect: Rect, color: Int) { if (rect.width <= 0 || rect.height <= 0) return diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayRawRenderCommandNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayRawRenderCommandNode.kt index 04190ed..d70e320 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayRawRenderCommandNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayRawRenderCommandNode.kt @@ -8,7 +8,7 @@ import org.dreamfinity.dsgl.core.render.RenderCommand internal class SystemOverlayRawRenderCommandNode( renderCommand: RenderCommand, - key: Any? + key: Any?, ) : DOMNode(key) { override val styleType: String = "dsgl-system-raw-render-command" private var renderCommand: RenderCommand = renderCommand @@ -24,7 +24,13 @@ internal class SystemOverlayRawRenderCommandNode( override fun measure(ctx: UiMeasureContext): Size = Size(0, 0) - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) } @@ -32,7 +38,5 @@ internal class SystemOverlayRawRenderCommandNode( out += renderCommand } - override fun volatileRenderCommandsSignature(nowMs: Long): Long { - return signature - } + override fun volatileRenderCommandsSignature(nowMs: Long): Long = signature } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayRootNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayRootNode.kt index e1476a1..240ac79 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayRootNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayRootNode.kt @@ -1,7 +1,6 @@ package org.dreamfinity.dsgl.core.overlay.system import org.dreamfinity.dsgl.core.DsglColors -import org.dreamfinity.dsgl.core.dsl.UiScope import org.dreamfinity.dsgl.core.debug.OverlayLayerDebugState.isTintEnabled import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.elements.ContainerNode @@ -9,6 +8,7 @@ import org.dreamfinity.dsgl.core.dom.layout.Border import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.Size import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.dsl.UiScope import org.dreamfinity.dsgl.core.dsl.div import org.dreamfinity.dsgl.core.font.FontRegistry import org.dreamfinity.dsgl.core.overlay.OverlayDebugVisualization @@ -17,25 +17,28 @@ import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.StyleEngine internal class SystemOverlayRootNode( - key: Any? = "dsgl-system-overlay-root" + key: Any? = "dsgl-system-overlay-root", ) : DOMNode(key) { override val styleType: String = "dsgl-system-overlay-root" private var viewportWidth: Int = 0 private var viewportHeight: Int = 0 - private val debugTintNode: ContainerNode = UiScope(this).div({ - this.key = "dsgl-system-overlay-debug-tint" - style = { - display = Display.None - } - }) - private val panelLaneNode: SystemOverlayLaneNode = SystemOverlayLaneNode( - key = "dsgl-system-overlay-panel-lane", - laneStyleType = "dsgl-system-overlay-panel-lane" - ) - private val transientLaneNode: SystemOverlayLaneNode = SystemOverlayLaneNode( - key = "dsgl-system-overlay-transient-lane", - laneStyleType = "dsgl-system-overlay-transient-lane" - ) + private val debugTintNode: ContainerNode = + UiScope(this).div({ + this.key = "dsgl-system-overlay-debug-tint" + style = { + display = Display.None + } + }) + private val panelLaneNode: SystemOverlayLaneNode = + SystemOverlayLaneNode( + key = "dsgl-system-overlay-panel-lane", + laneStyleType = "dsgl-system-overlay-panel-lane", + ) + private val transientLaneNode: SystemOverlayLaneNode = + SystemOverlayLaneNode( + key = "dsgl-system-overlay-transient-lane", + laneStyleType = "dsgl-system-overlay-transient-lane", + ) init { panelLaneNode.parent = this @@ -53,31 +56,33 @@ internal class SystemOverlayRootNode( transientLaneNode.bounds = rect } - internal fun setLaneChildren( - panelNodes: List, - transientNodes: List - ) { + internal fun setLaneChildren(panelNodes: List, transientNodes: List) { reconcileLane(panelLaneNode, panelNodes) reconcileLane(transientLaneNode, transientNodes) } - internal fun mountedLaneNodes(lane: SystemOverlayLane): List { - return when (lane) { + internal fun mountedLaneNodes(lane: SystemOverlayLane): List = + when (lane) { SystemOverlayLane.PanelContent -> panelLaneNode.children.toList() SystemOverlayLane.Transient -> transientLaneNode.children.toList() } - } override fun measure(ctx: UiMeasureContext): Size { val resolvedWidth = if (viewportWidth > 0) viewportWidth else StyleEngine.viewportWidthPx().coerceAtLeast(0) val resolvedHeight = if (viewportHeight > 0) viewportHeight else StyleEngine.viewportHeightPx().coerceAtLeast(0) return Size( width = resolvedWidth, - height = resolvedHeight + height = resolvedHeight, ) } - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { setViewportBounds(width, height) bounds = Rect(0, 0, viewportWidth, viewportHeight) val tintEnabled = OverlayDebugVisualization.enabled && isTintEnabled(UiLayerId.SystemOverlay) @@ -102,8 +107,9 @@ internal class SystemOverlayRootNode( private fun reconcileLane(laneNode: SystemOverlayLaneNode, desiredNodes: List) { val currentNodes = laneNode.children - val unchanged = currentNodes.size == desiredNodes.size && - currentNodes.indices.all { index -> currentNodes[index] === desiredNodes[index] } + val unchanged = + currentNodes.size == desiredNodes.size && + currentNodes.indices.all { index -> currentNodes[index] === desiredNodes[index] } if (unchanged) return currentNodes.forEach { node -> node.parent = null @@ -118,15 +124,20 @@ internal class SystemOverlayRootNode( private class SystemOverlayLaneNode( key: Any?, - private val laneStyleType: String + private val laneStyleType: String, ) : DOMNode(key) { override val styleType: String = laneStyleType - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } + override fun measure(ctx: UiMeasureContext): Size = + Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) children.forEach { child -> child.render(ctx, x, y, width, height) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/popup/FloatingPaneDragModel.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/popup/FloatingPaneDragModel.kt index 797c8e9..6ef2c67 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/popup/FloatingPaneDragModel.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/popup/FloatingPaneDragModel.kt @@ -4,7 +4,7 @@ import org.dreamfinity.dsgl.core.dom.layout.Rect import kotlin.math.abs class FloatingPaneDragModel( - private val moveThresholdPx: Int = 2 + private val moveThresholdPx: Int = 2, ) { private var startRect: Rect = Rect(0, 0, 0, 0) private var dragOffsetX: Int = 0 @@ -29,17 +29,20 @@ class FloatingPaneDragModel( mouseY: Int, viewportWidth: Int, viewportHeight: Int, - clamp: (Rect, Int, Int) -> Rect + clamp: (Rect, Int, Int) -> Rect, ): Rect { if (!dragging) return startRect - val target = Rect( - x = mouseX - dragOffsetX, - y = mouseY - dragOffsetY, - width = startRect.width, - height = startRect.height - ) + val target = + Rect( + x = mouseX - dragOffsetX, + y = mouseY - dragOffsetY, + width = startRect.width, + height = startRect.height, + ) val clamped = clamp(target, viewportWidth, viewportHeight) - if (!moved && (abs(clamped.x - startRect.x) >= moveThresholdPx || abs(clamped.y - startRect.y) >= moveThresholdPx)) { + if (!moved && + (abs(clamped.x - startRect.x) >= moveThresholdPx || abs(clamped.y - startRect.y) >= moveThresholdPx) + ) { moved = true } return clamped diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/render/RenderCommand.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/render/RenderCommand.kt index 62347c9..4bf09e4 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/render/RenderCommand.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/render/RenderCommand.kt @@ -13,7 +13,7 @@ sealed class RenderCommand { val y: Int, val width: Int, val height: Int, - val color: Int + val color: Int, ) : RenderCommand() /** Saturation/value picker field for color picker UI. */ @@ -22,7 +22,7 @@ sealed class RenderCommand { val y: Int, val width: Int, val height: Int, - val hueDeg: Float + val hueDeg: Float, ) : RenderCommand() /** Hue gradient bar for color picker UI. */ @@ -30,7 +30,7 @@ sealed class RenderCommand { val x: Int, val y: Int, val width: Int, - val height: Int + val height: Int, ) : RenderCommand() /** Alpha gradient bar for color picker UI over existing checker background. */ @@ -39,7 +39,7 @@ sealed class RenderCommand { val y: Int, val width: Int, val height: Int, - val rgbColor: Int + val rgbColor: Int, ) : RenderCommand() /** Procedural checkerboard background rendered efficiently by backend. */ @@ -52,7 +52,7 @@ sealed class RenderCommand { val lightColor: Int, val darkColor: Int, val offsetX: Int = 0, - val offsetY: Int = 0 + val offsetY: Int = 0, ) : RenderCommand() /** Text draw command. */ @@ -70,14 +70,14 @@ sealed class RenderCommand { val strikethrough: Boolean = false, val obfuscated: Boolean = false, val textStyleSpans: List = emptyList(), - val sourceKey: String? = null + val sourceKey: String? = null, ) : RenderCommand() { /** * Returns a DrawText command with replaced color while preserving * all other fields. This avoids relying on Kotlin synthetic copy$default ABI. */ - fun withColor(newColor: Int): DrawText { - return DrawText( + fun withColor(newColor: Int): DrawText = + DrawText( text = text, x = x, y = y, @@ -91,9 +91,8 @@ sealed class RenderCommand { strikethrough = strikethrough, obfuscated = obfuscated, textStyleSpans = textStyleSpans, - sourceKey = sourceKey + sourceKey = sourceKey, ) - } } data class TextStyleSpan( @@ -104,7 +103,7 @@ sealed class RenderCommand { val italic: Boolean, val underline: Boolean, val strikethrough: Boolean, - val obfuscated: Boolean + val obfuscated: Boolean, ) /** Image draw command. */ @@ -113,7 +112,7 @@ sealed class RenderCommand { val x: Int, val y: Int, val width: Int, - val height: Int + val height: Int, ) : RenderCommand() /** @@ -125,7 +124,7 @@ sealed class RenderCommand { val sourceY: Int, val sourceWidth: Int, val sourceHeight: Int, - val fallbackColor: Int + val fallbackColor: Int, ) : RenderCommand() /** Draws previously captured screen region as a magnified textured quad. */ @@ -134,7 +133,7 @@ sealed class RenderCommand { val y: Int, val width: Int, val height: Int, - val gridOverlay: CapturedGridOverlay? = null + val gridOverlay: CapturedGridOverlay? = null, ) : RenderCommand() /** Optional grid overlay rendered by backend as part of captured-region magnifier pass. */ @@ -142,7 +141,7 @@ sealed class RenderCommand { val columns: Int, val rows: Int, val magnification: Int, - val color: Int + val color: Int, ) /** Item stack draw command. */ @@ -153,7 +152,7 @@ sealed class RenderCommand { val width: Int, val size: Int, val rotYDeg: Double = 0.0, - val rotXDeg: Double = 0.0 + val rotXDeg: Double = 0.0, ) : RenderCommand() /** Pushes a clipping rectangle (GUI coordinates, top-left origin). */ @@ -161,7 +160,7 @@ sealed class RenderCommand { val x: Int, val y: Int, val width: Int, - val height: Int + val height: Int, ) : RenderCommand() /** Pops the current clipping rectangle. */ @@ -175,7 +174,7 @@ sealed class RenderCommand { val translateY: Float, val scaleX: Float, val scaleY: Float, - val rotateDeg: Float + val rotateDeg: Float, ) : RenderCommand() /** Pops current transform. */ @@ -183,7 +182,7 @@ sealed class RenderCommand { /** Multiplies current alpha by opacity (0..1). */ data class PushOpacity( - val opacity: Float + val opacity: Float, ) : RenderCommand() /** Pops current opacity multiplier. */ diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectDsl.kt index 8b19848..d4c4212 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectDsl.kt @@ -3,16 +3,13 @@ package org.dreamfinity.dsgl.core.select @DslMarker annotation class SelectDsl -fun selectModel( - id: String? = null, - block: SelectModelBuilder.() -> Unit -): SelectModel { +fun selectModel(id: String? = null, block: SelectModelBuilder.() -> Unit): SelectModel { val builder = SelectModelBuilder() builder.block() return SelectModel( id = id, placeholderProvider = builder.placeholderProvider, - entries = builder.buildEntries() + entries = builder.buildEntries(), ) } @@ -29,52 +26,38 @@ class SelectModelBuilder { placeholderProvider = provider } - fun option( - id: String, - label: String, - block: SelectOptionBuilder.() -> Unit = {} - ) { + fun option(id: String, label: String, block: SelectOptionBuilder.() -> Unit = {}) { option(id = id, labelProvider = { label }, block = block) } - fun option( - id: String, - labelProvider: () -> String, - block: SelectOptionBuilder.() -> Unit = {} - ) { + fun option(id: String, labelProvider: () -> String, block: SelectOptionBuilder.() -> Unit = {}) { val builder = SelectOptionBuilder() builder.block() - entries += SelectEntry.Option( - id = id, - labelProvider = labelProvider, - enabledProvider = builder.enabledProvider - ) + entries += + SelectEntry.Option( + id = id, + labelProvider = labelProvider, + enabledProvider = builder.enabledProvider, + ) } fun separator(id: String? = null) { entries += SelectEntry.Separator(id = id) } - fun group( - label: String, - id: String? = null, - block: SelectGroupBuilder.() -> Unit - ) { + fun group(label: String, id: String? = null, block: SelectGroupBuilder.() -> Unit) { group(labelProvider = { label }, id = id, block = block) } - fun group( - labelProvider: () -> String, - id: String? = null, - block: SelectGroupBuilder.() -> Unit - ) { + fun group(labelProvider: () -> String, id: String? = null, block: SelectGroupBuilder.() -> Unit) { val builder = SelectGroupBuilder() builder.block() - entries += SelectEntry.Group( - id = id, - labelProvider = labelProvider, - entries = builder.buildEntries() - ) + entries += + SelectEntry.Group( + id = id, + labelProvider = labelProvider, + entries = builder.buildEntries(), + ) } internal fun buildEntries(): List = entries.toList() @@ -84,52 +67,38 @@ class SelectModelBuilder { class SelectGroupBuilder { private val entries: MutableList = ArrayList() - fun option( - id: String, - label: String, - block: SelectOptionBuilder.() -> Unit = {} - ) { + fun option(id: String, label: String, block: SelectOptionBuilder.() -> Unit = {}) { option(id = id, labelProvider = { label }, block = block) } - fun option( - id: String, - labelProvider: () -> String, - block: SelectOptionBuilder.() -> Unit = {} - ) { + fun option(id: String, labelProvider: () -> String, block: SelectOptionBuilder.() -> Unit = {}) { val builder = SelectOptionBuilder() builder.block() - entries += SelectEntry.Option( - id = id, - labelProvider = labelProvider, - enabledProvider = builder.enabledProvider - ) + entries += + SelectEntry.Option( + id = id, + labelProvider = labelProvider, + enabledProvider = builder.enabledProvider, + ) } fun separator(id: String? = null) { entries += SelectEntry.Separator(id = id) } - fun group( - label: String, - id: String? = null, - block: SelectGroupBuilder.() -> Unit - ) { + fun group(label: String, id: String? = null, block: SelectGroupBuilder.() -> Unit) { group(labelProvider = { label }, id = id, block = block) } - fun group( - labelProvider: () -> String, - id: String? = null, - block: SelectGroupBuilder.() -> Unit - ) { + fun group(labelProvider: () -> String, id: String? = null, block: SelectGroupBuilder.() -> Unit) { val builder = SelectGroupBuilder() builder.block() - entries += SelectEntry.Group( - id = id, - labelProvider = labelProvider, - entries = builder.buildEntries() - ) + entries += + SelectEntry.Group( + id = id, + labelProvider = labelProvider, + entries = builder.buildEntries(), + ) } internal fun buildEntries(): List = entries.toList() diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt index 51ca007..97cbe57 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt @@ -12,7 +12,7 @@ import org.dreamfinity.dsgl.core.render.RenderCommand class SelectEngine( private val clock: SelectClock = SystemSelectClock, - private val measurementCache: SelectMeasurementCache = SelectMeasurementCache() + private val measurementCache: SelectMeasurementCache = SelectMeasurementCache(), ) : SelectHost { private data class PopupState( val owner: Any, @@ -31,7 +31,7 @@ class SelectEngine( var highlightedIndex: Int = -1, var scrollOffset: Int = 0, var scrollbarDragging: Boolean = false, - var scrollbarDragThumbOffsetY: Int = 0 + var scrollbarDragThumbOffsetY: Int = 0, ) data class Snapshot( @@ -40,14 +40,14 @@ class SelectEngine( val highlightedIndex: Int, val hoveredIndex: Int, val scrollOffset: Int, - val animationProgress: Float + val animationProgress: Float, ) private enum class VisibilityState { Hidden, Opening, Open, - Closing + Closing, } private var popup: PopupState? = null @@ -96,18 +96,19 @@ class SelectEngine( animationProgress = 0f onClose?.invoke() } - popup = PopupState( - owner = request.owner, - modelToken = request.modelToken, - entries = request.entries, - selectedId = request.selectedId, - anchorRect = request.anchorRect, - closeOnSelect = request.closeOnSelect, - onSelect = request.onSelect, - onClose = request.onClose, - fontId = request.fontId, - fontSize = request.fontSize - ) + popup = + PopupState( + owner = request.owner, + modelToken = request.modelToken, + entries = request.entries, + selectedId = request.selectedId, + anchorRect = request.anchorRect, + closeOnSelect = request.closeOnSelect, + onSelect = request.onSelect, + onClose = request.onClose, + fontId = request.fontId, + fontSize = request.fontSize, + ) typeAheadBuffer = "" typeAheadLastInputMs = 0L ensureHighlightValid(popup!!) @@ -158,7 +159,7 @@ class SelectEngine( highlightedIndex = current?.highlightedIndex ?: -1, hoveredIndex = current?.hoveredIndex ?: -1, scrollOffset = current?.scrollOffset ?: 0, - animationProgress = animationProgress + animationProgress = animationProgress, ) } @@ -178,7 +179,7 @@ class SelectEngine( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, - viewportScale: Float = 1f + viewportScale: Float = 1f, ) { lastMeasureContext = measureContext if (this.viewportWidth != viewportWidth || this.viewportHeight != viewportHeight) { @@ -198,7 +199,7 @@ class SelectEngine( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, - out: MutableList + out: MutableList, ) { if (!isOpen()) return onFrame(measureContext, viewportWidth, viewportHeight, viewportScale) @@ -215,15 +216,16 @@ class SelectEngine( if (progress < 0.999f) { val scale = style.openStartScale + (1f - style.openStartScale) * progress val translateY = style.openStartOffsetYPx * (1f - progress) - out += RenderCommand.PushTransform( - originX = panel.x + panel.width * 0.5f, - originY = panel.y.toFloat(), - translateX = 0f, - translateY = translateY, - scaleX = scale, - scaleY = scale, - rotateDeg = 0f - ) + out += + RenderCommand.PushTransform( + originX = panel.x + panel.width * 0.5f, + originY = panel.y.toFloat(), + translateX = 0f, + translateY = translateY, + scaleX = scale, + scaleY = scale, + rotateDeg = 0f, + ) out += RenderCommand.PushOpacity(progress) } @@ -268,59 +270,65 @@ class SelectEngine( val isHovered = index == current.hoveredIndex val isSelected = snapshot.optionId != null && snapshot.optionId == current.selectedId if (isHovered && snapshot.kind == SelectMeasurementCache.KIND_OPTION) { - out += RenderCommand.DrawRect( - rowRect.x, - rowRect.y, - rowRect.width, - rowRect.height, - style.optionHoverBackgroundColor - ) + out += + RenderCommand.DrawRect( + rowRect.x, + rowRect.y, + rowRect.width, + rowRect.height, + style.optionHoverBackgroundColor, + ) } else if (isSelected && snapshot.kind == SelectMeasurementCache.KIND_OPTION) { - out += RenderCommand.DrawRect( - rowRect.x, - rowRect.y, - rowRect.width, - rowRect.height, - style.optionSelectedBackgroundColor - ) + out += + RenderCommand.DrawRect( + rowRect.x, + rowRect.y, + rowRect.width, + rowRect.height, + style.optionSelectedBackgroundColor, + ) } val textY = rowRect.y + ((rowRect.height - fontHeight).coerceAtLeast(0) / 2) if (snapshot.kind == SelectMeasurementCache.KIND_OPTION) { val markerX = rowRect.x + style.rowPaddingX if (isSelected) { - out += RenderCommand.DrawText( - text = style.checkGlyph, - x = markerX, + out += + RenderCommand.DrawText( + text = style.checkGlyph, + x = markerX, + y = textY, + color = style.markerColor, + fontId = fontId, + fontSize = fontSize, + ) + } + val labelX = + markerX + + style.markerColumnWidth + + style.markerGap + + snapshot.depth * style.groupIndentX + val color = if (snapshot.enabled) style.optionTextColor else style.disabledTextColor + out += + RenderCommand.DrawText( + text = snapshot.label, + x = labelX, y = textY, - color = style.markerColor, + color = color, fontId = fontId, - fontSize = fontSize + fontSize = fontSize, ) - } - val labelX = markerX + - style.markerColumnWidth + - style.markerGap + - snapshot.depth * style.groupIndentX - val color = if (snapshot.enabled) style.optionTextColor else style.disabledTextColor - out += RenderCommand.DrawText( - text = snapshot.label, - x = labelX, - y = textY, - color = color, - fontId = fontId, - fontSize = fontSize - ) } else if (snapshot.kind == SelectMeasurementCache.KIND_GROUP) { val labelX = rowRect.x + style.rowPaddingX + snapshot.depth * style.groupIndentX - out += RenderCommand.DrawText( - text = snapshot.label, - x = labelX, - y = textY, - color = style.groupTextColor, - fontId = fontId, - fontSize = fontSize - ) + out += + RenderCommand.DrawText( + text = snapshot.label, + x = labelX, + y = textY, + color = style.groupTextColor, + fontId = fontId, + fontSize = fontSize, + ) } } } @@ -330,22 +338,24 @@ class SelectEngine( if (hasScrollbar) { val track = scrollbarTrackRect(current) if (track != null) { - out += RenderCommand.DrawRect( - track.x, - track.y, - track.width, - track.height, - style.scrollbarTrackColor - ) + out += + RenderCommand.DrawRect( + track.x, + track.y, + track.width, + track.height, + style.scrollbarTrackColor, + ) val thumb = scrollbarThumbRect(current, track) if (thumb != null) { - out += RenderCommand.DrawRect( - thumb.x, - thumb.y, - thumb.width, - thumb.height, - style.scrollbarThumbColor - ) + out += + RenderCommand.DrawRect( + thumb.x, + thumb.y, + thumb.width, + thumb.height, + style.scrollbarThumbColor, + ) } } } @@ -370,7 +380,10 @@ class SelectEngine( val hit = entryAt(current, mouseX, mouseY) current.hoveredIndex = hit if (hit >= 0) { - val snapshot = current.measurement?.snapshots?.getOrNull(hit) + val snapshot = + current.measurement + ?.snapshots + ?.getOrNull(hit) if (snapshot != null && snapshot.kind == SelectMeasurementCache.KIND_OPTION && snapshot.enabled) { current.highlightedIndex = hit } @@ -483,25 +496,28 @@ class SelectEngine( val ctx = lastMeasureContext ?: return if (!layoutDirty) return - val measurement = measurementCache.measure( - modelToken = current.modelToken, - entries = current.entries, - style = style, - ctx = ctx, - dpiScale = viewportScale, - fontId = current.fontId, - fontSize = current.fontSize - ) + val measurement = + measurementCache.measure( + modelToken = current.modelToken, + entries = current.entries, + style = style, + ctx = ctx, + dpiScale = viewportScale, + fontId = current.fontId, + fontSize = current.fontSize, + ) current.measurement = measurement val desiredHeight = measurement.totalContentHeight + style.panelPaddingY * 2 val maxPanelHeight = (viewportHeight - style.maxPanelHeightPadding * 2).coerceAtLeast(1) val panelHeight = desiredHeight.coerceIn(1, maxPanelHeight) val overflow = measurement.totalContentHeight > (panelHeight - style.panelPaddingY * 2).coerceAtLeast(1) - val desiredWidth = maxOf( - current.anchorRect.width.coerceAtLeast(1), - measurement.panelWidth + style.panelPaddingX * 2 + if (overflow) scrollbarReservedWidth() else 0, - style.minPanelWidth - ) + val desiredWidth = + maxOf( + current.anchorRect.width + .coerceAtLeast(1), + measurement.panelWidth + style.panelPaddingX * 2 + if (overflow) scrollbarReservedWidth() else 0, + style.minPanelWidth, + ) val maxPanelWidth = (viewportWidth - style.maxPanelWidthPadding * 2).coerceAtLeast(1) val panelWidth = desiredWidth.coerceIn(1, maxPanelWidth) @@ -511,14 +527,15 @@ class SelectEngine( val aboveSpace = current.anchorRect.y - style.viewportPadding val preferredY = if (belowSpace < panelHeight && aboveSpace > belowSpace) aboveY else belowY - val placement = PopupPlacement.resolve( - PopupPlacementRequest( - preferredRect = Rect(current.anchorRect.x, preferredY, panelWidth, panelHeight), - popupSize = Size(panelWidth, panelHeight), - viewport = Rect(0, 0, viewportWidth.coerceAtLeast(1), viewportHeight.coerceAtLeast(1)), - padding = style.viewportPadding + val placement = + PopupPlacement.resolve( + PopupPlacementRequest( + preferredRect = Rect(current.anchorRect.x, preferredY, panelWidth, panelHeight), + popupSize = Size(panelWidth, panelHeight), + viewport = Rect(0, 0, viewportWidth.coerceAtLeast(1), viewportHeight.coerceAtLeast(1)), + padding = style.viewportPadding, + ), ) - ) current.panelRect = placement.rect val maxScroll = maxScroll(current) current.scrollOffset = current.scrollOffset.coerceIn(0, maxScroll) @@ -544,13 +561,14 @@ class SelectEngine( private fun moveSelection(current: PopupState, direction: Int) { val measurement = current.measurement ?: return if (measurement.snapshots.isEmpty()) return - val start = if (current.highlightedIndex >= 0) { - current.highlightedIndex - } else if (direction >= 0) { - -1 - } else { - measurement.snapshots.size - } + val start = + if (current.highlightedIndex >= 0) { + current.highlightedIndex + } else if (direction >= 0) { + -1 + } else { + measurement.snapshots.size + } var index = start repeat(measurement.snapshots.size) { index += direction @@ -642,7 +660,8 @@ class SelectEngine( val measurement = current.measurement ?: return null if (entryIndex !in measurement.entryOffsets.indices) return null val rowX = current.panelRect.x + style.panelPaddingX - val rowY = current.panelRect.y + style.panelPaddingY + measurement.entryOffsets[entryIndex] - current.scrollOffset + val rowY = + current.panelRect.y + style.panelPaddingY + measurement.entryOffsets[entryIndex] - current.scrollOffset val rowW = contentWidth(current) val rowH = measurement.entryHeights[entryIndex] return Rect(rowX, rowY, rowW, rowH) @@ -669,18 +688,15 @@ class SelectEngine( current.scrollOffset = current.scrollOffset.coerceIn(0, maxScroll(current)) } - private fun contentHeight(current: PopupState): Int { - return (current.panelRect.height - style.panelPaddingY * 2).coerceAtLeast(1) - } + private fun contentHeight(current: PopupState): Int = + (current.panelRect.height - style.panelPaddingY * 2).coerceAtLeast(1) private fun contentWidth(current: PopupState): Int { val reserved = if (maxScroll(current) > 0) scrollbarReservedWidth() else 0 return (current.panelRect.width - style.panelPaddingX * 2 - reserved).coerceAtLeast(1) } - private fun scrollbarReservedWidth(): Int { - return (style.scrollbarGap + style.scrollbarWidth).coerceAtLeast(0) - } + private fun scrollbarReservedWidth(): Int = (style.scrollbarGap + style.scrollbarWidth).coerceAtLeast(0) private fun scrollbarTrackRect(current: PopupState): Rect? { if (maxScroll(current) <= 0) return null @@ -700,15 +716,17 @@ class SelectEngine( val trackHeight = trackRect.height.coerceAtLeast(1) val visibleHeight = contentHeight(current).coerceAtLeast(1) val rawThumb = ((visibleHeight.toLong() * trackHeight.toLong()) / totalContentHeight.toLong()).toInt() - val thumbHeight = rawThumb - .coerceAtLeast(style.scrollbarMinThumbHeight.coerceAtLeast(1)) - .coerceAtMost(trackHeight) + val thumbHeight = + rawThumb + .coerceAtLeast(style.scrollbarMinThumbHeight.coerceAtLeast(1)) + .coerceAtMost(trackHeight) val travel = (trackHeight - thumbHeight).coerceAtLeast(0) - val thumbOffset = if (travel == 0) { - 0 - } else { - ((current.scrollOffset.toLong() * travel.toLong()) / maxScroll.toLong()).toInt().coerceIn(0, travel) - } + val thumbOffset = + if (travel == 0) { + 0 + } else { + ((current.scrollOffset.toLong() * travel.toLong()) / maxScroll.toLong()).toInt().coerceIn(0, travel) + } return Rect(trackRect.x, trackRect.y + thumbOffset, trackRect.width, thumbHeight) } @@ -720,11 +738,12 @@ class SelectEngine( if (thumbRect == null || maxScroll <= 0) return false current.scrollbarDragging = true current.hoveredIndex = -1 - current.scrollbarDragThumbOffsetY = if (thumbRect.contains(mouseX, mouseY)) { - (mouseY - thumbRect.y).coerceIn(0, thumbRect.height.coerceAtLeast(1) - 1) - } else { - (thumbRect.height / 2).coerceAtLeast(0) - } + current.scrollbarDragThumbOffsetY = + if (thumbRect.contains(mouseX, mouseY)) { + (mouseY - thumbRect.y).coerceIn(0, thumbRect.height.coerceAtLeast(1) - 1) + } else { + (thumbRect.height / 2).coerceAtLeast(0) + } updateScrollbarDrag(current, mouseY) return true } @@ -756,11 +775,12 @@ class SelectEngine( val duration = animationDurationMs.coerceAtLeast(1L) val elapsed = (clock.nowMs() - animationStartedAtMs).coerceAtLeast(0L) val t = (elapsed.toFloat() / duration.toFloat()).coerceIn(0f, 1f) - val eased = when (visibilityState) { - VisibilityState.Opening -> Easings.EASE_OUT.map(t) - VisibilityState.Closing -> Easings.EASE_IN.map(t) - else -> t - } + val eased = + when (visibilityState) { + VisibilityState.Opening -> Easings.EASE_OUT.map(t) + VisibilityState.Closing -> Easings.EASE_IN.map(t) + else -> t + } animationProgress = animationFrom + (animationTo - animationFrom) * eased if (t >= 1f) { if (animationTo >= 0.999f) { @@ -786,11 +806,12 @@ class SelectEngine( animationTo = target.coerceIn(0f, 1f) animationDurationMs = durationMs.coerceAtLeast(1L) animationStartedAtMs = clock.nowMs() - visibilityState = when { - animationTo >= 0.999f -> if (from >= 0.999f) VisibilityState.Open else VisibilityState.Opening - animationTo <= 0.001f -> if (from <= 0.001f) VisibilityState.Hidden else VisibilityState.Closing - else -> VisibilityState.Opening - } + visibilityState = + when { + animationTo >= 0.999f -> if (from >= 0.999f) VisibilityState.Open else VisibilityState.Opening + animationTo <= 0.001f -> if (from <= 0.001f) VisibilityState.Hidden else VisibilityState.Closing + else -> VisibilityState.Opening + } if (visibilityState == VisibilityState.Hidden) { val onClose = current.onClose popup = null diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectHost.kt index 1678b93..6f363ec 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectHost.kt @@ -5,9 +5,13 @@ import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope interface SelectHost { fun open(request: SelectOpenRequest) + fun close(owner: Any) + fun closeAll() + fun isOpenFor(owner: Any): Boolean + fun isOpen(): Boolean } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectMeasurementCache.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectMeasurementCache.kt index 721df76..49fb31d 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectMeasurementCache.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectMeasurementCache.kt @@ -3,7 +3,7 @@ package org.dreamfinity.dsgl.core.select import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext class SelectMeasurementCache( - private val maxEntries: Int = 192 + private val maxEntries: Int = 192, ) { data class EntrySnapshot( val kind: Int, @@ -11,7 +11,7 @@ class SelectMeasurementCache( val optionId: String?, val label: String, val enabled: Boolean, - val depth: Int + val depth: Int, ) data class Measurement( @@ -21,7 +21,7 @@ class SelectMeasurementCache( val entryHeights: IntArray, val entryOffsets: IntArray, val totalContentHeight: Int, - val panelWidth: Int + val panelWidth: Int, ) data class Key( @@ -29,14 +29,13 @@ class SelectMeasurementCache( val styleHash: Int, val fontHash: Int, val dpiKey: Int, - val entriesHash: Int + val entriesHash: Int, ) private val cache: MutableMap = object : LinkedHashMap(64, 0.75f, true) { - override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean { - return size > maxEntries - } + override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean = + size > maxEntries } var computeCount: Long = 0L @@ -49,19 +48,20 @@ class SelectMeasurementCache( ctx: UiMeasureContext, dpiScale: Float, fontId: String?, - fontSize: Int? + fontSize: Int?, ): Measurement { val snapshots = flattenEntries(entries) val fingerprint = fingerprint(snapshots) val resolvedFontId = fontId ?: style.fontId val resolvedFontSize = fontSize ?: style.fontSize - val key = Key( - modelToken = modelToken, - styleHash = style.hashCode(), - fontHash = 31 * (resolvedFontId?.hashCode() ?: 0) + (resolvedFontSize ?: 0), - dpiKey = (dpiScale * 1000f).toInt(), - entriesHash = fingerprint - ) + val key = + Key( + modelToken = modelToken, + styleHash = style.hashCode(), + fontHash = 31 * (resolvedFontId?.hashCode() ?: 0) + (resolvedFontSize ?: 0), + dpiKey = (dpiScale * 1000f).toInt(), + entriesHash = fingerprint, + ) synchronized(cache) { cache[key]?.let { return it } } @@ -80,26 +80,36 @@ class SelectMeasurementCache( entryHeights[index] = height offset += height + style.rowGap - val labelWidth = if (snapshot.label.isEmpty()) 0 else ctx.measureText(snapshot.label, resolvedFontId, resolvedFontSize) - val width = when (snapshot.kind) { - KIND_OPTION -> { - style.rowPaddingX + - style.markerColumnWidth + - style.markerGap + - snapshot.depth * style.groupIndentX + - labelWidth + - style.rowPaddingX + val labelWidth = + if (snapshot.label.isEmpty()) { + 0 + } else { + ctx.measureText( + snapshot.label, + resolvedFontId, + resolvedFontSize, + ) } - - KIND_GROUP -> { - style.rowPaddingX + - snapshot.depth * style.groupIndentX + - labelWidth + - style.rowPaddingX + val width = + when (snapshot.kind) { + KIND_OPTION -> { + style.rowPaddingX + + style.markerColumnWidth + + style.markerGap + + snapshot.depth * style.groupIndentX + + labelWidth + + style.rowPaddingX + } + + KIND_GROUP -> { + style.rowPaddingX + + snapshot.depth * style.groupIndentX + + labelWidth + + style.rowPaddingX + } + + else -> style.minPanelWidth } - - else -> style.minPanelWidth - } if (width > maxWidth) { maxWidth = width } @@ -109,15 +119,16 @@ class SelectMeasurementCache( offset -= style.rowGap } - val measurement = Measurement( - snapshots = snapshots, - rowHeight = rowHeight, - separatorHeight = separatorHeight, - entryHeights = entryHeights, - entryOffsets = entryOffsets, - totalContentHeight = offset, - panelWidth = maxWidth.coerceAtLeast(style.minPanelWidth) - ) + val measurement = + Measurement( + snapshots = snapshots, + rowHeight = rowHeight, + separatorHeight = separatorHeight, + entryHeights = entryHeights, + entryOffsets = entryOffsets, + totalContentHeight = offset, + panelWidth = maxWidth.coerceAtLeast(style.minPanelWidth), + ) synchronized(cache) { cache[key] = measurement } @@ -135,36 +146,39 @@ class SelectMeasurementCache( entries.forEach { entry -> when (entry) { is SelectEntry.Option -> { - out += EntrySnapshot( - kind = KIND_OPTION, - id = entry.id, - optionId = entry.id, - label = entry.labelProvider.invoke(), - enabled = entry.enabledProvider.invoke(), - depth = depth - ) + out += + EntrySnapshot( + kind = KIND_OPTION, + id = entry.id, + optionId = entry.id, + label = entry.labelProvider.invoke(), + enabled = entry.enabledProvider.invoke(), + depth = depth, + ) } is SelectEntry.Separator -> { - out += EntrySnapshot( - kind = KIND_SEPARATOR, - id = entry.id, - optionId = null, - label = "", - enabled = false, - depth = depth - ) + out += + EntrySnapshot( + kind = KIND_SEPARATOR, + id = entry.id, + optionId = null, + label = "", + enabled = false, + depth = depth, + ) } is SelectEntry.Group -> { - out += EntrySnapshot( - kind = KIND_GROUP, - id = entry.id, - optionId = null, - label = entry.labelProvider.invoke(), - enabled = false, - depth = depth - ) + out += + EntrySnapshot( + kind = KIND_GROUP, + id = entry.id, + optionId = null, + label = entry.labelProvider.invoke(), + enabled = false, + depth = depth, + ) appendEntries(entry.entries, depth = depth + 1, out = out) } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectModel.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectModel.kt index eb3d983..c2f814f 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectModel.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectModel.kt @@ -4,6 +4,7 @@ import java.util.concurrent.atomic.AtomicLong private object SelectModelIds { private val nextToken = AtomicLong(1L) + fun next(): Long = nextToken.getAndIncrement() } @@ -11,25 +12,25 @@ data class SelectModel( val id: String? = null, val placeholderProvider: (() -> String?)? = null, val entries: List, - internal val token: Long = SelectModelIds.next() + internal val token: Long = SelectModelIds.next(), ) sealed class SelectEntry( - open val id: String? + open val id: String?, ) { data class Option( override val id: String, val labelProvider: () -> String, - val enabledProvider: () -> Boolean = { true } + val enabledProvider: () -> Boolean = { true }, ) : SelectEntry(id) data class Separator( - override val id: String? = null + override val id: String? = null, ) : SelectEntry(id) data class Group( override val id: String? = null, val labelProvider: () -> String, - val entries: List + val entries: List, ) : SelectEntry(id) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntime.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntime.kt index 0fce91d..cbe2b45 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntime.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntime.kt @@ -8,12 +8,11 @@ object SelectRuntime { val engine: SelectEngine = applicationEngine val host: SelectHost = RoutedSelectHost() - fun engineFor(ownerScope: OverlayOwnerScope): SelectEngine { - return when (ownerScope) { + fun engineFor(ownerScope: OverlayOwnerScope): SelectEngine = + when (ownerScope) { OverlayOwnerScope.Application -> applicationEngine OverlayOwnerScope.System -> systemEngine } - } private class RoutedSelectHost : SelectHost { override fun open(request: SelectOpenRequest) { @@ -33,12 +32,9 @@ object SelectRuntime { systemEngine.closeAll() } - override fun isOpenFor(owner: Any): Boolean { - return applicationEngine.isOpenFor(owner) || systemEngine.isOpenFor(owner) - } + override fun isOpenFor(owner: Any): Boolean = + applicationEngine.isOpenFor(owner) || systemEngine.isOpenFor(owner) - override fun isOpen(): Boolean { - return applicationEngine.isOpen() || systemEngine.isOpen() - } + override fun isOpen(): Boolean = applicationEngine.isOpen() || systemEngine.isOpen() } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectStyle.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectStyle.kt index 1bcec1e..693e0f4 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectStyle.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectStyle.kt @@ -41,5 +41,5 @@ data class SelectStyle( val openDurationMs: Long = 130L, val closeDurationMs: Long = 100L, val openStartScale: Float = 0.985f, - val openStartOffsetYPx: Float = -4f + val openStartOffsetYPx: Float = -4f, ) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/CssLength.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/CssLength.kt index c72b3a2..76f8651 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/CssLength.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/CssLength.kt @@ -3,22 +3,22 @@ package org.dreamfinity.dsgl.core.style import org.dreamfinity.dsgl.core.dom.layout.Insets import kotlin.math.roundToInt -enum class CssUnit(val token: String) { +enum class CssUnit( + val token: String, +) { Px("px"), Rem("rem"), Em("em"), Vw("vw"), Vh("vh"), - Percent("%") + Percent("%"), } data class CssLength( val value: Float, - val unit: CssUnit + val unit: CssUnit, ) { - override fun toString(): String { - return toCssLiteral() - } + override fun toString(): String = toCssLiteral() companion object { val ZERO_PX: CssLength = CssLength(0f, CssUnit.Px) @@ -27,53 +27,39 @@ data class CssLength( } } -fun CssLength.coerceAtLeast(minimumValue: Int): String { - return CssLength(value.coerceAtLeast(minimumValue.toFloat()), unit).toCssLiteral() -} +fun CssLength.coerceAtLeast(minimumValue: Int): String = + CssLength(value.coerceAtLeast(minimumValue.toFloat()), unit).toCssLiteral() -fun Number.px(): CssLength { - return CssLength(value = this.toFloat(), unit = CssUnit.Px) -} +fun Number.px(): CssLength = CssLength(value = this.toFloat(), unit = CssUnit.Px) -fun Number.em(): CssLength { - return CssLength(value = this.toFloat(), unit = CssUnit.Em) -} +fun Number.em(): CssLength = CssLength(value = this.toFloat(), unit = CssUnit.Em) -fun Number.rem(): CssLength { - return CssLength(value = this.toFloat(), unit = CssUnit.Rem) -} +fun Number.rem(): CssLength = CssLength(value = this.toFloat(), unit = CssUnit.Rem) -fun Number.vw(): CssLength { - return CssLength(value = this.toFloat(), unit = CssUnit.Vw) -} +fun Number.vw(): CssLength = CssLength(value = this.toFloat(), unit = CssUnit.Vw) -fun Number.vh(): CssLength { - return CssLength(value = this.toFloat(), unit = CssUnit.Vh) -} +fun Number.vh(): CssLength = CssLength(value = this.toFloat(), unit = CssUnit.Vh) -fun Number.percent(): CssLength { - return CssLength(value = this.toFloat(), unit = CssUnit.Percent) -} +fun Number.percent(): CssLength = CssLength(value = this.toFloat(), unit = CssUnit.Percent) data class LengthInsets( val top: CssLength, val right: CssLength, val bottom: CssLength, - val left: CssLength + val left: CssLength, ) { companion object { val ZERO: LengthInsets = all(CssLength.ZERO_PX) fun all(value: CssLength): LengthInsets = LengthInsets(value, value, value, value) - fun fromInsets(insets: Insets): LengthInsets { - return LengthInsets( + fun fromInsets(insets: Insets): LengthInsets = + LengthInsets( top = CssLength.px(insets.top), right = CssLength.px(insets.right), bottom = CssLength.px(insets.bottom), - left = CssLength.px(insets.left) + left = CssLength.px(insets.left), ) - } } } @@ -81,7 +67,7 @@ enum class LengthPercentBase { ContainerWidth, ContainerHeight, CurrentFontSize, - InheritedFontSize + InheritedFontSize, } data class LengthResolveContext( @@ -91,7 +77,7 @@ data class LengthResolveContext( val containingBlockHeightPx: Float? = null, val rootFontSizePx: Float = 16f, val currentFontSizePx: Float = 16f, - val inheritedFontSizePx: Float = 16f + val inheritedFontSizePx: Float = 16f, ) fun interface StyleWarningReporter { @@ -100,48 +86,58 @@ fun interface StyleWarningReporter { private val knownLengthUnits: Set = linkedSetOf("px", "rem", "em", "vw", "vh", "%") private val knownLengthUnitsPattern = knownLengthUnits.joinToString("|") { Regex.escape(it) } -private val cssLengthRegex = Regex( - pattern = """^(-?(?:\d+(?:\.\d+)?|\.\d+))(?:($knownLengthUnitsPattern))?$""", - option = RegexOption.IGNORE_CASE -) -private val cssLengthAnyUnitRegex = Regex( - pattern = """^(-?(?:\d+(?:\.\d+)?|\.\d+))(?:([a-zA-Z%]+))?$""", - option = RegexOption.IGNORE_CASE -) +private val cssLengthRegex = + Regex( + pattern = """^(-?(?:\d+(?:\.\d+)?|\.\d+))(?:($knownLengthUnitsPattern))?$""", + option = RegexOption.IGNORE_CASE, + ) +private val cssLengthAnyUnitRegex = + Regex( + pattern = """^(-?(?:\d+(?:\.\d+)?|\.\d+))(?:([a-zA-Z%]+))?$""", + option = RegexOption.IGNORE_CASE, + ) -fun parseCssLength( - raw: String, - allowUnitlessZero: Boolean = true -): CssLength { +fun parseCssLength(raw: String, allowUnitlessZero: Boolean = true): CssLength { val trimmed = raw.trim() - val match = cssLengthRegex.matchEntire(trimmed) - ?: run { - val anyUnitMatch = cssLengthAnyUnitRegex.matchEntire(trimmed) - if (anyUnitMatch != null) { - val unknownUnit = anyUnitMatch.groupValues.getOrNull(2).orEmpty().trim() - if (unknownUnit.isNotEmpty()) { - error("Unknown length unit '$unknownUnit'. Supported units: px, rem, em, vw, vh, %.") + val match = + cssLengthRegex.matchEntire(trimmed) + ?: run { + val anyUnitMatch = cssLengthAnyUnitRegex.matchEntire(trimmed) + if (anyUnitMatch != null) { + val unknownUnit = + anyUnitMatch.groupValues + .getOrNull(2) + .orEmpty() + .trim() + if (unknownUnit.isNotEmpty()) { + error("Unknown length unit '$unknownUnit'. Supported units: px, rem, em, vw, vh, %.") + } } + error("Expected CSS length but got '$raw'.") } - error("Expected CSS length but got '$raw'.") - } val value = match.groupValues[1].toFloat() - val unitToken = match.groupValues.getOrNull(2).orEmpty().trim().lowercase() + val unitToken = + match.groupValues + .getOrNull(2) + .orEmpty() + .trim() + .lowercase() if (unitToken.isEmpty()) { if (allowUnitlessZero && value == 0f) { return CssLength.ZERO_PX } error("Expected explicit unit in '$raw'.") } - val unit = when (unitToken) { - "px" -> CssUnit.Px - "rem" -> CssUnit.Rem - "em" -> CssUnit.Em - "vw" -> CssUnit.Vw - "vh" -> CssUnit.Vh - "%" -> CssUnit.Percent - else -> error("Unknown length unit '$unitToken'. Supported units: px, rem, em, vw, vh, %.") - } + val unit = + when (unitToken) { + "px" -> CssUnit.Px + "rem" -> CssUnit.Rem + "em" -> CssUnit.Em + "vw" -> CssUnit.Vw + "vh" -> CssUnit.Vh + "%" -> CssUnit.Percent + else -> error("Unknown length unit '$unitToken'. Supported units: px, rem, em, vw, vh, %.") + } return CssLength(value = value, unit = unit) } @@ -156,19 +152,17 @@ fun CssLength.toCssLiteral(): String { return "0px" } val asLong = value.toLong() - val number = if (asLong.toFloat() == value) { - asLong.toString() - } else { - value.toString() - } + val number = + if (asLong.toFloat() == value) { + asLong.toString() + } else { + value.toString() + } return number + unit.token } -fun CssLength.resolvePx( - context: LengthResolveContext, - percentBase: LengthPercentBase -): Float { - return when (unit) { +fun CssLength.resolvePx(context: LengthResolveContext, percentBase: LengthPercentBase): Float = + when (unit) { CssUnit.Px -> value CssUnit.Rem -> value * context.rootFontSizePx CssUnit.Em -> value * context.currentFontSizePx @@ -176,28 +170,27 @@ fun CssLength.resolvePx( CssUnit.Vh -> (value / 100f) * context.viewportHeightPx CssUnit.Percent -> (value / 100f) * percentBaseValue(context, percentBase) } -} -fun LengthInsets.resolveToInsets(context: LengthResolveContext): Insets { - return Insets( +fun LengthInsets.resolveToInsets(context: LengthResolveContext): Insets = + Insets( top = top.resolvePx(context, LengthPercentBase.ContainerHeight).roundToInt(), right = right.resolvePx(context, LengthPercentBase.ContainerWidth).roundToInt(), bottom = bottom.resolvePx(context, LengthPercentBase.ContainerHeight).roundToInt(), - left = left.resolvePx(context, LengthPercentBase.ContainerWidth).roundToInt() + left = left.resolvePx(context, LengthPercentBase.ContainerWidth).roundToInt(), ) -} fun parseLengthPx( raw: String, allowNegative: Boolean, percentBase: LengthPercentBase = LengthPercentBase.ContainerWidth, context: LengthResolveContext = LengthResolveContext(), - allowUnitlessZero: Boolean = true + allowUnitlessZero: Boolean = true, ): Float { - val px = parseCssLength( - raw = raw, - allowUnitlessZero = allowUnitlessZero - ).resolvePx(context, percentBase) + val px = + parseCssLength( + raw = raw, + allowUnitlessZero = allowUnitlessZero, + ).resolvePx(context, percentBase) if (!allowNegative && px < 0f) { error("Negative length is not allowed: '$raw'.") } @@ -209,23 +202,22 @@ fun parseLengthPxInt( allowNegative: Boolean, percentBase: LengthPercentBase = LengthPercentBase.ContainerWidth, context: LengthResolveContext = LengthResolveContext(), - allowUnitlessZero: Boolean = true -): Int { - return parseLengthPx( + allowUnitlessZero: Boolean = true, +): Int = + parseLengthPx( raw = raw, allowNegative = allowNegative, percentBase = percentBase, context = context, - allowUnitlessZero = allowUnitlessZero + allowUnitlessZero = allowUnitlessZero, ).roundToInt() -} fun parseOptionalLengthPxInt( raw: String, allowNegative: Boolean, percentBase: LengthPercentBase = LengthPercentBase.ContainerWidth, context: LengthResolveContext = LengthResolveContext(), - allowUnitlessZero: Boolean = true + allowUnitlessZero: Boolean = true, ): Int? { val normalized = raw.trim().lowercase() if (normalized == "auto") return null @@ -234,15 +226,14 @@ fun parseOptionalLengthPxInt( allowNegative = allowNegative, percentBase = percentBase, context = context, - allowUnitlessZero = allowUnitlessZero + allowUnitlessZero = allowUnitlessZero, ) } -private fun percentBaseValue(context: LengthResolveContext, base: LengthPercentBase): Float { - return when (base) { +private fun percentBaseValue(context: LengthResolveContext, base: LengthPercentBase): Float = + when (base) { LengthPercentBase.ContainerWidth -> context.containingBlockWidthPx ?: 0f LengthPercentBase.ContainerHeight -> context.containingBlockHeightPx ?: 0f LengthPercentBase.CurrentFontSize -> context.currentFontSizePx LengthPercentBase.InheritedFontSize -> context.inheritedFontSizePx } -} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/DssParser.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/DssParser.kt index d615c02..4fca186 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/DssParser.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/DssParser.kt @@ -6,7 +6,7 @@ class DssParseException( val path: String, val line: Int, val column: Int, - message: String + message: String, ) : RuntimeException("$path:$line:$column $message") object DssParser { @@ -46,37 +46,41 @@ object DssParser { index++ // '{' val declarations = StyleDeclarations() - index = parseDeclarations( - sourceName = sourceName, - text = text, - fromIndex = index, - declarations = declarations, - rootVars = rootVars, - allowVariables = selectorText == ":root", - warnings = warnings - ) - - val selector = when (selectorText) { - ":root" -> { - if (declarations.values.isEmpty()) { - null - } else { - StyleSelector.parse(ROOT_SELECTOR_ALIAS) + index = + parseDeclarations( + sourceName = sourceName, + text = text, + fromIndex = index, + declarations = declarations, + rootVars = rootVars, + allowVariables = selectorText == ":root", + warnings = warnings, + ) + + val selector = + when (selectorText) { + ":root" -> { + if (declarations.values.isEmpty()) { + null + } else { + StyleSelector.parse(ROOT_SELECTOR_ALIAS) + } } + else -> + try { + StyleSelector.parse(selectorText) + } catch (ex: IllegalArgumentException) { + throw parseError(sourceName, text, selectorStart, ex.message ?: "Invalid selector.") + } } - else -> try { - StyleSelector.parse(selectorText) - } catch (ex: IllegalArgumentException) { - throw parseError(sourceName, text, selectorStart, ex.message ?: "Invalid selector.") - } - } if (selector != null) { - rules += StyleRule( - selector = selector, - declarations = declarations, - sourceOrder = sourceOrder++, - fileName = sourceName - ) + rules += + StyleRule( + selector = selector, + declarations = declarations, + sourceOrder = sourceOrder++, + fileName = sourceName, + ) } } @@ -84,7 +88,7 @@ object DssParser { rules = rules, rootVariables = rootVars, source = sourceName, - warnings = warnings.messages() + warnings = warnings.messages(), ) } @@ -95,7 +99,7 @@ object DssParser { declarations: StyleDeclarations, rootVars: MutableMap, allowVariables: Boolean, - warnings: ParseWarnings + warnings: ParseWarnings, ): Int { var index = fromIndex while (index < text.length) { @@ -138,7 +142,7 @@ object DssParser { sourceName, text, nameStart, - "Variable declarations are only supported inside :root." + "Variable declarations are only supported inside :root.", ) } rootVars[rawName] = rawValue @@ -147,16 +151,17 @@ object DssParser { if (normalizedName == "foreground-color" || normalizedName == "foregroundcolor") { warnings.warnOnce( DEPRECATED_FOREGROUND_COLOR_WARNING_KEY, - "Property 'foreground-color' is deprecated; use 'color'." + "Property 'foreground-color' is deprecated; use 'color'.", ) } - val property = StyleProperty.fromKeyOrNull(rawName) - ?: throw parseError( - sourceName, - text, - nameStart, - "Unsupported style property '$rawName'." - ) + val property = + StyleProperty.fromKeyOrNull(rawName) + ?: throw parseError( + sourceName, + text, + nameStart, + "Unsupported style property '$rawName'.", + ) val important = importantSuffixRegex.containsMatchIn(rawValue) val normalizedValue = if (important) importantSuffixRegex.replace(rawValue, "") else rawValue if (normalizedValue.isEmpty()) { @@ -168,7 +173,7 @@ object DssParser { validateLiteralForProperty( property = property, literal = expression.value, - warningReporter = warnings + warningReporter = warnings, ) } catch (ex: Exception) { throw parseError(sourceName, text, valueStart, ex.message ?: "Invalid value.") @@ -194,7 +199,12 @@ object DssParser { return index } - private fun parseError(path: String, source: String, index: Int, message: String): DssParseException { + private fun parseError( + path: String, + source: String, + index: Int, + message: String, + ): DssParseException { val safeIndex = index.coerceIn(0, source.length) var line = 1 var col = 1 diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleApplicationScope.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleApplicationScope.kt index e16a942..be9a721 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleApplicationScope.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleApplicationScope.kt @@ -2,6 +2,5 @@ package org.dreamfinity.dsgl.core.style enum class StyleApplicationScope { Application, - SystemOverlay + SystemOverlay, } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt index 73ae9be..5bc4cfe 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt @@ -8,20 +8,22 @@ import java.util.WeakHashMap import kotlin.math.roundToInt object StyleEngine { - private data class AnonymousInspectorTarget(val path: String) + private data class AnonymousInspectorTarget( + val path: String, + ) data class StyleApplyReport( val layoutDirty: Boolean, val visualDirty: Boolean, val visitedNodes: Int, val cacheHits: Int, - val recomputedNodes: Int + val recomputedNodes: Int, ) private data class MutableApplyMetrics( var visitedNodes: Int = 0, var cacheHits: Int = 0, - var recomputedNodes: Int = 0 + var recomputedNodes: Int = 0, ) private data class CacheKey( @@ -44,18 +46,20 @@ object StyleEngine { val rootFontSizePx: Int, val viewportWidthPx: Int, val viewportHeightPx: Int, - val scope: StyleApplicationScope + val scope: StyleApplicationScope, ) private data class CachedStyle( val key: CacheKey, - val style: ComputedStyle + val style: ComputedStyle, ) - private enum class StyleOrigin(val precedence: Int) { + private enum class StyleOrigin( + val precedence: Int, + ) { Stylesheet(0), Inline(1), - Inspector(2) + Inspector(2), } private data class CascadeWinner( @@ -65,22 +69,22 @@ object StyleEngine { val sourceOrder: Int, val origin: StyleOrigin, val sourceKind: StyleSourceKind, - val sourceLabel: String + val sourceLabel: String, ) private data class NodeApplyFlags( val layoutDirty: Boolean, - val visualDirty: Boolean + val visualDirty: Boolean, ) private data class NodeApplyResult( val flags: NodeApplyFlags, - val cacheHit: Boolean + val cacheHit: Boolean, ) private enum class StylePassMode { Full, - Targeted + Targeted, } private val cache: MutableMap = WeakHashMap() @@ -106,13 +110,14 @@ object StyleEngine { private var lastAppliedSelectorStateVersion: Long = Long.MIN_VALUE private var viewportWidthPx: Int = 0 private var viewportHeightPx: Int = 0 - private var lastApplyReport: StyleApplyReport = StyleApplyReport( - layoutDirty = false, - visualDirty = false, - visitedNodes = 0, - cacheHits = 0, - recomputedNodes = 0 - ) + private var lastApplyReport: StyleApplyReport = + StyleApplyReport( + layoutDirty = false, + visualDirty = false, + visitedNodes = 0, + cacheHits = 0, + recomputedNodes = 0, + ) private const val DEFAULT_ROOT_FONT_SIZE_PX = 16 fun inspectorOverrideTarget(node: DOMNode): Any { @@ -176,16 +181,14 @@ object StyleEngine { setInspectorOverride(inspectorOverrideTarget(node), property, expression) } - fun setInspectorOverrideLiteral(nodeKey: Any, property: StyleProperty, literal: String): Result { - return runCatching { + fun setInspectorOverrideLiteral(nodeKey: Any, property: StyleProperty, literal: String): Result = + runCatching { validateLiteralForProperty(property, literal) setInspectorOverride(nodeKey, property, StyleExpression.Literal(literal)) } - } - fun setInspectorOverrideLiteral(node: DOMNode, property: StyleProperty, literal: String): Result { - return setInspectorOverrideLiteral(inspectorOverrideTarget(node), property, literal) - } + fun setInspectorOverrideLiteral(node: DOMNode, property: StyleProperty, literal: String): Result = + setInspectorOverrideLiteral(inspectorOverrideTarget(node), property, literal) fun clearInspectorOverride(nodeKey: Any, property: StyleProperty? = null) { val existing = inspectorOverrides[nodeKey] ?: return @@ -218,30 +221,25 @@ object StyleEngine { return inspectorOverrides[nodeKey]?.let(::copyStyleDeclarations) } - fun inspectorOverridesFor(node: DOMNode): StyleDeclarations? { - return inspectorOverridesFor(inspectorOverrideTarget(node)) - } + fun inspectorOverridesFor(node: DOMNode): StyleDeclarations? = inspectorOverridesFor(inspectorOverrideTarget(node)) fun inspectorOverrideFor(nodeKey: Any?, property: StyleProperty): StyleExpression? { if (nodeKey == null) return null return inspectorOverrides[nodeKey]?.get(property) } - fun inspectorOverrideFor(node: DOMNode, property: StyleProperty): StyleExpression? { - return inspectorOverrideFor(inspectorOverrideTarget(node), property) - } + fun inspectorOverrideFor(node: DOMNode, property: StyleProperty): StyleExpression? = + inspectorOverrideFor(inspectorOverrideTarget(node), property) - fun resolveInspectorExpression(expression: StyleExpression): Result { - return runCatching { + fun resolveInspectorExpression(expression: StyleExpression): Result = + runCatching { val snapshot = StylesheetManager.snapshot() val variables = resolvedVariables(snapshot, StyleApplicationScope.Application) resolveExpressionToLiteral(expression, variables) } - } - fun resolveInspectorVariable(name: String): Result { - return resolveInspectorExpression(StyleExpression.VariableRef(name)) - } + fun resolveInspectorVariable(name: String): Result = + resolveInspectorExpression(StyleExpression.VariableRef(name)) fun inspect(node: DOMNode): StyleInspection { val snapshot = StylesheetManager.snapshot() @@ -251,94 +249,98 @@ object StyleEngine { val matchedRules = candidates.map { "${selectorLabel(it.selector)} @ ${it.fileName}" } val inspector = inspectorOverrides[inspectorOverrideTarget(node)] val parentComputed = node.parent?.appliedComputedStyleSnapshot() - val winners = resolveCascadeWinners( - node = node, - candidates = candidates, - inline = node.inlineStyleDeclarations, - inspector = inspector - ) + val winners = + resolveCascadeWinners( + node = node, + candidates = candidates, + inline = node.inlineStyleDeclarations, + inspector = inspector, + ) val rootFontSizePx = rootFontSizeFor(node) val sources = linkedMapOf() StyleProperty.entries.forEach { property -> - sources[property] = StylePropertySource( - property = property, - kind = StyleSourceKind.Default, - source = "default" - ) + sources[property] = + StylePropertySource( + property = property, + kind = StyleSourceKind.Default, + source = "default", + ) } var result = defaults.toComputedStyle() for (property in StyleProperty.entries) { val winner = winners[property] if (winner != null) { - val applied = runCatching { - applyProperty( - current = result, - parentComputed = parentComputed, - property = property, - expression = winner.expression, - variables = variables, - rootFontSizePx = rootFontSizePx - ) - }.onFailure { error -> - println("[DSGL-Style] Failed to apply '${property.key}': ${error.message}") - }.getOrNull() + val applied = + runCatching { + applyProperty( + current = result, + parentComputed = parentComputed, + property = property, + expression = winner.expression, + variables = variables, + rootFontSizePx = rootFontSizePx, + ) + }.onFailure { error -> + println("[DSGL-Style] Failed to apply '${property.key}': ${error.message}") + }.getOrNull() if (applied != null) { result = applied } - sources[property] = StylePropertySource( - property = property, - kind = winner.sourceKind, - source = winner.sourceLabel - ) + sources[property] = + StylePropertySource( + property = property, + kind = winner.sourceKind, + source = winner.sourceLabel, + ) } else if (StylePropertyRegistry.isInherited(property) && parentComputed != null) { result = inheritProperty(result, parentComputed, property) - sources[property] = StylePropertySource( - property = property, - kind = StyleSourceKind.Inherited, - source = "inherited" - ) + sources[property] = + StylePropertySource( + property = property, + kind = StyleSourceKind.Inherited, + source = "inherited", + ) } } return StyleInspection( computed = result, propertySources = sources.toMap(), - matchedRules = matchedRules + matchedRules = matchedRules, ) } fun applyStylesRecursively( root: DOMNode, - scope: StyleApplicationScope = StyleApplicationScope.Application - ): Boolean { - return applyStylesRecursivelyDetailed(root, scope).layoutDirty - } + scope: StyleApplicationScope = StyleApplicationScope.Application, + ): Boolean = applyStylesRecursivelyDetailed(root, scope).layoutDirty fun applyStylesRecursivelyDetailed( root: DOMNode, - scope: StyleApplicationScope = StyleApplicationScope.Application + scope: StyleApplicationScope = StyleApplicationScope.Application, ): StyleApplyReport { val snapshot = snapshotForScope(scope) val metrics = MutableApplyMetrics() val variables = resolvedVariables(snapshot, scope) val allowInspectorOverrides = scope == StyleApplicationScope.Application - val result = if (scope == StyleApplicationScope.Application && shouldApplyTargetedPseudoPass(snapshot)) { - applyStylesToDirtySubtrees(root, snapshot, variables, metrics) - } else { - applyStylesRecursively( - root = root, - snapshot = snapshot, - variables = variables, - metrics = metrics, - parentComputed = null, - rootFontSizePx = DEFAULT_ROOT_FONT_SIZE_PX, - passMode = StylePassMode.Full, - scope = scope, - allowInspectorOverrides = allowInspectorOverrides - ) - } + val result = + if (scope == StyleApplicationScope.Application && shouldApplyTargetedPseudoPass(snapshot)) { + applyStylesToDirtySubtrees(root, snapshot, variables, metrics) + } else { + applyStylesRecursively( + root = root, + snapshot = snapshot, + variables = variables, + metrics = metrics, + parentComputed = null, + rootFontSizePx = DEFAULT_ROOT_FONT_SIZE_PX, + passMode = StylePassMode.Full, + scope = scope, + allowInspectorOverrides = allowInspectorOverrides, + ) + } if (scope == StyleApplicationScope.Application) { pseudoDirtyNodes.clear() selectorDirtyNodes.clear() @@ -351,31 +353,31 @@ object StyleEngine { lastAppliedSelectorStateVersion = selectorStateVersion } - val report = StyleApplyReport( - layoutDirty = result.layoutDirty, - visualDirty = result.visualDirty, - visitedNodes = metrics.visitedNodes, - cacheHits = metrics.cacheHits, - recomputedNodes = metrics.recomputedNodes - ) + val report = + StyleApplyReport( + layoutDirty = result.layoutDirty, + visualDirty = result.visualDirty, + visitedNodes = metrics.visitedNodes, + cacheHits = metrics.cacheHits, + recomputedNodes = metrics.recomputedNodes, + ) lastApplyReport = report return report } fun lastStyleApplyReport(): StyleApplyReport = lastApplyReport - fun currentStyleRevision( - scope: StyleApplicationScope = StyleApplicationScope.Application - ): Long { - val base = when (scope) { - StyleApplicationScope.Application -> { - (StylesheetManager.snapshot().version shl 2) xor - (themeVersion shl 1) xor - inspectorOverridesVersion - } + fun currentStyleRevision(scope: StyleApplicationScope = StyleApplicationScope.Application): Long { + val base = + when (scope) { + StyleApplicationScope.Application -> { + (StylesheetManager.snapshot().version shl 2) xor + (themeVersion shl 1) xor + inspectorOverridesVersion + } - StyleApplicationScope.SystemOverlay -> 0L - } + StyleApplicationScope.SystemOverlay -> 0L + } return base xor (pseudoStateVersion shl 3) xor (selectorStateVersion shl 4) xor @@ -410,23 +412,25 @@ object StyleEngine { return snapshot.version == lastAppliedStylesheetVersion && themeVersion == lastAppliedThemeVersion && inspectorOverridesVersion == lastAppliedInspectorOverridesVersion && - (pseudoStateVersion != lastAppliedPseudoStateVersion || - selectorStateVersion != lastAppliedSelectorStateVersion) + ( + pseudoStateVersion != lastAppliedPseudoStateVersion || + selectorStateVersion != lastAppliedSelectorStateVersion + ) } private fun applyStylesToDirtySubtrees( root: DOMNode, snapshot: StylesheetSnapshot, variables: Map, - metrics: MutableApplyMetrics + metrics: MutableApplyMetrics, ): NodeApplyFlags { - val dirty = expandDirtyNodesForCombinators( - root = root, - snapshot = snapshot, - rawDirty = pseudoDirtyNodes + selectorDirtyNodes - ) - .filter { it.isDescendantOfOrSame(root) } - .sortedBy { it.depth() } + val dirty = + expandDirtyNodesForCombinators( + root = root, + snapshot = snapshot, + rawDirty = pseudoDirtyNodes + selectorDirtyNodes, + ).filter { it.isDescendantOfOrSame(root) } + .sortedBy { it.depth() } if (dirty.isEmpty()) { // Dirty markers may reference detached/template nodes after reconcile. // Fall back to full tree style application to keep frame output deterministic. @@ -439,7 +443,7 @@ object StyleEngine { rootFontSizePx = DEFAULT_ROOT_FONT_SIZE_PX, passMode = StylePassMode.Full, scope = StyleApplicationScope.Application, - allowInspectorOverrides = true + allowInspectorOverrides = true, ) } @@ -452,21 +456,23 @@ object StyleEngine { var flags = NodeApplyFlags(layoutDirty = false, visualDirty = false) effectiveRoots.forEach { subtreeRoot -> - val subtreeFlags = applyStylesRecursively( - root = subtreeRoot, - snapshot = snapshot, - variables = variables, - metrics = metrics, - parentComputed = subtreeRoot.parent?.appliedComputedStyleSnapshot(), - rootFontSizePx = rootFontSizeFor(subtreeRoot), - passMode = StylePassMode.Targeted, - scope = StyleApplicationScope.Application, - allowInspectorOverrides = true - ) - flags = NodeApplyFlags( - layoutDirty = flags.layoutDirty || subtreeFlags.layoutDirty, - visualDirty = flags.visualDirty || subtreeFlags.visualDirty - ) + val subtreeFlags = + applyStylesRecursively( + root = subtreeRoot, + snapshot = snapshot, + variables = variables, + metrics = metrics, + parentComputed = subtreeRoot.parent?.appliedComputedStyleSnapshot(), + rootFontSizePx = rootFontSizeFor(subtreeRoot), + passMode = StylePassMode.Targeted, + scope = StyleApplicationScope.Application, + allowInspectorOverrides = true, + ) + flags = + NodeApplyFlags( + layoutDirty = flags.layoutDirty || subtreeFlags.layoutDirty, + visualDirty = flags.visualDirty || subtreeFlags.visualDirty, + ) } return flags } @@ -480,47 +486,52 @@ object StyleEngine { rootFontSizePx: Int, passMode: StylePassMode, scope: StyleApplicationScope, - allowInspectorOverrides: Boolean + allowInspectorOverrides: Boolean, ): NodeApplyFlags { - val nodeResult = applyStyleToNode( - node = root, - snapshot = snapshot, - variables = variables, - metrics = metrics, - parentComputed = parentComputed, - rootFontSizePx = rootFontSizePx, - scope = scope, - allowInspectorOverrides = allowInspectorOverrides - ) + val nodeResult = + applyStyleToNode( + node = root, + snapshot = snapshot, + variables = variables, + metrics = metrics, + parentComputed = parentComputed, + rootFontSizePx = rootFontSizePx, + scope = scope, + allowInspectorOverrides = allowInspectorOverrides, + ) var flags = nodeResult.flags - val canSkipSubtree = passMode == StylePassMode.Targeted && - nodeResult.cacheHit && - !snapshot.index.hasAncestorDependentSelectors + val canSkipSubtree = + passMode == StylePassMode.Targeted && + nodeResult.cacheHit && + !snapshot.index.hasAncestorDependentSelectors if (canSkipSubtree) { return flags } val nodeComputed = root.appliedComputedStyleSnapshot() - val nextRootFontSizePx = if (root.parent == null) { - nodeComputed?.fontSize ?: rootFontSizePx - } else { - rootFontSizePx - } + val nextRootFontSizePx = + if (root.parent == null) { + nodeComputed?.fontSize ?: rootFontSizePx + } else { + rootFontSizePx + } root.children.forEach { child -> - val childFlags = applyStylesRecursively( - root = child, - snapshot = snapshot, - variables = variables, - metrics = metrics, - parentComputed = nodeComputed, - rootFontSizePx = nextRootFontSizePx, - passMode = passMode, - scope = scope, - allowInspectorOverrides = allowInspectorOverrides - ) - flags = NodeApplyFlags( - layoutDirty = flags.layoutDirty || childFlags.layoutDirty, - visualDirty = flags.visualDirty || childFlags.visualDirty - ) + val childFlags = + applyStylesRecursively( + root = child, + snapshot = snapshot, + variables = variables, + metrics = metrics, + parentComputed = nodeComputed, + rootFontSizePx = nextRootFontSizePx, + passMode = passMode, + scope = scope, + allowInspectorOverrides = allowInspectorOverrides, + ) + flags = + NodeApplyFlags( + layoutDirty = flags.layoutDirty || childFlags.layoutDirty, + visualDirty = flags.visualDirty || childFlags.visualDirty, + ) } return flags } @@ -533,70 +544,82 @@ object StyleEngine { parentComputed: ComputedStyle?, rootFontSizePx: Int, scope: StyleApplicationScope, - allowInspectorOverrides: Boolean + allowInspectorOverrides: Boolean, ): NodeApplyResult { metrics.visitedNodes += 1 val defaults = node.captureStyleDefaults() - val key = CacheKey( - typeName = node.styleType, - nodeId = selectorNodeId(node), - classesHash = node.styleClasses.hashCode(), - inlineHash = node.inlineStyleDeclarations.toStableHash(), - inspectorHash = if (allowInspectorOverrides) inspectorOverrideHash(node) else 0, - hovered = node.styleHovered, - active = node.styleActive, - focused = node.styleFocused, - disabled = node.styleDisabled, - open = node.styleOpen, - stylesheetVersion = snapshot.version, - themeVersion = if (scope == StyleApplicationScope.Application) themeVersion else 0L, - defaultsHash = defaults.hashCode(), - parentInheritedHash = inheritedHash(parentComputed), - ancestorSelectorHash = if (snapshot.index.hasAncestorDependentSelectors) ancestorSelectorHash(node) else 0, - siblingSelectorHash = if ( - snapshot.index.hasAdjacentSiblingCombinators || snapshot.index.hasGeneralSiblingCombinators - ) { - previousSiblingSelectorHash(node) - } else { - 0 - }, - rootFontSizePx = rootFontSizePx, - viewportWidthPx = viewportWidthPx, - viewportHeightPx = viewportHeightPx, - scope = scope - ) + val key = + CacheKey( + typeName = node.styleType, + nodeId = selectorNodeId(node), + classesHash = node.styleClasses.hashCode(), + inlineHash = node.inlineStyleDeclarations.toStableHash(), + inspectorHash = if (allowInspectorOverrides) inspectorOverrideHash(node) else 0, + hovered = node.styleHovered, + active = node.styleActive, + focused = node.styleFocused, + disabled = node.styleDisabled, + open = node.styleOpen, + stylesheetVersion = snapshot.version, + themeVersion = if (scope == StyleApplicationScope.Application) themeVersion else 0L, + defaultsHash = defaults.hashCode(), + parentInheritedHash = inheritedHash(parentComputed), + ancestorSelectorHash = + if (snapshot.index.hasAncestorDependentSelectors) { + ancestorSelectorHash( + node, + ) + } else { + 0 + }, + siblingSelectorHash = + if ( + snapshot.index.hasAdjacentSiblingCombinators || snapshot.index.hasGeneralSiblingCombinators + ) { + previousSiblingSelectorHash(node) + } else { + 0 + }, + rootFontSizePx = rootFontSizePx, + viewportWidthPx = viewportWidthPx, + viewportHeightPx = viewportHeightPx, + scope = scope, + ) val cached = cache[node] if (cached != null && cached.key == key) { metrics.cacheHits += 1 val result = node.applyComputedStyle(cached.style) return NodeApplyResult( - flags = NodeApplyFlags( - layoutDirty = result.layoutDirty, - visualDirty = result.visualDirty - ), - cacheHit = true + flags = + NodeApplyFlags( + layoutDirty = result.layoutDirty, + visualDirty = result.visualDirty, + ), + cacheHit = true, ) } - val computed = computeStyle( - node = node, - defaults = defaults, - snapshot = snapshot, - variables = variables, - parentComputed = parentComputed, - rootFontSizePx = rootFontSizePx, - allowInspectorOverrides = allowInspectorOverrides - ) + val computed = + computeStyle( + node = node, + defaults = defaults, + snapshot = snapshot, + variables = variables, + parentComputed = parentComputed, + rootFontSizePx = rootFontSizePx, + allowInspectorOverrides = allowInspectorOverrides, + ) cache[node] = CachedStyle(key = key, style = computed) metrics.recomputedNodes += 1 val result = node.applyComputedStyle(computed) return NodeApplyResult( - flags = NodeApplyFlags( - layoutDirty = result.layoutDirty, - visualDirty = result.visualDirty - ), - cacheHit = false + flags = + NodeApplyFlags( + layoutDirty = result.layoutDirty, + visualDirty = result.visualDirty, + ), + cacheHit = false, ) } @@ -607,31 +630,33 @@ object StyleEngine { variables: Map, parentComputed: ComputedStyle?, rootFontSizePx: Int, - allowInspectorOverrides: Boolean + allowInspectorOverrides: Boolean, ): ComputedStyle { val candidates = matchingCandidates(node, snapshot.index) - val winners = resolveCascadeWinners( - node = node, - candidates = candidates, - inline = node.inlineStyleDeclarations, - inspector = if (allowInspectorOverrides) inspectorOverrides[inspectorOverrideTarget(node)] else null - ) + val winners = + resolveCascadeWinners( + node = node, + candidates = candidates, + inline = node.inlineStyleDeclarations, + inspector = if (allowInspectorOverrides) inspectorOverrides[inspectorOverrideTarget(node)] else null, + ) var result = defaults.toComputedStyle() for (property in StyleProperty.entries) { val winner = winners[property] if (winner != null) { - val applied = runCatching { - applyProperty( - current = result, - parentComputed = parentComputed, - property = property, - expression = winner.expression, - variables = variables, - rootFontSizePx = rootFontSizePx - ) - }.onFailure { error -> - println("[DSGL-Style] Failed to apply '${property.key}': ${error.message}") - }.getOrNull() + val applied = + runCatching { + applyProperty( + current = result, + parentComputed = parentComputed, + property = property, + expression = winner.expression, + variables = variables, + rootFontSizePx = rootFontSizePx, + ) + }.onFailure { error -> + println("[DSGL-Style] Failed to apply '${property.key}': ${error.message}") + }.getOrNull() if (applied != null) { result = applied } @@ -646,27 +671,29 @@ object StyleEngine { node: DOMNode, candidates: List, inline: StyleDeclarations, - inspector: StyleDeclarations? + inspector: StyleDeclarations?, ): EnumMap { val winners = EnumMap(StyleProperty::class.java) candidates.forEach { rule -> rule.declarations.values.forEach { (property, expression) -> val important = rule.declarations.isImportant(property) - val candidate = CascadeWinner( - expression = expression, - important = important, - specificity = rule.selector.specificity, - sourceOrder = rule.sourceOrder, - origin = StyleOrigin.Stylesheet, - sourceKind = StyleSourceKind.Selector, - sourceLabel = buildString { - append(selectorLabel(rule.selector)) - append(" @ ") - append(rule.fileName) - if (important) append(" !important") - } - ) + val candidate = + CascadeWinner( + expression = expression, + important = important, + specificity = rule.selector.specificity, + sourceOrder = rule.sourceOrder, + origin = StyleOrigin.Stylesheet, + sourceKind = StyleSourceKind.Selector, + sourceLabel = + buildString { + append(selectorLabel(rule.selector)) + append(" @ ") + append(rule.fileName) + if (important) append(" !important") + }, + ) if (shouldReplace(winners[property], candidate)) { winners[property] = candidate } @@ -675,15 +702,16 @@ object StyleEngine { inline.values.forEach { (property, expression) -> val important = inline.isImportant(property) - val candidate = CascadeWinner( - expression = expression, - important = important, - specificity = StyleSpecificity(idCount = 1, classLikeCount = 0, typeCount = 0), - sourceOrder = Int.MAX_VALUE - 1, - origin = StyleOrigin.Inline, - sourceKind = StyleSourceKind.Inline, - sourceLabel = if (important) "inline !important" else "inline" - ) + val candidate = + CascadeWinner( + expression = expression, + important = important, + specificity = StyleSpecificity(idCount = 1, classLikeCount = 0, typeCount = 0), + sourceOrder = Int.MAX_VALUE - 1, + origin = StyleOrigin.Inline, + sourceKind = StyleSourceKind.Inline, + sourceLabel = if (important) "inline !important" else "inline", + ) if (shouldReplace(winners[property], candidate)) { winners[property] = candidate } @@ -691,15 +719,16 @@ object StyleEngine { inspector?.values?.forEach { (property, expression) -> val important = inspector.isImportant(property) - val candidate = CascadeWinner( - expression = expression, - important = important, - specificity = StyleSpecificity(idCount = 1, classLikeCount = 0, typeCount = 0), - sourceOrder = Int.MAX_VALUE, - origin = StyleOrigin.Inspector, - sourceKind = StyleSourceKind.InspectorOverride, - sourceLabel = if (important) "inspector !important" else "inspector" - ) + val candidate = + CascadeWinner( + expression = expression, + important = important, + specificity = StyleSpecificity(idCount = 1, classLikeCount = 0, typeCount = 0), + sourceOrder = Int.MAX_VALUE, + origin = StyleOrigin.Inspector, + sourceKind = StyleSourceKind.InspectorOverride, + sourceLabel = if (important) "inspector !important" else "inspector", + ) if (shouldReplace(winners[property], candidate)) { winners[property] = candidate } @@ -726,16 +755,16 @@ object StyleEngine { return true } - private fun snapshotForScope(scope: StyleApplicationScope): StylesheetSnapshot { - return when (scope) { + private fun snapshotForScope(scope: StyleApplicationScope): StylesheetSnapshot = + when (scope) { StyleApplicationScope.Application -> StylesheetManager.snapshot() - StyleApplicationScope.SystemOverlay -> StylesheetSnapshot( - version = Long.MIN_VALUE, - index = RuleIndex.EMPTY, - rootVariables = emptyMap() - ) + StyleApplicationScope.SystemOverlay -> + StylesheetSnapshot( + version = Long.MIN_VALUE, + index = RuleIndex.EMPTY, + rootVariables = emptyMap(), + ) } - } private fun resolvedVariables(snapshot: StylesheetSnapshot, scope: StyleApplicationScope): Map { if (scope == StyleApplicationScope.SystemOverlay) { @@ -747,11 +776,10 @@ object StyleEngine { return variables } - private fun matchingCandidates(node: DOMNode, index: RuleIndex): List { - return gatherCandidates(node, index) + private fun matchingCandidates(node: DOMNode, index: RuleIndex): List = + gatherCandidates(node, index) .filter { selectorMatches(node, it.selector) } .sortedBy { it.sourceOrder } - } private fun gatherCandidates(node: DOMNode, index: RuleIndex): List { val out = linkedSetOf() @@ -777,18 +805,21 @@ object StyleEngine { val targetNode = current ?: return false if (!selectorPartMatches(targetNode, step.part)) return false if (index == 0) return true - current = when (step.combinatorToLeft ?: StyleCombinator.Descendant) { - StyleCombinator.Child -> targetNode.parent - StyleCombinator.Descendant -> findAncestorMatching( - from = targetNode.parent, - part = steps[index - 1].part - ) - StyleCombinator.AdjacentSibling -> targetNode.previousSiblingOrNull() - StyleCombinator.GeneralSibling -> findPreviousSiblingMatching( - from = targetNode.previousSiblingOrNull(), - part = steps[index - 1].part - ) - } + current = + when (step.combinatorToLeft ?: StyleCombinator.Descendant) { + StyleCombinator.Child -> targetNode.parent + StyleCombinator.Descendant -> + findAncestorMatching( + from = targetNode.parent, + part = steps[index - 1].part, + ) + StyleCombinator.AdjacentSibling -> targetNode.previousSiblingOrNull() + StyleCombinator.GeneralSibling -> + findPreviousSiblingMatching( + from = targetNode.previousSiblingOrNull(), + part = steps[index - 1].part, + ) + } if (current == null) return false } return true @@ -819,10 +850,11 @@ object StyleEngine { private fun selectorPartMatches(node: DOMNode, part: StyleSelectorPart): Boolean { if (part.id != null && part.id != selectorNodeId(node)) return false part.typeName?.let { typeName -> - val typeMatches = when (typeName) { - ROOT_SELECTOR_INTERNAL -> node.parent == null - else -> typeName == node.styleType - } + val typeMatches = + when (typeName) { + ROOT_SELECTOR_INTERNAL -> node.parent == null + else -> typeName == node.styleType + } if (!typeMatches) return false } if (part.classes.isNotEmpty() && !node.styleClasses.containsAll(part.classes)) return false @@ -836,38 +868,40 @@ object StyleEngine { } } - private fun selectorLabel(selector: StyleSelector): String { - return selector.steps.joinToString(separator = " ") { step -> - val part = buildString { - when { - step.part.universal -> append("*") - step.part.typeName != null -> append(step.part.typeName) - } - step.part.id?.let { append('#').append(it) } - step.part.classes.forEach { className -> append('.').append(className) } - when (step.part.pseudoState) { - StylePseudoState.HOVER -> append(":hover") - StylePseudoState.ACTIVE -> append(":active") - StylePseudoState.FOCUS -> append(":focus") - StylePseudoState.DISABLED -> append(":disabled") - StylePseudoState.OPEN -> append(":open") - null -> Unit - } - } - val prefix = when (step.combinatorToLeft) { - StyleCombinator.Child -> ">" - StyleCombinator.Descendant -> "" - StyleCombinator.AdjacentSibling -> "+" - StyleCombinator.GeneralSibling -> "~" - null -> "" - } - if (prefix.isEmpty()) part else "$prefix $part" - }.trim() - } + private fun selectorLabel(selector: StyleSelector): String = + selector.steps + .joinToString(separator = " ") { step -> + val part = + buildString { + when { + step.part.universal -> append("*") + step.part.typeName != null -> append(step.part.typeName) + } + step.part.id + ?.let { append('#').append(it) } + step.part.classes + .forEach { className -> append('.').append(className) } + when (step.part.pseudoState) { + StylePseudoState.HOVER -> append(":hover") + StylePseudoState.ACTIVE -> append(":active") + StylePseudoState.FOCUS -> append(":focus") + StylePseudoState.DISABLED -> append(":disabled") + StylePseudoState.OPEN -> append(":open") + null -> Unit + } + } + val prefix = + when (step.combinatorToLeft) { + StyleCombinator.Child -> ">" + StyleCombinator.Descendant -> "" + StyleCombinator.AdjacentSibling -> "+" + StyleCombinator.GeneralSibling -> "~" + null -> "" + } + if (prefix.isEmpty()) part else "$prefix $part" + }.trim() - private fun selectorNodeId(node: DOMNode): String? { - return node.styleId ?: (node.key as? String) - } + private fun selectorNodeId(node: DOMNode): String? = node.styleId ?: (node.key as? String) private fun ancestorSelectorHash(node: DOMNode): Int { var current = node.parent @@ -912,8 +946,12 @@ object StyleEngine { ?: DEFAULT_ROOT_FONT_SIZE_PX } - private fun inheritProperty(current: ComputedStyle, parent: ComputedStyle, property: StyleProperty): ComputedStyle { - return when (property) { + private fun inheritProperty( + current: ComputedStyle, + parent: ComputedStyle, + property: StyleProperty, + ): ComputedStyle = + when (property) { StyleProperty.FOREGROUND_COLOR -> current.copy(foregroundColor = parent.foregroundColor) StyleProperty.FONT_ID -> current.copy(fontId = parent.fontId) StyleProperty.FONT_SIZE -> current.copy(fontSize = parent.fontSize, fontSizeValue = parent.fontSizeValue) @@ -922,7 +960,6 @@ object StyleEngine { StyleProperty.FONT_STYLE -> current.copy(fontStyle = parent.fontStyle) else -> current } - } private const val ROOT_SELECTOR_INTERNAL = "dsgl-root" @@ -944,70 +981,84 @@ object StyleEngine { property: StyleProperty, expression: StyleExpression, variables: Map, - rootFontSizePx: Int + rootFontSizePx: Int, ): ComputedStyle { val literal = resolveExpressionToLiteral(expression, variables) return when (property) { - StyleProperty.MARGIN -> current.copy( - margin = parseSpacingLengthShorthand( - raw = literal, - allowNegative = true + StyleProperty.MARGIN -> + current.copy( + margin = + parseSpacingLengthShorthand( + raw = literal, + allowNegative = true, + ), ) - ) - StyleProperty.PADDING -> current.copy( - padding = parseSpacingLengthShorthand( - raw = literal, - allowNegative = false + StyleProperty.PADDING -> + current.copy( + padding = + parseSpacingLengthShorthand( + raw = literal, + allowNegative = false, + ), ) - ) StyleProperty.BACKGROUND_COLOR -> current.copy(backgroundColor = parseColor(literal)) StyleProperty.BACKGROUND_IMAGE -> current.copy(backgroundImage = parseStringLiteral(literal)) StyleProperty.BORDER_COLOR -> current.copy(borderColor = parseColor(literal)) - StyleProperty.BORDER_WIDTH -> current.copy( - borderWidth = parseLengthLiteral(literal, allowNegative = false) - ) - StyleProperty.BORDER_RADIUS -> current.copy( - borderRadius = parseLengthLiteral(literal, allowNegative = false) - ) + StyleProperty.BORDER_WIDTH -> + current.copy( + borderWidth = parseLengthLiteral(literal, allowNegative = false), + ) + StyleProperty.BORDER_RADIUS -> + current.copy( + borderRadius = parseLengthLiteral(literal, allowNegative = false), + ) StyleProperty.FOREGROUND_COLOR -> current.copy(foregroundColor = parseColor(literal)) StyleProperty.FONT_ID -> current.copy(fontId = parseStringLiteral(literal)) StyleProperty.FONT_SIZE -> { val fontSizeValue = parseLengthLiteral(literal, allowNegative = false) current.copy( - fontSize = resolveFontSizePx( - fontSizeValue = fontSizeValue, - current = current, - parentComputed = parentComputed, - rootFontSizePx = rootFontSizePx - ).coerceAtLeast(1), - fontSizeValue = fontSizeValue + fontSize = + resolveFontSizePx( + fontSizeValue = fontSizeValue, + current = current, + parentComputed = parentComputed, + rootFontSizePx = rootFontSizePx, + ).coerceAtLeast(1), + fontSizeValue = fontSizeValue, ) } - StyleProperty.LINE_HEIGHT -> current.copy( - lineHeight = StylePropertyRegistry.parseLineHeightLiteral(property, literal) - ) + StyleProperty.LINE_HEIGHT -> + current.copy( + lineHeight = StylePropertyRegistry.parseLineHeightLiteral(property, literal), + ) StyleProperty.FONT_WEIGHT -> current.copy(fontWeight = parseFontWeight(literal)) StyleProperty.FONT_STYLE -> current.copy(fontStyle = parseFontStyle(literal)) StyleProperty.TEXT_DECORATION -> current.copy(textDecoration = parseTextDecoration(literal)) StyleProperty.OBFUSCATED -> current.copy(obfuscated = parseBooleanLike(literal)) - StyleProperty.WIDTH -> current.copy( - width = parseLengthLiteral(literal, allowNegative = false) - ) - StyleProperty.HEIGHT -> current.copy( - height = parseLengthLiteral(literal, allowNegative = false) - ) - StyleProperty.MIN_WIDTH -> current.copy( - minWidth = parseOptionalConstraintLengthLiteral(literal) - ) - StyleProperty.MIN_HEIGHT -> current.copy( - minHeight = parseOptionalConstraintLengthLiteral(literal) - ) - StyleProperty.MAX_WIDTH -> current.copy( - maxWidth = parseOptionalConstraintLengthLiteral(literal) - ) - StyleProperty.MAX_HEIGHT -> current.copy( - maxHeight = parseOptionalConstraintLengthLiteral(literal) - ) + StyleProperty.WIDTH -> + current.copy( + width = parseLengthLiteral(literal, allowNegative = false), + ) + StyleProperty.HEIGHT -> + current.copy( + height = parseLengthLiteral(literal, allowNegative = false), + ) + StyleProperty.MIN_WIDTH -> + current.copy( + minWidth = parseOptionalConstraintLengthLiteral(literal), + ) + StyleProperty.MIN_HEIGHT -> + current.copy( + minHeight = parseOptionalConstraintLengthLiteral(literal), + ) + StyleProperty.MAX_WIDTH -> + current.copy( + maxWidth = parseOptionalConstraintLengthLiteral(literal), + ) + StyleProperty.MAX_HEIGHT -> + current.copy( + maxHeight = parseOptionalConstraintLengthLiteral(literal), + ) StyleProperty.ALIGN -> current.copy(align = parseAlign(literal)) StyleProperty.DISPLAY -> current.copy(display = parseDisplay(literal)) StyleProperty.POSITION -> current.copy(position = parsePosition(literal)) @@ -1019,13 +1070,14 @@ object StyleEngine { StyleProperty.OVERFLOW -> { val overflowAxes = parseOverflowShorthand(literal) current.copy( - overflow = if (overflowAxes.overflowX == overflowAxes.overflowY) { - overflowAxes.overflowX - } else { - Overflow.Visible - }, + overflow = + if (overflowAxes.overflowX == overflowAxes.overflowY) { + overflowAxes.overflowX + } else { + Overflow.Visible + }, overflowX = overflowAxes.overflowX, - overflowY = overflowAxes.overflowY + overflowY = overflowAxes.overflowY, ) } StyleProperty.OVERFLOW_X -> current.copy(overflowX = parseOverflow(literal)) @@ -1034,18 +1086,21 @@ object StyleEngine { StyleProperty.JUSTIFY_CONTENT -> current.copy(justifyContent = parseJustifyContent(literal)) StyleProperty.ALIGN_ITEMS -> current.copy(alignItems = parseAlignItems(literal)) StyleProperty.JUSTIFY_ITEMS -> current.copy(justifyItems = parseJustifyItems(literal)) - StyleProperty.GAP -> current.copy( - gap = parseLengthLiteral(literal, allowNegative = false) - ) + StyleProperty.GAP -> + current.copy( + gap = parseLengthLiteral(literal, allowNegative = false), + ) StyleProperty.FLEX_GROW -> current.copy(flexGrow = parseFloatLike(literal).coerceAtLeast(0f)) StyleProperty.FLEX_SHRINK -> current.copy(flexShrink = parseFloatLike(literal).coerceAtLeast(0f)) - StyleProperty.FLEX_BASIS -> current.copy( - flexBasis = parseOptionalCssLength(literal)?.also { length -> - if (length.value < 0f) { - error("Negative length is not allowed: '$literal'.") - } - } - ) + StyleProperty.FLEX_BASIS -> + current.copy( + flexBasis = + parseOptionalCssLength(literal)?.also { length -> + if (length.value < 0f) { + error("Negative length is not allowed: '$literal'.") + } + }, + ) StyleProperty.GRID_COLUMNS -> current.copy(gridColumns = parseIntLike(literal).coerceAtLeast(1)) StyleProperty.GRID_ROWS -> current.copy(gridRows = parseOptionalInt(literal)?.coerceAtLeast(1)) StyleProperty.GRID_AUTO_FLOW -> current.copy(gridAutoFlow = parseGridAutoFlow(literal)) @@ -1079,38 +1134,43 @@ object StyleEngine { fontSizeValue: CssLength, current: ComputedStyle, parentComputed: ComputedStyle?, - rootFontSizePx: Int + rootFontSizePx: Int, ): Int { - val inheritedFontPx = ( - parentComputed?.fontSize - ?: current.fontSize - ?: 16 + val inheritedFontPx = + ( + parentComputed?.fontSize + ?: current.fontSize + ?: 16 ).toFloat() - val context = LengthResolveContext( - viewportWidthPx = viewportWidthPx.toFloat(), - viewportHeightPx = viewportHeightPx.toFloat(), - containingBlockWidthPx = viewportWidthPx.toFloat(), - containingBlockHeightPx = viewportHeightPx.toFloat(), - rootFontSizePx = rootFontSizePx.toFloat(), - currentFontSizePx = inheritedFontPx, - inheritedFontSizePx = inheritedFontPx - ) + val context = + LengthResolveContext( + viewportWidthPx = viewportWidthPx.toFloat(), + viewportHeightPx = viewportHeightPx.toFloat(), + containingBlockWidthPx = viewportWidthPx.toFloat(), + containingBlockHeightPx = viewportHeightPx.toFloat(), + rootFontSizePx = rootFontSizePx.toFloat(), + currentFontSizePx = inheritedFontPx, + inheritedFontSizePx = inheritedFontPx, + ) return fontSizeValue .resolvePx(context, LengthPercentBase.InheritedFontSize) .roundToInt() .coerceAtLeast(1) } - private fun inspectorOverrideHash(node: DOMNode): Int { - return inspectorOverrideVersions[inspectorOverrideTarget(node)] ?: 0 - } + private fun inspectorOverrideHash(node: DOMNode): Int = + inspectorOverrideVersions[inspectorOverrideTarget(node)] ?: 0 private fun anonymousInspectorPath(node: DOMNode): String { val parts = ArrayList(8) var current: DOMNode? = node while (current != null) { val parent = current.parent - val index = parent?.children?.indexOf(current)?.coerceAtLeast(0) ?: 0 + val index = + parent + ?.children + ?.indexOf(current) + ?.coerceAtLeast(0) ?: 0 parts += "${current.styleType}[$index]" current = parent } @@ -1161,7 +1221,7 @@ object StyleEngine { private fun expandDirtyNodesForCombinators( root: DOMNode, snapshot: StylesheetSnapshot, - rawDirty: Collection + rawDirty: Collection, ): Set { if (rawDirty.isEmpty()) return emptySet() if (!snapshot.index.hasAdjacentSiblingCombinators && !snapshot.index.hasGeneralSiblingCombinators) { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleInspection.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleInspection.kt index 2fde298..841b469 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleInspection.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleInspection.kt @@ -5,17 +5,17 @@ enum class StyleSourceKind { Inherited, Selector, Inline, - InspectorOverride + InspectorOverride, } data class StylePropertySource( val property: StyleProperty, val kind: StyleSourceKind, - val source: String + val source: String, ) data class StyleInspection( val computed: ComputedStyle, val propertySources: Map, - val matchedRules: List + val matchedRules: List, ) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleModel.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleModel.kt index f4eaa19..8495a87 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleModel.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleModel.kt @@ -8,13 +8,13 @@ enum class StylePseudoState { ACTIVE, FOCUS, DISABLED, - OPEN + OPEN, } enum class StyleAlign { START, CENTER, - END + END, } enum class Display { @@ -22,7 +22,7 @@ enum class Display { Inline, None, Flex, - Grid + Grid, } enum class PositionMode { @@ -30,19 +30,19 @@ enum class PositionMode { Relative, Absolute, Fixed, - Sticky + Sticky, } enum class Overflow { Visible, Hidden, Scroll, - Auto + Auto, } enum class FlexDirection { Row, - Column + Column, } enum class JustifyContent { @@ -51,58 +51,61 @@ enum class JustifyContent { End, SpaceBetween, SpaceAround, - SpaceEvenly + SpaceEvenly, } enum class AlignItems { Start, Center, End, - Stretch + Stretch, } enum class JustifyItems { Start, Center, End, - Stretch + Stretch, } enum class GridAutoFlow { Row, - Column + Column, } enum class TextWrap { Wrap, - NoWrap + NoWrap, } enum class TextFormatting { None, - Minecraft + Minecraft, } enum class FontWeight { Normal, - Bold + Bold, } enum class FontStyle { Normal, - Italic + Italic, } enum class TextDecoration { None, Underline, Strikethrough, - UnderlineStrikethrough + UnderlineStrikethrough, } sealed class LineHeightValue { object Normal : LineHeightValue() - data class Length(val value: CssLength) : LineHeightValue() + + data class Length( + val value: CssLength, + ) : LineHeightValue() } data class UiTransform( @@ -110,19 +113,20 @@ data class UiTransform( val translateY: Float = 0f, val scaleX: Float = 1f, val scaleY: Float = 1f, - val rotateDeg: Float = 0f + val rotateDeg: Float = 0f, ) { fun translated(x: Float, y: Float): UiTransform = copy(translateX = translateX + x, translateY = translateY + y) + fun scaled(x: Float, y: Float = x): UiTransform = copy(scaleX = scaleX * x, scaleY = scaleY * y) + fun rotated(deg: Float): UiTransform = copy(rotateDeg = rotateDeg + deg) - fun isIdentity(): Boolean { - return translateX == 0f && - translateY == 0f && - scaleX == 1f && - scaleY == 1f && - rotateDeg == 0f - } + fun isIdentity(): Boolean = + translateX == 0f && + translateY == 0f && + scaleX == 1f && + scaleY == 1f && + rotateDeg == 0f companion object { val IDENTITY: UiTransform = UiTransform() @@ -131,7 +135,7 @@ data class UiTransform( data class TransformOrigin( val originX: Float = 0.5f, - val originY: Float = 0.5f + val originY: Float = 0.5f, ) { init { require(originX in 0f..1f) { "transform originX must be in [0..1]" } @@ -143,7 +147,9 @@ data class TransformOrigin( } } -enum class StyleProperty(val key: String) { +enum class StyleProperty( + val key: String, +) { MARGIN("margin"), PADDING("padding"), BACKGROUND_COLOR("background-color"), @@ -193,10 +199,12 @@ enum class StyleProperty(val key: String) { TEXT_FORMATTING("text-formatting"), TRANSFORM("transform"), TRANSFORM_ORIGIN("transform-origin"), - OPACITY("opacity"); + OPACITY("opacity"), + ; companion object { - private val byName: Map = entries.associateBy { it.key.lowercase() } + + private val byName: Map = + entries.associateBy { it.key.lowercase() } + mapOf( "backgroundcolor" to BACKGROUND_COLOR, "background-color" to BACKGROUND_COLOR, @@ -220,77 +228,83 @@ enum class StyleProperty(val key: String) { "fontweight" to FONT_WEIGHT, "font-weight" to FONT_WEIGHT, "fontstyle" to FONT_STYLE, - "font-style" to FONT_STYLE - ) + mapOf( - "position" to POSITION, - "left" to LEFT, - "top" to TOP, - "right" to RIGHT, - "bottom" to BOTTOM, - "zindex" to Z_INDEX, - "z-index" to Z_INDEX, - "flexdirection" to FLEX_DIRECTION, - "flex-direction" to FLEX_DIRECTION, - "justifycontent" to JUSTIFY_CONTENT, - "justify-content" to JUSTIFY_CONTENT, - "alignitems" to ALIGN_ITEMS, - "align-items" to ALIGN_ITEMS, - "justifyitems" to JUSTIFY_ITEMS, - "justify-items" to JUSTIFY_ITEMS, - "flexgrow" to FLEX_GROW, - "flex-grow" to FLEX_GROW, - "flexshrink" to FLEX_SHRINK, - "flex-shrink" to FLEX_SHRINK, - "flexbasis" to FLEX_BASIS, - "flex-basis" to FLEX_BASIS, - "gridcolumns" to GRID_COLUMNS, - "grid-columns" to GRID_COLUMNS, - "gridrows" to GRID_ROWS, - "grid-rows" to GRID_ROWS, - "gridautoflow" to GRID_AUTO_FLOW, - "grid-auto-flow" to GRID_AUTO_FLOW, - "gridcolumnspan" to GRID_COLUMN_SPAN, - "grid-column-span" to GRID_COLUMN_SPAN, - "gridrowspan" to GRID_ROW_SPAN, - "grid-row-span" to GRID_ROW_SPAN, - "overflow" to OVERFLOW, - "overflowx" to OVERFLOW_X, - "overflow-x" to OVERFLOW_X, - "overflowy" to OVERFLOW_Y, - "overflow-y" to OVERFLOW_Y, - "minwidth" to MIN_WIDTH, - "min-width" to MIN_WIDTH, - "minheight" to MIN_HEIGHT, - "min-height" to MIN_HEIGHT, - "maxwidth" to MAX_WIDTH, - "max-width" to MAX_WIDTH, - "maxheight" to MAX_HEIGHT, - "max-height" to MAX_HEIGHT, - "textwrap" to TEXT_WRAP, - "text-wrap" to TEXT_WRAP, - "textformatting" to TEXT_FORMATTING, - "text-formatting" to TEXT_FORMATTING, - "textdecoration" to TEXT_DECORATION, - "text-decoration" to TEXT_DECORATION, - "obfuscated" to OBFUSCATED, - "transform" to TRANSFORM, - "transformorigin" to TRANSFORM_ORIGIN, - "transform-origin" to TRANSFORM_ORIGIN, - "opacity" to OPACITY - ) + "font-style" to FONT_STYLE, + ) + + mapOf( + "position" to POSITION, + "left" to LEFT, + "top" to TOP, + "right" to RIGHT, + "bottom" to BOTTOM, + "zindex" to Z_INDEX, + "z-index" to Z_INDEX, + "flexdirection" to FLEX_DIRECTION, + "flex-direction" to FLEX_DIRECTION, + "justifycontent" to JUSTIFY_CONTENT, + "justify-content" to JUSTIFY_CONTENT, + "alignitems" to ALIGN_ITEMS, + "align-items" to ALIGN_ITEMS, + "justifyitems" to JUSTIFY_ITEMS, + "justify-items" to JUSTIFY_ITEMS, + "flexgrow" to FLEX_GROW, + "flex-grow" to FLEX_GROW, + "flexshrink" to FLEX_SHRINK, + "flex-shrink" to FLEX_SHRINK, + "flexbasis" to FLEX_BASIS, + "flex-basis" to FLEX_BASIS, + "gridcolumns" to GRID_COLUMNS, + "grid-columns" to GRID_COLUMNS, + "gridrows" to GRID_ROWS, + "grid-rows" to GRID_ROWS, + "gridautoflow" to GRID_AUTO_FLOW, + "grid-auto-flow" to GRID_AUTO_FLOW, + "gridcolumnspan" to GRID_COLUMN_SPAN, + "grid-column-span" to GRID_COLUMN_SPAN, + "gridrowspan" to GRID_ROW_SPAN, + "grid-row-span" to GRID_ROW_SPAN, + "overflow" to OVERFLOW, + "overflowx" to OVERFLOW_X, + "overflow-x" to OVERFLOW_X, + "overflowy" to OVERFLOW_Y, + "overflow-y" to OVERFLOW_Y, + "minwidth" to MIN_WIDTH, + "min-width" to MIN_WIDTH, + "minheight" to MIN_HEIGHT, + "min-height" to MIN_HEIGHT, + "maxwidth" to MAX_WIDTH, + "max-width" to MAX_WIDTH, + "maxheight" to MAX_HEIGHT, + "max-height" to MAX_HEIGHT, + "textwrap" to TEXT_WRAP, + "text-wrap" to TEXT_WRAP, + "textformatting" to TEXT_FORMATTING, + "text-formatting" to TEXT_FORMATTING, + "textdecoration" to TEXT_DECORATION, + "text-decoration" to TEXT_DECORATION, + "obfuscated" to OBFUSCATED, + "transform" to TRANSFORM, + "transformorigin" to TRANSFORM_ORIGIN, + "transform-origin" to TRANSFORM_ORIGIN, + "opacity" to OPACITY, + ) fun fromKeyOrNull(name: String): StyleProperty? = byName[name.trim().lowercase()] } } sealed class StyleExpression { - data class Literal(val value: String) : StyleExpression() - data class VariableRef(val name: String) : StyleExpression() + data class Literal( + val value: String, + ) : StyleExpression() + + data class VariableRef( + val name: String, + ) : StyleExpression() } data class StyleDeclarations( val values: MutableMap = linkedMapOf(), - val importantProperties: MutableSet = linkedSetOf() + val importantProperties: MutableSet = linkedSetOf(), ) { fun set(property: StyleProperty, value: StyleExpression, important: Boolean = false) { values[property] = value @@ -384,7 +398,7 @@ data class ComputedStyle( val textFormatting: TextFormatting, val transform: UiTransform, val transformOrigin: TransformOrigin, - val opacity: Float + val opacity: Float, ) data class ComputedStyleDefaults( @@ -438,10 +452,10 @@ data class ComputedStyleDefaults( val textFormatting: TextFormatting = TextFormatting.None, val transform: UiTransform = UiTransform.IDENTITY, val transformOrigin: TransformOrigin = TransformOrigin.CENTER, - val opacity: Float = 1f + val opacity: Float = 1f, ) { - fun toComputedStyle(): ComputedStyle { - return ComputedStyle( + fun toComputedStyle(): ComputedStyle = + ComputedStyle( margin = margin, padding = padding, backgroundColor = backgroundColor, @@ -492,7 +506,6 @@ data class ComputedStyleDefaults( textFormatting = textFormatting, transform = transform, transformOrigin = transformOrigin, - opacity = opacity + opacity = opacity, ) - } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StylePropertyRegistry.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StylePropertyRegistry.kt index 09dba1e..a7e8849 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StylePropertyRegistry.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StylePropertyRegistry.kt @@ -11,7 +11,7 @@ enum class StyleEditorValueType { Spacing, SpacingLengthPx, ColorHex, - StringPreset + StringPreset, } enum class StyleValueGrammarKind { @@ -19,14 +19,14 @@ enum class StyleValueGrammarKind { UnitlessInt, LengthLike, LineHeight, - Other + Other, } enum class StyleInspectorEditorKind { EnumSelect, FontSelect, NumericInput, - StringInput + StringInput, } data class StylePropertyDescriptor( @@ -38,147 +38,246 @@ data class StylePropertyDescriptor( val minFloat: Float = 0f, val isInherited: Boolean = false, val grammarKind: StyleValueGrammarKind = defaultGrammarKind(valueType), - val inspectorEditorKind: StyleInspectorEditorKind = defaultInspectorEditorKind(property, valueType) + val inspectorEditorKind: StyleInspectorEditorKind = defaultInspectorEditorKind(property, valueType), ) object StylePropertyRegistry { - val all: List = listOf( - StylePropertyDescriptor(StyleProperty.DISPLAY, StyleEditorValueType.EnumChoice, enumOptions = listOf("block", "inline", "none", "flex", "grid")), - StylePropertyDescriptor( - property = StyleProperty.POSITION, - valueType = StyleEditorValueType.EnumChoice, - enumOptions = listOf("static", "relative", "absolute", "fixed", "sticky"), - grammarKind = StyleValueGrammarKind.Enum, - inspectorEditorKind = StyleInspectorEditorKind.EnumSelect - ), - StylePropertyDescriptor( - property = StyleProperty.LEFT, - valueType = StyleEditorValueType.OptionalLengthPx, - numericStep = 1f, - grammarKind = StyleValueGrammarKind.LengthLike, - inspectorEditorKind = StyleInspectorEditorKind.NumericInput - ), - StylePropertyDescriptor( - property = StyleProperty.TOP, - valueType = StyleEditorValueType.OptionalLengthPx, - numericStep = 1f, - grammarKind = StyleValueGrammarKind.LengthLike, - inspectorEditorKind = StyleInspectorEditorKind.NumericInput - ), - StylePropertyDescriptor( - property = StyleProperty.RIGHT, - valueType = StyleEditorValueType.OptionalLengthPx, - numericStep = 1f, - grammarKind = StyleValueGrammarKind.LengthLike, - inspectorEditorKind = StyleInspectorEditorKind.NumericInput - ), - StylePropertyDescriptor( - property = StyleProperty.BOTTOM, - valueType = StyleEditorValueType.OptionalLengthPx, - numericStep = 1f, - grammarKind = StyleValueGrammarKind.LengthLike, - inspectorEditorKind = StyleInspectorEditorKind.NumericInput - ), - StylePropertyDescriptor( - property = StyleProperty.Z_INDEX, - valueType = StyleEditorValueType.IntNumber, - numericStep = 1f, - minInt = Int.MIN_VALUE, - grammarKind = StyleValueGrammarKind.UnitlessInt, - inspectorEditorKind = StyleInspectorEditorKind.NumericInput - ), - StylePropertyDescriptor(StyleProperty.WIDTH, StyleEditorValueType.LengthPx, numericStep = 4f), - StylePropertyDescriptor(StyleProperty.HEIGHT, StyleEditorValueType.LengthPx, numericStep = 4f), - StylePropertyDescriptor(StyleProperty.MIN_WIDTH, StyleEditorValueType.OptionalLengthPx, numericStep = 4f), - StylePropertyDescriptor(StyleProperty.MIN_HEIGHT, StyleEditorValueType.OptionalLengthPx, numericStep = 4f), - StylePropertyDescriptor(StyleProperty.MAX_WIDTH, StyleEditorValueType.OptionalLengthPx, numericStep = 4f), - StylePropertyDescriptor(StyleProperty.MAX_HEIGHT, StyleEditorValueType.OptionalLengthPx, numericStep = 4f), - StylePropertyDescriptor( - StyleProperty.OVERFLOW, - StyleEditorValueType.EnumChoice, - enumOptions = listOf("visible", "hidden", "scroll", "auto") - ), - StylePropertyDescriptor( - StyleProperty.OVERFLOW_X, - StyleEditorValueType.EnumChoice, - enumOptions = listOf("visible", "hidden", "scroll", "auto") - ), - StylePropertyDescriptor( - StyleProperty.OVERFLOW_Y, - StyleEditorValueType.EnumChoice, - enumOptions = listOf("visible", "hidden", "scroll", "auto") - ), - StylePropertyDescriptor(StyleProperty.MARGIN, StyleEditorValueType.SpacingLengthPx, numericStep = 1f), - StylePropertyDescriptor(StyleProperty.PADDING, StyleEditorValueType.SpacingLengthPx, numericStep = 1f), - StylePropertyDescriptor(StyleProperty.BACKGROUND_COLOR, StyleEditorValueType.ColorHex, enumOptions = colorPalette()), - StylePropertyDescriptor( - StyleProperty.BACKGROUND_IMAGE, - StyleEditorValueType.StringPreset, - enumOptions = listOf( - "textures/gui/options_background.png", - "minecraft:textures/blocks/stone.png" - ) - ), - StylePropertyDescriptor(StyleProperty.BORDER_COLOR, StyleEditorValueType.ColorHex, enumOptions = colorPalette()), - StylePropertyDescriptor(StyleProperty.BORDER_WIDTH, StyleEditorValueType.LengthPx, numericStep = 1f), - StylePropertyDescriptor(StyleProperty.BORDER_RADIUS, StyleEditorValueType.LengthPx, numericStep = 1f), - StylePropertyDescriptor(StyleProperty.FOREGROUND_COLOR, StyleEditorValueType.ColorHex, enumOptions = colorPalette(), isInherited = true), - StylePropertyDescriptor( - StyleProperty.FONT_ID, - StyleEditorValueType.StringPreset, - enumOptions = listOf("minecraft", "ubuntu", "JetBrains Mono"), - isInherited = true, - inspectorEditorKind = StyleInspectorEditorKind.FontSelect - ), - StylePropertyDescriptor(StyleProperty.FONT_SIZE, StyleEditorValueType.LengthPx, numericStep = 1f, minInt = 1, isInherited = true), - StylePropertyDescriptor( - property = StyleProperty.LINE_HEIGHT, - valueType = StyleEditorValueType.LineHeight, - enumOptions = listOf("normal"), - numericStep = 1f, - minInt = 0, - isInherited = true, - grammarKind = StyleValueGrammarKind.LineHeight, - inspectorEditorKind = StyleInspectorEditorKind.NumericInput - ), - StylePropertyDescriptor(StyleProperty.FONT_WEIGHT, StyleEditorValueType.EnumChoice, enumOptions = listOf("normal", "bold"), isInherited = true), - StylePropertyDescriptor(StyleProperty.FONT_STYLE, StyleEditorValueType.EnumChoice, enumOptions = listOf("normal", "italic"), isInherited = true), - StylePropertyDescriptor( - StyleProperty.TEXT_DECORATION, - StyleEditorValueType.EnumChoice, - enumOptions = listOf("none", "underline", "strikethrough", "underline-strikethrough") - ), - StylePropertyDescriptor(StyleProperty.OBFUSCATED, StyleEditorValueType.EnumChoice, enumOptions = listOf("false", "true")), - StylePropertyDescriptor(StyleProperty.ALIGN, StyleEditorValueType.EnumChoice, enumOptions = listOf("start", "center", "end")), - StylePropertyDescriptor(StyleProperty.FLEX_DIRECTION, StyleEditorValueType.EnumChoice, enumOptions = listOf("row", "column")), - StylePropertyDescriptor( - StyleProperty.JUSTIFY_CONTENT, - StyleEditorValueType.EnumChoice, - enumOptions = listOf("start", "center", "end", "space-between", "space-around", "space-evenly") - ), - StylePropertyDescriptor(StyleProperty.ALIGN_ITEMS, StyleEditorValueType.EnumChoice, enumOptions = listOf("start", "center", "end", "stretch")), - StylePropertyDescriptor(StyleProperty.JUSTIFY_ITEMS, StyleEditorValueType.EnumChoice, enumOptions = listOf("start", "center", "end", "stretch")), - StylePropertyDescriptor(StyleProperty.GAP, StyleEditorValueType.LengthPx, numericStep = 1f), - StylePropertyDescriptor(StyleProperty.FLEX_GROW, StyleEditorValueType.FloatNumber, numericStep = 0.25f), - StylePropertyDescriptor(StyleProperty.FLEX_SHRINK, StyleEditorValueType.FloatNumber, numericStep = 0.25f), - StylePropertyDescriptor(StyleProperty.FLEX_BASIS, StyleEditorValueType.OptionalLengthPx, numericStep = 4f), - StylePropertyDescriptor(StyleProperty.GRID_COLUMNS, StyleEditorValueType.IntNumber, numericStep = 1f, minInt = 1), - StylePropertyDescriptor(StyleProperty.GRID_ROWS, StyleEditorValueType.OptionalIntNumber, numericStep = 1f, minInt = 1), - StylePropertyDescriptor(StyleProperty.GRID_AUTO_FLOW, StyleEditorValueType.EnumChoice, enumOptions = listOf("row", "column")), - StylePropertyDescriptor(StyleProperty.GRID_COLUMN_SPAN, StyleEditorValueType.IntNumber, numericStep = 1f, minInt = 1), - StylePropertyDescriptor(StyleProperty.GRID_ROW_SPAN, StyleEditorValueType.IntNumber, numericStep = 1f, minInt = 1), - StylePropertyDescriptor(StyleProperty.TEXT_WRAP, StyleEditorValueType.EnumChoice, enumOptions = listOf("wrap", "nowrap")), - StylePropertyDescriptor(StyleProperty.TEXT_FORMATTING, StyleEditorValueType.EnumChoice, enumOptions = listOf("none", "minecraft")), - StylePropertyDescriptor(StyleProperty.TRANSFORM, StyleEditorValueType.StringPreset, enumOptions = listOf("none", "translate(12, 0)", "scale(1.2)", "rotate(15deg)")), - StylePropertyDescriptor(StyleProperty.TRANSFORM_ORIGIN, StyleEditorValueType.StringPreset, enumOptions = listOf("0 0", "0.5 0.5", "1 1", "50% 50%")), - StylePropertyDescriptor(StyleProperty.OPACITY, StyleEditorValueType.FloatNumber, numericStep = 0.05f, minFloat = 0f) - ) + val all: List = + listOf( + StylePropertyDescriptor( + StyleProperty.DISPLAY, + StyleEditorValueType.EnumChoice, + enumOptions = listOf("block", "inline", "none", "flex", "grid"), + ), + StylePropertyDescriptor( + property = StyleProperty.POSITION, + valueType = StyleEditorValueType.EnumChoice, + enumOptions = listOf("static", "relative", "absolute", "fixed", "sticky"), + grammarKind = StyleValueGrammarKind.Enum, + inspectorEditorKind = StyleInspectorEditorKind.EnumSelect, + ), + StylePropertyDescriptor( + property = StyleProperty.LEFT, + valueType = StyleEditorValueType.OptionalLengthPx, + numericStep = 1f, + grammarKind = StyleValueGrammarKind.LengthLike, + inspectorEditorKind = StyleInspectorEditorKind.NumericInput, + ), + StylePropertyDescriptor( + property = StyleProperty.TOP, + valueType = StyleEditorValueType.OptionalLengthPx, + numericStep = 1f, + grammarKind = StyleValueGrammarKind.LengthLike, + inspectorEditorKind = StyleInspectorEditorKind.NumericInput, + ), + StylePropertyDescriptor( + property = StyleProperty.RIGHT, + valueType = StyleEditorValueType.OptionalLengthPx, + numericStep = 1f, + grammarKind = StyleValueGrammarKind.LengthLike, + inspectorEditorKind = StyleInspectorEditorKind.NumericInput, + ), + StylePropertyDescriptor( + property = StyleProperty.BOTTOM, + valueType = StyleEditorValueType.OptionalLengthPx, + numericStep = 1f, + grammarKind = StyleValueGrammarKind.LengthLike, + inspectorEditorKind = StyleInspectorEditorKind.NumericInput, + ), + StylePropertyDescriptor( + property = StyleProperty.Z_INDEX, + valueType = StyleEditorValueType.IntNumber, + numericStep = 1f, + minInt = Int.MIN_VALUE, + grammarKind = StyleValueGrammarKind.UnitlessInt, + inspectorEditorKind = StyleInspectorEditorKind.NumericInput, + ), + StylePropertyDescriptor(StyleProperty.WIDTH, StyleEditorValueType.LengthPx, numericStep = 4f), + StylePropertyDescriptor(StyleProperty.HEIGHT, StyleEditorValueType.LengthPx, numericStep = 4f), + StylePropertyDescriptor(StyleProperty.MIN_WIDTH, StyleEditorValueType.OptionalLengthPx, numericStep = 4f), + StylePropertyDescriptor(StyleProperty.MIN_HEIGHT, StyleEditorValueType.OptionalLengthPx, numericStep = 4f), + StylePropertyDescriptor(StyleProperty.MAX_WIDTH, StyleEditorValueType.OptionalLengthPx, numericStep = 4f), + StylePropertyDescriptor(StyleProperty.MAX_HEIGHT, StyleEditorValueType.OptionalLengthPx, numericStep = 4f), + StylePropertyDescriptor( + StyleProperty.OVERFLOW, + StyleEditorValueType.EnumChoice, + enumOptions = listOf("visible", "hidden", "scroll", "auto"), + ), + StylePropertyDescriptor( + StyleProperty.OVERFLOW_X, + StyleEditorValueType.EnumChoice, + enumOptions = listOf("visible", "hidden", "scroll", "auto"), + ), + StylePropertyDescriptor( + StyleProperty.OVERFLOW_Y, + StyleEditorValueType.EnumChoice, + enumOptions = listOf("visible", "hidden", "scroll", "auto"), + ), + StylePropertyDescriptor(StyleProperty.MARGIN, StyleEditorValueType.SpacingLengthPx, numericStep = 1f), + StylePropertyDescriptor(StyleProperty.PADDING, StyleEditorValueType.SpacingLengthPx, numericStep = 1f), + StylePropertyDescriptor( + StyleProperty.BACKGROUND_COLOR, + StyleEditorValueType.ColorHex, + enumOptions = colorPalette(), + ), + StylePropertyDescriptor( + StyleProperty.BACKGROUND_IMAGE, + StyleEditorValueType.StringPreset, + enumOptions = + listOf( + "textures/gui/options_background.png", + "minecraft:textures/blocks/stone.png", + ), + ), + StylePropertyDescriptor( + StyleProperty.BORDER_COLOR, + StyleEditorValueType.ColorHex, + enumOptions = colorPalette(), + ), + StylePropertyDescriptor(StyleProperty.BORDER_WIDTH, StyleEditorValueType.LengthPx, numericStep = 1f), + StylePropertyDescriptor(StyleProperty.BORDER_RADIUS, StyleEditorValueType.LengthPx, numericStep = 1f), + StylePropertyDescriptor( + StyleProperty.FOREGROUND_COLOR, + StyleEditorValueType.ColorHex, + enumOptions = colorPalette(), + isInherited = true, + ), + StylePropertyDescriptor( + StyleProperty.FONT_ID, + StyleEditorValueType.StringPreset, + enumOptions = listOf("minecraft", "ubuntu", "JetBrains Mono"), + isInherited = true, + inspectorEditorKind = StyleInspectorEditorKind.FontSelect, + ), + StylePropertyDescriptor( + StyleProperty.FONT_SIZE, + StyleEditorValueType.LengthPx, + numericStep = 1f, + minInt = 1, + isInherited = true, + ), + StylePropertyDescriptor( + property = StyleProperty.LINE_HEIGHT, + valueType = StyleEditorValueType.LineHeight, + enumOptions = listOf("normal"), + numericStep = 1f, + minInt = 0, + isInherited = true, + grammarKind = StyleValueGrammarKind.LineHeight, + inspectorEditorKind = StyleInspectorEditorKind.NumericInput, + ), + StylePropertyDescriptor( + StyleProperty.FONT_WEIGHT, + StyleEditorValueType.EnumChoice, + enumOptions = listOf("normal", "bold"), + isInherited = true, + ), + StylePropertyDescriptor( + StyleProperty.FONT_STYLE, + StyleEditorValueType.EnumChoice, + enumOptions = listOf("normal", "italic"), + isInherited = true, + ), + StylePropertyDescriptor( + StyleProperty.TEXT_DECORATION, + StyleEditorValueType.EnumChoice, + enumOptions = listOf("none", "underline", "strikethrough", "underline-strikethrough"), + ), + StylePropertyDescriptor( + StyleProperty.OBFUSCATED, + StyleEditorValueType.EnumChoice, + enumOptions = listOf("false", "true"), + ), + StylePropertyDescriptor( + StyleProperty.ALIGN, + StyleEditorValueType.EnumChoice, + enumOptions = listOf("start", "center", "end"), + ), + StylePropertyDescriptor( + StyleProperty.FLEX_DIRECTION, + StyleEditorValueType.EnumChoice, + enumOptions = listOf("row", "column"), + ), + StylePropertyDescriptor( + StyleProperty.JUSTIFY_CONTENT, + StyleEditorValueType.EnumChoice, + enumOptions = listOf("start", "center", "end", "space-between", "space-around", "space-evenly"), + ), + StylePropertyDescriptor( + StyleProperty.ALIGN_ITEMS, + StyleEditorValueType.EnumChoice, + enumOptions = listOf("start", "center", "end", "stretch"), + ), + StylePropertyDescriptor( + StyleProperty.JUSTIFY_ITEMS, + StyleEditorValueType.EnumChoice, + enumOptions = listOf("start", "center", "end", "stretch"), + ), + StylePropertyDescriptor(StyleProperty.GAP, StyleEditorValueType.LengthPx, numericStep = 1f), + StylePropertyDescriptor(StyleProperty.FLEX_GROW, StyleEditorValueType.FloatNumber, numericStep = 0.25f), + StylePropertyDescriptor(StyleProperty.FLEX_SHRINK, StyleEditorValueType.FloatNumber, numericStep = 0.25f), + StylePropertyDescriptor(StyleProperty.FLEX_BASIS, StyleEditorValueType.OptionalLengthPx, numericStep = 4f), + StylePropertyDescriptor( + StyleProperty.GRID_COLUMNS, + StyleEditorValueType.IntNumber, + numericStep = 1f, + minInt = 1, + ), + StylePropertyDescriptor( + StyleProperty.GRID_ROWS, + StyleEditorValueType.OptionalIntNumber, + numericStep = 1f, + minInt = 1, + ), + StylePropertyDescriptor( + StyleProperty.GRID_AUTO_FLOW, + StyleEditorValueType.EnumChoice, + enumOptions = listOf("row", "column"), + ), + StylePropertyDescriptor( + StyleProperty.GRID_COLUMN_SPAN, + StyleEditorValueType.IntNumber, + numericStep = 1f, + minInt = 1, + ), + StylePropertyDescriptor( + StyleProperty.GRID_ROW_SPAN, + StyleEditorValueType.IntNumber, + numericStep = 1f, + minInt = 1, + ), + StylePropertyDescriptor( + StyleProperty.TEXT_WRAP, + StyleEditorValueType.EnumChoice, + enumOptions = listOf("wrap", "nowrap"), + ), + StylePropertyDescriptor( + StyleProperty.TEXT_FORMATTING, + StyleEditorValueType.EnumChoice, + enumOptions = listOf("none", "minecraft"), + ), + StylePropertyDescriptor( + StyleProperty.TRANSFORM, + StyleEditorValueType.StringPreset, + enumOptions = listOf("none", "translate(12, 0)", "scale(1.2)", "rotate(15deg)"), + ), + StylePropertyDescriptor( + StyleProperty.TRANSFORM_ORIGIN, + StyleEditorValueType.StringPreset, + enumOptions = listOf("0 0", "0.5 0.5", "1 1", "50% 50%"), + ), + StylePropertyDescriptor( + StyleProperty.OPACITY, + StyleEditorValueType.FloatNumber, + numericStep = 0.05f, + minFloat = 0f, + ), + ) private val byProperty: Map = all.associateBy { it.property } - fun descriptor(property: StyleProperty): StylePropertyDescriptor { - return byProperty[property] ?: error("Missing style descriptor for '${property.key}'.") - } + fun descriptor(property: StyleProperty): StylePropertyDescriptor = + byProperty[property] ?: error("Missing style descriptor for '${property.key}'.") fun isInherited(property: StyleProperty): Boolean = descriptor(property).isInherited @@ -188,8 +287,9 @@ object StylePropertyRegistry { "Property '${property.key}' does not use enum grammar." } val normalized = literal.trim().lowercase() - val matched = descriptor.enumOptions.firstOrNull { it.equals(normalized, ignoreCase = true) } - ?: error("Unsupported value '$literal' for '${property.key}'.") + val matched = + descriptor.enumOptions.firstOrNull { it.equals(normalized, ignoreCase = true) } + ?: error("Unsupported value '$literal' for '${property.key}'.") return matched } @@ -209,8 +309,8 @@ object StylePropertyRegistry { return parseLineHeightValue(literal) } - private fun colorPalette(): List { - return listOf( + private fun colorPalette(): List = + listOf( "#FF1B1F24", "#FF2D3748", "#FF4A5568", @@ -222,26 +322,25 @@ object StylePropertyRegistry { "#FFD69E2E", "#FF38A169", "#FF3182CE", - "#FF805AD5" + "#FF805AD5", ) - } } -private fun defaultGrammarKind(valueType: StyleEditorValueType): StyleValueGrammarKind { - return when (valueType) { +private fun defaultGrammarKind(valueType: StyleEditorValueType): StyleValueGrammarKind = + when (valueType) { StyleEditorValueType.EnumChoice -> StyleValueGrammarKind.Enum StyleEditorValueType.IntNumber -> StyleValueGrammarKind.UnitlessInt StyleEditorValueType.LineHeight -> StyleValueGrammarKind.LineHeight StyleEditorValueType.LengthPx, StyleEditorValueType.OptionalLengthPx, - StyleEditorValueType.SpacingLengthPx -> StyleValueGrammarKind.LengthLike + StyleEditorValueType.SpacingLengthPx, + -> StyleValueGrammarKind.LengthLike else -> StyleValueGrammarKind.Other } -} private fun defaultInspectorEditorKind( property: StyleProperty, - valueType: StyleEditorValueType + valueType: StyleEditorValueType, ): StyleInspectorEditorKind { if (property == StyleProperty.FONT_ID) { return StyleInspectorEditorKind.FontSelect @@ -255,7 +354,8 @@ private fun defaultInspectorEditorKind( StyleEditorValueType.LengthPx, StyleEditorValueType.OptionalLengthPx, StyleEditorValueType.Spacing, - StyleEditorValueType.SpacingLengthPx -> StyleInspectorEditorKind.NumericInput + StyleEditorValueType.SpacingLengthPx, + -> StyleInspectorEditorKind.NumericInput else -> StyleInspectorEditorKind.StringInput } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleSelector.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleSelector.kt index e9d9464..76c297e 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleSelector.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleSelector.kt @@ -4,13 +4,13 @@ enum class StyleCombinator { Descendant, Child, AdjacentSibling, - GeneralSibling + GeneralSibling, } data class StyleSpecificity( val idCount: Int = 0, val classLikeCount: Int = 0, - val typeCount: Int = 0 + val typeCount: Int = 0, ) : Comparable { override fun compareTo(other: StyleSpecificity): Int { if (idCount != other.idCount) return idCount.compareTo(other.idCount) @@ -18,13 +18,12 @@ data class StyleSpecificity( return typeCount.compareTo(other.typeCount) } - operator fun plus(other: StyleSpecificity): StyleSpecificity { - return StyleSpecificity( + operator fun plus(other: StyleSpecificity): StyleSpecificity = + StyleSpecificity( idCount = idCount + other.idCount, classLikeCount = classLikeCount + other.classLikeCount, - typeCount = typeCount + other.typeCount + typeCount = typeCount + other.typeCount, ) - } } data class StyleSelectorPart( @@ -32,30 +31,29 @@ data class StyleSelectorPart( val classes: Set = emptySet(), val id: String? = null, val pseudoState: StylePseudoState? = null, - val universal: Boolean = false + val universal: Boolean = false, ) { init { require( - universal || typeName != null || id != null || classes.isNotEmpty() || pseudoState != null + universal || typeName != null || id != null || classes.isNotEmpty() || pseudoState != null, ) { "Selector part must not be empty." } } - fun specificity(): StyleSpecificity { - return StyleSpecificity( + fun specificity(): StyleSpecificity = + StyleSpecificity( idCount = if (id != null) 1 else 0, classLikeCount = classes.size + if (pseudoState != null) 1 else 0, - typeCount = if (typeName != null) 1 else 0 + typeCount = if (typeName != null) 1 else 0, ) - } } data class StyleSelectorStep( val part: StyleSelectorPart, - val combinatorToLeft: StyleCombinator? = null + val combinatorToLeft: StyleCombinator? = null, ) data class StyleSelector( - val steps: List + val steps: List, ) { init { require(steps.isNotEmpty()) { "Selector must contain at least one step." } @@ -67,17 +65,41 @@ data class StyleSelector( val hasCombinators: Boolean = steps.size > 1 val typeName: String? - get() = if (steps.size == 1) steps.first().part.typeName else null + get() = + if (steps.size == 1) { + steps + .first() + .part.typeName + } else { + null + } val className: String? get() { if (steps.size != 1) return null - val classes = steps.first().part.classes + val classes = + steps + .first() + .part.classes return if (classes.size == 1) classes.first() else null } val id: String? - get() = if (steps.size == 1) steps.first().part.id else null + get() = + if (steps.size == 1) { + steps + .first() + .part.id + } else { + null + } val pseudoState: StylePseudoState? - get() = if (steps.size == 1) steps.first().part.pseudoState else null + get() = + if (steps.size == 1) { + steps + .first() + .part.pseudoState + } else { + null + } fun rightMostPart(): StyleSelectorPart = steps.last().part @@ -108,12 +130,13 @@ data class StyleSelector( if (trimmed[index] == '>' || trimmed[index] == '+' || trimmed[index] == '~') { require(steps.isNotEmpty()) { "Selector cannot start with a combinator." } - pendingCombinator = when (trimmed[index]) { - '>' -> StyleCombinator.Child - '+' -> StyleCombinator.AdjacentSibling - '~' -> StyleCombinator.GeneralSibling - else -> error("Unsupported combinator") - } + pendingCombinator = + when (trimmed[index]) { + '>' -> StyleCombinator.Child + '+' -> StyleCombinator.AdjacentSibling + '~' -> StyleCombinator.GeneralSibling + else -> error("Unsupported combinator") + } index++ continue } @@ -129,10 +152,17 @@ data class StyleSelector( index++ } val token = trimmed.substring(tokenStart, index) - val step = StyleSelectorStep( - part = parsePartToken(token, rawSelector), - combinatorToLeft = if (steps.isEmpty()) null else pendingCombinator ?: StyleCombinator.Descendant - ) + val step = + StyleSelectorStep( + part = parsePartToken(token, rawSelector), + combinatorToLeft = + if (steps.isEmpty()) { + null + } else { + pendingCombinator + ?: StyleCombinator.Descendant + }, + ) steps += step pendingCombinator = null } @@ -155,7 +185,9 @@ data class StyleSelector( index++ } else if (token[index].isLetter()) { val typeStart = index - while (index < token.length && (token[index].isLetterOrDigit() || token[index] == '_' || token[index] == '-')) { + while (index < token.length && + (token[index].isLetterOrDigit() || token[index] == '_' || token[index] == '-') + ) { index++ } val typeCandidate = token.substring(typeStart, index) @@ -168,7 +200,9 @@ data class StyleSelector( '.' -> { index++ val classStart = index - while (index < token.length && (token[index].isLetterOrDigit() || token[index] == '_' || token[index] == '-')) { + while (index < token.length && + (token[index].isLetterOrDigit() || token[index] == '_' || token[index] == '-') + ) { index++ } val classValue = token.substring(classStart, index) @@ -179,7 +213,9 @@ data class StyleSelector( '#' -> { index++ val idStart = index - while (index < token.length && (token[index].isLetterOrDigit() || token[index] == '_' || token[index] == '-')) { + while (index < token.length && + (token[index].isLetterOrDigit() || token[index] == '_' || token[index] == '-') + ) { index++ } val idValue = token.substring(idStart, index) @@ -193,14 +229,17 @@ data class StyleSelector( while (index < token.length && token[index].isLetter()) { index++ } - val parsedPseudo = when (val pseudoValue = token.substring(pseudoStart, index).lowercase()) { - "hover" -> StylePseudoState.HOVER - "active" -> StylePseudoState.ACTIVE - "focus" -> StylePseudoState.FOCUS - "disabled" -> StylePseudoState.DISABLED - "open" -> StylePseudoState.OPEN - else -> throw IllegalArgumentException("Unsupported pseudo-state '$pseudoValue' in '$rawSelector'.") - } + val parsedPseudo = + when (val pseudoValue = token.substring(pseudoStart, index).lowercase()) { + "hover" -> StylePseudoState.HOVER + "active" -> StylePseudoState.ACTIVE + "focus" -> StylePseudoState.FOCUS + "disabled" -> StylePseudoState.DISABLED + "open" -> StylePseudoState.OPEN + else -> throw IllegalArgumentException( + "Unsupported pseudo-state '$pseudoValue' in '$rawSelector'.", + ) + } pseudoState = parsedPseudo } @@ -213,7 +252,7 @@ data class StyleSelector( classes = classes, id = id, pseudoState = pseudoState, - universal = universal + universal = universal, ) } } @@ -223,12 +262,12 @@ data class StyleRule( val selector: StyleSelector, val declarations: StyleDeclarations, val sourceOrder: Int, - val fileName: String + val fileName: String, ) data class StylesheetData( val rules: List, val rootVariables: Map, val source: String, - val warnings: List = emptyList() + val warnings: List = emptyList(), ) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleValueParsing.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleValueParsing.kt index b3a8f0d..12e022c 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleValueParsing.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleValueParsing.kt @@ -17,32 +17,33 @@ fun parseExpression(rawValue: String): StyleExpression { } } -fun parseSpacingShorthand(raw: String): Insets { - return parseSpacingLengthShorthand( +fun parseSpacingShorthand(raw: String): Insets = + parseSpacingLengthShorthand( raw = raw, - allowNegative = true + allowNegative = true, ).resolveToInsets(LengthResolveContext()) -} fun parseSpacingLengthShorthand( raw: String, allowNegative: Boolean, allowUnitlessZero: Boolean = true, warningReporter: StyleWarningReporter? = null, - warningKey: String = "deprecated.unitless-length" + warningKey: String = "deprecated.unitless-length", ): LengthInsets { val parts = splitLengthTokens(raw) require(parts.isNotEmpty()) { "Spacing value cannot be empty." } - val values = parts.map { - val length = parseCssLength( - raw = it, - allowUnitlessZero = allowUnitlessZero - ) - if (!allowNegative && length.value < 0f) { - error("Negative length is not allowed: '$it'.") + val values = + parts.map { + val length = + parseCssLength( + raw = it, + allowUnitlessZero = allowUnitlessZero, + ) + if (!allowNegative && length.value < 0f) { + error("Negative length is not allowed: '$it'.") + } + length } - length - } return when (values.size) { 1 -> LengthInsets.all(values[0]) 2 -> LengthInsets(values[0], values[1], values[0], values[1]) @@ -57,16 +58,15 @@ fun parseSpacingShorthand( allowNegative: Boolean, allowUnitlessPx: Boolean = true, warningReporter: StyleWarningReporter? = null, - warningKey: String = "deprecated.unitless-length" -): Insets { - return parseSpacingLengthShorthand( + warningKey: String = "deprecated.unitless-length", +): Insets = + parseSpacingLengthShorthand( raw = raw, allowNegative = allowNegative, allowUnitlessZero = allowUnitlessPx, warningReporter = warningReporter, - warningKey = warningKey + warningKey = warningKey, ).resolveToInsets(LengthResolveContext()) -} fun parseColor(raw: String): Int { val value = raw.trim() @@ -90,17 +90,16 @@ fun parseColor(raw: String): Int { } } -fun parseAlign(raw: String): StyleAlign { - return when (raw.trim().lowercase()) { +fun parseAlign(raw: String): StyleAlign = + when (raw.trim().lowercase()) { "start", "left", "top" -> StyleAlign.START "center", "middle" -> StyleAlign.CENTER "end", "right", "bottom" -> StyleAlign.END else -> error("Unsupported align value '$raw'.") } -} -fun parseDisplay(raw: String): Display { - return when (raw.trim().lowercase()) { +fun parseDisplay(raw: String): Display = + when (raw.trim().lowercase()) { "block" -> Display.Block "inline" -> Display.Inline "none" -> Display.None @@ -108,10 +107,9 @@ fun parseDisplay(raw: String): Display { "grid" -> Display.Grid else -> error("Unsupported display value '$raw'.") } -} -fun parsePosition(raw: String): PositionMode { - return when (raw.trim().lowercase()) { +fun parsePosition(raw: String): PositionMode = + when (raw.trim().lowercase()) { "static" -> PositionMode.Static "relative" -> PositionMode.Relative "absolute" -> PositionMode.Absolute @@ -119,7 +117,6 @@ fun parsePosition(raw: String): PositionMode { "sticky" -> PositionMode.Sticky else -> error("Unsupported position value '$raw'.") } -} fun parseLineHeightValue(raw: String): LineHeightValue { val normalized = raw.trim().lowercase() @@ -133,41 +130,43 @@ fun parseLineHeightValue(raw: String): LineHeightValue { data class OverflowAxes( val overflowX: Overflow, - val overflowY: Overflow + val overflowY: Overflow, ) -fun parseOverflow(raw: String): Overflow { - return when (raw.trim().lowercase()) { +fun parseOverflow(raw: String): Overflow = + when (raw.trim().lowercase()) { "visible" -> Overflow.Visible "hidden" -> Overflow.Hidden "scroll" -> Overflow.Scroll "auto" -> Overflow.Auto else -> error("Unsupported overflow value '$raw'.") } -} fun parseOverflowShorthand(raw: String): OverflowAxes { - val parts = raw.trim().split(Regex("\\s+")).filter { it.isNotBlank() } + val parts = + raw + .trim() + .split(Regex("\\s+")) + .filter { it.isNotBlank() } require(parts.isNotEmpty()) { "overflow value cannot be empty." } require(parts.size <= 2) { "overflow supports one or two values." } val overflowX = parseOverflow(parts[0]) val overflowY = if (parts.size == 2) parseOverflow(parts[1]) else overflowX return OverflowAxes( overflowX = overflowX, - overflowY = overflowY + overflowY = overflowY, ) } -fun parseFlexDirection(raw: String): FlexDirection { - return when (raw.trim().lowercase()) { +fun parseFlexDirection(raw: String): FlexDirection = + when (raw.trim().lowercase()) { "row" -> FlexDirection.Row "column" -> FlexDirection.Column else -> error("Unsupported flex-direction value '$raw'.") } -} -fun parseJustifyContent(raw: String): JustifyContent { - return when (raw.trim().lowercase()) { +fun parseJustifyContent(raw: String): JustifyContent = + when (raw.trim().lowercase()) { "start", "flex-start", "left", "top" -> JustifyContent.Start "center" -> JustifyContent.Center "end", "flex-end", "right", "bottom" -> JustifyContent.End @@ -176,70 +175,62 @@ fun parseJustifyContent(raw: String): JustifyContent { "space-evenly" -> JustifyContent.SpaceEvenly else -> error("Unsupported justify-content value '$raw'.") } -} -fun parseAlignItems(raw: String): AlignItems { - return when (raw.trim().lowercase()) { +fun parseAlignItems(raw: String): AlignItems = + when (raw.trim().lowercase()) { "start", "flex-start", "top", "left" -> AlignItems.Start "center" -> AlignItems.Center "end", "flex-end", "bottom", "right" -> AlignItems.End "stretch" -> AlignItems.Stretch else -> error("Unsupported align-items value '$raw'.") } -} -fun parseJustifyItems(raw: String): JustifyItems { - return when (raw.trim().lowercase()) { +fun parseJustifyItems(raw: String): JustifyItems = + when (raw.trim().lowercase()) { "start", "left", "top" -> JustifyItems.Start "center" -> JustifyItems.Center "end", "right", "bottom" -> JustifyItems.End "stretch" -> JustifyItems.Stretch else -> error("Unsupported justify-items value '$raw'.") } -} -fun parseGridAutoFlow(raw: String): GridAutoFlow { - return when (raw.trim().lowercase()) { +fun parseGridAutoFlow(raw: String): GridAutoFlow = + when (raw.trim().lowercase()) { "row" -> GridAutoFlow.Row "column" -> GridAutoFlow.Column else -> error("Unsupported grid-auto-flow value '$raw'.") } -} -fun parseTextWrap(raw: String): TextWrap { - return when (raw.trim().lowercase()) { +fun parseTextWrap(raw: String): TextWrap = + when (raw.trim().lowercase()) { "wrap" -> TextWrap.Wrap "nowrap" -> TextWrap.NoWrap else -> error("Unsupported text-wrap value '$raw'.") } -} -fun parseTextFormatting(raw: String): TextFormatting { - return when (raw.trim().lowercase()) { +fun parseTextFormatting(raw: String): TextFormatting = + when (raw.trim().lowercase()) { "none" -> TextFormatting.None "minecraft" -> TextFormatting.Minecraft else -> error("Unsupported text-formatting value '$raw'.") } -} -fun parseFontWeight(raw: String): FontWeight { - return when (raw.trim().lowercase()) { +fun parseFontWeight(raw: String): FontWeight = + when (raw.trim().lowercase()) { "normal" -> FontWeight.Normal "bold" -> FontWeight.Bold else -> error("Unsupported font-weight value '$raw'.") } -} -fun parseFontStyle(raw: String): FontStyle { - return when (raw.trim().lowercase()) { +fun parseFontStyle(raw: String): FontStyle = + when (raw.trim().lowercase()) { "normal" -> FontStyle.Normal "italic" -> FontStyle.Italic else -> error("Unsupported font-style value '$raw'.") } -} -fun parseTextDecoration(raw: String): TextDecoration { - return when (raw.trim().lowercase()) { +fun parseTextDecoration(raw: String): TextDecoration = + when (raw.trim().lowercase()) { "none" -> TextDecoration.None "underline" -> TextDecoration.Underline "strikethrough", "line-through" -> TextDecoration.Strikethrough @@ -249,15 +240,13 @@ fun parseTextDecoration(raw: String): TextDecoration { else -> error("Unsupported text-decoration value '$raw'.") } -} -fun parseBooleanLike(raw: String): Boolean { - return when (raw.trim().lowercase()) { +fun parseBooleanLike(raw: String): Boolean = + when (raw.trim().lowercase()) { "true", "1", "yes", "on" -> true "false", "0", "no", "off" -> false else -> error("Expected boolean but got '$raw'.") } -} fun parseTransform(raw: String): UiTransform { val input = raw.trim() @@ -270,40 +259,49 @@ fun parseTransform(raw: String): UiTransform { require(matches.isNotEmpty()) { "Expected transform functions, got '$raw'." } matches.forEach { match -> - val fn = match.groupValues[1].trim().lowercase() - val args = match.groupValues[2] - .split(Regex("[,\\s]+")) - .map { it.trim() } - .filter { it.isNotBlank() } - transform = when (fn) { - "translate" -> { - require(args.size in 1..2) { "translate expects 1 or 2 arguments." } - val tx = parseFloatLike(args[0]) - val ty = if (args.size >= 2) parseFloatLike(args[1]) else 0f - transform.translated(tx, ty) + val fn = + match.groupValues[1] + .trim() + .lowercase() + val args = + match.groupValues[2] + .split(Regex("[,\\s]+")) + .map { it.trim() } + .filter { it.isNotBlank() } + transform = + when (fn) { + "translate" -> { + require(args.size in 1..2) { "translate expects 1 or 2 arguments." } + val tx = parseFloatLike(args[0]) + val ty = if (args.size >= 2) parseFloatLike(args[1]) else 0f + transform.translated(tx, ty) + } + + "scale" -> { + require(args.size in 1..2) { "scale expects 1 or 2 arguments." } + val sx = parseFloatLike(args[0]) + val sy = if (args.size >= 2) parseFloatLike(args[1]) else sx + transform.scaled(sx, sy) + } + + "rotate" -> { + require(args.size == 1) { "rotate expects 1 argument." } + val normalized = args[0].removeSuffix("deg").trim() + transform.rotated(parseFloatLike(normalized)) + } + + else -> error("Unsupported transform function '$fn'.") } - - "scale" -> { - require(args.size in 1..2) { "scale expects 1 or 2 arguments." } - val sx = parseFloatLike(args[0]) - val sy = if (args.size >= 2) parseFloatLike(args[1]) else sx - transform.scaled(sx, sy) - } - - "rotate" -> { - require(args.size == 1) { "rotate expects 1 argument." } - val normalized = args[0].removeSuffix("deg").trim() - transform.rotated(parseFloatLike(normalized)) - } - - else -> error("Unsupported transform function '$fn'.") - } } return transform } fun parseTransformOrigin(raw: String): TransformOrigin { - val parts = raw.trim().split(Regex("\\s+")).filter { it.isNotBlank() } + val parts = + raw + .trim() + .split(Regex("\\s+")) + .filter { it.isNotBlank() } require(parts.size == 2) { "transform-origin expects exactly two values." } val ox = parseOriginComponent(parts[0]) val oy = parseOriginComponent(parts[1]) @@ -320,9 +318,7 @@ private fun parseOriginComponent(raw: String): Float { } } -fun parseOpacity(raw: String): Float { - return parseFloatLike(raw).coerceIn(0f, 1f) -} +fun parseOpacity(raw: String): Float = parseFloatLike(raw).coerceIn(0f, 1f) fun parseIntLike(raw: String): Int { val trimmed = raw.trim() @@ -341,9 +337,11 @@ fun parseOptionalInt(raw: String): Int? { return if (normalized == "auto") null else parseIntLike(raw) } -private fun splitLengthTokens(raw: String): List { - return raw.trim().split(Regex("\\s+")).filter { it.isNotBlank() } -} +private fun splitLengthTokens(raw: String): List = + raw + .trim() + .split(Regex("\\s+")) + .filter { it.isNotBlank() } fun parseStringLiteral(raw: String): String { val trimmed = raw.trim() @@ -360,44 +358,50 @@ fun validateLiteralForProperty( property: StyleProperty, literal: String, warningReporter: StyleWarningReporter? = null, - deprecatedLengthWarningKey: String = "deprecated.unitless-length" + deprecatedLengthWarningKey: String = "deprecated.unitless-length", ) { when (property) { - StyleProperty.MARGIN -> parseSpacingLengthShorthand( - raw = literal, - allowNegative = true, - allowUnitlessZero = true, - warningReporter = warningReporter, - warningKey = deprecatedLengthWarningKey - ) - StyleProperty.PADDING -> parseSpacingLengthShorthand( - raw = literal, - allowNegative = false, - allowUnitlessZero = true, - warningReporter = warningReporter, - warningKey = deprecatedLengthWarningKey - ) + StyleProperty.MARGIN -> + parseSpacingLengthShorthand( + raw = literal, + allowNegative = true, + allowUnitlessZero = true, + warningReporter = warningReporter, + warningKey = deprecatedLengthWarningKey, + ) + StyleProperty.PADDING -> + parseSpacingLengthShorthand( + raw = literal, + allowNegative = false, + allowUnitlessZero = true, + warningReporter = warningReporter, + warningKey = deprecatedLengthWarningKey, + ) StyleProperty.BACKGROUND_COLOR, StyleProperty.BORDER_COLOR, - StyleProperty.FOREGROUND_COLOR -> parseColor(literal) + StyleProperty.FOREGROUND_COLOR, + -> parseColor(literal) StyleProperty.BACKGROUND_IMAGE -> parseStringLiteral(literal) StyleProperty.FONT_ID -> parseStringLiteral(literal) - StyleProperty.BORDER_WIDTH -> validateLengthLiteral( - literal = literal, - allowNegative = false - ) - StyleProperty.BORDER_RADIUS -> validateLengthLiteral( - literal = literal, - allowNegative = false - ) - StyleProperty.FONT_SIZE -> { - val parsed = parseCssLength( - raw = literal, - allowUnitlessZero = true + StyleProperty.BORDER_WIDTH -> + validateLengthLiteral( + literal = literal, + allowNegative = false, ) + StyleProperty.BORDER_RADIUS -> + validateLengthLiteral( + literal = literal, + allowNegative = false, + ) + StyleProperty.FONT_SIZE -> { + val parsed = + parseCssLength( + raw = literal, + allowUnitlessZero = true, + ) require(parsed.value >= 0f) { "font-size must be non-negative." } } StyleProperty.LINE_HEIGHT -> StylePropertyRegistry.parseLineHeightLiteral(property, literal) @@ -406,7 +410,8 @@ fun validateLiteralForProperty( StyleProperty.MIN_WIDTH, StyleProperty.MIN_HEIGHT, StyleProperty.MAX_WIDTH, - StyleProperty.MAX_HEIGHT -> { + StyleProperty.MAX_HEIGHT, + -> { val parsed = parseOptionalCssLength(literal) if (parsed != null && parsed.value < 0f) { error("Negative length is not allowed: '$literal'.") @@ -419,11 +424,13 @@ fun validateLiteralForProperty( StyleProperty.LEFT, StyleProperty.TOP, StyleProperty.RIGHT, - StyleProperty.BOTTOM -> parseOptionalCssLength(literal) + StyleProperty.BOTTOM, + -> parseOptionalCssLength(literal) StyleProperty.Z_INDEX -> StylePropertyRegistry.parseUnitlessIntLiteral(property, literal) StyleProperty.OVERFLOW -> parseOverflowShorthand(literal) StyleProperty.OVERFLOW_X, - StyleProperty.OVERFLOW_Y -> parseOverflow(literal) + StyleProperty.OVERFLOW_Y, + -> parseOverflow(literal) StyleProperty.FLEX_DIRECTION -> parseFlexDirection(literal) StyleProperty.JUSTIFY_CONTENT -> parseJustifyContent(literal) StyleProperty.ALIGN_ITEMS -> parseAlignItems(literal) @@ -454,10 +461,7 @@ fun validateLiteralForProperty( } } -private fun validateLengthLiteral( - literal: String, - allowNegative: Boolean -) { +private fun validateLengthLiteral(literal: String, allowNegative: Boolean) { val parsed = parseCssLength(literal) if (!allowNegative && parsed.value < 0f) { error("Negative length is not allowed: '$literal'.") @@ -467,9 +471,9 @@ private fun validateLengthLiteral( fun resolveExpressionToLiteral( expression: StyleExpression, variables: Map, - resolving: MutableSet = linkedSetOf() -): String { - return when (expression) { + resolving: MutableSet = linkedSetOf(), +): String = + when (expression) { is StyleExpression.Literal -> expression.value is StyleExpression.VariableRef -> { val varName = expression.name @@ -483,4 +487,3 @@ fun resolveExpressionToLiteral( resolved } } -} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StylesheetManager.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StylesheetManager.kt index 080e6b7..4c8c21c 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StylesheetManager.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StylesheetManager.kt @@ -6,7 +6,7 @@ import java.util.concurrent.ConcurrentHashMap data class StylesheetSnapshot( val version: Long, val index: RuleIndex, - val rootVariables: Map + val rootVariables: Map, ) data class RuleIndex( @@ -16,18 +16,19 @@ data class RuleIndex( val universalIndex: List, val hasAncestorDependentSelectors: Boolean, val hasAdjacentSiblingCombinators: Boolean, - val hasGeneralSiblingCombinators: Boolean + val hasGeneralSiblingCombinators: Boolean, ) { companion object { - val EMPTY = RuleIndex( - typeIndex = emptyMap(), - classIndex = emptyMap(), - idIndex = emptyMap(), - universalIndex = emptyList(), - hasAncestorDependentSelectors = false, - hasAdjacentSiblingCombinators = false, - hasGeneralSiblingCombinators = false - ) + val EMPTY = + RuleIndex( + typeIndex = emptyMap(), + classIndex = emptyMap(), + idIndex = emptyMap(), + universalIndex = emptyList(), + hasAncestorDependentSelectors = false, + hasAdjacentSiblingCombinators = false, + hasGeneralSiblingCombinators = false, + ) } } @@ -35,7 +36,7 @@ object StylesheetManager { private data class LoadedSheet( val file: File, val lastModified: Long, - val data: StylesheetData + val data: StylesheetData, ) private var stylesDir: File? = null @@ -85,10 +86,16 @@ object StylesheetManager { lastDirectoryScanMillis = now scannedOnce = true - val currentFiles = dir.walkTopDown() - .filter { it.isFile && it.extension.equals("dss", ignoreCase = true) } - .sortedBy { it.relativeTo(dir).path.replace('\\', '/') } - .toList() + val currentFiles = + dir + .walkTopDown() + .filter { it.isFile && it.extension.equals("dss", ignoreCase = true) } + .sortedBy { + it + .relativeTo(dir) + .path + .replace('\\', '/') + }.toList() val currentPaths = currentFiles.map { it.absolutePath }.toSet() val removedPaths = loadedSheets.keys.filter { it !in currentPaths } @@ -100,11 +107,11 @@ object StylesheetManager { val lastModified = file.lastModified() val loaded = loadedSheets[key] if (force || loaded == null || loaded.lastModified != lastModified) { - val parsed = runCatching { DssParser.parse(file) } - .onFailure { error -> - println("[DSGL-Style] Parse error in ${file.path}: ${error.message}") - } - .getOrNull() + val parsed = + runCatching { DssParser.parse(file) } + .onFailure { error -> + println("[DSGL-Style] Parse error in ${file.path}: ${error.message}") + }.getOrNull() if (parsed != null) { loadedSheets[key] = LoadedSheet(file, lastModified, parsed) @@ -123,14 +130,18 @@ object StylesheetManager { } @Synchronized - fun snapshot(): StylesheetSnapshot { - return snapshot - } + fun snapshot(): StylesheetSnapshot = snapshot @Synchronized private fun rebuildSnapshot(baseDir: File) { - val orderedSheets = loadedSheets.values - .sortedBy { it.file.relativeTo(baseDir).path.replace('\\', '/') } + val orderedSheets = + loadedSheets.values + .sortedBy { + it.file + .relativeTo(baseDir) + .path + .replace('\\', '/') + } var sourceOrder = 0 val rules = mutableListOf() @@ -141,18 +152,20 @@ object StylesheetManager { rootVars[name] = value } loaded.data.rules.forEach { rule -> - rules += rule.copy( - sourceOrder = sourceOrder++, - fileName = loaded.file.path - ) + rules += + rule.copy( + sourceOrder = sourceOrder++, + fileName = loaded.file.path, + ) } } - snapshot = StylesheetSnapshot( - version = ++versionCounter, - index = buildIndex(rules), - rootVariables = rootVars.toMap() - ) + snapshot = + StylesheetSnapshot( + version = ++versionCounter, + index = buildIndex(rules), + rootVariables = rootVars.toMap(), + ) } private fun buildIndex(rules: List): RuleIndex { @@ -205,7 +218,7 @@ object StylesheetManager { universalIndex = universalIndex.toList(), hasAncestorDependentSelectors = hasAncestorDependentSelectors, hasAdjacentSiblingCombinators = hasAdjacentSiblingCombinators, - hasGeneralSiblingCombinators = hasGeneralSiblingCombinators + hasGeneralSiblingCombinators = hasGeneralSiblingCombinators, ) } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/text/MinecraftFormattingParser.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/text/MinecraftFormattingParser.kt index 7d107b9..6cb5477 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/text/MinecraftFormattingParser.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/text/MinecraftFormattingParser.kt @@ -7,10 +7,10 @@ data class TextFormattingFlags( val bold: Boolean = false, val strikethrough: Boolean = false, val underline: Boolean = false, - val italic: Boolean = false + val italic: Boolean = false, ) { - fun withLegacyCode(code: Char): TextFormattingFlags { - return when (code.lowercaseChar()) { + fun withLegacyCode(code: Char): TextFormattingFlags = + when (code.lowercaseChar()) { 'k' -> copy(obfuscated = true) 'l' -> copy(bold = true) 'm' -> copy(strikethrough = true) @@ -18,7 +18,6 @@ data class TextFormattingFlags( 'o' -> copy(italic = true) else -> this } - } companion object { val NONE: TextFormattingFlags = TextFormattingFlags() @@ -29,18 +28,18 @@ data class ParsedTextSpan( val start: Int, val end: Int, val colorRgb: Int?, - val flags: TextFormattingFlags + val flags: TextFormattingFlags, ) data class ParsedText( val plainText: String, - val spans: List + val spans: List, ) data class ResolvedTextColorSpan( val start: Int, val end: Int, - val color: Int + val color: Int, ) data class TextStyleFlags( @@ -48,7 +47,7 @@ data class TextStyleFlags( val italic: Boolean = false, val underline: Boolean = false, val strikethrough: Boolean = false, - val obfuscated: Boolean = false + val obfuscated: Boolean = false, ) { companion object { val NONE: TextStyleFlags = TextStyleFlags() @@ -59,28 +58,29 @@ data class ResolvedTextStyleSpan( val start: Int, val end: Int, val color: Int, - val flags: TextStyleFlags + val flags: TextStyleFlags, ) object MinecraftLegacyColors { - private val colorsByCode: Map = mapOf( - '0' to 0x000000, - '1' to 0x0000AA, - '2' to 0x00AA00, - '3' to 0x00AAAA, - '4' to 0xAA0000, - '5' to 0xAA00AA, - '6' to 0xFFAA00, - '7' to 0xAAAAAA, - '8' to 0x555555, - '9' to 0x5555FF, - 'a' to 0x55FF55, - 'b' to 0x55FFFF, - 'c' to 0xFF5555, - 'd' to 0xFF55FF, - 'e' to 0xFFFF55, - 'f' to 0xFFFFFF - ) + private val colorsByCode: Map = + mapOf( + '0' to 0x000000, + '1' to 0x0000AA, + '2' to 0x00AA00, + '3' to 0x00AAAA, + '4' to 0xAA0000, + '5' to 0xAA00AA, + '6' to 0xFFAA00, + '7' to 0xAAAAAA, + '8' to 0x555555, + '9' to 0x5555FF, + 'a' to 0x55FF55, + 'b' to 0x55FFFF, + 'c' to 0xFF5555, + 'd' to 0xFF55FF, + 'e' to 0xFFFF55, + 'f' to 0xFFFFFF, + ) fun rgb(code: Char): Int? = colorsByCode[code.lowercaseChar()] } @@ -88,21 +88,20 @@ object MinecraftLegacyColors { object MinecraftFormattingParser { private data class CacheKey( val text: String, - val mode: TextFormatting + val mode: TextFormatting, ) private data class SpanStyle( val colorRgb: Int? = null, - val flags: TextFormattingFlags = TextFormattingFlags.NONE + val flags: TextFormattingFlags = TextFormattingFlags.NONE, ) private const val PREFIX: Char = '\u00A7' private const val MAX_CACHE_SIZE: Int = 512 private val cache: MutableMap = object : LinkedHashMap(128, 0.75f, true) { - override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean { - return size > MAX_CACHE_SIZE - } + override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean = + size > MAX_CACHE_SIZE } fun clearCache() { @@ -121,10 +120,11 @@ object MinecraftFormattingParser { cache[key]?.let { return it } } - val parsed = when (mode) { - TextFormatting.None -> ParsedText(plainText = text, spans = emptyList()) - TextFormatting.Minecraft -> parseMinecraft(text) - } + val parsed = + when (mode) { + TextFormatting.None -> ParsedText(plainText = text, spans = emptyList()) + TextFormatting.Minecraft -> parseMinecraft(text) + } synchronized(cache) { cache[key] = parsed @@ -136,7 +136,7 @@ object MinecraftFormattingParser { parsed: ParsedText, baseColor: Int, rangeStart: Int = 0, - rangeEnd: Int = parsed.plainText.length + rangeEnd: Int = parsed.plainText.length, ): List { if (rangeEnd <= rangeStart || parsed.spans.isEmpty()) return emptyList() val safeStart = rangeStart.coerceIn(0, parsed.plainText.length) @@ -151,11 +151,12 @@ object MinecraftFormattingParser { val rgb = span.colorRgb ?: return@forEach val alpha = (baseColor ushr 24) and 0xFF val argb = (alpha shl 24) or (rgb and 0x00FF_FFFF) - out += ResolvedTextColorSpan( - start = start - safeStart, - end = end - safeStart, - color = argb - ) + out += + ResolvedTextColorSpan( + start = start - safeStart, + end = end - safeStart, + color = argb, + ) } return out } @@ -165,7 +166,7 @@ object MinecraftFormattingParser { baseColor: Int, baseFlags: TextStyleFlags, rangeStart: Int = 0, - rangeEnd: Int = parsed.plainText.length + rangeEnd: Int = parsed.plainText.length, ): List { if (rangeEnd <= rangeStart) return emptyList() if (parsed.spans.isEmpty()) return emptyList() @@ -178,23 +179,26 @@ object MinecraftFormattingParser { val start = maxOf(span.start, safeStart) val end = minOf(span.end, safeEnd) if (end <= start) return@forEach - val color = span.colorRgb?.let { rgb -> - val alpha = (baseColor ushr 24) and 0xFF - (alpha shl 24) or (rgb and 0x00FF_FFFF) - } ?: baseColor - val flags = TextStyleFlags( - bold = baseFlags.bold || span.flags.bold, - italic = baseFlags.italic || span.flags.italic, - underline = baseFlags.underline || span.flags.underline, - strikethrough = baseFlags.strikethrough || span.flags.strikethrough, - obfuscated = baseFlags.obfuscated || span.flags.obfuscated - ) - out += ResolvedTextStyleSpan( - start = start - safeStart, - end = end - safeStart, - color = color, - flags = flags - ) + val color = + span.colorRgb?.let { rgb -> + val alpha = (baseColor ushr 24) and 0xFF + (alpha shl 24) or (rgb and 0x00FF_FFFF) + } ?: baseColor + val flags = + TextStyleFlags( + bold = baseFlags.bold || span.flags.bold, + italic = baseFlags.italic || span.flags.italic, + underline = baseFlags.underline || span.flags.underline, + strikethrough = baseFlags.strikethrough || span.flags.strikethrough, + obfuscated = baseFlags.obfuscated || span.flags.obfuscated, + ) + out += + ResolvedTextStyleSpan( + start = start - safeStart, + end = end - safeStart, + color = color, + flags = flags, + ) } return out } @@ -209,12 +213,13 @@ object MinecraftFormattingParser { fun flush() { val end = plain.length if (end <= segmentStart) return - spans += ParsedTextSpan( - start = segmentStart, - end = end, - colorRgb = style.colorRgb, - flags = style.flags - ) + spans += + ParsedTextSpan( + start = segmentStart, + end = end, + colorRgb = style.colorRgb, + flags = style.flags, + ) segmentStart = end } @@ -261,10 +266,11 @@ object MinecraftFormattingParser { MinecraftLegacyColors.rgb(lower) != null -> { flush() - style = SpanStyle( - colorRgb = MinecraftLegacyColors.rgb(lower), - flags = TextFormattingFlags.NONE - ) + style = + SpanStyle( + colorRgb = MinecraftLegacyColors.rgb(lower), + flags = TextFormattingFlags.NONE, + ) index += 2 segmentStart = plain.length } @@ -302,7 +308,8 @@ object MinecraftFormattingParser { var current = spans.first() for (index in 1 until spans.size) { val next = spans[index] - val mergeable = current.end == next.start && + val mergeable = + current.end == next.start && current.colorRgb == next.colorRgb && current.flags == next.flags if (mergeable) { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/text/ObfuscationTextSelector.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/text/ObfuscationTextSelector.kt index 7dde08f..af192ad 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/text/ObfuscationTextSelector.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/text/ObfuscationTextSelector.kt @@ -1,9 +1,7 @@ package org.dreamfinity.dsgl.core.text object ObfuscationTextSelector { - fun shouldObfuscateCodepoint(codepoint: Int): Boolean { - return !TextStyleMetrics.isWhitespaceCodepoint(codepoint) - } + fun shouldObfuscateCodepoint(codepoint: Int): Boolean = !TextStyleMetrics.isWhitespaceCodepoint(codepoint) fun selectCandidateIndex( sourceKey: String, @@ -11,7 +9,7 @@ object ObfuscationTextSelector { glyphIndexInLine: Int, timeSlice: Long, originalCodepoint: Int, - candidateCount: Int + candidateCount: Int, ): Int { if (candidateCount <= 0) return 0 var seed = 17 diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/text/TextDecorationLayout.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/text/TextDecorationLayout.kt index 14de4e3..6ac496e 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/text/TextDecorationLayout.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/text/TextDecorationLayout.kt @@ -4,7 +4,7 @@ import kotlin.math.roundToInt enum class DecorationType { Underline, - Strikethrough + Strikethrough, } data class TextVisualLine( @@ -13,7 +13,7 @@ data class TextVisualLine( val baselineY: Float, val lineHeightPx: Float, val glyphStartIndex: Int, - val glyphEndIndexExclusive: Int + val glyphEndIndexExclusive: Int, ) data class GlyphDecorationSample( @@ -23,7 +23,7 @@ data class GlyphDecorationSample( val xEnd: Float, val color: Int, val underline: Boolean, - val strikethrough: Boolean + val strikethrough: Boolean, ) data class DecorationFontMetrics( @@ -32,14 +32,14 @@ data class DecorationFontMetrics( val ascenderEm: Float, val descenderEm: Float, val underlineYEm: Float?, - val underlineThicknessEm: Float? + val underlineThicknessEm: Float?, ) data class ResolvedLineDecorationMetrics( val underlineY: Float, val underlineThickness: Float, val strikethroughY: Float, - val strikethroughThickness: Float + val strikethroughThickness: Float, ) data class DecorationQuad( @@ -49,7 +49,7 @@ data class DecorationQuad( val xEnd: Float, val y: Float, val thickness: Float, - val color: Int + val color: Int, ) object TextDecorationLayout { @@ -58,38 +58,38 @@ object TextDecorationLayout { return fontPx.coerceAtLeast(1) / safeEm } - fun baselineY(lineTopY: Float, ascenderEm: Float, scalePx: Float): Float { - return lineTopY + ascenderEm * scalePx - } + fun baselineY(lineTopY: Float, ascenderEm: Float, scalePx: Float): Float = lineTopY + ascenderEm * scalePx - fun screenYFromYUpMetric(baselineY: Float, metricYUpEm: Float, scalePx: Float): Float { - return baselineY - metricYUpEm * scalePx - } + fun screenYFromYUpMetric(baselineY: Float, metricYUpEm: Float, scalePx: Float): Float = + baselineY - metricYUpEm * scalePx fun resolveLineMetrics( line: TextVisualLine, fontMetrics: DecorationFontMetrics, - fontPx: Int + fontPx: Int, ): ResolvedLineDecorationMetrics { val lineHeight = line.lineHeightPx.coerceAtLeast(1f) val scale = scalePx(fontPx, fontMetrics.emSize) val rawUnderlineThickness = (fontMetrics.underlineThicknessEm ?: 0f) * scale val fallbackThickness = maxOf(1f, (0.06f * lineHeight).roundToInt().toFloat()) - val underlineThickness = when { - rawUnderlineThickness < 0.5f -> fallbackThickness - else -> maxOf(1f, rawUnderlineThickness.roundToInt().toFloat()) - } + val underlineThickness = + when { + rawUnderlineThickness < 0.5f -> fallbackThickness + else -> maxOf(1f, rawUnderlineThickness.roundToInt().toFloat()) + } val fallbackUnderlineY = line.baselineY + 0.08f * lineHeight - val metaUnderlineY = fontMetrics.underlineYEm?.let { metricY -> - screenYFromYUpMetric(line.baselineY, metricY, scale) - } - val underlineY = when { - metaUnderlineY == null -> fallbackUnderlineY - metaUnderlineY <= line.baselineY + 0.5f -> fallbackUnderlineY - else -> metaUnderlineY - }.roundToInt().toFloat() + val metaUnderlineY = + fontMetrics.underlineYEm?.let { metricY -> + screenYFromYUpMetric(line.baselineY, metricY, scale) + } + val underlineY = + when { + metaUnderlineY == null -> fallbackUnderlineY + metaUnderlineY <= line.baselineY + 0.5f -> fallbackUnderlineY + else -> metaUnderlineY + }.roundToInt().toFloat() val strikeY = (line.baselineY - 0.30f * lineHeight).roundToInt().toFloat() val strikeThickness = maxOf(1f, (0.06f * lineHeight).roundToInt().toFloat()) @@ -102,7 +102,7 @@ object TextDecorationLayout { underlineY = underlineY.coerceIn(line.lineTopY, underlineMaxY), underlineThickness = underlineThickness, strikethroughY = strikeY.coerceIn(line.lineTopY, strikeMaxY), - strikethroughThickness = strikeThickness + strikethroughThickness = strikeThickness, ) } @@ -110,17 +110,19 @@ object TextDecorationLayout { lines: List, glyphs: List, fontMetrics: DecorationFontMetrics, - fontPx: Int + fontPx: Int, ): List { if (lines.isEmpty() || glyphs.isEmpty()) return emptyList() val lineByIndex = lines.associateBy { it.lineIndex } - val lineMetricsByIndex = lines.associate { line -> - line.lineIndex to resolveLineMetrics( - line = line, - fontMetrics = fontMetrics, - fontPx = fontPx - ) - } + val lineMetricsByIndex = + lines.associate { line -> + line.lineIndex to + resolveLineMetrics( + line = line, + fontMetrics = fontMetrics, + fontPx = fontPx, + ) + } val out = ArrayList(glyphs.size) glyphs.sortedWith(compareBy { it.lineIndex }.thenBy { it.glyphIndex }).forEach { glyph -> if (glyph.xEnd <= glyph.xStart) return@forEach @@ -132,29 +134,31 @@ object TextDecorationLayout { if (glyph.underline) { appendMerged( out = out, - quad = DecorationQuad( - type = DecorationType.Underline, - lineIndex = glyph.lineIndex, - xStart = glyph.xStart, - xEnd = glyph.xEnd, - y = metrics.underlineY, - thickness = metrics.underlineThickness, - color = glyph.color - ) + quad = + DecorationQuad( + type = DecorationType.Underline, + lineIndex = glyph.lineIndex, + xStart = glyph.xStart, + xEnd = glyph.xEnd, + y = metrics.underlineY, + thickness = metrics.underlineThickness, + color = glyph.color, + ), ) } if (glyph.strikethrough) { appendMerged( out = out, - quad = DecorationQuad( - type = DecorationType.Strikethrough, - lineIndex = glyph.lineIndex, - xStart = glyph.xStart, - xEnd = glyph.xEnd, - y = metrics.strikethroughY, - thickness = metrics.strikethroughThickness, - color = glyph.color - ) + quad = + DecorationQuad( + type = DecorationType.Strikethrough, + lineIndex = glyph.lineIndex, + xStart = glyph.xStart, + xEnd = glyph.xEnd, + y = metrics.strikethroughY, + thickness = metrics.strikethroughThickness, + color = glyph.color, + ), ) } } @@ -177,4 +181,3 @@ object TextDecorationLayout { out += quad } } - diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/text/TextStyleMetrics.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/text/TextStyleMetrics.kt index b363604..0fe46a2 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/text/TextStyleMetrics.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/text/TextStyleMetrics.kt @@ -8,23 +8,22 @@ object TextStyleMetrics { spans: List, baseFlags: TextStyleFlags, rangeStart: Int = 0, - rangeEnd: Int = plainText.length - ): Int { - return boldExtraPxForRangeInText( + rangeEnd: Int = plainText.length, + ): Int = + boldExtraPxForRangeInText( plainText = plainText, spans = spans, baseFlags = baseFlags, rangeStart = rangeStart, - rangeEnd = rangeEnd + rangeEnd = rangeEnd, ) - } fun boldExtraPxForRangeInText( plainText: String, spans: List, baseFlags: TextStyleFlags, rangeStart: Int = 0, - rangeEnd: Int = plainText.length + rangeEnd: Int = plainText.length, ): Int { if (plainText.isEmpty()) return 0 val safeStart = rangeStart.coerceIn(0, plainText.length) @@ -39,12 +38,13 @@ object TextStyleMetrics { while (spanIndex < spans.size && absoluteIndex >= spans[spanIndex].end) { spanIndex += 1 } - val spanBold = if (spanIndex < spans.size) { - val span = spans[spanIndex] - absoluteIndex >= span.start && absoluteIndex < span.end && span.flags.bold - } else { - false - } + val spanBold = + if (spanIndex < spans.size) { + val span = spans[spanIndex] + absoluteIndex >= span.start && absoluteIndex < span.end && span.flags.bold + } else { + false + } val bold = baseFlags.bold || spanBold if (bold && !isWhitespaceCodepoint(codepoint)) { extra += BOLD_ADVANCE_EXTRA_PX @@ -54,12 +54,11 @@ object TextStyleMetrics { return extra } - fun isWhitespaceCodepoint(codepoint: Int): Boolean { - return codepoint == ' '.code || - codepoint == '\n'.code || - codepoint == '\r'.code || - codepoint == '\t'.code || - codepoint == 0x00A0 || - Character.isWhitespace(codepoint) - } + fun isWhitespaceCodepoint(codepoint: Int): Boolean = + codepoint == ' '.code || + codepoint == '\n'.code || + codepoint == '\r'.code || + codepoint == '\t'.code || + codepoint == 0x00A0 || + Character.isWhitespace(codepoint) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/DomTreeCachingTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/DomTreeCachingTests.kt index 14fb08a..80422ab 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/DomTreeCachingTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/DomTreeCachingTests.kt @@ -1,8 +1,8 @@ package org.dreamfinity.dsgl.core import org.dreamfinity.dsgl.core.components.modal.internal.ModalHostNode -import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.elements.TextNode import org.dreamfinity.dsgl.core.dom.elements.TextSource @@ -15,14 +15,14 @@ import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.UiTransform import kotlin.test.AfterTest import kotlin.test.Test -import kotlin.test.assertSame import kotlin.test.assertEquals +import kotlin.test.assertSame import kotlin.test.assertTrue class DomTreeCachingTests { private class CountingRectNode( private var color: Int, - key: Any? = null + key: Any? = null, ) : DOMNode(key) { var buildCount: Int = 0 private set @@ -45,7 +45,7 @@ class DomTreeCachingTests { private class TestRectNode( private val color: Int, - key: Any? = null + key: Any? = null, ) : DOMNode(key) { var throwOnBuild: Boolean = false override val styleType: String = "test-rect" @@ -62,7 +62,7 @@ class DomTreeCachingTests { private class LegacyManualChildrenNode( private val color: Int, - key: Any? = null + key: Any? = null, ) : DOMNode(key) { override val styleType: String = "legacy-manual" @@ -76,13 +76,18 @@ class DomTreeCachingTests { } } - private val ctx = object : UiMeasureContext { - override fun measureText(text: String): Int = text.length * 6 - override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * 6 - override val fontHeight: Int = 9 - override fun fontHeight(fontId: String?, fontSize: Int?): Int = 9 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override fun measureText(text: String): Int = text.length * 6 + + override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * 6 + + override val fontHeight: Int = 9 + + override fun fontHeight(fontId: String?, fontSize: Int?): Int = 9 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -175,10 +180,12 @@ class DomTreeCachingTests { node.display = Display.None val afterHide = tree.paint(ctx) - assertTrue(afterHide.none { command -> - command is RenderCommand.DrawRect && - command.color == 0xFF3399CC.toInt() - }) + assertTrue( + afterHide.none { command -> + command is RenderCommand.DrawRect && + command.color == 0xFF3399CC.toInt() + }, + ) } @Test @@ -214,9 +221,10 @@ class DomTreeCachingTests { @Test fun `render command chunk does not hold dom node references`() { - val hasNodeField = RenderCommandChunk::class.java.declaredFields.any { field -> - DOMNode::class.java.isAssignableFrom(field.type) - } + val hasNodeField = + RenderCommandChunk::class.java.declaredFields.any { field -> + DOMNode::class.java.isAssignableFrom(field.type) + } assertTrue(!hasNodeField) } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/GlyphsTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/GlyphsTests.kt index d3ed69a..e71b21e 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/GlyphsTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/GlyphsTests.kt @@ -3,11 +3,9 @@ package org.dreamfinity.dsgl.core import sun.font.StandardGlyphVector import java.awt.Font import java.awt.font.FontRenderContext -import java.awt.font.TextLayout import java.io.File import kotlin.test.Test - class GlyphsTests { @Test fun `test glyphs`() { diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/LayoutStrictModeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/LayoutStrictModeTests.kt index 1bf0697..262725e 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/LayoutStrictModeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/LayoutStrictModeTests.kt @@ -1,8 +1,5 @@ package org.dreamfinity.dsgl.core -import kotlin.test.Test -import kotlin.test.assertFalse -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.debug.LayoutDebug @@ -13,6 +10,9 @@ import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.event.MouseClickEvent import org.dreamfinity.dsgl.core.render.RenderCommand +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue class LayoutStrictModeTests { @Test @@ -27,10 +27,11 @@ class LayoutStrictModeTests { LayoutDebug.drawBounds = false LayoutDebug.logViolations = false - val root = ContainerNode(key = "root").apply { - width = 20 - height = 10 - } + val root = + ContainerNode(key = "root").apply { + width = 20 + height = 10 + } RogueNode(key = "rogue").applyParent(root) val tree = DomTree(root) val ctx = testMeasureContext() @@ -42,7 +43,7 @@ class LayoutStrictModeTests { assertTrue(commands.isEmpty(), "Strict invalid layout should suppress normal paint commands.") assertFalse( tree.dispatchClick(MouseClickEvent(5, 5, MouseButton.LEFT)), - "Strict invalid layout should suppress hit-testing." + "Strict invalid layout should suppress hit-testing.", ) } finally { LayoutDebug.validateLayouts = prevValidate @@ -52,19 +53,28 @@ class LayoutStrictModeTests { } } - private class RogueNode(key: Any?) : DOMNode(key) { + private class RogueNode( + key: Any?, + ) : DOMNode(key) { override fun measure(ctx: UiMeasureContext): Size = Size(6, 4) - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x + 128, y + 64, width, height) } } - private fun testMeasureContext(): UiMeasureContext { - return object : UiMeasureContext { + private fun testMeasureContext(): UiMeasureContext = + object : UiMeasureContext { override val fontHeight: Int = 8 + override fun measureText(text: String): Int = text.length * 6 + override fun paint(commands: List) = Unit } - } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/UiScopeHookApiTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/UiScopeHookApiTests.kt index f85788a..2d77b47 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/UiScopeHookApiTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/UiScopeHookApiTests.kt @@ -2,13 +2,13 @@ package org.dreamfinity.dsgl.core import org.dreamfinity.dsgl.core.hooks.createContext import org.dreamfinity.dsgl.core.hooks.provideContext +import org.dreamfinity.dsgl.core.hooks.ref.useRef import org.dreamfinity.dsgl.core.hooks.useCallback import org.dreamfinity.dsgl.core.hooks.useContext import org.dreamfinity.dsgl.core.hooks.useEffect import org.dreamfinity.dsgl.core.hooks.useMemo import org.dreamfinity.dsgl.core.hooks.useReducer import org.dreamfinity.dsgl.core.hooks.useState -import org.dreamfinity.dsgl.core.hooks.ref.useRef import kotlin.test.Test import kotlin.test.assertEquals @@ -69,8 +69,8 @@ class UiScopeHookApiTests { var contextSeen: String? = null val effectEvents: MutableList = arrayListOf() - override fun render(): DomTree { - return ui { + override fun render(): DomTree = + ui { var count by useState(initial) val valueRef by useRef() val memoValue by useMemo(count) { count * 2 } @@ -78,10 +78,11 @@ class UiScopeHookApiTests { val captured = count { captured } } - val (reducerValue, _) = useReducer( - initialState = 10, - reducer = { old: Int, action: Int -> old + action } - ) + val (reducerValue, _) = + useReducer( + initialState = 10, + reducer = { old: Int, action: Int -> old + action }, + ) useEffect(count) { val captured = count effectEvents += "run:$captured" @@ -98,6 +99,5 @@ class UiScopeHookApiTests { contextSeen = useContext(ApiContext) } } - } } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseContextTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseContextTests.kt index b5b8f12..8eac3e3 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseContextTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseContextTests.kt @@ -74,11 +74,10 @@ class UseContextTests { private class DefaultOnlyContextWindow : DsglWindow() { var lastSeen: String? = null - override fun render(): DomTree { - return ui { + override fun render(): DomTree = + ui { lastSeen = useContext(ThemeContext) } - } } private class NestedProviderWindow : DsglWindow() { @@ -86,8 +85,8 @@ class UseContextTests { var innerSeen: String? = null var afterNestedSeen: String? = null - override fun render(): DomTree { - return ui { + override fun render(): DomTree = + ui { provideContext(ThemeContext, "Outer") { outerSeen = useContext(ThemeContext) provideContext(ThemeContext, "Inner") { @@ -96,15 +95,14 @@ class UseContextTests { afterNestedSeen = useContext(ThemeContext) } } - } } private class ProviderValueChangeWindow : DsglWindow() { var pendingProviderValue: String? = null var lastSeen: String? = null - override fun render(): DomTree { - return ui { + override fun render(): DomTree = + ui { var providerValue by useState("Light") pendingProviderValue?.let { next -> providerValue = next @@ -114,15 +112,14 @@ class UseContextTests { lastSeen = useContext(ThemeContext) } } - } } private class ConditionalProviderWindow : DsglWindow() { var showProvider: Boolean = true var lastSeen: String? = null - override fun render(): DomTree { - return ui { + override fun render(): DomTree = + ui { if (showProvider) { provideContext(ThemeContext, "Provided") { lastSeen = useContext(ThemeContext) @@ -131,6 +128,5 @@ class UseContextTests { lastSeen = useContext(ThemeContext) } } - } } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseEffectHookRuntimeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseEffectHookRuntimeTests.kt index 39f7870..fdea4cb 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseEffectHookRuntimeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseEffectHookRuntimeTests.kt @@ -75,9 +75,10 @@ class UseEffectHookRuntimeTests { fun `render failure attempt does not execute effect`() { val window = FailingEffectWindow() - val error = assertFailsWith { - renderWithHookSession(window, commit = false) - } + val error = + assertFailsWith { + renderWithHookSession(window, commit = false) + } assertTrue(error.message?.contains("forced render failure") == true) assertEquals(emptyList(), window.events) } @@ -90,9 +91,10 @@ class UseEffectHookRuntimeTests { renderWithHookSession(window, commit = true) window.everyCommitBranch = true - val error = assertFailsWith { - renderWithHookSession(window, commit = true) - } + val error = + assertFailsWith { + renderWithHookSession(window, commit = true) + } assertTrue(error.message?.contains("Hook signature mismatch") == true) assertTrue(error.message?.contains("useEffect#0") == true) @@ -122,11 +124,7 @@ class UseEffectHookRuntimeTests { assertEquals(listOf("run:1", "cleanup:1"), window.events) } - private fun renderWithHookSession( - window: DsglWindow, - mode: HookRenderSessionMode = HookRenderSessionMode.Normal, - commit: Boolean - ): DomTree { + private fun renderWithHookSession(window: DsglWindow, mode: HookRenderSessionMode = HookRenderSessionMode.Normal, commit: Boolean): DomTree { window.beginRenderBuild(mode) var renderSucceeded = false return try { diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseMemoHookRuntimeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseMemoHookRuntimeTests.kt index 6d98165..bcb6d03 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseMemoHookRuntimeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseMemoHookRuntimeTests.kt @@ -75,9 +75,10 @@ class UseMemoHookRuntimeTests { renderWithHookSession(window) window.useStringBranch = true - val error = assertFailsWith { - renderWithHookSession(window) - } + val error = + assertFailsWith { + renderWithHookSession(window) + } assertTrue(error.message?.contains("Hook signature mismatch") == true) assertTrue(error.message?.contains("memoValue") == true) @@ -155,9 +156,10 @@ class UseMemoHookRuntimeTests { window.beginRenderBuild() window.render() - val error = assertFailsWith { - window.endRenderBuild() - } + val error = + assertFailsWith { + window.endRenderBuild() + } assertTrue(error.message?.contains("Storage-backed hook 'useMemo'") == true) assertTrue(error.message?.contains("delegated property syntax") == true) @@ -169,18 +171,16 @@ class UseMemoHookRuntimeTests { window.beginRenderBuild() window.render() - val error = assertFailsWith { - window.endRenderBuild() - } + val error = + assertFailsWith { + window.endRenderBuild() + } assertTrue(error.message?.contains("Storage-backed hook 'useCallback'") == true) assertTrue(error.message?.contains("delegated property syntax") == true) } - private fun renderWithHookSession( - window: DsglWindow, - mode: HookRenderSessionMode = HookRenderSessionMode.Normal - ): DomTree { + private fun renderWithHookSession(window: DsglWindow, mode: HookRenderSessionMode = HookRenderSessionMode.Normal): DomTree { window.beginRenderBuild(mode) return try { window.render() diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseReducerHookRuntimeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseReducerHookRuntimeTests.kt index 23c9c01..a1fda70 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseReducerHookRuntimeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseReducerHookRuntimeTests.kt @@ -80,9 +80,10 @@ class UseReducerHookRuntimeTests { renderWithHookSession(window) window.useStringBranch = true - val error = assertFailsWith { - renderWithHookSession(window) - } + val error = + assertFailsWith { + renderWithHookSession(window) + } assertTrue(error.message?.contains("Hook signature mismatch") == true) assertTrue(error.message?.contains("useReducer#0") == true) @@ -122,19 +123,17 @@ class UseReducerHookRuntimeTests { fun `useReducer outside active render fails loudly`() { val window = ReducerProbeWindow() - val error = assertFailsWith { - window.useReducer(0) { old: Int, action: Int -> - old + action + val error = + assertFailsWith { + window.useReducer(0) { old: Int, action: Int -> + old + action + } } - } assertTrue(error.message?.contains("outside active component render") == true) } - private fun renderWithHookSession( - window: DsglWindow, - mode: HookRenderSessionMode = HookRenderSessionMode.Normal - ): DomTree { + private fun renderWithHookSession(window: DsglWindow, mode: HookRenderSessionMode = HookRenderSessionMode.Normal): DomTree { window.beginRenderBuild(mode) return try { window.render() @@ -162,7 +161,11 @@ class UseReducerHookRuntimeTests { private class ReducerProbeWindow : DsglWindow() { sealed interface CounterAction { data object Increment : CounterAction - data class Add(val delta: Int) : CounterAction + + data class Add( + val delta: Int, + ) : CounterAction + data object Noop : CounterAction } @@ -182,13 +185,12 @@ class UseReducerHookRuntimeTests { return DomTree(ContainerNode(key = "reducer.probe.root")) } - private fun reduceCounter(old: Int, action: CounterAction): Int { - return when (action) { + private fun reduceCounter(old: Int, action: CounterAction): Int = + when (action) { CounterAction.Increment -> old + 1 is CounterAction.Add -> old + action.delta CounterAction.Noop -> old } - } } private class ConditionalReducerTypeWindow : DsglWindow() { @@ -198,18 +200,20 @@ class UseReducerHookRuntimeTests { override fun render(): DomTree { if (useStringBranch) { - val (state, dispatch) = useReducer("fresh") { old: String, action: String -> - old + action - } + val (state, dispatch) = + useReducer("fresh") { old: String, action: String -> + old + action + } lastSeen = state lastIntDispatch = null if (dispatch.hashCode() == Int.MIN_VALUE) { error("unreachable") } } else { - val (state, dispatch) = useReducer(0) { old: Int, action: Int -> - old + action - } + val (state, dispatch) = + useReducer(0) { old: Int, action: Int -> + old + action + } lastSeen = state lastIntDispatch = dispatch } @@ -218,7 +222,7 @@ class UseReducerHookRuntimeTests { } private class RecordingHost( - override val window: DsglWindow + override val window: DsglWindow, ) : DsglWindowHost { var rebuildRequests: Int = 0 @@ -229,8 +233,6 @@ class UseReducerHookRuntimeTests { override fun requestRedraw() { } - override fun getViewport(): Viewport { - return Viewport(width = 0, height = 0) - } + override fun getViewport(): Viewport = Viewport(width = 0, height = 0) } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseStateHookRuntimeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseStateHookRuntimeTests.kt index 6b2192b..54cd966 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseStateHookRuntimeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseStateHookRuntimeTests.kt @@ -1,11 +1,11 @@ package org.dreamfinity.dsgl.core import org.dreamfinity.dsgl.core.dom.elements.ContainerNode +import org.dreamfinity.dsgl.core.dsl.UiScope import org.dreamfinity.dsgl.core.hooks.HookHotReloadRemountException import org.dreamfinity.dsgl.core.hooks.HookRenderSessionMode import org.dreamfinity.dsgl.core.hooks.HookUsageException import org.dreamfinity.dsgl.core.hooks.useState -import org.dreamfinity.dsgl.core.dsl.UiScope import org.dreamfinity.dsgl.core.host.DsglWindowHost import org.dreamfinity.dsgl.core.host.Viewport import kotlin.test.Test @@ -53,14 +53,14 @@ class UseStateHookRuntimeTests { renderWithHookSession(window) assertEquals( linkedMapOf("Left panel" to 1, "Right panel" to 0), - window.observedCountsSnapshot() + window.observedCountsSnapshot(), ) window.enqueueIncrement("Right panel") renderWithHookSession(window) assertEquals( linkedMapOf("Left panel" to 1, "Right panel" to 1), - window.observedCountsSnapshot() + window.observedCountsSnapshot(), ) } @@ -73,13 +73,13 @@ class UseStateHookRuntimeTests { renderWithHookSession(window) assertEquals( linkedMapOf("A" to 0, "B" to 1, "C" to 0), - window.observedCountsSnapshot() + window.observedCountsSnapshot(), ) renderWithHookSession(window) assertEquals( linkedMapOf("A" to 0, "B" to 1, "C" to 0), - window.observedCountsSnapshot() + window.observedCountsSnapshot(), ) } @@ -92,14 +92,14 @@ class UseStateHookRuntimeTests { renderWithHookSession(window) assertEquals( linkedMapOf("A" to 0, "B" to 1, "C" to 0), - window.observedCountsSnapshot() + window.observedCountsSnapshot(), ) window.order = listOf("B", "A", "C") renderWithHookSession(window) assertEquals( linkedMapOf("B" to 0, "A" to 1, "C" to 0), - window.observedCountsSnapshot() + window.observedCountsSnapshot(), ) } @@ -133,9 +133,10 @@ class UseStateHookRuntimeTests { renderWithHookSession(window) window.useStringBranch = true - val error = assertFailsWith { - renderWithHookSession(window) - } + val error = + assertFailsWith { + renderWithHookSession(window) + } assertTrue(error.message?.contains("Hook signature mismatch") == true) assertTrue(error.message?.contains("counter") == true) @@ -163,9 +164,10 @@ class UseStateHookRuntimeTests { window.beginRenderBuild() window.render() - val error = assertFailsWith { - window.endRenderBuild() - } + val error = + assertFailsWith { + window.endRenderBuild() + } assertEquals(error.message?.contains("Storage-backed hook 'useState'"), true) assertEquals(error.message?.contains("delegated property syntax"), true) @@ -175,17 +177,15 @@ class UseStateHookRuntimeTests { fun `useState outside active render fails loudly`() { val window = StateProbeWindow() - val error = assertFailsWith { - window.useState(0) - } + val error = + assertFailsWith { + window.useState(0) + } assertEquals(error.message?.contains("outside active component render"), true) } - private fun renderWithHookSession( - window: DsglWindow, - mode: HookRenderSessionMode = HookRenderSessionMode.Normal - ): DomTree { + private fun renderWithHookSession(window: DsglWindow, mode: HookRenderSessionMode = HookRenderSessionMode.Normal): DomTree { window.beginRenderBuild(mode) return try { window.render() @@ -213,7 +213,10 @@ class UseStateHookRuntimeTests { private class StateProbeWindow : DsglWindow() { sealed interface Mutation { data object None : Mutation - data class Assign(val value: Int) : Mutation + + data class Assign( + val value: Int, + ) : Mutation } var showState: Boolean = true @@ -278,9 +281,7 @@ class UseStateHookRuntimeTests { pendingIncrements += label } - fun observedCountsSnapshot(): LinkedHashMap { - return LinkedHashMap(observedCounts) - } + fun observedCountsSnapshot(): LinkedHashMap = LinkedHashMap(observedCounts) override fun render(): DomTree { observedCounts.clear() @@ -301,7 +302,7 @@ class UseStateHookRuntimeTests { } private class RecordingHost( - override val window: DsglWindow + override val window: DsglWindow, ) : DsglWindowHost { var rebuildRequests: Int = 0 @@ -312,8 +313,6 @@ class UseStateHookRuntimeTests { override fun requestRedraw() { } - override fun getViewport(): Viewport { - return Viewport(width = 0, height = 0) - } + override fun getViewport(): Viewport = Viewport(width = 0, height = 0) } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/animation/AnimationEngineTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/animation/AnimationEngineTests.kt index 061977c..6e907ee 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/animation/AnimationEngineTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/animation/AnimationEngineTests.kt @@ -32,15 +32,16 @@ class AnimationEngineTests { fun `transition retargeting starts from current interpolated value`() { val root = ContainerNode(key = "root") val node = TextNode(TextSource.Static("x"), key = "animated").applyParent(root) - node.transitionSpec = TransitionSpec( - listOf( - TransitionPropertySpec( - property = AnimatedStyleProperty.Opacity, - durationMs = 1000, - easing = Easings.LINEAR - ) + node.transitionSpec = + TransitionSpec( + listOf( + TransitionPropertySpec( + property = AnimatedStyleProperty.Opacity, + durationMs = 1000, + easing = Easings.LINEAR, + ), + ), ) - ) node.applyComputedStyle(style(opacity = 0f)) node.applyComputedStyle(style(opacity = 1f)) @@ -58,16 +59,17 @@ class AnimationEngineTests { fun `transition delay is respected`() { val root = ContainerNode(key = "root") val node = TextNode(TextSource.Static("x"), key = "animated").applyParent(root) - node.transitionSpec = TransitionSpec( - listOf( - TransitionPropertySpec( - property = AnimatedStyleProperty.Opacity, - durationMs = 500, - delayMs = 300, - easing = Easings.LINEAR - ) + node.transitionSpec = + TransitionSpec( + listOf( + TransitionPropertySpec( + property = AnimatedStyleProperty.Opacity, + durationMs = 500, + delayMs = 300, + easing = Easings.LINEAR, + ), + ), ) - ) node.applyComputedStyle(style(opacity = 0f)) node.applyComputedStyle(style(opacity = 1f)) @@ -85,16 +87,17 @@ class AnimationEngineTests { } val root = ContainerNode(key = "root") val node = TextNode(TextSource.Static("x"), key = "animated").applyParent(root) - node.animationSpecs = listOf( - AnimationSpec( - name = "pulse", - durationMs = 1000, - delayMs = 200, - easing = Easings.LINEAR, - direction = AnimationDirection.Reverse, - fillMode = AnimationFillMode.Both + node.animationSpecs = + listOf( + AnimationSpec( + name = "pulse", + durationMs = 1000, + delayMs = 200, + easing = Easings.LINEAR, + direction = AnimationDirection.Reverse, + fillMode = AnimationFillMode.Both, + ), ) - ) node.applyComputedStyle(style(opacity = 1f)) StyleAnimationEngine.tickAndApply(root, 0.1, null) @@ -115,13 +118,14 @@ class AnimationEngineTests { } val root = ContainerNode(key = "root") val node = TextNode(TextSource.Static("x"), key = "animated").applyParent(root) - node.animationSpecs = listOf( - AnimationSpec( - name = "fadeOnly", - durationMs = 1000, - easing = Easings.LINEAR + node.animationSpecs = + listOf( + AnimationSpec( + name = "fadeOnly", + durationMs = 1000, + easing = Easings.LINEAR, + ), ) - ) node.applyComputedStyle(style(opacity = 1f, color = 0xFF123456.toInt())) StyleAnimationEngine.tickAndApply(root, 0.5, null) @@ -137,16 +141,17 @@ class AnimationEngineTests { } val root = ContainerNode(key = "root") val node = TextNode(TextSource.Static("x"), key = "animated").applyParent(root) - node.animationSpecs = listOf( - AnimationSpec( - name = "altDir", - durationMs = 1000, - easing = Easings.LINEAR, - iterationCount = IterationCount.Count(2), - direction = AnimationDirection.Alternate, - fillMode = AnimationFillMode.Forwards + node.animationSpecs = + listOf( + AnimationSpec( + name = "altDir", + durationMs = 1000, + easing = Easings.LINEAR, + iterationCount = IterationCount.Count(2), + direction = AnimationDirection.Alternate, + fillMode = AnimationFillMode.Forwards, + ), ) - ) node.applyComputedStyle(style(opacity = 1f)) StyleAnimationEngine.tickAndApply(root, 1.25, null) assertTrue(node.effectiveOpacity() in 0.70f..0.80f) @@ -162,15 +167,16 @@ class AnimationEngineTests { } val root = ContainerNode(key = "root") val node = TextNode(TextSource.Static("x"), key = "animated").applyParent(root) - node.animationSpecs = listOf( - AnimationSpec( - name = "delayBackwards", - durationMs = 1000, - delayMs = 400, - easing = Easings.LINEAR, - fillMode = AnimationFillMode.Backwards + node.animationSpecs = + listOf( + AnimationSpec( + name = "delayBackwards", + durationMs = 1000, + delayMs = 400, + easing = Easings.LINEAR, + fillMode = AnimationFillMode.Backwards, + ), ) - ) node.applyComputedStyle(style(opacity = 1f)) StyleAnimationEngine.tickAndApply(root, 0.2, null) assertEquals(0f, node.effectiveOpacity()) @@ -180,20 +186,21 @@ class AnimationEngineTests { fun `transition animates transform opacity and color together`() { val root = ContainerNode(key = "root") val node = TextNode(TextSource.Static("x"), key = "animated").applyParent(root) - node.transitionSpec = TransitionSpec( - listOf( - TransitionPropertySpec(AnimatedStyleProperty.Transform, 1000, easing = Easings.LINEAR), - TransitionPropertySpec(AnimatedStyleProperty.Opacity, 1000, easing = Easings.LINEAR), - TransitionPropertySpec(AnimatedStyleProperty.Color, 1000, easing = Easings.LINEAR) + node.transitionSpec = + TransitionSpec( + listOf( + TransitionPropertySpec(AnimatedStyleProperty.Transform, 1000, easing = Easings.LINEAR), + TransitionPropertySpec(AnimatedStyleProperty.Opacity, 1000, easing = Easings.LINEAR), + TransitionPropertySpec(AnimatedStyleProperty.Color, 1000, easing = Easings.LINEAR), + ), ) - ) node.applyComputedStyle(style(opacity = 1f, color = 0xFFFFFFFF.toInt(), transform = UiTransform.IDENTITY)) node.applyComputedStyle( style( opacity = 0.5f, color = 0xFF000000.toInt(), - transform = UiTransform(translateX = 10f, translateY = 0f, scaleX = 1f, scaleY = 1f, rotateDeg = 0f) - ) + transform = UiTransform(translateX = 10f, translateY = 0f, scaleX = 1f, scaleY = 1f, rotateDeg = 0f), + ), ) StyleAnimationEngine.tickAndApply(root, 0.5, null) @@ -205,15 +212,10 @@ class AnimationEngineTests { assertTrue(red in 120..136) } - private fun style( - transform: UiTransform = UiTransform.IDENTITY, - opacity: Float = 1f, - color: Int = 0xFFFFFFFF.toInt() - ): ComputedStyle { - return ComputedStyleDefaults( + private fun style(transform: UiTransform = UiTransform.IDENTITY, opacity: Float = 1f, color: Int = 0xFFFFFFFF.toInt()): ComputedStyle = + ComputedStyleDefaults( foregroundColor = color, opacity = opacity, - transform = transform + transform = transform, ).toComputedStyle() - } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/animation/EasingTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/animation/EasingTests.kt index 149243d..09555ab 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/animation/EasingTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/animation/EasingTests.kt @@ -39,4 +39,3 @@ class EasingTests { assertTrue(easeInOutMid in 0.3f..0.7f) } } - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerControllerTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerControllerTests.kt index e07744b..99c5032 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerControllerTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerControllerTests.kt @@ -11,18 +11,26 @@ class ColorPickerControllerTests { @Test fun `mode switch keeps selected color`() { val initial = RgbaColor(0.25f, 0.5f, 0.75f, 0.6f) - val controller = ColorPickerController( - initial = ColorPickerState( - color = initial, - previous = initial, - mode = ColorFormatMode.RGB, - alphaEnabled = true + val controller = + ColorPickerController( + initial = + ColorPickerState( + color = initial, + previous = initial, + mode = ColorFormatMode.RGB, + alphaEnabled = true, + ), ) - ) val firstLayout = controller.buildLayout(Rect(0, 0, 320, controller.preferredHeight(true))) - controller.handleMouseDown(firstLayout.modeSelectRect.x + 2, firstLayout.modeSelectRect.y + 2, MouseButton.LEFT, firstLayout) + controller.handleMouseDown( + firstLayout.modeSelectRect.x + 2, + firstLayout.modeSelectRect.y + 2, + MouseButton.LEFT, + firstLayout, + ) val dropdownLayout = controller.buildLayout(Rect(0, 0, 320, controller.preferredHeight(true))) - val hslOption = dropdownLayout.modeOptions.firstOrNull { it.mode == ColorFormatMode.HSL } ?: error("HSL option missing") + val hslOption = + dropdownLayout.modeOptions.firstOrNull { it.mode == ColorFormatMode.HSL } ?: error("HSL option missing") controller.handleMouseDown(hslOption.rect.x + 2, hslOption.rect.y + 2, MouseButton.LEFT, dropdownLayout) val after = controller.snapshot() @@ -38,11 +46,12 @@ class ColorPickerControllerTests { val sampled = 0xCC112233.toInt() val history = ColorRecentHistory() var committed: RgbaColor? = null - val controller = ColorPickerController( - initial = ColorPickerState(RgbaColor.WHITE, closeOnSelect = false), - recentHistory = history, - screenSampler = ScreenColorSampler { _, _ -> sampled } - ) + val controller = + ColorPickerController( + initial = ColorPickerState(RgbaColor.WHITE, closeOnSelect = false), + recentHistory = history, + screenSampler = ScreenColorSampler { _, _ -> sampled }, + ) controller.onCommit = { committed = it } controller.beginEyedropper() val layout = controller.buildLayout(Rect(0, 0, 320, controller.preferredHeight(true))) @@ -60,32 +69,41 @@ class ColorPickerControllerTests { @Test fun `commit stores color in recents`() { val history = ColorRecentHistory() - val controller = ColorPickerController( - initial = ColorPickerState(color = RgbaColor.WHITE), - recentHistory = history - ) + val controller = + ColorPickerController( + initial = ColorPickerState(color = RgbaColor.WHITE), + recentHistory = history, + ) val next = RgbaColor(0.1f, 0.2f, 0.3f, 0.4f) controller.setState( ColorPickerState( color = next, previous = RgbaColor.WHITE, mode = ColorFormatMode.HEX, - alphaEnabled = true - ) + alphaEnabled = true, + ), ) controller.commitCurrentColor() - assertEquals(next.toArgbInt(), history.snapshot().first().toArgbInt()) + assertEquals( + next.toArgbInt(), + history + .snapshot() + .first() + .toArgbInt(), + ) } @Test fun `input slot layout separates label and input rectangles`() { - val controller = ColorPickerController( - initial = ColorPickerState( - color = RgbaColor.WHITE, - mode = ColorFormatMode.RGB, - alphaEnabled = true + val controller = + ColorPickerController( + initial = + ColorPickerState( + color = RgbaColor.WHITE, + mode = ColorFormatMode.RGB, + alphaEnabled = true, + ), ) - ) val layout = controller.buildLayout(Rect(0, 0, 360, controller.preferredHeight(true))) assertTrue(layout.inputSlots.isNotEmpty()) layout.inputSlots.forEach { slot -> @@ -97,14 +115,16 @@ class ColorPickerControllerTests { @Test fun `rgb channel order switch updates active mode and input order`() { - val controller = ColorPickerController( - initial = ColorPickerState( - color = RgbaColor(0.3f, 0.4f, 0.5f, 0.6f), - mode = ColorFormatMode.RGB, - rgbOrder = RgbChannelOrder.RGBA, - alphaEnabled = true + val controller = + ColorPickerController( + initial = + ColorPickerState( + color = RgbaColor(0.3f, 0.4f, 0.5f, 0.6f), + mode = ColorFormatMode.RGB, + rgbOrder = RgbChannelOrder.RGBA, + alphaEnabled = true, + ), ) - ) val firstLayout = controller.buildLayout(Rect(0, 0, 360, controller.preferredHeight(true))) val argbRect = firstLayout.argbOrderRect ?: error("ARGB switch not rendered") controller.handleMouseDown(argbRect.x + 2, argbRect.y + 2, MouseButton.LEFT, firstLayout) @@ -120,18 +140,21 @@ class ColorPickerControllerTests { @Test fun `eyedropper overlay shows mode and formatted value tooltip`() { val sampled = 0xFF336699.toInt() - val sampler = object : ScreenColorSampler { - override fun sampleColorAt(x: Int, y: Int): Int? = sampled - } - val controller = ColorPickerController( - initial = ColorPickerState( - color = RgbaColor.WHITE, - mode = ColorFormatMode.RGB, - rgbOrder = RgbChannelOrder.ARGB, - alphaEnabled = true - ), - screenSampler = sampler - ) + val sampler = + object : ScreenColorSampler { + override fun sampleColorAt(x: Int, y: Int): Int? = sampled + } + val controller = + ColorPickerController( + initial = + ColorPickerState( + color = RgbaColor.WHITE, + mode = ColorFormatMode.RGB, + rgbOrder = RgbChannelOrder.ARGB, + alphaEnabled = true, + ), + screenSampler = sampler, + ) controller.beginEyedropper() val layout = controller.buildLayout(Rect(20, 20, 340, controller.preferredHeight(true))) controller.handleMouseMove(120, 160, layout) @@ -141,26 +164,29 @@ class ColorPickerControllerTests { val textCommands = out.filterIsInstance() assertTrue(textCommands.any { it.text.contains("Mode: RGB (ARGB)") }) - val expected = ColorTextCodec.format( - RgbaColor.fromArgbInt(sampled).normalized(), - ColorFormatMode.RGB, - true, - RgbChannelOrder.ARGB - ) + val expected = + ColorTextCodec.format( + RgbaColor.fromArgbInt(sampled).normalized(), + ColorFormatMode.RGB, + true, + RgbChannelOrder.ARGB, + ) assertTrue(textCommands.any { it.text == expected }) } @Test fun `eyedropper overlay preview path does not use area sampling`() { val sampler = RecordingSampler() - val controller = ColorPickerController( - initial = ColorPickerState( - color = RgbaColor.WHITE, - mode = ColorFormatMode.HEX, - alphaEnabled = true - ), - screenSampler = sampler - ) + val controller = + ColorPickerController( + initial = + ColorPickerState( + color = RgbaColor.WHITE, + mode = ColorFormatMode.HEX, + alphaEnabled = true, + ), + screenSampler = sampler, + ) controller.beginEyedropper() val layout = controller.buildLayout(Rect(0, 0, 360, controller.preferredHeight(true))) controller.handleMouseMove(80, 90, layout) @@ -176,13 +202,15 @@ class ColorPickerControllerTests { @Test fun `eyedropper overlay emits capture and textured magnifier commands instead of per-cell rectangles`() { - val controller = ColorPickerController( - initial = ColorPickerState( - color = RgbaColor.WHITE, - mode = ColorFormatMode.HEX, - alphaEnabled = true + val controller = + ColorPickerController( + initial = + ColorPickerState( + color = RgbaColor.WHITE, + mode = ColorFormatMode.HEX, + alphaEnabled = true, + ), ) - ) controller.beginEyedropper() val layout = controller.buildLayout(Rect(0, 0, 360, controller.preferredHeight(true))) controller.handleMouseMove(80, 90, layout) @@ -191,26 +219,32 @@ class ColorPickerControllerTests { assertTrue(out.any { it is RenderCommand.CaptureScreenRegion }) assertTrue(out.any { it is RenderCommand.DrawCapturedScreenRegion }) - assertTrue(out.none { command -> - command is RenderCommand.DrawRect && command.width == 8 && command.height == 8 - }) + assertTrue( + out.none { command -> + command is RenderCommand.DrawRect && command.width == 8 && command.height == 8 + }, + ) } + @Test fun `eyedropper overlay draws aligned light grid over captured magnifier`() { val gridColor = 0x7F57C2FF - val controller = ColorPickerController( - initial = ColorPickerState( - color = RgbaColor.WHITE, - mode = ColorFormatMode.HEX, - alphaEnabled = true - ), - style = ColorPickerStyle( - eyedropperGridSize = 5, - eyedropperCellSize = 4, - eyedropperGridOverlayEnabled = true, - eyedropperGridOverlayColor = gridColor + val controller = + ColorPickerController( + initial = + ColorPickerState( + color = RgbaColor.WHITE, + mode = ColorFormatMode.HEX, + alphaEnabled = true, + ), + style = + ColorPickerStyle( + eyedropperGridSize = 5, + eyedropperCellSize = 4, + eyedropperGridOverlayEnabled = true, + eyedropperGridOverlayColor = gridColor, + ), ) - ) controller.beginEyedropper() val layout = controller.buildLayout(Rect(0, 0, 360, controller.preferredHeight(true))) controller.handleMouseMove(80, 90, layout) @@ -227,14 +261,16 @@ class ColorPickerControllerTests { @Test fun `eyedropper keeps existing alpha while sampling rgb`() { val sampled = 0x00336699 - val controller = ColorPickerController( - initial = ColorPickerState( - color = RgbaColor(0.1f, 0.2f, 0.3f, 0.4f), - mode = ColorFormatMode.HEX, - alphaEnabled = true - ), - screenSampler = ScreenColorSampler { _, _ -> sampled } - ) + val controller = + ColorPickerController( + initial = + ColorPickerState( + color = RgbaColor(0.1f, 0.2f, 0.3f, 0.4f), + mode = ColorFormatMode.HEX, + alphaEnabled = true, + ), + screenSampler = ScreenColorSampler { _, _ -> sampled }, + ) controller.beginEyedropper() val layout = controller.buildLayout(Rect(0, 0, 320, controller.preferredHeight(true))) controller.handleMouseMove(20, 24, layout) @@ -249,13 +285,15 @@ class ColorPickerControllerTests { @Test fun `picker draws gradient bars with dedicated render commands`() { - val controller = ColorPickerController( - initial = ColorPickerState( - color = RgbaColor.WHITE, - mode = ColorFormatMode.RGB, - alphaEnabled = true + val controller = + ColorPickerController( + initial = + ColorPickerState( + color = RgbaColor.WHITE, + mode = ColorFormatMode.RGB, + alphaEnabled = true, + ), ) - ) val layout = controller.buildLayout(Rect(0, 0, 320, controller.preferredHeight(true))) val out = ArrayList() controller.appendCommands(layout, out) @@ -269,26 +307,32 @@ class ColorPickerControllerTests { fun `picker checker backgrounds use dedicated checkerboard command`() { val checkerLight = 0x7FA0D010 val checkerDark = 0x7F104090 - val controller = ColorPickerController( - initial = ColorPickerState( - color = RgbaColor.WHITE, - mode = ColorFormatMode.RGB, - alphaEnabled = true - ), - style = ColorPickerStyle( - checkerLightColor = checkerLight, - checkerDarkColor = checkerDark + val controller = + ColorPickerController( + initial = + ColorPickerState( + color = RgbaColor.WHITE, + mode = ColorFormatMode.RGB, + alphaEnabled = true, + ), + style = + ColorPickerStyle( + checkerLightColor = checkerLight, + checkerDarkColor = checkerDark, + ), ) - ) val layout = controller.buildLayout(Rect(0, 0, 320, controller.preferredHeight(true))) val out = ArrayList() controller.appendCommands(layout, out) assertTrue(out.any { it is RenderCommand.DrawCheckerboard }) - assertTrue(out.none { command -> - command is RenderCommand.DrawRect && (command.color == checkerLight || command.color == checkerDark) - }) + assertTrue( + out.none { command -> + command is RenderCommand.DrawRect && (command.color == checkerLight || command.color == checkerDark) + }, + ) } + private class RecordingSampler : ScreenColorSampler { var colorCalls: Int = 0 var areaCalls: Int = 0 @@ -298,7 +342,13 @@ class ColorPickerControllerTests { return 0xFF112233.toInt() } - override fun sampleArea(x: Int, y: Int, width: Int, height: Int, outArgb: IntArray): Boolean { + override fun sampleArea( + x: Int, + y: Int, + width: Int, + height: Int, + outArgb: IntArray, + ): Boolean { areaCalls += 1 var index = 0 while (index < width * height && index < outArgb.size) { @@ -309,8 +359,5 @@ class ColorPickerControllerTests { } } - private fun closeEnough(a: Float, b: Float): Boolean { - return kotlin.math.abs(a - b) <= 0.01f - } + private fun closeEnough(a: Float, b: Float): Boolean = kotlin.math.abs(a - b) <= 0.01f } - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInlineNodeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInlineNodeTests.kt index 7b50a81..3fe7ce0 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInlineNodeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInlineNodeTests.kt @@ -15,45 +15,51 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue class ColorPickerInlineNodeTests { - private val ctx = object : UiMeasureContext { - override fun measureText(text: String): Int = text.length * 6 - override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * 6 - override val fontHeight: Int = 9 - override fun fontHeight(fontId: String?, fontSize: Int?): Int = 9 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override fun measureText(text: String): Int = text.length * 6 + + override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * 6 + + override val fontHeight: Int = 9 + + override fun fontHeight(fontId: String?, fontSize: Int?): Int = 9 + + override fun paint(commands: List) = Unit + } @Test fun `controlled inline picker drag survives controlled rerender`() { var controlledValue = RgbaColor(1f, 0f, 0f, 1f) - fun createPicker(): ColorPickerInlineNode { - return ColorPickerInlineNode( + fun createPicker(): ColorPickerInlineNode = + ColorPickerInlineNode( controlled = true, value = controlledValue, mode = ColorFormatMode.HSB, alphaEnabled = true, - key = "picker" + key = "picker", ).apply { closeOnSelect = false onPreviewColor = { controlledValue = it } onChangeColor = { controlledValue = it } onCommitColor = { controlledValue = it } } - } val retained = createPicker() retained.render(ctx, 0, 0, 350, 392) - val layoutProbe = ColorPickerController( - initial = ColorPickerState( - color = RgbaColor(1f, 0f, 0f, 1f), - previous = RgbaColor(1f, 0f, 0f, 1f), - mode = ColorFormatMode.HSB, - alphaEnabled = true, - closeOnSelect = false - ) - ).buildLayout(Rect(0, 0, 350, 392)) + val layoutProbe = + ColorPickerController( + initial = + ColorPickerState( + color = RgbaColor(1f, 0f, 0f, 1f), + previous = RgbaColor(1f, 0f, 0f, 1f), + mode = ColorFormatMode.HSB, + alphaEnabled = true, + closeOnSelect = false, + ), + ).buildLayout(Rect(0, 0, 350, 392)) val startX = layoutProbe.colorFieldRect.x + 4 val startY = layoutProbe.colorFieldRect.y + layoutProbe.colorFieldRect.height - 4 @@ -72,8 +78,8 @@ class ColorPickerInlineNodeTests { lastMouseY = startY, dx = endX - startX, dy = endY - startY, - mouseButton = MouseButton.LEFT - ).also { it.target = retained } + mouseButton = MouseButton.LEFT, + ).also { it.target = retained }, ) assertTrue(afterPress.toArgbInt() != controlledValue.toArgbInt()) @@ -81,15 +87,16 @@ class ColorPickerInlineNodeTests { @Test fun `inline picker draws mode options after clicking mode selector`() { - val picker = ColorPickerInlineNode( - controlled = true, - value = RgbaColor.WHITE, - mode = ColorFormatMode.HEX, - alphaEnabled = true, - key = "picker" - ).apply { - closeOnSelect = false - } + val picker = + ColorPickerInlineNode( + controlled = true, + value = RgbaColor.WHITE, + mode = ColorFormatMode.HEX, + alphaEnabled = true, + key = "picker", + ).apply { + closeOnSelect = false + } picker.render(ctx, 0, 0, 350, 392) val probeLayout = layoutProbe(mode = ColorFormatMode.HEX, alphaEnabled = true) @@ -98,29 +105,32 @@ class ColorPickerInlineNodeTests { MouseDownEvent( probeLayout.modeSelectRect.x + 4, probeLayout.modeSelectRect.y + 4, - MouseButton.LEFT - ).also { it.target = picker } + MouseButton.LEFT, + ).also { it.target = picker }, ) val commands = buildCommands(picker) - val modeTexts = commands.filterIsInstance() - .map { it.text } - .filter { text -> ColorFormatMode.entries.any { it.name == text } } + val modeTexts = + commands + .filterIsInstance() + .map { it.text } + .filter { text -> ColorFormatMode.entries.any { it.name == text } } assertTrue(modeTexts.size >= 5) } @Test fun `inline picker draws eyedropper overlay after clicking pipette`() { - val picker = ColorPickerInlineNode( - controlled = true, - value = RgbaColor.WHITE, - mode = ColorFormatMode.RGB, - alphaEnabled = true, - key = "picker" - ).apply { - closeOnSelect = false - } + val picker = + ColorPickerInlineNode( + controlled = true, + value = RgbaColor.WHITE, + mode = ColorFormatMode.RGB, + alphaEnabled = true, + key = "picker", + ).apply { + closeOnSelect = false + } picker.render(ctx, 0, 0, 350, 392) val probeLayout = layoutProbe(mode = ColorFormatMode.RGB, alphaEnabled = true) @@ -129,8 +139,8 @@ class ColorPickerInlineNodeTests { MouseDownEvent( probeLayout.pipetteRect.x + 4, probeLayout.pipetteRect.y + 4, - MouseButton.LEFT - ).also { it.target = picker } + MouseButton.LEFT, + ).also { it.target = picker }, ) val commands = buildGlobalEyedropperCommands(picker) @@ -145,17 +155,18 @@ class ColorPickerInlineNodeTests { ScreenColorSamplerBridge.install(ScreenColorSampler { _, _ -> sampledArgb }) try { var current = RgbaColor.WHITE - val picker = ColorPickerInlineNode( - controlled = true, - value = current, - mode = ColorFormatMode.RGB, - alphaEnabled = true, - key = "picker" - ).apply { - closeOnSelect = false - onPreviewColor = { current = it } - onChangeColor = { current = it } - } + val picker = + ColorPickerInlineNode( + controlled = true, + value = current, + mode = ColorFormatMode.RGB, + alphaEnabled = true, + key = "picker", + ).apply { + closeOnSelect = false + onPreviewColor = { current = it } + onChangeColor = { current = it } + } picker.render(ctx, 0, 0, 350, 392) val probeLayout = layoutProbe(mode = ColorFormatMode.RGB, alphaEnabled = true) @@ -163,8 +174,8 @@ class ColorPickerInlineNodeTests { MouseDownEvent( probeLayout.pipetteRect.x + 4, probeLayout.pipetteRect.y + 4, - MouseButton.LEFT - ).also { it.target = picker } + MouseButton.LEFT, + ).also { it.target = picker }, ) EventBus.post( @@ -172,8 +183,8 @@ class ColorPickerInlineNodeTests { mouseX = 1200, mouseY = 900, prevX = 300, - prevY = 250 - ).also { it.target = picker } + prevY = 250, + ).also { it.target = picker }, ) picker.captureEyedropperSample() @@ -189,19 +200,19 @@ class ColorPickerInlineNodeTests { ScreenColorSamplerBridge.install(ScreenColorSampler { _, _ -> sampledArgb }) try { var current = RgbaColor.WHITE - fun createPicker(value: RgbaColor): ColorPickerInlineNode { - return ColorPickerInlineNode( + + fun createPicker(value: RgbaColor): ColorPickerInlineNode = + ColorPickerInlineNode( controlled = true, value = value, mode = ColorFormatMode.RGB, alphaEnabled = true, - key = "picker" + key = "picker", ).apply { closeOnSelect = false onPreviewColor = { current = it } onChangeColor = { current = it } } - } val retained = createPicker(current) retained.render(ctx, 0, 0, 350, 392) @@ -210,8 +221,8 @@ class ColorPickerInlineNodeTests { MouseDownEvent( probeLayout.pipetteRect.x + 4, probeLayout.pipetteRect.y + 4, - MouseButton.LEFT - ).also { it.target = retained } + MouseButton.LEFT, + ).also { it.target = retained }, ) val template = createPicker(current) @@ -223,8 +234,8 @@ class ColorPickerInlineNodeTests { mouseX = 1440, mouseY = 820, prevX = 300, - prevY = 250 - ).also { it.target = retained } + prevY = 250, + ).also { it.target = retained }, ) retained.captureEyedropperSample() @@ -237,15 +248,16 @@ class ColorPickerInlineNodeTests { @Test fun `external mode and alpha changes apply after non drag click`() { - val retained = ColorPickerInlineNode( - controlled = true, - value = RgbaColor.WHITE, - mode = ColorFormatMode.HEX, - alphaEnabled = true, - key = "picker" - ).apply { - closeOnSelect = false - } + val retained = + ColorPickerInlineNode( + controlled = true, + value = RgbaColor.WHITE, + mode = ColorFormatMode.HEX, + alphaEnabled = true, + key = "picker", + ).apply { + closeOnSelect = false + } retained.render(ctx, 0, 0, 350, 392) val firstLayout = layoutProbe(mode = ColorFormatMode.HEX, alphaEnabled = true) @@ -253,26 +265,27 @@ class ColorPickerInlineNodeTests { MouseDownEvent( firstLayout.modeSelectRect.x + 4, firstLayout.modeSelectRect.y + 4, - MouseButton.LEFT - ).also { it.target = retained } + MouseButton.LEFT, + ).also { it.target = retained }, ) EventBus.post( MouseUpEvent( firstLayout.modeSelectRect.x + 4, firstLayout.modeSelectRect.y + 4, - MouseButton.LEFT - ).also { it.target = retained } + MouseButton.LEFT, + ).also { it.target = retained }, ) - val template = ColorPickerInlineNode( - controlled = true, - value = RgbaColor.WHITE, - mode = ColorFormatMode.RGB, - alphaEnabled = false, - key = "picker" - ).apply { - closeOnSelect = false - } + val template = + ColorPickerInlineNode( + controlled = true, + value = RgbaColor.WHITE, + mode = ColorFormatMode.RGB, + alphaEnabled = false, + key = "picker", + ).apply { + closeOnSelect = false + } retained.syncFrom(template) retained.render(ctx, 0, 0, 350, 392) @@ -295,20 +308,20 @@ class ColorPickerInlineNodeTests { picker.appendEyedropperOverlayCommands( viewportWidth = 1920, viewportHeight = 1080, - out = out + out = out, ) return out } - private fun layoutProbe(mode: ColorFormatMode, alphaEnabled: Boolean): ColorPickerLayout { - return ColorPickerController( - initial = ColorPickerState( - color = RgbaColor.WHITE, - previous = RgbaColor.WHITE, - mode = mode, - alphaEnabled = alphaEnabled, - closeOnSelect = false - ) + private fun layoutProbe(mode: ColorFormatMode, alphaEnabled: Boolean): ColorPickerLayout = + ColorPickerController( + initial = + ColorPickerState( + color = RgbaColor.WHITE, + previous = RgbaColor.WHITE, + mode = mode, + alphaEnabled = alphaEnabled, + closeOnSelect = false, + ), ).buildLayout(Rect(0, 0, 350, 392)) - } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInlineStylingTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInlineStylingTests.kt index 9de1080..8ebc11b 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInlineStylingTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInlineStylingTests.kt @@ -22,16 +22,17 @@ class ColorPickerInlineStylingTests { @Test fun `inline color picker is stylable in application scope`() { - val stylesDir = createTempStylesDir( - """ - color-picker { - border-width: 3px; - border-color: #224466; - padding: 7px; - width: 280px; - } - """.trimIndent() - ) + val stylesDir = + createTempStylesDir( + """ + color-picker { + border-width: 3px; + border-color: #224466; + padding: 7px; + width: 280px; + } + """.trimIndent(), + ) StyleEngine.setStylesDirectory(stylesDir) StyleEngine.forceReloadStylesheets() diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngineTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngineTests.kt index 4a11070..4e0560b 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngineTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngineTests.kt @@ -6,6 +6,7 @@ import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.overlay.OverlayLayerContracts import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.overlay.UiLayerId +import org.dreamfinity.dsgl.core.render.RenderCommand import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -13,7 +14,6 @@ import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertSame import kotlin.test.assertTrue -import org.dreamfinity.dsgl.core.render.RenderCommand class ColorPickerPopupEngineTests { @Test @@ -24,8 +24,8 @@ class ColorPickerPopupEngineTests { ColorPickerPopupRequest( owner = "owner", anchorRect = Rect(120, 80, 40, 18), - state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false) - ) + state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), + ), ) assertTrue(engine.isOpen()) @@ -42,8 +42,8 @@ class ColorPickerPopupEngineTests { ColorPickerPopupRequest( owner = "owner", anchorRect = Rect(40, 40, 20, 20), - state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false) - ) + state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), + ), ) assertFalse(engine.handleKeyDown(KeyCodes.ESCAPE)) @@ -59,8 +59,8 @@ class ColorPickerPopupEngineTests { ColorPickerPopupRequest( owner = owner, anchorRect = Rect(100, 80, 32, 20), - state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false) - ) + state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), + ), ) val panel = engine.debugPanelRect(owner) ?: error("panel missing") val closeX = panel.x + panel.width - 14 @@ -74,11 +74,12 @@ class ColorPickerPopupEngineTests { val engine = ColorPickerPopupEngine() val owner = "owner" engine.onFrame(640, 360) - val request = ColorPickerPopupRequest( - owner = owner, - anchorRect = Rect(100, 80, 32, 20), - state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false) - ) + val request = + ColorPickerPopupRequest( + owner = owner, + anchorRect = Rect(100, 80, 32, 20), + state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), + ) engine.open(request) assertNotNull(engine.debugPanelRect(owner)) @@ -100,8 +101,8 @@ class ColorPickerPopupEngineTests { ColorPickerPopupRequest( owner = owner, anchorRect = Rect(100, 80, 32, 20), - state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false) - ) + state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), + ), ) val firstController = engine.debugController(owner) ?: error("controller missing") @@ -109,8 +110,8 @@ class ColorPickerPopupEngineTests { ColorPickerPopupRequest( owner = owner, anchorRect = Rect(120, 90, 32, 20), - state = ColorPickerState(color = RgbaColor(0f, 0f, 0f, 1f), closeOnSelect = false) - ) + state = ColorPickerState(color = RgbaColor(0f, 0f, 0f, 1f), closeOnSelect = false), + ), ) val secondController = engine.debugController(owner) ?: error("controller missing") @@ -127,8 +128,8 @@ class ColorPickerPopupEngineTests { ColorPickerPopupRequest( owner = owner, anchorRect = Rect(180, 120, 32, 20), - state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false) - ) + state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), + ), ) val panelBeforeFieldDrag = engine.debugPanelRect(owner) ?: error("panel missing") val layout = engine.debugBodyLayout(owner) ?: error("layout missing") @@ -160,8 +161,8 @@ class ColorPickerPopupEngineTests { ColorPickerPopupRequest( owner = owner, anchorRect = Rect(180, 120, 32, 20), - state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false) - ) + state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), + ), ) val layout = engine.debugBodyLayout(owner) ?: error("layout missing") val x = layout.colorFieldRect.x + 12 @@ -185,8 +186,8 @@ class ColorPickerPopupEngineTests { anchorRect = Rect(180, 120, 32, 20), state = ColorPickerState(color = RgbaColor(1f, 0f, 0f, 1f), closeOnSelect = false), onPreview = { previews += it }, - onCommit = { committed = it } - ) + onCommit = { committed = it }, + ), ) val layout = engine.debugBodyLayout(owner) ?: error("layout missing") val startX = layout.colorFieldRect.x + 4 @@ -202,7 +203,12 @@ class ColorPickerPopupEngineTests { assertTrue(engine.handleMouseUp(endX, endY, MouseButton.LEFT)) assertTrue(previews.size >= 2) - assertTrue(previews.map { it.toArgbInt() }.distinct().size >= 2) + assertTrue( + previews + .map { it.toArgbInt() } + .distinct() + .size >= 2, + ) assertEquals(previews.last().toArgbInt(), committed?.toArgbInt()) } @@ -217,8 +223,8 @@ class ColorPickerPopupEngineTests { owner = owner, anchorRect = Rect(180, 120, 32, 20), state = ColorPickerState(color = RgbaColor(1f, 0f, 0f, 1f), closeOnSelect = false), - onCommit = { committed = it } - ) + onCommit = { committed = it }, + ), ) val layout = engine.debugBodyLayout(owner) ?: error("layout missing") val startX = layout.hueRect.x + 4 @@ -228,9 +234,17 @@ class ColorPickerPopupEngineTests { assertTrue(engine.handleMouseDown(startX, y, MouseButton.LEFT)) assertTrue(engine.handleMouseMove(midX, y)) - val midColor = engine.debugController(owner)?.snapshot()?.color ?: error("controller missing") + val midColor = + engine + .debugController(owner) + ?.snapshot() + ?.color ?: error("controller missing") assertTrue(engine.handleMouseMove(endX, y)) - val endColor = engine.debugController(owner)?.snapshot()?.color ?: error("controller missing") + val endColor = + engine + .debugController(owner) + ?.snapshot() + ?.color ?: error("controller missing") assertTrue(engine.handleMouseUp(endX, y, MouseButton.LEFT)) assertTrue(midColor.toArgbInt() != endColor.toArgbInt()) @@ -248,14 +262,15 @@ class ColorPickerPopupEngineTests { ColorPickerPopupRequest( owner = owner, anchorRect = Rect(180, 120, 32, 20), - state = ColorPickerState( - color = RgbaColor(0.5f, 0.4f, 0.3f, 1f), - closeOnSelect = false, - alphaEnabled = true - ), + state = + ColorPickerState( + color = RgbaColor(0.5f, 0.4f, 0.3f, 1f), + closeOnSelect = false, + alphaEnabled = true, + ), onPreview = { previews += it }, - onCommit = { committed = it } - ) + onCommit = { committed = it }, + ), ) val layout = engine.debugBodyLayout(owner) ?: error("layout missing") val alphaRect = layout.alphaRect ?: error("alpha rect missing") @@ -283,8 +298,8 @@ class ColorPickerPopupEngineTests { ColorPickerPopupRequest( owner = owner, anchorRect = Rect(120, 100, 20, 20), - state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false) - ) + state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), + ), ) val header = engine.debugHeaderRect(owner) ?: error("header missing") val startX = header.x + 6 @@ -299,8 +314,8 @@ class ColorPickerPopupEngineTests { ColorPickerPopupRequest( owner = owner, anchorRect = Rect(20, 20, 16, 16), - state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false) - ) + state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), + ), ) val reopened = engine.debugPanelRect(owner) ?: error("panel missing") assertEquals(moved.x, reopened.x) @@ -320,8 +335,8 @@ class ColorPickerPopupEngineTests { owner = owner, anchorRect = Rect(120, 80, 18, 18), state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), - onCommit = { commits++ } - ) + onCommit = { commits++ }, + ), ) val layout = engine.debugBodyLayout(owner) ?: error("layout missing") assertTrue(engine.handleMouseDown(layout.pipetteRect.x + 2, layout.pipetteRect.y + 2, MouseButton.LEFT)) @@ -344,12 +359,14 @@ class ColorPickerPopupEngineTests { @Test fun `pipette samples from capture pass before commit`() { - ScreenColorSamplerBridge.install(ScreenColorSampler { x, y -> - val r = (x and 0xFF) - val g = (y and 0xFF) - val b = 0x44 - (0xFF shl 24) or (r shl 16) or (g shl 8) or b - }) + ScreenColorSamplerBridge.install( + ScreenColorSampler { x, y -> + val r = (x and 0xFF) + val g = (y and 0xFF) + val b = 0x44 + (0xFF shl 24) or (r shl 16) or (g shl 8) or b + }, + ) try { val engine = ColorPickerPopupEngine() val owner = "owner" @@ -360,8 +377,8 @@ class ColorPickerPopupEngineTests { owner = owner, anchorRect = Rect(120, 80, 18, 18), state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), - onCommit = { committed = it } - ) + onCommit = { committed = it }, + ), ) val layout = engine.debugBodyLayout(owner) ?: error("layout missing") assertTrue(engine.handleMouseDown(layout.pipetteRect.x + 2, layout.pipetteRect.y + 2, MouseButton.LEFT)) @@ -379,12 +396,14 @@ class ColorPickerPopupEngineTests { @Test fun `pipette capture updates preview continuously while moving`() { - ScreenColorSamplerBridge.install(ScreenColorSampler { x, y -> - val r = (x and 0xFF) - val g = (y and 0xFF) - val b = 0x55 - (0xFF shl 24) or (r shl 16) or (g shl 8) or b - }) + ScreenColorSamplerBridge.install( + ScreenColorSampler { x, y -> + val r = (x and 0xFF) + val g = (y and 0xFF) + val b = 0x55 + (0xFF shl 24) or (r shl 16) or (g shl 8) or b + }, + ) try { val engine = ColorPickerPopupEngine() val owner = "owner" @@ -395,19 +414,27 @@ class ColorPickerPopupEngineTests { owner = owner, anchorRect = Rect(120, 80, 18, 18), state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), - onPreview = { previews += it } - ) + onPreview = { previews += it }, + ), ) val layout = engine.debugBodyLayout(owner) ?: error("layout missing") assertTrue(engine.handleMouseDown(layout.pipetteRect.x + 2, layout.pipetteRect.y + 2, MouseButton.LEFT)) engine.handleMouseMove(40, 72) engine.captureEyedropperSample() - val first = engine.debugController(owner)?.snapshot()?.color ?: error("controller missing") + val first = + engine + .debugController(owner) + ?.snapshot() + ?.color ?: error("controller missing") engine.handleMouseMove(96, 24) engine.captureEyedropperSample() - val second = engine.debugController(owner)?.snapshot()?.color ?: error("controller missing") + val second = + engine + .debugController(owner) + ?.snapshot() + ?.color ?: error("controller missing") assertNotEquals(first.toArgbInt(), second.toArgbInt()) assertTrue(previews.isNotEmpty()) @@ -427,8 +454,8 @@ class ColorPickerPopupEngineTests { owner = owner, ownerScope = OverlayOwnerScope.Application, anchorRect = Rect(120, 80, 18, 18), - state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false) - ) + state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), + ), ) val layout = engine.debugBodyLayout(owner) ?: error("layout missing") assertTrue(engine.handleMouseDown(layout.pipetteRect.x + 2, layout.pipetteRect.y + 2, MouseButton.LEFT)) @@ -439,7 +466,10 @@ class ColorPickerPopupEngineTests { assertTrue(overlay.isNotEmpty()) assertEquals(OverlayOwnerScope.Application, engine.debugActiveOwnerScope()) - assertEquals(UiLayerId.ApplicationOverlay, OverlayLayerContracts.resolveTransientLayer(engine.debugActiveOwnerScope()!!)) + assertEquals( + UiLayerId.ApplicationOverlay, + OverlayLayerContracts.resolveTransientLayer(engine.debugActiveOwnerScope()!!), + ) } @Test @@ -452,8 +482,8 @@ class ColorPickerPopupEngineTests { owner = owner, ownerScope = OverlayOwnerScope.System, anchorRect = Rect(120, 80, 18, 18), - state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false) - ) + state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), + ), ) val layout = engine.debugBodyLayout(owner) ?: error("layout missing") assertTrue(engine.handleMouseDown(layout.pipetteRect.x + 2, layout.pipetteRect.y + 2, MouseButton.LEFT)) @@ -464,7 +494,10 @@ class ColorPickerPopupEngineTests { assertTrue(overlay.isNotEmpty()) assertEquals(OverlayOwnerScope.System, engine.debugActiveOwnerScope()) - assertEquals(UiLayerId.SystemOverlay, OverlayLayerContracts.resolveTransientLayer(engine.debugActiveOwnerScope()!!)) + assertEquals( + UiLayerId.SystemOverlay, + OverlayLayerContracts.resolveTransientLayer(engine.debugActiveOwnerScope()!!), + ) } @Test @@ -476,8 +509,8 @@ class ColorPickerPopupEngineTests { ColorPickerPopupRequest( owner = owner, anchorRect = Rect(120, 80, 18, 18), - state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false) - ) + state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), + ), ) val initialLayout = engine.debugBodyLayout(owner) ?: error("layout missing") assertTrue(initialLayout.modeOptionsRect == null) @@ -485,8 +518,8 @@ class ColorPickerPopupEngineTests { engine.handleMouseDown( initialLayout.modeSelectRect.x + 2, initialLayout.modeSelectRect.y + 2, - MouseButton.LEFT - ) + MouseButton.LEFT, + ), ) val openedLayout = engine.debugBodyLayout(owner) ?: error("layout missing") assertNotNull(openedLayout.modeOptionsRect) @@ -504,15 +537,16 @@ class ColorPickerPopupEngineTests { ColorPickerPopupRequest( owner = owner, anchorRect = Rect(120, 80, 18, 18), - state = ColorPickerState( - color = RgbaColor.WHITE, - mode = ColorFormatMode.RGB, - alphaEnabled = true, - closeOnSelect = false - ), + state = + ColorPickerState( + color = RgbaColor.WHITE, + mode = ColorFormatMode.RGB, + alphaEnabled = true, + closeOnSelect = false, + ), onPreview = { previews += it }, - onCommit = { committed = it } - ) + onCommit = { committed = it }, + ), ) val layout = engine.debugBodyLayout(owner) ?: error("layout missing") val redInput = layout.inputSlots.firstOrNull { it.key == "r" } ?: error("red input missing") @@ -548,8 +582,8 @@ class ColorPickerPopupEngineTests { owner = owner, anchorRect = Rect(120, 80, 18, 18), title = "Popup Test", - state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false) - ) + state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), + ), ) engine.onFrame(900, 700) @@ -571,8 +605,8 @@ class ColorPickerPopupEngineTests { owner = owner, anchorRect = Rect(180, 120, 32, 20), state = ColorPickerState(color = RgbaColor(1f, 0f, 0f, 1f), closeOnSelect = false), - onPreview = { previews += it } - ) + onPreview = { previews += it }, + ), ) val layout = engine.debugBodyLayout(owner) ?: error("layout missing") val startX = layout.colorFieldRect.x + 4 @@ -584,19 +618,27 @@ class ColorPickerPopupEngineTests { assertTrue(engine.handleMouseDown(startX, startY, MouseButton.LEFT)) assertTrue(engine.handleMouseMove(midX, midY)) - val midColor = engine.debugController(owner)?.snapshot()?.color ?: error("controller missing") + val midColor = + engine + .debugController(owner) + ?.snapshot() + ?.color ?: error("controller missing") engine.sync( ColorPickerPopupRequest( owner = owner, anchorRect = Rect(180, 120, 32, 20), - state = ColorPickerState(color = RgbaColor(0f, 1f, 0f, 1f), closeOnSelect = false) - ) + state = ColorPickerState(color = RgbaColor(0f, 1f, 0f, 1f), closeOnSelect = false), + ), ) assertTrue(engine.handleMouseMove(endX, endY)) assertTrue(engine.handleMouseUp(endX, endY, MouseButton.LEFT)) - val finalColor = engine.debugController(owner)?.snapshot()?.color ?: error("controller missing") + val finalColor = + engine + .debugController(owner) + ?.snapshot() + ?.color ?: error("controller missing") assertNotEquals(midColor.toArgbInt(), finalColor.toArgbInt()) assertTrue(previews.size >= 2) } @@ -610,8 +652,8 @@ class ColorPickerPopupEngineTests { ColorPickerPopupRequest( owner = owner, anchorRect = Rect(120, 80, 18, 18), - state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false) - ) + state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), + ), ) val layout = engine.debugBodyLayout(owner) ?: error("layout missing") assertTrue(engine.handleMouseDown(layout.pipetteRect.x + 2, layout.pipetteRect.y + 2, MouseButton.LEFT)) @@ -621,8 +663,8 @@ class ColorPickerPopupEngineTests { ColorPickerPopupRequest( owner = owner, anchorRect = Rect(120, 80, 18, 18), - state = ColorPickerState(color = RgbaColor(0.2f, 0.3f, 0.4f, 1f), closeOnSelect = false) - ) + state = ColorPickerState(color = RgbaColor(0.2f, 0.3f, 0.4f, 1f), closeOnSelect = false), + ), ) assertTrue(engine.debugController(owner)?.isEyedropperActive() == true) @@ -637,8 +679,8 @@ class ColorPickerPopupEngineTests { ColorPickerPopupRequest( owner = owner, anchorRect = Rect(120, 80, 18, 18), - state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false) - ) + state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), + ), ) assertFalse(engine.hasActiveEyedropper()) @@ -647,6 +689,7 @@ class ColorPickerPopupEngineTests { assertTrue(engine.hasActiveEyedropper()) } + @Test fun `manager reuses same owner token`() { val fakeHost = FakeColorPickerHost() @@ -654,12 +697,12 @@ class ColorPickerPopupEngineTests { manager.open( anchorRect = Rect(10, 10, 10, 10), title = "A", - state = ColorPickerState(RgbaColor.WHITE) + state = ColorPickerState(RgbaColor.WHITE), ) manager.open( anchorRect = Rect(20, 20, 10, 10), title = "B", - state = ColorPickerState(RgbaColor(0f, 0f, 0f, 1f)) + state = ColorPickerState(RgbaColor(0f, 0f, 0f, 1f)), ) assertEquals(2, fakeHost.opened.size) @@ -678,13 +721,13 @@ class ColorPickerPopupEngineTests { manager.open( anchorRect = Rect(10, 10, 10, 10), title = "App", - state = ColorPickerState(RgbaColor.WHITE) + state = ColorPickerState(RgbaColor.WHITE), ) manager.open( ownerScope = OverlayOwnerScope.System, anchorRect = Rect(20, 20, 10, 10), title = "System", - state = ColorPickerState(RgbaColor.WHITE) + state = ColorPickerState(RgbaColor.WHITE), ) assertEquals(2, fakeHost.opened.size) @@ -713,5 +756,3 @@ class ColorPickerPopupEngineTests { override fun isOpen(): Boolean = false } } - - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupGeometryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupGeometryTests.kt index 3a40f23..893467b 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupGeometryTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupGeometryTests.kt @@ -8,11 +8,12 @@ import kotlin.test.assertTrue class ColorPickerPopupGeometryTests { @Test fun `buildFrame derives header body and close rectangles from panel`() { - val frame = ColorPickerPopupGeometry.buildFrame( - panelRect = Rect(100, 80, 320, 260), - headerHeight = 26, - panelPadding = 6 - ) + val frame = + ColorPickerPopupGeometry.buildFrame( + panelRect = Rect(100, 80, 320, 260), + headerHeight = 26, + panelPadding = 6, + ) assertEquals(Rect(100, 80, 320, 260), frame.panelRect) assertEquals(Rect(100, 80, 320, 26), frame.headerRect) @@ -22,11 +23,12 @@ class ColorPickerPopupGeometryTests { @Test fun `clampPanel keeps popup inside viewport with margins`() { - val clamped = ColorPickerPopupGeometry.clampPanel( - rect = Rect(-50, 999, 300, 220), - viewportWidth = 640, - viewportHeight = 360 - ) + val clamped = + ColorPickerPopupGeometry.clampPanel( + rect = Rect(-50, 999, 300, 220), + viewportWidth = 640, + viewportHeight = 360, + ) assertEquals(2, clamped.x) assertEquals(138, clamped.y) @@ -37,20 +39,22 @@ class ColorPickerPopupGeometryTests { @Test fun `resolvePanelRect uses remembered position when available`() { val owner = "owner" - val store = ColorPickerPopupPositionStore().apply { - remember(owner, Rect(140, 120, 300, 200)) - } - val resolved = ColorPickerPopupGeometry.resolvePanelRect( - owner = owner, - anchorRect = Rect(20, 20, 10, 10), - width = 320, - height = 260, - viewportWidth = 800, - viewportHeight = 600, - keepPosition = false, - currentRect = null, - store = store - ) + val store = + ColorPickerPopupPositionStore().apply { + remember(owner, Rect(140, 120, 300, 200)) + } + val resolved = + ColorPickerPopupGeometry.resolvePanelRect( + owner = owner, + anchorRect = Rect(20, 20, 10, 10), + width = 320, + height = 260, + viewportWidth = 800, + viewportHeight = 600, + keepPosition = false, + currentRect = null, + store = store, + ) assertEquals(140, resolved.x) assertEquals(120, resolved.y) @@ -60,17 +64,18 @@ class ColorPickerPopupGeometryTests { @Test fun `resolvePanelRect honors keepPosition using current rect`() { - val resolved = ColorPickerPopupGeometry.resolvePanelRect( - owner = "owner", - anchorRect = Rect(20, 20, 10, 10), - width = 300, - height = 240, - viewportWidth = 800, - viewportHeight = 600, - keepPosition = true, - currentRect = Rect(200, 150, 120, 80), - store = ColorPickerPopupPositionStore() - ) + val resolved = + ColorPickerPopupGeometry.resolvePanelRect( + owner = "owner", + anchorRect = Rect(20, 20, 10, 10), + width = 300, + height = 240, + viewportWidth = 800, + viewportHeight = 600, + keepPosition = true, + currentRect = Rect(200, 150, 120, 80), + store = ColorPickerPopupPositionStore(), + ) assertEquals(Rect(200, 150, 300, 240), resolved) assertTrue(resolved.x >= 2 && resolved.y >= 2) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerStateTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerStateTests.kt index 017dbb4..e68eedf 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerStateTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerStateTests.kt @@ -6,10 +6,11 @@ import kotlin.test.assertEquals class ColorPickerStateTests { @Test fun `disables alpha by forcing opaque color`() { - val state = ColorPickerState( - color = RgbaColor(0.2f, 0.4f, 0.6f, 0.3f), - alphaEnabled = false - ) + val state = + ColorPickerState( + color = RgbaColor(0.2f, 0.4f, 0.6f, 0.3f), + alphaEnabled = false, + ) val updated = state.withColor(RgbaColor(0.1f, 0.2f, 0.3f, 0.1f)) assertEquals(1f, updated.color.a) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorTextCodecTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorTextCodecTests.kt index c3a62ba..54e077c 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorTextCodecTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorTextCodecTests.kt @@ -54,7 +54,13 @@ class ColorTextCodecTests { fun `rgb hsl and hsb formatting includes mode-specific prefixes`() { val source = RgbaColor(0.1f, 0.5f, 0.9f, 0.7f) val rgb = ColorTextCodec.format(source, ColorFormatMode.RGB, includeAlpha = true) - val argb = ColorTextCodec.format(source, ColorFormatMode.RGB, includeAlpha = true, rgbOrder = RgbChannelOrder.ARGB) + val argb = + ColorTextCodec.format( + source, + ColorFormatMode.RGB, + includeAlpha = true, + rgbOrder = RgbChannelOrder.ARGB, + ) val hsl = ColorTextCodec.format(source, ColorFormatMode.HSL, includeAlpha = true) val hsb = ColorTextCodec.format(source, ColorFormatMode.HSB, includeAlpha = true) @@ -73,7 +79,5 @@ class ColorTextCodecTests { assertEquals(234f, hsl.hueDeg) } - private fun closeEnough(a: Float, b: Float): Boolean { - return abs(a - b) <= 0.01f - } + private fun closeEnough(a: Float, b: Float): Boolean = abs(a - b) <= 0.01f } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalHostHookOwnerPropagationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalHostHookOwnerPropagationTests.kt index a87ba1a..f26b92d 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalHostHookOwnerPropagationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalHostHookOwnerPropagationTests.kt @@ -23,10 +23,7 @@ class ModalHostHookOwnerPropagationTests { assertEquals(6, window.lastSeenCount) } - private fun renderWithHookSession( - window: DsglWindow, - mode: HookRenderSessionMode = HookRenderSessionMode.Normal - ): DomTree { + private fun renderWithHookSession(window: DsglWindow, mode: HookRenderSessionMode = HookRenderSessionMode.Normal): DomTree { window.beginRenderBuild(mode) return try { window.render() @@ -40,11 +37,11 @@ class ModalHostHookOwnerPropagationTests { var pendingMutation: Int? = null var lastSeenCount: Int = -1 - override fun render(): DomTree { - return ui { + override fun render(): DomTree = + ui { modalHost( modals = emptyList(), - modalKey = "test.modal.host" + modalKey = "test.modal.host", ) { var count by useState(0) pendingMutation?.let { mutation -> @@ -54,6 +51,5 @@ class ModalHostHookOwnerPropagationTests { lastSeenCount = count } } - } } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalHostKeyboardRegressionTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalHostKeyboardRegressionTests.kt index 57c573f..387f368 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalHostKeyboardRegressionTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalHostKeyboardRegressionTests.kt @@ -1,14 +1,14 @@ package org.dreamfinity.dsgl.core.components.modal import org.dreamfinity.dsgl.core.DomTree +import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.dsl.div +import org.dreamfinity.dsgl.core.dsl.ui import org.dreamfinity.dsgl.core.event.EventBus import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.KeyboardKeyDownEvent -import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext -import org.dreamfinity.dsgl.core.dsl.div import org.dreamfinity.dsgl.core.render.RenderCommand -import org.dreamfinity.dsgl.core.dsl.ui import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals @@ -17,13 +17,18 @@ import kotlin.test.assertNotNull class ModalHostKeyboardRegressionTests { private val trees: MutableList = ArrayList() - private val measureContext = object : UiMeasureContext { - override fun measureText(text: String): Int = text.length * 6 - override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * 6 - override val fontHeight: Int = 9 - override fun fontHeight(fontId: String?, fontSize: Int?): Int = 9 - override fun paint(commands: List) = Unit - } + private val measureContext = + object : UiMeasureContext { + override fun measureText(text: String): Int = text.length * 6 + + override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * 6 + + override val fontHeight: Int = 9 + + override fun fontHeight(fontId: String?, fontSize: Int?): Int = 9 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -64,7 +69,9 @@ class ModalHostKeyboardRegressionTests { tree.render(measureContext, 1920, 1080) - val host = tree.root.children.firstOrNull() + val host = + tree.root.children + .firstOrNull() assertNotNull(host) assertEquals(0, host.bounds.x) assertEquals(0, host.bounds.y) @@ -77,19 +84,17 @@ class ModalHostKeyboardRegressionTests { assertEquals(1080, content.bounds.height) } - private fun buildTree(hostKey: String, modals: List): DomTree { - return ui { + private fun buildTree(hostKey: String, modals: List): DomTree = + ui { modalHost(modals = modals, modalKey = hostKey) { div({ key = "$hostKey.content" }) } } - } - private fun staticModal(): ModalSpec { - return ModalSpec( + private fun staticModal(): ModalSpec = + ModalSpec( key = "modal.static", backdrop = BackdropMode.Static, - keyboard = false + keyboard = false, ) { _ -> } - } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalRuntimeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalRuntimeTests.kt index 73a3c92..a520ae7 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalRuntimeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalRuntimeTests.kt @@ -90,7 +90,9 @@ class ModalRuntimeTests { error("Missing node key=$key") } - private class FocusableNode(key: Any) : DOMNode(key) { + private class FocusableNode( + key: Any, + ) : DOMNode(key) { override val focusable: Boolean = true } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngineTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngineTests.kt index b1299ef..5b41022 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngineTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngineTests.kt @@ -13,41 +13,49 @@ import kotlin.test.assertTrue class ContextMenuEngineTests { private class FakeClock( - var now: Long = 0L + var now: Long = 0L, ) : ContextMenuClock { override fun nowMs(): Long = now + fun advance(ms: Long) { now += ms } } - private val ctx = object : UiMeasureContext { - override fun measureText(text: String): Int = text.length * 6 - override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * 6 - override val fontHeight: Int = 9 - override fun fontHeight(fontId: String?, fontSize: Int?): Int = 9 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override fun measureText(text: String): Int = text.length * 6 + + override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * 6 + + override val fontHeight: Int = 9 + + override fun fontHeight(fontId: String?, fontSize: Int?): Int = 9 + + override fun paint(commands: List) = Unit + } @Test fun `stack supports open submenu switch esc pop and outside close`() { val clock = FakeClock() - val style = ContextMenuStyle( - hoverOpenDelayMs = 80L, - submenuCloseDelayMs = 120L - ) + val style = + ContextMenuStyle( + hoverOpenDelayMs = 80L, + submenuCloseDelayMs = 120L, + ) val engine = ContextMenuEngine(clock = clock) engine.setStyle(style) - val model = contextMenu(id = "state.machine") { - submenu("More", id = "more") { - item("A") - item("B") - } - submenu("Tools", id = "tools") { - item("Hammer") + val model = + contextMenu(id = "state.machine") { + submenu("More", id = "more") { + item("A") + item("B") + } + submenu("Tools", id = "tools") { + item("Hammer") + } + item("Leaf", id = "leaf") } - item("Leaf", id = "leaf") - } engine.openAtCursor(model, 20, 20) engine.onFrame(ctx, 320, 180, 1f) @@ -64,7 +72,13 @@ class ContextMenuEngineTests { clock.advance(style.hoverOpenDelayMs + 1L) engine.onFrame(ctx, 320, 180, 1f) assertEquals(2, engine.snapshot().levelCount) - assertEquals(1, engine.snapshot().hoveredIndices.first()) + assertEquals( + 1, + engine + .snapshot() + .hoveredIndices + .first(), + ) assertTrue(engine.handleKeyDown(KeyCodes.ESCAPE)) assertEquals(1, engine.snapshot().levelCount) @@ -82,10 +96,11 @@ class ContextMenuEngineTests { fun `overlay consumes pointer before base dispatch when menu is open`() { val clock = FakeClock() val engine = ContextMenuEngine(clock = clock) - val model = contextMenu(id = "overlay.order") { - item("Run") - item("Build") - } + val model = + contextMenu(id = "overlay.order") { + item("Run") + item("Build") + } engine.openAtCursor(model, 24, 24) engine.onFrame(ctx, 320, 180, 1f) @@ -105,13 +120,14 @@ class ContextMenuEngineTests { val clock = FakeClock() val engine = ContextMenuEngine(clock = clock) var actionHits = 0 - val model = contextMenu(id = "keyboard") { - submenu("RootSub") { - item("Leaf") { - onClick { actionHits += 1 } + val model = + contextMenu(id = "keyboard") { + submenu("RootSub") { + item("Leaf") { + onClick { actionHits += 1 } + } } } - } engine.openAtCursor(model, 20, 20) engine.onFrame(ctx, 320, 180, 1f) @@ -128,10 +144,11 @@ class ContextMenuEngineTests { @Test fun `open at cursor keeps root panel under click point`() { val engine = ContextMenuEngine() - val model = contextMenu(id = "placement.cursor") { - item("Open") - item("Rename") - } + val model = + contextMenu(id = "placement.cursor") { + item("Open") + item("Rename") + } val openX = 84 val openY = 62 @@ -146,10 +163,11 @@ class ContextMenuEngineTests { @Test fun `open anchored positions root panel below anchor`() { val engine = ContextMenuEngine() - val model = contextMenu(id = "placement.anchor") { - item("Open") - item("Rename") - } + val model = + contextMenu(id = "placement.anchor") { + item("Open") + item("Rename") + } val anchor = Rect(48, 34, 96, 18) engine.openAnchored(model, anchor) @@ -164,9 +182,10 @@ class ContextMenuEngineTests { @Test fun `menu model font size is used for rendered text`() { val engine = ContextMenuEngine() - val model = contextMenu(id = "font.size", fontSize = 24) { - item("Open") - } + val model = + contextMenu(id = "font.size", fontSize = 24) { + item("Open") + } engine.openAtCursor(model, 20, 20) engine.onFrame(ctx, 320, 180, 1f) @@ -185,5 +204,6 @@ class ContextMenuEngineTests { } private fun centerX(rect: Rect): Int = rect.x + rect.width / 2 + private fun centerY(rect: Rect): Int = rect.y + rect.height / 2 } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuMeasurementCacheTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuMeasurementCacheTests.kt index 3c45ca9..e0de9eb 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuMeasurementCacheTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuMeasurementCacheTests.kt @@ -8,21 +8,27 @@ import kotlin.test.assertSame import kotlin.test.assertTrue class ContextMenuMeasurementCacheTests { - private val ctx = object : UiMeasureContext { - override fun measureText(text: String): Int = text.length * 6 - override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * 6 - override val fontHeight: Int = 9 - override fun fontHeight(fontId: String?, fontSize: Int?): Int = 9 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override fun measureText(text: String): Int = text.length * 6 + + override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * 6 + + override val fontHeight: Int = 9 + + override fun fontHeight(fontId: String?, fontSize: Int?): Int = 9 + + override fun paint(commands: List) = Unit + } @Test fun `measurement reuses cached value for unchanged inputs`() { val cache = ContextMenuMeasurementCache() - val model = contextMenu(id = "cache.same") { - item("Copy") - item("Paste") - } + val model = + contextMenu(id = "cache.same") { + item("Copy") + item("Paste") + } val first = cache.measure(model.token, model.entries, ContextMenuStyle(), null, null, ctx, 1f) val second = cache.measure(model.token, model.entries, ContextMenuStyle(), null, null, ctx, 1f) @@ -34,9 +40,10 @@ class ContextMenuMeasurementCacheTests { fun `measurement recomputes when resolved content changes`() { val cache = ContextMenuMeasurementCache() var dynamicLabel = "Open" - val model = contextMenu(id = "cache.dynamic") { - item({ dynamicLabel }) - } + val model = + contextMenu(id = "cache.dynamic") { + item({ dynamicLabel }) + } val first = cache.measure(model.token, model.entries, ContextMenuStyle(), null, null, ctx, 1f) dynamicLabel = "Open in new window" @@ -49,10 +56,11 @@ class ContextMenuMeasurementCacheTests { @Test fun `measurement recomputes when style hash changes`() { val cache = ContextMenuMeasurementCache() - val model = contextMenu(id = "cache.style") { - item("Run") - item("Rename") - } + val model = + contextMenu(id = "cache.style") { + item("Run") + item("Rename") + } cache.measure(model.token, model.entries, ContextMenuStyle(), null, null, ctx, 1f) cache.measure(model.token, model.entries, ContextMenuStyle(minPanelWidth = 220), null, null, ctx, 1f) @@ -63,14 +71,16 @@ class ContextMenuMeasurementCacheTests { @Test fun `measurement expands panel when icon column grows`() { val cache = ContextMenuMeasurementCache() - val noIcons = contextMenu(id = "cache.icons.none") { - item("Rename") - } - val withIcons = contextMenu(id = "cache.icons.present") { - item("Rename") { - icon("FILE-LONG") + val noIcons = + contextMenu(id = "cache.icons.none") { + item("Rename") + } + val withIcons = + contextMenu(id = "cache.icons.present") { + item("Rename") { + icon("FILE-LONG") + } } - } val style = ContextMenuStyle(minPanelWidth = 1) val noIconMeasurement = cache.measure(noIcons.token, noIcons.entries, style, null, null, ctx, 1f) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/contextmenu/PopupPlacementTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/contextmenu/PopupPlacementTests.kt index 8f0f8a8..b472eb4 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/contextmenu/PopupPlacementTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/contextmenu/PopupPlacementTests.kt @@ -9,15 +9,16 @@ import kotlin.test.assertTrue class PopupPlacementTests { @Test fun `submenu flips left when overflowing right edge`() { - val result = PopupPlacement.resolve( - PopupPlacementRequest( - preferredRect = Rect(290, 40, 120, 100), - popupSize = Size(120, 100), - viewport = Rect(0, 0, 320, 180), - padding = 6, - horizontalFlipX = 150 + val result = + PopupPlacement.resolve( + PopupPlacementRequest( + preferredRect = Rect(290, 40, 120, 100), + popupSize = Size(120, 100), + viewport = Rect(0, 0, 320, 180), + padding = 6, + horizontalFlipX = 150, + ), ) - ) assertTrue(result.flippedHorizontally) assertEquals(150, result.rect.x) @@ -25,14 +26,15 @@ class PopupPlacementTests { @Test fun `placement clamps vertically into viewport`() { - val result = PopupPlacement.resolve( - PopupPlacementRequest( - preferredRect = Rect(40, 170, 120, 80), - popupSize = Size(120, 80), - viewport = Rect(0, 0, 320, 180), - padding = 6 + val result = + PopupPlacement.resolve( + PopupPlacementRequest( + preferredRect = Rect(40, 170, 120, 80), + popupSize = Size(120, 80), + viewport = Rect(0, 0, 320, 180), + padding = 6, + ), ) - ) assertTrue(result.clampedVertically) assertEquals(94, result.rect.y) @@ -40,14 +42,15 @@ class PopupPlacementTests { @Test fun `oversized popup is constrained to viewport bounds`() { - val result = PopupPlacement.resolve( - PopupPlacementRequest( - preferredRect = Rect(0, 0, 600, 500), - popupSize = Size(600, 500), - viewport = Rect(0, 0, 320, 180), - padding = 8 + val result = + PopupPlacement.resolve( + PopupPlacementRequest( + preferredRect = Rect(0, 0, 600, 500), + popupSize = Size(600, 500), + viewport = Rect(0, 0, 320, 180), + padding = 8, + ), ) - ) assertEquals(304, result.rect.width) assertEquals(164, result.rect.height) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHostTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHostTests.kt index 7c675a3..d3e4051 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHostTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHostTests.kt @@ -1,23 +1,27 @@ package org.dreamfinity.dsgl.core.debug +import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.overlay.UiLayerId +import org.dreamfinity.dsgl.core.render.RenderCommand +import java.util.Locale import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertTrue -import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext -import org.dreamfinity.dsgl.core.render.RenderCommand -import org.dreamfinity.dsgl.core.overlay.UiLayerId -import java.util.Locale class OverlayDebugControlHostTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } + @AfterTest fun cleanup() { OverlayLayerDebugState.resetAll() @@ -71,13 +75,25 @@ class OverlayDebugControlHostTests { host.render(960, 540) val layout = host.debugLayout() ?: error("layout missing") - assertTrue(host.handleMouseDown(layout.appOverlayRenderRect.x + 2, layout.appOverlayRenderRect.y + 2, MouseButton.LEFT)) + assertTrue( + host.handleMouseDown( + layout.appOverlayRenderRect.x + 2, + layout.appOverlayRenderRect.y + 2, + MouseButton.LEFT, + ), + ) assertFalse(OverlayLayerDebugState.applicationOverlayRenderEnabled) assertTrue(OverlayLayerDebugState.applicationOverlayInputEnabled) assertTrue(OverlayLayerDebugState.systemOverlayRenderEnabled) assertTrue(OverlayLayerDebugState.systemOverlayInputEnabled) - assertTrue(host.handleMouseDown(layout.systemOverlayInputRect.x + 2, layout.systemOverlayInputRect.y + 2, MouseButton.LEFT)) + assertTrue( + host.handleMouseDown( + layout.systemOverlayInputRect.x + 2, + layout.systemOverlayInputRect.y + 2, + MouseButton.LEFT, + ), + ) assertFalse(OverlayLayerDebugState.systemOverlayInputEnabled) assertFalse(OverlayLayerDebugState.applicationOverlayRenderEnabled) } @@ -91,15 +107,18 @@ class OverlayDebugControlHostTests { host.render(960, 540) host.paint(ctx) val commands = host.paint(ctx) - val drawTexts = commands - .filterIsInstance() - val statusTexts = drawTexts - .filter { it.sourceKey == "dsgl-overlay-debug-status" } - .map { it.text } - val statusTextValue = assertNotNull( - statusTexts.lastOrNull { it.isNotBlank() } ?: statusTexts.lastOrNull(), - "draw texts: ${drawTexts.joinToString { "${it.sourceKey}:${it.text}" }}" - ) + val drawTexts = + commands + .filterIsInstance() + val statusTexts = + drawTexts + .filter { it.sourceKey == "dsgl-overlay-debug-status" } + .map { it.text } + val statusTextValue = + assertNotNull( + statusTexts.lastOrNull { it.isNotBlank() } ?: statusTexts.lastOrNull(), + "draw texts: ${drawTexts.joinToString { "${it.sourceKey}:${it.text}" }}", + ) val expectedFps = OverlayLayerDebugState.frameFps val expectedFrameMs = String.format(Locale.US, "%.1f", OverlayLayerDebugState.frameTimeMs) val expectedWindowFps = OverlayLayerDebugState.frameFpsWindow @@ -118,19 +137,29 @@ class OverlayDebugControlHostTests { host.render(960, 540) val layout = host.debugLayout() ?: error("layout missing") - val initialText = host.paint(ctx) - .filterIsInstance() - .lastOrNull { it.sourceKey == "dsgl-overlay-debug-toggle-app-render" } - ?.text + val initialText = + host + .paint(ctx) + .filterIsInstance() + .lastOrNull { it.sourceKey == "dsgl-overlay-debug-toggle-app-render" } + ?.text assertEquals("ON", initialText) - assertTrue(host.handleMouseDown(layout.appOverlayRenderRect.x + 2, layout.appOverlayRenderRect.y + 2, MouseButton.LEFT)) + assertTrue( + host.handleMouseDown( + layout.appOverlayRenderRect.x + 2, + layout.appOverlayRenderRect.y + 2, + MouseButton.LEFT, + ), + ) host.render(960, 540) - val updatedText = host.paint(ctx) - .filterIsInstance() - .lastOrNull { it.sourceKey == "dsgl-overlay-debug-toggle-app-render" } - ?.text + val updatedText = + host + .paint(ctx) + .filterIsInstance() + .lastOrNull { it.sourceKey == "dsgl-overlay-debug-toggle-app-render" } + ?.text assertEquals("OFF", updatedText) } @@ -180,9 +209,9 @@ class OverlayDebugControlHostTests { frameFps = 0, frameTimeMs = 0f, frameFpsWindow = 0, - frameTimeWindowMs = 0f + frameTimeWindowMs = 0f, ), - OverlayLayerDebugState.snapshot() + OverlayLayerDebugState.snapshot(), ) } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dnd/DndHooksRuntimeIntegrationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dnd/DndHooksRuntimeIntegrationTests.kt index f12caa3..e419e17 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dnd/DndHooksRuntimeIntegrationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dnd/DndHooksRuntimeIntegrationTests.kt @@ -2,34 +2,34 @@ package org.dreamfinity.dsgl.core.dnd import org.dreamfinity.dsgl.core.DomTree import org.dreamfinity.dsgl.core.DsglWindow -import org.dreamfinity.dsgl.core.hooks.HookUsageException import org.dreamfinity.dsgl.core.dnd.internal.DefaultDndEngine +import org.dreamfinity.dsgl.core.hooks.HookUsageException +import java.util.WeakHashMap import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNotEquals import kotlin.test.assertNotNull -import java.util.WeakHashMap class DndHooksRuntimeIntegrationTests { @Test fun `dnd hooks are callable from UiScope without window qualification`() { val baselineListeners = monitorListenersSnapshot().keys - val window = object : DsglWindow() { - override fun render(): DomTree { - return ui { - useDraggable(id = "card-a", nodeKey = "card-a") - useDroppable(id = "lane-a", nodeKey = "lane-a") - useSortable( - id = "card-b", - nodeKey = "card-b", - containerId = "lane-a", - items = listOf("card-b") - ) - useDragDropMonitor(DragDropMonitorCallbacks()) - } + val window = + object : DsglWindow() { + override fun render(): DomTree = + ui { + useDraggable(id = "card-a", nodeKey = "card-a") + useDroppable(id = "lane-a", nodeKey = "lane-a") + useSortable( + id = "card-b", + nodeKey = "card-b", + containerId = "lane-a", + items = listOf("card-b"), + ) + useDragDropMonitor(DragDropMonitorCallbacks()) + } } - } try { renderWithHookSession(window, commit = true) @@ -42,19 +42,20 @@ class DndHooksRuntimeIntegrationTests { @Test fun `duplicate hook-component identity in same render fails loudly`() { - val window = object : DsglWindow() { - override fun render(): DomTree { - return ui { - useDraggable(id = "card-a", nodeKey = "same") - useDraggable(id = "card-b", nodeKey = "same") - } + val window = + object : DsglWindow() { + override fun render(): DomTree = + ui { + useDraggable(id = "card-a", nodeKey = "same") + useDraggable(id = "card-b", nodeKey = "same") + } } - } window.beginRenderBuild() - val error = assertFailsWith { - window.render() - } + val error = + assertFailsWith { + window.render() + } assertEquals(error.message?.contains("Duplicate component identity"), true) window.endRenderBuild() } @@ -63,19 +64,19 @@ class DndHooksRuntimeIntegrationTests { fun `drag-drop monitor keeps one subscription and refreshes callbacks`() { val baselineListeners = monitorListenersSnapshot() val callbackCalls: MutableList = arrayListOf() - val window = object : DsglWindow() { - var callbackVersion: String = "v1" - - override fun render(): DomTree { - return ui { - useDragDropMonitor( - DragDropMonitorCallbacks( - onDragCancel = { callbackCalls += callbackVersion } + val window = + object : DsglWindow() { + var callbackVersion: String = "v1" + + override fun render(): DomTree = + ui { + useDragDropMonitor( + DragDropMonitorCallbacks( + onDragCancel = { callbackCalls += callbackVersion }, + ), ) - ) - } + } } - } try { renderWithHookSession(window, commit = true) @@ -102,17 +103,17 @@ class DndHooksRuntimeIntegrationTests { @Test fun `drag-drop monitor cleans up on disappearance and re-subscribes on reappearance`() { val baselineCount = monitorListenersSnapshot().size - val window = object : DsglWindow() { - var showMonitor: Boolean = true - - override fun render(): DomTree { - return ui { - if (showMonitor) { - useDragDropMonitor(DragDropMonitorCallbacks()) + val window = + object : DsglWindow() { + var showMonitor: Boolean = true + + override fun render(): DomTree = + ui { + if (showMonitor) { + useDragDropMonitor(DragDropMonitorCallbacks()) + } } - } } - } try { renderWithHookSession(window, commit = true) @@ -136,22 +137,22 @@ class DndHooksRuntimeIntegrationTests { @Test fun `sortable container state persists while mounted and resets after disappearance`() { - val window = object : DsglWindow() { - var showSortable: Boolean = true - - override fun render(): DomTree { - return ui { - if (showSortable) { - useSortable( - id = "card-a", - nodeKey = "card-a", - containerId = "lane", - items = listOf("card-a") - ) + val window = + object : DsglWindow() { + var showSortable: Boolean = true + + override fun render(): DomTree = + ui { + if (showSortable) { + useSortable( + id = "card-a", + nodeKey = "card-a", + containerId = "lane", + items = listOf("card-a"), + ) + } } - } } - } try { renderWithHookSession(window, commit = true) @@ -220,8 +221,8 @@ class DndHooksRuntimeIntegrationTests { return System.identityHashCode(state) } - private fun sampleActiveDrag(): ActiveDrag { - return ActiveDrag( + private fun sampleActiveDrag(): ActiveDrag = + ActiveDrag( id = "sample", type = "sample", sourceKey = "source", @@ -231,7 +232,6 @@ class DndHooksRuntimeIntegrationTests { cursorY = 0, transform = Transform(0.0, 0.0), dropEffect = DropEffect.NONE, - dataTransfer = DataTransfer() + dataTransfer = DataTransfer(), ) - } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngineTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngineTests.kt index e448256..5256ea6 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngineTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngineTests.kt @@ -16,10 +16,11 @@ class DefaultDndEngineTests { val list = ContainerNode(key = "list") val folder = ContainerNode(key = "folder") - val selected = DefaultDndEngine.selectDropTargetCandidate( - candidates = listOf(list, folder), - previousTarget = list - ) + val selected = + DefaultDndEngine.selectDropTargetCandidate( + candidates = listOf(list, folder), + previousTarget = list, + ) assertSame(folder, selected) } @@ -28,34 +29,37 @@ class DefaultDndEngineTests { fun `drop target selection keeps deepest candidate when already selected`() { val folder = ContainerNode(key = "folder") - val selected = DefaultDndEngine.selectDropTargetCandidate( - candidates = listOf(folder), - previousTarget = folder - ) + val selected = + DefaultDndEngine.selectDropTargetCandidate( + candidates = listOf(folder), + previousTarget = folder, + ) assertSame(folder, selected) } @Test fun `shiftCommand translates checkerboard command without changing checker offsets`() { - val shift = DefaultDndEngine::class.java.getDeclaredMethod( - "shiftCommand", - RenderCommand::class.java, - Int::class.javaPrimitiveType, - Int::class.javaPrimitiveType - ) + val shift = + DefaultDndEngine::class.java.getDeclaredMethod( + "shiftCommand", + RenderCommand::class.java, + Int::class.javaPrimitiveType, + Int::class.javaPrimitiveType, + ) shift.isAccessible = true - val command = RenderCommand.DrawCheckerboard( - x = 10, - y = 15, - width = 20, - height = 25, - cellSize = 4, - lightColor = 0xFFCCDDEE.toInt(), - darkColor = 0xFF445566.toInt(), - offsetX = 3, - offsetY = 7 - ) + val command = + RenderCommand.DrawCheckerboard( + x = 10, + y = 15, + width = 20, + height = 25, + cellSize = 4, + lightColor = 0xFFCCDDEE.toInt(), + darkColor = 0xFF445566.toInt(), + offsetX = 3, + offsetY = 7, + ) val shifted = shift.invoke(DefaultDndEngine, command, 8, -5) as RenderCommand.DrawCheckerboard @@ -67,10 +71,11 @@ class DefaultDndEngineTests { @Test fun `drop target selection returns null for empty candidates`() { - val selected = DefaultDndEngine.selectDropTargetCandidate( - candidates = emptyList(), - previousTarget = null - ) + val selected = + DefaultDndEngine.selectDropTargetCandidate( + candidates = emptyList(), + previousTarget = null, + ) assertNull(selected) } @@ -78,27 +83,34 @@ class DefaultDndEngineTests { @Test fun `hover chain remains coherent for dnd candidate selection after core hover migration`() { val root = ContainerNode(key = "dnd-root") - val parent = ContainerNode(key = "dnd-parent").apply { - width = 120 - height = 60 - droppable = true - }.applyParent(root) - val child = ContainerNode(key = "dnd-child").apply { - width = 34 - height = 16 - droppable = true - }.applyParent(parent) + val parent = + ContainerNode(key = "dnd-parent") + .apply { + width = 120 + height = 60 + droppable = true + }.applyParent(root) + val child = + ContainerNode(key = "dnd-child") + .apply { + width = 34 + height = 16 + droppable = true + }.applyParent(parent) root.render( - ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - }, + ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + }, x = 0, y = 0, width = 260, - height = 140 + height = 140, ) val chain = collectHoverChain(root, 10, 10) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEventsTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEventsTests.kt index aa4ed1b..169ef65 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEventsTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEventsTests.kt @@ -38,9 +38,10 @@ class ContextMenuEventsTests { val host = RecordingHost() val node = ContainerNode() node.bounds = Rect(10, 15, 120, 30) - val model = contextMenu(id = "events.cursor") { - item("Open") - } + val model = + contextMenu(id = "events.cursor") { + item("Open") + } node.onContextMenu(host = host) { openMenu(model) } @@ -58,9 +59,10 @@ class ContextMenuEventsTests { val host = RecordingHost() val node = ContainerNode() node.bounds = Rect(22, 41, 86, 19) - val model = contextMenu(id = "events.anchor") { - item("Open") - } + val model = + contextMenu(id = "events.anchor") { + item("Open") + } node.onContextMenu(host = host) { openMenuAnchored(model) } @@ -82,9 +84,10 @@ class ContextMenuEventsTests { node.bounds = Rect(22, 41, 86, 19) node.fontId = "test-font" node.fontSize = 24 - val model = contextMenu(id = "events.font") { - item("Open") - } + val model = + contextMenu(id = "events.font") { + item("Open") + } node.onContextMenu(host = host) { openMenu(model) } @@ -110,9 +113,10 @@ class ContextMenuEventsTests { child.bounds = Rect(10, 10, 60, 16) child.fontId = "child-font" child.fontSize = 28 - val model = contextMenu(id = "events.target.font") { - item("Open") - } + val model = + contextMenu(id = "events.target.font") { + item("Open") + } parent.onContextMenu(host = host) { openMenu(model) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/OverflowClippingTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/OverflowClippingTests.kt index 5e6429a..67016d4 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/OverflowClippingTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/OverflowClippingTests.kt @@ -1,8 +1,5 @@ package org.dreamfinity.dsgl.core.dom -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.DomTree import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Insets @@ -11,23 +8,31 @@ import org.dreamfinity.dsgl.core.dom.layout.Size import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.Overflow +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue class OverflowClippingTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @Test fun `overflow hidden container emits clip around child paint`() { val childColor = 0xFF0A8A3C.toInt() val root = ContainerNode(key = "root") - val clip = ContainerNode(backgroundColor = 0xFF1B2330.toInt(), key = "clip").apply { - width = 80 - height = 40 - overflow = Overflow.Hidden - }.applyParent(root) + val clip = + ContainerNode(backgroundColor = 0xFF1B2330.toInt(), key = "clip") + .apply { + width = 80 + height = 40 + overflow = Overflow.Hidden + }.applyParent(root) FixedPaintNode(color = childColor, nodeWidth = 120, nodeHeight = 20, key = "child") .applyParent(clip) @@ -35,23 +40,26 @@ class OverflowClippingTests { tree.render(ctx, 200, 120) val commands = tree.paint(ctx, applyStyles = false) - val clipPushIndex = commands.indexOfFirst { command -> - command is RenderCommand.PushClip && - command.x == clip.bounds.x && - command.y == clip.bounds.y && - command.width == clip.bounds.width && - command.height == clip.bounds.height - } + val clipPushIndex = + commands.indexOfFirst { command -> + command is RenderCommand.PushClip && + command.x == clip.bounds.x && + command.y == clip.bounds.y && + command.width == clip.bounds.width && + command.height == clip.bounds.height + } assertTrue(clipPushIndex >= 0) - val childDrawIndex = commands.indexOfFirst { command -> - command is RenderCommand.DrawRect && command.color == childColor - } + val childDrawIndex = + commands.indexOfFirst { command -> + command is RenderCommand.DrawRect && command.color == childColor + } assertTrue(childDrawIndex > clipPushIndex) - val clipPopIndex = commands.indices.firstOrNull { index -> - index > childDrawIndex && commands[index] is RenderCommand.PopClip - } ?: -1 + val clipPopIndex = + commands.indices.firstOrNull { index -> + index > childDrawIndex && commands[index] is RenderCommand.PopClip + } ?: -1 assertTrue(clipPopIndex > childDrawIndex) val pushCount = commands.count { it is RenderCommand.PushClip } @@ -63,13 +71,16 @@ class OverflowClippingTests { fun `overflow clipping preserves child geometry for partial visibility`() { val childColor = 0xFFB0522D.toInt() val root = ContainerNode(key = "root") - val clip = ContainerNode(backgroundColor = 0xFF18212C.toInt(), key = "clip").apply { - width = 80 - height = 40 - overflow = Overflow.Hidden - }.applyParent(root) - val child = FixedPaintNode(color = childColor, nodeWidth = 120, nodeHeight = 28, key = "child") - .applyParent(clip) + val clip = + ContainerNode(backgroundColor = 0xFF18212C.toInt(), key = "clip") + .apply { + width = 80 + height = 40 + overflow = Overflow.Hidden + }.applyParent(root) + val child = + FixedPaintNode(color = childColor, nodeWidth = 120, nodeHeight = 28, key = "child") + .applyParent(clip) val tree = DomTree(root) tree.render(ctx, 220, 120) @@ -78,10 +89,11 @@ class OverflowClippingTests { assertEquals(120, child.bounds.width) assertTrue(child.bounds.x + child.bounds.width > clip.bounds.x + clip.bounds.width) - val childDraw = commands - .filterIsInstance() - .firstOrNull { it.color == childColor } - ?: error("child draw command missing") + val childDraw = + commands + .filterIsInstance() + .firstOrNull { it.color == childColor } + ?: error("child draw command missing") assertEquals(120, childDraw.width) } @@ -89,17 +101,21 @@ class OverflowClippingTests { fun `nested overflow containers emit nested clip commands`() { val leafColor = 0xFF3F7FBF.toInt() val root = ContainerNode(key = "root") - val outer = ContainerNode(backgroundColor = 0xFF101820.toInt(), key = "outer").apply { - width = 100 - height = 80 - overflow = Overflow.Hidden - }.applyParent(root) - val inner = ContainerNode(backgroundColor = 0xFF182230.toInt(), key = "inner").apply { - width = 60 - height = 40 - margin = Insets(top = 10, right = 0, bottom = 0, left = 20) - overflow = Overflow.Hidden - }.applyParent(outer) + val outer = + ContainerNode(backgroundColor = 0xFF101820.toInt(), key = "outer") + .apply { + width = 100 + height = 80 + overflow = Overflow.Hidden + }.applyParent(root) + val inner = + ContainerNode(backgroundColor = 0xFF182230.toInt(), key = "inner") + .apply { + width = 60 + height = 40 + margin = Insets(top = 10, right = 0, bottom = 0, left = 20) + overflow = Overflow.Hidden + }.applyParent(outer) FixedPaintNode(color = leafColor, nodeWidth = 120, nodeHeight = 24, key = "leaf") .applyParent(inner) @@ -107,23 +123,26 @@ class OverflowClippingTests { tree.render(ctx, 240, 180) val commands = tree.paint(ctx, applyStyles = false) - val outerClipIndex = commands.indexOfFirst { command -> - command is RenderCommand.PushClip && - command.x == outer.bounds.x && - command.y == outer.bounds.y && - command.width == outer.bounds.width && - command.height == outer.bounds.height - } - val innerClipIndex = commands.indexOfFirst { command -> - command is RenderCommand.PushClip && - command.x == inner.bounds.x && - command.y == inner.bounds.y && - command.width == inner.bounds.width && - command.height == inner.bounds.height - } - val leafDrawIndex = commands.indexOfFirst { command -> - command is RenderCommand.DrawRect && command.color == leafColor - } + val outerClipIndex = + commands.indexOfFirst { command -> + command is RenderCommand.PushClip && + command.x == outer.bounds.x && + command.y == outer.bounds.y && + command.width == outer.bounds.width && + command.height == outer.bounds.height + } + val innerClipIndex = + commands.indexOfFirst { command -> + command is RenderCommand.PushClip && + command.x == inner.bounds.x && + command.y == inner.bounds.y && + command.width == inner.bounds.width && + command.height == inner.bounds.height + } + val leafDrawIndex = + commands.indexOfFirst { command -> + command is RenderCommand.DrawRect && command.color == leafColor + } assertTrue(outerClipIndex >= 0) assertTrue(innerClipIndex > outerClipIndex) @@ -133,21 +152,26 @@ class OverflowClippingTests { val popCount = commands.count { it is RenderCommand.PopClip } assertEquals(pushCount, popCount) } + @Test fun `nested overflow still emits child clip when child extends beyond parent`() { val leafColor = 0xFF46A0D8.toInt() val root = ContainerNode(key = "root") - val outer = ContainerNode(backgroundColor = 0xFF111820.toInt(), key = "outer").apply { - width = 100 - height = 50 - overflow = Overflow.Hidden - }.applyParent(root) - val inner = ContainerNode(backgroundColor = 0xFF182536.toInt(), key = "inner").apply { - width = 80 - height = 40 - margin = Insets(top = 8, right = 0, bottom = 0, left = 72) - overflow = Overflow.Hidden - }.applyParent(outer) + val outer = + ContainerNode(backgroundColor = 0xFF111820.toInt(), key = "outer") + .apply { + width = 100 + height = 50 + overflow = Overflow.Hidden + }.applyParent(root) + val inner = + ContainerNode(backgroundColor = 0xFF182536.toInt(), key = "inner") + .apply { + width = 80 + height = 40 + margin = Insets(top = 8, right = 0, bottom = 0, left = 72) + overflow = Overflow.Hidden + }.applyParent(outer) FixedPaintNode(color = leafColor, nodeWidth = 120, nodeHeight = 24, key = "leaf") .applyParent(inner) @@ -155,26 +179,29 @@ class OverflowClippingTests { tree.render(ctx, 240, 180) val commands = tree.paint(ctx, applyStyles = false) - val outerClip = commands - .filterIsInstance() - .firstOrNull { push -> - push.x == outer.bounds.x && - push.y == outer.bounds.y && - push.width == outer.bounds.width && - push.height == outer.bounds.height - } - ?: error("outer clip command missing") - - val expectedInner = Rect(inner.bounds.x, inner.bounds.y, inner.bounds.width, inner.bounds.height) - .intersection(Rect(outerClip.x, outerClip.y, outerClip.width, outerClip.height)) - ?: error("inner should intersect outer") + val outerClip = + commands + .filterIsInstance() + .firstOrNull { push -> + push.x == outer.bounds.x && + push.y == outer.bounds.y && + push.width == outer.bounds.width && + push.height == outer.bounds.height + } + ?: error("outer clip command missing") + + val expectedInner = + Rect(inner.bounds.x, inner.bounds.y, inner.bounds.width, inner.bounds.height) + .intersection(Rect(outerClip.x, outerClip.y, outerClip.width, outerClip.height)) + ?: error("inner should intersect outer") val pushClips = commands.filterIsInstance() - val outerClipIndex = pushClips.indexOfFirst { push -> - push.x == outerClip.x && - push.y == outerClip.y && - push.width == outerClip.width && - push.height == outerClip.height - } + val outerClipIndex = + pushClips.indexOfFirst { push -> + push.x == outerClip.x && + push.y == outerClip.y && + push.width == outerClip.width && + push.height == outerClip.height + } assertTrue(outerClipIndex >= 0) val innerClip = pushClips.getOrNull(outerClipIndex + 1) ?: error("inner clip command missing") assertEquals(inner.bounds.x, innerClip.x) @@ -183,11 +210,12 @@ class OverflowClippingTests { assertEquals(inner.bounds.height, innerClip.height) assertTrue(innerClip.x < expectedInner.x + expectedInner.width) } + private class FixedPaintNode( private val color: Int, private val nodeWidth: Int, private val nodeHeight: Int, - key: Any? = null + key: Any? = null, ) : DOMNode(key) { init { width = nodeWidth @@ -202,6 +230,3 @@ class OverflowClippingTests { } } } - - - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/OverflowInputClippingTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/OverflowInputClippingTests.kt index 5797566..e5f1b5b 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/OverflowInputClippingTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/OverflowInputClippingTests.kt @@ -1,9 +1,5 @@ package org.dreamfinity.dsgl.core.dom -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.dom.elements.ButtonNode import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Rect @@ -12,13 +8,20 @@ import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.Overflow +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue class OverflowInputClippingTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @Test fun `generic clipped container rejects pointer input outside viewport`() { @@ -26,16 +29,18 @@ class OverflowInputClippingTests { val (root, router) = createLayerRouter(layer) var clicks = 0 - val clippedViewport = ContainerNode(key = "$layer-clip").apply { - bounds = Rect(20, 20, 100, 40) - overflow = Overflow.Hidden - } + val clippedViewport = + ContainerNode(key = "$layer-clip").apply { + bounds = Rect(20, 20, 100, 40) + overflow = Overflow.Hidden + } clippedViewport.applyParent(root) - val child = ButtonNode("child", key = "$layer-child").apply { - bounds = Rect(24, 46, 80, 22) - onClick { clicks += 1 } - } + val child = + ButtonNode("child", key = "$layer-child").apply { + bounds = Rect(24, 46, 80, 22) + onClick { clicks += 1 } + } child.applyParent(clippedViewport) assertFalse(router.handleMouseDown(30, 65, MouseButton.LEFT)) @@ -49,16 +54,18 @@ class OverflowInputClippingTests { val (root, router) = createLayerRouter("partial-visible") var clicks = 0 - val clippedViewport = ContainerNode(key = "partial-clip").apply { - bounds = Rect(20, 20, 100, 40) - overflow = Overflow.Hidden - } + val clippedViewport = + ContainerNode(key = "partial-clip").apply { + bounds = Rect(20, 20, 100, 40) + overflow = Overflow.Hidden + } clippedViewport.applyParent(root) - val child = ButtonNode("child", key = "partial-child").apply { - bounds = Rect(24, 46, 80, 22) - onClick { clicks += 1 } - } + val child = + ButtonNode("child", key = "partial-child").apply { + bounds = Rect(24, 46, 80, 22) + onClick { clicks += 1 } + } child.applyParent(clippedViewport) assertTrue(router.handleMouseDown(30, 58, MouseButton.LEFT)) @@ -75,22 +82,25 @@ class OverflowInputClippingTests { val (root, router) = createLayerRouter("nested-clip") var clicks = 0 - val outer = ContainerNode(key = "outer").apply { - bounds = Rect(10, 10, 130, 90) - overflow = Overflow.Hidden - } + val outer = + ContainerNode(key = "outer").apply { + bounds = Rect(10, 10, 130, 90) + overflow = Overflow.Hidden + } outer.applyParent(root) - val inner = ContainerNode(key = "inner").apply { - bounds = Rect(20, 20, 90, 40) - overflow = Overflow.Hidden - } + val inner = + ContainerNode(key = "inner").apply { + bounds = Rect(20, 20, 90, 40) + overflow = Overflow.Hidden + } inner.applyParent(outer) - val child = ButtonNode("child", key = "nested-child").apply { - bounds = Rect(24, 52, 80, 24) - onClick { clicks += 1 } - } + val child = + ButtonNode("child", key = "nested-child").apply { + bounds = Rect(24, 52, 80, 24) + onClick { clicks += 1 } + } child.applyParent(inner) assertTrue(router.handleMouseDown(30, 58, MouseButton.LEFT)) @@ -102,28 +112,30 @@ class OverflowInputClippingTests { assertEquals(1, clicks) } - @Test fun `nested clipped containers clamp interaction to parent child intersection`() { val (root, router) = createLayerRouter("nested-intersection") var clicks = 0 - val outer = ContainerNode(key = "outer").apply { - bounds = Rect(10, 10, 100, 60) - overflow = Overflow.Hidden - } + val outer = + ContainerNode(key = "outer").apply { + bounds = Rect(10, 10, 100, 60) + overflow = Overflow.Hidden + } outer.applyParent(root) - val inner = ContainerNode(key = "inner").apply { - bounds = Rect(80, 20, 70, 40) - overflow = Overflow.Hidden - } + val inner = + ContainerNode(key = "inner").apply { + bounds = Rect(80, 20, 70, 40) + overflow = Overflow.Hidden + } inner.applyParent(outer) - val child = ButtonNode("child", key = "intersection-child").apply { - bounds = Rect(84, 24, 60, 24) - onClick { clicks += 1 } - } + val child = + ButtonNode("child", key = "intersection-child").apply { + bounds = Rect(84, 24, 60, 24) + onClick { clicks += 1 } + } child.applyParent(inner) // Visible point inside effective intersection. @@ -136,33 +148,38 @@ class OverflowInputClippingTests { assertFalse(router.handleMouseUp(118, 30, MouseButton.LEFT)) assertEquals(1, clicks) } + @Test fun `paint and pointer clipping stay consistent for clipped containers`() { val (root, router) = createLayerRouter("paint-input-consistency") var clicks = 0 - val clippedViewport = ContainerNode(backgroundColor = 0xFF112233.toInt(), key = "clip").apply { - bounds = Rect(20, 20, 100, 40) - overflow = Overflow.Hidden - } + val clippedViewport = + ContainerNode(backgroundColor = 0xFF112233.toInt(), key = "clip").apply { + bounds = Rect(20, 20, 100, 40) + overflow = Overflow.Hidden + } clippedViewport.applyParent(root) - val child = ButtonNode("child", key = "clip-child").apply { - bounds = Rect(24, 46, 80, 22) - onClick { clicks += 1 } - } + val child = + ButtonNode("child", key = "clip-child").apply { + bounds = Rect(24, 46, 80, 22) + onClick { clicks += 1 } + } child.applyParent(clippedViewport) val commands = ArrayList() root.appendRenderCommands(ctx, commands) - assertTrue(commands.any { command -> - command is RenderCommand.PushClip && - command.x == clippedViewport.bounds.x && - command.y == clippedViewport.bounds.y && - command.width == clippedViewport.bounds.width && - command.height == clippedViewport.bounds.height - }) + assertTrue( + commands.any { command -> + command is RenderCommand.PushClip && + command.x == clippedViewport.bounds.x && + command.y == clippedViewport.bounds.y && + command.width == clippedViewport.bounds.width && + command.height == clippedViewport.bounds.height + }, + ) assertTrue(router.handleMouseDown(30, 58, MouseButton.LEFT)) assertTrue(router.handleMouseUp(30, 58, MouseButton.LEFT)) @@ -174,9 +191,10 @@ class OverflowInputClippingTests { } private fun createLayerRouter(key: String): Pair { - val root = ContainerNode(key = "$key-root").apply { - bounds = Rect(0, 0, 320, 200) - } + val root = + ContainerNode(key = "$key-root").apply { + bounds = Rect(0, 0, 320, 200) + } return root to LayerDomInputRouter { root } } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/OverflowPositionedClippingTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/OverflowPositionedClippingTests.kt index 7364b37..afa1495 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/OverflowPositionedClippingTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/OverflowPositionedClippingTests.kt @@ -1,10 +1,5 @@ package org.dreamfinity.dsgl.core.dom -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.DomTree import org.dreamfinity.dsgl.core.dom.elements.ButtonNode import org.dreamfinity.dsgl.core.dom.elements.ContainerNode @@ -19,13 +14,21 @@ import org.dreamfinity.dsgl.core.style.StyleDeclarations import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleExpression import org.dreamfinity.dsgl.core.style.StyleProperty +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue class OverflowPositionedClippingTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -36,15 +39,18 @@ class OverflowPositionedClippingTests { @Test fun `overflow auto keeps clipping for ordinary in-flow child`() { val root = ContainerNode(key = "root") - val clip = ContainerNode(key = "clip").apply { - width = 80 - height = 40 - overflow = Overflow.Auto - }.applyParent(root) - ButtonNode("child", key = "child").apply { - width = 120 - height = 16 - }.applyParent(clip) + val clip = + ContainerNode(key = "clip") + .apply { + width = 80 + height = 40 + overflow = Overflow.Auto + }.applyParent(root) + ButtonNode("child", key = "child") + .apply { + width = 120 + height = 16 + }.applyParent(clip) val tree = DomTree(root) tree.render(ctx, 220, 160) @@ -52,12 +58,13 @@ class OverflowPositionedClippingTests { val commands = tree.paint(ctx, applyStyles = false) assertTrue(state.axisX.clipsToViewport) - val clipPush = commands.filterIsInstance().firstOrNull { push -> - push.x == state.viewportRect.x && - push.y == state.viewportRect.y && - push.width == state.viewportRect.width && - push.height == state.viewportRect.height - } + val clipPush = + commands.filterIsInstance().firstOrNull { push -> + push.x == state.viewportRect.x && + push.y == state.viewportRect.y && + push.width == state.viewportRect.width && + push.height == state.viewportRect.height + } assertTrue(clipPush != null) } @@ -76,29 +83,37 @@ class OverflowPositionedClippingTests { @Test fun `nested mixed-axis overflow still clips positioned descendant at outer gutter edge`() { val root = ContainerNode(key = "root") - val outer = ContainerNode(key = "outer").apply { - width = 90 - height = 50 - overflowX = Overflow.Visible - overflowY = Overflow.Auto - }.applyParent(root) - val inner = ContainerNode(key = "inner").apply { - width = 86 - height = 40 - }.applyParent(outer) - ContainerNode(key = "filler").apply { - width = 20 - height = 220 - }.applyParent(outer) - val absolute = ButtonNode("abs", key = "nested-absolute").apply { - width = 24 - height = 12 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "absolute", - StyleProperty.LEFT to "74px", - StyleProperty.TOP to "0px" - ) - }.applyParent(inner) + val outer = + ContainerNode(key = "outer") + .apply { + width = 90 + height = 50 + overflowX = Overflow.Visible + overflowY = Overflow.Auto + }.applyParent(root) + val inner = + ContainerNode(key = "inner") + .apply { + width = 86 + height = 40 + }.applyParent(outer) + ContainerNode(key = "filler") + .apply { + width = 20 + height = 220 + }.applyParent(outer) + val absolute = + ButtonNode("abs", key = "nested-absolute") + .apply { + width = 24 + height = 12 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "absolute", + StyleProperty.LEFT to "74px", + StyleProperty.TOP to "0px", + ) + }.applyParent(inner) val tree = DomTree(root) tree.render(ctx, 240, 180) @@ -115,41 +130,48 @@ class OverflowPositionedClippingTests { private fun verifyPositionedChildGutterClipping(position: PositionMode, overflowY: Overflow) { val root = ContainerNode(key = "root-$position-$overflowY") - val scroll = ContainerNode(key = "scroll-$position-$overflowY").apply { - width = 80 - height = 40 - overflowX = Overflow.Visible - this.overflowY = overflowY - }.applyParent(root) + val scroll = + ContainerNode(key = "scroll-$position-$overflowY") + .apply { + width = 80 + height = 40 + overflowX = Overflow.Visible + this.overflowY = overflowY + }.applyParent(root) var clicks = 0 - val child = ButtonNode("child", key = "child-$position-$overflowY").apply { - width = 20 - height = 12 - val modeLiteral = when (position) { - PositionMode.Absolute -> "absolute" - PositionMode.Relative -> "relative" - PositionMode.Fixed -> "fixed" - PositionMode.Static -> "static" - PositionMode.Sticky -> "sticky" + val child = + ButtonNode("child", key = "child-$position-$overflowY").apply { + width = 20 + height = 12 + val modeLiteral = + when (position) { + PositionMode.Absolute -> "absolute" + PositionMode.Relative -> "relative" + PositionMode.Fixed -> "fixed" + PositionMode.Static -> "static" + PositionMode.Sticky -> "sticky" + } + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to modeLiteral, + StyleProperty.LEFT to "68px", + StyleProperty.TOP to "0px", + ) + onClick { clicks += 1 } } - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to modeLiteral, - StyleProperty.LEFT to "68px", - StyleProperty.TOP to "0px" - ) - onClick { clicks += 1 } - } if (position == PositionMode.Relative) { child.applyParent(scroll) - ContainerNode(key = "filler-$position-$overflowY").apply { - width = 16 - height = 220 - }.applyParent(scroll) + ContainerNode(key = "filler-$position-$overflowY") + .apply { + width = 16 + height = 220 + }.applyParent(scroll) } else { - ContainerNode(key = "filler-$position-$overflowY").apply { - width = 16 - height = 220 - }.applyParent(scroll) + ContainerNode(key = "filler-$position-$overflowY") + .apply { + width = 16 + height = 220 + }.applyParent(scroll) child.applyParent(scroll) } @@ -162,7 +184,7 @@ class OverflowPositionedClippingTests { assertTrue(state.verticalScrollbarGutter > 0) assertEquals( state.baseViewportRect.width - state.verticalScrollbarGutter, - state.viewportRect.width + state.viewportRect.width, ) val visibleX = state.viewportRect.x + state.viewportRect.width - 1 @@ -175,20 +197,20 @@ class OverflowPositionedClippingTests { assertFalse(dispatchClick(root, MouseClickEvent(gutterX, probeY, MouseButton.LEFT))) assertEquals(1, clicks) - val clipPush = commands.filterIsInstance().firstOrNull { push -> - push.x == state.viewportRect.x && - push.y == state.viewportRect.y && - push.width == state.viewportRect.width && - push.height == state.viewportRect.height - } + val clipPush = + commands.filterIsInstance().firstOrNull { push -> + push.x == state.viewportRect.x && + push.y == state.viewportRect.y && + push.width == state.viewportRect.width && + push.height == state.viewportRect.height + } assertTrue(clipPush != null) } - private fun styleDeclarations(vararg entries: Pair): StyleDeclarations { - return StyleDeclarations().apply { + private fun styleDeclarations(vararg entries: Pair): StyleDeclarations = + StyleDeclarations().apply { entries.forEach { (property, literal) -> set(property, StyleExpression.Literal(literal)) } } - } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutFixedBehaviorTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutFixedBehaviorTests.kt index 95a5232..f5de131 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutFixedBehaviorTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutFixedBehaviorTests.kt @@ -10,7 +10,6 @@ import org.dreamfinity.dsgl.core.event.MouseClickEvent import org.dreamfinity.dsgl.core.event.dispatchClick import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.Overflow -import org.dreamfinity.dsgl.core.style.PositionMode import org.dreamfinity.dsgl.core.style.StyleDeclarations import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleExpression @@ -22,11 +21,14 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue class PositionedLayoutFixedBehaviorTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -37,19 +39,22 @@ class PositionedLayoutFixedBehaviorTests { @Test fun `fixed element is removed from normal flow`() { val root = ContainerNode(key = "fixed-flow-root") - val fixed = ContainerNode(key = "fixed-flow-fixed").apply { - width = 40 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "8px", - StyleProperty.TOP to "10px" - ) - } - val follower = ContainerNode(key = "fixed-flow-follower").apply { - width = 30 - height = 12 - } + val fixed = + ContainerNode(key = "fixed-flow-fixed").apply { + width = 40 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "8px", + StyleProperty.TOP to "10px", + ) + } + val follower = + ContainerNode(key = "fixed-flow-follower").apply { + width = 30 + height = 12 + } fixed.applyParent(root) follower.applyParent(root) @@ -63,15 +68,17 @@ class PositionedLayoutFixedBehaviorTests { @Test fun `fixed anchors to current root viewport`() { val root = ContainerNode(key = "fixed-root-anchor") - val child = ContainerNode(key = "fixed-child").apply { - width = 24 - height = 10 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "20px", - StyleProperty.TOP to "14px" - ) - } + val child = + ContainerNode(key = "fixed-child").apply { + width = 24 + height = 10 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "20px", + StyleProperty.TOP to "14px", + ) + } child.applyParent(root) renderTree(root, width = 260, height = 180) @@ -84,22 +91,26 @@ class PositionedLayoutFixedBehaviorTests { @Test fun `fixed ignores ordinary content scroll`() { - val root = ContainerNode(key = "fixed-scroll-root").apply { - overflowY = Overflow.Scroll - } - val spacer = ContainerNode(key = "fixed-scroll-spacer").apply { - width = 100 - height = 280 - } - val fixed = ContainerNode(key = "fixed-scroll-fixed").apply { - width = 20 - height = 10 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "6px", - StyleProperty.TOP to "5px" - ) - } + val root = + ContainerNode(key = "fixed-scroll-root").apply { + overflowY = Overflow.Scroll + } + val spacer = + ContainerNode(key = "fixed-scroll-spacer").apply { + width = 100 + height = 280 + } + val fixed = + ContainerNode(key = "fixed-scroll-fixed").apply { + width = 20 + height = 10 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "6px", + StyleProperty.TOP to "5px", + ) + } spacer.applyParent(root) fixed.applyParent(root) @@ -118,23 +129,27 @@ class PositionedLayoutFixedBehaviorTests { @Test fun `fixed is not anchored to nearest positioned ancestor`() { val root = ContainerNode(key = "fixed-ancestor-root") - val ancestor = ContainerNode(key = "fixed-ancestor").apply { - width = 120 - height = 80 - margin = Insets(top = 40, right = 0, bottom = 0, left = 60) - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "relative" - ) - } - val child = ContainerNode(key = "fixed-inside-relative").apply { - width = 20 - height = 10 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "9px", - StyleProperty.TOP to "11px" - ) - } + val ancestor = + ContainerNode(key = "fixed-ancestor").apply { + width = 120 + height = 80 + margin = Insets(top = 40, right = 0, bottom = 0, left = 60) + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "relative", + ) + } + val child = + ContainerNode(key = "fixed-inside-relative").apply { + width = 20 + height = 10 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "9px", + StyleProperty.TOP to "11px", + ) + } child.applyParent(ancestor) ancestor.applyParent(root) @@ -148,16 +163,18 @@ class PositionedLayoutFixedBehaviorTests { fun `fixed hit-testing uses final fixed rect`() { val root = ContainerNode(key = "fixed-hit-root") var clicks = 0 - val button = ButtonNode("fixed", key = "fixed-hit-button").apply { - width = 40 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "30px", - StyleProperty.TOP to "18px" - ) - onClick { clicks += 1 } - } + val button = + ButtonNode("fixed", key = "fixed-hit-button").apply { + width = 40 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "30px", + StyleProperty.TOP to "18px", + ) + onClick { clicks += 1 } + } button.applyParent(root) renderTree(root, width = 240, height = 160) @@ -173,28 +190,32 @@ class PositionedLayoutFixedBehaviorTests { var lowerClicks = 0 var upperClicks = 0 - val lower = ButtonNode("lower", key = "fixed-lower").apply { - width = 60 - height = 24 - zIndex = 1 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "10px", - StyleProperty.TOP to "8px" - ) - onClick { lowerClicks += 1 } - } - val upper = ButtonNode("upper", key = "fixed-upper").apply { - width = 60 - height = 24 - zIndex = 4 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "10px", - StyleProperty.TOP to "8px" - ) - onClick { upperClicks += 1 } - } + val lower = + ButtonNode("lower", key = "fixed-lower").apply { + width = 60 + height = 24 + zIndex = 1 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "10px", + StyleProperty.TOP to "8px", + ) + onClick { lowerClicks += 1 } + } + val upper = + ButtonNode("upper", key = "fixed-upper").apply { + width = 60 + height = 24 + zIndex = 4 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "10px", + StyleProperty.TOP to "8px", + ) + onClick { upperClicks += 1 } + } lower.applyParent(root) upper.applyParent(root) @@ -209,17 +230,19 @@ class PositionedLayoutFixedBehaviorTests { @Test fun `fixed uses left over right and top over bottom`() { val root = ContainerNode(key = "fixed-precedence-root") - val child = ContainerNode(key = "fixed-precedence-child").apply { - width = 20 - height = 10 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "25px", - StyleProperty.RIGHT to "7px", - StyleProperty.TOP to "12px", - StyleProperty.BOTTOM to "3px" - ) - } + val child = + ContainerNode(key = "fixed-precedence-child").apply { + width = 20 + height = 10 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "25px", + StyleProperty.RIGHT to "7px", + StyleProperty.TOP to "12px", + StyleProperty.BOTTOM to "3px", + ) + } child.applyParent(root) renderTree(root, width = 240, height = 160) @@ -231,37 +254,44 @@ class PositionedLayoutFixedBehaviorTests { @Test fun `static relative and absolute behavior remains intact`() { val root = ContainerNode(key = "fixed-nr-root") - val staticChild = ContainerNode(key = "fixed-nr-static").apply { - width = 30 - height = 12 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "static", - StyleProperty.LEFT to "50px", - StyleProperty.TOP to "20px" - ) - } - val relativeChild = ContainerNode(key = "fixed-nr-relative").apply { - width = 30 - height = 12 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "relative", - StyleProperty.LEFT to "18px", - StyleProperty.TOP to "7px" - ) - } - val absoluteChild = ContainerNode(key = "fixed-nr-absolute").apply { - width = 20 - height = 10 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "absolute", - StyleProperty.LEFT to "40px", - StyleProperty.TOP to "22px" - ) - } - val follower = ContainerNode(key = "fixed-nr-follower").apply { - width = 22 - height = 10 - } + val staticChild = + ContainerNode(key = "fixed-nr-static").apply { + width = 30 + height = 12 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "static", + StyleProperty.LEFT to "50px", + StyleProperty.TOP to "20px", + ) + } + val relativeChild = + ContainerNode(key = "fixed-nr-relative").apply { + width = 30 + height = 12 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "relative", + StyleProperty.LEFT to "18px", + StyleProperty.TOP to "7px", + ) + } + val absoluteChild = + ContainerNode(key = "fixed-nr-absolute").apply { + width = 20 + height = 10 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "absolute", + StyleProperty.LEFT to "40px", + StyleProperty.TOP to "22px", + ) + } + val follower = + ContainerNode(key = "fixed-nr-follower").apply { + width = 22 + height = 10 + } staticChild.applyParent(root) relativeChild.applyParent(root) absoluteChild.applyParent(root) @@ -280,17 +310,20 @@ class PositionedLayoutFixedBehaviorTests { @Test fun `inspector overrides keep static relative absolute and fixed behavior`() { - val root = ContainerNode(key = "fixed-inspector-root").apply { - overflowY = Overflow.Scroll - } - val spacer = ContainerNode(key = "fixed-inspector-spacer").apply { - width = 80 - height = 220 - } - val child = ContainerNode(key = "fixed-inspector-child").apply { - width = 20 - height = 10 - } + val root = + ContainerNode(key = "fixed-inspector-root").apply { + overflowY = Overflow.Scroll + } + val spacer = + ContainerNode(key = "fixed-inspector-spacer").apply { + width = 80 + height = 220 + } + val child = + ContainerNode(key = "fixed-inspector-child").apply { + width = 20 + height = 10 + } child.applyParent(root) spacer.applyParent(root) val tree = DomTree(root) @@ -326,11 +359,10 @@ class PositionedLayoutFixedBehaviorTests { DomTree(root).render(ctx, width, height) } - private fun styleDeclarations(vararg entries: Pair): StyleDeclarations { - return StyleDeclarations().apply { + private fun styleDeclarations(vararg entries: Pair): StyleDeclarations = + StyleDeclarations().apply { entries.forEach { (property, literal) -> set(property, StyleExpression.Literal(literal)) } } - } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutFixedClippingBehaviorTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutFixedClippingBehaviorTests.kt index db001a4..1b2de50 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutFixedClippingBehaviorTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutFixedClippingBehaviorTests.kt @@ -22,11 +22,14 @@ import kotlin.test.assertNotNull import kotlin.test.assertTrue class PositionedLayoutFixedClippingBehaviorTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -38,30 +41,36 @@ class PositionedLayoutFixedClippingBehaviorTests { fun `promoted fixed escapes ancestor overflow clipping but keeps paint hit symmetry`() { val fixedColor = 0x00_33_99_CC val root = ContainerNode(key = "fixed-overflow-root", stackLayout = true) - val clippedAncestor = ContainerNode(key = "fixed-overflow-ancestor").apply { - width = 90 - height = 46 - margin = Insets(top = 8, right = 0, bottom = 0, left = 12) - overflowY = Overflow.Auto - stackLayout = true - }.applyParent(root) - ContainerNode(key = "fixed-overflow-filler").apply { - width = 80 - height = 220 - }.applyParent(clippedAncestor) + val clippedAncestor = + ContainerNode(key = "fixed-overflow-ancestor") + .apply { + width = 90 + height = 46 + margin = Insets(top = 8, right = 0, bottom = 0, left = 12) + overflowY = Overflow.Auto + stackLayout = true + }.applyParent(root) + ContainerNode(key = "fixed-overflow-filler") + .apply { + width = 80 + height = 220 + }.applyParent(clippedAncestor) var fixedClicks = 0 - val fixed = ButtonNode("fixed", key = "fixed-overflow-node", backgroundColor = fixedColor).apply { - width = 44 - height = 20 - zIndex = 9_999 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "140px", - StyleProperty.TOP to "90px" - ) - onClick { fixedClicks += 1 } - }.applyParent(clippedAncestor) + val fixed = + ButtonNode("fixed", key = "fixed-overflow-node", backgroundColor = fixedColor) + .apply { + width = 44 + height = 20 + zIndex = 9_999 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "140px", + StyleProperty.TOP to "90px", + ) + onClick { fixedClicks += 1 } + }.applyParent(clippedAncestor) val tree = DomTree(root) tree.render(ctx, 220, 160) @@ -71,12 +80,13 @@ class PositionedLayoutFixedClippingBehaviorTests { assertTrue(dispatchClick(root, MouseClickEvent(145, 95, MouseButton.LEFT))) assertEquals(1, fixedClicks) - val fixedDrawIndex = commands.indexOfFirst { command -> - command is RenderCommand.DrawRect && - command.color == fixedColor && - command.x == 140 && - command.y == 90 - } + val fixedDrawIndex = + commands.indexOfFirst { command -> + command is RenderCommand.DrawRect && + command.color == fixedColor && + command.x == 140 && + command.y == 90 + } assertTrue(fixedDrawIndex > 0, "Expected fixed draw rect in root-level paint stream") val fixedClip = commands[fixedDrawIndex - 1] as? RenderCommand.PushClip @@ -91,15 +101,18 @@ class PositionedLayoutFixedClippingBehaviorTests { @Test fun `promoted fixed still respects root viewport clipping`() { val root = ContainerNode(key = "fixed-root-clip-root") - val fixed = ButtonNode("fixed", key = "fixed-root-clip-node").apply { - width = 50 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "180px", - StyleProperty.TOP to "24px" - ) - }.applyParent(root) + val fixed = + ButtonNode("fixed", key = "fixed-root-clip-node") + .apply { + width = 50 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "180px", + StyleProperty.TOP to "24px", + ) + }.applyParent(root) val tree = DomTree(root) tree.render(ctx, 200, 120) @@ -108,12 +121,13 @@ class PositionedLayoutFixedClippingBehaviorTests { assertTrue(fixed.containsGlobalPoint(195, 30)) assertFalse( fixed.containsGlobalPoint(225, 30), - "Point inside fixed bounds but outside root viewport must be clipped" + "Point inside fixed bounds but outside root viewport must be clipped", ) - val drawIndex = commands.indexOfFirst { command -> - command is RenderCommand.DrawRect && command.x == 180 && command.y == 24 - } + val drawIndex = + commands.indexOfFirst { command -> + command is RenderCommand.DrawRect && command.x == 180 && command.y == 24 + } assertTrue(drawIndex > 0) val clipBeforeDraw = commands[drawIndex - 1] as? RenderCommand.PushClip assertNotNull(clipBeforeDraw) @@ -127,24 +141,29 @@ class PositionedLayoutFixedClippingBehaviorTests { @Test fun `non-fixed descendants still obey ancestor overflow clipping`() { val root = ContainerNode(key = "nonfixed-clip-root", stackLayout = true) - val clippedAncestor = ContainerNode(key = "nonfixed-clip-ancestor").apply { - width = 90 - height = 46 - margin = Insets(top = 8, right = 0, bottom = 0, left = 12) - overflowY = Overflow.Scroll - }.applyParent(root) + val clippedAncestor = + ContainerNode(key = "nonfixed-clip-ancestor") + .apply { + width = 90 + height = 46 + margin = Insets(top = 8, right = 0, bottom = 0, left = 12) + overflowY = Overflow.Scroll + }.applyParent(root) var clicks = 0 - val absolute = ButtonNode("absolute", key = "nonfixed-clip-node").apply { - width = 44 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "absolute", - StyleProperty.LEFT to "140px", - StyleProperty.TOP to "90px" - ) - onClick { clicks += 1 } - }.applyParent(clippedAncestor) + val absolute = + ButtonNode("absolute", key = "nonfixed-clip-node") + .apply { + width = 44 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "absolute", + StyleProperty.LEFT to "140px", + StyleProperty.TOP to "90px", + ) + onClick { clicks += 1 } + }.applyParent(clippedAncestor) val tree = DomTree(root) tree.render(ctx, 220, 160) @@ -156,11 +175,10 @@ class PositionedLayoutFixedClippingBehaviorTests { assertEquals(0, clicks) } - private fun styleDeclarations(vararg entries: Pair): StyleDeclarations { - return StyleDeclarations().apply { + private fun styleDeclarations(vararg entries: Pair): StyleDeclarations = + StyleDeclarations().apply { entries.forEach { (property, literal) -> set(property, StyleExpression.Literal(literal)) } } - } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutFixedStackingCharacterizationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutFixedStackingCharacterizationTests.kt index af44df1..3d9311b 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutFixedStackingCharacterizationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutFixedStackingCharacterizationTests.kt @@ -9,10 +9,10 @@ import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.event.MouseClickEvent import org.dreamfinity.dsgl.core.event.dispatchClick import org.dreamfinity.dsgl.core.render.RenderCommand +import org.dreamfinity.dsgl.core.style.PositionMode import org.dreamfinity.dsgl.core.style.StyleDeclarations import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleExpression -import org.dreamfinity.dsgl.core.style.PositionMode import org.dreamfinity.dsgl.core.style.StyleProperty import kotlin.test.AfterTest import kotlin.test.Test @@ -22,11 +22,14 @@ import kotlin.test.assertSame import kotlin.test.assertTrue class PositionedLayoutFixedStackingCharacterizationTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -37,21 +40,24 @@ class PositionedLayoutFixedStackingCharacterizationTests { @Test fun `nested fixed geometry is still root anchored in current model`() { val root = ContainerNode(key = "fixed-geom-root", stackLayout = true) - val ancestor = ContainerNode(key = "fixed-geom-ancestor").apply { - width = 140 - height = 90 - margin = Insets(top = 40, right = 0, bottom = 0, left = 60) - } - val fixed = ContainerNode(key = "fixed-geom-node").apply { - width = 24 - height = 12 - zIndex = 9_999 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "9px", - StyleProperty.TOP to "11px" - ) - } + val ancestor = + ContainerNode(key = "fixed-geom-ancestor").apply { + width = 140 + height = 90 + margin = Insets(top = 40, right = 0, bottom = 0, left = 60) + } + val fixed = + ContainerNode(key = "fixed-geom-node").apply { + width = 24 + height = 12 + zIndex = 9_999 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "9px", + StyleProperty.TOP to "11px", + ) + } fixed.applyParent(ancestor) ancestor.applyParent(root) @@ -69,28 +75,33 @@ class PositionedLayoutFixedStackingCharacterizationTests { val laterColor = 0x00_AA_33_55 val root = ContainerNode(key = "fixed-paint-root", stackLayout = true) - val earlySubtree = ContainerNode(key = "fixed-paint-early").apply { - width = 120 - height = 60 - } - val laterSubtree = ContainerNode(key = "fixed-paint-later").apply { - width = 120 - height = 60 - } - val fixed = ButtonNode("fixed", backgroundColor = fixedColor, key = "fixed-paint-node").apply { - width = 72 - height = 24 - zIndex = 9_999 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "8px", - StyleProperty.TOP to "8px" - ) - } - val later = ButtonNode("later", backgroundColor = laterColor, key = "fixed-paint-later-node").apply { - width = 72 - height = 24 - } + val earlySubtree = + ContainerNode(key = "fixed-paint-early").apply { + width = 120 + height = 60 + } + val laterSubtree = + ContainerNode(key = "fixed-paint-later").apply { + width = 120 + height = 60 + } + val fixed = + ButtonNode("fixed", backgroundColor = fixedColor, key = "fixed-paint-node").apply { + width = 72 + height = 24 + zIndex = 9_999 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "8px", + StyleProperty.TOP to "8px", + ) + } + val later = + ButtonNode("later", backgroundColor = laterColor, key = "fixed-paint-later-node").apply { + width = 72 + height = 24 + } fixed.applyParent(earlySubtree) later.applyParent(laterSubtree) earlySubtree.applyParent(root) @@ -101,52 +112,59 @@ class PositionedLayoutFixedStackingCharacterizationTests { val commands = tree.paint(ctx) val drawRects = commands.filterIsInstance() - val fixedPaintIndex = drawRects.indexOfFirst { rect -> - rect.color == fixedColor && rect.x == 8 && rect.y == 8 - } - val laterPaintIndex = drawRects.indexOfFirst { rect -> - rect.color == laterColor && rect.x == 0 && rect.y == 0 - } + val fixedPaintIndex = + drawRects.indexOfFirst { rect -> + rect.color == fixedColor && rect.x == 8 && rect.y == 8 + } + val laterPaintIndex = + drawRects.indexOfFirst { rect -> + rect.color == laterColor && rect.x == 0 && rect.y == 0 + } assertTrue(fixedPaintIndex >= 0, "Expected fixed draw rect in paint command stream") assertTrue(laterPaintIndex >= 0, "Expected later sibling draw rect in paint command stream") assertTrue( fixedPaintIndex > laterPaintIndex, - "Fixed should participate in root paint ordering and paint above lower-priority later sibling content" + "Fixed should participate in root paint ordering and paint above lower-priority later sibling content", ) } @Test fun `nested fixed high z-index now wins hit over later root sibling subtree`() { val root = ContainerNode(key = "fixed-hit-root", stackLayout = true) - val earlySubtree = ContainerNode(key = "fixed-hit-early").apply { - width = 120 - height = 60 - } - val laterSubtree = ContainerNode(key = "fixed-hit-later").apply { - width = 120 - height = 60 - } + val earlySubtree = + ContainerNode(key = "fixed-hit-early").apply { + width = 120 + height = 60 + } + val laterSubtree = + ContainerNode(key = "fixed-hit-later").apply { + width = 120 + height = 60 + } var fixedClicks = 0 var laterClicks = 0 - val fixed = ButtonNode("fixed", key = "fixed-hit-node").apply { - width = 72 - height = 24 - zIndex = 9_999 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "8px", - StyleProperty.TOP to "8px" - ) - onClick { fixedClicks += 1 } - } - val later = ButtonNode("later", key = "fixed-hit-later-node").apply { - width = 72 - height = 24 - onClick { laterClicks += 1 } - } + val fixed = + ButtonNode("fixed", key = "fixed-hit-node").apply { + width = 72 + height = 24 + zIndex = 9_999 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "8px", + StyleProperty.TOP to "8px", + ) + onClick { fixedClicks += 1 } + } + val later = + ButtonNode("later", key = "fixed-hit-later-node").apply { + width = 72 + height = 24 + onClick { laterClicks += 1 } + } fixed.applyParent(earlySubtree) later.applyParent(laterSubtree) @@ -166,15 +184,18 @@ class PositionedLayoutFixedStackingCharacterizationTests { val root = ContainerNode(key = "fixed-local-root", stackLayout = true) val earlySubtree = ContainerNode(key = "fixed-local-early").applyParent(root) val laterSubtree = ContainerNode(key = "fixed-local-later").applyParent(root) - val nestedFixed = ContainerNode(key = "fixed-local-nested").apply { - position = PositionMode.Fixed - zIndex = 9_999 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "4px", - StyleProperty.TOP to "4px" - ) - }.applyParent(earlySubtree) + val nestedFixed = + ContainerNode(key = "fixed-local-nested") + .apply { + position = PositionMode.Fixed + zIndex = 9_999 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "4px", + StyleProperty.TOP to "4px", + ) + }.applyParent(earlySubtree) assertSame(earlySubtree, nestedFixed.parent) assertEquals(listOf(earlySubtree, laterSubtree, nestedFixed), root.orderedChildrenForPaintTraversal()) @@ -188,15 +209,18 @@ class PositionedLayoutFixedStackingCharacterizationTests { val root = ContainerNode(key = "fixed-sym-root", stackLayout = true) val earlySubtree = ContainerNode(key = "fixed-sym-early").applyParent(root) val laterSubtree = ContainerNode(key = "fixed-sym-later").applyParent(root) - val fixed = ContainerNode(key = "fixed-sym-nested").apply { - position = PositionMode.Fixed - zIndex = 9_999 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "6px", - StyleProperty.TOP to "6px" - ) - }.applyParent(earlySubtree) + val fixed = + ContainerNode(key = "fixed-sym-nested") + .apply { + position = PositionMode.Fixed + zIndex = 9_999 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "6px", + StyleProperty.TOP to "6px", + ) + }.applyParent(earlySubtree) val nonFixedNested = ContainerNode(key = "fixed-sym-nested-normal").applyParent(earlySubtree) val rootPaint = root.orderedChildrenForPaintTraversal() @@ -227,11 +251,10 @@ class PositionedLayoutFixedStackingCharacterizationTests { DomTree(root).render(ctx, width, height) } - private fun styleDeclarations(vararg entries: Pair): StyleDeclarations { - return StyleDeclarations().apply { + private fun styleDeclarations(vararg entries: Pair): StyleDeclarations = + StyleDeclarations().apply { entries.forEach { (property, literal) -> set(property, StyleExpression.Literal(literal)) } } - } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutModelTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutModelTests.kt index 2b31979..84beb21 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutModelTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutModelTests.kt @@ -36,16 +36,18 @@ class PositionedLayoutModelTests { val staticChild = ContainerNode(key = "static") staticChild.applyParent(root) - val positionedLow = ContainerNode(key = "positioned-low").apply { - position = PositionMode.Relative - zIndex = -1 - } + val positionedLow = + ContainerNode(key = "positioned-low").apply { + position = PositionMode.Relative + zIndex = -1 + } positionedLow.applyParent(root) - val positionedHigh = ContainerNode(key = "positioned-high").apply { - position = PositionMode.Relative - zIndex = 5 - } + val positionedHigh = + ContainerNode(key = "positioned-high").apply { + position = PositionMode.Relative + zIndex = 5 + } positionedHigh.applyParent(root) val paintOrder = root.orderedChildrenForPaintTraversal() @@ -58,15 +60,18 @@ class PositionedLayoutModelTests { @Test fun `equal ordering priority uses dom order tie-breaker`() { val root = ContainerNode(key = "tie-root") - val first = ContainerNode(key = "first").apply { - position = PositionMode.Relative - } - val second = ContainerNode(key = "second").apply { - position = PositionMode.Relative - } - val third = ContainerNode(key = "third").apply { - position = PositionMode.Relative - } + val first = + ContainerNode(key = "first").apply { + position = PositionMode.Relative + } + val second = + ContainerNode(key = "second").apply { + position = PositionMode.Relative + } + val third = + ContainerNode(key = "third").apply { + position = PositionMode.Relative + } first.applyParent(root) second.applyParent(root) @@ -80,9 +85,11 @@ class PositionedLayoutModelTests { fun `absolute containing block primitive resolves nearest positioned ancestor or root`() { val root = ContainerNode(key = "cb-root") val staticAncestor = ContainerNode(key = "cb-static-ancestor").applyParent(root) - val positionedAncestor = ContainerNode(key = "cb-positioned-ancestor").apply { - position = PositionMode.Relative - }.applyParent(staticAncestor) + val positionedAncestor = + ContainerNode(key = "cb-positioned-ancestor") + .apply { + position = PositionMode.Relative + }.applyParent(staticAncestor) val nestedStatic = ContainerNode(key = "cb-nested-static").applyParent(positionedAncestor) val leaf = ContainerNode(key = "cb-leaf").applyParent(nestedStatic) @@ -96,13 +103,17 @@ class PositionedLayoutModelTests { @Test fun `sticky participates in ordering but not absolute containing-block semantics`() { val root = ContainerNode(key = "sticky-root") - val stickyAncestor = ContainerNode(key = "sticky-ancestor").apply { - position = PositionMode.Sticky - zIndex = 99 - }.applyParent(root) - val absoluteLeaf = ContainerNode(key = "sticky-absolute-leaf").apply { - position = PositionMode.Absolute - }.applyParent(stickyAncestor) + val stickyAncestor = + ContainerNode(key = "sticky-ancestor") + .apply { + position = PositionMode.Sticky + zIndex = 99 + }.applyParent(root) + val absoluteLeaf = + ContainerNode(key = "sticky-absolute-leaf") + .apply { + position = PositionMode.Absolute + }.applyParent(stickyAncestor) assertTrue(stickyAncestor.participatesInPositionedOrderingModel()) assertTrue(PositionedLayoutModel.matchesChildContextTrigger(stickyAncestor)) @@ -127,26 +138,31 @@ class PositionedLayoutModelTests { val rootA = ContainerNode(key = "z-root-a") val rootB = ContainerNode(key = "z-root-b") - val aLow = ContainerNode(key = "a-low").apply { - position = PositionMode.Relative - zIndex = -10 - }.applyParent(rootA) - val aHigh = ContainerNode(key = "a-high").apply { - position = PositionMode.Relative - zIndex = 10 - }.applyParent(rootA) - - val bOnly = ContainerNode(key = "b-only").apply { - position = PositionMode.Relative - zIndex = 999 - }.applyParent(rootB) + val aLow = + ContainerNode(key = "a-low") + .apply { + position = PositionMode.Relative + zIndex = -10 + }.applyParent(rootA) + val aHigh = + ContainerNode(key = "a-high") + .apply { + position = PositionMode.Relative + zIndex = 10 + }.applyParent(rootA) + + val bOnly = + ContainerNode(key = "b-only") + .apply { + position = PositionMode.Relative + zIndex = 999 + }.applyParent(rootB) assertEquals(listOf(aLow, aHigh), rootA.orderedChildrenForPaintTraversal()) assertEquals(listOf(bOnly), rootB.orderedChildrenForPaintTraversal()) assertFalse(aHigh.sharesRootStackingScopeForPositioning(bOnly)) } - @Test fun `offset precedence contract prefers left over right and top over bottom`() { val left = CssLength.px(12) @@ -170,26 +186,30 @@ class PositionedLayoutModelTests { assertEquals(StyleProperty.BOTTOM, verticalFallback.sourceProperty) assertEquals(bottom, verticalFallback.value) } + @Test fun `dispatch click follows reverse paint order`() { - val root = ContainerNode(key = "click-root").apply { - bounds = Rect(0, 0, 120, 80) - } + val root = + ContainerNode(key = "click-root").apply { + bounds = Rect(0, 0, 120, 80) + } var underClicks = 0 var overClicks = 0 - val under = ButtonNode("under", key = "under").apply { - bounds = Rect(10, 10, 80, 24) - onClick { underClicks += 1 } - } + val under = + ButtonNode("under", key = "under").apply { + bounds = Rect(10, 10, 80, 24) + onClick { underClicks += 1 } + } under.applyParent(root) - val over = ButtonNode("over", key = "over").apply { - bounds = Rect(10, 10, 80, 24) - position = PositionMode.Relative - zIndex = 2 - onClick { overClicks += 1 } - } + val over = + ButtonNode("over", key = "over").apply { + bounds = Rect(10, 10, 80, 24) + position = PositionMode.Relative + zIndex = 2 + onClick { overClicks += 1 } + } over.applyParent(root) val paintOrder = root.orderedChildrenForPaintTraversal() @@ -201,4 +221,3 @@ class PositionedLayoutModelTests { assertEquals(0, underClicks) } } - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutRelativeBehaviorTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutRelativeBehaviorTests.kt index 8fca9c7..8782a8a 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutRelativeBehaviorTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutRelativeBehaviorTests.kt @@ -19,11 +19,14 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue class PositionedLayoutRelativeBehaviorTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -34,15 +37,17 @@ class PositionedLayoutRelativeBehaviorTests { @Test fun `relative applies visible offset to final geometry`() { val root = ContainerNode(key = "root") - val child = ContainerNode(key = "relative-child").apply { - width = 40 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "relative", - StyleProperty.LEFT to "12px", - StyleProperty.TOP to "8px" - ) - } + val child = + ContainerNode(key = "relative-child").apply { + width = 40 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "relative", + StyleProperty.LEFT to "12px", + StyleProperty.TOP to "8px", + ) + } child.applyParent(root) renderTree(root) @@ -54,19 +59,22 @@ class PositionedLayoutRelativeBehaviorTests { @Test fun `relative preserves normal-flow layout slot`() { val root = ContainerNode(key = "flow-root") - val first = ContainerNode(key = "first-relative").apply { - width = 50 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "relative", - StyleProperty.LEFT to "24px", - StyleProperty.TOP to "16px" - ) - } - val second = ContainerNode(key = "second-static").apply { - width = 30 - height = 12 - } + val first = + ContainerNode(key = "first-relative").apply { + width = 50 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "relative", + StyleProperty.LEFT to "24px", + StyleProperty.TOP to "16px", + ) + } + val second = + ContainerNode(key = "second-static").apply { + width = 30 + height = 12 + } first.applyParent(root) second.applyParent(root) @@ -79,15 +87,17 @@ class PositionedLayoutRelativeBehaviorTests { fun `relative hit-testing tracks visible shifted rect`() { val root = ContainerNode(key = "hit-root") var clicks = 0 - val button = ButtonNode("relative", key = "relative-button").apply { - width = 40 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "relative", - StyleProperty.LEFT to "30px" - ) - onClick { clicks += 1 } - } + val button = + ButtonNode("relative", key = "relative-button").apply { + width = 40 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "relative", + StyleProperty.LEFT to "30px", + ) + onClick { clicks += 1 } + } button.applyParent(root) renderTree(root) @@ -102,15 +112,17 @@ class PositionedLayoutRelativeBehaviorTests { @Test fun `relative horizontal precedence prefers left over right`() { val root = ContainerNode(key = "horizontal-root") - val child = ContainerNode(key = "horizontal-relative").apply { - width = 30 - height = 16 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "relative", - StyleProperty.LEFT to "20px", - StyleProperty.RIGHT to "5px" - ) - } + val child = + ContainerNode(key = "horizontal-relative").apply { + width = 30 + height = 16 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "relative", + StyleProperty.LEFT to "20px", + StyleProperty.RIGHT to "5px", + ) + } child.applyParent(root) renderTree(root) @@ -122,15 +134,17 @@ class PositionedLayoutRelativeBehaviorTests { @Test fun `relative vertical precedence prefers top over bottom`() { val root = ContainerNode(key = "vertical-root") - val child = ContainerNode(key = "vertical-relative").apply { - width = 30 - height = 16 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "relative", - StyleProperty.TOP to "15px", - StyleProperty.BOTTOM to "4px" - ) - } + val child = + ContainerNode(key = "vertical-relative").apply { + width = 30 + height = 16 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "relative", + StyleProperty.TOP to "15px", + StyleProperty.BOTTOM to "4px", + ) + } child.applyParent(root) renderTree(root) @@ -142,15 +156,17 @@ class PositionedLayoutRelativeBehaviorTests { @Test fun `static continues to ignore offsets for visible behavior`() { val root = ContainerNode(key = "static-root") - val child = ContainerNode(key = "static-child").apply { - width = 40 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "static", - StyleProperty.LEFT to "50px", - StyleProperty.TOP to "30px" - ) - } + val child = + ContainerNode(key = "static-child").apply { + width = 40 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "static", + StyleProperty.LEFT to "50px", + StyleProperty.TOP to "30px", + ) + } child.applyParent(root) renderTree(root) @@ -162,10 +178,11 @@ class PositionedLayoutRelativeBehaviorTests { @Test fun `inspector overrides reposition for relative but not static`() { val root = ContainerNode(key = "inspector-root") - val child = ContainerNode(key = "inspector-child").apply { - width = 40 - height = 20 - } + val child = + ContainerNode(key = "inspector-child").apply { + width = 40 + height = 20 + } child.applyParent(root) val tree = DomTree(root) tree.render(ctx, 220, 140) @@ -189,11 +206,10 @@ class PositionedLayoutRelativeBehaviorTests { DomTree(root).render(ctx, 240, 160) } - private fun styleDeclarations(vararg entries: Pair): StyleDeclarations { - return StyleDeclarations().apply { + private fun styleDeclarations(vararg entries: Pair): StyleDeclarations = + StyleDeclarations().apply { entries.forEach { (property, literal) -> set(property, StyleExpression.Literal(literal)) } } - } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutStaticBaselineTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutStaticBaselineTests.kt index 44dab32..a280ec4 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutStaticBaselineTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutStaticBaselineTests.kt @@ -23,11 +23,14 @@ import kotlin.test.assertNotNull import kotlin.test.assertTrue class PositionedLayoutStaticBaselineTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -38,10 +41,11 @@ class PositionedLayoutStaticBaselineTests { @Test fun `default position remains static`() { val root = ContainerNode(key = "root") - val child = ContainerNode(key = "child").apply { - width = 40 - height = 18 - } + val child = + ContainerNode(key = "child").apply { + width = 40 + height = 18 + } child.applyParent(root) renderTree(root) @@ -55,20 +59,23 @@ class PositionedLayoutStaticBaselineTests { @Test fun `explicit static matches default layout behavior`() { val defaultRoot = ContainerNode(key = "default-root") - val defaultChild = ContainerNode(key = "default-child").apply { - width = 40 - height = 18 - } + val defaultChild = + ContainerNode(key = "default-child").apply { + width = 40 + height = 18 + } defaultChild.applyParent(defaultRoot) val explicitRoot = ContainerNode(key = "explicit-root") - val explicitChild = ContainerNode(key = "explicit-child").apply { - width = 40 - height = 18 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "static" - ) - } + val explicitChild = + ContainerNode(key = "explicit-child").apply { + width = 40 + height = 18 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "static", + ) + } explicitChild.applyParent(explicitRoot) renderTree(defaultRoot) @@ -80,24 +87,27 @@ class PositionedLayoutStaticBaselineTests { @Test fun `static ignores offsets for visible and hit geometry`() { val baselineRoot = ContainerNode(key = "baseline-root") - val baselineChild = ContainerNode(key = "baseline-child").apply { - width = 40 - height = 18 - } + val baselineChild = + ContainerNode(key = "baseline-child").apply { + width = 40 + height = 18 + } baselineChild.applyParent(baselineRoot) val offsetRoot = ContainerNode(key = "offset-root") - val offsetChild = ContainerNode(key = "offset-child").apply { - width = 40 - height = 18 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "static", - StyleProperty.LEFT to "40px", - StyleProperty.TOP to "30px", - StyleProperty.RIGHT to "8px", - StyleProperty.BOTTOM to "6px" - ) - } + val offsetChild = + ContainerNode(key = "offset-child").apply { + width = 40 + height = 18 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "static", + StyleProperty.LEFT to "40px", + StyleProperty.TOP to "30px", + StyleProperty.RIGHT to "8px", + StyleProperty.BOTTOM to "6px", + ) + } offsetChild.applyParent(offsetRoot) renderTree(baselineRoot) @@ -119,19 +129,22 @@ class PositionedLayoutStaticBaselineTests { @Test fun `static node remains in normal flow even with offsets declared`() { val root = ContainerNode(key = "flow-root") - val first = ContainerNode(key = "first").apply { - width = 50 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "static", - StyleProperty.LEFT to "60px", - StyleProperty.TOP to "40px" - ) - } - val second = ContainerNode(key = "second").apply { - width = 30 - height = 12 - } + val first = + ContainerNode(key = "first").apply { + width = 50 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "static", + StyleProperty.LEFT to "60px", + StyleProperty.TOP to "40px", + ) + } + val second = + ContainerNode(key = "second").apply { + width = 30 + height = 12 + } first.applyParent(root) second.applyParent(root) @@ -142,24 +155,27 @@ class PositionedLayoutStaticBaselineTests { @Test fun `static z-index does not reorder static paint or hit order`() { - val root = ContainerNode(key = "click-root").apply { - bounds = Rect(0, 0, 120, 80) - } + val root = + ContainerNode(key = "click-root").apply { + bounds = Rect(0, 0, 120, 80) + } var underClicks = 0 var overClicks = 0 - val under = ButtonNode("under", key = "under").apply { - bounds = Rect(10, 10, 80, 24) - zIndex = 100 - onClick { underClicks += 1 } - } + val under = + ButtonNode("under", key = "under").apply { + bounds = Rect(10, 10, 80, 24) + zIndex = 100 + onClick { underClicks += 1 } + } under.applyParent(root) - val over = ButtonNode("over", key = "over").apply { - bounds = Rect(10, 10, 80, 24) - zIndex = -100 - onClick { overClicks += 1 } - } + val over = + ButtonNode("over", key = "over").apply { + bounds = Rect(10, 10, 80, 24) + zIndex = -100 + onClick { overClicks += 1 } + } over.applyParent(root) assertEquals(listOf(under, over), root.orderedChildrenForPaintTraversal()) @@ -172,10 +188,11 @@ class PositionedLayoutStaticBaselineTests { @Test fun `inspector static offset overrides keep static geometry unchanged`() { val root = ContainerNode(key = "inspector-root") - val child = ContainerNode(key = "inspector-child").apply { - width = 44 - height = 16 - } + val child = + ContainerNode(key = "inspector-child").apply { + width = 44 + height = 16 + } child.applyParent(root) val tree = DomTree(root) tree.render(ctx, 200, 120) @@ -193,20 +210,23 @@ class PositionedLayoutStaticBaselineTests { @Test fun `sticky style value remains in-flow but participates in ordering`() { val root = ContainerNode(key = "sticky-inactive-root") - val sticky = ContainerNode(key = "sticky-inactive-node").apply { - width = 50 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.LEFT to "60px", - StyleProperty.TOP to "30px" - ) - zIndex = 100 - } - val follower = ContainerNode(key = "sticky-inactive-follower").apply { - width = 30 - height = 12 - } + val sticky = + ContainerNode(key = "sticky-inactive-node").apply { + width = 50 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.LEFT to "60px", + StyleProperty.TOP to "30px", + ) + zIndex = 100 + } + val follower = + ContainerNode(key = "sticky-inactive-follower").apply { + width = 30 + height = 12 + } sticky.applyParent(root) follower.applyParent(root) @@ -223,11 +243,10 @@ class PositionedLayoutStaticBaselineTests { DomTree(root).render(ctx, 240, 160) } - private fun styleDeclarations(vararg entries: Pair): StyleDeclarations { - return StyleDeclarations().apply { + private fun styleDeclarations(vararg entries: Pair): StyleDeclarations = + StyleDeclarations().apply { entries.forEach { (property, literal) -> set(property, StyleExpression.Literal(literal)) } } - } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutStickyBehaviorTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutStickyBehaviorTests.kt index 5385a5c..56e1f30 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutStickyBehaviorTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutStickyBehaviorTests.kt @@ -26,11 +26,14 @@ import kotlin.test.assertNotNull import kotlin.test.assertTrue class PositionedLayoutStickyBehaviorTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -40,21 +43,25 @@ class PositionedLayoutStickyBehaviorTests { @Test fun `sticky top sticks visually and keeps normal flow slot`() { - val root = ContainerNode(key = "sticky-top-root").apply { - overflowY = Overflow.Scroll - } - val sticky = ContainerNode(key = "sticky-top-node").apply { - width = 100 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.TOP to "0px" - ) - } - val follower = ContainerNode(key = "sticky-top-follower").apply { - width = 100 - height = 280 - } + val root = + ContainerNode(key = "sticky-top-root").apply { + overflowY = Overflow.Scroll + } + val sticky = + ContainerNode(key = "sticky-top-node").apply { + width = 100 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.TOP to "0px", + ) + } + val follower = + ContainerNode(key = "sticky-top-follower").apply { + width = 100 + height = 280 + } sticky.applyParent(root) follower.applyParent(root) @@ -72,25 +79,30 @@ class PositionedLayoutStickyBehaviorTests { @Test fun `sticky bottom-only mode is deterministic`() { - val root = ContainerNode(key = "sticky-bottom-root").apply { - overflowY = Overflow.Scroll - } - val topSpacer = ContainerNode(key = "sticky-bottom-spacer-top").apply { - width = 100 - height = 140 - } - val sticky = ContainerNode(key = "sticky-bottom-node").apply { - width = 100 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.BOTTOM to "0px" - ) - } - val bottomSpacer = ContainerNode(key = "sticky-bottom-spacer-bottom").apply { - width = 100 - height = 180 - } + val root = + ContainerNode(key = "sticky-bottom-root").apply { + overflowY = Overflow.Scroll + } + val topSpacer = + ContainerNode(key = "sticky-bottom-spacer-top").apply { + width = 100 + height = 140 + } + val sticky = + ContainerNode(key = "sticky-bottom-node").apply { + width = 100 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.BOTTOM to "0px", + ) + } + val bottomSpacer = + ContainerNode(key = "sticky-bottom-spacer-bottom").apply { + width = 100 + height = 180 + } topSpacer.applyParent(root) sticky.applyParent(root) bottomSpacer.applyParent(root) @@ -106,22 +118,26 @@ class PositionedLayoutStickyBehaviorTests { @Test fun `sticky both top and bottom uses top precedence`() { - val root = ContainerNode(key = "sticky-both-root").apply { - overflowY = Overflow.Scroll - } - val sticky = ContainerNode(key = "sticky-both-node").apply { - width = 80 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.TOP to "10px", - StyleProperty.BOTTOM to "0px" - ) - } - val follower = ContainerNode(key = "sticky-both-follower").apply { - width = 80 - height = 220 - } + val root = + ContainerNode(key = "sticky-both-root").apply { + overflowY = Overflow.Scroll + } + val sticky = + ContainerNode(key = "sticky-both-node").apply { + width = 80 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.TOP to "10px", + StyleProperty.BOTTOM to "0px", + ) + } + val follower = + ContainerNode(key = "sticky-both-follower").apply { + width = 80 + height = 220 + } sticky.applyParent(root) follower.applyParent(root) @@ -133,22 +149,26 @@ class PositionedLayoutStickyBehaviorTests { @Test fun `sticky left sticks visually on horizontal axis and keeps normal flow slot`() { - val root = ContainerNode(key = "sticky-left-root").apply { - display = Display.Flex - overflowX = Overflow.Scroll - } - val sticky = ContainerNode(key = "sticky-left-node").apply { - width = 40 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.LEFT to "0px" - ) - } - val filler = ContainerNode(key = "sticky-left-filler").apply { - width = 260 - height = 20 - } + val root = + ContainerNode(key = "sticky-left-root").apply { + display = Display.Flex + overflowX = Overflow.Scroll + } + val sticky = + ContainerNode(key = "sticky-left-node").apply { + width = 40 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.LEFT to "0px", + ) + } + val filler = + ContainerNode(key = "sticky-left-filler").apply { + width = 260 + height = 20 + } sticky.applyParent(root) filler.applyParent(root) @@ -166,26 +186,31 @@ class PositionedLayoutStickyBehaviorTests { @Test fun `sticky right-only mode is deterministic on horizontal axis`() { - val root = ContainerNode(key = "sticky-right-root").apply { - display = Display.Flex - overflowX = Overflow.Scroll - } - val spacer = ContainerNode(key = "sticky-right-spacer").apply { - width = 140 - height = 20 - } - val sticky = ContainerNode(key = "sticky-right-node").apply { - width = 20 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.RIGHT to "0px" - ) - } - val tail = ContainerNode(key = "sticky-right-tail").apply { - width = 120 - height = 20 - } + val root = + ContainerNode(key = "sticky-right-root").apply { + display = Display.Flex + overflowX = Overflow.Scroll + } + val spacer = + ContainerNode(key = "sticky-right-spacer").apply { + width = 140 + height = 20 + } + val sticky = + ContainerNode(key = "sticky-right-node").apply { + width = 20 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.RIGHT to "0px", + ) + } + val tail = + ContainerNode(key = "sticky-right-tail").apply { + width = 120 + height = 20 + } spacer.applyParent(root) sticky.applyParent(root) tail.applyParent(root) @@ -201,23 +226,27 @@ class PositionedLayoutStickyBehaviorTests { @Test fun `sticky both left and right uses left precedence`() { - val root = ContainerNode(key = "sticky-horizontal-both-root").apply { - display = Display.Flex - overflowX = Overflow.Scroll - } - val sticky = ContainerNode(key = "sticky-horizontal-both-node").apply { - width = 20 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.LEFT to "7px", - StyleProperty.RIGHT to "0px" - ) - } - val filler = ContainerNode(key = "sticky-horizontal-both-filler").apply { - width = 200 - height = 20 - } + val root = + ContainerNode(key = "sticky-horizontal-both-root").apply { + display = Display.Flex + overflowX = Overflow.Scroll + } + val sticky = + ContainerNode(key = "sticky-horizontal-both-node").apply { + width = 20 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.LEFT to "7px", + StyleProperty.RIGHT to "0px", + ) + } + val filler = + ContainerNode(key = "sticky-horizontal-both-filler").apply { + width = 200 + height = 20 + } sticky.applyParent(root) filler.applyParent(root) @@ -229,23 +258,27 @@ class PositionedLayoutStickyBehaviorTests { @Test fun `sticky combines horizontal and vertical offsets from independent axis rules`() { - val root = ContainerNode(key = "sticky-xy-root").apply { - overflowX = Overflow.Scroll - overflowY = Overflow.Scroll - } - val sticky = ContainerNode(key = "sticky-xy-node").apply { - width = 80 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.LEFT to "0px", - StyleProperty.TOP to "0px" - ) - } - val filler = ContainerNode(key = "sticky-xy-filler").apply { - width = 400 - height = 400 - } + val root = + ContainerNode(key = "sticky-xy-root").apply { + overflowX = Overflow.Scroll + overflowY = Overflow.Scroll + } + val sticky = + ContainerNode(key = "sticky-xy-node").apply { + width = 80 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.LEFT to "0px", + StyleProperty.TOP to "0px", + ) + } + val filler = + ContainerNode(key = "sticky-xy-filler").apply { + width = 400 + height = 400 + } sticky.applyParent(root) filler.applyParent(root) @@ -262,21 +295,25 @@ class PositionedLayoutStickyBehaviorTests { @Test fun `sticky with no horizontal inset stays inactive on horizontal axis`() { - val root = ContainerNode(key = "sticky-horizontal-inactive-root").apply { - display = Display.Flex - overflowX = Overflow.Scroll - } - val sticky = ContainerNode(key = "sticky-horizontal-inactive-node").apply { - width = 40 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky" - ) - } - val filler = ContainerNode(key = "sticky-horizontal-inactive-filler").apply { - width = 260 - height = 20 - } + val root = + ContainerNode(key = "sticky-horizontal-inactive-root").apply { + display = Display.Flex + overflowX = Overflow.Scroll + } + val sticky = + ContainerNode(key = "sticky-horizontal-inactive-node").apply { + width = 40 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + ) + } + val filler = + ContainerNode(key = "sticky-horizontal-inactive-filler").apply { + width = 260 + height = 20 + } sticky.applyParent(root) filler.applyParent(root) @@ -290,20 +327,24 @@ class PositionedLayoutStickyBehaviorTests { @Test fun `sticky with no inset stays inactive on vertical axis`() { - val root = ContainerNode(key = "sticky-inactive-root").apply { - overflowY = Overflow.Scroll - } - val sticky = ContainerNode(key = "sticky-inactive-node").apply { - width = 90 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky" - ) - } - val filler = ContainerNode(key = "sticky-inactive-filler").apply { - width = 90 - height = 260 - } + val root = + ContainerNode(key = "sticky-inactive-root").apply { + overflowY = Overflow.Scroll + } + val sticky = + ContainerNode(key = "sticky-inactive-node").apply { + width = 90 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + ) + } + val filler = + ContainerNode(key = "sticky-inactive-filler").apply { + width = 90 + height = 260 + } sticky.applyParent(root) filler.applyParent(root) @@ -317,21 +358,25 @@ class PositionedLayoutStickyBehaviorTests { @Test fun `sticky used geometry resolves from shared path without render-owned refresh`() { - val root = ContainerNode(key = "sticky-refresh-root").apply { - overflowY = Overflow.Scroll - } - val sticky = ContainerNode(key = "sticky-refresh-node").apply { - width = 100 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.TOP to "0px" - ) - } - val filler = ContainerNode(key = "sticky-refresh-filler").apply { - width = 100 - height = 260 - } + val root = + ContainerNode(key = "sticky-refresh-root").apply { + overflowY = Overflow.Scroll + } + val sticky = + ContainerNode(key = "sticky-refresh-node").apply { + width = 100 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.TOP to "0px", + ) + } + val filler = + ContainerNode(key = "sticky-refresh-filler").apply { + width = 100 + height = 260 + } sticky.applyParent(root) filler.applyParent(root) @@ -345,23 +390,27 @@ class PositionedLayoutStickyBehaviorTests { @Test fun `sticky render and interaction use same final geometry`() { - val root = ContainerNode(key = "sticky-click-root").apply { - overflowY = Overflow.Scroll - } + val root = + ContainerNode(key = "sticky-click-root").apply { + overflowY = Overflow.Scroll + } var clicks = 0 - val sticky = ButtonNode("sticky", key = "sticky-click-node").apply { - width = 100 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.TOP to "0px" - ) - onClick { clicks += 1 } - } - val filler = ContainerNode(key = "sticky-click-filler").apply { - width = 100 - height = 260 - } + val sticky = + ButtonNode("sticky", key = "sticky-click-node").apply { + width = 100 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.TOP to "0px", + ) + onClick { clicks += 1 } + } + val filler = + ContainerNode(key = "sticky-click-filler").apply { + width = 100 + height = 260 + } sticky.applyParent(root) filler.applyParent(root) @@ -379,32 +428,38 @@ class PositionedLayoutStickyBehaviorTests { fun `sticky with high z-index paints above later normal-flow overlap content`() { val stickyColor = 0xFF1B6BA8.toInt() val overlapColor = 0xFFE06262.toInt() - val root = ContainerNode(key = "sticky-z-paint-root").apply { - width = 120 - height = 80 - overflowY = Overflow.Auto - } - val sticky = ContainerNode(key = "sticky-z-paint-sticky", backgroundColor = stickyColor).apply { - width = 120 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.TOP to "0px", - StyleProperty.Z_INDEX to "999" - ) - } - val filler = ContainerNode(key = "sticky-z-paint-filler").apply { - width = 120 - height = 80 - } - val overlap = ContainerNode(key = "sticky-z-paint-overlap", backgroundColor = overlapColor).apply { - width = 120 - height = 20 - } - val tail = ContainerNode(key = "sticky-z-paint-tail").apply { - width = 120 - height = 120 - } + val root = + ContainerNode(key = "sticky-z-paint-root").apply { + width = 120 + height = 80 + overflowY = Overflow.Auto + } + val sticky = + ContainerNode(key = "sticky-z-paint-sticky", backgroundColor = stickyColor).apply { + width = 120 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.TOP to "0px", + StyleProperty.Z_INDEX to "999", + ) + } + val filler = + ContainerNode(key = "sticky-z-paint-filler").apply { + width = 120 + height = 80 + } + val overlap = + ContainerNode(key = "sticky-z-paint-overlap", backgroundColor = overlapColor).apply { + width = 120 + height = 20 + } + val tail = + ContainerNode(key = "sticky-z-paint-tail").apply { + width = 120 + height = 120 + } sticky.applyParent(root) filler.applyParent(root) overlap.applyParent(root) @@ -420,52 +475,60 @@ class PositionedLayoutStickyBehaviorTests { assertNotNull(stickyRect.intersection(overlapRect)) val commands = tree.paint(ctx) - val stickyDrawIndex = commands.indexOfFirst { - it is RenderCommand.DrawRect && it.color == stickyColor - } - val overlapDrawIndex = commands.indexOfFirst { - it is RenderCommand.DrawRect && it.color == overlapColor - } + val stickyDrawIndex = + commands.indexOfFirst { + it is RenderCommand.DrawRect && it.color == stickyColor + } + val overlapDrawIndex = + commands.indexOfFirst { + it is RenderCommand.DrawRect && it.color == overlapColor + } assertTrue(stickyDrawIndex >= 0, "Expected sticky draw command") assertTrue(overlapDrawIndex >= 0, "Expected overlap draw command") assertTrue( stickyDrawIndex > overlapDrawIndex, - "Expected sticky draw after overlap draw, but stickyDrawIndex=$stickyDrawIndex overlapDrawIndex=$overlapDrawIndex" + "Expected sticky draw after overlap draw, but stickyDrawIndex=$stickyDrawIndex overlapDrawIndex=$overlapDrawIndex", ) } @Test fun `sticky with high z-index wins hover and click over later overlap content`() { - val root = ContainerNode(key = "sticky-z-hit-root").apply { - width = 120 - height = 80 - overflowY = Overflow.Auto - } + val root = + ContainerNode(key = "sticky-z-hit-root").apply { + width = 120 + height = 80 + overflowY = Overflow.Auto + } var stickyClicks = 0 var overlapClicks = 0 - val sticky = ButtonNode("sticky-top", key = "sticky-z-hit-sticky").apply { - width = 120 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.TOP to "0px", - StyleProperty.Z_INDEX to "999" - ) - onClick { stickyClicks += 1 } - } - val filler = ContainerNode(key = "sticky-z-hit-filler").apply { - width = 120 - height = 80 - } - val overlap = ButtonNode("later-overlap", key = "sticky-z-hit-overlap").apply { - width = 120 - height = 20 - onClick { overlapClicks += 1 } - } - val tail = ContainerNode(key = "sticky-z-hit-tail").apply { - width = 120 - height = 120 - } + val sticky = + ButtonNode("sticky-top", key = "sticky-z-hit-sticky").apply { + width = 120 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.TOP to "0px", + StyleProperty.Z_INDEX to "999", + ) + onClick { stickyClicks += 1 } + } + val filler = + ContainerNode(key = "sticky-z-hit-filler").apply { + width = 120 + height = 80 + } + val overlap = + ButtonNode("later-overlap", key = "sticky-z-hit-overlap").apply { + width = 120 + height = 20 + onClick { overlapClicks += 1 } + } + val tail = + ContainerNode(key = "sticky-z-hit-tail").apply { + width = 120 + height = 120 + } sticky.applyParent(root) filler.applyParent(root) overlap.applyParent(root) @@ -493,27 +556,32 @@ class PositionedLayoutStickyBehaviorTests { @Test fun `sticky top remains correct during scrollbar thumb drag`() { - val root = ContainerNode(key = "sticky-drag-root").apply { - width = 120 - height = 90 - overflowY = Overflow.Auto - } - val topSpacer = ContainerNode(key = "sticky-drag-spacer").apply { - width = 120 - height = 40 - } - val sticky = ContainerNode(key = "sticky-drag-node").apply { - width = 120 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.TOP to "0px" - ) - } - val filler = ContainerNode(key = "sticky-drag-filler").apply { - width = 120 - height = 320 - } + val root = + ContainerNode(key = "sticky-drag-root").apply { + width = 120 + height = 90 + overflowY = Overflow.Auto + } + val topSpacer = + ContainerNode(key = "sticky-drag-spacer").apply { + width = 120 + height = 40 + } + val sticky = + ContainerNode(key = "sticky-drag-node").apply { + width = 120 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.TOP to "0px", + ) + } + val filler = + ContainerNode(key = "sticky-drag-filler").apply { + width = 120 + height = 320 + } topSpacer.applyParent(root) sticky.applyParent(root) filler.applyParent(root) @@ -546,27 +614,32 @@ class PositionedLayoutStickyBehaviorTests { @Test fun `sticky drag stays correct when transform is read before paint`() { - val root = ContainerNode(key = "sticky-drag-transform-read-root").apply { - width = 120 - height = 90 - overflowY = Overflow.Auto - } - val topSpacer = ContainerNode(key = "sticky-drag-transform-read-spacer").apply { - width = 120 - height = 40 - } - val sticky = ContainerNode(key = "sticky-drag-transform-read-node").apply { - width = 120 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.TOP to "0px" - ) - } - val filler = ContainerNode(key = "sticky-drag-transform-read-filler").apply { - width = 120 - height = 320 - } + val root = + ContainerNode(key = "sticky-drag-transform-read-root").apply { + width = 120 + height = 90 + overflowY = Overflow.Auto + } + val topSpacer = + ContainerNode(key = "sticky-drag-transform-read-spacer").apply { + width = 120 + height = 40 + } + val sticky = + ContainerNode(key = "sticky-drag-transform-read-node").apply { + width = 120 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.TOP to "0px", + ) + } + val filler = + ContainerNode(key = "sticky-drag-transform-read-filler").apply { + width = 120 + height = 320 + } topSpacer.applyParent(root) sticky.applyParent(root) filler.applyParent(root) @@ -600,29 +673,34 @@ class PositionedLayoutStickyBehaviorTests { @Test fun `sticky render interaction and inspector stay aligned for combined axis movement`() { - val root = ContainerNode(key = "sticky-xy-consistency-root").apply { - overflowX = Overflow.Scroll - overflowY = Overflow.Scroll - } + val root = + ContainerNode(key = "sticky-xy-consistency-root").apply { + overflowX = Overflow.Scroll + overflowY = Overflow.Scroll + } var clicks = 0 - val sticky = ButtonNode("sticky", key = "sticky-xy-consistency-node").apply { - width = 80 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.LEFT to "0px", - StyleProperty.TOP to "0px" - ) - onClick { clicks += 1 } - } - val spacer = ContainerNode(key = "sticky-xy-consistency-spacer").apply { - width = 20 - height = 120 - } - val filler = ContainerNode(key = "sticky-xy-consistency-filler").apply { - width = 420 - height = 420 - } + val sticky = + ButtonNode("sticky", key = "sticky-xy-consistency-node").apply { + width = 80 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.LEFT to "0px", + StyleProperty.TOP to "0px", + ) + onClick { clicks += 1 } + } + val spacer = + ContainerNode(key = "sticky-xy-consistency-spacer").apply { + width = 20 + height = 120 + } + val filler = + ContainerNode(key = "sticky-xy-consistency-filler").apply { + width = 420 + height = 420 + } sticky.applyParent(root) spacer.applyParent(root) filler.applyParent(root) @@ -650,21 +728,25 @@ class PositionedLayoutStickyBehaviorTests { @Test fun `inspector picks and highlights sticky at final used geometry`() { - val root = ContainerNode(key = "sticky-inspector-root").apply { - overflowY = Overflow.Scroll - } - val sticky = ButtonNode("sticky", key = "sticky-inspector-node").apply { - width = 100 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.TOP to "0px" - ) - } - val filler = ContainerNode(key = "sticky-inspector-filler").apply { - width = 40 - height = 240 - } + val root = + ContainerNode(key = "sticky-inspector-root").apply { + overflowY = Overflow.Scroll + } + val sticky = + ButtonNode("sticky", key = "sticky-inspector-node").apply { + width = 100 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.TOP to "0px", + ) + } + val filler = + ContainerNode(key = "sticky-inspector-filler").apply { + width = 40 + height = 240 + } sticky.applyParent(root) filler.applyParent(root) @@ -690,34 +772,41 @@ class PositionedLayoutStickyBehaviorTests { @Test fun `sticky horizontal movement is clamped by direct-parent containing block`() { - val root = ContainerNode(key = "sticky-clamp-x-root").apply { - display = Display.Flex - overflowX = Overflow.Scroll - } - val leftSpacer = ContainerNode(key = "sticky-clamp-x-left-spacer").apply { - width = 200 - height = 80 - } - val section = ContainerNode(key = "sticky-clamp-x-section").apply { - width = 60 - height = 80 - } - val sticky = ContainerNode(key = "sticky-clamp-x-node").apply { - width = 20 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.LEFT to "0px" - ) - } - val sectionFiller = ContainerNode(key = "sticky-clamp-x-section-filler").apply { - width = 120 - height = 40 - } - val rightSpacer = ContainerNode(key = "sticky-clamp-x-right-spacer").apply { - width = 220 - height = 80 - } + val root = + ContainerNode(key = "sticky-clamp-x-root").apply { + display = Display.Flex + overflowX = Overflow.Scroll + } + val leftSpacer = + ContainerNode(key = "sticky-clamp-x-left-spacer").apply { + width = 200 + height = 80 + } + val section = + ContainerNode(key = "sticky-clamp-x-section").apply { + width = 60 + height = 80 + } + val sticky = + ContainerNode(key = "sticky-clamp-x-node").apply { + width = 20 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.LEFT to "0px", + ) + } + val sectionFiller = + ContainerNode(key = "sticky-clamp-x-section-filler").apply { + width = 120 + height = 40 + } + val rightSpacer = + ContainerNode(key = "sticky-clamp-x-right-spacer").apply { + width = 220 + height = 80 + } leftSpacer.applyParent(root) section.applyParent(root) rightSpacer.applyParent(root) @@ -735,33 +824,40 @@ class PositionedLayoutStickyBehaviorTests { @Test fun `sticky movement is clamped by direct-parent containing block`() { - val root = ContainerNode(key = "sticky-clamp-root").apply { - overflowY = Overflow.Scroll - } - val topSpacer = ContainerNode(key = "sticky-clamp-top-spacer").apply { - width = 120 - height = 60 - } - val section = ContainerNode(key = "sticky-clamp-section").apply { - width = 120 - height = 120 - } - val sticky = ContainerNode(key = "sticky-clamp-node").apply { - width = 120 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.TOP to "0px" - ) - } - val sectionFiller = ContainerNode(key = "sticky-clamp-section-filler").apply { - width = 120 - height = 220 - } - val bottomSpacer = ContainerNode(key = "sticky-clamp-bottom-spacer").apply { - width = 120 - height = 260 - } + val root = + ContainerNode(key = "sticky-clamp-root").apply { + overflowY = Overflow.Scroll + } + val topSpacer = + ContainerNode(key = "sticky-clamp-top-spacer").apply { + width = 120 + height = 60 + } + val section = + ContainerNode(key = "sticky-clamp-section").apply { + width = 120 + height = 120 + } + val sticky = + ContainerNode(key = "sticky-clamp-node").apply { + width = 120 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.TOP to "0px", + ) + } + val sectionFiller = + ContainerNode(key = "sticky-clamp-section-filler").apply { + width = 120 + height = 220 + } + val bottomSpacer = + ContainerNode(key = "sticky-clamp-bottom-spacer").apply { + width = 120 + height = 260 + } topSpacer.applyParent(root) section.applyParent(root) sticky.applyParent(section) @@ -780,40 +876,55 @@ class PositionedLayoutStickyBehaviorTests { @Test fun `nested scroll containers preserve sticky correctness on inner local scroll updates`() { val root = ContainerNode(key = "sticky-nested-root") - val outer = ContainerNode(key = "sticky-nested-outer").apply { - width = 220 - height = 180 - overflowY = Overflow.Auto - }.applyParent(root) - val outerSpacer = ContainerNode(key = "sticky-nested-outer-spacer").apply { - width = 200 - height = 48 - }.applyParent(outer) - val inner = ContainerNode(key = "sticky-nested-inner").apply { - width = 200 - height = 110 - overflowY = Overflow.Auto - }.applyParent(outer) - val innerSpacer = ContainerNode(key = "sticky-nested-inner-spacer").apply { - width = 200 - height = 28 - }.applyParent(inner) - val sticky = ContainerNode(key = "sticky-nested-node").apply { - width = 200 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.TOP to "0px" - ) - }.applyParent(inner) - val innerFiller = ContainerNode(key = "sticky-nested-inner-filler").apply { - width = 200 - height = 260 - }.applyParent(inner) - val outerFiller = ContainerNode(key = "sticky-nested-outer-filler").apply { - width = 200 - height = 220 - }.applyParent(outer) + val outer = + ContainerNode(key = "sticky-nested-outer") + .apply { + width = 220 + height = 180 + overflowY = Overflow.Auto + }.applyParent(root) + val outerSpacer = + ContainerNode(key = "sticky-nested-outer-spacer") + .apply { + width = 200 + height = 48 + }.applyParent(outer) + val inner = + ContainerNode(key = "sticky-nested-inner") + .apply { + width = 200 + height = 110 + overflowY = Overflow.Auto + }.applyParent(outer) + val innerSpacer = + ContainerNode(key = "sticky-nested-inner-spacer") + .apply { + width = 200 + height = 28 + }.applyParent(inner) + val sticky = + ContainerNode(key = "sticky-nested-node") + .apply { + width = 200 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.TOP to "0px", + ) + }.applyParent(inner) + val innerFiller = + ContainerNode(key = "sticky-nested-inner-filler") + .apply { + width = 200 + height = 260 + }.applyParent(inner) + val outerFiller = + ContainerNode(key = "sticky-nested-outer-filler") + .apply { + width = 200 + height = 220 + }.applyParent(outer) val tree = DomTree(root) tree.render(ctx, 320, 260) @@ -835,40 +946,47 @@ class PositionedLayoutStickyBehaviorTests { @Test fun `fixed subtree and sticky remain correct during local scroll updates`() { - val root = ContainerNode(key = "sticky-fixed-root").apply { - width = 220 - height = 140 - overflowY = Overflow.Auto - } + val root = + ContainerNode(key = "sticky-fixed-root").apply { + width = 220 + height = 140 + overflowY = Overflow.Auto + } var stickyClicks = 0 var fixedClicks = 0 - val spacer = ContainerNode(key = "sticky-fixed-spacer").apply { - width = 200 - height = 36 - } - val sticky = ButtonNode("sticky", key = "sticky-fixed-sticky").apply { - width = 120 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.TOP to "0px" - ) - onClick { stickyClicks += 1 } - } - val fixed = ButtonNode("fixed", key = "sticky-fixed-fixed").apply { - width = 80 - height = 18 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "8px", - StyleProperty.TOP to "6px" - ) - onClick { fixedClicks += 1 } - } - val filler = ContainerNode(key = "sticky-fixed-filler").apply { - width = 200 - height = 300 - } + val spacer = + ContainerNode(key = "sticky-fixed-spacer").apply { + width = 200 + height = 36 + } + val sticky = + ButtonNode("sticky", key = "sticky-fixed-sticky").apply { + width = 120 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.TOP to "0px", + ) + onClick { stickyClicks += 1 } + } + val fixed = + ButtonNode("fixed", key = "sticky-fixed-fixed").apply { + width = 80 + height = 18 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "8px", + StyleProperty.TOP to "6px", + ) + onClick { fixedClicks += 1 } + } + val filler = + ContainerNode(key = "sticky-fixed-filler").apply { + width = 200 + height = 300 + } spacer.applyParent(root) sticky.applyParent(root) fixed.applyParent(root) @@ -899,57 +1017,69 @@ class PositionedLayoutStickyBehaviorTests { @Test fun `non-sticky positioned modes remain unchanged with sticky enabled`() { - val root = ContainerNode(key = "sticky-regression-root").apply { - overflowY = Overflow.Scroll - } - val staticNode = ContainerNode(key = "sticky-regression-static").apply { - width = 30 - height = 12 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "static", - StyleProperty.LEFT to "50px", - StyleProperty.TOP to "20px" - ) - } - val relativeNode = ContainerNode(key = "sticky-regression-relative").apply { - width = 30 - height = 12 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "relative", - StyleProperty.LEFT to "18px", - StyleProperty.TOP to "7px" - ) - } - val absoluteNode = ContainerNode(key = "sticky-regression-absolute").apply { - width = 20 - height = 10 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "absolute", - StyleProperty.LEFT to "40px", - StyleProperty.TOP to "22px" - ) - } - val fixedNode = ContainerNode(key = "sticky-regression-fixed").apply { - width = 20 - height = 10 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "15px", - StyleProperty.TOP to "9px" - ) - } - val stickyNode = ContainerNode(key = "sticky-regression-sticky").apply { - width = 80 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.TOP to "0px" - ) - } - val filler = ContainerNode(key = "sticky-regression-filler").apply { - width = 120 - height = 260 - } + val root = + ContainerNode(key = "sticky-regression-root").apply { + overflowY = Overflow.Scroll + } + val staticNode = + ContainerNode(key = "sticky-regression-static").apply { + width = 30 + height = 12 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "static", + StyleProperty.LEFT to "50px", + StyleProperty.TOP to "20px", + ) + } + val relativeNode = + ContainerNode(key = "sticky-regression-relative").apply { + width = 30 + height = 12 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "relative", + StyleProperty.LEFT to "18px", + StyleProperty.TOP to "7px", + ) + } + val absoluteNode = + ContainerNode(key = "sticky-regression-absolute").apply { + width = 20 + height = 10 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "absolute", + StyleProperty.LEFT to "40px", + StyleProperty.TOP to "22px", + ) + } + val fixedNode = + ContainerNode(key = "sticky-regression-fixed").apply { + width = 20 + height = 10 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "15px", + StyleProperty.TOP to "9px", + ) + } + val stickyNode = + ContainerNode(key = "sticky-regression-sticky").apply { + width = 80 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.TOP to "0px", + ) + } + val filler = + ContainerNode(key = "sticky-regression-filler").apply { + width = 120 + height = 260 + } staticNode.applyParent(root) relativeNode.applyParent(root) @@ -976,13 +1106,9 @@ class PositionedLayoutStickyBehaviorTests { assertEquals(fixedBeforeScroll, fixedNode.bounds) } - private fun usedRect(node: ContainerNode): Rect { - return UsedInteractionGeometryResolver.resolveNodeGeometry(node).usedBorderRect - } + private fun usedRect(node: ContainerNode): Rect = UsedInteractionGeometryResolver.resolveNodeGeometry(node).usedBorderRect - private fun usedRect(node: ButtonNode): Rect { - return UsedInteractionGeometryResolver.resolveNodeGeometry(node).usedBorderRect - } + private fun usedRect(node: ButtonNode): Rect = UsedInteractionGeometryResolver.resolveNodeGeometry(node).usedBorderRect private fun visibleRect(node: ContainerNode): Rect { val geometry = UsedInteractionGeometryResolver.resolveNodeGeometry(node) @@ -994,12 +1120,10 @@ class PositionedLayoutStickyBehaviorTests { return geometry.visibleBorderRect ?: geometry.usedBorderRect } - private fun styleDeclarations(vararg entries: Pair): StyleDeclarations { - return StyleDeclarations().apply { + private fun styleDeclarations(vararg entries: Pair): StyleDeclarations = + StyleDeclarations().apply { entries.forEach { (property, literal) -> set(property, StyleExpression.Literal(literal)) } } - } } - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutZIndexOrderingTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutZIndexOrderingTests.kt index 9f57055..bd3e02f 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutZIndexOrderingTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutZIndexOrderingTests.kt @@ -18,11 +18,14 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue class PositionedLayoutZIndexOrderingTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -49,12 +52,14 @@ class PositionedLayoutZIndexOrderingTests { var lowClicks = 0 var highClicks = 0 - button("low", zIndex = 0, position = PositionMode.Relative).apply { - onClick { lowClicks += 1 } - }.applyParent(root) - button("high", zIndex = 6, position = PositionMode.Relative).apply { - onClick { highClicks += 1 } - }.applyParent(root) + button("low", zIndex = 0, position = PositionMode.Relative) + .apply { + onClick { lowClicks += 1 } + }.applyParent(root) + button("high", zIndex = 6, position = PositionMode.Relative) + .apply { + onClick { highClicks += 1 } + }.applyParent(root) renderTree(root, width = 220, height = 140) @@ -69,12 +74,16 @@ class PositionedLayoutZIndexOrderingTests { var firstClicks = 0 var secondClicks = 0 - val first = button("first", zIndex = 2, position = PositionMode.Relative).apply { - onClick { firstClicks += 1 } - }.applyParent(root) - val second = button("second", zIndex = 2, position = PositionMode.Relative).apply { - onClick { secondClicks += 1 } - }.applyParent(root) + val first = + button("first", zIndex = 2, position = PositionMode.Relative) + .apply { + onClick { firstClicks += 1 } + }.applyParent(root) + val second = + button("second", zIndex = 2, position = PositionMode.Relative) + .apply { + onClick { secondClicks += 1 } + }.applyParent(root) renderTree(root, width = 220, height = 140) @@ -90,12 +99,16 @@ class PositionedLayoutZIndexOrderingTests { var staticClicks = 0 var positionedClicks = 0 - val staticNode = button("static", zIndex = 999, position = PositionMode.Static).apply { - onClick { staticClicks += 1 } - }.applyParent(root) - val positionedNode = button("positioned", zIndex = -100, position = PositionMode.Relative).apply { - onClick { positionedClicks += 1 } - }.applyParent(root) + val staticNode = + button("static", zIndex = 999, position = PositionMode.Static) + .apply { + onClick { staticClicks += 1 } + }.applyParent(root) + val positionedNode = + button("positioned", zIndex = -100, position = PositionMode.Relative) + .apply { + onClick { positionedClicks += 1 } + }.applyParent(root) renderTree(root, width = 220, height = 140) @@ -129,12 +142,16 @@ class PositionedLayoutZIndexOrderingTests { var lowerClicks = 0 var upperClicks = 0 - val lower = button("lower", zIndex = 1, position = PositionMode.Relative).apply { - onClick { lowerClicks += 1 } - }.applyParent(root) - val upper = button("upper", zIndex = 3, position = PositionMode.Relative).apply { - onClick { upperClicks += 1 } - }.applyParent(root) + val lower = + button("lower", zIndex = 1, position = PositionMode.Relative) + .apply { + onClick { lowerClicks += 1 } + }.applyParent(root) + val upper = + button("upper", zIndex = 3, position = PositionMode.Relative) + .apply { + onClick { upperClicks += 1 } + }.applyParent(root) val tree = DomTree(root) tree.render(ctx, 220, 140) @@ -156,12 +173,11 @@ class PositionedLayoutZIndexOrderingTests { DomTree(root).render(ctx, width, height) } - private fun button(key: String, zIndex: Int, position: PositionMode): ButtonNode { - return ButtonNode(key, key = key).apply { + private fun button(key: String, zIndex: Int, position: PositionMode): ButtonNode = + ButtonNode(key, key = key).apply { width = 80 height = 24 this.zIndex = zIndex this.position = position } - } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/RangeInputScrollFastPathTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/RangeInputScrollFastPathTests.kt index f1f8fee..db937e5 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/RangeInputScrollFastPathTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/RangeInputScrollFastPathTests.kt @@ -1,10 +1,5 @@ package org.dreamfinity.dsgl.core.dom -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.DomTree import org.dreamfinity.dsgl.core.debug.ScrollPerformanceCounters import org.dreamfinity.dsgl.core.dom.DOMNode @@ -16,17 +11,25 @@ import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.Overflow +import org.dreamfinity.dsgl.core.style.StyleDeclarations import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleExpression import org.dreamfinity.dsgl.core.style.StyleProperty -import org.dreamfinity.dsgl.core.style.StyleDeclarations +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue class RangeInputScrollFastPathTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -61,7 +64,10 @@ class RangeInputScrollFastPathTests { @Test fun `range track follows bounds during scrollbar thumb drag fast path`() { val fixture = createFixture(includeSticky = false) - val visual = fixture.viewport.debugScrollbarVisualState().vertical ?: error("Expected vertical scrollbar") + val visual = + fixture.viewport + .debugScrollbarVisualState() + .vertical ?: error("Expected vertical scrollbar") val dragX = visual.thumbRect.x + visual.thumbRect.width / 2 val startY = visual.thumbRect.y + visual.thumbRect.height / 2 @@ -133,7 +139,10 @@ class RangeInputScrollFastPathTests { fun `sticky remains correct while thumb dragging with range input present`() { val fixture = createFixture(includeSticky = true) val sticky = assertNotNull(fixture.sticky) - val visual = fixture.viewport.debugScrollbarVisualState().vertical ?: error("Expected vertical scrollbar") + val visual = + fixture.viewport + .debugScrollbarVisualState() + .vertical ?: error("Expected vertical scrollbar") val dragX = visual.thumbRect.x + visual.thumbRect.width / 2 val startY = visual.thumbRect.y + visual.thumbRect.height / 2 @@ -154,18 +163,27 @@ class RangeInputScrollFastPathTests { @Test fun `high frequency tiny thumb drag deltas keep range track aligned`() { val fixture = createFixture(includeSticky = false) - val visual = fixture.viewport.debugScrollbarVisualState().vertical ?: error("Expected vertical scrollbar") + val visual = + fixture.viewport + .debugScrollbarVisualState() + .vertical ?: error("Expected vertical scrollbar") val dragX = visual.thumbRect.x + visual.thumbRect.width / 2 val startY = visual.thumbRect.y + visual.thumbRect.height / 2 assertTrue(fixture.router.handleMouseDown(dragX, startY, MouseButton.LEFT)) - var previousScroll = fixture.viewport.scrollContainerState().scrollY + var previousScroll = + fixture.viewport + .scrollContainerState() + .scrollY var previousTrackY = expectedTrackRect(fixture.range).y repeat(32) { step -> assertTrue(fixture.router.handleMouseMove(dragX, startY + step + 1)) val commands = fixture.tree.paint(ctx) - val scrollY = fixture.viewport.scrollContainerState().scrollY + val scrollY = + fixture.viewport + .scrollContainerState() + .scrollY val trackY = expectedTrackRect(fixture.range).y assertTrue(scrollY >= previousScroll) assertTrue(trackY <= previousTrackY) @@ -193,41 +211,52 @@ class RangeInputScrollFastPathTests { private fun createFixture(includeSticky: Boolean): Fixture { val root = ContainerNode(key = "range-fast-root") - val viewport = ContainerNode(key = "range-fast-viewport").apply { - width = 170 - height = 90 - overflowX = Overflow.Hidden - overflowY = Overflow.Auto - }.applyParent(root) - - val sticky = if (includeSticky) { - ContainerNode(key = "range-fast-sticky").apply { - width = 170 - height = 18 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.TOP to "0px" - ) - }.applyParent(viewport) - } else { - null - } + val viewport = + ContainerNode(key = "range-fast-viewport") + .apply { + width = 170 + height = 90 + overflowX = Overflow.Hidden + overflowY = Overflow.Auto + }.applyParent(root) + + val sticky = + if (includeSticky) { + ContainerNode(key = "range-fast-sticky") + .apply { + width = 170 + height = 18 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.TOP to "0px", + ) + }.applyParent(viewport) + } else { + null + } - val controls = ContainerNode(key = "range-fast-controls").apply { - width = 170 - }.applyParent(viewport) - ContainerNode(key = "range-fast-spacer").apply { - width = 170 - height = 42 - }.applyParent(controls) - val range = RangeInputNode(value = 0L, min = 0L, max = 100L, key = "range-fast-input").apply { - width = 150 - height = 16 - }.applyParent(controls) - ContainerNode(key = "range-fast-filler").apply { - width = 170 - height = 360 - }.applyParent(controls) + val controls = + ContainerNode(key = "range-fast-controls") + .apply { + width = 170 + }.applyParent(viewport) + ContainerNode(key = "range-fast-spacer") + .apply { + width = 170 + height = 42 + }.applyParent(controls) + val range = + RangeInputNode(value = 0L, min = 0L, max = 100L, key = "range-fast-input") + .apply { + width = 150 + height = 16 + }.applyParent(controls) + ContainerNode(key = "range-fast-filler") + .apply { + width = 170 + height = 360 + }.applyParent(controls) val tree = DomTree(root) tree.render(ctx, 420, 260) @@ -241,42 +270,52 @@ class RangeInputScrollFastPathTests { sticky = sticky, router = router, baseRangeY = range.bounds.y, - baseStickyY = sticky?.bounds?.y ?: 0 + baseStickyY = sticky?.bounds?.y ?: 0, ) } private fun createNestedFixture(): NestedFixture { val root = ContainerNode(key = "range-nested-root") - val outer = ContainerNode(key = "range-nested-outer").apply { - width = 200 - height = 120 - overflowY = Overflow.Auto - }.applyParent(root) - ContainerNode(key = "range-nested-outer-spacer").apply { - width = 200 - height = 26 - }.applyParent(outer) - val inner = ContainerNode(key = "range-nested-inner").apply { - width = 180 - height = 80 - overflowY = Overflow.Auto - }.applyParent(outer) - ContainerNode(key = "range-nested-inner-spacer").apply { - width = 180 - height = 20 - }.applyParent(inner) - val range = RangeInputNode(value = 0L, min = 0L, max = 100L, key = "range-nested-input").apply { - width = 150 - height = 16 - }.applyParent(inner) - ContainerNode(key = "range-nested-inner-filler").apply { - width = 180 - height = 280 - }.applyParent(inner) - ContainerNode(key = "range-nested-outer-filler").apply { - width = 200 - height = 260 - }.applyParent(outer) + val outer = + ContainerNode(key = "range-nested-outer") + .apply { + width = 200 + height = 120 + overflowY = Overflow.Auto + }.applyParent(root) + ContainerNode(key = "range-nested-outer-spacer") + .apply { + width = 200 + height = 26 + }.applyParent(outer) + val inner = + ContainerNode(key = "range-nested-inner") + .apply { + width = 180 + height = 80 + overflowY = Overflow.Auto + }.applyParent(outer) + ContainerNode(key = "range-nested-inner-spacer") + .apply { + width = 180 + height = 20 + }.applyParent(inner) + val range = + RangeInputNode(value = 0L, min = 0L, max = 100L, key = "range-nested-input") + .apply { + width = 150 + height = 16 + }.applyParent(inner) + ContainerNode(key = "range-nested-inner-filler") + .apply { + width = 180 + height = 280 + }.applyParent(inner) + ContainerNode(key = "range-nested-outer-filler") + .apply { + width = 200 + height = 260 + }.applyParent(outer) val tree = DomTree(root) tree.render(ctx, 520, 320) @@ -289,24 +328,25 @@ class RangeInputScrollFastPathTests { inner = inner, range = range, router = router, - baseRangeY = range.bounds.y + baseRangeY = range.bounds.y, ) } private fun assertTrackMatchesBounds(commands: List, range: RangeInputNode) { val expected = expectedTrackRect(range) - val match = commands - .filterIsInstance() - .firstOrNull { command -> - command.x == expected.x && - command.y == expected.y && - command.width == expected.width && - command.height == expected.height && - command.color == range.trackColor - } + val match = + commands + .filterIsInstance() + .firstOrNull { command -> + command.x == expected.x && + command.y == expected.y && + command.width == expected.width && + command.height == expected.height && + command.color == range.trackColor + } assertNotNull( match, - "Range track command did not match expected geometry: expected=$expected bounds=${range.bounds}" + "Range track command did not match expected geometry: expected=$expected bounds=${range.bounds}", ) } @@ -317,7 +357,7 @@ class RangeInputScrollFastPathTests { x = range.bounds.x, y = trackY, width = range.bounds.width, - height = trackHeight + height = trackHeight, ) } @@ -326,13 +366,12 @@ class RangeInputScrollFastPathTests { return geometry.visibleBorderRect ?: geometry.usedBorderRect } - private fun styleDeclarations(vararg entries: Pair): StyleDeclarations { - return StyleDeclarations().apply { + private fun styleDeclarations(vararg entries: Pair): StyleDeclarations = + StyleDeclarations().apply { entries.forEach { (property, literal) -> set(property, StyleExpression.Literal(literal)) } } - } private data class Fixture( val tree: DomTree, @@ -342,7 +381,7 @@ class RangeInputScrollFastPathTests { val sticky: ContainerNode?, val router: LayerDomInputRouter, val baseRangeY: Int, - val baseStickyY: Int + val baseStickyY: Int, ) private data class NestedFixture( @@ -352,6 +391,6 @@ class RangeInputScrollFastPathTests { val inner: ContainerNode, val range: RangeInputNode, val router: LayerDomInputRouter, - val baseRangeY: Int + val baseRangeY: Int, ) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollContainerStateTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollContainerStateTests.kt index 6402a32..ab760c4 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollContainerStateTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollContainerStateTests.kt @@ -1,9 +1,5 @@ package org.dreamfinity.dsgl.core.dom -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.DomTree import org.dreamfinity.dsgl.core.dom.elements.ButtonNode import org.dreamfinity.dsgl.core.dom.elements.ContainerNode @@ -14,13 +10,20 @@ import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.Overflow import org.dreamfinity.dsgl.core.style.StyleEngine +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue class ScrollContainerStateTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -32,15 +35,18 @@ class ScrollContainerStateTests { @Test fun `generic viewport rect is exposed for scroll-capable container`() { val root = ContainerNode(key = "root") - val container = ContainerNode(padding = 4, key = "viewport").apply { - width = 100 - height = 60 - overflow = Overflow.Hidden - }.applyParent(root) - ContainerNode(key = "child").apply { - width = 140 - height = 24 - }.applyParent(container) + val container = + ContainerNode(padding = 4, key = "viewport") + .apply { + width = 100 + height = 60 + overflow = Overflow.Hidden + }.applyParent(root) + ContainerNode(key = "child") + .apply { + width = 140 + height = 24 + }.applyParent(container) DomTree(root).render(ctx, 320, 180) val state = container.scrollContainerState() @@ -54,19 +60,23 @@ class ScrollContainerStateTests { @Test fun `generic content extent is distinct from viewport when content exceeds bounds`() { val root = ContainerNode(key = "root") - val container = ContainerNode(key = "extent").apply { - width = 80 - height = 40 - overflow = Overflow.Hidden - }.applyParent(root) - ContainerNode(key = "child-a").apply { - width = 70 - height = 30 - }.applyParent(container) - ContainerNode(key = "child-b").apply { - width = 70 - height = 30 - }.applyParent(container) + val container = + ContainerNode(key = "extent") + .apply { + width = 80 + height = 40 + overflow = Overflow.Hidden + }.applyParent(root) + ContainerNode(key = "child-a") + .apply { + width = 70 + height = 30 + }.applyParent(container) + ContainerNode(key = "child-b") + .apply { + width = 70 + height = 30 + }.applyParent(container) DomTree(root).render(ctx, 320, 180) val state = container.scrollContainerState() @@ -77,10 +87,12 @@ class ScrollContainerStateTests { @Test fun `per-axis overflow modes resolve to expected scroll-container capability`() { val root = ContainerNode(key = "root") - val subject = ContainerNode(key = "axis").apply { - width = 80 - height = 40 - }.applyParent(root) + val subject = + ContainerNode(key = "axis") + .apply { + width = 80 + height = 40 + }.applyParent(root) DomTree(root).render(ctx, 320, 180) @@ -105,16 +117,19 @@ class ScrollContainerStateTests { @Test fun `generic scroll offsets are meaningful for scroll-capable axes`() { val root = ContainerNode(key = "root") - val container = ContainerNode(key = "scroll-state").apply { - width = 90 - height = 40 - overflowX = Overflow.Visible - overflowY = Overflow.Auto - }.applyParent(root) - ContainerNode(key = "tall-child").apply { - width = 70 - height = 180 - }.applyParent(container) + val container = + ContainerNode(key = "scroll-state") + .apply { + width = 90 + height = 40 + overflowX = Overflow.Visible + overflowY = Overflow.Auto + }.applyParent(root) + ContainerNode(key = "tall-child") + .apply { + width = 70 + height = 180 + }.applyParent(container) container.setScrollOffsets(scrollX = 45, scrollY = 70) DomTree(root).render(ctx, 320, 220) @@ -135,16 +150,19 @@ class ScrollContainerStateTests { @Test fun `overflow scroll always marks axis scrollbar present and reserves gutter`() { val root = ContainerNode(key = "root") - val container = ContainerNode(key = "scroll-axis").apply { - width = 100 - height = 60 - overflowX = Overflow.Scroll - overflowY = Overflow.Scroll - }.applyParent(root) - ContainerNode(key = "child").apply { - width = 40 - height = 20 - }.applyParent(container) + val container = + ContainerNode(key = "scroll-axis") + .apply { + width = 100 + height = 60 + overflowX = Overflow.Scroll + overflowY = Overflow.Scroll + }.applyParent(root) + ContainerNode(key = "child") + .apply { + width = 40 + height = 20 + }.applyParent(container) DomTree(root).render(ctx, 320, 180) val state = container.scrollContainerState() @@ -161,17 +179,21 @@ class ScrollContainerStateTests { @Test fun `overflow auto marks scrollbar presence only when axis content exceeds viewport`() { - val root = ContainerNode(key = "root").apply { - bounds = Rect(0, 0, 320, 180) - } - val container = ContainerNode(key = "auto-axis").apply { - bounds = Rect(10, 10, 100, 60) - overflowX = Overflow.Auto - overflowY = Overflow.Auto - }.applyParent(root) - val child = ContainerNode(key = "child").apply { - bounds = Rect(10, 10, 90, 50) - } + val root = + ContainerNode(key = "root").apply { + bounds = Rect(0, 0, 320, 180) + } + val container = + ContainerNode(key = "auto-axis") + .apply { + bounds = Rect(10, 10, 100, 60) + overflowX = Overflow.Auto + overflowY = Overflow.Auto + }.applyParent(root) + val child = + ContainerNode(key = "child").apply { + bounds = Rect(10, 10, 90, 50) + } child.applyParent(container) var state = container.scrollContainerState() @@ -185,19 +207,23 @@ class ScrollContainerStateTests { assertTrue(state.horizontalScrollbarGutter > 0) assertTrue(state.verticalScrollbarGutter > 0) } + @Test fun `overflow x auto reserves horizontal gutter when measured content width exceeds viewport`() { val root = ContainerNode(key = "root") - val container = ContainerNode(key = "auto-horizontal").apply { - width = 794 - height = 634 - overflowX = Overflow.Auto - overflowY = Overflow.Hidden - }.applyParent(root) - ContainerNode(key = "child").apply { - width = 826 - height = 620 - }.applyParent(container) + val container = + ContainerNode(key = "auto-horizontal") + .apply { + width = 794 + height = 634 + overflowX = Overflow.Auto + overflowY = Overflow.Hidden + }.applyParent(root) + ContainerNode(key = "child") + .apply { + width = 826 + height = 620 + }.applyParent(container) DomTree(root).render(ctx, 1400, 1000) val state = container.scrollContainerState() @@ -212,19 +238,23 @@ class ScrollContainerStateTests { assertEquals(0, state.verticalScrollbarGutter) assertEquals(state.baseViewportRect.height - state.horizontalScrollbarGutter, state.viewportRect.height) } + @Test fun `visible and hidden overflow never expose visible scrollbars or gutters`() { val root = ContainerNode(key = "root") - val container = ContainerNode(key = "hidden-visible-axis").apply { - width = 100 - height = 60 - overflowX = Overflow.Visible - overflowY = Overflow.Hidden - }.applyParent(root) - ContainerNode(key = "child").apply { - width = 180 - height = 220 - }.applyParent(container) + val container = + ContainerNode(key = "hidden-visible-axis") + .apply { + width = 100 + height = 60 + overflowX = Overflow.Visible + overflowY = Overflow.Hidden + }.applyParent(root) + ContainerNode(key = "child") + .apply { + width = 180 + height = 220 + }.applyParent(container) DomTree(root).render(ctx, 320, 220) val state = container.scrollContainerState() @@ -239,27 +269,30 @@ class ScrollContainerStateTests { assertEquals(0, state.axisY.scrollbarGutter) } - @Test fun `mixed overflow axis combinations resolve deterministic state`() { - val cases = listOf( - Triple(Overflow.Hidden, Overflow.Auto, Pair(true, true)), - Triple(Overflow.Visible, Overflow.Scroll, Pair(false, true)), - Triple(Overflow.Scroll, Overflow.Visible, Pair(true, false)) - ) + val cases = + listOf( + Triple(Overflow.Hidden, Overflow.Auto, Pair(true, true)), + Triple(Overflow.Visible, Overflow.Scroll, Pair(false, true)), + Triple(Overflow.Scroll, Overflow.Visible, Pair(true, false)), + ) cases.forEachIndexed { index, (overflowX, overflowY, expectedScrollContainer) -> val root = ContainerNode(key = "root-$index") - val container = ContainerNode(key = "mixed-$index").apply { - width = 110 - height = 66 - this.overflowX = overflowX - this.overflowY = overflowY - }.applyParent(root) - ContainerNode(key = "child-$index").apply { - width = 220 - height = 190 - }.applyParent(container) + val container = + ContainerNode(key = "mixed-$index") + .apply { + width = 110 + height = 66 + this.overflowX = overflowX + this.overflowY = overflowY + }.applyParent(root) + ContainerNode(key = "child-$index") + .apply { + width = 220 + height = 190 + }.applyParent(container) DomTree(root).render(ctx, 360, 220) val state = container.scrollContainerState() @@ -267,27 +300,36 @@ class ScrollContainerStateTests { assertEquals(expectedScrollContainer.first, state.axisX.scrollContainer) assertEquals(expectedScrollContainer.second, state.axisY.scrollContainer) - val expectedVisibleX = overflowX == Overflow.Scroll || (overflowX == Overflow.Auto && state.contentExtent.width > state.viewportRect.width) - val expectedVisibleY = overflowY == Overflow.Scroll || (overflowY == Overflow.Auto && state.contentExtent.height > state.viewportRect.height) + val expectedVisibleX = + overflowX == Overflow.Scroll || + (overflowX == Overflow.Auto && state.contentExtent.width > state.viewportRect.width) + val expectedVisibleY = + overflowY == Overflow.Scroll || + (overflowY == Overflow.Auto && state.contentExtent.height > state.viewportRect.height) assertEquals(expectedVisibleX, state.axisX.scrollbarPresent) assertEquals(expectedVisibleY, state.axisY.scrollbarPresent) assertEquals(if (expectedVisibleX) state.horizontalScrollbarGutter else 0, state.axisX.scrollbarGutter) assertEquals(if (expectedVisibleY) state.verticalScrollbarGutter else 0, state.axisY.scrollbarGutter) } } + @Test fun `cross-axis gutter forcing enables dependent auto scrollbar deterministically`() { - val root = ContainerNode(key = "root").apply { - bounds = Rect(0, 0, 320, 220) - } - val container = ContainerNode(key = "cross-axis").apply { - bounds = Rect(10, 10, 100, 80) - overflowX = Overflow.Auto - overflowY = Overflow.Auto - }.applyParent(root) - ContainerNode(key = "child").apply { - bounds = Rect(10, 10, 96, 220) - }.applyParent(container) + val root = + ContainerNode(key = "root").apply { + bounds = Rect(0, 0, 320, 220) + } + val container = + ContainerNode(key = "cross-axis") + .apply { + bounds = Rect(10, 10, 100, 80) + overflowX = Overflow.Auto + overflowY = Overflow.Auto + }.applyParent(root) + ContainerNode(key = "child") + .apply { + bounds = Rect(10, 10, 96, 220) + }.applyParent(container) val state = container.scrollContainerState() @@ -302,16 +344,19 @@ class ScrollContainerStateTests { @Test fun `final viewport resolution remains stable after gutter resolution`() { val root = ContainerNode(key = "root") - val container = ContainerNode(key = "stable-viewport").apply { - width = 120 - height = 72 - overflowX = Overflow.Auto - overflowY = Overflow.Scroll - }.applyParent(root) - ContainerNode(key = "child").apply { - width = 118 - height = 190 - }.applyParent(container) + val container = + ContainerNode(key = "stable-viewport") + .apply { + width = 120 + height = 72 + overflowX = Overflow.Auto + overflowY = Overflow.Scroll + }.applyParent(root) + ContainerNode(key = "child") + .apply { + width = 118 + height = 190 + }.applyParent(container) DomTree(root).render(ctx, 320, 220) val first = container.scrollContainerState() @@ -326,26 +371,32 @@ class ScrollContainerStateTests { @Test fun `paint and input clipping remain consistent with overflow auto state`() { - val root = ContainerNode(key = "root").apply { - bounds = Rect(0, 0, 320, 200) - } - val clippedViewport = ContainerNode(key = "clip").apply { - bounds = Rect(20, 20, 100, 40) - overflowX = Overflow.Visible - overflowY = Overflow.Auto - }.applyParent(root) - val child = ButtonNode("child", key = "clip-child").apply { - bounds = Rect(24, 46, 80, 22) - } + val root = + ContainerNode(key = "root").apply { + bounds = Rect(0, 0, 320, 200) + } + val clippedViewport = + ContainerNode(key = "clip") + .apply { + bounds = Rect(20, 20, 100, 40) + overflowX = Overflow.Visible + overflowY = Overflow.Auto + }.applyParent(root) + val child = + ButtonNode("child", key = "clip-child").apply { + bounds = Rect(24, 46, 80, 22) + } child.applyParent(clippedViewport) val commands = ArrayList() root.appendRenderCommands(ctx, commands) - assertTrue(commands.any { command -> - command is RenderCommand.PushClip && - command.y == clippedViewport.bounds.y && - command.height == clippedViewport.bounds.height - }) + assertTrue( + commands.any { command -> + command is RenderCommand.PushClip && + command.y == clippedViewport.bounds.y && + command.height == clippedViewport.bounds.height + }, + ) val router = LayerDomInputRouter { root } assertTrue(router.handleMouseDown(30, 58, MouseButton.LEFT)) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollInvalidationSemanticsTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollInvalidationSemanticsTests.kt index fffaa91..080ce0b 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollInvalidationSemanticsTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollInvalidationSemanticsTests.kt @@ -1,11 +1,11 @@ package org.dreamfinity.dsgl.core.dom -import kotlin.test.Test -import kotlin.test.assertFalse -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.style.Overflow +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue class ScrollInvalidationSemanticsTests { @Test @@ -28,11 +28,12 @@ class ScrollInvalidationSemanticsTests { fun `restore snapshot with visual-only delta marks visual interaction without layout`() { val viewport = createScrollableViewport() val baseline = viewport.captureScrollSessionSnapshot() - val visualOnly = baseline.copy( - targetY = baseline.targetY + 36, - displayedY = baseline.displayedY + 36.0, - resolvedY = baseline.resolvedY - ) + val visualOnly = + baseline.copy( + targetY = baseline.targetY + 36, + displayedY = baseline.displayedY + 36.0, + resolvedY = baseline.resolvedY, + ) viewport.restoreScrollSessionSnapshot(visualOnly) val invalidation = viewport.consumeScrollInvalidationRecursively() @@ -45,9 +46,10 @@ class ScrollInvalidationSemanticsTests { @Test fun `root consumption aggregates child scroll invalidation recursively`() { - val root = ContainerNode(key = "root").apply { - bounds = Rect(0, 0, 400, 280) - } + val root = + ContainerNode(key = "root").apply { + bounds = Rect(0, 0, 400, 280) + } val viewport = createScrollableViewport().applyParent(root) viewport.setScrollOffsets(0, 32) @@ -63,11 +65,12 @@ class ScrollInvalidationSemanticsTests { fun `layout-dirty snapshot restore keeps legacy layout-dirty consumer compatible`() { val viewport = createScrollableViewport() val baseline = viewport.captureScrollSessionSnapshot() - val layoutDirty = baseline.copy( - targetY = baseline.targetY + 28, - displayedY = baseline.displayedY + 28.0, - resolvedY = baseline.resolvedY + 28 - ) + val layoutDirty = + baseline.copy( + targetY = baseline.targetY + 28, + displayedY = baseline.displayedY + 28.0, + resolvedY = baseline.resolvedY + 28, + ) viewport.restoreScrollSessionSnapshot(layoutDirty) assertTrue(viewport.consumeScrollLayoutDirtyRecursively()) @@ -77,14 +80,16 @@ class ScrollInvalidationSemanticsTests { } private fun createScrollableViewport(): ContainerNode { - val viewport = ContainerNode(key = "viewport").apply { - bounds = Rect(0, 0, 180, 90) - overflowX = Overflow.Hidden - overflowY = Overflow.Auto - } - ContainerNode(key = "content").apply { - bounds = Rect(0, 0, 180, 420) - }.applyParent(viewport) + val viewport = + ContainerNode(key = "viewport").apply { + bounds = Rect(0, 0, 180, 90) + overflowX = Overflow.Hidden + overflowY = Overflow.Auto + } + ContainerNode(key = "content") + .apply { + bounds = Rect(0, 0, 180, 420) + }.applyParent(viewport) return viewport } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollPerformanceCountersTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollPerformanceCountersTests.kt index 9a1bce3..02abb7a 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollPerformanceCountersTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollPerformanceCountersTests.kt @@ -1,9 +1,5 @@ package org.dreamfinity.dsgl.core.dom -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.DomTree import org.dreamfinity.dsgl.core.debug.ScrollPerformanceCounters import org.dreamfinity.dsgl.core.dom.elements.ContainerNode @@ -17,13 +13,20 @@ import org.dreamfinity.dsgl.core.style.StyleDeclarations import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleExpression import org.dreamfinity.dsgl.core.style.StyleProperty +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue class ScrollPerformanceCountersTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -128,10 +131,11 @@ class ScrollPerformanceCountersTests { fun `guarded scroll visual fast path skips full rerender for visual-only scroll invalidation`() { val fixture = createScrollStickyFixture() val baseline = fixture.viewport.captureScrollSessionSnapshot() - val visualOnlySnapshot = baseline.copy( - displayedY = baseline.displayedY + 0.4, - resolvedY = baseline.resolvedY - ) + val visualOnlySnapshot = + baseline.copy( + displayedY = baseline.displayedY + 0.4, + resolvedY = baseline.resolvedY, + ) fixture.viewport.restoreScrollSessionSnapshot(visualOnlySnapshot) ScrollPerformanceCounters.resetForTests() @@ -156,8 +160,11 @@ class ScrollPerformanceCountersTests { @Test fun `thumb-drag frame keeps sticky correct and remains visual-path only`() { val fixture = createScrollStickyFixture() - val visual = fixture.viewport.debugScrollbarVisualState().vertical - ?: error("Expected vertical scrollbar") + val visual = + fixture.viewport + .debugScrollbarVisualState() + .vertical + ?: error("Expected vertical scrollbar") val dragX = visual.thumbRect.x + visual.thumbRect.width / 2 val startY = visual.thumbRect.y + visual.thumbRect.height / 2 val dragY = startY + 26 @@ -179,8 +186,9 @@ class ScrollPerformanceCountersTests { assertTrue(counters.chunkTraversalCalls > 0L) assertTrue(counters.chunkRebuildCalls > 0L) - val visibleStickyRect = UsedInteractionGeometryResolver.resolveNodeGeometry(fixture.sticky).visibleBorderRect - ?: UsedInteractionGeometryResolver.resolveNodeGeometry(fixture.sticky).usedBorderRect + val visibleStickyRect = + UsedInteractionGeometryResolver.resolveNodeGeometry(fixture.sticky).visibleBorderRect + ?: UsedInteractionGeometryResolver.resolveNodeGeometry(fixture.sticky).usedBorderRect assertEquals(0, visibleStickyRect.y) assertTrue(fixture.router.handleMouseUp(dragX, dragY, MouseButton.LEFT)) } @@ -204,8 +212,9 @@ class ScrollPerformanceCountersTests { val state = fixture.viewport.scrollContainerState() val expectedBaseY = fixture.stickyBaseTopY - state.scrollY val expectedVisibleY = maxOf(expectedBaseY, 0) - val visibleStickyRect = UsedInteractionGeometryResolver.resolveNodeGeometry(fixture.sticky).visibleBorderRect - ?: UsedInteractionGeometryResolver.resolveNodeGeometry(fixture.sticky).usedBorderRect + val visibleStickyRect = + UsedInteractionGeometryResolver.resolveNodeGeometry(fixture.sticky).visibleBorderRect + ?: UsedInteractionGeometryResolver.resolveNodeGeometry(fixture.sticky).usedBorderRect assertEquals(1L, counters.guardedScrollVisualFastPathRuns) assertEquals(0L, counters.fullRerenderLayoutRuns) @@ -221,42 +230,52 @@ class ScrollPerformanceCountersTests { @Test fun `local scroll invalidation refreshes only sticky nodes in affected subtree`() { val root = ContainerNode(key = "perf-narrow-root") - val leftViewport = ContainerNode(key = "perf-narrow-left").apply { - width = 160 - height = 90 - overflowY = Overflow.Auto - }.applyParent(root) - val rightViewport = ContainerNode(key = "perf-narrow-right").apply { - width = 160 - height = 90 - overflowY = Overflow.Auto - }.applyParent(root) - - ContainerNode(key = "perf-narrow-left-sticky").apply { - width = 140 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.TOP to "0px" - ) - }.applyParent(leftViewport) - ContainerNode(key = "perf-narrow-left-filler").apply { - width = 140 - height = 320 - }.applyParent(leftViewport) - - ContainerNode(key = "perf-narrow-right-sticky").apply { - width = 140 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.TOP to "0px" - ) - }.applyParent(rightViewport) - ContainerNode(key = "perf-narrow-right-filler").apply { - width = 140 - height = 320 - }.applyParent(rightViewport) + val leftViewport = + ContainerNode(key = "perf-narrow-left") + .apply { + width = 160 + height = 90 + overflowY = Overflow.Auto + }.applyParent(root) + val rightViewport = + ContainerNode(key = "perf-narrow-right") + .apply { + width = 160 + height = 90 + overflowY = Overflow.Auto + }.applyParent(root) + + ContainerNode(key = "perf-narrow-left-sticky") + .apply { + width = 140 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.TOP to "0px", + ) + }.applyParent(leftViewport) + ContainerNode(key = "perf-narrow-left-filler") + .apply { + width = 140 + height = 320 + }.applyParent(leftViewport) + + ContainerNode(key = "perf-narrow-right-sticky") + .apply { + width = 140 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.TOP to "0px", + ) + }.applyParent(rightViewport) + ContainerNode(key = "perf-narrow-right-filler") + .apply { + width = 140 + height = 320 + }.applyParent(rightViewport) val tree = DomTree(root) tree.render(ctx, 420, 220) @@ -284,11 +303,12 @@ class ScrollPerformanceCountersTests { fun `layout-dirty scroll invalidation still falls back to full rerender`() { val fixture = createScrollStickyFixture() val baseline = fixture.viewport.captureScrollSessionSnapshot() - val layoutDirtySnapshot = baseline.copy( - targetY = baseline.targetY + 48, - displayedY = baseline.displayedY + 48.0, - resolvedY = baseline.resolvedY + 48 - ) + val layoutDirtySnapshot = + baseline.copy( + targetY = baseline.targetY + 48, + displayedY = baseline.displayedY + 48.0, + resolvedY = baseline.resolvedY + 48, + ) fixture.viewport.restoreScrollSessionSnapshot(layoutDirtySnapshot) ScrollPerformanceCounters.resetForTests() @@ -304,29 +324,37 @@ class ScrollPerformanceCountersTests { private fun createScrollStickyFixture(): ScrollStickyFixture { val root = ContainerNode(key = "perf-root") - val viewport = ContainerNode(key = "perf-scroll-viewport").apply { - width = 180 - height = 100 - overflowY = Overflow.Auto - }.applyParent(root) - val topSpacer = ContainerNode(key = "perf-top-spacer").apply { - width = 160 - height = 32 - }.applyParent(viewport) - - val sticky = ContainerNode(key = "perf-sticky").apply { - width = 160 - height = 24 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.TOP to "0px" - ) - }.applyParent(viewport) - - ContainerNode(key = "perf-filler").apply { - width = 160 - height = 420 - }.applyParent(viewport) + val viewport = + ContainerNode(key = "perf-scroll-viewport") + .apply { + width = 180 + height = 100 + overflowY = Overflow.Auto + }.applyParent(root) + val topSpacer = + ContainerNode(key = "perf-top-spacer") + .apply { + width = 160 + height = 32 + }.applyParent(viewport) + + val sticky = + ContainerNode(key = "perf-sticky") + .apply { + width = 160 + height = 24 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.TOP to "0px", + ) + }.applyParent(viewport) + + ContainerNode(key = "perf-filler") + .apply { + width = 160 + height = 420 + }.applyParent(viewport) val tree = DomTree(root) tree.render(ctx, 320, 220) @@ -338,17 +366,16 @@ class ScrollPerformanceCountersTests { viewport = viewport, sticky = sticky, router = router, - stickyBaseTopY = topSpacer.height ?: 0 + stickyBaseTopY = topSpacer.height ?: 0, ) } - private fun styleDeclarations(vararg entries: Pair): StyleDeclarations { - return StyleDeclarations().apply { + private fun styleDeclarations(vararg entries: Pair): StyleDeclarations = + StyleDeclarations().apply { entries.forEach { (property, literal) -> set(property, StyleExpression.Literal(literal)) } } - } private data class ScrollStickyFixture( val tree: DomTree, @@ -356,6 +383,6 @@ class ScrollPerformanceCountersTests { val viewport: ContainerNode, val sticky: ContainerNode, val router: LayerDomInputRouter, - val stickyBaseTopY: Int + val stickyBaseTopY: Int, ) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollReactiveSmoothTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollReactiveSmoothTests.kt index bdca003..0a19847 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollReactiveSmoothTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollReactiveSmoothTests.kt @@ -1,16 +1,9 @@ package org.dreamfinity.dsgl.core.dom -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue -import kotlin.math.roundToInt import org.dreamfinity.dsgl.core.DomTree import org.dreamfinity.dsgl.core.dom.elements.ButtonNode import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Insets -import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.event.MouseClickEvent @@ -18,13 +11,22 @@ import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.Overflow import org.dreamfinity.dsgl.core.style.StyleEngine +import kotlin.math.roundToInt +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue class ScrollReactiveSmoothTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -71,23 +73,36 @@ class ScrollReactiveSmoothTests { assertTrue(fixture.router.handleMouseWheel(wheelX, wheelY, -240)) - var previousScroll = fixture.viewport.scrollContainerState().scrollY - var previousThumb = fixture.viewport.debugScrollbarVisualState().vertical?.thumbRect?.y - ?: error("Expected vertical scrollbar") + var previousScroll = + fixture.viewport + .scrollContainerState() + .scrollY + var previousThumb = + fixture.viewport + .debugScrollbarVisualState() + .vertical + ?.thumbRect + ?.y + ?: error("Expected vertical scrollbar") var previousButtonY = fixture.button.bounds.y repeat(14) { fixture.tree.paint(ctx) val state = fixture.viewport.scrollContainerState() - val thumbY = fixture.viewport.debugScrollbarVisualState().vertical?.thumbRect?.y - ?: error("Expected vertical scrollbar") + val thumbY = + fixture.viewport + .debugScrollbarVisualState() + .vertical + ?.thumbRect + ?.y + ?: error("Expected vertical scrollbar") assertTrue(state.scrollY >= previousScroll) assertTrue(thumbY >= previousThumb) assertTrue(fixture.button.bounds.y <= previousButtonY) val expectedButtonY = state.viewportRect.y - state.scrollY + fixture.button.margin.top assertTrue( kotlin.math.abs(expectedButtonY - fixture.button.bounds.y) <= 1, - "expectedButtonY=$expectedButtonY actualButtonY=${fixture.button.bounds.y} scrollY=${state.scrollY} viewportY=${state.viewportRect.y}" + "expectedButtonY=$expectedButtonY actualButtonY=${fixture.button.bounds.y} scrollY=${state.scrollY} viewportY=${state.viewportRect.y}", ) previousScroll = state.scrollY previousThumb = thumbY @@ -130,7 +145,10 @@ class ScrollReactiveSmoothTests { val history = ArrayList(160) repeat(160) { fixture.tree.paint(ctx) - history += fixture.viewport.debugScrollAnimationState().resolvedY + history += + fixture.viewport + .debugScrollAnimationState() + .resolvedY } val finalState = fixture.viewport.debugScrollAnimationState() assertEquals(finalState.targetY, finalState.resolvedY) @@ -141,22 +159,38 @@ class ScrollReactiveSmoothTests { repeat(10) { fixture.tree.paint(ctx) - assertEquals(finalState.targetY, fixture.viewport.debugScrollAnimationState().resolvedY) + assertEquals( + finalState.targetY, + fixture.viewport + .debugScrollAnimationState() + .resolvedY, + ) } } @Test fun `thumb drag updates content and thumb continuously in sync`() { val fixture = createFixture() - val visual = fixture.viewport.debugScrollbarVisualState().vertical ?: error("Expected vertical scrollbar") + val visual = + fixture.viewport + .debugScrollbarVisualState() + .vertical ?: error("Expected vertical scrollbar") val dragX = visual.thumbRect.x + visual.thumbRect.width / 2 val startY = visual.thumbRect.y + visual.thumbRect.height / 2 assertTrue(fixture.router.handleMouseDown(dragX, startY, MouseButton.LEFT)) - var previousScroll = fixture.viewport.scrollContainerState().scrollY - var previousThumbY = fixture.viewport.debugScrollbarVisualState().vertical?.thumbRect?.y - ?: error("Expected vertical scrollbar") + var previousScroll = + fixture.viewport + .scrollContainerState() + .scrollY + var previousThumbY = + fixture.viewport + .debugScrollbarVisualState() + .vertical + ?.thumbRect + ?.y + ?: error("Expected vertical scrollbar") var previousButtonY = fixture.button.bounds.y repeat(8) { step -> val nextY = startY + (step + 1) * 6 @@ -164,15 +198,20 @@ class ScrollReactiveSmoothTests { fixture.tree.paint(ctx) val state = fixture.viewport.scrollContainerState() val debug = fixture.viewport.debugScrollAnimationState() - val currentThumbY = fixture.viewport.debugScrollbarVisualState().vertical?.thumbRect?.y - ?: error("Expected vertical scrollbar") + val currentThumbY = + fixture.viewport + .debugScrollbarVisualState() + .vertical + ?.thumbRect + ?.y + ?: error("Expected vertical scrollbar") assertTrue(state.scrollY >= previousScroll) assertTrue(currentThumbY >= previousThumbY) assertTrue(fixture.button.bounds.y <= previousButtonY) val expectedButtonY = state.viewportRect.y - state.scrollY + fixture.button.margin.top assertTrue( kotlin.math.abs(expectedButtonY - fixture.button.bounds.y) <= 1, - "expectedButtonY=$expectedButtonY actualButtonY=${fixture.button.bounds.y} scrollY=${state.scrollY} viewportY=${state.viewportRect.y}" + "expectedButtonY=$expectedButtonY actualButtonY=${fixture.button.bounds.y} scrollY=${state.scrollY} viewportY=${state.viewportRect.y}", ) assertEquals(state.scrollY, debug.resolvedY) assertTrue(kotlin.math.abs(debug.displayedY - debug.resolvedY.toDouble()) <= 1.0) @@ -181,10 +220,14 @@ class ScrollReactiveSmoothTests { previousButtonY = fixture.button.bounds.y } } + @Test fun `drag keeps target and displayed state coherent`() { val fixture = createFixture() - val visual = fixture.viewport.debugScrollbarVisualState().vertical ?: error("Expected vertical scrollbar") + val visual = + fixture.viewport + .debugScrollbarVisualState() + .vertical ?: error("Expected vertical scrollbar") val dragX = visual.thumbRect.x + visual.thumbRect.width / 2 val startY = visual.thumbRect.y + visual.thumbRect.height / 2 @@ -201,7 +244,10 @@ class ScrollReactiveSmoothTests { @Test fun `fast thumb drag to max remains stable and coherent at boundary`() { val fixture = createFixture() - val visual = fixture.viewport.debugScrollbarVisualState().vertical ?: error("Expected vertical scrollbar") + val visual = + fixture.viewport + .debugScrollbarVisualState() + .vertical ?: error("Expected vertical scrollbar") val dragX = visual.thumbRect.x + visual.thumbRect.width / 2 val startY = visual.thumbRect.y + visual.thumbRect.height / 2 @@ -232,7 +278,10 @@ class ScrollReactiveSmoothTests { @Test fun `fast thumb drag to min remains stable and coherent at boundary`() { val fixture = createFixture() - val visual = fixture.viewport.debugScrollbarVisualState().vertical ?: error("Expected vertical scrollbar") + val visual = + fixture.viewport + .debugScrollbarVisualState() + .vertical ?: error("Expected vertical scrollbar") val dragX = visual.thumbRect.x + visual.thumbRect.width / 2 val startY = visual.thumbRect.y + visual.thumbRect.height / 2 @@ -255,7 +304,10 @@ class ScrollReactiveSmoothTests { @Test fun `drag session baseline stays frozen when live scrollbar geometry changes`() { val fixture = createFixture() - val initialVisual = fixture.viewport.debugScrollbarVisualState().vertical ?: error("Expected vertical scrollbar") + val initialVisual = + fixture.viewport + .debugScrollbarVisualState() + .vertical ?: error("Expected vertical scrollbar") val dragX = initialVisual.thumbRect.x + initialVisual.thumbRect.width / 2 val startY = initialVisual.thumbRect.y + initialVisual.thumbRect.height / 2 @@ -270,7 +322,10 @@ class ScrollReactiveSmoothTests { fixture.tree.render(ctx, 420, 260) fixture.tree.paint(ctx) - val liveAfterResize = fixture.viewport.debugScrollbarVisualState().vertical ?: error("Expected vertical scrollbar") + val liveAfterResize = + fixture.viewport + .debugScrollbarVisualState() + .vertical ?: error("Expected vertical scrollbar") assertTrue(liveAfterResize.maxScroll >= baseline.maxScroll) val baselineAfterResize = fixture.viewport.debugScrollbarDragSession() ?: error("Expected active drag session") @@ -290,7 +345,7 @@ class ScrollReactiveSmoothTests { val debug = fixture.viewport.debugScrollAnimationState() assertTrue( kotlin.math.abs(expectedScroll - state.scrollY) <= 1, - "expectedScroll=$expectedScroll actualScroll=${state.scrollY} moveY=$moveY baseline=$baseline stateMax=${state.maxScrollY}" + "expectedScroll=$expectedScroll actualScroll=${state.scrollY} moveY=$moveY baseline=$baseline stateMax=${state.maxScrollY}", ) assertEquals(state.scrollY, debug.resolvedY) assertEquals(debug.displayedY, debug.resolvedY.toDouble()) @@ -302,19 +357,28 @@ class ScrollReactiveSmoothTests { @Test fun `drag pointer capture continues when pointer leaves container bounds`() { val fixture = createFixture() - val visual = fixture.viewport.debugScrollbarVisualState().vertical ?: error("Expected vertical scrollbar") + val visual = + fixture.viewport + .debugScrollbarVisualState() + .vertical ?: error("Expected vertical scrollbar") val dragX = visual.thumbRect.x + visual.thumbRect.width / 2 val startY = visual.thumbRect.y + visual.thumbRect.height / 2 assertTrue(fixture.router.handleMouseDown(dragX, startY, MouseButton.LEFT)) - val before = fixture.viewport.scrollContainerState().scrollY + val before = + fixture.viewport + .scrollContainerState() + .scrollY val outsideX = fixture.viewport.bounds.x + fixture.viewport.bounds.width + 1200 val outsideY = fixture.viewport.bounds.y + fixture.viewport.bounds.height + 1200 assertTrue(fixture.router.handleMouseMove(outsideX, outsideY)) fixture.tree.paint(ctx) - val after = fixture.viewport.scrollContainerState().scrollY + val after = + fixture.viewport + .scrollContainerState() + .scrollY assertTrue(after >= before) assertTrue(fixture.router.handleMouseUp(outsideX, outsideY, MouseButton.LEFT)) } @@ -322,7 +386,10 @@ class ScrollReactiveSmoothTests { @Test fun `drag release is stable without snap back`() { val fixture = createFixture() - val visual = fixture.viewport.debugScrollbarVisualState().vertical ?: error("Expected vertical scrollbar") + val visual = + fixture.viewport + .debugScrollbarVisualState() + .vertical ?: error("Expected vertical scrollbar") val dragX = visual.thumbRect.x + visual.thumbRect.width / 2 val startY = visual.thumbRect.y + visual.thumbRect.height / 2 @@ -343,11 +410,13 @@ class ScrollReactiveSmoothTests { } } - @Test fun `wheel smoothness remains after thumb drag interaction`() { val fixture = createFixture() - val visual = fixture.viewport.debugScrollbarVisualState().vertical ?: error("Expected vertical scrollbar") + val visual = + fixture.viewport + .debugScrollbarVisualState() + .vertical ?: error("Expected vertical scrollbar") val dragX = visual.thumbRect.x + visual.thumbRect.width / 2 val startY = visual.thumbRect.y + visual.thumbRect.height / 2 @@ -371,6 +440,7 @@ class ScrollReactiveSmoothTests { val settled = fixture.viewport.debugScrollAnimationState() assertEquals(settled.targetY, settled.resolvedY) } + @Test fun `scroll updates do not depend on unrelated viewport dimension changes`() { val fixture = createFixture() @@ -387,31 +457,35 @@ class ScrollReactiveSmoothTests { assertEquals(afterScrollPaint, fixture.button.bounds.y) } - private fun dispatchClick(tree: DomTree, x: Int, y: Int): Boolean { - return tree.dispatchClick(MouseClickEvent(x, y, MouseButton.LEFT)) - } + private fun dispatchClick(tree: DomTree, x: Int, y: Int): Boolean = tree.dispatchClick(MouseClickEvent(x, y, MouseButton.LEFT)) private fun createFixture(): Fixture { val clickCount = IntBox() val root = ContainerNode(key = "root") - val viewport = ContainerNode(key = "viewport").apply { - width = 160 - height = 90 - overflowX = Overflow.Hidden - overflowY = Overflow.Auto - }.applyParent(root) - val button = ButtonNode("row", key = "row-button").apply { - width = 120 - height = 24 - margin = Insets(top = 40, right = 0, bottom = 0, left = 0) - onClick { - clickCount.value += 1 - } - }.applyParent(viewport) - val filler = ContainerNode(key = "filler").apply { - width = 120 - height = 320 - }.applyParent(viewport) + val viewport = + ContainerNode(key = "viewport") + .apply { + width = 160 + height = 90 + overflowX = Overflow.Hidden + overflowY = Overflow.Auto + }.applyParent(root) + val button = + ButtonNode("row", key = "row-button") + .apply { + width = 120 + height = 24 + margin = Insets(top = 40, right = 0, bottom = 0, left = 0) + onClick { + clickCount.value += 1 + } + }.applyParent(viewport) + val filler = + ContainerNode(key = "filler") + .apply { + width = 120 + height = 320 + }.applyParent(viewport) val tree = DomTree(root) tree.render(ctx, 420, 260) @@ -427,16 +501,19 @@ class ScrollReactiveSmoothTests { val button: ButtonNode, val filler: ContainerNode, val router: LayerDomInputRouter, - val clickCount: IntBox + val clickCount: IntBox, ) private fun expectedScrollFromSession(session: ScrollbarDragSessionDebugState, pointerAxisPx: Int): Int { if (session.maxScroll <= 0 || session.maxThumbTravelPx <= 0) return 0 - val desiredThumbStart = (pointerAxisPx - session.trackStartPx - session.grabOffsetPx) - .coerceIn(0, session.maxThumbTravelPx) + val desiredThumbStart = + (pointerAxisPx - session.trackStartPx - session.grabOffsetPx) + .coerceIn(0, session.maxThumbTravelPx) val ratio = desiredThumbStart.toDouble() / session.maxThumbTravelPx.toDouble() return (ratio * session.maxScroll.toDouble()).roundToInt().coerceIn(0, session.maxScroll) } - private data class IntBox(var value: Int = 0) + private data class IntBox( + var value: Int = 0, + ) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollbarRenderingInteractionTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollbarRenderingInteractionTests.kt index d211660..32f3c18 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollbarRenderingInteractionTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollbarRenderingInteractionTests.kt @@ -1,11 +1,5 @@ package org.dreamfinity.dsgl.core.dom -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.dom.elements.ButtonNode import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Rect @@ -16,15 +10,24 @@ import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.Overflow import org.dreamfinity.dsgl.core.style.StyleEngine +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue class ScrollbarRenderingInteractionTests { private val trackColor = 0x55303030 private val thumbColor = 0xAA9AA5B1.toInt() - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -36,59 +39,74 @@ class ScrollbarRenderingInteractionTests { @Test fun `generic scrollbar rendering emits track and thumb when present`() { - val (_, viewport, _, _) = createFixture( - overflowX = Overflow.Visible, - overflowY = Overflow.Scroll, - viewportWidth = 120, - viewportHeight = 70, - contentWidth = 90, - contentHeight = 40 - ) + val (_, viewport, _, _) = + createFixture( + overflowX = Overflow.Visible, + overflowY = Overflow.Scroll, + viewportWidth = 120, + viewportHeight = 70, + contentWidth = 90, + contentHeight = 40, + ) val commands = selfCommands(viewport) val vertical = viewport.debugScrollbarVisualState().vertical assertNotNull(vertical) - assertTrue(commands.any { command -> - command is RenderCommand.DrawRect && - command.color == trackColor && - command.x == vertical.trackRect.x && - command.y == vertical.trackRect.y && - command.width == vertical.trackRect.width && - command.height == vertical.trackRect.height - }) - assertTrue(commands.any { command -> - command is RenderCommand.DrawRect && - command.color == thumbColor && - command.x == vertical.thumbRect.x && - command.y == vertical.thumbRect.y && - command.width == vertical.thumbRect.width && - command.height == vertical.thumbRect.height - }) + assertTrue( + commands.any { command -> + command is RenderCommand.DrawRect && + command.color == trackColor && + command.x == vertical.trackRect.x && + command.y == vertical.trackRect.y && + command.width == vertical.trackRect.width && + command.height == vertical.trackRect.height + }, + ) + assertTrue( + commands.any { command -> + command is RenderCommand.DrawRect && + command.color == thumbColor && + command.x == vertical.thumbRect.x && + command.y == vertical.thumbRect.y && + command.width == vertical.thumbRect.width && + command.height == vertical.thumbRect.height + }, + ) } @Test fun `thumb geometry stays synchronized with scroll offsets`() { - val (_, viewport, _, _) = createFixture( - overflowX = Overflow.Visible, - overflowY = Overflow.Auto, - viewportWidth = 120, - viewportHeight = 70, - contentWidth = 90, - contentHeight = 260 - ) + val (_, viewport, _, _) = + createFixture( + overflowX = Overflow.Visible, + overflowY = Overflow.Auto, + viewportWidth = 120, + viewportHeight = 70, + contentWidth = 90, + contentHeight = 260, + ) val initialState = viewport.scrollContainerState() - val initialThumb = viewport.debugScrollbarVisualState().vertical?.thumbRect - ?: error("Expected vertical scrollbar thumb") + val initialThumb = + viewport + .debugScrollbarVisualState() + .vertical + ?.thumbRect + ?: error("Expected vertical scrollbar thumb") viewport.setScrollOffsets(0, initialState.maxScrollY / 2) val middleState = viewport.scrollContainerState() - val middleThumb = viewport.debugScrollbarVisualState().vertical?.thumbRect - ?: error("Expected vertical scrollbar thumb") + val middleThumb = + viewport + .debugScrollbarVisualState() + .vertical + ?.thumbRect + ?: error("Expected vertical scrollbar thumb") viewport.setScrollOffsets(0, middleState.maxScrollY) - val endVisual = viewport.debugScrollbarVisualState().vertical - ?: error("Expected vertical scrollbar thumb") + val endVisual = + viewport.debugScrollbarVisualState().vertical + ?: error("Expected vertical scrollbar thumb") val endThumb = endVisual.thumbRect assertTrue(initialThumb.height >= 8) @@ -102,14 +120,15 @@ class ScrollbarRenderingInteractionTests { @Test fun `wheel scrolling updates generic container scroll state`() { - val (root, viewport, wheelTarget, router) = createFixture( - overflowX = Overflow.Visible, - overflowY = Overflow.Auto, - viewportWidth = 120, - viewportHeight = 70, - contentWidth = 90, - contentHeight = 260 - ) + val (root, viewport, wheelTarget, router) = + createFixture( + overflowX = Overflow.Visible, + overflowY = Overflow.Auto, + viewportWidth = 120, + viewportHeight = 70, + contentWidth = 90, + contentHeight = 260, + ) val wheelX = wheelTarget.bounds.x + 2 val wheelY = wheelTarget.bounds.y + 2 @@ -125,17 +144,19 @@ class ScrollbarRenderingInteractionTests { @Test fun `thumb drag updates scroll offset through generic pointer capture`() { - val (_, viewport, _, router) = createFixture( - overflowX = Overflow.Visible, - overflowY = Overflow.Auto, - viewportWidth = 120, - viewportHeight = 70, - contentWidth = 90, - contentHeight = 320 - ) + val (_, viewport, _, router) = + createFixture( + overflowX = Overflow.Visible, + overflowY = Overflow.Auto, + viewportWidth = 120, + viewportHeight = 70, + contentWidth = 90, + contentHeight = 320, + ) - val visual = viewport.debugScrollbarVisualState().vertical - ?: error("Expected vertical scrollbar visual state") + val visual = + viewport.debugScrollbarVisualState().vertical + ?: error("Expected vertical scrollbar visual state") val startX = visual.thumbRect.x + (visual.thumbRect.width / 2).coerceAtLeast(1) val startY = visual.thumbRect.y + (visual.thumbRect.height / 2).coerceAtLeast(1) val dragY = (visual.trackRect.y + visual.trackRect.height - 2).coerceAtLeast(startY + 1) @@ -151,65 +172,73 @@ class ScrollbarRenderingInteractionTests { @Test fun `overflow modes control generic scrollbar visuals`() { - val modes = listOf( - Overflow.Visible to false, - Overflow.Hidden to false, - Overflow.Scroll to true, - Overflow.Auto to true - ) - modes.forEach { (mode, expectedVisible) -> - val (_, viewport, _, _) = createFixture( - overflowX = Overflow.Visible, - overflowY = mode, - viewportWidth = 120, - viewportHeight = 70, - contentWidth = 90, - contentHeight = 240 + val modes = + listOf( + Overflow.Visible to false, + Overflow.Hidden to false, + Overflow.Scroll to true, + Overflow.Auto to true, ) + modes.forEach { (mode, expectedVisible) -> + val (_, viewport, _, _) = + createFixture( + overflowX = Overflow.Visible, + overflowY = mode, + viewportWidth = 120, + viewportHeight = 70, + contentWidth = 90, + contentHeight = 240, + ) val commands = selfCommands(viewport) val vertical = viewport.debugScrollbarVisualState().vertical assertEquals(expectedVisible, vertical != null, "Mode=$mode") - val hasTrack = commands.any { command -> - command is RenderCommand.DrawRect && command.color == trackColor - } + val hasTrack = + commands.any { command -> + command is RenderCommand.DrawRect && command.color == trackColor + } assertEquals(expectedVisible, hasTrack, "Mode=$mode") } } @Test fun `horizontal auto scrollbar appears when content width exceeds viewport`() { - val (_, viewport, _, _) = createFixture( - overflowX = Overflow.Auto, - overflowY = Overflow.Hidden, - viewportWidth = 794, - viewportHeight = 634, - contentWidth = 826, - contentHeight = 620 - ) + val (_, viewport, _, _) = + createFixture( + overflowX = Overflow.Auto, + overflowY = Overflow.Hidden, + viewportWidth = 794, + viewportHeight = 634, + contentWidth = 826, + contentHeight = 620, + ) val commands = selfCommands(viewport) val horizontal = viewport.debugScrollbarVisualState().horizontal assertNotNull(horizontal) - assertTrue(commands.any { command -> - command is RenderCommand.DrawRect && - command.color == trackColor && - command.x == horizontal.trackRect.x && - command.y == horizontal.trackRect.y && - command.width == horizontal.trackRect.width && - command.height == horizontal.trackRect.height - }) + assertTrue( + commands.any { command -> + command is RenderCommand.DrawRect && + command.color == trackColor && + command.x == horizontal.trackRect.x && + command.y == horizontal.trackRect.y && + command.width == horizontal.trackRect.width && + command.height == horizontal.trackRect.height + }, + ) } + @Test fun `normal wheel scrolls only vertical axis`() { - val (_, viewport, wheelTarget, router) = createFixture( - overflowX = Overflow.Auto, - overflowY = Overflow.Auto, - viewportWidth = 120, - viewportHeight = 70, - contentWidth = 280, - contentHeight = 260 - ) + val (_, viewport, wheelTarget, router) = + createFixture( + overflowX = Overflow.Auto, + overflowY = Overflow.Auto, + viewportWidth = 120, + viewportHeight = 70, + contentWidth = 280, + contentHeight = 260, + ) KeyModifiers.sync(shift = false, control = false, meta = false) val wheelX = wheelTarget.bounds.x + 2 @@ -224,14 +253,15 @@ class ScrollbarRenderingInteractionTests { @Test fun `shift plus wheel scrolls only horizontal axis`() { - val (_, viewport, wheelTarget, router) = createFixture( - overflowX = Overflow.Auto, - overflowY = Overflow.Auto, - viewportWidth = 120, - viewportHeight = 70, - contentWidth = 280, - contentHeight = 260 - ) + val (_, viewport, wheelTarget, router) = + createFixture( + overflowX = Overflow.Auto, + overflowY = Overflow.Auto, + viewportWidth = 120, + viewportHeight = 70, + contentWidth = 280, + contentHeight = 260, + ) KeyModifiers.sync(shift = true, control = false, meta = false) val wheelX = wheelTarget.bounds.x + 2 @@ -246,30 +276,39 @@ class ScrollbarRenderingInteractionTests { @Test fun `wheel chains to ancestor when intended axis cannot scroll on hovered container`() { - val root = ContainerNode(key = "root").apply { - bounds = Rect(0, 0, 640, 360) - } - val parent = ContainerNode(key = "parent").apply { - bounds = Rect(20, 20, 260, 120) - overflowX = Overflow.Hidden - overflowY = Overflow.Auto - }.applyParent(root) - ContainerNode(key = "parent-content").apply { - bounds = Rect(parent.bounds.x, parent.bounds.y, 250, 420) - }.applyParent(parent) - - val child = ContainerNode(key = "child").apply { - bounds = Rect(parent.bounds.x + 8, parent.bounds.y + 8, 140, 60) - overflowX = Overflow.Auto - overflowY = Overflow.Hidden - }.applyParent(parent) - ContainerNode(key = "child-content").apply { - bounds = Rect(child.bounds.x, child.bounds.y, 340, 50) - }.applyParent(child) - val childButton = ButtonNode("child", key = "child-button").apply { - bounds = Rect(child.bounds.x + 6, child.bounds.y + 6, 80, 20) - onClick { } - }.applyParent(child) + val root = + ContainerNode(key = "root").apply { + bounds = Rect(0, 0, 640, 360) + } + val parent = + ContainerNode(key = "parent") + .apply { + bounds = Rect(20, 20, 260, 120) + overflowX = Overflow.Hidden + overflowY = Overflow.Auto + }.applyParent(root) + ContainerNode(key = "parent-content") + .apply { + bounds = Rect(parent.bounds.x, parent.bounds.y, 250, 420) + }.applyParent(parent) + + val child = + ContainerNode(key = "child") + .apply { + bounds = Rect(parent.bounds.x + 8, parent.bounds.y + 8, 140, 60) + overflowX = Overflow.Auto + overflowY = Overflow.Hidden + }.applyParent(parent) + ContainerNode(key = "child-content") + .apply { + bounds = Rect(child.bounds.x, child.bounds.y, 340, 50) + }.applyParent(child) + val childButton = + ButtonNode("child", key = "child-button") + .apply { + bounds = Rect(child.bounds.x + 6, child.bounds.y + 6, 80, 20) + onClick { } + }.applyParent(child) val router = LayerDomInputRouter { root } KeyModifiers.sync(shift = false, control = false, meta = false) @@ -290,48 +329,58 @@ class ScrollbarRenderingInteractionTests { @Test fun `wheel scroll keeps thumb position synchronized`() { - val (_, viewport, wheelTarget, router) = createFixture( - overflowX = Overflow.Visible, - overflowY = Overflow.Auto, - viewportWidth = 120, - viewportHeight = 70, - contentWidth = 90, - contentHeight = 320 - ) + val (_, viewport, wheelTarget, router) = + createFixture( + overflowX = Overflow.Visible, + overflowY = Overflow.Auto, + viewportWidth = 120, + viewportHeight = 70, + contentWidth = 90, + contentHeight = 320, + ) KeyModifiers.sync(shift = false, control = false, meta = false) - val beforeVisual = viewport.debugScrollbarVisualState().vertical ?: error("Expected vertical scrollbar visual state") + val beforeVisual = + viewport.debugScrollbarVisualState().vertical ?: error("Expected vertical scrollbar visual state") val wheelX = wheelTarget.bounds.x + 2 val wheelY = wheelTarget.bounds.y + 2 assertTrue(router.handleMouseWheel(wheelX, wheelY, -120)) advanceScrollAnimation(viewport) - val afterVisual = viewport.debugScrollbarVisualState().vertical ?: error("Expected vertical scrollbar visual state") + val afterVisual = + viewport.debugScrollbarVisualState().vertical ?: error("Expected vertical scrollbar visual state") assertTrue(afterVisual.thumbRect.y > beforeVisual.thumbRect.y) assertFalse(afterVisual.thumbRect == beforeVisual.thumbRect) } @Test fun `wheel chains to ancestor when inner container is at vertical limit`() { - val root = ContainerNode(key = "root").apply { - bounds = Rect(0, 0, 640, 360) - } - val outer = ContainerNode(key = "outer").apply { - bounds = Rect(20, 20, 260, 150) - overflowX = Overflow.Hidden - overflowY = Overflow.Auto - }.applyParent(root) - ContainerNode(key = "outer-content").apply { - bounds = Rect(outer.bounds.x, outer.bounds.y, 240, 460) - }.applyParent(outer) - val inner = ContainerNode(key = "inner").apply { - bounds = Rect(36, 36, 140, 72) - overflowX = Overflow.Hidden - overflowY = Overflow.Auto - }.applyParent(outer) - ContainerNode(key = "inner-content").apply { - bounds = Rect(inner.bounds.x, inner.bounds.y, 120, 320) - }.applyParent(inner) + val root = + ContainerNode(key = "root").apply { + bounds = Rect(0, 0, 640, 360) + } + val outer = + ContainerNode(key = "outer") + .apply { + bounds = Rect(20, 20, 260, 150) + overflowX = Overflow.Hidden + overflowY = Overflow.Auto + }.applyParent(root) + ContainerNode(key = "outer-content") + .apply { + bounds = Rect(outer.bounds.x, outer.bounds.y, 240, 460) + }.applyParent(outer) + val inner = + ContainerNode(key = "inner") + .apply { + bounds = Rect(36, 36, 140, 72) + overflowX = Overflow.Hidden + overflowY = Overflow.Auto + }.applyParent(outer) + ContainerNode(key = "inner-content") + .apply { + bounds = Rect(inner.bounds.x, inner.bounds.y, 120, 320) + }.applyParent(inner) val router = LayerDomInputRouter { root } KeyModifiers.sync(shift = false, control = false, meta = false) @@ -358,29 +407,40 @@ class ScrollbarRenderingInteractionTests { @Test fun `dragging nested inner thumb does not affect outer scroll state`() { - val root = ContainerNode(key = "root").apply { - bounds = Rect(0, 0, 680, 420) - } - val outer = ContainerNode(key = "outer").apply { - bounds = Rect(18, 18, 280, 180) - overflowX = Overflow.Hidden - overflowY = Overflow.Scroll - }.applyParent(root) - ContainerNode(key = "outer-content").apply { - bounds = Rect(outer.bounds.x, outer.bounds.y, 250, 520) - }.applyParent(outer) - - val inner = ContainerNode(key = "inner").apply { - bounds = Rect(40, 42, 160, 90) - overflowX = Overflow.Hidden - overflowY = Overflow.Scroll - }.applyParent(outer) - ContainerNode(key = "inner-content").apply { - bounds = Rect(inner.bounds.x, inner.bounds.y, 130, 360) - }.applyParent(inner) + val root = + ContainerNode(key = "root").apply { + bounds = Rect(0, 0, 680, 420) + } + val outer = + ContainerNode(key = "outer") + .apply { + bounds = Rect(18, 18, 280, 180) + overflowX = Overflow.Hidden + overflowY = Overflow.Scroll + }.applyParent(root) + ContainerNode(key = "outer-content") + .apply { + bounds = Rect(outer.bounds.x, outer.bounds.y, 250, 520) + }.applyParent(outer) + + val inner = + ContainerNode(key = "inner") + .apply { + bounds = Rect(40, 42, 160, 90) + overflowX = Overflow.Hidden + overflowY = Overflow.Scroll + }.applyParent(outer) + ContainerNode(key = "inner-content") + .apply { + bounds = Rect(inner.bounds.x, inner.bounds.y, 130, 360) + }.applyParent(inner) val router = LayerDomInputRouter { root } - val innerThumb = inner.debugScrollbarVisualState().vertical?.thumbRect ?: error("inner thumb missing") + val innerThumb = + inner + .debugScrollbarVisualState() + .vertical + ?.thumbRect ?: error("inner thumb missing") val dragX = innerThumb.x + innerThumb.width / 2 val startY = innerThumb.y + innerThumb.height / 2 val endY = startY + 32 @@ -402,6 +462,7 @@ class ScrollbarRenderingInteractionTests { assertEquals(innerAfter.scrollY, innerDebug.resolvedY) assertTrue(kotlin.math.abs(innerDebug.displayedY - innerDebug.resolvedY.toDouble()) <= 1.0) } + private fun advanceScrollAnimation(node: DOMNode, frames: Int = 6) { repeat(frames) { node.advanceScrollAnimationsRecursively(1.0 / 60.0) @@ -423,23 +484,28 @@ class ScrollbarRenderingInteractionTests { viewportWidth: Int, viewportHeight: Int, contentWidth: Int, - contentHeight: Int + contentHeight: Int, ): Quad { - val root = ContainerNode(key = "root").apply { - bounds = Rect(0, 0, 2000, 1200) - } - val viewport = ContainerNode(key = "viewport").apply { - bounds = Rect(40, 30, viewportWidth, viewportHeight) - this.overflowX = overflowX - this.overflowY = overflowY - }.applyParent(root) - ContainerNode(key = "content").apply { - bounds = Rect(viewport.bounds.x, viewport.bounds.y, contentWidth, contentHeight) - }.applyParent(viewport) - val wheelTarget = ButtonNode("wheel", key = "wheel-target").apply { - bounds = Rect(viewport.bounds.x + 6, viewport.bounds.y + 6, 64, 20) - onClick { } - } + val root = + ContainerNode(key = "root").apply { + bounds = Rect(0, 0, 2000, 1200) + } + val viewport = + ContainerNode(key = "viewport") + .apply { + bounds = Rect(40, 30, viewportWidth, viewportHeight) + this.overflowX = overflowX + this.overflowY = overflowY + }.applyParent(root) + ContainerNode(key = "content") + .apply { + bounds = Rect(viewport.bounds.x, viewport.bounds.y, contentWidth, contentHeight) + }.applyParent(viewport) + val wheelTarget = + ButtonNode("wheel", key = "wheel-target").apply { + bounds = Rect(viewport.bounds.x + 6, viewport.bounds.y + 6, 64, 20) + onClick { } + } wheelTarget.applyParent(viewport) val router = LayerDomInputRouter { root } @@ -450,6 +516,6 @@ class ScrollbarRenderingInteractionTests { val root: ContainerNode, val viewport: ContainerNode, val wheelTarget: ButtonNode, - val router: LayerDomInputRouter + val router: LayerDomInputRouter, ) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerScopeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerScopeTests.kt index 575f231..beae55e 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerScopeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerScopeTests.kt @@ -17,11 +17,14 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue class SelectNodeOwnerScopeTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -33,18 +36,20 @@ class SelectNodeOwnerScopeTests { val root = ContainerNode(key = "root") root.bounds = Rect(0, 0, 300, 200) val ownerKey = "system-select-owner" - val select = SelectNode( - model = selectModel(id = "system.select.model") { - option("a", "Alpha") - option("b", "Beta") - }, - ownerScope = OverlayOwnerScope.System, - key = ownerKey - ).apply { - width = 120 - height = 20 - bounds = Rect(20, 20, 120, 20) - } + val select = + SelectNode( + model = + selectModel(id = "system.select.model") { + option("a", "Alpha") + option("b", "Beta") + }, + ownerScope = OverlayOwnerScope.System, + key = ownerKey, + ).apply { + width = 120 + height = 20 + bounds = Rect(20, 20, 120, 20) + } select.applyParent(root) val tree = DomTree(root) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectPopupAnchoringStickyTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectPopupAnchoringStickyTests.kt index e262a46..71066e8 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectPopupAnchoringStickyTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectPopupAnchoringStickyTests.kt @@ -1,11 +1,5 @@ package org.dreamfinity.dsgl.core.dom -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.DomTree import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.elements.SelectNode @@ -22,16 +16,24 @@ import org.dreamfinity.dsgl.core.style.StyleDeclarations import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleExpression import org.dreamfinity.dsgl.core.style.StyleProperty +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue class SelectPopupAnchoringStickyTests { private val viewportWidth = 420 private val viewportHeight = 260 - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -110,71 +112,84 @@ class SelectPopupAnchoringStickyTests { measureContext = ctx, viewportWidth = viewportWidth, viewportHeight = viewportHeight, - viewportScale = 1f + viewportScale = 1f, ) - val anchor = SelectRuntime.engine.debugAnchorRect(fixture.ownerKey) - ?: error("Expected select anchor rect for owner=${fixture.ownerKey}") - val panel = SelectRuntime.engine.debugPanelRect(fixture.ownerKey) - ?: error("Expected select panel rect for owner=${fixture.ownerKey}") + val anchor = + SelectRuntime.engine.debugAnchorRect(fixture.ownerKey) + ?: error("Expected select anchor rect for owner=${fixture.ownerKey}") + val panel = + SelectRuntime.engine.debugPanelRect(fixture.ownerKey) + ?: error("Expected select panel rect for owner=${fixture.ownerKey}") return PopupGeometry(anchor = anchor, panel = panel) } private fun createNonStickyFixture(): Fixture { val root = ContainerNode(key = "select-anchor-root") - val host = ContainerNode(key = "select-anchor-host").apply { - width = 220 - height = 80 - padding = Insets(0, 20, 0, 0) - }.applyParent(root) + val host = + ContainerNode(key = "select-anchor-host") + .apply { + width = 220 + height = 80 + padding = Insets(0, 20, 0, 0) + }.applyParent(root) val ownerKey = "select-anchor-target" - val select = SelectNode( - model = demoModel(), - key = ownerKey - ).apply { - width = 120 - height = 18 - }.applyParent(host) + val select = + SelectNode( + model = demoModel(), + key = ownerKey, + ).apply { + width = 120 + height = 18 + }.applyParent(host) return buildFixture(root, host, select, ownerKey) } private fun createStickyFixture(): Fixture { val root = ContainerNode(key = "select-anchor-sticky-root") - val scroller = ContainerNode(key = "select-anchor-sticky-scroller").apply { - width = 220 - height = 90 - overflowY = Overflow.Auto - }.applyParent(root) - - ContainerNode(key = "select-anchor-top-spacer").apply { - width = 220 - height = 22 - }.applyParent(scroller) - - val sticky = ContainerNode(key = "select-anchor-sticky-row").apply { - width = 220 - height = 24 - padding = Insets(0, 20, 0, 0) - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "sticky", - StyleProperty.TOP to "0px" - ) - }.applyParent(scroller) + val scroller = + ContainerNode(key = "select-anchor-sticky-scroller") + .apply { + width = 220 + height = 90 + overflowY = Overflow.Auto + }.applyParent(root) + + ContainerNode(key = "select-anchor-top-spacer") + .apply { + width = 220 + height = 22 + }.applyParent(scroller) + + val sticky = + ContainerNode(key = "select-anchor-sticky-row") + .apply { + width = 220 + height = 24 + padding = Insets(0, 20, 0, 0) + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "sticky", + StyleProperty.TOP to "0px", + ) + }.applyParent(scroller) val ownerKey = "select-anchor-sticky-target" - val select = SelectNode( - model = demoModel(), - key = ownerKey - ).apply { - width = 120 - height = 18 - }.applyParent(sticky) - - ContainerNode(key = "select-anchor-filler").apply { - width = 220 - height = 300 - }.applyParent(scroller) + val select = + SelectNode( + model = demoModel(), + key = ownerKey, + ).apply { + width = 120 + height = 18 + }.applyParent(sticky) + + ContainerNode(key = "select-anchor-filler") + .apply { + width = 220 + height = 300 + }.applyParent(scroller) return buildFixture(root, scroller, select, ownerKey) } @@ -183,7 +198,7 @@ class SelectPopupAnchoringStickyTests { root: ContainerNode, scroller: ContainerNode, select: SelectNode, - ownerKey: String + ownerKey: String, ): Fixture { val tree = DomTree(root) tree.render(ctx, viewportWidth, viewportHeight) @@ -194,39 +209,39 @@ class SelectPopupAnchoringStickyTests { scroller = scroller, select = select, ownerKey = ownerKey, - router = router + router = router, ) } - private fun demoModel() = selectModel(id = "select.anchor.model") { - option("a", "Alpha") - option("b", "Beta") - option("c", "Gamma") - } + private fun demoModel() = + selectModel(id = "select.anchor.model") { + option("a", "Alpha") + option("b", "Beta") + option("c", "Gamma") + } private fun visibleRect(node: SelectNode): Rect { val geometry = UsedInteractionGeometryResolver.resolveNodeGeometry(node) return geometry.visibleBorderRect ?: geometry.usedBorderRect } - private fun styleDeclarations(vararg entries: Pair): StyleDeclarations { - return StyleDeclarations().apply { + private fun styleDeclarations(vararg entries: Pair): StyleDeclarations = + StyleDeclarations().apply { entries.forEach { (property, literal) -> set(property, StyleExpression.Literal(literal)) } } - } private data class Fixture( val tree: DomTree, val scroller: ContainerNode, val select: SelectNode, val ownerKey: String, - val router: LayerDomInputRouter + val router: LayerDomInputRouter, ) private data class PopupGeometry( val anchor: Rect, - val panel: Rect + val panel: Rect, ) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/StackingContextChildContextBehaviorTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/StackingContextChildContextBehaviorTests.kt index dab9cf9..871dbb7 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/StackingContextChildContextBehaviorTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/StackingContextChildContextBehaviorTests.kt @@ -23,11 +23,14 @@ import kotlin.test.assertSame import kotlin.test.assertTrue class StackingContextChildContextBehaviorTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -40,22 +43,30 @@ class StackingContextChildContextBehaviorTests { val root = ContainerNode(key = "trigger-root") val owner = ContainerNode(key = "trigger-owner").applyParent(root) - val staticHigh = ContainerNode(key = "static-high").apply { - position = PositionMode.Static - zIndex = 5 - }.applyParent(owner) - val positionedDefaultZ = ContainerNode(key = "positioned-default").apply { - position = PositionMode.Relative - zIndex = 0 - }.applyParent(owner) - val positionedExplicitZ = ContainerNode(key = "positioned-explicit").apply { - position = PositionMode.Relative - zIndex = 3 - }.applyParent(owner) - val fixedExplicitZ = ContainerNode(key = "fixed-explicit").apply { - position = PositionMode.Fixed - zIndex = 4 - }.applyParent(owner) + val staticHigh = + ContainerNode(key = "static-high") + .apply { + position = PositionMode.Static + zIndex = 5 + }.applyParent(owner) + val positionedDefaultZ = + ContainerNode(key = "positioned-default") + .apply { + position = PositionMode.Relative + zIndex = 0 + }.applyParent(owner) + val positionedExplicitZ = + ContainerNode(key = "positioned-explicit") + .apply { + position = PositionMode.Relative + zIndex = 3 + }.applyParent(owner) + val fixedExplicitZ = + ContainerNode(key = "fixed-explicit") + .apply { + position = PositionMode.Fixed + zIndex = 4 + }.applyParent(owner) assertFalse(PositionedLayoutModel.matchesChildContextTrigger(staticHigh)) assertFalse(PositionedLayoutModel.matchesChildContextTrigger(positionedDefaultZ)) @@ -80,30 +91,36 @@ class StackingContextChildContextBehaviorTests { @Test fun `child context participation is atomic in parent ordering`() { val root = ContainerNode(key = "atomic-root", stackLayout = true) - val lowerContext = ContainerNode(key = "atomic-lower").apply { - position = PositionMode.Relative - zIndex = 1 - }.applyParent(root) - val higherContext = ContainerNode(key = "atomic-higher").apply { - position = PositionMode.Relative - zIndex = 2 - }.applyParent(root) + val lowerContext = + ContainerNode(key = "atomic-lower") + .apply { + position = PositionMode.Relative + zIndex = 1 + }.applyParent(root) + val higherContext = + ContainerNode(key = "atomic-higher") + .apply { + position = PositionMode.Relative + zIndex = 2 + }.applyParent(root) var lowerDescendantClicks = 0 var higherDescendantClicks = 0 - ButtonNode("lower-descendant", key = "atomic-lower-desc", backgroundColor = 0x00_11_44_88).apply { - width = 72 - height = 24 - position = PositionMode.Relative - zIndex = 9_999 - onClick { lowerDescendantClicks += 1 } - }.applyParent(lowerContext) - ButtonNode("higher-descendant", key = "atomic-higher-desc", backgroundColor = 0x00_88_22_44).apply { - width = 72 - height = 24 - onClick { higherDescendantClicks += 1 } - }.applyParent(higherContext) + ButtonNode("lower-descendant", key = "atomic-lower-desc", backgroundColor = 0x00_11_44_88) + .apply { + width = 72 + height = 24 + position = PositionMode.Relative + zIndex = 9_999 + onClick { lowerDescendantClicks += 1 } + }.applyParent(lowerContext) + ButtonNode("higher-descendant", key = "atomic-higher-desc", backgroundColor = 0x00_88_22_44) + .apply { + width = 72 + height = 24 + onClick { higherDescendantClicks += 1 } + }.applyParent(higherContext) renderTree(root, width = 240, height = 140) @@ -116,28 +133,34 @@ class StackingContextChildContextBehaviorTests { @Test fun `local ordering inside child context remains by local z-index and dom tie-break`() { val root = ContainerNode(key = "local-root", stackLayout = true) - val contextOwner = ContainerNode(key = "local-owner").apply { - position = PositionMode.Relative - zIndex = 2 - stackLayout = true - }.applyParent(root) + val contextOwner = + ContainerNode(key = "local-owner") + .apply { + position = PositionMode.Relative + zIndex = 2 + stackLayout = true + }.applyParent(root) var lowClicks = 0 var highClicks = 0 - val low = ButtonNode("low", key = "local-low").apply { - width = 72 - height = 24 - position = PositionMode.Relative - zIndex = 1 - onClick { lowClicks += 1 } - }.applyParent(contextOwner) - val high = ButtonNode("high", key = "local-high").apply { - width = 72 - height = 24 - position = PositionMode.Relative - zIndex = 5 - onClick { highClicks += 1 } - }.applyParent(contextOwner) + val low = + ButtonNode("low", key = "local-low") + .apply { + width = 72 + height = 24 + position = PositionMode.Relative + zIndex = 1 + onClick { lowClicks += 1 } + }.applyParent(contextOwner) + val high = + ButtonNode("high", key = "local-high") + .apply { + width = 72 + height = 24 + position = PositionMode.Relative + zIndex = 5 + onClick { highClicks += 1 } + }.applyParent(contextOwner) renderTree(root, width = 240, height = 140) @@ -153,26 +176,32 @@ class StackingContextChildContextBehaviorTests { val lowColor = 0x00_12_34_56 val highColor = 0x00_65_43_21 val root = ContainerNode(key = "paint-root", stackLayout = true) - val lowerContext = ContainerNode(key = "paint-lower").apply { - position = PositionMode.Relative - zIndex = 1 - }.applyParent(root) - val higherContext = ContainerNode(key = "paint-higher").apply { - position = PositionMode.Relative - zIndex = 2 - }.applyParent(root) - - ButtonNode("lower", backgroundColor = lowColor, key = "paint-lower-btn").apply { - width = 72 - height = 24 - position = PositionMode.Relative - zIndex = 9_999 - }.applyParent(lowerContext) - ButtonNode("higher", backgroundColor = highColor, key = "paint-higher-btn").apply { - width = 72 - height = 24 - zIndex = 0 - }.applyParent(higherContext) + val lowerContext = + ContainerNode(key = "paint-lower") + .apply { + position = PositionMode.Relative + zIndex = 1 + }.applyParent(root) + val higherContext = + ContainerNode(key = "paint-higher") + .apply { + position = PositionMode.Relative + zIndex = 2 + }.applyParent(root) + + ButtonNode("lower", backgroundColor = lowColor, key = "paint-lower-btn") + .apply { + width = 72 + height = 24 + position = PositionMode.Relative + zIndex = 9_999 + }.applyParent(lowerContext) + ButtonNode("higher", backgroundColor = highColor, key = "paint-higher-btn") + .apply { + width = 72 + height = 24 + zIndex = 0 + }.applyParent(higherContext) val tree = DomTree(root) tree.render(ctx, 240, 140) @@ -188,37 +217,46 @@ class StackingContextChildContextBehaviorTests { @Test fun `recursive hit traversal mirrors recursive paint order in reverse`() { val root = ContainerNode(key = "hit-root", stackLayout = true) - val low = ContainerNode(key = "hit-low").apply { - position = PositionMode.Relative - zIndex = 1 - }.applyParent(root) - val mid = ContainerNode(key = "hit-mid").apply { - position = PositionMode.Relative - zIndex = 2 - }.applyParent(root) - val high = ContainerNode(key = "hit-high").apply { - position = PositionMode.Relative - zIndex = 3 - }.applyParent(root) + val low = + ContainerNode(key = "hit-low") + .apply { + position = PositionMode.Relative + zIndex = 1 + }.applyParent(root) + val mid = + ContainerNode(key = "hit-mid") + .apply { + position = PositionMode.Relative + zIndex = 2 + }.applyParent(root) + val high = + ContainerNode(key = "hit-high") + .apply { + position = PositionMode.Relative + zIndex = 3 + }.applyParent(root) var lowClicks = 0 var midClicks = 0 var highClicks = 0 - ButtonNode("low", key = "hit-low-btn").apply { - width = 72 - height = 24 - onClick { lowClicks += 1 } - }.applyParent(low) - ButtonNode("mid", key = "hit-mid-btn").apply { - width = 72 - height = 24 - onClick { midClicks += 1 } - }.applyParent(mid) - ButtonNode("high", key = "hit-high-btn").apply { - width = 72 - height = 24 - onClick { highClicks += 1 } - }.applyParent(high) + ButtonNode("low", key = "hit-low-btn") + .apply { + width = 72 + height = 24 + onClick { lowClicks += 1 } + }.applyParent(low) + ButtonNode("mid", key = "hit-mid-btn") + .apply { + width = 72 + height = 24 + onClick { midClicks += 1 } + }.applyParent(mid) + ButtonNode("high", key = "hit-high-btn") + .apply { + width = 72 + height = 24 + onClick { highClicks += 1 } + }.applyParent(high) renderTree(root, width = 240, height = 140) assertEquals(listOf(high, mid, low), root.orderedChildrenForHitTestingTraversal()) @@ -231,35 +269,43 @@ class StackingContextChildContextBehaviorTests { @Test fun `fixed root participation remains authoritative when fixed matches trigger`() { val root = ContainerNode(key = "fixed-root", stackLayout = true) - val contextA = ContainerNode(key = "fixed-context-a").apply { - position = PositionMode.Relative - zIndex = 1 - margin = Insets(top = 30, right = 0, bottom = 0, left = 50) - }.applyParent(root) - val contextB = ContainerNode(key = "fixed-context-b").apply { - position = PositionMode.Relative - zIndex = 2 - }.applyParent(root) + val contextA = + ContainerNode(key = "fixed-context-a") + .apply { + position = PositionMode.Relative + zIndex = 1 + margin = Insets(top = 30, right = 0, bottom = 0, left = 50) + }.applyParent(root) + val contextB = + ContainerNode(key = "fixed-context-b") + .apply { + position = PositionMode.Relative + zIndex = 2 + }.applyParent(root) var fixedClicks = 0 var normalClicks = 0 - val fixed = ButtonNode("fixed", key = "fixed-node").apply { - width = 72 - height = 24 - position = PositionMode.Fixed - zIndex = 9_999 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "8px", - StyleProperty.TOP to "9px" - ) - onClick { fixedClicks += 1 } - }.applyParent(contextA) - ButtonNode("normal", key = "fixed-normal").apply { - width = 72 - height = 24 - onClick { normalClicks += 1 } - }.applyParent(contextB) + val fixed = + ButtonNode("fixed", key = "fixed-node") + .apply { + width = 72 + height = 24 + position = PositionMode.Fixed + zIndex = 9_999 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "8px", + StyleProperty.TOP to "9px", + ) + onClick { fixedClicks += 1 } + }.applyParent(contextA) + ButtonNode("normal", key = "fixed-normal") + .apply { + width = 72 + height = 24 + onClick { normalClicks += 1 } + }.applyParent(contextB) renderTree(root, width = 260, height = 180) @@ -289,11 +335,10 @@ class StackingContextChildContextBehaviorTests { DomTree(root).render(ctx, width, height) } - private fun styleDeclarations(vararg entries: Pair): StyleDeclarations { - return StyleDeclarations().apply { + private fun styleDeclarations(vararg entries: Pair): StyleDeclarations = + StyleDeclarations().apply { entries.forEach { (property, literal) -> set(property, StyleExpression.Literal(literal)) } } - } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/StackingContextScaffoldingTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/StackingContextScaffoldingTests.kt index ae9bd7c..a1602fb 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/StackingContextScaffoldingTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/StackingContextScaffoldingTests.kt @@ -8,10 +8,10 @@ import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.event.MouseClickEvent import org.dreamfinity.dsgl.core.event.dispatchClick import org.dreamfinity.dsgl.core.render.RenderCommand +import org.dreamfinity.dsgl.core.style.PositionMode import org.dreamfinity.dsgl.core.style.StyleDeclarations import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleExpression -import org.dreamfinity.dsgl.core.style.PositionMode import org.dreamfinity.dsgl.core.style.StyleProperty import kotlin.test.AfterTest import kotlin.test.Test @@ -23,11 +23,14 @@ import kotlin.test.assertSame import kotlin.test.assertTrue class StackingContextScaffoldingTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -59,10 +62,12 @@ class StackingContextScaffoldingTests { val root = ContainerNode(key = "stacking-scaffold-root") val owner = ContainerNode(key = "stacking-scaffold-owner").applyParent(root) val normalChild = ContainerNode(key = "stacking-normal").applyParent(owner) - val fixedChild = ContainerNode(key = "stacking-fixed").apply { - position = PositionMode.Fixed - zIndex = 9999 - }.applyParent(owner) + val fixedChild = + ContainerNode(key = "stacking-fixed") + .apply { + position = PositionMode.Fixed + zIndex = 9999 + }.applyParent(owner) val context = owner.stackingContextScaffoldForTraversalOwner() val rootContext = root.stackingContextScaffoldForTraversalOwner() @@ -91,15 +96,18 @@ class StackingContextScaffoldingTests { val root = ContainerNode(key = "stacking-paint-root", stackLayout = true) val early = ContainerNode(key = "stacking-paint-early").applyParent(root) val later = ContainerNode(key = "stacking-paint-later").applyParent(root) - val fixed = ContainerNode(key = "stacking-paint-fixed").apply { - position = PositionMode.Fixed - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "4px", - StyleProperty.TOP to "4px" - ) - zIndex = 9999 - }.applyParent(early) + val fixed = + ContainerNode(key = "stacking-paint-fixed") + .apply { + position = PositionMode.Fixed + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "4px", + StyleProperty.TOP to "4px", + ) + zIndex = 9999 + }.applyParent(early) assertEquals(listOf(early, later, fixed), root.orderedChildrenForPaintTraversal()) assertTrue(early.orderedChildrenForPaintTraversal().isEmpty()) @@ -116,22 +124,26 @@ class StackingContextScaffoldingTests { var fixedClicks = 0 var laterClicks = 0 - val fixedNode = ButtonNode("fixed", key = "stacking-hit-fixed").apply { - width = 72 - height = 24 - zIndex = 9999 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "8px", - StyleProperty.TOP to "8px" - ) - onClick { fixedClicks += 1 } - }.applyParent(earlySubtree) - ButtonNode("later", key = "stacking-hit-later-node").apply { - width = 72 - height = 24 - onClick { laterClicks += 1 } - }.applyParent(laterSubtree) + val fixedNode = + ButtonNode("fixed", key = "stacking-hit-fixed") + .apply { + width = 72 + height = 24 + zIndex = 9999 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "8px", + StyleProperty.TOP to "8px", + ) + onClick { fixedClicks += 1 } + }.applyParent(earlySubtree) + ButtonNode("later", key = "stacking-hit-later-node") + .apply { + width = 72 + height = 24 + onClick { laterClicks += 1 } + }.applyParent(laterSubtree) renderTree(root, width = 220, height = 140) assertEquals(listOf(fixedNode, laterSubtree, earlySubtree), root.orderedChildrenForHitTestingTraversal()) @@ -144,34 +156,39 @@ class StackingContextScaffoldingTests { @Test fun `nested fixed behavior is root ordered and still root anchored`() { val root = ContainerNode(key = "stacking-phase-root", stackLayout = true) - val earlySubtree = ContainerNode(key = "stacking-phase-early").apply { - width = 120 - height = 60 - } - val laterSubtree = ContainerNode(key = "stacking-phase-later").apply { - width = 120 - height = 60 - } + val earlySubtree = + ContainerNode(key = "stacking-phase-early").apply { + width = 120 + height = 60 + } + val laterSubtree = + ContainerNode(key = "stacking-phase-later").apply { + width = 120 + height = 60 + } var fixedClicks = 0 var laterClicks = 0 - val fixed = ButtonNode("fixed", key = "stacking-phase-fixed").apply { - width = 72 - height = 24 - zIndex = 9999 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "8px", - StyleProperty.TOP to "8px" - ) - onClick { fixedClicks += 1 } - } - val later = ButtonNode("later", key = "stacking-phase-later-node").apply { - width = 72 - height = 24 - onClick { laterClicks += 1 } - } + val fixed = + ButtonNode("fixed", key = "stacking-phase-fixed").apply { + width = 72 + height = 24 + zIndex = 9999 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "8px", + StyleProperty.TOP to "8px", + ) + onClick { fixedClicks += 1 } + } + val later = + ButtonNode("later", key = "stacking-phase-later-node").apply { + width = 72 + height = 24 + onClick { laterClicks += 1 } + } fixed.applyParent(earlySubtree) later.applyParent(laterSubtree) @@ -190,11 +207,10 @@ class StackingContextScaffoldingTests { DomTree(root).render(ctx, width, height) } - private fun styleDeclarations(vararg entries: Pair): StyleDeclarations { - return StyleDeclarations().apply { + private fun styleDeclarations(vararg entries: Pair): StyleDeclarations = + StyleDeclarations().apply { entries.forEach { (property, literal) -> set(property, StyleExpression.Literal(literal)) } } - } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/StickyLayoutModelContractTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/StickyLayoutModelContractTests.kt index 4b0fb81..bbe127f 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/StickyLayoutModelContractTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/StickyLayoutModelContractTests.kt @@ -13,12 +13,16 @@ class StickyLayoutModelContractTests { @Test fun `sticky scroll-container rule chooses nearest horizontal scroll-container ancestor`() { val root = ContainerNode(key = "sticky-root-x") - val farScroll = ContainerNode(key = "sticky-scroll-far-x").apply { - overflowX = Overflow.Auto - }.applyParent(root) - val nearScroll = ContainerNode(key = "sticky-scroll-near-x").apply { - overflowX = Overflow.Hidden - }.applyParent(farScroll) + val farScroll = + ContainerNode(key = "sticky-scroll-far-x") + .apply { + overflowX = Overflow.Auto + }.applyParent(root) + val nearScroll = + ContainerNode(key = "sticky-scroll-near-x") + .apply { + overflowX = Overflow.Hidden + }.applyParent(farScroll) val parent = ContainerNode(key = "sticky-parent-x").applyParent(nearScroll) val node = ContainerNode(key = "sticky-node-x").applyParent(parent) @@ -38,12 +42,16 @@ class StickyLayoutModelContractTests { @Test fun `sticky scroll-container rule chooses nearest vertical scroll-container ancestor`() { val root = ContainerNode(key = "sticky-root") - val farScroll = ContainerNode(key = "sticky-scroll-far").apply { - overflowY = Overflow.Auto - }.applyParent(root) - val nearScroll = ContainerNode(key = "sticky-scroll-near").apply { - overflowY = Overflow.Hidden - }.applyParent(farScroll) + val farScroll = + ContainerNode(key = "sticky-scroll-far") + .apply { + overflowY = Overflow.Auto + }.applyParent(root) + val nearScroll = + ContainerNode(key = "sticky-scroll-near") + .apply { + overflowY = Overflow.Hidden + }.applyParent(farScroll) val parent = ContainerNode(key = "sticky-parent").applyParent(nearScroll) val node = ContainerNode(key = "sticky-node").applyParent(parent) @@ -63,9 +71,11 @@ class StickyLayoutModelContractTests { @Test fun `sticky containing-block rule is parent slot owner and is independent from sticky scroll-container rule`() { val root = ContainerNode(key = "sticky-container-root") - val scrollAncestor = ContainerNode(key = "sticky-scroll-ancestor").apply { - overflowY = Overflow.Scroll - }.applyParent(root) + val scrollAncestor = + ContainerNode(key = "sticky-scroll-ancestor") + .apply { + overflowY = Overflow.Scroll + }.applyParent(root) val flowParent = ContainerNode(key = "sticky-flow-parent").applyParent(scrollAncestor) val node = ContainerNode(key = "sticky-child").applyParent(flowParent) @@ -140,7 +150,7 @@ class StickyLayoutModelContractTests { assertEquals( StickyLayoutModel.PositionedGeometryIntegrationPoint.SharedUsedGeometryTransform, - node.stickyPositionedGeometryIntegrationPoint() + node.stickyPositionedGeometryIntegrationPoint(), ) } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/UnifiedUsedGeometryInspectorCharacterizationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/UnifiedUsedGeometryInspectorCharacterizationTests.kt index 83d0351..e74c40f 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/UnifiedUsedGeometryInspectorCharacterizationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/UnifiedUsedGeometryInspectorCharacterizationTests.kt @@ -25,11 +25,14 @@ import kotlin.test.assertNull import kotlin.test.assertTrue class UnifiedUsedGeometryInspectorCharacterizationTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -41,24 +44,27 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { fun `core click and hover reach render-visible absolute outside ancestor bounds`() { val fixture = createAbsoluteOutsideAncestorFixture() fixture.tree.render(ctx, width = 260, height = 140) - val drawRects = fixture.tree.paint(ctx).filterIsInstance() + val drawRects = + fixture.tree + .paint(ctx) + .filterIsInstance() assertTrue( drawRects.any { it.color == fixture.childColor && it.x == 100 && it.y == 5 }, - "Render draws absolute child at its final positioned rect outside ancestor bounds" + "Render draws absolute child at its final positioned rect outside ancestor bounds", ) var childClicks = 0 fixture.child.onClick { childClicks += 1 } assertTrue( dispatchClick(fixture.root, MouseClickEvent(105, 10, MouseButton.LEFT)), - "Core click now reaches visible absolute child outside ancestor-local bounds" + "Core click now reaches visible absolute child outside ancestor-local bounds", ) assertEquals(1, childClicks) val hoverChain = collectHoverChain(fixture.root, 105, 10) assertEquals( fixture.child.key, hoverChain.lastOrNull()?.key, - "Core hover now resolves the same visible absolute target" + "Core hover now resolves the same visible absolute target", ) } @@ -72,9 +78,10 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { inspector.onCursorMoved(105, 10) assertEquals( - fixture.child.key?.toString(), + fixture.child.key + ?.toString(), inspector.hoveredKey, - "Inspector picking now follows shared used geometry for absolute-outside-ancestor case" + "Inspector picking now follows shared used geometry for absolute-outside-ancestor case", ) } @@ -82,7 +89,10 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { fun `render core interaction and inspector all target promoted fixed`() { val fixture = createPositionedOverlapFixture() fixture.tree.render(ctx, width = 220, height = 140) - val drawRects = fixture.tree.paint(ctx).filterIsInstance() + val drawRects = + fixture.tree + .paint(ctx) + .filterIsInstance() val fixedPaintIndex = drawRects.indexOfFirst { it.color == fixture.fixedColor && it.x == 8 && it.y == 8 } val laterPaintIndex = drawRects.indexOfFirst { it.color == fixture.laterColor && it.x == 0 && it.y == 0 } @@ -104,9 +114,10 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { inspector.onLayoutCommitted(fixture.root, 1L) inspector.onCursorMoved(10, 10) assertEquals( - fixture.fixed.key?.toString(), + fixture.fixed.key + ?.toString(), inspector.hoveredKey, - "Inspector now resolves the same topmost promoted fixed node as render/core interaction" + "Inspector now resolves the same topmost promoted fixed node as render/core interaction", ) } @@ -118,7 +129,7 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { assertEquals( listOf(fixture.fixed, fixture.laterContainer, fixture.earlyContainer), fixture.root.orderedChildrenForHitTestingTraversal(), - "Positioned hit traversal resolves promoted fixed first" + "Positioned hit traversal resolves promoted fixed first", ) val inspector = InspectorController() @@ -127,9 +138,10 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { inspector.onCursorMoved(10, 10) assertEquals( - fixture.fixed.key?.toString(), + fixture.fixed.key + ?.toString(), inspector.hoveredKey, - "Inspector now uses shared ordering for overlap picking" + "Inspector now uses shared ordering for overlap picking", ) } @@ -149,56 +161,70 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { val absoluteHover = collectHoverChain(absoluteFixture.root, 105, 10).lastOrNull()?.key assertEquals(fixedFixture.fixed.key, fixedHover, "Core hover sees promoted fixed in overlap case") - assertEquals(absoluteFixture.child.key, absoluteHover, "Core hover reaches absolute child outside ancestor bounds") assertEquals( - fixedFixture.fixed.key?.toString(), + absoluteFixture.child.key, + absoluteHover, + "Core hover reaches absolute child outside ancestor bounds", + ) + assertEquals( + fixedFixture.fixed.key + ?.toString(), inspector.hoveredKey, - "Inspector now matches core positioned ordering in overlap case" + "Inspector now matches core positioned ordering in overlap case", ) var absoluteClicks = 0 absoluteFixture.child.onClick { absoluteClicks += 1 } assertTrue( dispatchClick(absoluteFixture.root, MouseClickEvent(105, 10, MouseButton.LEFT)), - "Core click reaches absolute-outside-ancestor case after shared used-geometry migration" + "Core click reaches absolute-outside-ancestor case after shared used-geometry migration", ) assertEquals(1, absoluteClicks) inspector.onLayoutCommitted(absoluteFixture.root, 22L) inspector.onCursorMoved(105, 10) assertEquals( - absoluteFixture.child.key?.toString(), + absoluteFixture.child.key + ?.toString(), inspector.hoveredKey, - "Inspector now matches core/app-host geometry for absolute-outside-ancestor case" + "Inspector now matches core/app-host geometry for absolute-outside-ancestor case", ) } @Test fun `core and inspector preserve fixed and non-fixed clip semantics`() { val root = ContainerNode(key = "clip-root", stackLayout = true) - val overflowParent = ContainerNode(key = "clip-parent").apply { - width = 80 - height = 40 - overflowY = Overflow.Hidden - inlineStyleDeclarations = styleDeclarations(StyleProperty.POSITION to "relative") - }.applyParent(root) - val fixed = ButtonNode("fixed", key = "clip-fixed").apply { - width = 40 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "180px", - StyleProperty.TOP to "20px" - ) - }.applyParent(overflowParent) - val absolute = ButtonNode("absolute", key = "clip-absolute").apply { - width = 40 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "absolute", - StyleProperty.LEFT to "140px", - StyleProperty.TOP to "90px" - ) - }.applyParent(overflowParent) + val overflowParent = + ContainerNode(key = "clip-parent") + .apply { + width = 80 + height = 40 + overflowY = Overflow.Hidden + inlineStyleDeclarations = styleDeclarations(StyleProperty.POSITION to "relative") + }.applyParent(root) + val fixed = + ButtonNode("fixed", key = "clip-fixed") + .apply { + width = 40 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "180px", + StyleProperty.TOP to "20px", + ) + }.applyParent(overflowParent) + val absolute = + ButtonNode("absolute", key = "clip-absolute") + .apply { + width = 40 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "absolute", + StyleProperty.LEFT to "140px", + StyleProperty.TOP to "90px", + ) + }.applyParent(overflowParent) val tree = DomTree(root) tree.render(ctx, width = 200, height = 120) @@ -232,34 +258,43 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { @Test fun `inspector highlight uses shared used geometry and clipping`() { - val root = ContainerNode(key = "highlight-root", stackLayout = true).apply { - width = 200 - height = 120 - } - val overflowParent = ContainerNode(key = "highlight-parent").apply { - width = 80 - height = 40 - overflowY = Overflow.Hidden - inlineStyleDeclarations = styleDeclarations(StyleProperty.POSITION to "relative") - }.applyParent(root) - val fixed = ButtonNode("fixed", key = "highlight-fixed").apply { - width = 40 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "180px", - StyleProperty.TOP to "20px" - ) - }.applyParent(overflowParent) - val absolute = ButtonNode("absolute", key = "highlight-absolute").apply { - width = 40 - height = 20 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "absolute", - StyleProperty.LEFT to "140px", - StyleProperty.TOP to "90px" - ) - }.applyParent(overflowParent) + val root = + ContainerNode(key = "highlight-root", stackLayout = true).apply { + width = 200 + height = 120 + } + val overflowParent = + ContainerNode(key = "highlight-parent") + .apply { + width = 80 + height = 40 + overflowY = Overflow.Hidden + inlineStyleDeclarations = styleDeclarations(StyleProperty.POSITION to "relative") + }.applyParent(root) + val fixed = + ButtonNode("fixed", key = "highlight-fixed") + .apply { + width = 40 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "180px", + StyleProperty.TOP to "20px", + ) + }.applyParent(overflowParent) + val absolute = + ButtonNode("absolute", key = "highlight-absolute") + .apply { + width = 40 + height = 20 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "absolute", + StyleProperty.LEFT to "140px", + StyleProperty.TOP to "90px", + ) + }.applyParent(overflowParent) val tree = DomTree(root) tree.render(ctx, width = 200, height = 120) @@ -274,7 +309,7 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { val fixedUsedGeometry = UsedInteractionGeometryResolver.resolveNodeGeometry(fixed) assertEquals( fixedUsedGeometry.visibleBorderRect ?: Rect(0, 0, 0, 0), - fixedHighlight.borderRect + fixedHighlight.borderRect, ) inspector.onCursorMoved(145, 95) @@ -285,26 +320,31 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { assertEquals( rootUsedGeometry.visibleBorderRect ?: Rect(0, 0, 0, 0), clippedHighlight.borderRect, - "Non-fixed clipped case highlights resolved fallback target with shared used geometry" + "Non-fixed clipped case highlights resolved fallback target with shared used geometry", ) } @Test fun `relative positioned node highlight uses final rendered position`() { val root = ContainerNode(key = "relative-root") - val relative = ButtonNode("relative", key = "relative-target").apply { - width = 60 - height = 24 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "relative", - StyleProperty.LEFT to "40px", - StyleProperty.TOP to "18px" - ) - }.applyParent(root) - val sibling = ButtonNode("sibling", key = "relative-sibling").apply { - width = 70 - height = 24 - }.applyParent(root) + val relative = + ButtonNode("relative", key = "relative-target") + .apply { + width = 60 + height = 24 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "relative", + StyleProperty.LEFT to "40px", + StyleProperty.TOP to "18px", + ) + }.applyParent(root) + val sibling = + ButtonNode("sibling", key = "relative-sibling") + .apply { + width = 70 + height = 24 + }.applyParent(root) val tree = DomTree(root) tree.render(ctx, width = 260, height = 140) @@ -324,7 +364,7 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { assertEquals( usedGeometry.visibleBorderRect ?: Rect(0, 0, 0, 0), highlight.borderRect, - "Relative highlight must use final rendered geometry, not original layout slot" + "Relative highlight must use final rendered geometry, not original layout slot", ) // Ensure we did not accidentally break neighboring static hit/highlight behavior. @@ -345,14 +385,18 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { inspector.onCursorMoved(10, 10) inspector.buildDomSnapshot(800, 600) - assertEquals(fixture.fixed.key?.toString(), inspector.hoveredKey) + assertEquals( + fixture.fixed.key + ?.toString(), + inspector.hoveredKey, + ) val highlight = inspector.overlayHoveredHighlight() assertNotNull(highlight) val fixedGeometry = UsedInteractionGeometryResolver.resolveNodeGeometry(fixture.fixed) assertEquals( fixedGeometry.visibleBorderRect ?: Rect(0, 0, 0, 0), highlight.borderRect, - "Inspector highlight must match picked node final used geometry in overlap cases" + "Inspector highlight must match picked node final used geometry in overlap cases", ) } @@ -364,7 +408,7 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { val fixed: ButtonNode, val later: ButtonNode, val fixedColor: Int, - val laterColor: Int + val laterColor: Int, ) private data class AbsoluteOutsideAncestorFixture( @@ -372,35 +416,42 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { val root: ContainerNode, val ancestor: ContainerNode, val child: ButtonNode, - val childColor: Int + val childColor: Int, ) private fun createPositionedOverlapFixture(): PositionedOverlapFixture { val fixedColor = 0x00_27_83_B4 val laterColor = 0x00_B4_5D_27 val root = ContainerNode(key = "root", stackLayout = true) - val early = ContainerNode(key = "early", stackLayout = true).apply { - width = 120 - height = 60 - } - val laterContainer = ContainerNode(key = "later-container", stackLayout = true).apply { - width = 120 - height = 60 - } - val fixed = ButtonNode("fixed", backgroundColor = fixedColor, key = "fixed").apply { - width = 72 - height = 24 - zIndex = 9_999 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "fixed", - StyleProperty.LEFT to "8px", - StyleProperty.TOP to "8px" - ) - }.applyParent(early) - val later = ButtonNode("later", backgroundColor = laterColor, key = "later").apply { - width = 72 - height = 24 - }.applyParent(laterContainer) + val early = + ContainerNode(key = "early", stackLayout = true).apply { + width = 120 + height = 60 + } + val laterContainer = + ContainerNode(key = "later-container", stackLayout = true).apply { + width = 120 + height = 60 + } + val fixed = + ButtonNode("fixed", backgroundColor = fixedColor, key = "fixed") + .apply { + width = 72 + height = 24 + zIndex = 9_999 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "fixed", + StyleProperty.LEFT to "8px", + StyleProperty.TOP to "8px", + ) + }.applyParent(early) + val later = + ButtonNode("later", backgroundColor = laterColor, key = "later") + .apply { + width = 72 + height = 24 + }.applyParent(laterContainer) early.applyParent(root) laterContainer.applyParent(root) return PositionedOverlapFixture( @@ -411,44 +462,48 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { fixed = fixed, later = later, fixedColor = fixedColor, - laterColor = laterColor + laterColor = laterColor, ) } private fun createAbsoluteOutsideAncestorFixture(): AbsoluteOutsideAncestorFixture { val childColor = 0x00_1F_9A_55 val root = ContainerNode(key = "abs-root") - val ancestor = ContainerNode(key = "abs-ancestor").apply { - width = 40 - height = 40 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "relative" - ) - }.applyParent(root) - val child = ButtonNode("abs-child", backgroundColor = childColor, key = "abs-child").apply { - width = 36 - height = 16 - inlineStyleDeclarations = styleDeclarations( - StyleProperty.POSITION to "absolute", - StyleProperty.LEFT to "100px", - StyleProperty.TOP to "5px" - ) - }.applyParent(ancestor) + val ancestor = + ContainerNode(key = "abs-ancestor") + .apply { + width = 40 + height = 40 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "relative", + ) + }.applyParent(root) + val child = + ButtonNode("abs-child", backgroundColor = childColor, key = "abs-child") + .apply { + width = 36 + height = 16 + inlineStyleDeclarations = + styleDeclarations( + StyleProperty.POSITION to "absolute", + StyleProperty.LEFT to "100px", + StyleProperty.TOP to "5px", + ) + }.applyParent(ancestor) return AbsoluteOutsideAncestorFixture( tree = DomTree(root), root = root, ancestor = ancestor, child = child, - childColor = childColor + childColor = childColor, ) } - private fun styleDeclarations(vararg entries: Pair): StyleDeclarations { - return StyleDeclarations().apply { + private fun styleDeclarations(vararg entries: Pair): StyleDeclarations = + StyleDeclarations().apply { entries.forEach { (property, literal) -> set(property, StyleExpression.Literal(literal)) } } - } } - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/InlineLayoutTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/InlineLayoutTests.kt index 1ece9db..4ea9acb 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/InlineLayoutTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/InlineLayoutTests.kt @@ -1,7 +1,7 @@ package org.dreamfinity.dsgl.core.dom.elements -import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.layout.Border import org.dreamfinity.dsgl.core.dom.layout.Insets import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext @@ -17,24 +17,28 @@ class InlineLayoutTests { @Test fun `inline child measured with parent width constraint expands height after wrap`() { val ctx = testMeasureContext() - val root = ContainerNode(key = "root").apply { - display = Display.Block - width = 30 - } - val inline = ContainerNode(key = "inline").apply { - display = Display.Inline - gap = 2 - padding = Insets.all(1) - border = Border.all(1, 0xFF000000.toInt()) - }.applyParent(root) + val root = + ContainerNode(key = "root").apply { + display = Display.Block + width = 30 + } + val inline = + ContainerNode(key = "inline") + .apply { + display = Display.Inline + gap = 2 + padding = Insets.all(1) + border = Border.all(1, 0xFF000000.toInt()) + }.applyParent(root) repeat(3) { index -> - ContainerNode(key = "item.$index").apply { - display = Display.Inline - width = 10 - height = 4 - margin = Insets(1, 1, 1, 1) - }.applyParent(inline) + ContainerNode(key = "item.$index") + .apply { + display = Display.Inline + width = 10 + height = 4 + margin = Insets(1, 1, 1, 1) + }.applyParent(inline) } val measured = root.measure(ctx) @@ -42,8 +46,19 @@ class InlineLayoutTests { val expectedLineHeight = expectedNormalLineHeightPx(ctx, 16) val expectedTotalHeight = expectedLineHeight * 2 + inline.gap + inline.padding.vertical + inline.border.vertical - assertEquals(expectedTotalHeight, inline.bounds.height, "Inline height must include wrapped second line with line-box reservation.") - assertEquals(2, inline.children.map { it.bounds.y }.distinct().size, "Inline items should wrap to two lines.") + assertEquals( + expectedTotalHeight, + inline.bounds.height, + "Inline height must include wrapped second line with line-box reservation.", + ) + assertEquals( + 2, + inline.children + .map { it.bounds.y } + .distinct() + .size, + "Inline items should wrap to two lines.", + ) assertOuterWidthsFitInlineContent(inline) assertContentFitsIntoInlineBounds(inline) } @@ -51,32 +66,42 @@ class InlineLayoutTests { @Test fun `inline flow with nested flex and block items keeps content inside measured bounds`() { val ctx = testMeasureContext() - val root = ContainerNode(key = "root.nested").apply { - display = Display.Block - width = 36 - } - val inline = ContainerNode(key = "inline.nested").apply { - display = Display.Inline - gap = 3 - padding = Insets.all(2) - border = Border.all(1, 0xFF000000.toInt()) - }.applyParent(root) + val root = + ContainerNode(key = "root.nested").apply { + display = Display.Block + width = 36 + } + val inline = + ContainerNode(key = "inline.nested") + .apply { + display = Display.Inline + gap = 3 + padding = Insets.all(2) + border = Border.all(1, 0xFF000000.toInt()) + }.applyParent(root) createInlineFlexItem("flex", 8, 7).applyParent(inline) createInlineBlockItem("block", 10, 6).applyParent(inline) - ContainerNode(key = "simple").apply { - display = Display.Inline - width = 12 - height = 4 - margin = Insets(1, 2, 1, 1) - border = Border.all(1, 0xFF000000.toInt()) - padding = Insets.all(1) - }.applyParent(inline) + ContainerNode(key = "simple") + .apply { + display = Display.Inline + width = 12 + height = 4 + margin = Insets(1, 2, 1, 1) + border = Border.all(1, 0xFF000000.toInt()) + padding = Insets.all(1) + }.applyParent(inline) val measured = root.measure(ctx) root.render(ctx, 0, 0, measured.width, measured.height) - assertTrue(inline.children.map { it.bounds.y }.distinct().size >= 2, "Nested inline items must wrap.") + assertTrue( + inline.children + .map { it.bounds.y } + .distinct() + .size >= 2, + "Nested inline items must wrap.", + ) assertOuterWidthsFitInlineContent(inline) assertContentFitsIntoInlineBounds(inline) } @@ -84,16 +109,19 @@ class InlineLayoutTests { @Test fun `inline wrappers with mixed text plus nested containers do not overlap between lines`() { val ctx = testMeasureContext() - val root = ContainerNode(key = "root.mixed").apply { - display = Display.Block - width = 44 - } - val inline = ContainerNode(key = "inline.mixed").apply { - display = Display.Inline - gap = 2 - padding = Insets.all(2) - border = Border.all(1, 0xFF000000.toInt()) - }.applyParent(root) + val root = + ContainerNode(key = "root.mixed").apply { + display = Display.Block + width = 44 + } + val inline = + ContainerNode(key = "inline.mixed") + .apply { + display = Display.Inline + gap = 2 + padding = Insets.all(2) + border = Border.all(1, 0xFF000000.toInt()) + }.applyParent(root) createMixedInlineWrapper("a", includeFlex = true).applyParent(inline) createMixedInlineWrapper("b", includeFlex = false).applyParent(inline) @@ -122,12 +150,14 @@ class InlineLayoutTests { val chipChildren = inline.children.filter { (it.key as? String)?.startsWith("chip.") == true } assertTrue( chipChildren.all { chip -> chip.bounds.width + chip.margin.horizontal < inlineContentWidth }, - "Inline chips should remain shrink-to-fit and not stretch to full line width." + "Inline chips should remain shrink-to-fit and not stretch to full line width.", ) assertOuterWidthsFitInlineContent(inline) assertContentFitsIntoInlineBounds(inline) assertNoVerticalOverlapBetweenSiblings(inline.children) - inline.children.filterIsInstance().forEach { assertContainerChildrenFit(it) } + inline.children + .filterIsInstance() + .forEach { assertContainerChildrenFit(it) } } @Test @@ -157,17 +187,20 @@ class InlineLayoutTests { @Test fun `inline container explicit width larger than parent is constrained without overlap`() { val ctx = testMeasureContext() - val root = ContainerNode(key = "root.small").apply { - display = Display.Block - width = 60 - } - val inline = ContainerNode(key = "inline.explicit.large").apply { - display = Display.Inline - width = 96 - gap = 2 - padding = Insets.all(3) - border = Border.all(1, 0xFF000000.toInt()) - }.applyParent(root) + val root = + ContainerNode(key = "root.small").apply { + display = Display.Block + width = 60 + } + val inline = + ContainerNode(key = "inline.explicit.large") + .apply { + display = Display.Inline + width = 96 + gap = 2 + padding = Insets.all(3) + border = Border.all(1, 0xFF000000.toInt()) + }.applyParent(root) createMixedInlineWrapper("x", includeFlex = true).applyParent(inline) createMixedInlineWrapper("y", includeFlex = false).applyParent(inline) @@ -182,85 +215,102 @@ class InlineLayoutTests { } private fun createInlineFlexItem(key: String, aWidth: Int, bWidth: Int): ContainerNode { - val wrapper = ContainerNode(key = "$key.wrapper").apply { - display = Display.Inline - margin = Insets(1, 2, 1, 1) - padding = Insets.all(1) - border = Border.all(1, 0xFF000000.toInt()) - } - val flex = ContainerNode(key = "$key.flex").apply { - display = Display.Flex - flexDirection = FlexDirection.Row - gap = 1 - padding = Insets.all(1) - }.applyParent(wrapper) - ContainerNode(key = "$key.a").apply { - width = aWidth - height = 4 - }.applyParent(flex) - ContainerNode(key = "$key.b").apply { - width = bWidth - height = 4 - }.applyParent(flex) + val wrapper = + ContainerNode(key = "$key.wrapper").apply { + display = Display.Inline + margin = Insets(1, 2, 1, 1) + padding = Insets.all(1) + border = Border.all(1, 0xFF000000.toInt()) + } + val flex = + ContainerNode(key = "$key.flex") + .apply { + display = Display.Flex + flexDirection = FlexDirection.Row + gap = 1 + padding = Insets.all(1) + }.applyParent(wrapper) + ContainerNode(key = "$key.a") + .apply { + width = aWidth + height = 4 + }.applyParent(flex) + ContainerNode(key = "$key.b") + .apply { + width = bWidth + height = 4 + }.applyParent(flex) return wrapper } private fun createInlineBlockItem(key: String, topWidth: Int, bottomWidth: Int): ContainerNode { - val wrapper = ContainerNode(key = "$key.wrapper").apply { - display = Display.Inline - margin = Insets(1, 2, 1, 1) - padding = Insets.all(1) - border = Border.all(1, 0xFF000000.toInt()) - } - val block = ContainerNode(key = "$key.block").apply { - display = Display.Block - gap = 1 - padding = Insets.all(1) - }.applyParent(wrapper) - ContainerNode(key = "$key.top").apply { - width = topWidth - height = 3 - }.applyParent(block) - ContainerNode(key = "$key.bottom").apply { - width = bottomWidth - height = 3 - }.applyParent(block) + val wrapper = + ContainerNode(key = "$key.wrapper").apply { + display = Display.Inline + margin = Insets(1, 2, 1, 1) + padding = Insets.all(1) + border = Border.all(1, 0xFF000000.toInt()) + } + val block = + ContainerNode(key = "$key.block") + .apply { + display = Display.Block + gap = 1 + padding = Insets.all(1) + }.applyParent(wrapper) + ContainerNode(key = "$key.top") + .apply { + width = topWidth + height = 3 + }.applyParent(block) + ContainerNode(key = "$key.bottom") + .apply { + width = bottomWidth + height = 3 + }.applyParent(block) return wrapper } private fun createMixedInlineWrapper(key: String, includeFlex: Boolean): ContainerNode { - val wrapper = ContainerNode(key = "$key.mixed.wrapper").apply { - display = Display.Inline - width = 24 - margin = Insets(1, 2, 1, 1) - padding = Insets.all(1) - border = Border.all(1, 0xFF000000.toInt()) - gap = 1 - } - TextNode(TextSource.Static("label-$key"), key = "$key.label").applyParent(wrapper) - if (includeFlex) { - val flex = ContainerNode(key = "$key.flex").apply { - display = Display.Flex - flexDirection = FlexDirection.Row - gap = 1 + val wrapper = + ContainerNode(key = "$key.mixed.wrapper").apply { + display = Display.Inline + width = 24 + margin = Insets(1, 2, 1, 1) padding = Insets.all(1) border = Border.all(1, 0xFF000000.toInt()) - }.applyParent(wrapper) - ContainerNode(key = "$key.flex.a").apply { - width = 9 - height = 4 - }.applyParent(flex) - ContainerNode(key = "$key.flex.b").apply { - width = 8 - height = 4 - }.applyParent(flex) - } else { - val block = ContainerNode(key = "$key.block").apply { - display = Display.Block gap = 1 - padding = Insets.all(1) - border = Border.all(1, 0xFF000000.toInt()) - }.applyParent(wrapper) + } + TextNode(TextSource.Static("label-$key"), key = "$key.label").applyParent(wrapper) + if (includeFlex) { + val flex = + ContainerNode(key = "$key.flex") + .apply { + display = Display.Flex + flexDirection = FlexDirection.Row + gap = 1 + padding = Insets.all(1) + border = Border.all(1, 0xFF000000.toInt()) + }.applyParent(wrapper) + ContainerNode(key = "$key.flex.a") + .apply { + width = 9 + height = 4 + }.applyParent(flex) + ContainerNode(key = "$key.flex.b") + .apply { + width = 8 + height = 4 + }.applyParent(flex) + } else { + val block = + ContainerNode(key = "$key.block") + .apply { + display = Display.Block + gap = 1 + padding = Insets.all(1) + border = Border.all(1, 0xFF000000.toInt()) + }.applyParent(wrapper) TextNode(TextSource.Static("top"), key = "$key.block.top").applyParent(block) TextNode(TextSource.Static("bottom"), key = "$key.block.bottom").applyParent(block) } @@ -268,93 +318,110 @@ class InlineLayoutTests { } private fun createDisplayInlineScenario(rootWidth: Int, inlineContentWidth: Int): Pair { - val root = ContainerNode(key = "root.display.inline").apply { - display = Display.Block - width = rootWidth - } - val inline = ContainerNode(key = "display.inline.container").apply { - display = Display.Inline - width = inlineContentWidth - gap = 2 - padding = Insets.all(3) - border = Border.all(1, 0xFF000000.toInt()) - }.applyParent(root) + val root = + ContainerNode(key = "root.display.inline").apply { + display = Display.Block + width = rootWidth + } + val inline = + ContainerNode(key = "display.inline.container") + .apply { + display = Display.Inline + width = inlineContentWidth + gap = 2 + padding = Insets.all(3) + border = Border.all(1, 0xFF000000.toInt()) + }.applyParent(root) listOf("alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta").forEachIndexed { index, label -> - ContainerNode(key = "chip.$index").apply { + ContainerNode(key = "chip.$index") + .apply { + display = Display.Inline + margin = Insets(1, 2, 1, 1) + padding = Insets.all(2) + border = Border.all(1, 0xFF000000.toInt()) + }.applyParent(inline) + .also { wrapper -> + TextNode(TextSource.Static(label), key = "chip.$index.label").applyParent(wrapper) + } + } + + ContainerNode(key = "display.inline.flex.item") + .apply { display = Display.Inline margin = Insets(1, 2, 1, 1) padding = Insets.all(2) border = Border.all(1, 0xFF000000.toInt()) - }.applyParent(inline).also { wrapper -> - TextNode(TextSource.Static(label), key = "chip.$index.label").applyParent(wrapper) - } - } - - ContainerNode(key = "display.inline.flex.item").apply { - display = Display.Inline - margin = Insets(1, 2, 1, 1) - padding = Insets.all(2) - border = Border.all(1, 0xFF000000.toInt()) - }.applyParent(inline).also { wrapper -> - TextNode(TextSource.Static("flex"), key = "display.inline.flex.label").applyParent(wrapper) - ContainerNode(key = "display.inline.flex.container").apply { - display = Display.Flex - flexDirection = FlexDirection.Row - gap = 1 - padding = Insets.all(1) - border = Border.all(1, 0xFF000000.toInt()) - }.applyParent(wrapper).also { flex -> - ContainerNode(key = "display.inline.flex.a").apply { - width = 8 - height = 4 - }.applyParent(flex) - ContainerNode(key = "display.inline.flex.b").apply { - width = 6 - height = 4 - }.applyParent(flex) + }.applyParent(inline) + .also { wrapper -> + TextNode(TextSource.Static("flex"), key = "display.inline.flex.label").applyParent(wrapper) + ContainerNode(key = "display.inline.flex.container") + .apply { + display = Display.Flex + flexDirection = FlexDirection.Row + gap = 1 + padding = Insets.all(1) + border = Border.all(1, 0xFF000000.toInt()) + }.applyParent(wrapper) + .also { flex -> + ContainerNode(key = "display.inline.flex.a") + .apply { + width = 8 + height = 4 + }.applyParent(flex) + ContainerNode(key = "display.inline.flex.b") + .apply { + width = 6 + height = 4 + }.applyParent(flex) + } } - } - ContainerNode(key = "display.inline.block.item").apply { - display = Display.Inline - margin = Insets(1, 2, 1, 1) - padding = Insets.all(2) - border = Border.all(1, 0xFF000000.toInt()) - }.applyParent(inline).also { wrapper -> - ContainerNode(key = "display.inline.block.container").apply { - display = Display.Block - gap = 1 - padding = Insets.all(1) + ContainerNode(key = "display.inline.block.item") + .apply { + display = Display.Inline + margin = Insets(1, 2, 1, 1) + padding = Insets.all(2) border = Border.all(1, 0xFF000000.toInt()) - }.applyParent(wrapper).also { block -> - TextNode(TextSource.Static("block"), key = "display.inline.block.label").applyParent(block) - TextNode(TextSource.Static("A"), key = "display.inline.block.a").applyParent(block) - TextNode(TextSource.Static("B"), key = "display.inline.block.b").applyParent(block) + }.applyParent(inline) + .also { wrapper -> + ContainerNode(key = "display.inline.block.container") + .apply { + display = Display.Block + gap = 1 + padding = Insets.all(1) + border = Border.all(1, 0xFF000000.toInt()) + }.applyParent(wrapper) + .also { block -> + TextNode(TextSource.Static("block"), key = "display.inline.block.label").applyParent(block) + TextNode(TextSource.Static("A"), key = "display.inline.block.a").applyParent(block) + TextNode(TextSource.Static("B"), key = "display.inline.block.b").applyParent(block) + } } - } return root to inline } private fun assertContentFitsIntoInlineBounds(inline: ContainerNode) { val contentBottom = inline.bounds.height - inline.border.bottom - inline.padding.bottom - val usedBottom = inline.children.maxOf { child -> - (child.bounds.y - inline.bounds.y) + child.bounds.height + child.margin.bottom - } + val usedBottom = + inline.children.maxOf { child -> + (child.bounds.y - inline.bounds.y) + child.bounds.height + child.margin.bottom + } assertTrue( usedBottom <= contentBottom, - "Inline child content should fit into measured bounds: used=$usedBottom, contentBottom=$contentBottom" + "Inline child content should fit into measured bounds: used=$usedBottom, contentBottom=$contentBottom", ) } private fun assertOuterWidthsFitInlineContent(inline: ContainerNode) { val contentWidth = inline.bounds.width - inline.border.horizontal - inline.padding.horizontal - val maxOuterWidth = inline.children.maxOf { child -> - child.bounds.width + child.margin.horizontal - } + val maxOuterWidth = + inline.children.maxOf { child -> + child.bounds.width + child.margin.horizontal + } assertTrue( maxOuterWidth <= contentWidth, - "Inline child outer width should fit content width: maxOuterWidth=$maxOuterWidth, contentWidth=$contentWidth" + "Inline child outer width should fit content width: maxOuterWidth=$maxOuterWidth, contentWidth=$contentWidth", ) } @@ -379,12 +446,13 @@ class InlineLayoutTests { private fun assertContainerChildrenFit(container: ContainerNode) { if (container.children.isEmpty()) return val contentBottom = container.bounds.height - container.border.bottom - container.padding.bottom - val usedBottom = container.children.maxOf { child -> - (child.bounds.y - container.bounds.y) + child.bounds.height + child.margin.bottom - } + val usedBottom = + container.children.maxOf { child -> + (child.bounds.y - container.bounds.y) + child.bounds.height + child.margin.bottom + } assertTrue( usedBottom <= contentBottom, - "Container child content should fit: key=${container.key} used=$usedBottom contentBottom=$contentBottom" + "Container child content should fit: key=${container.key} used=$usedBottom contentBottom=$contentBottom", ) } @@ -396,11 +464,12 @@ class InlineLayoutTests { .coerceAtLeast(1) } - private fun testMeasureContext(): UiMeasureContext { - return object : UiMeasureContext { + private fun testMeasureContext(): UiMeasureContext = + object : UiMeasureContext { override val fontHeight: Int = 8 + override fun measureText(text: String): Int = text.length * 6 + override fun paint(commands: List) = Unit } - } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/LayoutValidatorTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/LayoutValidatorTests.kt index 6bc16c6..0730da7 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/LayoutValidatorTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/LayoutValidatorTests.kt @@ -1,8 +1,5 @@ package org.dreamfinity.dsgl.core.dom.elements -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.debug.LayoutDebug @@ -13,6 +10,9 @@ import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.Size import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.render.RenderCommand +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue class LayoutValidatorTests { @Test @@ -22,14 +22,15 @@ class LayoutValidatorTests { LayoutDebug.strictBounds = false val ctx = testMeasureContext() - val root = ContainerNode(key = "root").apply { - width = 28 - padding = Insets.all(2) - border = Border.all(1, 0xFF000000.toInt()) - } + val root = + ContainerNode(key = "root").apply { + width = 28 + padding = Insets.all(2) + border = Border.all(1, 0xFF000000.toInt()) + } TextNode( textSource = TextSource.Static("this is a long sentence that must wrap"), - key = "child.text" + key = "child.text", ).apply { margin = Insets(1, 1, 1, 1) }.applyParent(root) @@ -40,7 +41,7 @@ class LayoutValidatorTests { val violations = LayoutValidator.validate(root, ctx) assertTrue( violations.none { it.code == "CHILD_OUTSIDE_PARENT_CONTENT" }, - "Expected child to stay inside parent content, got: $violations" + "Expected child to stay inside parent content, got: $violations", ) } @@ -51,11 +52,13 @@ class LayoutValidatorTests { LayoutDebug.strictBounds = false val ctx = testMeasureContext() - val root = ContainerNode(key = "root.wrap").apply { - width = 32 - gap = 1 - } - val first = TextNode(TextSource.Static("first wrapped text line that should occupy multiple rows"), key = "text.a") + val root = + ContainerNode(key = "root.wrap").apply { + width = 32 + gap = 1 + } + val first = + TextNode(TextSource.Static("first wrapped text line that should occupy multiple rows"), key = "text.a") val second = TextNode(TextSource.Static("second wrapped text line must be below the first one"), key = "text.b") first.applyParent(root) second.applyParent(root) @@ -65,12 +68,12 @@ class LayoutValidatorTests { assertTrue( second.bounds.y >= first.bounds.y + first.bounds.height, - "Second text node should start after first node bottom." + "Second text node should start after first node bottom.", ) val violations = LayoutValidator.validate(root, ctx) assertTrue( violations.none { it.code == "TEXT_HEIGHT_UNDERSIZED" || it.code == "TEXT_LINE_COLLISION" }, - "Wrapped text must not produce line-stack violations: $violations" + "Wrapped text must not produce line-stack violations: $violations", ) } @@ -81,12 +84,13 @@ class LayoutValidatorTests { LayoutDebug.strictBounds = false val ctx = testMeasureContext() - val root = ContainerNode(key = "root.rogue").apply { - width = 20 - height = 10 - padding = Insets.all(1) - border = Border.all(1, 0xFF000000.toInt()) - } + val root = + ContainerNode(key = "root.rogue").apply { + width = 20 + height = 10 + padding = Insets.all(1) + border = Border.all(1, 0xFF000000.toInt()) + } RogueNode(key = "rogue").applyParent(root) val measured = root.measure(ctx) @@ -96,18 +100,27 @@ class LayoutValidatorTests { assertEquals(1, violations.count { it.code == "CHILD_OUTSIDE_PARENT_CONTENT" }) } - private fun testMeasureContext(): UiMeasureContext { - return object : UiMeasureContext { + private fun testMeasureContext(): UiMeasureContext = + object : UiMeasureContext { override val fontHeight: Int = 8 + override fun measureText(text: String): Int = text.length * 4 + override fun paint(commands: List) = Unit } - } - private class RogueNode(key: Any?) : DOMNode(key) { + private class RogueNode( + key: Any?, + ) : DOMNode(key) { override fun measure(ctx: UiMeasureContext): Size = Size(6, 4) - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x + 100, y, width, height) } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/SizeConstraintLayoutTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/SizeConstraintLayoutTests.kt index fdf7c31..4491852 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/SizeConstraintLayoutTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/SizeConstraintLayoutTests.kt @@ -4,20 +4,25 @@ import org.dreamfinity.dsgl.core.DomTree import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.render.RenderCommand -import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.Display +import org.dreamfinity.dsgl.core.style.StyleEngine import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals class SizeConstraintLayoutTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 10 - override fun measureText(text: String): Int = text.length * 6 - override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * ((fontSize ?: 10) / 2).coerceAtLeast(1) - override fun fontHeight(fontId: String?, fontSize: Int?): Int = fontSize ?: 10 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 10 + + override fun measureText(text: String): Int = text.length * 6 + + override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * ((fontSize ?: 10) / 2).coerceAtLeast(1) + + override fun fontHeight(fontId: String?, fontSize: Int?): Int = fontSize ?: 10 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextLayoutEngineTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextLayoutEngineTests.kt index fa5faf7..2b6c272 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextLayoutEngineTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextLayoutEngineTests.kt @@ -11,13 +11,14 @@ class TextLayoutEngineTests { @Test fun `wrap mode breaks lines by width`() { - val layout = TextLayoutEngine.layout( - text = "one two three", - maxWidth = 5, - wrap = TextWrap.Wrap, - fontHeight = 9, - measureText = measure - ) + val layout = + TextLayoutEngine.layout( + text = "one two three", + maxWidth = 5, + wrap = TextWrap.Wrap, + fontHeight = 9, + measureText = measure, + ) assertTrue(layout.lines.size > 1) assertTrue(layout.lines.all { it.width <= 5 }) @@ -26,27 +27,34 @@ class TextLayoutEngineTests { @Test fun `nowrap mode keeps single line`() { - val layout = TextLayoutEngine.layout( - text = "one two three", - maxWidth = 5, - wrap = TextWrap.NoWrap, - fontHeight = 9, - measureText = measure - ) + val layout = + TextLayoutEngine.layout( + text = "one two three", + maxWidth = 5, + wrap = TextWrap.NoWrap, + fontHeight = 9, + measureText = measure, + ) assertEquals(1, layout.lines.size) - assertEquals("one two three", layout.lines.single().text) + assertEquals( + "one two three", + layout.lines + .single() + .text, + ) } @Test fun `wrap mode hard breaks long words`() { - val layout = TextLayoutEngine.layout( - text = "abcdefghij", - maxWidth = 4, - wrap = TextWrap.Wrap, - fontHeight = 10, - measureText = measure - ) + val layout = + TextLayoutEngine.layout( + text = "abcdefghij", + maxWidth = 4, + wrap = TextWrap.Wrap, + fontHeight = 10, + measureText = measure, + ) assertEquals(listOf("abcd", "efgh", "ij"), layout.lines.map { it.text }) assertTrue(layout.lines.all { it.width <= 4 }) @@ -54,13 +62,14 @@ class TextLayoutEngineTests { @Test fun `newline indices are preserved for caret mapping`() { - val layout = TextLayoutEngine.layout( - text = "ab\ncd", - maxWidth = null, - wrap = TextWrap.NoWrap, - fontHeight = 8, - measureText = measure - ) + val layout = + TextLayoutEngine.layout( + text = "ab\ncd", + maxWidth = null, + wrap = TextWrap.NoWrap, + fontHeight = 8, + measureText = measure, + ) assertEquals(2, layout.lines.size) assertEquals(0, layout.lineForCaret(2)) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextLineSpaceReservationBaselineTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextLineSpaceReservationBaselineTests.kt index 7508d79..6c3507d 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextLineSpaceReservationBaselineTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextLineSpaceReservationBaselineTests.kt @@ -12,9 +12,9 @@ import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection import org.dreamfinity.dsgl.core.style.LineHeightValue import org.dreamfinity.dsgl.core.style.Overflow +import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleExpression import org.dreamfinity.dsgl.core.style.StyleProperty -import org.dreamfinity.dsgl.core.style.StyleEngine import kotlin.math.ceil import kotlin.math.roundToInt import kotlin.test.AfterTest @@ -23,51 +23,52 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue class TextLineSpaceReservationBaselineTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 10 + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 10 - override fun measureText(text: String): Int = text.length * 6 + override fun measureText(text: String): Int = text.length * 6 - override fun measureText(text: String, fontId: String?, fontSize: Int?): Int { - val glyphWidth = ((fontSize ?: 16) * 0.5f).roundToInt().coerceAtLeast(1) - return text.length * glyphWidth - } + override fun measureText(text: String, fontId: String?, fontSize: Int?): Int { + val glyphWidth = ((fontSize ?: 16) * 0.5f).roundToInt().coerceAtLeast(1) + return text.length * glyphWidth + } - override fun fontHeight(fontId: String?, fontSize: Int?): Int { - val size = (fontSize ?: 16).coerceAtLeast(1) - return (size * 0.625f).roundToInt().coerceAtLeast(1) + override fun fontHeight(fontId: String?, fontSize: Int?): Int { + val size = (fontSize ?: 16).coerceAtLeast(1) + return (size * 0.625f).roundToInt().coerceAtLeast(1) + } + + override fun paint(commands: List) = Unit } - override fun paint(commands: List) = Unit - } + private val nativeMetricsCtx = + object : UiMeasureContext { + override val fontHeight: Int = 10 - private val nativeMetricsCtx = object : UiMeasureContext { - override val fontHeight: Int = 10 + override fun measureText(text: String): Int = text.length * 6 - override fun measureText(text: String): Int = text.length * 6 + override fun measureText(text: String, fontId: String?, fontSize: Int?): Int { + val glyphWidth = ((fontSize ?: 16) * 0.5f).roundToInt().coerceAtLeast(1) + return text.length * glyphWidth + } - override fun measureText(text: String, fontId: String?, fontSize: Int?): Int { - val glyphWidth = ((fontSize ?: 16) * 0.5f).roundToInt().coerceAtLeast(1) - return text.length * glyphWidth - } + override fun fontHeight(fontId: String?, fontSize: Int?): Int { + val size = (fontSize ?: 16).coerceAtLeast(1) + return (size * 0.625f).roundToInt().coerceAtLeast(1) + } - override fun fontHeight(fontId: String?, fontSize: Int?): Int { - val size = (fontSize ?: 16).coerceAtLeast(1) - return (size * 0.625f).roundToInt().coerceAtLeast(1) - } + override fun fontLineMetrics(fontId: String?, fontSize: Int?): FontLineMetrics = + FontLineMetrics( + emSize = 1f, + lineHeightEm = 0.9166667f, + ascenderEm = 0.75f, + descenderEm = -0.16666667f, + ) - override fun fontLineMetrics(fontId: String?, fontSize: Int?): FontLineMetrics { - return FontLineMetrics( - emSize = 1f, - lineHeightEm = 0.9166667f, - ascenderEm = 0.75f, - descenderEm = -0.16666667f - ) + override fun paint(commands: List) = Unit } - override fun paint(commands: List) = Unit - } - @AfterTest fun cleanup() { StyleEngine.clearAllInspectorOverrides() @@ -76,9 +77,10 @@ class TextLineSpaceReservationBaselineTests { @Test fun `text node intrinsic height baseline uses explicit normal line-height rule`() { - val node = TextNode(TextSource.Static("single line"), key = "text.single").apply { - fontSize = 16 - } + val node = + TextNode(TextSource.Static("single line"), key = "text.single").apply { + fontSize = 16 + } val measured = node.measure(ctx) @@ -87,21 +89,25 @@ class TextLineSpaceReservationBaselineTests { @Test fun `div with span text baseline reserves one normal line-height line`() { - val div = ContainerNode(key = "div.root").apply { - display = Display.Block - width = 320 - applyStyle { - fontSize = 20.px - } - } - val span = ContainerNode(key = "span.inline").apply { - display = Display.Inline - }.applyParent(div) - TextNode(TextSource.Static("row"), key = "span.text").apply { - applyStyle { - fontSize = 12.px + val div = + ContainerNode(key = "div.root").apply { + display = Display.Block + width = 320 + applyStyle { + fontSize = 20.px + } } - }.applyParent(span) + val span = + ContainerNode(key = "span.inline") + .apply { + display = Display.Inline + }.applyParent(div) + TextNode(TextSource.Static("row"), key = "span.text") + .apply { + applyStyle { + fontSize = 12.px + } + }.applyParent(span) val tree = DomTree(div) tree.render(ctx, 320, 120) @@ -111,7 +117,7 @@ class TextLineSpaceReservationBaselineTests { assertEquals(expectedContainerLineHeight, div.measure(ctx).height) assertTrue( span.bounds.height > expectedNormalLineHeightPx(12), - "Container reserves a line box from its own computed line-height baseline, not only child text height." + "Container reserves a line box from its own computed line-height baseline, not only child text height.", ) } @@ -120,24 +126,28 @@ class TextLineSpaceReservationBaselineTests { val rowCount = 20 val containerFontSize = 20 val textFontSize = 12 - val root = ContainerNode(key = "rows.root").apply { - display = Display.Flex - flexDirection = FlexDirection.Column - width = 320 - gap = 0 - } + val root = + ContainerNode(key = "rows.root").apply { + display = Display.Flex + flexDirection = FlexDirection.Column + width = 320 + gap = 0 + } repeat(rowCount) { index -> - val row = ContainerNode(key = "row.$index").apply { - display = Display.Block - applyStyle { - fontSize = containerFontSize.px - } - }.applyParent(root) - TextNode(TextSource.Static("row $index"), key = "row.$index.text").apply { - applyStyle { - fontSize = textFontSize.px - } - }.applyParent(row) + val row = + ContainerNode(key = "row.$index") + .apply { + display = Display.Block + applyStyle { + fontSize = containerFontSize.px + } + }.applyParent(root) + TextNode(TextSource.Static("row $index"), key = "row.$index.text") + .apply { + applyStyle { + fontSize = textFontSize.px + } + }.applyParent(row) } val tree = DomTree(root) @@ -154,35 +164,41 @@ class TextLineSpaceReservationBaselineTests { @Test fun `minHeight 1em workaround is not required for ordinary text row reservation`() { - val root = ContainerNode(key = "root").apply { - display = Display.Block - width = 320 - } - val ordinaryRow = ContainerNode(key = "row.ordinary").apply { - display = Display.Block - applyStyle { - fontSize = 20.px + val root = + ContainerNode(key = "root").apply { + display = Display.Block + width = 320 } - } - TextNode(TextSource.Static("row"), key = "row.ordinary.text").apply { - applyStyle { - fontSize = 12.px + val ordinaryRow = + ContainerNode(key = "row.ordinary").apply { + display = Display.Block + applyStyle { + fontSize = 20.px + } } - }.applyParent(ordinaryRow) + TextNode(TextSource.Static("row"), key = "row.ordinary.text") + .apply { + applyStyle { + fontSize = 12.px + } + }.applyParent(ordinaryRow) ordinaryRow.applyParent(root) - val withWorkaround = ContainerNode(key = "row.with").apply { - display = Display.Block - applyStyle { - fontSize = 20.px - minHeight = 1.em - } - }.applyParent(root) - TextNode(TextSource.Static("row"), key = "row.with.text").apply { - applyStyle { - fontSize = 12.px - } - }.applyParent(withWorkaround) + val withWorkaround = + ContainerNode(key = "row.with") + .apply { + display = Display.Block + applyStyle { + fontSize = 20.px + minHeight = 1.em + } + }.applyParent(root) + TextNode(TextSource.Static("row"), key = "row.with.text") + .apply { + applyStyle { + fontSize = 12.px + } + }.applyParent(withWorkaround) val tree = DomTree(root) tree.render(ctx, 320, 120) @@ -191,11 +207,11 @@ class TextLineSpaceReservationBaselineTests { assertEquals(expectedOrdinaryLineBoxHeight, ordinaryRow.bounds.height) assertTrue( ordinaryRow.bounds.height >= expectedOrdinaryLineBoxHeight, - "Ordinary text rows now meet line-box reservation without minHeight workaround." + "Ordinary text rows now meet line-box reservation without minHeight workaround.", ) assertTrue( withWorkaround.bounds.height >= ordinaryRow.bounds.height, - "Workaround may still increase explicit minimums, but ordinary line-box reservation no longer depends on it." + "Workaround may still increase explicit minimums, but ordinary line-box reservation no longer depends on it.", ) } @@ -203,24 +219,29 @@ class TextLineSpaceReservationBaselineTests { fun `scroll content height grows naturally from stacked text rows`() { val rowCount = 30 val rowFontSize = 20 - val root = ContainerNode(key = "scroll.viewport").apply { - display = Display.Block - width = 220 - height = 80 - overflowY = Overflow.Scroll - } - val list = ContainerNode(key = "scroll.content").apply { - display = Display.Flex - flexDirection = FlexDirection.Column - width = 220 - }.applyParent(root) - repeat(rowCount) { index -> - val row = ContainerNode(key = "scroll.row.$index").apply { + val root = + ContainerNode(key = "scroll.viewport").apply { display = Display.Block - applyStyle { - fontSize = rowFontSize.px - } - }.applyParent(list) + width = 220 + height = 80 + overflowY = Overflow.Scroll + } + val list = + ContainerNode(key = "scroll.content") + .apply { + display = Display.Flex + flexDirection = FlexDirection.Column + width = 220 + }.applyParent(root) + repeat(rowCount) { index -> + val row = + ContainerNode(key = "scroll.row.$index") + .apply { + display = Display.Block + applyStyle { + fontSize = rowFontSize.px + } + }.applyParent(list) TextNode(TextSource.Static("item $index"), key = "scroll.row.$index.text").applyParent(row) } @@ -235,12 +256,13 @@ class TextLineSpaceReservationBaselineTests { @Test fun `explicit line-height override drives text node intrinsic height`() { - val node = TextNode(TextSource.Static("single line"), key = "text.override").apply { - fontSize = 16 - applyStyle { - lineHeight = LineHeightValue.Length(24.px) + val node = + TextNode(TextSource.Static("single line"), key = "text.override").apply { + fontSize = 16 + applyStyle { + lineHeight = LineHeightValue.Length(24.px) + } } - } StyleEngine.applyStylesRecursively(node) val measured = node.measure(ctx) @@ -250,22 +272,26 @@ class TextLineSpaceReservationBaselineTests { @Test fun `explicit line-height override affects container reserved row height`() { - val root = ContainerNode(key = "line-height.root").apply { - display = Display.Block - width = 320 - } - val row = ContainerNode(key = "line-height.row").apply { - display = Display.Block - applyStyle { - fontSize = 20.px - lineHeight = LineHeightValue.Length(26.px) - } - }.applyParent(root) - TextNode(TextSource.Static("row"), key = "line-height.text").apply { - applyStyle { - fontSize = 12.px + val root = + ContainerNode(key = "line-height.root").apply { + display = Display.Block + width = 320 } - }.applyParent(row) + val row = + ContainerNode(key = "line-height.row") + .apply { + display = Display.Block + applyStyle { + fontSize = 20.px + lineHeight = LineHeightValue.Length(26.px) + } + }.applyParent(root) + TextNode(TextSource.Static("row"), key = "line-height.text") + .apply { + applyStyle { + fontSize = 12.px + } + }.applyParent(row) val tree = DomTree(root) tree.render(ctx, 320, 120) @@ -275,16 +301,17 @@ class TextLineSpaceReservationBaselineTests { @Test fun `text width measurement remains stable when line-height changes`() { - val node = TextNode(TextSource.Static("width-check"), key = "text.width").apply { - fontSize = 16 - } + val node = + TextNode(TextSource.Static("width-check"), key = "text.width").apply { + fontSize = 16 + } StyleEngine.applyStylesRecursively(node) val baselineWidth = node.measure(ctx).width node.inlineStyleDeclarations.set( StyleProperty.LINE_HEIGHT, - StyleExpression.Literal("32px") + StyleExpression.Literal("32px"), ) StyleEngine.clearCache() StyleEngine.applyStylesRecursively(node) @@ -295,18 +322,22 @@ class TextLineSpaceReservationBaselineTests { @Test fun `inspector line-height override affects measured text intrinsic height`() { - val root = ContainerNode(key = "root").apply { - display = Display.Block - } - val node = TextNode(TextSource.Static("inspector"), key = "text.inspector").apply { - fontSize = 16 - }.applyParent(root) + val root = + ContainerNode(key = "root").apply { + display = Display.Block + } + val node = + TextNode(TextSource.Static("inspector"), key = "text.inspector") + .apply { + fontSize = 16 + }.applyParent(root) StyleEngine.applyStylesRecursively(root) val baselineHeight = node.measure(ctx).height assertEquals(expectedNormalLineHeightPx(16), baselineHeight) - StyleEngine.setInspectorOverrideLiteral(node, StyleProperty.LINE_HEIGHT, "22px") + StyleEngine + .setInspectorOverrideLiteral(node, StyleProperty.LINE_HEIGHT, "22px") .getOrThrow() StyleEngine.applyStylesRecursively(root) val overriddenHeight = node.measure(ctx).height @@ -317,18 +348,25 @@ class TextLineSpaceReservationBaselineTests { @Test fun `non-text child container sizing remains unchanged`() { - val root = ContainerNode(key = "non-text.root").apply { - display = Display.Block - width = 200 - } - val row = ContainerNode(key = "non-text.row").apply { - display = Display.Block - applyStyle { - fontSize = 20.px + val root = + ContainerNode(key = "non-text.root").apply { + display = Display.Block + width = 200 } - }.applyParent(root) - ImageNode(url = "minecraft:textures/blocks/stone.png", imageWidth = 40, imageHeight = 30, key = "non-text.image") - .applyParent(row) + val row = + ContainerNode(key = "non-text.row") + .apply { + display = Display.Block + applyStyle { + fontSize = 20.px + } + }.applyParent(root) + ImageNode( + url = "minecraft:textures/blocks/stone.png", + imageWidth = 40, + imageHeight = 30, + key = "non-text.image", + ).applyParent(row) val tree = DomTree(root) tree.render(ctx, 200, 80) @@ -338,17 +376,20 @@ class TextLineSpaceReservationBaselineTests { @Test fun `font-size em resolution uses inherited semantic base`() { - val root = ContainerNode(key = "font-size.base.root").apply { - display = Display.Block - applyStyle { - fontSize = 20.px - } - } - val text = TextNode(TextSource.Static("em"), key = "font-size.base.text").apply { - applyStyle { - fontSize = 2.em + val root = + ContainerNode(key = "font-size.base.root").apply { + display = Display.Block + applyStyle { + fontSize = 20.px + } } - }.applyParent(root) + val text = + TextNode(TextSource.Static("em"), key = "font-size.base.text") + .apply { + applyStyle { + fontSize = 2.em + } + }.applyParent(root) val tree = DomTree(root) tree.render(ctx, 320, 120) @@ -360,20 +401,24 @@ class TextLineSpaceReservationBaselineTests { @Test fun `changing text font-size changes parent reserved row height in ordinary case`() { fun reservedHeightFor(emValue: Float): Int { - val root = ContainerNode(key = "font-size.grow.root.$emValue").apply { - display = Display.Block - width = 320 - } - val row = ContainerNode(key = "font-size.grow.row.$emValue").apply { - display = Display.Block - padding = Insets.all(1) - border = Border.all(1, 0xFF617A90.toInt()) - }.applyParent(root) - TextNode(TextSource.Static("grow"), key = "font-size.grow.text.$emValue").apply { - applyStyle { - fontSize = emValue.em + val root = + ContainerNode(key = "font-size.grow.root.$emValue").apply { + display = Display.Block + width = 320 } - }.applyParent(row) + val row = + ContainerNode(key = "font-size.grow.row.$emValue") + .apply { + display = Display.Block + padding = Insets.all(1) + border = Border.all(1, 0xFF617A90.toInt()) + }.applyParent(root) + TextNode(TextSource.Static("grow"), key = "font-size.grow.text.$emValue") + .apply { + applyStyle { + fontSize = emValue.em + } + }.applyParent(row) val tree = DomTree(root) tree.render(ctx, 320, 120) return row.bounds.height @@ -389,25 +434,29 @@ class TextLineSpaceReservationBaselineTests { @Test fun `demo-like flex overflow audit keeps measured text reservation and produces scroll`() { val viewportHeight = 120 - val root = ContainerNode(key = "demo.audit.root").apply { - display = Display.Flex - flexDirection = FlexDirection.Column - width = 260 - height = viewportHeight - overflowY = Overflow.Auto - gap = 0 - } + val root = + ContainerNode(key = "demo.audit.root").apply { + display = Display.Flex + flexDirection = FlexDirection.Column + width = 260 + height = viewportHeight + overflowY = Overflow.Auto + gap = 0 + } repeat(20) { index -> - val row = ContainerNode(key = "demo.audit.row.$index").apply { - display = Display.Block - padding = Insets.all(1) - border = Border.all(1, 0xFF617A90.toInt()) - }.applyParent(root) - TextNode(TextSource.Static("Hi there, #$index"), key = "demo.audit.text.$index").apply { - applyStyle { - fontSize = 10.em - } - }.applyParent(row) + val row = + ContainerNode(key = "demo.audit.row.$index") + .apply { + display = Display.Block + padding = Insets.all(1) + border = Border.all(1, 0xFF617A90.toInt()) + }.applyParent(root) + TextNode(TextSource.Static("Hi there, #$index"), key = "demo.audit.text.$index") + .apply { + applyStyle { + fontSize = 10.em + } + }.applyParent(row) } val tree = DomTree(root) @@ -439,13 +488,16 @@ class TextLineSpaceReservationBaselineTests { @Test fun `single source of truth keeps effective font size consistent across layout measurement and render command`() { - val root = ContainerNode(key = "single.source.root").apply { - display = Display.Block - applyStyle { fontSize = 16.px } - } - val textNode = TextNode(TextSource.Static("sync"), key = "single.source.text").apply { - applyStyle { fontSize = 10.em } - }.applyParent(root) + val root = + ContainerNode(key = "single.source.root").apply { + display = Display.Block + applyStyle { fontSize = 16.px } + } + val textNode = + TextNode(TextSource.Static("sync"), key = "single.source.text") + .apply { + applyStyle { fontSize = 10.em } + }.applyParent(root) val tree = DomTree(root) tree.render(ctx, 320, 120) @@ -464,16 +516,23 @@ class TextLineSpaceReservationBaselineTests { @Test fun `large font size growth increases width height and render size consistently`() { - data class Snapshot(val width: Int, val height: Int, val renderFontSize: Int) + data class Snapshot( + val width: Int, + val height: Int, + val renderFontSize: Int, + ) fun snapshotFor(emValue: Float): Snapshot { - val root = ContainerNode(key = "single.source.growth.root.$emValue").apply { - display = Display.Block - applyStyle { fontSize = 16.px } - } - val textNode = TextNode(TextSource.Static("MMMM"), key = "single.source.growth.text.$emValue").apply { - applyStyle { fontSize = emValue.em } - }.applyParent(root) + val root = + ContainerNode(key = "single.source.growth.root.$emValue").apply { + display = Display.Block + applyStyle { fontSize = 16.px } + } + val textNode = + TextNode(TextSource.Static("MMMM"), key = "single.source.growth.text.$emValue") + .apply { + applyStyle { fontSize = emValue.em } + }.applyParent(root) val tree = DomTree(root) tree.render(ctx, 400, 200) val measured = textNode.measure(ctx) @@ -483,7 +542,7 @@ class TextLineSpaceReservationBaselineTests { return Snapshot( width = measured.width, height = measured.height, - renderFontSize = draw.fontSize ?: -1 + renderFontSize = draw.fontSize ?: -1, ) } @@ -500,13 +559,16 @@ class TextLineSpaceReservationBaselineTests { @Test fun `no hidden clamp on authoritative text path for large computed font size`() { - val root = ContainerNode(key = "single.source.noclamp.root").apply { - display = Display.Block - applyStyle { fontSize = 16.px } - } - val textNode = TextNode(TextSource.Static("MMMM"), key = "single.source.noclamp.text").apply { - applyStyle { fontSize = 20.em } - }.applyParent(root) + val root = + ContainerNode(key = "single.source.noclamp.root").apply { + display = Display.Block + applyStyle { fontSize = 16.px } + } + val textNode = + TextNode(TextSource.Static("MMMM"), key = "single.source.noclamp.text") + .apply { + applyStyle { fontSize = 20.em } + }.applyParent(root) val tree = DomTree(root) tree.render(ctx, 640, 240) @@ -525,9 +587,10 @@ class TextLineSpaceReservationBaselineTests { @Test fun `normal line-height uses native metrics when available`() { - val node = TextNode(TextSource.Static("native"), key = "native.metrics.normal").apply { - fontSize = 16 - } + val node = + TextNode(TextSource.Static("native"), key = "native.metrics.normal").apply { + fontSize = 16 + } StyleEngine.applyStylesRecursively(node) val measured = node.measure(nativeMetricsCtx) @@ -542,12 +605,14 @@ class TextLineSpaceReservationBaselineTests { @Test fun `native ascender descender and line-height scale with font size`() { - val small = TextNode(TextSource.Static("small"), key = "native.metrics.small").apply { - fontSize = 16 - } - val large = TextNode(TextSource.Static("large"), key = "native.metrics.large").apply { - fontSize = 32 - } + val small = + TextNode(TextSource.Static("small"), key = "native.metrics.small").apply { + fontSize = 16 + } + val large = + TextNode(TextSource.Static("large"), key = "native.metrics.large").apply { + fontSize = 32 + } val smallLine = invokeProtectedInt(small, "resolveEffectiveLineHeight", nativeMetricsCtx) val largeLine = invokeProtectedInt(large, "resolveEffectiveLineHeight", nativeMetricsCtx) @@ -565,16 +630,19 @@ class TextLineSpaceReservationBaselineTests { @Test fun `explicit line-height adds symmetric leading for text draw origin`() { - val root = ContainerNode(key = "native.leading.root").apply { - display = Display.Block - width = 320 - } - val node = TextNode(TextSource.Static("lead"), key = "native.leading.text").apply { - fontSize = 16 - applyStyle { - lineHeight = LineHeightValue.Length(24.px) + val root = + ContainerNode(key = "native.leading.root").apply { + display = Display.Block + width = 320 } - }.applyParent(root) + val node = + TextNode(TextSource.Static("lead"), key = "native.leading.text") + .apply { + fontSize = 16 + applyStyle { + lineHeight = LineHeightValue.Length(24.px) + } + }.applyParent(root) val tree = DomTree(root) tree.render(nativeMetricsCtx, 320, 80) @@ -591,16 +659,19 @@ class TextLineSpaceReservationBaselineTests { @Test fun `render line advance stays coherent with measured line-height`() { - val root = ContainerNode(key = "native.coherence.root").apply { - display = Display.Block - width = 320 - } - val node = TextNode(TextSource.Static("a\nb\nc"), key = "native.coherence.text").apply { - fontSize = 16 - applyStyle { - lineHeight = LineHeightValue.Length(24.px) + val root = + ContainerNode(key = "native.coherence.root").apply { + display = Display.Block + width = 320 } - }.applyParent(root) + val node = + TextNode(TextSource.Static("a\nb\nc"), key = "native.coherence.text") + .apply { + fontSize = 16 + applyStyle { + lineHeight = LineHeightValue.Length(24.px) + } + }.applyParent(root) val tree = DomTree(root) tree.render(nativeMetricsCtx, 320, 160) @@ -617,18 +688,22 @@ class TextLineSpaceReservationBaselineTests { @Test fun `native metrics path keeps ordinary row reservation non-collapsed`() { - val root = ContainerNode(key = "native.reservation.root").apply { - display = Display.Block - width = 260 - } - val row = ContainerNode(key = "native.reservation.row").apply { - display = Display.Block - }.applyParent(root) - TextNode(TextSource.Static("row"), key = "native.reservation.text").apply { - applyStyle { - fontSize = 10.em + val root = + ContainerNode(key = "native.reservation.root").apply { + display = Display.Block + width = 260 } - }.applyParent(row) + val row = + ContainerNode(key = "native.reservation.row") + .apply { + display = Display.Block + }.applyParent(root) + TextNode(TextSource.Static("row"), key = "native.reservation.text") + .apply { + applyStyle { + fontSize = 10.em + } + }.applyParent(row) val tree = DomTree(root) tree.render(nativeMetricsCtx, 260, 120) @@ -644,9 +719,7 @@ class TextLineSpaceReservationBaselineTests { .coerceAtLeast(1) } - private fun expectedNativeNormalLineHeightPx(fontSize: Int): Int { - return ceil(0.9166667f * fontSize).toInt().coerceAtLeast(1) - } + private fun expectedNativeNormalLineHeightPx(fontSize: Int): Int = ceil(0.9166667f * fontSize).toInt().coerceAtLeast(1) private fun invokeProtectedInt(node: DOMNode, methodName: String, ctx: UiMeasureContext): Int { val method = DOMNode::class.java.getDeclaredMethod(methodName, UiMeasureContext::class.java) @@ -659,5 +732,4 @@ class TextLineSpaceReservationBaselineTests { method.isAccessible = true return method.invoke(node, ctx) as Float } - } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextPerformanceHotPathCharacterizationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextPerformanceHotPathCharacterizationTests.kt index e0dd07d..844b8fc 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextPerformanceHotPathCharacterizationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextPerformanceHotPathCharacterizationTests.kt @@ -1,10 +1,10 @@ package org.dreamfinity.dsgl.core.dom.elements import org.dreamfinity.dsgl.core.DomTree +import org.dreamfinity.dsgl.core.dom.UsedInteractionGeometryResolver import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.support.MeasuredTextRangeWidthSource import org.dreamfinity.dsgl.core.dom.elements.support.TextLayoutEngine -import org.dreamfinity.dsgl.core.dom.UsedInteractionGeometryResolver import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.collectHoverChain import org.dreamfinity.dsgl.core.font.FontRegistry @@ -16,49 +16,43 @@ import org.dreamfinity.dsgl.core.style.Overflow import org.dreamfinity.dsgl.core.style.TextWrap import org.dreamfinity.dsgl.core.text.TextStyleFlags import java.awt.Font -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue +import kotlin.test.* class TextPerformanceHotPathCharacterizationTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 10 + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 10 + + override fun measureText(text: String): Int = FontRegistry.measureText(text, FontRegistry.FONT_MINECRAFT, FontRegistry.DEFAULT_FONT_SIZE) + + override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = FontRegistry.measureText(text, fontId, fontSize) + + override fun measureTextRange( + text: String, + startIndex: Int, + endIndexExclusive: Int, + fontId: String?, + fontSize: Int?, + ): Int { + val shaped = + FontRegistry.shapeTextRange( + text = text, + startIndex = startIndex, + endIndexExclusive = endIndexExclusive, + fontId = fontId, + fontSize = fontSize, + formattingMode = "plain", + ) + return shaped.width + .toInt() + .coerceAtLeast(0) + } - override fun measureText(text: String): Int { - return FontRegistry.measureText(text, FontRegistry.FONT_MINECRAFT, FontRegistry.DEFAULT_FONT_SIZE) - } + override fun fontHeight(fontId: String?, fontSize: Int?): Int = FontRegistry.lineHeight(fontId, fontSize) - override fun measureText(text: String, fontId: String?, fontSize: Int?): Int { - return FontRegistry.measureText(text, fontId, fontSize) + override fun paint(commands: List) = Unit } - override fun measureTextRange( - text: String, - startIndex: Int, - endIndexExclusive: Int, - fontId: String?, - fontSize: Int? - ): Int { - val shaped = FontRegistry.shapeTextRange( - text = text, - startIndex = startIndex, - endIndexExclusive = endIndexExclusive, - fontId = fontId, - fontSize = fontSize, - formattingMode = "plain" - ) - return shaped.width.toInt().coerceAtLeast(0) - } - - override fun fontHeight(fontId: String?, fontSize: Int?): Int { - return FontRegistry.lineHeight(fontId, fontSize) - } - - override fun paint(commands: List) = Unit - } - @BeforeTest fun resetInstrumentation() { FontRegistry.clearLoadedCache() @@ -94,7 +88,7 @@ class TextPerformanceHotPathCharacterizationTests { val fallbackOnlyCodepoint = findFallbackOnlyCodepoint(primary, fallback) val fallbackOnlyChar = String(Character.toChars(fallbackOnlyCodepoint)) val missingChar = String(Character.toChars(0x10FFFF)) - val text = "CacheProbe-${fallbackOnlyChar}-${missingChar}-X" + val text = "CacheProbe-$fallbackOnlyChar-$missingChar-X" FontRegistry.shapeText(text, FontRegistry.FONT_MINECRAFT, 16, formattingMode = "cache-probe-first") val first = FontRegistry.textHotPathStats() @@ -151,7 +145,13 @@ class TextPerformanceHotPathCharacterizationTests { val optimized = optimizedWrappedLayout(text = text, width = 120, fontSize = 16) assertEquals(legacy.lines.map { it.text }, optimized.lines.map { it.text }) - assertEquals(legacy.lines.map { it.startIndex to it.endIndexExclusive }, optimized.lines.map { it.startIndex to it.endIndexExclusive }) + assertEquals( + legacy.lines.map { it.startIndex to it.endIndexExclusive }, + optimized.lines.map { + it.startIndex to + it.endIndexExclusive + }, + ) assertEquals(legacy.lines.map { it.width }, optimized.lines.map { it.width }) assertEquals(legacy.maxLineWidth, optimized.maxLineWidth) assertEquals(legacy.totalHeight, optimized.totalHeight) @@ -159,22 +159,25 @@ class TextPerformanceHotPathCharacterizationTests { @Test fun `wrapped text node path uses cache-keyed range source instead of unconditional bypass`() { - val root = ContainerNode(key = "text.wrap.root").apply { - display = Display.Block - width = 180 - height = 120 - } - val node = TextNode( - textSource = TextSource.Static( - "Wrapped text baseline path should repeatedly measure many ranges while fitting lines for wrapping." - ), - key = "text.hotpath.wrap" - ).apply { - width = 120 - fontId = FontRegistry.FONT_MINECRAFT - fontSize = 16 - textWrap = TextWrap.Wrap - }.applyParent(root) + val root = + ContainerNode(key = "text.wrap.root").apply { + display = Display.Block + width = 180 + height = 120 + } + val node = + TextNode( + textSource = + TextSource.Static( + "Wrapped text baseline path should repeatedly measure many ranges while fitting lines for wrapping.", + ), + key = "text.hotpath.wrap", + ).apply { + width = 120 + fontId = FontRegistry.FONT_MINECRAFT + fontSize = 16 + textWrap = TextWrap.Wrap + }.applyParent(root) val tree = DomTree(root) tree.render(ctx, 180, 120) @@ -192,23 +195,24 @@ class TextPerformanceHotPathCharacterizationTests { assertTrue(layoutStats.finalLineTextSubstringCalls > 0) assertEquals( layoutStats.finalLineTextSubstringCalls + - layoutStats.probeMeasureSubstringCalls + - layoutStats.temporarySegmentSubstringCalls, - layoutStats.substringSliceCalls + layoutStats.probeMeasureSubstringCalls + + layoutStats.temporarySegmentSubstringCalls, + layoutStats.substringSliceCalls, ) assertTrue(fontStats.shapeTextRangeCalls > 0) } @Test fun `wrapped button path uses range measurement without fallback substring measurement`() { - val root = ContainerNode(key = "button.wrap.root").apply { - display = Display.Block - width = 220 - height = 120 - } + val root = + ContainerNode(key = "button.wrap.root").apply { + display = Display.Block + width = 220 + height = 120 + } ButtonNode( text = "Button wrapping should use authoritative range measurement over substring-based fallback.", - key = "button.wrap.hotpath" + key = "button.wrap.hotpath", ).apply { width = 140 fontId = FontRegistry.FONT_MINECRAFT @@ -232,14 +236,15 @@ class TextPerformanceHotPathCharacterizationTests { @Test fun `wrapped optimized path limits substring materialization to final line output boundary`() { val text = "Final materialization boundary should keep hot internal probes on index ranges." - val source = MeasuredTextRangeWidthSource( - plainText = text, - fontId = FontRegistry.FONT_MINECRAFT, - fontSizePx = 16, - baseFlags = baseTextFlags(), - spans = emptyList(), - ctx = ctx - ) + val source = + MeasuredTextRangeWidthSource( + plainText = text, + fontId = FontRegistry.FONT_MINECRAFT, + fontSizePx = 16, + baseFlags = baseTextFlags(), + spans = emptyList(), + ctx = ctx, + ) TextLayoutEngine.layout( text = text, maxWidth = 90, @@ -247,7 +252,7 @@ class TextPerformanceHotPathCharacterizationTests { fontHeight = FontRegistry.lineHeight(FontRegistry.FONT_MINECRAFT, 16), measureText = { value -> ctx.measureText(value, FontRegistry.FONT_MINECRAFT, 16) }, measureRange = source::measureRange, - measureRangeCacheKey = source.cacheKey + measureRangeCacheKey = source.cacheKey, ) val stats = TextLayoutEngine.hotPathStats() @@ -256,7 +261,7 @@ class TextPerformanceHotPathCharacterizationTests { assertTrue(stats.finalLineTextSubstringCalls > 0) assertEquals( stats.finalLineTextSubstringCalls + stats.probeMeasureSubstringCalls + stats.temporarySegmentSubstringCalls, - stats.substringSliceCalls + stats.substringSliceCalls, ) } @@ -272,14 +277,14 @@ class TextPerformanceHotPathCharacterizationTests { maxWidth = 90, wrap = TextWrap.Wrap, fontHeight = 14, - measureText = measureText + measureText = measureText, ) TextLayoutEngine.layout( text = text, maxWidth = 90, wrap = TextWrap.Wrap, fontHeight = 14, - measureText = measureText + measureText = measureText, ) val cachedStats = TextLayoutEngine.hotPathStats() @@ -302,7 +307,7 @@ class TextPerformanceHotPathCharacterizationTests { wrap = TextWrap.Wrap, fontHeight = 14, measureText = measureText, - measureRange = measureRange + measureRange = measureRange, ) TextLayoutEngine.layout( text = text, @@ -310,7 +315,7 @@ class TextPerformanceHotPathCharacterizationTests { wrap = TextWrap.Wrap, fontHeight = 14, measureText = measureText, - measureRange = measureRange + measureRange = measureRange, ) val rangeStats = TextLayoutEngine.hotPathStats() @@ -321,14 +326,15 @@ class TextPerformanceHotPathCharacterizationTests { TextLayoutEngine.clearCache() TextLayoutEngine.resetHotPathStats() - val rangeSource = MeasuredTextRangeWidthSource( - plainText = text, - fontId = FontRegistry.FONT_MINECRAFT, - fontSizePx = 14, - baseFlags = baseTextFlags(), - spans = emptyList(), - ctx = ctx - ) + val rangeSource = + MeasuredTextRangeWidthSource( + plainText = text, + fontId = FontRegistry.FONT_MINECRAFT, + fontSizePx = 14, + baseFlags = baseTextFlags(), + spans = emptyList(), + ctx = ctx, + ) TextLayoutEngine.layout( text = text, maxWidth = 90, @@ -336,7 +342,7 @@ class TextPerformanceHotPathCharacterizationTests { fontHeight = 14, measureText = measureText, measureRange = rangeSource::measureRange, - measureRangeCacheKey = rangeSource.cacheKey + measureRangeCacheKey = rangeSource.cacheKey, ) TextLayoutEngine.layout( text = text, @@ -345,7 +351,7 @@ class TextPerformanceHotPathCharacterizationTests { fontHeight = 14, measureText = measureText, measureRange = rangeSource::measureRange, - measureRangeCacheKey = rangeSource.cacheKey + measureRangeCacheKey = rangeSource.cacheKey, ) val keyedStats = TextLayoutEngine.hotPathStats() assertEquals(0, keyedStats.cacheBypassedForRangeMeasure) @@ -361,49 +367,54 @@ class TextPerformanceHotPathCharacterizationTests { val measureText: (String) -> Int = { value -> ctx.measureText(value, FontRegistry.FONT_MINECRAFT, 14) } - val source14 = MeasuredTextRangeWidthSource( - plainText = text, - fontId = FontRegistry.FONT_MINECRAFT, - fontSizePx = 14, - baseFlags = baseTextFlags(), - spans = emptyList(), - ctx = ctx - ) - val source18 = MeasuredTextRangeWidthSource( - plainText = text, - fontId = FontRegistry.FONT_MINECRAFT, - fontSizePx = 18, - baseFlags = baseTextFlags(), - spans = emptyList(), - ctx = ctx - ) - val first = TextLayoutEngine.layout( - text = text, - maxWidth = 90, - wrap = TextWrap.Wrap, - fontHeight = FontRegistry.lineHeight(FontRegistry.FONT_MINECRAFT, 14), - measureText = measureText, - measureRange = source14::measureRange, - measureRangeCacheKey = source14.cacheKey - ) - val second = TextLayoutEngine.layout( - text = text, - maxWidth = 90, - wrap = TextWrap.Wrap, - fontHeight = FontRegistry.lineHeight(FontRegistry.FONT_MINECRAFT, 18), - measureText = { value -> ctx.measureText(value, FontRegistry.FONT_MINECRAFT, 18) }, - measureRange = source18::measureRange, - measureRangeCacheKey = source18.cacheKey - ) - val differentWidth = TextLayoutEngine.layout( - text = text, - maxWidth = 70, - wrap = TextWrap.Wrap, - fontHeight = FontRegistry.lineHeight(FontRegistry.FONT_MINECRAFT, 14), - measureText = measureText, - measureRange = source14::measureRange, - measureRangeCacheKey = source14.cacheKey - ) + val source14 = + MeasuredTextRangeWidthSource( + plainText = text, + fontId = FontRegistry.FONT_MINECRAFT, + fontSizePx = 14, + baseFlags = baseTextFlags(), + spans = emptyList(), + ctx = ctx, + ) + val source18 = + MeasuredTextRangeWidthSource( + plainText = text, + fontId = FontRegistry.FONT_MINECRAFT, + fontSizePx = 18, + baseFlags = baseTextFlags(), + spans = emptyList(), + ctx = ctx, + ) + val first = + TextLayoutEngine.layout( + text = text, + maxWidth = 90, + wrap = TextWrap.Wrap, + fontHeight = FontRegistry.lineHeight(FontRegistry.FONT_MINECRAFT, 14), + measureText = measureText, + measureRange = source14::measureRange, + measureRangeCacheKey = source14.cacheKey, + ) + val second = + TextLayoutEngine.layout( + text = text, + maxWidth = 90, + wrap = TextWrap.Wrap, + fontHeight = FontRegistry.lineHeight(FontRegistry.FONT_MINECRAFT, 18), + measureText = { value -> ctx.measureText(value, FontRegistry.FONT_MINECRAFT, 18) }, + measureRange = source18::measureRange, + measureRangeCacheKey = source18.cacheKey, + ) + val differentWidth = + TextLayoutEngine.layout( + text = text, + maxWidth = 70, + wrap = TextWrap.Wrap, + fontHeight = FontRegistry.lineHeight(FontRegistry.FONT_MINECRAFT, 14), + measureText = measureText, + measureRange = source14::measureRange, + measureRangeCacheKey = source14.cacheKey, + ) val stats = TextLayoutEngine.hotPathStats() assertTrue(stats.cacheMisses >= 3) @@ -413,7 +424,9 @@ class TextPerformanceHotPathCharacterizationTests { @Test fun `optimized wrapped path reduces structural hot-path work versus legacy two-pass baseline`() { - val text = "Structural hot-path reduction should be visible when wrapped layout avoids legacy per-pass repeated range shaping." + val text = + "Structural hot-path reduction should be visible when wrapped layout avoids " + + "legacy per-pass repeated range shaping." val legacyStats = runLegacyTwoPassLayout(text = text, width = 120, fontSize = 16) val optimizedStats = runOptimizedTwoPassLayout(text = text, width = 120, fontSize = 16) @@ -427,28 +440,34 @@ class TextPerformanceHotPathCharacterizationTests { @Test fun `small scroll-heavy text scenario keeps semantics and reduces wrapped-measure work`() { - val root = ContainerNode(key = "scroll-hot-root").apply { - display = Display.Block - width = 300 - height = 120 - overflowY = Overflow.Scroll - } - val content = ContainerNode(key = "scroll-hot-content").apply { - display = Display.Flex - flexDirection = FlexDirection.Column - width = 300 - }.applyParent(root) + val root = + ContainerNode(key = "scroll-hot-root").apply { + display = Display.Block + width = 300 + height = 120 + overflowY = Overflow.Scroll + } + val content = + ContainerNode(key = "scroll-hot-content") + .apply { + display = Display.Flex + flexDirection = FlexDirection.Column + width = 300 + }.applyParent(root) repeat(10) { index -> - val row = ContainerNode(key = "scroll-hot-row-$index").apply { - display = Display.Block - width = 280 - }.applyParent(content) + val row = + ContainerNode(key = "scroll-hot-row-$index") + .apply { + display = Display.Block + width = 280 + }.applyParent(content) TextNode( - textSource = TextSource.Static( - "Row $index wraps repeatedly to characterize current range-based text measurement under scroll-heavy updates." - ), - key = "scroll-hot-text-$index" + textSource = + TextSource.Static( + "Row $index wraps repeatedly to characterize current range-based text measurement under scroll-heavy updates.", + ), + key = "scroll-hot-text-$index", ).apply { width = 260 fontId = FontRegistry.FONT_MINECRAFT @@ -477,18 +496,20 @@ class TextPerformanceHotPathCharacterizationTests { assertTrue(shapeCallsDelta >= 0) assertTrue( rangeCallsDelta < layoutBeforeScroll.rangeMeasureCalls, - "Scroll update should not repeat the full initial wrapped range-measure workload" + "Scroll update should not repeat the full initial wrapped range-measure workload", ) assertTrue( shapeCallsDelta < fontBeforeScroll.shapeTextRangeCalls, - "Scroll update should not repeat full wrapped range shaping workload" + "Scroll update should not repeat full wrapped range shaping workload", ) assertTrue(shapeCacheAfterScroll.requests > shapeCacheBeforeScroll.requests) } @Test fun `wrapped layout follow-through uses probing cache without changing line results`() { - val text = "Wrapped probing follow-through should keep the same line results while reducing repeated expensive probing." + val text = + "Wrapped probing follow-through should keep the same line results while reducing " + + "repeated expensive probing." val firstLayout = wrappedLayoutForProbing(text = text, width = 120, fontSize = 16, cacheSalt = "first") val first = FontRegistry.textHotPathStats() assertTrue(first.canDisplayAwtCalls > 0) @@ -499,7 +520,13 @@ class TextPerformanceHotPathCharacterizationTests { val secondLayout = wrappedLayoutForProbing(text = text, width = 120, fontSize = 16, cacheSalt = "second") val second = FontRegistry.textHotPathStats() assertEquals(firstLayout.lines.map { it.text }, secondLayout.lines.map { it.text }) - assertEquals(firstLayout.lines.map { it.startIndex to it.endIndexExclusive }, secondLayout.lines.map { it.startIndex to it.endIndexExclusive }) + assertEquals( + firstLayout.lines.map { it.startIndex to it.endIndexExclusive }, + secondLayout.lines.map { + it.startIndex to + it.endIndexExclusive + }, + ) assertEquals(firstLayout.lines.map { it.width }, secondLayout.lines.map { it.width }) assertEquals(firstLayout.maxLineWidth, secondLayout.maxLineWidth) assertEquals(firstLayout.totalHeight, secondLayout.totalHeight) @@ -510,20 +537,22 @@ class TextPerformanceHotPathCharacterizationTests { @Test fun `interaction and inspector picking remain aligned for wrapped text node`() { - val root = ContainerNode(key = "interaction-root").apply { - display = Display.Block - width = 240 - height = 120 - } - val text = TextNode( - textSource = TextSource.Static("Wrapped interaction smoke test for optimized text path."), - key = "interaction-text" - ).apply { - width = 120 - fontId = FontRegistry.FONT_MINECRAFT - fontSize = 16 - textWrap = TextWrap.Wrap - }.applyParent(root) + val root = + ContainerNode(key = "interaction-root").apply { + display = Display.Block + width = 240 + height = 120 + } + val text = + TextNode( + textSource = TextSource.Static("Wrapped interaction smoke test for optimized text path."), + key = "interaction-text", + ).apply { + width = 120 + fontId = FontRegistry.FONT_MINECRAFT + fontSize = 16 + textWrap = TextWrap.Wrap + }.applyParent(root) val tree = DomTree(root) tree.render(ctx, 240, 120) @@ -543,7 +572,7 @@ class TextPerformanceHotPathCharacterizationTests { private data class ProfileSnapshot( val layoutStats: TextLayoutEngine.HotPathStats, val fontStats: FontRegistry.TextHotPathStats, - val rangeSubstringCalls: Long + val rangeSubstringCalls: Long, ) private fun runLegacyTwoPassLayout(text: String, width: Int, fontSize: Int): ProfileSnapshot { @@ -570,13 +599,13 @@ class TextPerformanceHotPathCharacterizationTests { wrap = TextWrap.Wrap, fontHeight = FontRegistry.lineHeight(FontRegistry.FONT_MINECRAFT, fontSize), measureText = measureText, - measureRange = measureRange + measureRange = measureRange, ) } return ProfileSnapshot( layoutStats = TextLayoutEngine.hotPathStats(), fontStats = FontRegistry.textHotPathStats(), - rangeSubstringCalls = rangeSubstringCalls + rangeSubstringCalls = rangeSubstringCalls, ) } @@ -590,14 +619,15 @@ class TextPerformanceHotPathCharacterizationTests { val measureText: (String) -> Int = { value -> ctx.measureText(value, FontRegistry.FONT_MINECRAFT, fontSize) } - val rangeSource = MeasuredTextRangeWidthSource( - plainText = text, - fontId = FontRegistry.FONT_MINECRAFT, - fontSizePx = fontSize, - baseFlags = baseTextFlags(), - spans = emptyList(), - ctx = ctx - ) + val rangeSource = + MeasuredTextRangeWidthSource( + plainText = text, + fontId = FontRegistry.FONT_MINECRAFT, + fontSizePx = fontSize, + baseFlags = baseTextFlags(), + spans = emptyList(), + ctx = ctx, + ) repeat(2) { TextLayoutEngine.layout( text = text, @@ -606,18 +636,18 @@ class TextPerformanceHotPathCharacterizationTests { fontHeight = FontRegistry.lineHeight(FontRegistry.FONT_MINECRAFT, fontSize), measureText = measureText, measureRange = rangeSource::measureRange, - measureRangeCacheKey = rangeSource.cacheKey + measureRangeCacheKey = rangeSource.cacheKey, ) } return ProfileSnapshot( layoutStats = TextLayoutEngine.hotPathStats(), fontStats = FontRegistry.textHotPathStats(), - rangeSubstringCalls = 0L + rangeSubstringCalls = 0L, ) } - private fun legacyWrappedLayout(text: String, width: Int, fontSize: Int): TextLayoutEngine.Layout { - return TextLayoutEngine.layout( + private fun legacyWrappedLayout(text: String, width: Int, fontSize: Int): TextLayoutEngine.Layout = + TextLayoutEngine.layout( text = text, maxWidth = width, wrap = TextWrap.Wrap, @@ -627,19 +657,19 @@ class TextPerformanceHotPathCharacterizationTests { val safeStart = start.coerceIn(0, text.length) val safeEnd = end.coerceIn(safeStart, text.length) ctx.measureText(text.substring(safeStart, safeEnd), FontRegistry.FONT_MINECRAFT, fontSize) - } + }, ) - } private fun optimizedWrappedLayout(text: String, width: Int, fontSize: Int): TextLayoutEngine.Layout { - val source = MeasuredTextRangeWidthSource( - plainText = text, - fontId = FontRegistry.FONT_MINECRAFT, - fontSizePx = fontSize, - baseFlags = baseTextFlags(), - spans = emptyList(), - ctx = ctx - ) + val source = + MeasuredTextRangeWidthSource( + plainText = text, + fontId = FontRegistry.FONT_MINECRAFT, + fontSizePx = fontSize, + baseFlags = baseTextFlags(), + spans = emptyList(), + ctx = ctx, + ) return TextLayoutEngine.layout( text = text, maxWidth = width, @@ -647,7 +677,7 @@ class TextPerformanceHotPathCharacterizationTests { fontHeight = FontRegistry.lineHeight(FontRegistry.FONT_MINECRAFT, fontSize), measureText = { value -> ctx.measureText(value, FontRegistry.FONT_MINECRAFT, fontSize) }, measureRange = source::measureRange, - measureRangeCacheKey = source.cacheKey + measureRangeCacheKey = source.cacheKey, ) } @@ -655,17 +685,18 @@ class TextPerformanceHotPathCharacterizationTests { text: String, width: Int, fontSize: Int, - cacheSalt: String + cacheSalt: String, ): TextLayoutEngine.Layout { TextLayoutEngine.clearCache() - val source = MeasuredTextRangeWidthSource( - plainText = text, - fontId = FontRegistry.FONT_MINECRAFT, - fontSizePx = fontSize, - baseFlags = baseTextFlags(), - spans = emptyList(), - ctx = ctx - ) + val source = + MeasuredTextRangeWidthSource( + plainText = text, + fontId = FontRegistry.FONT_MINECRAFT, + fontSizePx = fontSize, + baseFlags = baseTextFlags(), + spans = emptyList(), + ctx = ctx, + ) return TextLayoutEngine.layout( text = text, maxWidth = width, @@ -673,32 +704,35 @@ class TextPerformanceHotPathCharacterizationTests { fontHeight = FontRegistry.lineHeight(FontRegistry.FONT_MINECRAFT, fontSize), measureText = { value -> ctx.measureText(value, FontRegistry.FONT_MINECRAFT, fontSize) }, measureRange = source::measureRange, - measureRangeCacheKey = source.cacheKey to cacheSalt + measureRangeCacheKey = source.cacheKey to cacheSalt, ) } - private fun baseTextFlags() = TextStyleFlags( - bold = false, - italic = false, - underline = false, - strikethrough = false, - obfuscated = false - ) + private fun baseTextFlags() = + TextStyleFlags( + bold = false, + italic = false, + underline = false, + strikethrough = false, + obfuscated = false, + ) private fun findFallbackOnlyCodepoint(primary: Font?, fallback: Font?): Int { requireNotNull(primary) { "Primary font AWT handle must be available for hot-path characterization" } requireNotNull(fallback) { "Fallback font AWT handle must be available for hot-path characterization" } - val candidateCodepoints = listOf( - 0x1F642, // 🙂 - 0x0E01, // Thai - 0x0531, // Armenian - 0x05D0, // Hebrew - 0x16A0 // Runic - ) - candidateCodepoints.firstOrNull { cp -> - Character.isValidCodePoint(cp) && !primary.canDisplay(cp) && fallback.canDisplay(cp) - }?.let { return it } + val candidateCodepoints = + listOf( + 0x1F642, // 🙂 + 0x0E01, // Thai + 0x0531, // Armenian + 0x05D0, // Hebrew + 0x16A0, // Runic + ) + candidateCodepoints + .firstOrNull { cp -> + Character.isValidCodePoint(cp) && !primary.canDisplay(cp) && fallback.canDisplay(cp) + }?.let { return it } for (cp in 0x20..0x2FFF) { if (!Character.isValidCodePoint(cp)) continue diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/ToggleNodeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/ToggleNodeTests.kt index 5fb97be..bc9f95c 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/ToggleNodeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/ToggleNodeTests.kt @@ -1,11 +1,8 @@ package org.dreamfinity.dsgl.core.dom.elements -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertSame -import kotlin.test.assertTrue +import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.dsl.toggle +import org.dreamfinity.dsgl.core.dsl.ui import org.dreamfinity.dsgl.core.event.EventBus import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.event.KeyCodes @@ -13,19 +10,27 @@ import org.dreamfinity.dsgl.core.event.KeyboardKeyDownEvent import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.event.MouseClickEvent import org.dreamfinity.dsgl.core.event.ValueChangedEvent -import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext -import org.dreamfinity.dsgl.core.dsl.toggle import org.dreamfinity.dsgl.core.render.RenderCommand -import org.dreamfinity.dsgl.core.dsl.ui +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertSame +import kotlin.test.assertTrue class ToggleNodeTests { - private val ctx = object : UiMeasureContext { - override fun measureText(text: String): Int = text.length * 6 - override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * 6 - override val fontHeight: Int = 9 - override fun fontHeight(fontId: String?, fontSize: Int?): Int = 9 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override fun measureText(text: String): Int = text.length * 6 + + override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * 6 + + override val fontHeight: Int = 9 + + override fun fontHeight(fontId: String?, fontSize: Int?): Int = 9 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -91,29 +96,35 @@ class ToggleNodeTests { @Test fun `uncontrolled toggle keeps value after reconcile`() { - val current = ui { - toggle({ - key = "toggle.reconcile" - defaultChecked = false - }) - } + val current = + ui { + toggle({ + key = "toggle.reconcile" + defaultChecked = false + }) + } current.render(ctx, 80, 40) - val retainedBefore = current.root.children.single() as ToggleNode + val retainedBefore = + current.root.children + .single() as ToggleNode val click = MouseClickEvent(mouseX = 6, mouseY = 6, mouseButton = MouseButton.LEFT) click.target = retainedBefore EventBus.post(click) assertTrue(retainedBefore.isChecked()) - val next = ui { - toggle({ - key = "toggle.reconcile" - defaultChecked = false - }) - } + val next = + ui { + toggle({ + key = "toggle.reconcile" + defaultChecked = false + }) + } current.reconcileWith(next) - val retainedAfter = current.root.children.single() as ToggleNode + val retainedAfter = + current.root.children + .single() as ToggleNode assertSame(retainedBefore, retainedAfter) assertTrue(retainedAfter.isChecked()) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/ColorPickerCustomNodeReconcileTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/ColorPickerCustomNodeReconcileTests.kt index 89106ab..d6cd2eb 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/ColorPickerCustomNodeReconcileTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/ColorPickerCustomNodeReconcileTests.kt @@ -1,9 +1,5 @@ package org.dreamfinity.dsgl.core.dom.reconcile -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals -import kotlin.test.assertSame import org.dreamfinity.dsgl.core.colorpicker.ColorPickerStyle import org.dreamfinity.dsgl.core.colorpicker.RgbaColor import org.dreamfinity.dsgl.core.colorpicker.internal.AlphaSurfaceNode @@ -16,13 +12,20 @@ import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.render.RenderCommand +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertSame class ColorPickerCustomNodeReconcileTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @Test fun `color field custom bind state syncs on reconcile reuse`() { @@ -124,4 +127,3 @@ class ColorPickerCustomNodeReconcileTests { val NODE_RECT: Rect = Rect(12, 24, 56, 14) } } - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/TextSourceReconcileTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/TextSourceReconcileTests.kt index 02d8ba8..f870cfb 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/TextSourceReconcileTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/TextSourceReconcileTests.kt @@ -1,7 +1,7 @@ package org.dreamfinity.dsgl.core.dom.reconcile -import org.dreamfinity.dsgl.core.dsl.TextProps import org.dreamfinity.dsgl.core.dom.elements.TextNode +import org.dreamfinity.dsgl.core.dsl.TextProps import org.dreamfinity.dsgl.core.dsl.text import org.dreamfinity.dsgl.core.dsl.ui import kotlin.test.Test @@ -25,20 +25,30 @@ class TextSourceReconcileTests { @Test fun `dynamic text updates on reconcile without replacing text node`() { var counter = 0 - val current = ui { - text("Count=$counter") - } - val retainedBefore = assertIs(current.root.children.single()) + val current = + ui { + text("Count=$counter") + } + val retainedBefore = + assertIs( + current.root.children + .single(), + ) assertEquals("Count=0", retainedBefore.text) counter = 7 - val next = ui { - text("Count=$counter") - } + val next = + ui { + text("Count=$counter") + } current.reconcileWith(next) - val retainedAfter = assertIs(current.root.children.single()) + val retainedAfter = + assertIs( + current.root.children + .single(), + ) assertSame(retainedBefore, retainedAfter) assertEquals("Count=7", retainedAfter.text) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/event/TransformHitTestTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/event/TransformHitTestTests.kt index b5e2b81..81bb0ff 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/event/TransformHitTestTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/event/TransformHitTestTests.kt @@ -10,11 +10,14 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue class TransformHitTestTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @Test fun `hover chain follows translated transform`() { diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/font/FontDiscoveryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/font/FontDiscoveryTests.kt index 0b9f114..b96ba5a 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/font/FontDiscoveryTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/font/FontDiscoveryTests.kt @@ -13,25 +13,29 @@ class FontDiscoveryTests { fun `fontId mapping preserves subdirectories`() { assertEquals("Minecraft", FontDiscovery.fontIdFromRelativeTtfPath("Minecraft.ttf")) assertEquals("ui/Ubuntu", FontDiscovery.fontIdFromRelativeTtfPath("ui/Ubuntu.ttf")) - assertEquals("noto/Noto_Sans/NotoSans-Regular", FontDiscovery.fontIdFromRelativeTtfPath("noto\\Noto_Sans\\NotoSans-Regular.ttf")) + assertEquals( + "noto/Noto_Sans/NotoSans-Regular", + FontDiscovery.fontIdFromRelativeTtfPath("noto\\Noto_Sans\\NotoSans-Regular.ttf"), + ) } @Test fun `generated index parsing keeps only ttf entries`() { - val parsed = FontDiscovery.parseGeneratedFontIndex( - """ - minecraft/MinecraftDefault-Regular.ttf - not-a-font.txt + val parsed = + FontDiscovery.parseGeneratedFontIndex( + """ + minecraft/MinecraftDefault-Regular.ttf + not-a-font.txt - ubuntu/Ubuntu-Regular.ttf - """.trimIndent() - ) + ubuntu/Ubuntu-Regular.ttf + """.trimIndent(), + ) assertEquals( listOf( "minecraft/MinecraftDefault-Regular.ttf", - "ubuntu/Ubuntu-Regular.ttf" + "ubuntu/Ubuntu-Regular.ttf", ), - parsed + parsed, ) } @@ -65,10 +69,11 @@ class FontDiscoveryTests { @Test fun `external source overrides jar for same font id`() { - val prioritized = FontDiscovery.resolveSourcePriority( - jarFontIds = listOf("minecraft/MinecraftDefault-Regular", "ui/Ubuntu"), - externalFontIds = listOf("ui/Ubuntu") - ) + val prioritized = + FontDiscovery.resolveSourcePriority( + jarFontIds = listOf("minecraft/MinecraftDefault-Regular", "ui/Ubuntu"), + externalFontIds = listOf("ui/Ubuntu"), + ) assertEquals(FontAssetSource.Jar, prioritized["minecraft/MinecraftDefault-Regular"]) assertEquals(FontAssetSource.External, prioritized["ui/Ubuntu"]) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/font/MsdfFontTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/font/MsdfFontTests.kt index 3cb4740..eedd5a9 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/font/MsdfFontTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/font/MsdfFontTests.kt @@ -28,7 +28,8 @@ class MsdfFontTests { @Test fun `parser ignores unknown keys and preserves glyph fields`() { - val raw = """ + val raw = + """ { "atlas": { "type": "mtsdf", @@ -58,7 +59,7 @@ class MsdfFontTests { "kerning": [], "unknownTopLevel": { "nested": 1 } } - """.trimIndent() + """.trimIndent() val meta = MsdfFontMetaParser.parse(raw) val glyph = meta.glyphByIndex(65) @@ -70,7 +71,8 @@ class MsdfFontTests { @Test fun `parser handles missing optional fields`() { - val raw = """ + val raw = + """ { "atlas": { "type": "mtsdf", @@ -90,7 +92,7 @@ class MsdfFontTests { { "index": 63, "advance": 0.5 } ] } - """.trimIndent() + """.trimIndent() val meta = MsdfFontMetaParser.parse(raw) val glyph = meta.glyphByIndex(63) @@ -101,7 +103,8 @@ class MsdfFontTests { @Test fun `parser resolves codepoint from unicode and char fields`() { - val raw = """ + val raw = + """ { "atlas": { "type": "mtsdf", "distanceRange": 4, "size": 32, "width": 64, "height": 64, "yOrigin": "bottom" }, "metrics": { "emSize": 1, "lineHeight": 1.1, "ascender": 0.8, "descender": -0.2 }, @@ -110,7 +113,7 @@ class MsdfFontTests { { "index": 66, "char": "\uD83D\uDE00", "advance": 0.7 } ] } - """.trimIndent() + """.trimIndent() val meta = MsdfFontMetaParser.parse(raw) assertNotNull(meta.glyph(65)) @@ -122,7 +125,7 @@ class MsdfFontTests { assertEquals(listOf(72, 101, 108, 108, 111), "Hello".toCodepointList()) assertEquals( listOf(0x041F, 0x0440, 0x0438, 0x0432, 0x0435, 0x0442), - "\u041F\u0440\u0438\u0432\u0435\u0442".toCodepointList() + "\u041F\u0440\u0438\u0432\u0435\u0442".toCodepointList(), ) assertEquals(listOf(0x1F600), "\uD83D\uDE00".toCodepointList()) } @@ -171,9 +174,10 @@ class MsdfFontTests { @Test fun `missing glyph in both fonts falls back without throwing`() { val missing = String(Character.toChars(0x10FFFF)) - val shaped = runCatching { - FontRegistry.shapeText(missing, FontRegistry.FONT_MINECRAFT, 16) - } + val shaped = + runCatching { + FontRegistry.shapeText(missing, FontRegistry.FONT_MINECRAFT, 16) + } assertTrue(shaped.isSuccess) assertTrue((shaped.getOrNull()?.width ?: -1f) >= 0f) } @@ -246,12 +250,24 @@ class MsdfFontTests { FontRegistry.clearLoadedCache() FontRegistry.resetShapeCacheStats() FontRegistry.resetTextHotPathStats() - val cold = FontRegistry.shapeText(mixed, FontRegistry.FONT_MINECRAFT, 16, formattingMode = "probe-semantic-cold") + val cold = + FontRegistry.shapeText( + mixed, + FontRegistry.FONT_MINECRAFT, + 16, + formattingMode = "probe-semantic-cold", + ) val coldStats = FontRegistry.textHotPathStats() assertTrue(coldStats.requiresReplacementGlyphEvaluations > 0) FontRegistry.resetTextHotPathStats() - val warm = FontRegistry.shapeText(mixed, FontRegistry.FONT_MINECRAFT, 16, formattingMode = "probe-semantic-warm") + val warm = + FontRegistry.shapeText( + mixed, + FontRegistry.FONT_MINECRAFT, + 16, + formattingMode = "probe-semantic-warm", + ) val warmStats = FontRegistry.textHotPathStats() assertTrue(warmStats.requiresReplacementGlyphCacheHits > 0) @@ -268,13 +284,14 @@ class MsdfFontTests { val lineHeight = FontRegistry.lineHeight(FontRegistry.FONT_MINECRAFT, fontSize) val text = "MSDF wrapping should keep lines within container width and avoid overlap for long text runs." - val layout = TextLayoutEngine.layout( - text = text, - maxWidth = maxWidth, - wrap = TextWrap.Wrap, - fontHeight = lineHeight, - measureText = { value -> FontRegistry.measureText(value, FontRegistry.FONT_MINECRAFT, fontSize) } - ) + val layout = + TextLayoutEngine.layout( + text = text, + maxWidth = maxWidth, + wrap = TextWrap.Wrap, + fontHeight = lineHeight, + measureText = { value -> FontRegistry.measureText(value, FontRegistry.FONT_MINECRAFT, fontSize) }, + ) assertTrue(layout.lines.isNotEmpty()) assertTrue(layout.lines.all { it.width <= maxWidth + 1 }) @@ -286,20 +303,22 @@ class MsdfFontTests { val source = "A\uD83D\uDE00B\u0416C" val start = source.indexOf('B') val end = source.length - val range = FontRegistry.shapeTextRange( - text = source, - startIndex = start, - endIndexExclusive = end, - fontId = FontRegistry.FONT_MINECRAFT, - fontSize = 16, - formattingMode = "plain" - ) - val plain = FontRegistry.shapeText( - text = source.substring(start, end), - fontId = FontRegistry.FONT_MINECRAFT, - fontSize = 16, - formattingMode = "plain" - ) + val range = + FontRegistry.shapeTextRange( + text = source, + startIndex = start, + endIndexExclusive = end, + fontId = FontRegistry.FONT_MINECRAFT, + fontSize = 16, + formattingMode = "plain", + ) + val plain = + FontRegistry.shapeText( + text = source.substring(start, end), + fontId = FontRegistry.FONT_MINECRAFT, + fontSize = 16, + formattingMode = "plain", + ) assertEquals(plain.glyphs.size, range.glyphs.size) assertEquals(plain.runs.size, range.runs.size) assertTrue(abs(plain.width - range.width) <= 0.01f) @@ -316,22 +335,24 @@ class MsdfFontTests { @Test fun `shapeTextRange returns empty for empty or inverted range`() { val source = "Hello" - val empty = FontRegistry.shapeTextRange( - text = source, - startIndex = 2, - endIndexExclusive = 2, - fontId = FontRegistry.FONT_MINECRAFT, - fontSize = 14, - formattingMode = "plain" - ) - val inverted = FontRegistry.shapeTextRange( - text = source, - startIndex = 4, - endIndexExclusive = 1, - fontId = FontRegistry.FONT_MINECRAFT, - fontSize = 14, - formattingMode = "plain" - ) + val empty = + FontRegistry.shapeTextRange( + text = source, + startIndex = 2, + endIndexExclusive = 2, + fontId = FontRegistry.FONT_MINECRAFT, + fontSize = 14, + formattingMode = "plain", + ) + val inverted = + FontRegistry.shapeTextRange( + text = source, + startIndex = 4, + endIndexExclusive = 1, + fontId = FontRegistry.FONT_MINECRAFT, + fontSize = 14, + formattingMode = "plain", + ) assertTrue(empty.glyphs.isEmpty()) assertTrue(inverted.glyphs.isEmpty()) assertEquals(0f, empty.width) @@ -346,10 +367,11 @@ class MsdfFontTests { image.setRGB(0, 1, 0xFF0000FF.toInt()) image.setRGB(1, 1, 0xFFFFFFFF.toInt()) - val pngBytes = ByteArrayOutputStream().use { output -> - ImageIO.write(image, "png", output) - output.toByteArray() - } + val pngBytes = + ByteArrayOutputStream().use { output -> + ImageIO.write(image, "png", output) + output.toByteArray() + } val decoded = AtlasPayload(pngBytes).ensureDecoded() assertEquals(2, decoded.width) assertEquals(2, decoded.height) @@ -371,26 +393,32 @@ class MsdfFontTests { @Test fun `deflated atlas decode path remains supported`() { - val expected = byteArrayOf( - 0x01, 0x02, 0x03, 0x04 - ) - val rawPayload = ByteArrayOutputStream().use { rawOut -> - DataOutputStream(rawOut).use { data -> - data.writeInt(0x4453474C) - data.writeInt(1) - data.writeInt(1) - data.write(expected) + val expected = + byteArrayOf( + 0x01, + 0x02, + 0x03, + 0x04, + ) + val rawPayload = + ByteArrayOutputStream().use { rawOut -> + DataOutputStream(rawOut).use { data -> + data.writeInt(0x4453474C) + data.writeInt(1) + data.writeInt(1) + data.write(expected) + } + rawOut.toByteArray() } - rawOut.toByteArray() - } - val deflated = ByteArrayOutputStream().use { compressedOut -> - val deflater = Deflater(Deflater.BEST_SPEED, true) - DeflaterOutputStream(compressedOut, deflater).use { zipOut -> - zipOut.write(rawPayload) + val deflated = + ByteArrayOutputStream().use { compressedOut -> + val deflater = Deflater(Deflater.BEST_SPEED, true) + DeflaterOutputStream(compressedOut, deflater).use { zipOut -> + zipOut.write(rawPayload) + } + deflater.end() + compressedOut.toByteArray() } - deflater.end() - compressedOut.toByteArray() - } val decoded = AtlasPayload(deflated).ensureDecoded() assertEquals(1, decoded.width) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/hooks/ComponentHookRuntimeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/hooks/ComponentHookRuntimeTests.kt index 1797f75..7889fbe 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/hooks/ComponentHookRuntimeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/hooks/ComponentHookRuntimeTests.kt @@ -6,8 +6,8 @@ import kotlin.ExperimentalStdlibApi import kotlin.reflect.typeOf import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertFalse import kotlin.test.assertFailsWith +import kotlin.test.assertFalse import kotlin.test.assertTrue import kotlin.test.fail @@ -118,9 +118,10 @@ class ComponentHookRuntimeTests { render(runtime, owner) { resolveNamedEntry(HookEntryKind.State, "count") { 0 } - val error = assertFailsWith { - resolveNamedEntry(HookEntryKind.State, "count") { 0 } - } + val error = + assertFailsWith { + resolveNamedEntry(HookEntryKind.State, "count") { 0 } + } assertTrue(error.message?.contains("Duplicate hook path") == true) } } @@ -131,9 +132,10 @@ class ComponentHookRuntimeTests { val owner = Any() render(runtime, owner) { - val error = assertFailsWith { - resolveDuplicateHookPathProbe() - } + val error = + assertFailsWith { + resolveDuplicateHookPathProbe() + } assertEquals(error.message?.contains("Duplicate hook path"), true) } } @@ -149,9 +151,10 @@ class ComponentHookRuntimeTests { assertTrue(first.created) first.entry.value = 5 - val error = assertFailsWith { - resolveStateHookFromHelper("count") - } + val error = + assertFailsWith { + resolveStateHookFromHelper("count") + } assertEquals(error.message?.contains("Duplicate hook path"), true) } } @@ -175,9 +178,10 @@ class ComponentHookRuntimeTests { } render(runtime, owner) { - val error = assertFailsWith { - resolveNamedEntry(HookEntryKind.State, "shared") { 0 } - } + val error = + assertFailsWith { + resolveNamedEntry(HookEntryKind.State, "shared") { 0 } + } assertTrue(error.message?.contains("Hook kind mismatch") == true) } } @@ -212,9 +216,10 @@ class ComponentHookRuntimeTests { render(runtime, owner) { resolveNamedEntry(HookEntryKind.State, "useEffect#0") { 0 } - val error = assertFailsWith { - resolveUnnamedEntry(HookEntryKind.Custom, "useEffect") { Unit } - } + val error = + assertFailsWith { + resolveUnnamedEntry(HookEntryKind.Custom, "useEffect") { Unit } + } assertTrue(error.message?.contains("Synthetic hook key collision") == true) } } @@ -224,15 +229,17 @@ class ComponentHookRuntimeTests { val runtime = ComponentHookRuntime() val owner = Any() - val outsideRender = assertFailsWith { - runtime.enterCustomHookScope("counter") - } + val outsideRender = + assertFailsWith { + runtime.enterCustomHookScope("counter") + } assertTrue(outsideRender.message?.contains("outside active component render") == true) render(runtime, owner) { - val noScope = assertFailsWith { - leaveCustomHookScope() - } + val noScope = + assertFailsWith { + leaveCustomHookScope() + } assertTrue(noScope.message?.contains("no active custom hook scope") == true) } } @@ -244,30 +251,32 @@ class ComponentHookRuntimeTests { val owner = Any() render(runtime, owner) { - val value = resolveNamedTypedEntry( - kind = HookEntryKind.State, - delegateName = "counter", - signature = HookSignatures.state(typeOf()), - expectedRawType = CounterHolder::class.java - ) { - CounterHolder(0) - } - value.value.count = 7 - } - - val error = assertFailsWith { - render(runtime, owner) { + val value = resolveNamedTypedEntry( kind = HookEntryKind.State, delegateName = "counter", - signature = HookSignatures.state(typeOf()), - expectedRawType = CounterHolder::class.java + signature = HookSignatures.state(typeOf()), + expectedRawType = CounterHolder::class.java, ) { CounterHolder(0) } - } + value.value.count = 7 } + val error = + assertFailsWith { + render(runtime, owner) { + resolveNamedTypedEntry( + kind = HookEntryKind.State, + delegateName = "counter", + signature = HookSignatures.state(typeOf()), + expectedRawType = CounterHolder::class.java, + ) { + CounterHolder(0) + } + } + } + assertTrue(error.message?.contains("Hook signature mismatch") == true) assertTrue(error.message?.contains("counter") == true) } @@ -283,24 +292,25 @@ class ComponentHookRuntimeTests { kind = HookEntryKind.Ref, delegateName = "inputRef", signature = HookSignatures.ref(typeOf()), - expectedRawType = Ref::class.java + expectedRawType = Ref::class.java, ) { RefObject() } } - val error = assertFailsWith { - render(runtime, owner) { - resolveNamedTypedEntry( - kind = HookEntryKind.Ref, - delegateName = "inputRef", - signature = HookSignatures.ref(typeOf()), - expectedRawType = Ref::class.java - ) { - RefObject() + val error = + assertFailsWith { + render(runtime, owner) { + resolveNamedTypedEntry( + kind = HookEntryKind.Ref, + delegateName = "inputRef", + signature = HookSignatures.ref(typeOf()), + expectedRawType = Ref::class.java, + ) { + RefObject() + } } } - } assertTrue(error.message?.contains("Hook signature mismatch") == true) assertTrue(error.message?.contains("inputRef") == true) @@ -321,7 +331,7 @@ class ComponentHookRuntimeTests { kind = HookEntryKind.State, delegateName = "branchValue", signature = HookSignatures.state(typeOf()), - expectedRawType = BranchHolder::class.java + expectedRawType = BranchHolder::class.java, ) { BranchHolder("s") } @@ -330,7 +340,7 @@ class ComponentHookRuntimeTests { kind = HookEntryKind.State, delegateName = "branchValue", signature = HookSignatures.state(typeOf()), - expectedRawType = BranchHolder::class.java + expectedRawType = BranchHolder::class.java, ) { BranchHolder(1) } @@ -338,29 +348,30 @@ class ComponentHookRuntimeTests { } useStringBranch = true - val error = assertFailsWith { - render(runtime, owner) { - if (useStringBranch) { - resolveNamedTypedEntry( - kind = HookEntryKind.State, - delegateName = "branchValue", - signature = HookSignatures.state(typeOf()), - expectedRawType = BranchHolder::class.java - ) { - BranchHolder("s") - } - } else { - resolveNamedTypedEntry( - kind = HookEntryKind.State, - delegateName = "branchValue", - signature = HookSignatures.state(typeOf()), - expectedRawType = BranchHolder::class.java - ) { - BranchHolder(1) + val error = + assertFailsWith { + render(runtime, owner) { + if (useStringBranch) { + resolveNamedTypedEntry( + kind = HookEntryKind.State, + delegateName = "branchValue", + signature = HookSignatures.state(typeOf()), + expectedRawType = BranchHolder::class.java, + ) { + BranchHolder("s") + } + } else { + resolveNamedTypedEntry( + kind = HookEntryKind.State, + delegateName = "branchValue", + signature = HookSignatures.state(typeOf()), + expectedRawType = BranchHolder::class.java, + ) { + BranchHolder(1) + } } } } - } assertTrue(error.message?.contains("Hook signature mismatch") == true) assertTrue(error.message?.contains("branchValue") == true) @@ -373,29 +384,32 @@ class ComponentHookRuntimeTests { val owner = Any() render(runtime, owner) { - val value = resolveNamedTypedEntry( - kind = HookEntryKind.State, - delegateName = "counter", - signature = HookSignatures.state(typeOf()), - expectedRawType = CounterHolder::class.java - ) { - CounterHolder(0) - } + val value = + resolveNamedTypedEntry( + kind = HookEntryKind.State, + delegateName = "counter", + signature = HookSignatures.state(typeOf()), + expectedRawType = CounterHolder::class.java, + ) { + CounterHolder(0) + } value.value.count = 42 } - val attempts = renderWithHotReloadRecovery(runtime, owner) { - val value = resolveNamedTypedEntry( - kind = HookEntryKind.State, - delegateName = "counter", - signature = HookSignatures.state(typeOf()), - expectedRawType = StringHolder::class.java - ) { - StringHolder("fresh") + val attempts = + renderWithHotReloadRecovery(runtime, owner) { + val value = + resolveNamedTypedEntry( + kind = HookEntryKind.State, + delegateName = "counter", + signature = HookSignatures.state(typeOf()), + expectedRawType = StringHolder::class.java, + ) { + StringHolder("fresh") + } + assertTrue(value.created) + assertEquals("fresh", value.value.value) } - assertTrue(value.created) - assertEquals("fresh", value.value.value) - } assertEquals(2, attempts) } @@ -422,7 +436,7 @@ class ComponentHookRuntimeTests { runtime: ComponentHookRuntime, owner: Any, mode: HookRenderSessionMode = HookRenderSessionMode.Normal, - block: ComponentHookRuntime.() -> Unit + block: ComponentHookRuntime.() -> Unit, ) { runtime.beginRender(owner, mode) try { @@ -436,7 +450,7 @@ class ComponentHookRuntimeTests { runtime: ComponentHookRuntime, owner: Any, maxAttempts: Int = 8, - block: ComponentHookRuntime.() -> Unit + block: ComponentHookRuntime.() -> Unit, ): Int { var attempt = 0 var lastWarning: HookHotReloadRemountException? = null @@ -454,15 +468,15 @@ class ComponentHookRuntimeTests { } private data class CounterHolder( - var count: Int + var count: Int, ) private data class StringHolder( - var value: String + var value: String, ) private data class BranchHolder( - var value: Any + var value: Any, ) private class AlphaRefTarget @@ -474,7 +488,8 @@ class ComponentHookRuntimeTests { resolveNamedEntry(HookEntryKind.State, "count") { 0 } } - private fun ComponentHookRuntime.resolveStateHookFromHelper(name: String): ResolvedHookEntry { - return resolveNamedEntry(HookEntryKind.State, name) { 0 } - } + private fun ComponentHookRuntime.resolveStateHookFromHelper(name: String): ResolvedHookEntry = + resolveNamedEntry(HookEntryKind.State, name) { + 0 + } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/host/ViewportMappingTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/host/ViewportMappingTests.kt index ba7f691..2a00cb9 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/host/ViewportMappingTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/host/ViewportMappingTests.kt @@ -24,12 +24,13 @@ class ViewportMappingTests { fun `DSGL clip rect converts to GL scissor using viewport height and origin`() { val viewport = Viewport(width = 800, height = 600, scale = 1f, x = 40, y = 30) - val scissor = viewport.dsglRectToGlScissor( - dsglX = 100, - dsglY = 120, - dsglWidth = 240, - dsglHeight = 80 - ) + val scissor = + viewport.dsglRectToGlScissor( + dsglX = 100, + dsglY = 120, + dsglWidth = 240, + dsglHeight = 80, + ) assertEquals(140, scissor.x) assertEquals(430, scissor.y) @@ -41,12 +42,13 @@ class ViewportMappingTests { fun `scissor conversion clamps negative width and height to zero`() { val viewport = Viewport(width = 640, height = 360, scale = 1f, x = 0, y = 0) - val scissor = viewport.dsglRectToGlScissor( - dsglX = 10, - dsglY = 20, - dsglWidth = -2, - dsglHeight = -3 - ) + val scissor = + viewport.dsglRectToGlScissor( + dsglX = 10, + dsglY = 20, + dsglWidth = -2, + dsglHeight = -3, + ) assertEquals(10, scissor.x) assertEquals(340, scissor.y) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorControllerTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorControllerTests.kt index 7b3ef3c..7b7aa04 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorControllerTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorControllerTests.kt @@ -1,13 +1,5 @@ package org.dreamfinity.dsgl.core.inspector -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue -import kotlin.test.fail import org.dreamfinity.dsgl.core.colorpicker.ColorPickerState import org.dreamfinity.dsgl.core.colorpicker.ColorPickerStyle import org.dreamfinity.dsgl.core.colorpicker.RgbaColor @@ -16,13 +8,19 @@ import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.event.KeyCodes -import org.dreamfinity.dsgl.core.event.KeyModifiers import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.input.ClipboardAccess -import org.dreamfinity.dsgl.core.input.ClipboardBridge import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleExpression import org.dreamfinity.dsgl.core.style.StyleProperty +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.test.fail class InspectorControllerTests { @AfterTest @@ -303,17 +301,22 @@ class InspectorControllerTests { controller.handleMouseDown(1006, 126, MouseButton.LEFT) val snapshot = renderFrame(controller, 520, 340) - val pathLines = snapshot.infoLines - .filter { - it.startsWith("Path:") || it.startsWith(" >") || it.startsWith(" root") || - it.startsWith(" middle") || it.startsWith(" leaf") - } + val pathLines = + snapshot.infoLines + .filter { + it.startsWith("Path:") || + it.startsWith(" >") || + it.startsWith(" root") || + it.startsWith(" middle") || + it.startsWith(" leaf") + } assertTrue(pathLines.size >= 2) pathLines.forEach { line -> assertTrue(line.length <= 45) } } + @Test fun `expanded snapshot includes full computed style property list`() { val controller = InspectorController() @@ -357,6 +360,7 @@ class InspectorControllerTests { assertTrue(snapshot.childLabels.isNotEmpty()) assertTrue(snapshot.childLabels.any { it.endsWith("...") }) } + @Test fun `inspector opens picker for color property and stays interactive after preview commit`() { val pickerHost = RecordingInspectorColorPickerHost() @@ -376,8 +380,8 @@ class InspectorControllerTests { assertTrue( controller.debugOpenColorPickerForSelection( StyleProperty.BACKGROUND_COLOR, - Rect(120, 80, 16, 16) - ) + Rect(120, 80, 16, 16), + ), ) val opened = pickerHost.lastOpen @@ -385,11 +389,23 @@ class InspectorControllerTests { assertTrue(pickerHost.isOpen()) opened.onPreview?.invoke(RgbaColor(0.25f, 0.5f, 0.75f, 1f)) - val previewLiteral = (StyleEngine.inspectorOverrideFor(selected, StyleProperty.BACKGROUND_COLOR) as? StyleExpression.Literal)?.value + val previewLiteral = + ( + StyleEngine.inspectorOverrideFor( + selected, + StyleProperty.BACKGROUND_COLOR, + ) as? StyleExpression.Literal + )?.value assertEquals("#FF4080BF", previewLiteral) opened.onCommit?.invoke(RgbaColor(1f, 0f, 0f, 1f)) - val committedLiteral = (StyleEngine.inspectorOverrideFor(selected, StyleProperty.BACKGROUND_COLOR) as? StyleExpression.Literal)?.value + val committedLiteral = + ( + StyleEngine.inspectorOverrideFor( + selected, + StyleProperty.BACKGROUND_COLOR, + ) as? StyleExpression.Literal + )?.value assertEquals("#FFFF0000", committedLiteral) pickerHost.close() @@ -400,7 +416,6 @@ class InspectorControllerTests { assertFalse(controller.isDraggingPanel) } - @Test fun `native inspector rows no longer activate controller text edit session`() { val controller = InspectorController() @@ -417,9 +432,10 @@ class InspectorControllerTests { assertTrue(controller.handleMouseDown(988, 126, MouseButton.LEFT)) renderFrame(controller, 1200, 700) - val row = controller.overlayStyleEditorRows().firstOrNull { - it.property == StyleProperty.BACKGROUND_COLOR && it.editorKind == InspectorEditorKind.StringInput - } ?: error("Expected color string input row.") + val row = + controller.overlayStyleEditorRows().firstOrNull { + it.property == StyleProperty.BACKGROUND_COLOR && it.editorKind == InspectorEditorKind.StringInput + } ?: error("Expected color string input row.") val inputRect = row.inputRect ?: row.controlRect val clickX = inputRect.x + 10 val clickY = inputRect.y + inputRect.height / 2 @@ -455,6 +471,7 @@ class InspectorControllerTests { assertTrue(next.rowRect.y >= prev.rowRect.y + prev.rowRect.height + 4) } } + @Test fun `inspector dropdown option click is consumed and applied over underlying controls`() { val controller = InspectorController() @@ -470,8 +487,9 @@ class InspectorControllerTests { controller.handleMouseDown(988, 126, MouseButton.LEFT) renderFrame(controller, 1200, 700) - val row = controller.overlayStyleEditorRows().firstOrNull { it.editorKind == InspectorEditorKind.EnumSelect } - ?: error("Expected enum select row.") + val row = + controller.overlayStyleEditorRows().firstOrNull { it.editorKind == InspectorEditorKind.EnumSelect } + ?: error("Expected enum select row.") val openX = row.controlRect.x + 4 val openY = row.controlRect.y + row.controlRect.height / 2 controller.onCursorMoved(openX, openY) @@ -479,9 +497,10 @@ class InspectorControllerTests { renderFrame(controller, 1200, 700) val dropdown = controller.overlayStyleEditorDropdowns().firstOrNull() ?: error("Expected open dropdown.") - val option = dropdown.options.firstOrNull { !it.text.equals(row.controlValue, ignoreCase = true) } - ?: dropdown.options.firstOrNull() - ?: error("Expected dropdown option.") + val option = + dropdown.options.firstOrNull { !it.text.equals(row.controlValue, ignoreCase = true) } + ?: dropdown.options.firstOrNull() + ?: error("Expected dropdown option.") val optionX = option.rect.x + 2 val optionY = option.rect.y + option.rect.height / 2 @@ -496,7 +515,6 @@ class InspectorControllerTests { assertTrue(literal.equals(option.text, ignoreCase = true)) } - @Test fun `numeric override commit follows property grammar`() { val controller = InspectorController() @@ -512,24 +530,40 @@ class InspectorControllerTests { controller.handleMouseDown(988, 126, MouseButton.LEFT) assertTrue(controller.overlayApplyNumericOverride(StyleProperty.Z_INDEX, "5", "px")) - val zIndexLiteral = (StyleEngine.inspectorOverrideFor(selected, StyleProperty.Z_INDEX) as? StyleExpression.Literal)?.value + val zIndexLiteral = + ( + StyleEngine.inspectorOverrideFor( + selected, + StyleProperty.Z_INDEX, + ) as? StyleExpression.Literal + )?.value assertEquals("5", zIndexLiteral) assertTrue(controller.overlayApplyNumericOverride(StyleProperty.WIDTH, "24", "em")) - val widthLiteral = (StyleEngine.inspectorOverrideFor(selected, StyleProperty.WIDTH) as? StyleExpression.Literal)?.value + val widthLiteral = + ( + StyleEngine.inspectorOverrideFor( + selected, + StyleProperty.WIDTH, + ) as? StyleExpression.Literal + )?.value assertEquals("24em", widthLiteral) } - private fun renderFrame(controller: InspectorController, viewportWidth: Int, viewportHeight: Int): InspectorDomSnapshot { - return controller.buildDomSnapshot(viewportWidth, viewportHeight) + + private fun renderFrame(controller: InspectorController, viewportWidth: Int, viewportHeight: Int): InspectorDomSnapshot = + controller.buildDomSnapshot(viewportWidth, viewportHeight) ?: error("Inspector snapshot must exist while active.") - } - private fun container(key: Any, x: Int, y: Int, width: Int, height: Int): ContainerNode { - return ContainerNode(key = key).apply { + private fun container( + key: Any, + x: Int, + y: Int, + width: Int, + height: Int, + ): ContainerNode = + ContainerNode(key = key).apply { bounds = Rect(x, y, width, height) } - } - private class RecordingClipboardAccess : ClipboardAccess { var contents: String = "" @@ -540,6 +574,7 @@ class InspectorControllerTests { contents = value } } + private class RecordingInspectorColorPickerHost : InspectorColorPickerHost { var lastOpen: OpenCall? = null private var open: Boolean = false @@ -555,17 +590,18 @@ class InspectorControllerTests { onPreview: ((RgbaColor) -> Unit)?, onChange: ((RgbaColor) -> Unit)?, onCommit: ((RgbaColor) -> Unit)?, - onClose: (() -> Unit)? + onClose: (() -> Unit)?, ) { - lastOpen = OpenCall( - anchorRect = anchorRect, - title = title, - state = state, - onPreview = onPreview, - onChange = onChange, - onCommit = onCommit, - onClose = onClose - ) + lastOpen = + OpenCall( + anchorRect = anchorRect, + title = title, + state = state, + onPreview = onPreview, + onChange = onChange, + onCommit = onCommit, + onClose = onClose, + ) open = true } @@ -585,8 +621,6 @@ class InspectorControllerTests { val onPreview: ((RgbaColor) -> Unit)?, val onChange: ((RgbaColor) -> Unit)?, val onCommit: ((RgbaColor) -> Unit)?, - val onClose: (() -> Unit)? + val onClose: (() -> Unit)?, ) } - - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorEditSessionTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorEditSessionTests.kt index 9e044e1..e5e72b5 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorEditSessionTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorEditSessionTests.kt @@ -16,7 +16,7 @@ class InspectorEditSessionTests { property = StyleProperty.WIDTH, initialBuffer = "42", initialUnit = CssUnit.Px, - isNumeric = true + isNumeric = true, ) assertEquals(StyleProperty.WIDTH, session.activeProperty) @@ -27,12 +27,13 @@ class InspectorEditSessionTests { @Test fun `closeAllDropdowns clears dropdown ownership and scroll state`() { - val session = InspectorEditSession().apply { - openValueProperty = StyleProperty.DISPLAY - openValueScrollIndex = 4 - openUnitProperty = StyleProperty.WIDTH - openUnitScrollIndex = 3 - } + val session = + InspectorEditSession().apply { + openValueProperty = StyleProperty.DISPLAY + openValueScrollIndex = 4 + openUnitProperty = StyleProperty.WIDTH + openUnitScrollIndex = 3 + } session.closeAllDropdowns() @@ -44,16 +45,17 @@ class InspectorEditSessionTests { @Test fun `resetAll clears both edit and dropdown state`() { - val session = InspectorEditSession().apply { - begin( - property = StyleProperty.HEIGHT, - initialBuffer = "11", - initialUnit = CssUnit.Em, - isNumeric = false - ) - openValueProperty = StyleProperty.DISPLAY - openValueScrollIndex = 1 - } + val session = + InspectorEditSession().apply { + begin( + property = StyleProperty.HEIGHT, + initialBuffer = "11", + initialUnit = CssUnit.Em, + isNumeric = false, + ) + openValueProperty = StyleProperty.DISPLAY + openValueScrollIndex = 1 + } session.resetAll() diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorEditorRegistryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorEditorRegistryTests.kt index 7bac786..7e731b4 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorEditorRegistryTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorEditorRegistryTests.kt @@ -12,30 +12,33 @@ import kotlin.test.assertTrue class InspectorEditorRegistryTests { @Test fun `maps enum and font properties to dropdown editors`() { - val display = InspectorEditorRegistry.describe( - property = StyleProperty.DISPLAY, - literal = "block", - expression = StyleExpression.Literal("block") - ) + val display = + InspectorEditorRegistry.describe( + property = StyleProperty.DISPLAY, + literal = "block", + expression = StyleExpression.Literal("block"), + ) assertEquals(InspectorEditorKind.EnumSelect, display.kind) assertTrue(display.options.isNotEmpty()) - val font = InspectorEditorRegistry.describe( - property = StyleProperty.FONT_ID, - literal = "minecraft", - expression = StyleExpression.Literal("minecraft") - ) + val font = + InspectorEditorRegistry.describe( + property = StyleProperty.FONT_ID, + literal = "minecraft", + expression = StyleExpression.Literal("minecraft"), + ) assertEquals(InspectorEditorKind.FontSelect, font.kind) assertTrue(font.options.isNotEmpty()) } @Test fun `position is exposed as enum select with explicit options`() { - val position = InspectorEditorRegistry.describe( - property = StyleProperty.POSITION, - literal = "relative", - expression = StyleExpression.Literal("relative") - ) + val position = + InspectorEditorRegistry.describe( + property = StyleProperty.POSITION, + literal = "relative", + expression = StyleExpression.Literal("relative"), + ) assertEquals(InspectorEditorKind.EnumSelect, position.kind) assertEquals(listOf("static", "relative", "absolute", "fixed", "sticky"), position.options) @@ -43,11 +46,12 @@ class InspectorEditorRegistryTests { @Test fun `z-index uses unitless numeric grammar in inspector`() { - val descriptor = InspectorEditorRegistry.describe( - property = StyleProperty.Z_INDEX, - literal = "5", - expression = StyleExpression.Literal("5") - ) + val descriptor = + InspectorEditorRegistry.describe( + property = StyleProperty.Z_INDEX, + literal = "5", + expression = StyleExpression.Literal("5"), + ) assertEquals(InspectorEditorKind.NumericInput, descriptor.kind) assertFalse(descriptor.supportsUnits) @@ -62,11 +66,12 @@ class InspectorEditorRegistryTests { @Test fun `line-height supports normal and length values in inspector editor`() { - val descriptor = InspectorEditorRegistry.describe( - property = StyleProperty.LINE_HEIGHT, - literal = "normal", - expression = StyleExpression.Literal("normal") - ) + val descriptor = + InspectorEditorRegistry.describe( + property = StyleProperty.LINE_HEIGHT, + literal = "normal", + expression = StyleExpression.Literal("normal"), + ) assertEquals(InspectorEditorKind.NumericInput, descriptor.kind) assertTrue(descriptor.supportsUnits) assertEquals(listOf("normal"), descriptor.options) @@ -87,11 +92,12 @@ class InspectorEditorRegistryTests { @Test fun `length-like numeric properties keep unit-aware grammar`() { - val width = InspectorEditorRegistry.describe( - property = StyleProperty.WIDTH, - literal = "12px", - expression = StyleExpression.Literal("12px") - ) + val width = + InspectorEditorRegistry.describe( + property = StyleProperty.WIDTH, + literal = "12px", + expression = StyleExpression.Literal("12px"), + ) assertEquals(InspectorEditorKind.NumericInput, width.kind) assertTrue(width.supportsUnits) @@ -103,15 +109,15 @@ class InspectorEditorRegistryTests { assertEquals("18em", InspectorEditorRegistry.formatNumericLiteral(StyleProperty.WIDTH, "18", "em")) } - @Test fun `offset properties expose unit-aware numeric editor`() { listOf(StyleProperty.LEFT, StyleProperty.TOP, StyleProperty.RIGHT, StyleProperty.BOTTOM).forEach { property -> - val descriptor = InspectorEditorRegistry.describe( - property = property, - literal = "auto", - expression = StyleExpression.Literal("auto") - ) + val descriptor = + InspectorEditorRegistry.describe( + property = property, + literal = "auto", + expression = StyleExpression.Literal("auto"), + ) assertEquals(InspectorEditorKind.NumericInput, descriptor.kind) assertTrue(descriptor.supportsUnits) } @@ -133,19 +139,21 @@ class InspectorEditorRegistryTests { @Test fun `detects color-like values for preview`() { - val stringColor = InspectorEditorRegistry.describe( - property = StyleProperty.BACKGROUND_IMAGE, - literal = "#AABBCC", - expression = StyleExpression.Literal("#AABBCC") - ) + val stringColor = + InspectorEditorRegistry.describe( + property = StyleProperty.BACKGROUND_IMAGE, + literal = "#AABBCC", + expression = StyleExpression.Literal("#AABBCC"), + ) assertEquals(InspectorEditorKind.StringInput, stringColor.kind) assertTrue(stringColor.showColorPreview) - val plain = InspectorEditorRegistry.describe( - property = StyleProperty.BACKGROUND_IMAGE, - literal = "textures/gui/options_background.png", - expression = StyleExpression.Literal("textures/gui/options_background.png") - ) + val plain = + InspectorEditorRegistry.describe( + property = StyleProperty.BACKGROUND_IMAGE, + literal = "textures/gui/options_background.png", + expression = StyleExpression.Literal("textures/gui/options_background.png"), + ) assertFalse(plain.showColorPreview) } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorStyleEditorSnapshotBuilderTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorStyleEditorSnapshotBuilderTests.kt index baaf143..07a95d9 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorStyleEditorSnapshotBuilderTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorStyleEditorSnapshotBuilderTests.kt @@ -10,7 +10,6 @@ import org.dreamfinity.dsgl.core.style.StyleProperty import kotlin.test.* class InspectorStyleEditorSnapshotBuilderTests { - @AfterTest fun cleanup() { StyleEngine.clearAllInspectorOverrides() @@ -23,29 +22,34 @@ class InspectorStyleEditorSnapshotBuilderTests { StyleEngine.setInspectorOverrideLiteral(selected, StyleProperty.BACKGROUND_COLOR, "#FF336699").getOrThrow() val inspection = StyleEngine.inspect(selected) - val result = builder().build( - context( - selected = selected, - inspection = inspection, - editableProperties = listOf( - StyleProperty.BACKGROUND_COLOR, - StyleProperty.WIDTH, - StyleProperty.DISPLAY + val result = + builder().build( + context( + selected = selected, + inspection = inspection, + editableProperties = + listOf( + StyleProperty.BACKGROUND_COLOR, + StyleProperty.WIDTH, + StyleProperty.DISPLAY, + ), + openValueSelectProperty = StyleProperty.DISPLAY, + openUnitSelectProperty = StyleProperty.WIDTH, + openValueSelectScrollIndex = 99, + openUnitSelectScrollIndex = 99, ), - openValueSelectProperty = StyleProperty.DISPLAY, - openUnitSelectProperty = StyleProperty.WIDTH, - openValueSelectScrollIndex = 99, - openUnitSelectScrollIndex = 99 ) - ) assertEquals(3, result.rows.size) - val colorRow = result.rows.firstOrNull { it.property == StyleProperty.BACKGROUND_COLOR } - ?: error("background color row missing") - val widthRow = result.rows.firstOrNull { it.property == StyleProperty.WIDTH } - ?: error("width row missing") - val displayRow = result.rows.firstOrNull { it.property == StyleProperty.DISPLAY } - ?: error("display row missing") + val colorRow = + result.rows.firstOrNull { it.property == StyleProperty.BACKGROUND_COLOR } + ?: error("background color row missing") + val widthRow = + result.rows.firstOrNull { it.property == StyleProperty.WIDTH } + ?: error("width row missing") + val displayRow = + result.rows.firstOrNull { it.property == StyleProperty.DISPLAY } + ?: error("display row missing") assertNotNull(colorRow.colorPreviewRect) assertNotNull(colorRow.colorPreviewColor) @@ -64,8 +68,9 @@ class InspectorStyleEditorSnapshotBuilderTests { assertTrue( result.actionSpecs.any { - it.type == InspectorStyleEditorActionType.OpenColorPicker && it.property == StyleProperty.BACKGROUND_COLOR - } + it.type == InspectorStyleEditorActionType.OpenColorPicker && + it.property == StyleProperty.BACKGROUND_COLOR + }, ) assertTrue(result.resetRect.width > 0 && result.resetRect.height > 0) assertTrue(result.clearRect.width > 0 && result.clearRect.height > 0) @@ -75,29 +80,32 @@ class InspectorStyleEditorSnapshotBuilderTests { fun `builder projects dropdown hover through pointer projection scroll and preserves option value`() { val (_, selected) = inspectedSelection() val inspection = StyleEngine.inspect(selected) - val baseContext = context( - selected = selected, - inspection = inspection, - editableProperties = listOf(StyleProperty.DISPLAY), - openValueSelectProperty = StyleProperty.DISPLAY, - pointerProjectionScrollY = 32, - mouseX = 0, - mouseY = 0 - ) + val baseContext = + context( + selected = selected, + inspection = inspection, + editableProperties = listOf(StyleProperty.DISPLAY), + openValueSelectProperty = StyleProperty.DISPLAY, + pointerProjectionScrollY = 32, + mouseX = 0, + mouseY = 0, + ) val baseline = builder().build(baseContext) val dropdown = baseline.dropdowns.firstOrNull() ?: error("display dropdown missing") val option = dropdown.options.firstOrNull() ?: error("display dropdown option missing") val projectedOptionY = option.rect.y - 32 - val hovered = builder().build( - baseContext.copy( - mouseX = option.rect.x + 2, - mouseY = projectedOptionY + (option.rect.height / 2).coerceAtLeast(1) + val hovered = + builder().build( + baseContext.copy( + mouseX = option.rect.x + 2, + mouseY = projectedOptionY + (option.rect.height / 2).coerceAtLeast(1), + ), ) - ) val hoveredDropdown = hovered.dropdowns.firstOrNull() ?: error("display dropdown missing after hover pass") - val hoveredOption = hoveredDropdown.options.firstOrNull { it.value == option.value } - ?: error("hovered option missing") + val hoveredOption = + hoveredDropdown.options.firstOrNull { it.value == option.value } + ?: error("hovered option missing") assertTrue(hoveredOption.hovered) assertEquals(option.value, hoveredOption.value) @@ -109,34 +117,34 @@ class InspectorStyleEditorSnapshotBuilderTests { StyleEngine.setInspectorOverride( selected, StyleProperty.BACKGROUND_COLOR, - StyleExpression.VariableRef("--missing-color") + StyleExpression.VariableRef("--missing-color"), ) val inspection = StyleEngine.inspect(selected) val panelRect = Rect(20, 20, 360, 260) val rowY = 64 + 32 - val result = builder().build( - context( - selected = selected, - inspection = inspection, - panelRect = panelRect, - editableProperties = listOf(StyleProperty.BACKGROUND_COLOR), - mouseX = panelRect.x + 18, - mouseY = rowY + 8 + val result = + builder().build( + context( + selected = selected, + inspection = inspection, + panelRect = panelRect, + editableProperties = listOf(StyleProperty.BACKGROUND_COLOR), + mouseX = panelRect.x + 18, + mouseY = rowY + 8, + ), ) - ) val tooltip = result.variableTooltip ?: error("expected variable tooltip") assertTrue(tooltip.text.contains("--missing-color")) assertTrue(tooltip.rect.width > 0 && tooltip.rect.height > 0) } - private fun builder(): InspectorStyleEditorSnapshotBuilder { - return InspectorStyleEditorSnapshotBuilder( + private fun builder(): InspectorStyleEditorSnapshotBuilder = + InspectorStyleEditorSnapshotBuilder( resolveLiteralFromComputed = ::literalForProperty, - renderExpressionLabel = ::expressionLabel + renderExpressionLabel = ::expressionLabel, ) - } private fun context( selected: ContainerNode, @@ -149,9 +157,9 @@ class InspectorStyleEditorSnapshotBuilderTests { openValueSelectProperty: StyleProperty? = null, openUnitSelectProperty: StyleProperty? = null, openValueSelectScrollIndex: Int = 0, - openUnitSelectScrollIndex: Int = 0 - ): InspectorStyleEditorSnapshotBuildContext { - return InspectorStyleEditorSnapshotBuildContext( + openUnitSelectScrollIndex: Int = 0, + ): InspectorStyleEditorSnapshotBuildContext = + InspectorStyleEditorSnapshotBuildContext( panelRect = panelRect, panelBounds = panelRect, selected = selected, @@ -169,37 +177,37 @@ class InspectorStyleEditorSnapshotBuilderTests { openValueSelectProperty = openValueSelectProperty, openUnitSelectProperty = openUnitSelectProperty, openValueSelectScrollIndex = openValueSelectScrollIndex, - openUnitSelectScrollIndex = openUnitSelectScrollIndex + openUnitSelectScrollIndex = openUnitSelectScrollIndex, ) - } private fun inspectedSelection(): Pair { - val root = ContainerNode(key = "root").apply { - bounds = Rect(0, 0, 1280, 720) - } - val selected = ContainerNode(key = "target").apply { - bounds = Rect(980, 140, 120, 30) - } + val root = + ContainerNode(key = "root").apply { + bounds = Rect(0, 0, 1280, 720) + } + val selected = + ContainerNode(key = "target").apply { + bounds = Rect(980, 140, 120, 30) + } selected.applyParent(root) return root to selected } - private fun literalForProperty(style: ComputedStyle, property: StyleProperty): String { - return when (property) { + private fun literalForProperty(style: ComputedStyle, property: StyleProperty): String = + when (property) { StyleProperty.BACKGROUND_COLOR -> "#FF336699" StyleProperty.WIDTH -> "24px" StyleProperty.DISPLAY -> "block" - else -> when (property) { - StyleProperty.FONT_ID -> style.fontId ?: "minecraft" - else -> "0" - } + else -> + when (property) { + StyleProperty.FONT_ID -> style.fontId ?: "minecraft" + else -> "0" + } } - } - private fun expressionLabel(expression: StyleExpression): String { - return when (expression) { + private fun expressionLabel(expression: StyleExpression): String = + when (expression) { is StyleExpression.Literal -> expression.value is StyleExpression.VariableRef -> "var(${expression.name})" } - } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayFocusIsolationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayFocusIsolationTests.kt index cd7e833..6ca60f5 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayFocusIsolationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayFocusIsolationTests.kt @@ -1,10 +1,5 @@ package org.dreamfinity.dsgl.core.inspector.internal -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode @@ -25,13 +20,21 @@ import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayHost import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleProperty +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue class SystemInspectorOverlayFocusIsolationTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } private val clipboard = RecordingClipboardAccess() @@ -46,12 +49,14 @@ class SystemInspectorOverlayFocusIsolationTests { @Test fun `text input keeps focus while inspector is mounted and rendered`() { - val appRoot = ContainerNode(key = "app-root").apply { - bounds = Rect(0, 0, 1280, 720) - } - val input = TextInputNode(text = "value", key = "app-input").apply { - bounds = Rect(24, 24, 180, 24) - } + val appRoot = + ContainerNode(key = "app-root").apply { + bounds = Rect(0, 0, 1280, 720) + } + val input = + TextInputNode(text = "value", key = "app-input").apply { + bounds = Rect(24, 24, 180, 24) + } input.applyParent(appRoot) val router = LayerDomInputRouter { appRoot } @@ -76,12 +81,14 @@ class SystemInspectorOverlayFocusIsolationTests { fun `text selection drag and keyboard edit work while inspector is mounted`() { ClipboardBridge.install(clipboard) - val appRoot = ContainerNode(key = "app-root").apply { - bounds = Rect(0, 0, 1280, 720) - } - val input = TextInputNode(text = "abcdef", key = "app-input").apply { - bounds = Rect(24, 24, 180, 24) - } + val appRoot = + ContainerNode(key = "app-root").apply { + bounds = Rect(0, 0, 1280, 720) + } + val input = + TextInputNode(text = "abcdef", key = "app-input").apply { + bounds = Rect(24, 24, 180, 24) + } input.applyParent(appRoot) val router = LayerDomInputRouter { appRoot } @@ -105,9 +112,10 @@ class SystemInspectorOverlayFocusIsolationTests { @Test fun `range drag works while inspector is mounted`() { - val appRoot = ContainerNode(key = "app-root").apply { - bounds = Rect(0, 0, 1280, 720) - } + val appRoot = + ContainerNode(key = "app-root").apply { + bounds = Rect(0, 0, 1280, 720) + } val range = RangeInputNode(value = 0L, min = 0L, max = 100L, key = "app-range") range.applyParent(appRoot) range.render(ctx, 24, 24, 120, 12) @@ -126,12 +134,14 @@ class SystemInspectorOverlayFocusIsolationTests { @Test fun `focused app control keeps keyboard routing with inspector mounted`() { - val appRoot = ContainerNode(key = "app-root").apply { - bounds = Rect(0, 0, 1280, 720) - } - val input = TextInputNode(text = "x", key = "app-input").apply { - bounds = Rect(24, 24, 180, 24) - } + val appRoot = + ContainerNode(key = "app-root").apply { + bounds = Rect(0, 0, 1280, 720) + } + val input = + TextInputNode(text = "x", key = "app-input").apply { + bounds = Rect(24, 24, 180, 24) + } input.applyParent(appRoot) val router = LayerDomInputRouter { appRoot } @@ -184,12 +194,14 @@ class SystemInspectorOverlayFocusIsolationTests { } private fun inspectedRoot(): ContainerNode { - val root = ContainerNode(key = "root").apply { - bounds = Rect(0, 0, 1280, 720) - } - val target = ContainerNode(key = "target").apply { - bounds = Rect(980, 140, 120, 30) - } + val root = + ContainerNode(key = "root").apply { + bounds = Rect(0, 0, 1280, 720) + } + val target = + ContainerNode(key = "target").apply { + bounds = Rect(980, 140, 120, 30) + } target.applyParent(root) StyleEngine.setInspectorOverrideLiteral(target, StyleProperty.BACKGROUND_COLOR, "#FF112233").getOrThrow() return root @@ -197,10 +209,11 @@ class SystemInspectorOverlayFocusIsolationTests { private class RecordingClipboardAccess : ClipboardAccess { var value: String = "" + override fun readText(): String = value + override fun writeText(value: String) { this.value = value } } } - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayInputBoundsTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayInputBoundsTests.kt index b37ea87..c802936 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayInputBoundsTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayInputBoundsTests.kt @@ -1,48 +1,50 @@ package org.dreamfinity.dsgl.core.inspector.internal -import kotlin.test.Test -import kotlin.test.assertFalse -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.inspector.InspectorDropdownOptionSnapshot import org.dreamfinity.dsgl.core.inspector.InspectorDropdownSnapshot import org.dreamfinity.dsgl.core.style.StyleProperty +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue class SystemInspectorOverlayInputBoundsTests { - @Test fun `input bounds include rendered dropdown popup outside panel`() { - val controller = InspectorController().also { - it.toggle() - it.setPickMode(false) - } + val controller = + InspectorController().also { + it.toggle() + it.setPickMode(false) + } val node = SystemInspectorOverlayNode(controller) val panelRect = controller.overlayPanelRect() ?: error("expected panel rect") - val popupRect = Rect( - x = panelRect.x + panelRect.width + 32, - y = panelRect.y + 80, - width = 180, - height = 120 - ) + val popupRect = + Rect( + x = panelRect.x + panelRect.width + 32, + y = panelRect.y + 80, + width = 180, + height = 120, + ) controller.onNativeDomDropdownSnapshots( listOf( InspectorDropdownSnapshot( popupRect = popupRect, property = StyleProperty.ALIGN, unitSelect = false, - options = listOf( - InspectorDropdownOptionSnapshot( - rect = Rect(popupRect.x + 2, popupRect.y + 2, popupRect.width - 4, 24), - text = "start", - value = "start", - hovered = false - ) - ), - footerText = null - ) - ) + options = + listOf( + InspectorDropdownOptionSnapshot( + rect = Rect(popupRect.x + 2, popupRect.y + 2, popupRect.width - 4, 24), + text = "start", + value = "start", + hovered = false, + ), + ), + footerText = null, + ), + ), ) node.syncInputBounds(viewportWidth = 1400, viewportHeight = 800) @@ -55,4 +57,3 @@ class SystemInspectorOverlayInputBoundsTests { assertFalse(node.bounds.contains(popupProbeX, popupProbeY)) } } - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt index 5481a3f..dbc888d 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt @@ -1,10 +1,5 @@ package org.dreamfinity.dsgl.core.overlay -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Rect @@ -15,40 +10,50 @@ import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayEntryId import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayHost import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayPanelDemoNode import org.dreamfinity.dsgl.core.render.RenderCommand +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue class LiveLayerInteractionPathTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @Test fun `runtime layer path resolves in debug system app-overlay app-root order`() { val callOrder = ArrayList(4) - val harness = LiveLayerInputHarness( - debugHandler = { _, _, _ -> - callOrder += UiLayerId.Debug - false - }, - systemOverlayHandler = { _, _, _ -> - callOrder += UiLayerId.SystemOverlay - false - }, - applicationOverlayHandler = { _, _, _ -> - callOrder += UiLayerId.ApplicationOverlay - false + val harness = + LiveLayerInputHarness( + debugHandler = { _, _, _ -> + callOrder += UiLayerId.Debug + false + }, + systemOverlayHandler = { _, _, _ -> + callOrder += UiLayerId.SystemOverlay + false + }, + applicationOverlayHandler = { _, _, _ -> + callOrder += UiLayerId.ApplicationOverlay + false + }, + ) + val consumedBy = + harness.dispatchMouseDown(10, 10, MouseButton.LEFT) { + callOrder += UiLayerId.ApplicationRoot + true } - ) - val consumedBy = harness.dispatchMouseDown(10, 10, MouseButton.LEFT) { - callOrder += UiLayerId.ApplicationRoot - true - } assertEquals(UiLayerId.ApplicationRoot, consumedBy) assertEquals( listOf(UiLayerId.Debug, UiLayerId.SystemOverlay, UiLayerId.ApplicationOverlay, UiLayerId.ApplicationRoot), - callOrder + callOrder, ) } @@ -57,21 +62,23 @@ class LiveLayerInteractionPathTests { var systemReceived = false var appOverlayReceived = false var appRootReceived = false - val harness = LiveLayerInputHarness( - debugHandler = { _, _, _ -> true }, - systemOverlayHandler = { _, _, _ -> - systemReceived = true - false - }, - applicationOverlayHandler = { _, _, _ -> - appOverlayReceived = true - false + val harness = + LiveLayerInputHarness( + debugHandler = { _, _, _ -> true }, + systemOverlayHandler = { _, _, _ -> + systemReceived = true + false + }, + applicationOverlayHandler = { _, _, _ -> + appOverlayReceived = true + false + }, + ) + val consumedBy = + harness.dispatchMouseDown(12, 14, MouseButton.LEFT) { + appRootReceived = true + true } - ) - val consumedBy = harness.dispatchMouseDown(12, 14, MouseButton.LEFT) { - appRootReceived = true - true - } assertEquals(UiLayerId.Debug, consumedBy) assertFalse(systemReceived) @@ -85,20 +92,28 @@ class LiveLayerInteractionPathTests { val root = inspectedRoot() systemHost.onInputFrame(1280, 720) systemHost.togglePanelDemo(anchorX = 240, anchorY = 180) - systemHost.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 244, cursorY = 186, inspectorPointerCaptured = false) + systemHost.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 244, + cursorY = 186, + inspectorPointerCaptured = false, + ) val entryState = systemHost.debugEntryState(SystemOverlayEntryId.PanelDemo) ?: error("panel demo state missing") val panelRect = entryState.panelState.currentRectOrNull() ?: error("panel demo rect missing") - val harness = LiveLayerInputHarness( - debugHandler = { _, _, _ -> false }, - systemOverlayHandler = { x, y, button -> systemHost.handleMouseDown(x, y, button) }, - applicationOverlayHandler = { _, _, _ -> false } - ) + val harness = + LiveLayerInputHarness( + debugHandler = { _, _, _ -> false }, + systemOverlayHandler = { x, y, button -> systemHost.handleMouseDown(x, y, button) }, + applicationOverlayHandler = { _, _, _ -> false }, + ) var appRootReceived = false - val consumedBy = harness.dispatchMouseDown(panelRect.x + 20, panelRect.y + 70, MouseButton.LEFT) { - appRootReceived = true - true - } + val consumedBy = + harness.dispatchMouseDown(panelRect.x + 20, panelRect.y + 70, MouseButton.LEFT) { + appRootReceived = true + true + } assertEquals(UiLayerId.SystemOverlay, consumedBy) assertFalse(appRootReceived) @@ -112,7 +127,13 @@ class LiveLayerInteractionPathTests { systemHost.onInputFrame(1280, 720) inspector.toggle() inspector.setPickMode(false) - systemHost.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + systemHost.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false, + ) systemHost.render(ctx, 1280, 720) val panelRect = inspector.overlayPanelRect() ?: error("inspector panel rect missing") @@ -120,32 +141,37 @@ class LiveLayerInteractionPathTests { val outsideY = (panelRect.y + panelRect.height / 2).coerceIn(1, 719) assertFalse(panelRect.contains(outsideX, outsideY)) - val harness = LiveLayerInputHarness( - debugHandler = { _, _, _ -> false }, - systemOverlayHandler = { x, y, button -> systemHost.handleMouseDown(x, y, button) }, - applicationOverlayHandler = { _, _, _ -> false } - ) + val harness = + LiveLayerInputHarness( + debugHandler = { _, _, _ -> false }, + systemOverlayHandler = { x, y, button -> systemHost.handleMouseDown(x, y, button) }, + applicationOverlayHandler = { _, _, _ -> false }, + ) var appRootReceivedOutside = false - val consumedOutside = harness.dispatchMouseDown(outsideX, outsideY, MouseButton.LEFT) { - appRootReceivedOutside = true - true - } + val consumedOutside = + harness.dispatchMouseDown(outsideX, outsideY, MouseButton.LEFT) { + appRootReceivedOutside = true + true + } assertEquals(UiLayerId.ApplicationRoot, consumedOutside) assertTrue(appRootReceivedOutside) } + @Test fun `application overlay consumption prevents app-root fallthrough`() { - val harness = LiveLayerInputHarness( - debugHandler = { _, _, _ -> false }, - systemOverlayHandler = { _, _, _ -> false }, - applicationOverlayHandler = { _, _, _ -> true } - ) + val harness = + LiveLayerInputHarness( + debugHandler = { _, _, _ -> false }, + systemOverlayHandler = { _, _, _ -> false }, + applicationOverlayHandler = { _, _, _ -> true }, + ) var appRootReceived = false - val consumedBy = harness.dispatchMouseDown(24, 30, MouseButton.LEFT) { - appRootReceived = true - true - } + val consumedBy = + harness.dispatchMouseDown(24, 30, MouseButton.LEFT) { + appRootReceived = true + true + } assertEquals(UiLayerId.ApplicationOverlay, consumedBy) assertFalse(appRootReceived) @@ -157,23 +183,32 @@ class LiveLayerInteractionPathTests { val root = inspectedRoot() systemHost.onInputFrame(1280, 720) systemHost.togglePanelDemo(anchorX = 260, anchorY = 200) - systemHost.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 260, cursorY = 200, inspectorPointerCaptured = false) + systemHost.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 260, + cursorY = 200, + inspectorPointerCaptured = false, + ) systemHost.render(ctx, 1280, 720) - val demoNode = systemHost.debugEntryNode(SystemOverlayEntryId.PanelDemo) as? SystemOverlayPanelDemoNode - ?: error("panel demo node missing") + val demoNode = + systemHost.debugEntryNode(SystemOverlayEntryId.PanelDemo) as? SystemOverlayPanelDemoNode + ?: error("panel demo node missing") val buttonRect = demoNode.buttonRect() assertNotNull(buttonRect) - val harness = LiveLayerInputHarness( - debugHandler = { _, _, _ -> false }, - systemOverlayHandler = { x, y, button -> systemHost.handleMouseDown(x, y, button) }, - applicationOverlayHandler = { _, _, _ -> false } - ) + val harness = + LiveLayerInputHarness( + debugHandler = { _, _, _ -> false }, + systemOverlayHandler = { x, y, button -> systemHost.handleMouseDown(x, y, button) }, + applicationOverlayHandler = { _, _, _ -> false }, + ) var appRootReceived = false - val consumedBy = harness.dispatchMouseDown(buttonRect.x + 1, buttonRect.y + 1, MouseButton.LEFT) { - appRootReceived = true - true - } + val consumedBy = + harness.dispatchMouseDown(buttonRect.x + 1, buttonRect.y + 1, MouseButton.LEFT) { + appRootReceived = true + true + } assertEquals(UiLayerId.SystemOverlay, consumedBy) assertFalse(appRootReceived) @@ -182,24 +217,25 @@ class LiveLayerInteractionPathTests { private fun inspectedRoot(): ContainerNode { val root = ContainerNode(key = "root") root.bounds = Rect(0, 0, 1280, 720) - ContainerNode(key = "child").apply { - bounds = Rect(20, 20, 120, 32) - }.applyParent(root) + ContainerNode(key = "child") + .apply { + bounds = Rect(20, 20, 120, 32) + }.applyParent(root) return root } private class LiveLayerInputHarness( private val debugHandler: (Int, Int, MouseButton) -> Boolean, private val systemOverlayHandler: (Int, Int, MouseButton) -> Boolean, - private val applicationOverlayHandler: (Int, Int, MouseButton) -> Boolean + private val applicationOverlayHandler: (Int, Int, MouseButton) -> Boolean, ) { fun dispatchMouseDown( mouseX: Int, mouseY: Int, button: MouseButton, - applicationRootHandler: () -> Boolean - ): UiLayerId? { - return OverlayLayerContracts.firstInputConsumer( + applicationRootHandler: () -> Boolean, + ): UiLayerId? = + OverlayLayerContracts.firstInputConsumer( canConsume = { layer -> when (layer) { UiLayerId.Debug -> debugHandler(mouseX, mouseY, button) @@ -207,9 +243,7 @@ class LiveLayerInteractionPathTests { UiLayerId.ApplicationOverlay -> applicationOverlayHandler(mouseX, mouseY, button) UiLayerId.ApplicationRoot -> applicationRootHandler() } - } + }, ) - } } } - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayDebugVisualizationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayDebugVisualizationTests.kt index f1d6fff..1490ff9 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayDebugVisualizationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayDebugVisualizationTests.kt @@ -1,22 +1,25 @@ package org.dreamfinity.dsgl.core.overlay -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertFalse -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.DomTree import org.dreamfinity.dsgl.core.debug.OverlayLayerDebugState import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayRootNode import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.StyleApplicationScope +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue class OverlayDebugVisualizationTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -36,16 +39,24 @@ class OverlayDebugVisualizationTests { val appCommands = appTree.paint(ctx, applyStyles = true) val systemCommands = systemTree.paint(ctx, applyStyles = true) - assertFalse(appCommands.any { command -> - command is RenderCommand.DrawRect && - (command.color == OverlayDebugVisualization.applicationOverlayFillColor || - command.color == OverlayDebugVisualization.applicationOverlayBorderColor) - }) - assertFalse(systemCommands.any { command -> - command is RenderCommand.DrawRect && - (command.color == OverlayDebugVisualization.systemOverlayFillColor || - command.color == OverlayDebugVisualization.systemOverlayBorderColor) - }) + assertFalse( + appCommands.any { command -> + command is RenderCommand.DrawRect && + ( + command.color == OverlayDebugVisualization.applicationOverlayFillColor || + command.color == OverlayDebugVisualization.applicationOverlayBorderColor + ) + }, + ) + assertFalse( + systemCommands.any { command -> + command is RenderCommand.DrawRect && + ( + command.color == OverlayDebugVisualization.systemOverlayFillColor || + command.color == OverlayDebugVisualization.systemOverlayBorderColor + ) + }, + ) } @Test @@ -62,16 +73,23 @@ class OverlayDebugVisualizationTests { val appCommands = appTree.paint(ctx, applyStyles = true) val systemCommands = systemTree.paint(ctx, applyStyles = true) - assertTrue(appCommands.any { command -> - command is RenderCommand.DrawRect && - (command.color == OverlayDebugVisualization.applicationOverlayFillColor || - command.color == OverlayDebugVisualization.applicationOverlayBorderColor) - }) - assertTrue(systemCommands.any { command -> - command is RenderCommand.DrawRect && - (command.color == OverlayDebugVisualization.systemOverlayFillColor || - command.color == OverlayDebugVisualization.systemOverlayBorderColor) - }) + assertTrue( + appCommands.any { command -> + command is RenderCommand.DrawRect && + ( + command.color == OverlayDebugVisualization.applicationOverlayFillColor || + command.color == OverlayDebugVisualization.applicationOverlayBorderColor + ) + }, + ) + assertTrue( + systemCommands.any { command -> + command is RenderCommand.DrawRect && + ( + command.color == OverlayDebugVisualization.systemOverlayFillColor || + command.color == OverlayDebugVisualization.systemOverlayBorderColor + ) + }, + ) } } - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayGeometryIntegrationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayGeometryIntegrationTests.kt index b87a15b..87098b0 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayGeometryIntegrationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayGeometryIntegrationTests.kt @@ -1,10 +1,5 @@ package org.dreamfinity.dsgl.core.overlay -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupEngine import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupRequest import org.dreamfinity.dsgl.core.colorpicker.ColorPickerState @@ -15,13 +10,21 @@ import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayHost import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.StyleEngine +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue class OverlayGeometryIntegrationTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @Test fun `system overlay root uses full game viewport bounds in live host path`() { @@ -55,8 +58,8 @@ class OverlayGeometryIntegrationTests { ColorPickerPopupRequest( owner = owner, anchorRect = anchor, - state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false) - ) + state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), + ), ) val panel = engine.debugPanelRect(owner) @@ -77,8 +80,8 @@ class OverlayGeometryIntegrationTests { ColorPickerPopupRequest( owner = owner, anchorRect = anchor, - state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false) - ) + state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), + ), ) val preFramePanel = engine.debugPanelRect(owner) assertNotNull(preFramePanel) @@ -103,8 +106,8 @@ class OverlayGeometryIntegrationTests { ColorPickerPopupRequest( owner = owner, anchorRect = anchor, - state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false) - ) + state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), + ), ) val panel = engine.debugPanelRect(owner) @@ -123,8 +126,8 @@ class OverlayGeometryIntegrationTests { ColorPickerPopupRequest( owner = owner, anchorRect = Rect(420, 260, 24, 18), - state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false) - ) + state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), + ), ) engine.forcePanelRect(owner, Rect(980, 200, 320, 340)) engine.close(owner) @@ -134,8 +137,8 @@ class OverlayGeometryIntegrationTests { ColorPickerPopupRequest( owner = owner, anchorRect = Rect(20, 20, 10, 10), - state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false) - ) + state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), + ), ) val reopened = engine.debugPanelRect(owner) assertNotNull(reopened) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContractsTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContractsTests.kt index da5635a..eff2c41 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContractsTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContractsTests.kt @@ -14,7 +14,7 @@ class OverlayLayerContractsTests { fun `paint order is app root then app overlay then system overlay then debug`() { assertEquals( listOf(UiLayerId.ApplicationRoot, UiLayerId.ApplicationOverlay, UiLayerId.SystemOverlay, UiLayerId.Debug), - OverlayLayerContracts.paintOrder + OverlayLayerContracts.paintOrder, ) } @@ -22,17 +22,20 @@ class OverlayLayerContractsTests { fun `input priority is debug then system overlay then app overlay then app root`() { assertEquals( listOf(UiLayerId.Debug, UiLayerId.SystemOverlay, UiLayerId.ApplicationOverlay, UiLayerId.ApplicationRoot), - OverlayLayerContracts.inputPriority + OverlayLayerContracts.inputPriority, ) } @Test fun `firstInputConsumer respects configured input priority`() { - val consumed = OverlayLayerContracts.firstInputConsumer( - canConsume = { layer -> - layer == UiLayerId.Debug || layer == UiLayerId.ApplicationOverlay || layer == UiLayerId.ApplicationRoot - } - ) + val consumed = + OverlayLayerContracts.firstInputConsumer( + canConsume = { layer -> + layer == UiLayerId.Debug || + layer == UiLayerId.ApplicationOverlay || + layer == UiLayerId.ApplicationRoot + }, + ) assertEquals(UiLayerId.Debug, consumed) } @@ -44,26 +47,30 @@ class OverlayLayerContractsTests { @Test fun `transient ownership uses owner scope and not cursor position`() { - val appAtA = OverlayLayerContracts.resolveTransientLayer( - ownerScope = OverlayOwnerScope.Application, - cursorX = 10, - cursorY = 20 - ) - val appAtB = OverlayLayerContracts.resolveTransientLayer( - ownerScope = OverlayOwnerScope.Application, - cursorX = 800, - cursorY = 640 - ) - val systemAtA = OverlayLayerContracts.resolveTransientLayer( - ownerScope = OverlayOwnerScope.System, - cursorX = 10, - cursorY = 20 - ) - val systemAtB = OverlayLayerContracts.resolveTransientLayer( - ownerScope = OverlayOwnerScope.System, - cursorX = 800, - cursorY = 640 - ) + val appAtA = + OverlayLayerContracts.resolveTransientLayer( + ownerScope = OverlayOwnerScope.Application, + cursorX = 10, + cursorY = 20, + ) + val appAtB = + OverlayLayerContracts.resolveTransientLayer( + ownerScope = OverlayOwnerScope.Application, + cursorX = 800, + cursorY = 640, + ) + val systemAtA = + OverlayLayerContracts.resolveTransientLayer( + ownerScope = OverlayOwnerScope.System, + cursorX = 10, + cursorY = 20, + ) + val systemAtB = + OverlayLayerContracts.resolveTransientLayer( + ownerScope = OverlayOwnerScope.System, + cursorX = 800, + cursorY = 640, + ) assertEquals(UiLayerId.ApplicationOverlay, appAtA) assertEquals(UiLayerId.ApplicationOverlay, appAtB) assertEquals(UiLayerId.SystemOverlay, systemAtA) @@ -101,12 +108,15 @@ class OverlayLayerContractsTests { systemOverlay = system, debug = debug, out = out, - shouldRenderLayer = { layer -> layer != UiLayerId.ApplicationOverlay } + shouldRenderLayer = { layer -> layer != UiLayerId.ApplicationOverlay }, ) - assertEquals(listOf(0xFF000001.toInt(), 0xFF000003.toInt(), 0xFF000004.toInt()), out.map { - (it as RenderCommand.DrawRect).color - }) + assertEquals( + listOf(0xFF000001.toInt(), 0xFF000003.toInt(), 0xFF000004.toInt()), + out.map { + (it as RenderCommand.DrawRect).color + }, + ) } @Test @@ -123,24 +133,28 @@ class OverlayLayerContractsTests { systemOverlay = system, debug = debug, out = out, - shouldRenderLayer = { layer -> layer != UiLayerId.SystemOverlay } + shouldRenderLayer = { layer -> layer != UiLayerId.SystemOverlay }, ) - assertEquals(listOf(0xFF000001.toInt(), 0xFF000002.toInt(), 0xFF000004.toInt()), out.map { - (it as RenderCommand.DrawRect).color - }) + assertEquals( + listOf(0xFF000001.toInt(), 0xFF000002.toInt(), 0xFF000004.toInt()), + out.map { + (it as RenderCommand.DrawRect).color + }, + ) } @Test fun `firstInputConsumer skips app overlay input when disabled`() { val order = ArrayList() - val consumed = OverlayLayerContracts.firstInputConsumer( - canConsume = { layer -> - order += layer - layer == UiLayerId.ApplicationOverlay || layer == UiLayerId.ApplicationRoot - }, - isLayerInputEnabled = { layer -> layer != UiLayerId.ApplicationOverlay } - ) + val consumed = + OverlayLayerContracts.firstInputConsumer( + canConsume = { layer -> + order += layer + layer == UiLayerId.ApplicationOverlay || layer == UiLayerId.ApplicationRoot + }, + isLayerInputEnabled = { layer -> layer != UiLayerId.ApplicationOverlay }, + ) assertEquals(UiLayerId.ApplicationRoot, consumed) assertEquals(listOf(UiLayerId.Debug, UiLayerId.SystemOverlay, UiLayerId.ApplicationRoot), order) } @@ -148,36 +162,39 @@ class OverlayLayerContractsTests { @Test fun `firstInputConsumer skips system overlay input when disabled`() { val order = ArrayList() - val consumed = OverlayLayerContracts.firstInputConsumer( - canConsume = { layer -> - order += layer - layer == UiLayerId.SystemOverlay || layer == UiLayerId.ApplicationRoot - }, - isLayerInputEnabled = { layer -> layer != UiLayerId.SystemOverlay } - ) + val consumed = + OverlayLayerContracts.firstInputConsumer( + canConsume = { layer -> + order += layer + layer == UiLayerId.SystemOverlay || layer == UiLayerId.ApplicationRoot + }, + isLayerInputEnabled = { layer -> layer != UiLayerId.SystemOverlay }, + ) assertEquals(UiLayerId.ApplicationRoot, consumed) assertEquals(listOf(UiLayerId.Debug, UiLayerId.ApplicationOverlay, UiLayerId.ApplicationRoot), order) } @Test fun `color picker popup defaults to application overlay ownership`() { - val request = ColorPickerPopupRequest( - owner = "owner", - anchorRect = Rect(10, 12, 20, 18), - state = ColorPickerState(color = RgbaColor.WHITE) - ) + val request = + ColorPickerPopupRequest( + owner = "owner", + anchorRect = Rect(10, 12, 20, 18), + state = ColorPickerState(color = RgbaColor.WHITE), + ) assertEquals(OverlayOwnerScope.Application, request.ownerScope) assertEquals(UiLayerId.ApplicationOverlay, ColorPickerPopupOverlayOwnership.resolveLayer(request)) } @Test fun `system-owned color picker popup resolves to system overlay`() { - val request = ColorPickerPopupRequest( - owner = "owner", - ownerScope = OverlayOwnerScope.System, - anchorRect = Rect(10, 12, 20, 18), - state = ColorPickerState(color = RgbaColor.WHITE) - ) + val request = + ColorPickerPopupRequest( + owner = "owner", + ownerScope = OverlayOwnerScope.System, + anchorRect = Rect(10, 12, 20, 18), + state = ColorPickerState(color = RgbaColor.WHITE), + ) assertEquals(UiLayerId.SystemOverlay, ColorPickerPopupOverlayOwnership.resolveLayer(request)) } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouterTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouterTests.kt index 589814d..b8af560 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouterTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouterTests.kt @@ -1,19 +1,14 @@ package org.dreamfinity.dsgl.core.overlay.input -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.dom.applyParent -import org.dreamfinity.dsgl.core.dom.onInput import org.dreamfinity.dsgl.core.dom.elements.ButtonNode import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.elements.RangeInputNode -import org.dreamfinity.dsgl.core.dom.elements.TextInputNode import org.dreamfinity.dsgl.core.dom.elements.TextAreaNode +import org.dreamfinity.dsgl.core.dom.elements.TextInputNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.dom.onInput import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.KeyModifiers @@ -22,15 +17,22 @@ import org.dreamfinity.dsgl.core.input.ClipboardAccess import org.dreamfinity.dsgl.core.input.ClipboardBridge import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.Overflow +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue class LayerDomInputRouterTests { private val clipboard = RecordingClipboardAccess() - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) {} - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) {} + } @AfterTest fun cleanup() { @@ -45,9 +47,10 @@ class LayerDomInputRouterTests { listOf("app-dom", "app-overlay", "system-overlay").forEach { layer -> clipboard.value = "" val (root, router) = createLayerRouter(layer) - val input = TextInputNode(text = "abcdef", key = "$layer-input").apply { - bounds = Rect(20, 20, 180, 24) - } + val input = + TextInputNode(text = "abcdef", key = "$layer-input").apply { + bounds = Rect(20, 20, 180, 24) + } input.applyParent(root) assertTrue(router.handleMouseDown(24, 24, MouseButton.LEFT)) @@ -93,16 +96,18 @@ class LayerDomInputRouterTests { var underClicks = 0 var topClicks = 0 - val under = ButtonNode("under", key = "$layer-under").apply { - bounds = Rect(40, 40, 120, 24) - onClick { underClicks += 1 } - } + val under = + ButtonNode("under", key = "$layer-under").apply { + bounds = Rect(40, 40, 120, 24) + onClick { underClicks += 1 } + } under.applyParent(root) - val top = ButtonNode("top", key = "$layer-top").apply { - bounds = Rect(40, 40, 120, 24) - onClick { topClicks += 1 } - } + val top = + ButtonNode("top", key = "$layer-top").apply { + bounds = Rect(40, 40, 120, 24) + onClick { topClicks += 1 } + } top.applyParent(root) assertTrue(router.handleMouseMove(50, 48)) @@ -119,13 +124,15 @@ class LayerDomInputRouterTests { val (rootA, routerA) = createLayerRouter("layer-a") val (rootB, routerB) = createLayerRouter("layer-b") - val inputA = TextInputNode(text = "a", key = "a-input").apply { - bounds = Rect(10, 10, 100, 20) - } + val inputA = + TextInputNode(text = "a", key = "a-input").apply { + bounds = Rect(10, 10, 100, 20) + } inputA.applyParent(rootA) - val inputB = TextInputNode(text = "b", key = "b-input").apply { - bounds = Rect(10, 10, 100, 20) - } + val inputB = + TextInputNode(text = "b", key = "b-input").apply { + bounds = Rect(10, 10, 100, 20) + } inputB.applyParent(rootB) FocusManager.requestFocus(inputA) @@ -140,10 +147,11 @@ class LayerDomInputRouterTests { listOf("header-drag", "thumb-drag").forEach { key -> val (root, router) = createLayerRouter(key) var dragEvents = 0 - val dragNode = ContainerNode(key = "$key-node").apply { - bounds = Rect(60, 20, 90, 20) - onMouseDrag = { dragEvents += 1 } - } + val dragNode = + ContainerNode(key = "$key-node").apply { + bounds = Rect(60, 20, 90, 20) + onMouseDrag = { dragEvents += 1 } + } dragNode.applyParent(root) assertTrue(router.handleMouseDown(64, 28, MouseButton.LEFT)) @@ -158,16 +166,18 @@ class LayerDomInputRouterTests { val (root, router) = createLayerRouter("drag-release") var buttonClicks = 0 - val dragSurface = ContainerNode(key = "drag-surface").apply { - bounds = Rect(20, 20, 80, 24) - onMouseMove = {} - } + val dragSurface = + ContainerNode(key = "drag-surface").apply { + bounds = Rect(20, 20, 80, 24) + onMouseMove = {} + } dragSurface.applyParent(root) - val releaseButton = ButtonNode("release", key = "release-button").apply { - bounds = Rect(120, 20, 100, 24) - onClick { buttonClicks += 1 } - } + val releaseButton = + ButtonNode("release", key = "release-button").apply { + bounds = Rect(120, 20, 100, 24) + onClick { buttonClicks += 1 } + } releaseButton.applyParent(root) assertTrue(router.handleMouseDown(24, 24, MouseButton.LEFT)) @@ -180,10 +190,11 @@ class LayerDomInputRouterTests { fun `unkeyed drag capture remains active across pointer move`() { val (root, router) = createLayerRouter("unkeyed-drag") var dragEvents = 0 - val dragNode = ContainerNode().apply { - bounds = Rect(60, 20, 90, 20) - onMouseDrag = { dragEvents += 1 } - } + val dragNode = + ContainerNode().apply { + bounds = Rect(60, 20, 90, 20) + onMouseDrag = { dragEvents += 1 } + } dragNode.applyParent(root) assertTrue(router.handleMouseDown(64, 28, MouseButton.LEFT)) @@ -205,7 +216,6 @@ class LayerDomInputRouterTests { assertTrue(range.value > 0L) } - @Test fun `range input drag survives unkeyed rerender replacement`() { val (root, router) = createLayerRouter("range-rerender") @@ -231,15 +241,17 @@ class LayerDomInputRouterTests { assertTrue(router.handleMouseUp(220, 26, MouseButton.LEFT)) assertEquals(100L, model) } + @Test fun `mouse up stays consumed after press when pointer is released outside targets`() { val (root, router) = createLayerRouter("outside-release") var buttonClicks = 0 - val button = ButtonNode("press", key = "outside-release-button").apply { - bounds = Rect(20, 20, 100, 24) - onClick { buttonClicks += 1 } - } + val button = + ButtonNode("press", key = "outside-release-button").apply { + bounds = Rect(20, 20, 100, 24) + onClick { buttonClicks += 1 } + } button.applyParent(root) assertTrue(router.handleMouseDown(24, 24, MouseButton.LEFT)) @@ -253,46 +265,52 @@ class LayerDomInputRouterTests { val (root, router) = createLayerRouter("wheel-bubble") var wheelEvents = 0 - val scrollHost = ContainerNode(key = "wheel-host").apply { - bounds = Rect(10, 10, 220, 140) - onMouseWheel = { event -> - wheelEvents += 1 - event.cancelled = true + val scrollHost = + ContainerNode(key = "wheel-host").apply { + bounds = Rect(10, 10, 220, 140) + onMouseWheel = { event -> + wheelEvents += 1 + event.cancelled = true + } } - } scrollHost.applyParent(root) - val input = TextInputNode(text = "value", key = "wheel-input").apply { - bounds = Rect(24, 24, 150, 24) - } + val input = + TextInputNode(text = "value", key = "wheel-input").apply { + bounds = Rect(24, 24, 150, 24) + } input.applyParent(scrollHost) assertTrue(router.handleMouseDown(28, 28, MouseButton.LEFT)) assertTrue(router.handleMouseWheel(28, 28, -120)) assertEquals(1, wheelEvents) } + @Test fun `focused textarea does not steal wheel from hovered control`() { val (root, router) = createLayerRouter("wheel-focused-textarea") var hostWheelEvents = 0 - val scrollHost = ContainerNode(key = "wheel-focused-host").apply { - bounds = Rect(10, 10, 260, 180) - onMouseWheel = { event -> - hostWheelEvents += 1 - event.cancelled = true + val scrollHost = + ContainerNode(key = "wheel-focused-host").apply { + bounds = Rect(10, 10, 260, 180) + onMouseWheel = { event -> + hostWheelEvents += 1 + event.cancelled = true + } } - } scrollHost.applyParent(root) - val textArea = TextAreaNode(text = "first\nsecond\nthird", key = "wheel-focused-textarea").apply { - bounds = Rect(24, 24, 160, 48) - } + val textArea = + TextAreaNode(text = "first\nsecond\nthird", key = "wheel-focused-textarea").apply { + bounds = Rect(24, 24, 160, 48) + } textArea.applyParent(scrollHost) - val input = TextInputNode(text = "value", key = "wheel-focused-input").apply { - bounds = Rect(24, 92, 150, 24) - } + val input = + TextInputNode(text = "value", key = "wheel-focused-input").apply { + bounds = Rect(24, 92, 150, 24) + } input.applyParent(scrollHost) assertTrue(router.handleMouseDown(28, 28, MouseButton.LEFT)) @@ -305,31 +323,36 @@ class LayerDomInputRouterTests { @Test fun `wheel without cancellation is not consumed`() { val (root, router) = createLayerRouter("wheel-unhandled") - val input = TextInputNode(text = "value", key = "wheel-unhandled-input").apply { - bounds = Rect(24, 24, 150, 24) - } + val input = + TextInputNode(text = "value", key = "wheel-unhandled-input").apply { + bounds = Rect(24, 24, 150, 24) + } input.applyParent(root) assertTrue(router.handleMouseDown(28, 28, MouseButton.LEFT)) assertFalse(router.handleMouseWheel(28, 28, -120)) } + @Test fun `wheel axis semantics stay consistent across layers`() { listOf("app-dom", "app-overlay", "system-overlay").forEach { layer -> val (root, router) = createLayerRouter("wheel-axis-$layer") - val viewport = ContainerNode(key = "$layer-scroll").apply { - bounds = Rect(20, 20, 150, 70) - overflowX = Overflow.Auto - overflowY = Overflow.Auto - } + val viewport = + ContainerNode(key = "$layer-scroll").apply { + bounds = Rect(20, 20, 150, 70) + overflowX = Overflow.Auto + overflowY = Overflow.Auto + } viewport.applyParent(root) - ContainerNode(key = "$layer-scroll-content").apply { - bounds = Rect(20, 20, 320, 220) - }.applyParent(viewport) - val wheelTarget = ButtonNode("wheel", key = "$layer-wheel-target").apply { - bounds = Rect(26, 26, 64, 20) - onClick { } - } + ContainerNode(key = "$layer-scroll-content") + .apply { + bounds = Rect(20, 20, 320, 220) + }.applyParent(viewport) + val wheelTarget = + ButtonNode("wheel", key = "$layer-wheel-target").apply { + bounds = Rect(26, 26, 64, 20) + onClick { } + } wheelTarget.applyParent(viewport) val wheelX = wheelTarget.bounds.x + 1 @@ -353,10 +376,12 @@ class LayerDomInputRouterTests { assertEquals(0, horizontalState.scrollY) } } + private fun createLayerRouter(key: String): Pair { - val root = ContainerNode(key = "$key-root").apply { - bounds = Rect(0, 0, 320, 200) - } + val root = + ContainerNode(key = "$key-root").apply { + bounds = Rect(0, 0, 320, 200) + } return root to LayerDomInputRouter { root } } @@ -370,4 +395,3 @@ class LayerDomInputRouterTests { } } } - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanelTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanelTests.kt index 5a7564c..8bdd57a 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanelTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanelTests.kt @@ -1,36 +1,44 @@ package org.dreamfinity.dsgl.core.overlay.panel -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.Size import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.render.RenderCommand +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue class OverlayPanelTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 - @Test - fun `panel uses native node path and does not expose legacy append commands api`() { - assertTrue(OverlayPanel::class.java.methods.none { it.name == "appendCommands" }) + override fun measureText(text: String): Int = text.length * 6 - val panelState = OverlayPanelState().apply { - updateFromRect(Rect(30, 40, 240, 180)) + override fun paint(commands: List) = Unit } - val panel = OverlayPanel( - ownerId = "demo-owner", - panelState = panelState, - dragSession = OverlayPanelDragSession() + + @Test + fun `panel uses native node path and does not expose legacy append commands api`() { + assertTrue( + OverlayPanel::class.java.methods + .none { it.name == "appendCommands" }, ) + + val panelState = + OverlayPanelState().apply { + updateFromRect(Rect(30, 40, 240, 180)) + } + val panel = + OverlayPanel( + ownerId = "demo-owner", + panelState = panelState, + dragSession = OverlayPanelDragSession(), + ) panel.configure(title = "Demo", draggable = true) panel.setBodyContent(FillNode("body")) @@ -46,15 +54,17 @@ class OverlayPanelTests { @Test fun `panel drag keeps persistent drag session and updates panel state`() { - val panelState = OverlayPanelState().apply { - updateFromRect(Rect(60, 70, 260, 180)) - } + val panelState = + OverlayPanelState().apply { + updateFromRect(Rect(60, 70, 260, 180)) + } val dragSession = OverlayPanelDragSession() - val panel = OverlayPanel( - ownerId = "drag-owner", - panelState = panelState, - dragSession = dragSession - ) + val panel = + OverlayPanel( + ownerId = "drag-owner", + panelState = panelState, + dragSession = dragSession, + ) panel.configure(title = "Drag", draggable = true) val header = panel.headerRect() ?: error("header rect missing") @@ -70,10 +80,10 @@ class OverlayPanelTests { mouseX = startX + 42, mouseY = startY + 26, viewportWidth = 1200, - viewportHeight = 800 + viewportHeight = 800, ) { rect -> lastRect = rect - } + }, ) val movedRect = panelState.currentRectOrNull() ?: error("panel rect missing") assertNotNull(lastRect) @@ -86,10 +96,10 @@ class OverlayPanelTests { mouseY = startY + 26, button = MouseButton.LEFT, viewportWidth = 1200, - viewportHeight = 800 + viewportHeight = 800, ) { rect -> lastRect = rect - } + }, ) assertFalse(dragSession.active) assertEquals(null, dragSession.ownerId) @@ -98,14 +108,16 @@ class OverlayPanelTests { @Test fun `panel close button invokes close callback`() { - val panelState = OverlayPanelState().apply { - updateFromRect(Rect(12, 20, 220, 140)) - } - val panel = OverlayPanel( - ownerId = Any(), - panelState = panelState, - dragSession = OverlayPanelDragSession() - ) + val panelState = + OverlayPanelState().apply { + updateFromRect(Rect(12, 20, 220, 140)) + } + val panel = + OverlayPanel( + ownerId = Any(), + panelState = panelState, + dragSession = OverlayPanelDragSession(), + ) var closed = 0 panel.configure(title = "Closable", draggable = true, onClose = { closed += 1 }) val closeRect = panel.closeRect() ?: error("close rect missing") @@ -115,13 +127,17 @@ class OverlayPanelTests { } private class FillNode( - key: Any? + key: Any?, ) : DOMNode(key) { - override fun measure(ctx: UiMeasureContext): Size { - return Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - } + override fun measure(ctx: UiMeasureContext): Size = Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt index b12fc87..3f68d68 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt @@ -21,11 +21,14 @@ import org.dreamfinity.dsgl.core.style.StyleProperty import kotlin.test.* class InspectorDragScrollDomMigrationTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -52,7 +55,12 @@ class InspectorDragScrollDomMigrationTests { assertFalse(fixture.inspector.isDraggingPanel) assertFalse(fixture.inspector.isPointerCaptured) - assertTrue(fixture.host.debugEntryState(SystemOverlayEntryId.Inspector)?.dragSession?.active == true) + assertTrue( + fixture.host + .debugEntryState(SystemOverlayEntryId.Inspector) + ?.dragSession + ?.active == true, + ) assertTrue(fixture.host.handleMouseMove(moveX, moveY)) syncAndRender(fixture, moveX, moveY) @@ -64,7 +72,12 @@ class InspectorDragScrollDomMigrationTests { assertFalse(fixture.inspector.isDraggingPanel) assertFalse(fixture.inspector.isPointerCaptured) - assertFalse(fixture.host.debugEntryState(SystemOverlayEntryId.Inspector)?.dragSession?.active == true) + assertFalse( + fixture.host + .debugEntryState(SystemOverlayEntryId.Inspector) + ?.dragSession + ?.active == true, + ) } @Test @@ -84,7 +97,7 @@ class InspectorDragScrollDomMigrationTests { val afterDomDrag = fixture.inspector.overlayPanelRect() ?: error("expected moved panel rect") assertTrue( afterDomDrag.x > before.x || afterDomDrag.y > before.y, - "expected drag move to advance panel: before=$before afterDomDrag=$afterDomDrag" + "expected drag move to advance panel: before=$before afterDomDrag=$afterDomDrag", ) syncAndRender(fixture, downX, downY) @@ -93,13 +106,13 @@ class InspectorDragScrollDomMigrationTests { if (afterDomDrag.x > before.x) { assertTrue( afterStaleSync.x >= afterDomDrag.x, - "stale sync cursor regressed panel x: before=$before dom=$afterDomDrag stale=$afterStaleSync" + "stale sync cursor regressed panel x: before=$before dom=$afterDomDrag stale=$afterStaleSync", ) } if (afterDomDrag.y > before.y) { assertTrue( afterStaleSync.y >= afterDomDrag.y, - "stale sync cursor regressed panel y: before=$before dom=$afterDomDrag stale=$afterStaleSync" + "stale sync cursor regressed panel y: before=$before dom=$afterDomDrag stale=$afterStaleSync", ) } @@ -108,12 +121,17 @@ class InspectorDragScrollDomMigrationTests { val afterNextMove = fixture.inspector.overlayPanelRect() ?: error("expected panel rect after next drag move") assertTrue( afterNextMove.x >= afterStaleSync.x && afterNextMove.y >= afterStaleSync.y, - "expected monotonic drag progression across sync/render cycle: stale=$afterStaleSync next=$afterNextMove" + "expected monotonic drag progression across sync/render cycle: stale=$afterStaleSync next=$afterNextMove", ) assertTrue(fixture.host.handleMouseUp(dragX + 28, dragY + 16, MouseButton.LEFT)) syncAndRender(fixture, dragX + 28, dragY + 16) - assertFalse(fixture.host.debugEntryState(SystemOverlayEntryId.Inspector)?.dragSession?.active == true) + assertFalse( + fixture.host + .debugEntryState(SystemOverlayEntryId.Inspector) + ?.dragSession + ?.active == true, + ) } @Test @@ -133,7 +151,12 @@ class InspectorDragScrollDomMigrationTests { assertTrue(after > before) assertFalse(fixture.inspector.isPointerCaptured) assertFalse(fixture.inspector.isDraggingPanel) - assertFalse(fixture.host.debugEntryState(SystemOverlayEntryId.Inspector)?.dragSession?.active == true) + assertFalse( + fixture.host + .debugEntryState(SystemOverlayEntryId.Inspector) + ?.dragSession + ?.active == true, + ) } @Test @@ -154,7 +177,12 @@ class InspectorDragScrollDomMigrationTests { assertFalse(fixture.inspector.isPointerCaptured) assertFalse(fixture.inspector.isDraggingPanel) - assertFalse(fixture.host.debugEntryState(SystemOverlayEntryId.Inspector)?.dragSession?.active == true) + assertFalse( + fixture.host + .debugEntryState(SystemOverlayEntryId.Inspector) + ?.dragSession + ?.active == true, + ) assertTrue(fixture.host.handleMouseMove(dragX, startY + 40)) syncAndRender(fixture, dragX, startY + 40) @@ -163,7 +191,12 @@ class InspectorDragScrollDomMigrationTests { assertTrue(fixture.host.handleMouseUp(dragX, startY + 40, MouseButton.LEFT)) syncAndRender(fixture, dragX, startY + 40) - assertFalse(fixture.host.debugEntryState(SystemOverlayEntryId.Inspector)?.dragSession?.active == true) + assertFalse( + fixture.host + .debugEntryState(SystemOverlayEntryId.Inspector) + ?.dragSession + ?.active == true, + ) } @Test @@ -179,14 +212,24 @@ class InspectorDragScrollDomMigrationTests { val startY = thumb.y + thumb.height / 2 assertTrue(fixture.host.handleMouseDown(dragX, startY, MouseButton.LEFT)) syncAndRender(fixture, dragX, startY) - assertFalse(fixture.host.debugEntryState(SystemOverlayEntryId.Inspector)?.dragSession?.active == true) + assertFalse( + fixture.host + .debugEntryState(SystemOverlayEntryId.Inspector) + ?.dragSession + ?.active == true, + ) assertTrue(fixture.host.handleMouseMove(dragX, startY + 22)) syncAndRender(fixture, dragX, startY + 22) val afterFirstMove = fixture.inspector.panelScrollOffsetY syncAndRender(fixture, dragX, startY + 22) - assertFalse(fixture.host.debugEntryState(SystemOverlayEntryId.Inspector)?.dragSession?.active == true) + assertFalse( + fixture.host + .debugEntryState(SystemOverlayEntryId.Inspector) + ?.dragSession + ?.active == true, + ) assertTrue(fixture.host.handleMouseMove(dragX, startY + 54)) syncAndRender(fixture, dragX, startY + 54) @@ -195,7 +238,12 @@ class InspectorDragScrollDomMigrationTests { assertTrue(fixture.host.handleMouseUp(dragX, startY + 54, MouseButton.LEFT)) syncAndRender(fixture, dragX, startY + 54) - assertFalse(fixture.host.debugEntryState(SystemOverlayEntryId.Inspector)?.dragSession?.active == true) + assertFalse( + fixture.host + .debugEntryState(SystemOverlayEntryId.Inspector) + ?.dragSession + ?.active == true, + ) } @Test @@ -210,7 +258,12 @@ class InspectorDragScrollDomMigrationTests { syncAndRender(fixture, downX, downY) assertFalse(fixture.inspector.isDraggingPanel) assertFalse(fixture.inspector.isPointerCaptured) - assertTrue(fixture.host.debugEntryState(SystemOverlayEntryId.Inspector)?.dragSession?.active == true) + assertTrue( + fixture.host + .debugEntryState(SystemOverlayEntryId.Inspector) + ?.dragSession + ?.active == true, + ) assertTrue(fixture.host.handleMouseUp(downX, downY, MouseButton.LEFT)) syncAndRender(fixture, downX, downY) @@ -237,8 +290,11 @@ class InspectorDragScrollDomMigrationTests { fun `dropdown migration remains intact after drag-scroll migration`() { val fixture = openInspectorAndSelectTarget(withManyChildren = false) val row = findVisibleSelectRow(fixture) - val rowIndex = fixture.inspector.overlayStyleEditorRows().indexOfFirst { it.property == row.property } - .takeIf { it >= 0 } ?: error("expected style row index for ${row.property.key}") + val rowIndex = + fixture.inspector + .overlayStyleEditorRows() + .indexOfFirst { it.property == row.property } + .takeIf { it >= 0 } ?: error("expected style row index for ${row.property.key}") val ownerKey = "dsgl-system-inspector-editor-select-$rowIndex" val trigger = visibleControlRect(fixture, row) val clickX = trigger.x + 2 @@ -282,7 +338,7 @@ class InspectorDragScrollDomMigrationTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) host.paint(ctx) @@ -290,14 +346,15 @@ class InspectorDragScrollDomMigrationTests { assertTrue(host.handleMouseUp(984, 144, MouseButton.LEFT)) inspector.setPickMode(false) - val fixture = Fixture( - inspector = inspector, - host = host, - root = root, - revision = 2L, - viewportWidth = 1280, - viewportHeight = 720 - ) + val fixture = + Fixture( + inspector = inspector, + host = host, + root = root, + revision = 2L, + viewportWidth = 1280, + viewportHeight = 720, + ) syncAndRender(fixture, 984, 144) return fixture } @@ -315,7 +372,7 @@ class InspectorDragScrollDomMigrationTests { inspectedLayoutRevision = fixture.revision++, cursorX = cursorX, cursorY = cursorY, - inspectorPointerCaptured = fixture.inspector.isPointerCaptured + inspectorPointerCaptured = fixture.inspector.isPointerCaptured, ) fixture.host.render(ctx, fixture.viewportWidth, fixture.viewportHeight) fixture.host.paint(ctx) @@ -333,22 +390,25 @@ class InspectorDragScrollDomMigrationTests { private fun findVisibleSelectRow(fixture: Fixture): InspectorStyleEditorRowSnapshot { repeat(120) { - val rows = fixture.inspector.overlayStyleEditorRows().filter { row -> - row.editorKind == InspectorEditorKind.EnumSelect || row.editorKind == InspectorEditorKind.FontSelect - } + val rows = + fixture.inspector.overlayStyleEditorRows().filter { row -> + row.editorKind == InspectorEditorKind.EnumSelect || row.editorKind == InspectorEditorKind.FontSelect + } val contentRect = fixture.inspector.overlayContentRect() val bodyScrollY = fixture.inspector.panelScrollOffsetY - rows.firstOrNull { row -> - val rect = Rect( - row.controlRect.x, - row.controlRect.y - bodyScrollY, - row.controlRect.width, - row.controlRect.height - ) - val centerX = rect.x + (rect.width / 2).coerceAtLeast(1) - val centerY = rect.y + (rect.height / 2).coerceAtLeast(1) - contentRect.contains(centerX, centerY) - }?.let { return it } + rows + .firstOrNull { row -> + val rect = + Rect( + row.controlRect.x, + row.controlRect.y - bodyScrollY, + row.controlRect.width, + row.controlRect.height, + ) + val centerX = rect.x + (rect.width / 2).coerceAtLeast(1) + val centerY = rect.y + (rect.height / 2).coerceAtLeast(1) + contentRect.contains(centerX, centerY) + }?.let { return it } scrollInspectorBodyDown(fixture, steps = 1) } @@ -361,7 +421,7 @@ class InspectorDragScrollDomMigrationTests { row.controlRect.x, row.controlRect.y - bodyScrollY, row.controlRect.width, - row.controlRect.height + row.controlRect.height, ) } @@ -384,12 +444,14 @@ class InspectorDragScrollDomMigrationTests { } private fun findVisibleInputNode(fixture: Fixture, propertyKey: String): TextInputNode { - val inspectorNode = fixture.host.debugEntryNode(SystemOverlayEntryId.Inspector) - ?: error("inspector entry missing") + val inspectorNode = + fixture.host.debugEntryNode(SystemOverlayEntryId.Inspector) + ?: error("inspector entry missing") val contentRect = fixture.inspector.overlayContentRect() - val candidates = collectNodes(inspectorNode) - .filterIsInstance() - .filter { (it.key?.toString() ?: "") == "dsgl-system-inspector-editor-numeric-input-$propertyKey" } + val candidates = + collectNodes(inspectorNode) + .filterIsInstance() + .filter { (it.key?.toString() ?: "") == "dsgl-system-inspector-editor-numeric-input-$propertyKey" } return candidates.firstOrNull { node -> val probeX = node.bounds.x + 2 @@ -401,15 +463,17 @@ class InspectorDragScrollDomMigrationTests { private fun inspectedRoot(withManyChildren: Boolean): ContainerNode { val root = ContainerNode(key = "root") root.bounds = Rect(0, 0, 1280, 720) - val target = ContainerNode(key = "target").apply { - bounds = Rect(980, 140, 120, 30) - } + val target = + ContainerNode(key = "target").apply { + bounds = Rect(980, 140, 120, 30) + } target.applyParent(root) if (withManyChildren) { repeat(60) { index -> - ContainerNode(key = "child-$index").apply { - bounds = Rect(980, 180 + index * 12, 180, 10) - }.applyParent(target) + ContainerNode(key = "child-$index") + .apply { + bounds = Rect(980, 180 + index * 12, 180, 10) + }.applyParent(target) } } StyleEngine.setInspectorOverrideLiteral(target, StyleProperty.BACKGROUND_COLOR, "#FF112233").getOrThrow() @@ -418,6 +482,7 @@ class InspectorDragScrollDomMigrationTests { private fun collectNodes(root: DOMNode): List { val out = ArrayList() + fun walk(node: DOMNode) { out += node node.children.forEach(::walk) @@ -432,8 +497,6 @@ class InspectorDragScrollDomMigrationTests { val root: ContainerNode, var revision: Long, var viewportWidth: Int, - var viewportHeight: Int + var viewportHeight: Int, ) } - - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt index a012a2f..09e52e0 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt @@ -20,11 +20,14 @@ import org.dreamfinity.dsgl.core.style.StyleProperty import kotlin.test.* class InspectorDropdownCorrectiveTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -96,8 +99,9 @@ class InspectorDropdownCorrectiveTests { val (trigger, ownerKey) = openInspectorSelectDropdown(fixture, requireScrollable = false) val popup = selectPanelRect(ownerKey, fixture) - val expectedX = trigger.x - .coerceIn(2, (fixture.viewportWidth - popup.width - 2).coerceAtLeast(2)) + val expectedX = + trigger.x + .coerceIn(2, (fixture.viewportWidth - popup.width - 2).coerceAtLeast(2)) assertEquals(expectedX, popup.x) } @@ -151,7 +155,7 @@ class InspectorDropdownCorrectiveTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) host.paint(ctx) @@ -159,14 +163,15 @@ class InspectorDropdownCorrectiveTests { assertTrue(host.handleMouseUp(984, 144, MouseButton.LEFT)) inspector.setPickMode(false) - val fixture = Fixture( - inspector = inspector, - host = host, - root = root, - revision = 2L, - viewportWidth = 1280, - viewportHeight = 720 - ) + val fixture = + Fixture( + inspector = inspector, + host = host, + root = root, + revision = 2L, + viewportWidth = 1280, + viewportHeight = 720, + ) syncAndRender(fixture, 984, 144) return fixture } @@ -184,7 +189,7 @@ class InspectorDropdownCorrectiveTests { inspectedLayoutRevision = fixture.revision++, cursorX = cursorX, cursorY = cursorY, - inspectorPointerCaptured = fixture.inspector.isPointerCaptured + inspectorPointerCaptured = fixture.inspector.isPointerCaptured, ) fixture.host.render(ctx, fixture.viewportWidth, fixture.viewportHeight) fixture.host.paint(ctx) @@ -209,34 +214,40 @@ class InspectorDropdownCorrectiveTests { } } - private fun openVisibleInspectorSelectDropdownWithoutBodyScroll( - fixture: Fixture - ): Pair { + private fun openVisibleInspectorSelectDropdownWithoutBodyScroll(fixture: Fixture): Pair { val contentRect = fixture.inspector.overlayContentRect() val bodyScrollY = fixture.inspector.panelScrollOffsetY - val row = fixture.inspector.overlayStyleEditorRows().firstOrNull { row -> - if (row.editorKind != InspectorEditorKind.EnumSelect && row.editorKind != InspectorEditorKind.FontSelect) { - return@firstOrNull false - } - val visibleRect = Rect( + val row = + fixture.inspector.overlayStyleEditorRows().firstOrNull { row -> + if (row.editorKind != InspectorEditorKind.EnumSelect && + row.editorKind != InspectorEditorKind.FontSelect + ) { + return@firstOrNull false + } + val visibleRect = + Rect( + row.controlRect.x, + row.controlRect.y - bodyScrollY, + row.controlRect.width, + row.controlRect.height, + ) + val centerX = visibleRect.x + (visibleRect.width / 2).coerceAtLeast(1) + val centerY = visibleRect.y + (visibleRect.height / 2).coerceAtLeast(1) + contentRect.contains(centerX, centerY) + } ?: error("expected visible inspector select row without body scrolling") + + val triggerRect = + Rect( row.controlRect.x, row.controlRect.y - bodyScrollY, row.controlRect.width, - row.controlRect.height + row.controlRect.height, ) - val centerX = visibleRect.x + (visibleRect.width / 2).coerceAtLeast(1) - val centerY = visibleRect.y + (visibleRect.height / 2).coerceAtLeast(1) - contentRect.contains(centerX, centerY) - } ?: error("expected visible inspector select row without body scrolling") - - val triggerRect = Rect( - row.controlRect.x, - row.controlRect.y - bodyScrollY, - row.controlRect.width, - row.controlRect.height - ) - val rowIndex = fixture.inspector.overlayStyleEditorRows().indexOfFirst { it.property == row.property } - .takeIf { it >= 0 } ?: error("expected style row index for ${row.property.key}") + val rowIndex = + fixture.inspector + .overlayStyleEditorRows() + .indexOfFirst { it.property == row.property } + .takeIf { it >= 0 } ?: error("expected style row index for ${row.property.key}") val ownerKey = "dsgl-system-inspector-editor-select-$rowIndex" val clickX = triggerRect.x + 2 val clickY = triggerRect.y + (triggerRect.height / 2).coerceAtLeast(1) @@ -248,37 +259,42 @@ class InspectorDropdownCorrectiveTests { return triggerRect to ownerKey } - private fun openInspectorSelectDropdown( - fixture: Fixture, - requireScrollable: Boolean - ): Pair { + private fun openInspectorSelectDropdown(fixture: Fixture, requireScrollable: Boolean): Pair { repeat(120) { val contentRect = fixture.inspector.overlayContentRect() val bodyScrollY = fixture.inspector.panelScrollOffsetY - val visibleSelectRows = fixture.inspector.overlayStyleEditorRows().filter { row -> - if (row.editorKind != InspectorEditorKind.EnumSelect && row.editorKind != InspectorEditorKind.FontSelect) { - return@filter false + val visibleSelectRows = + fixture.inspector.overlayStyleEditorRows().filter { row -> + if (row.editorKind != InspectorEditorKind.EnumSelect && + row.editorKind != InspectorEditorKind.FontSelect + ) { + return@filter false + } + val visibleRect = + Rect( + row.controlRect.x, + row.controlRect.y - bodyScrollY, + row.controlRect.width, + row.controlRect.height, + ) + val centerX = visibleRect.x + (visibleRect.width / 2).coerceAtLeast(1) + val centerY = visibleRect.y + (visibleRect.height / 2).coerceAtLeast(1) + contentRect.contains(centerX, centerY) } - val visibleRect = Rect( - row.controlRect.x, - row.controlRect.y - bodyScrollY, - row.controlRect.width, - row.controlRect.height - ) - val centerX = visibleRect.x + (visibleRect.width / 2).coerceAtLeast(1) - val centerY = visibleRect.y + (visibleRect.height / 2).coerceAtLeast(1) - contentRect.contains(centerX, centerY) - } visibleSelectRows.forEach { row -> - val triggerRect = Rect( - row.controlRect.x, - row.controlRect.y - bodyScrollY, - row.controlRect.width, - row.controlRect.height - ) - val rowIndex = fixture.inspector.overlayStyleEditorRows().indexOfFirst { it.property == row.property } - .takeIf { it >= 0 } ?: return@forEach + val triggerRect = + Rect( + row.controlRect.x, + row.controlRect.y - bodyScrollY, + row.controlRect.width, + row.controlRect.height, + ) + val rowIndex = + fixture.inspector + .overlayStyleEditorRows() + .indexOfFirst { it.property == row.property } + .takeIf { it >= 0 } ?: return@forEach val ownerKey = "dsgl-system-inspector-editor-select-$rowIndex" val clickX = triggerRect.x + 2 val clickY = triggerRect.y + (triggerRect.height / 2).coerceAtLeast(1) @@ -311,22 +327,29 @@ class InspectorDropdownCorrectiveTests { ?: error("expected system select popup for owner=$ownerKey") } - private fun dispatchSystemMouseDown(fixture: Fixture, x: Int, y: Int): Boolean { - return SelectRuntime.systemEngine.handleMouseDown(x, y, MouseButton.LEFT) || - fixture.host.handleMouseDown(x, y, MouseButton.LEFT) - } - - private fun dispatchSystemMouseUp(fixture: Fixture, x: Int, y: Int): Boolean { - return SelectRuntime.systemEngine.handleMouseUp(x, y, MouseButton.LEFT) || - fixture.host.handleMouseUp(x, y, MouseButton.LEFT) - } + private fun dispatchSystemMouseDown(fixture: Fixture, x: Int, y: Int): Boolean = + SelectRuntime.systemEngine.handleMouseDown(x, y, MouseButton.LEFT) || + fixture.host.handleMouseDown(x, y, MouseButton.LEFT) - private fun dispatchSystemMouseWheel(fixture: Fixture, x: Int, y: Int, delta: Int): Boolean { - return SelectRuntime.systemEngine.handleMouseWheel(x, y, delta) || - fixture.host.handleMouseWheel(x, y, delta) - } + private fun dispatchSystemMouseUp(fixture: Fixture, x: Int, y: Int): Boolean = + SelectRuntime.systemEngine.handleMouseUp(x, y, MouseButton.LEFT) || + fixture.host.handleMouseUp(x, y, MouseButton.LEFT) - private fun waitForSystemSelectClosed(fixture: Fixture, ownerKey: String, cursorX: Int, cursorY: Int) { + private fun dispatchSystemMouseWheel( + fixture: Fixture, + x: Int, + y: Int, + delta: Int, + ): Boolean = + SelectRuntime.systemEngine.handleMouseWheel(x, y, delta) || + fixture.host.handleMouseWheel(x, y, delta) + + private fun waitForSystemSelectClosed( + fixture: Fixture, + ownerKey: String, + cursorX: Int, + cursorY: Int, + ) { repeat(30) { if (!SelectRuntime.systemEngine.isOpenFor(ownerKey)) return Thread.sleep(5) @@ -355,12 +378,14 @@ class InspectorDropdownCorrectiveTests { } private fun findVisibleInputNode(fixture: Fixture, propertyKey: String): TextInputNode { - val inspectorNode = fixture.host.debugEntryNode(SystemOverlayEntryId.Inspector) - ?: error("inspector entry missing") + val inspectorNode = + fixture.host.debugEntryNode(SystemOverlayEntryId.Inspector) + ?: error("inspector entry missing") val contentRect = fixture.inspector.overlayContentRect() - val candidates = collectNodes(inspectorNode) - .filterIsInstance() - .filter { (it.key?.toString() ?: "") == "dsgl-system-inspector-editor-numeric-input-$propertyKey" } + val candidates = + collectNodes(inspectorNode) + .filterIsInstance() + .filter { (it.key?.toString() ?: "") == "dsgl-system-inspector-editor-numeric-input-$propertyKey" } return candidates.firstOrNull { node -> val probeX = node.bounds.x + 2 @@ -372,15 +397,17 @@ class InspectorDropdownCorrectiveTests { private fun inspectedRoot(withManyChildren: Boolean): ContainerNode { val root = ContainerNode(key = "root") root.bounds = Rect(0, 0, 1280, 720) - val target = ContainerNode(key = "target").apply { - bounds = Rect(980, 140, 120, 30) - } + val target = + ContainerNode(key = "target").apply { + bounds = Rect(980, 140, 120, 30) + } target.applyParent(root) if (withManyChildren) { repeat(24) { index -> - ContainerNode(key = "child-$index").apply { - bounds = Rect(980, 170 + index * 14, 120, 10) - }.applyParent(target) + ContainerNode(key = "child-$index") + .apply { + bounds = Rect(980, 170 + index * 14, 120, 10) + }.applyParent(target) } } StyleEngine.setInspectorOverrideLiteral(target, StyleProperty.BACKGROUND_COLOR, "#FF112233").getOrThrow() @@ -389,6 +416,7 @@ class InspectorDropdownCorrectiveTests { private fun collectNodes(root: DOMNode): List { val out = ArrayList() + fun walk(node: DOMNode) { out += node node.children.forEach(::walk) @@ -403,8 +431,6 @@ class InspectorDropdownCorrectiveTests { val root: ContainerNode, var revision: Long, var viewportWidth: Int, - var viewportHeight: Int + var viewportHeight: Int, ) } - - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorInputPathBaselineTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorInputPathBaselineTests.kt index 86d509a..a661449 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorInputPathBaselineTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorInputPathBaselineTests.kt @@ -21,11 +21,14 @@ import org.dreamfinity.dsgl.core.style.StyleProperty import kotlin.test.* class InspectorInputPathBaselineTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -48,13 +51,15 @@ class InspectorInputPathBaselineTests { assertFalse(fixture.inspector.closeOpenStyleDropdowns()) assertFalse(fixture.inspector.handleOpenStyleDropdownWheel(-120)) - val inspectorNode = fixture.host.debugEntryNode(SystemOverlayEntryId.Inspector) - ?: error("inspector entry missing") + val inspectorNode = + fixture.host.debugEntryNode(SystemOverlayEntryId.Inspector) + ?: error("inspector entry missing") val routeProbe = inspectorNode.javaClass.getDeclaredMethod("isDomOwnedInteractionTarget", DOMNode::class.java) routeProbe.isAccessible = true - val triggerNode = collectNodes(inspectorNode).firstOrNull { it.key?.toString() == ownerKey } - ?: error("expected select trigger node") + val triggerNode = + collectNodes(inspectorNode).firstOrNull { it.key?.toString() == ownerKey } + ?: error("expected select trigger node") assertTrue(routeProbe.invoke(inspectorNode, triggerNode) as Boolean) val triggerCenterX = triggerNode.bounds.x + (triggerNode.bounds.width / 2).coerceAtLeast(1) @@ -216,7 +221,7 @@ class InspectorInputPathBaselineTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) host.paint(ctx) @@ -224,14 +229,15 @@ class InspectorInputPathBaselineTests { assertTrue(host.handleMouseUp(984, 144, MouseButton.LEFT)) inspector.setPickMode(false) - val fixture = Fixture( - inspector = inspector, - host = host, - root = root, - revision = 2L, - viewportWidth = 1280, - viewportHeight = 720 - ) + val fixture = + Fixture( + inspector = inspector, + host = host, + root = root, + revision = 2L, + viewportWidth = 1280, + viewportHeight = 720, + ) syncAndRender(fixture, 984, 144) return fixture } @@ -249,7 +255,7 @@ class InspectorInputPathBaselineTests { inspectedLayoutRevision = fixture.revision++, cursorX = cursorX, cursorY = cursorY, - inspectorPointerCaptured = fixture.inspector.isPointerCaptured + inspectorPointerCaptured = fixture.inspector.isPointerCaptured, ) fixture.host.render(ctx, fixture.viewportWidth, fixture.viewportHeight) fixture.host.paint(ctx) @@ -267,22 +273,25 @@ class InspectorInputPathBaselineTests { private fun findOrScrollToVisibleSelectRow(fixture: Fixture): InspectorStyleEditorRowSnapshot { repeat(120) { - val rows = fixture.inspector.overlayStyleEditorRows().filter { row -> - row.editorKind == InspectorEditorKind.EnumSelect || row.editorKind == InspectorEditorKind.FontSelect - } + val rows = + fixture.inspector.overlayStyleEditorRows().filter { row -> + row.editorKind == InspectorEditorKind.EnumSelect || row.editorKind == InspectorEditorKind.FontSelect + } val contentRect = fixture.inspector.overlayContentRect() val bodyScrollY = fixture.inspector.panelScrollOffsetY - rows.firstOrNull { row -> - val rect = Rect( - row.controlRect.x, - row.controlRect.y - bodyScrollY, - row.controlRect.width, - row.controlRect.height - ) - val centerX = rect.x + (rect.width / 2).coerceAtLeast(1) - val centerY = rect.y + (rect.height / 2).coerceAtLeast(1) - contentRect.contains(centerX, centerY) - }?.let { return it } + rows + .firstOrNull { row -> + val rect = + Rect( + row.controlRect.x, + row.controlRect.y - bodyScrollY, + row.controlRect.width, + row.controlRect.height, + ) + val centerX = rect.x + (rect.width / 2).coerceAtLeast(1) + val centerY = rect.y + (rect.height / 2).coerceAtLeast(1) + contentRect.contains(centerX, centerY) + }?.let { return it } scrollInspectorBodyDown(fixture, steps = 1) } @@ -295,22 +304,26 @@ class InspectorInputPathBaselineTests { row.controlRect.x, row.controlRect.y - bodyScrollY, row.controlRect.width, - row.controlRect.height + row.controlRect.height, ) } - private fun findRowByProperty(fixture: Fixture, property: StyleProperty): InspectorStyleEditorRowSnapshot { - return fixture.inspector.overlayStyleEditorRows().firstOrNull { it.property == property } + private fun findRowByProperty(fixture: Fixture, property: StyleProperty): InspectorStyleEditorRowSnapshot = + fixture.inspector + .overlayStyleEditorRows() + .firstOrNull { it.property == property } ?: error("expected row for $property") - } private fun openDropdownFromVisibleSelectRow( fixture: Fixture, - row: InspectorStyleEditorRowSnapshot = findOrScrollToVisibleSelectRow(fixture) + row: InspectorStyleEditorRowSnapshot = findOrScrollToVisibleSelectRow(fixture), ): Pair { val triggerRect = visibleControlRect(fixture, row) - val rowIndex = fixture.inspector.overlayStyleEditorRows().indexOfFirst { it.property == row.property } - .takeIf { it >= 0 } ?: error("expected style row index for ${row.property.key}") + val rowIndex = + fixture.inspector + .overlayStyleEditorRows() + .indexOfFirst { it.property == row.property } + .takeIf { it >= 0 } ?: error("expected style row index for ${row.property.key}") val ownerKey = "dsgl-system-inspector-editor-select-$rowIndex" val clickX = triggerRect.x + 2 val clickY = triggerRect.y + (triggerRect.height / 2).coerceAtLeast(1) @@ -328,22 +341,29 @@ class InspectorInputPathBaselineTests { ?: error("expected system select popup for owner=$ownerKey") } - private fun dispatchSystemMouseDown(fixture: Fixture, x: Int, y: Int): Boolean { - return SelectRuntime.systemEngine.handleMouseDown(x, y, MouseButton.LEFT) || - fixture.host.handleMouseDown(x, y, MouseButton.LEFT) - } - - private fun dispatchSystemMouseUp(fixture: Fixture, x: Int, y: Int): Boolean { - return SelectRuntime.systemEngine.handleMouseUp(x, y, MouseButton.LEFT) || - fixture.host.handleMouseUp(x, y, MouseButton.LEFT) - } + private fun dispatchSystemMouseDown(fixture: Fixture, x: Int, y: Int): Boolean = + SelectRuntime.systemEngine.handleMouseDown(x, y, MouseButton.LEFT) || + fixture.host.handleMouseDown(x, y, MouseButton.LEFT) - private fun dispatchSystemMouseWheel(fixture: Fixture, x: Int, y: Int, delta: Int): Boolean { - return SelectRuntime.systemEngine.handleMouseWheel(x, y, delta) || - fixture.host.handleMouseWheel(x, y, delta) - } + private fun dispatchSystemMouseUp(fixture: Fixture, x: Int, y: Int): Boolean = + SelectRuntime.systemEngine.handleMouseUp(x, y, MouseButton.LEFT) || + fixture.host.handleMouseUp(x, y, MouseButton.LEFT) - private fun waitForSystemSelectClosed(fixture: Fixture, ownerKey: String, cursorX: Int, cursorY: Int) { + private fun dispatchSystemMouseWheel( + fixture: Fixture, + x: Int, + y: Int, + delta: Int, + ): Boolean = + SelectRuntime.systemEngine.handleMouseWheel(x, y, delta) || + fixture.host.handleMouseWheel(x, y, delta) + + private fun waitForSystemSelectClosed( + fixture: Fixture, + ownerKey: String, + cursorX: Int, + cursorY: Int, + ) { repeat(30) { if (!SelectRuntime.systemEngine.isOpenFor(ownerKey)) return Thread.sleep(5) @@ -372,12 +392,14 @@ class InspectorInputPathBaselineTests { } private fun findVisibleInputNode(fixture: Fixture, propertyKey: String): TextInputNode { - val inspectorNode = fixture.host.debugEntryNode(SystemOverlayEntryId.Inspector) - ?: error("inspector entry missing") + val inspectorNode = + fixture.host.debugEntryNode(SystemOverlayEntryId.Inspector) + ?: error("inspector entry missing") val contentRect = fixture.inspector.overlayContentRect() - val candidates = collectNodes(inspectorNode) - .filterIsInstance() - .filter { (it.key?.toString() ?: "") == "dsgl-system-inspector-editor-numeric-input-$propertyKey" } + val candidates = + collectNodes(inspectorNode) + .filterIsInstance() + .filter { (it.key?.toString() ?: "") == "dsgl-system-inspector-editor-numeric-input-$propertyKey" } return candidates.firstOrNull { node -> val probeX = node.bounds.x + 2 @@ -389,15 +411,17 @@ class InspectorInputPathBaselineTests { private fun inspectedRoot(withManyChildren: Boolean): ContainerNode { val root = ContainerNode(key = "root") root.bounds = Rect(0, 0, 1280, 720) - val target = ContainerNode(key = "target").apply { - bounds = Rect(980, 140, 120, 30) - } + val target = + ContainerNode(key = "target").apply { + bounds = Rect(980, 140, 120, 30) + } target.applyParent(root) if (withManyChildren) { repeat(24) { index -> - ContainerNode(key = "child-$index").apply { - bounds = Rect(980, 170 + index * 14, 120, 10) - }.applyParent(target) + ContainerNode(key = "child-$index") + .apply { + bounds = Rect(980, 170 + index * 14, 120, 10) + }.applyParent(target) } } StyleEngine.setInspectorOverrideLiteral(target, StyleProperty.BACKGROUND_COLOR, "#FF112233").getOrThrow() @@ -406,6 +430,7 @@ class InspectorInputPathBaselineTests { private fun collectNodes(root: DOMNode): List { val out = ArrayList() + fun walk(node: DOMNode) { out += node node.children.forEach(::walk) @@ -420,8 +445,6 @@ class InspectorInputPathBaselineTests { val root: ContainerNode, var revision: Long, var viewportWidth: Int, - var viewportHeight: Int + var viewportHeight: Int, ) } - - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt index ff01977..a94a264 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt @@ -21,11 +21,14 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue class InspectorPointerAlignmentTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -111,8 +114,11 @@ class InspectorPointerAlignmentTests { assertTrue(fixture.inspector.panelScrollOffsetY > 0) val row = findOrScrollToVisibleSelectRow(fixture) - val rowIndex = fixture.inspector.overlayStyleEditorRows().indexOfFirst { it.property == row.property } - .takeIf { it >= 0 } ?: error("expected style row index for ${row.property.key}") + val rowIndex = + fixture.inspector + .overlayStyleEditorRows() + .indexOfFirst { it.property == row.property } + .takeIf { it >= 0 } ?: error("expected style row index for ${row.property.key}") val ownerKey = "dsgl-system-inspector-editor-select-$rowIndex" val property = row.property val triggerRect = openDropdownFromVisibleSelectRow(fixture, row) @@ -121,12 +127,12 @@ class InspectorPointerAlignmentTests { fixture.host.handleMouseDown( triggerRect.x + 2, triggerRect.y + (triggerRect.height / 2).coerceAtLeast(1), - MouseButton.LEFT + MouseButton.LEFT, ) fixture.host.handleMouseUp( triggerRect.x + 2, triggerRect.y + (triggerRect.height / 2).coerceAtLeast(1), - MouseButton.LEFT + MouseButton.LEFT, ) syncAndRender(fixture, triggerRect.x + 2, triggerRect.y + 2) waitForSystemSelectClosed(fixture, ownerKey, triggerRect.x + 2, triggerRect.y + 2) @@ -146,8 +152,11 @@ class InspectorPointerAlignmentTests { setViewport(fixture, 420, 280) val row = findOrScrollToVisibleSelectRow(fixture) - val rowIndex = fixture.inspector.overlayStyleEditorRows().indexOfFirst { it.property == row.property } - .takeIf { it >= 0 } ?: error("expected style row index for ${row.property.key}") + val rowIndex = + fixture.inspector + .overlayStyleEditorRows() + .indexOfFirst { it.property == row.property } + .takeIf { it >= 0 } ?: error("expected style row index for ${row.property.key}") val ownerKey = "dsgl-system-inspector-editor-select-$rowIndex" val triggerRect = openDropdownFromVisibleSelectRow(fixture, row) val dropdown = selectPanelRect(ownerKey, fixture) @@ -184,7 +193,7 @@ class InspectorPointerAlignmentTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) host.paint(ctx) @@ -192,14 +201,15 @@ class InspectorPointerAlignmentTests { assertTrue(host.handleMouseUp(984, 144, MouseButton.LEFT)) inspector.setPickMode(false) - val fixture = Fixture( - inspector = inspector, - host = host, - root = root, - revision = 2L, - viewportWidth = 1280, - viewportHeight = 720 - ) + val fixture = + Fixture( + inspector = inspector, + host = host, + root = root, + revision = 2L, + viewportWidth = 1280, + viewportHeight = 720, + ) syncAndRender(fixture, 984, 144) return fixture } @@ -217,7 +227,7 @@ class InspectorPointerAlignmentTests { inspectedLayoutRevision = fixture.revision++, cursorX = cursorX, cursorY = cursorY, - inspectorPointerCaptured = fixture.inspector.isPointerCaptured + inspectorPointerCaptured = fixture.inspector.isPointerCaptured, ) fixture.host.render(ctx, fixture.viewportWidth, fixture.viewportHeight) fixture.host.paint(ctx) @@ -256,18 +266,20 @@ class InspectorPointerAlignmentTests { } private fun findVisibleSelectRowWithoutScrolling(fixture: Fixture): InspectorStyleEditorRowSnapshot { - val rows = fixture.inspector.overlayStyleEditorRows().filter { row -> - row.editorKind == InspectorEditorKind.EnumSelect || row.editorKind == InspectorEditorKind.FontSelect - } + val rows = + fixture.inspector.overlayStyleEditorRows().filter { row -> + row.editorKind == InspectorEditorKind.EnumSelect || row.editorKind == InspectorEditorKind.FontSelect + } val contentRect = fixture.inspector.overlayContentRect() val bodyScrollY = fixture.inspector.panelScrollOffsetY return rows.firstOrNull { row -> - val rect = Rect( - row.controlRect.x, - row.controlRect.y - bodyScrollY, - row.controlRect.width, - row.controlRect.height - ) + val rect = + Rect( + row.controlRect.x, + row.controlRect.y - bodyScrollY, + row.controlRect.width, + row.controlRect.height, + ) val centerX = rect.x + (rect.width / 2).coerceAtLeast(1) val centerY = rect.y + (rect.height / 2).coerceAtLeast(1) contentRect.contains(centerX, centerY) @@ -276,32 +288,32 @@ class InspectorPointerAlignmentTests { private fun findOrScrollToVisibleSelectRow(fixture: Fixture): InspectorStyleEditorRowSnapshot { repeat(120) { - val rows = fixture.inspector.overlayStyleEditorRows().filter { row -> - row.editorKind == InspectorEditorKind.EnumSelect || row.editorKind == InspectorEditorKind.FontSelect - } + val rows = + fixture.inspector.overlayStyleEditorRows().filter { row -> + row.editorKind == InspectorEditorKind.EnumSelect || row.editorKind == InspectorEditorKind.FontSelect + } val contentRect = fixture.inspector.overlayContentRect() val bodyScrollY = fixture.inspector.panelScrollOffsetY - val visible = rows.firstOrNull { row -> - val rect = Rect( - row.controlRect.x, - row.controlRect.y - bodyScrollY, - row.controlRect.width, - row.controlRect.height - ) - val centerX = rect.x + (rect.width / 2).coerceAtLeast(1) - val centerY = rect.y + (rect.height / 2).coerceAtLeast(1) - contentRect.contains(centerX, centerY) - } + val visible = + rows.firstOrNull { row -> + val rect = + Rect( + row.controlRect.x, + row.controlRect.y - bodyScrollY, + row.controlRect.width, + row.controlRect.height, + ) + val centerX = rect.x + (rect.width / 2).coerceAtLeast(1) + val centerY = rect.y + (rect.height / 2).coerceAtLeast(1) + contentRect.contains(centerX, centerY) + } if (visible != null) return visible scrollInspectorBodyDown(fixture, steps = 1) } error("expected visible inspector select row") } - private fun openDropdownFromVisibleSelectRow( - fixture: Fixture, - row: InspectorStyleEditorRowSnapshot - ): Rect { + private fun openDropdownFromVisibleSelectRow(fixture: Fixture, row: InspectorStyleEditorRowSnapshot): Rect { val triggerRect = visibleControlRect(fixture, row) val clickX = triggerRect.x + 2 val clickY = triggerRect.y + (triggerRect.height / 2).coerceAtLeast(1) @@ -317,22 +329,29 @@ class InspectorPointerAlignmentTests { ?: error("expected system select popup for owner=$ownerKey") } - private fun dispatchSystemMouseDown(fixture: Fixture, x: Int, y: Int): Boolean { - return SelectRuntime.systemEngine.handleMouseDown(x, y, MouseButton.LEFT) || - fixture.host.handleMouseDown(x, y, MouseButton.LEFT) - } - - private fun dispatchSystemMouseUp(fixture: Fixture, x: Int, y: Int): Boolean { - return SelectRuntime.systemEngine.handleMouseUp(x, y, MouseButton.LEFT) || - fixture.host.handleMouseUp(x, y, MouseButton.LEFT) - } + private fun dispatchSystemMouseDown(fixture: Fixture, x: Int, y: Int): Boolean = + SelectRuntime.systemEngine.handleMouseDown(x, y, MouseButton.LEFT) || + fixture.host.handleMouseDown(x, y, MouseButton.LEFT) - private fun dispatchSystemMouseWheel(fixture: Fixture, x: Int, y: Int, delta: Int): Boolean { - return SelectRuntime.systemEngine.handleMouseWheel(x, y, delta) || - fixture.host.handleMouseWheel(x, y, delta) - } + private fun dispatchSystemMouseUp(fixture: Fixture, x: Int, y: Int): Boolean = + SelectRuntime.systemEngine.handleMouseUp(x, y, MouseButton.LEFT) || + fixture.host.handleMouseUp(x, y, MouseButton.LEFT) - private fun waitForSystemSelectClosed(fixture: Fixture, ownerKey: String, cursorX: Int, cursorY: Int) { + private fun dispatchSystemMouseWheel( + fixture: Fixture, + x: Int, + y: Int, + delta: Int, + ): Boolean = + SelectRuntime.systemEngine.handleMouseWheel(x, y, delta) || + fixture.host.handleMouseWheel(x, y, delta) + + private fun waitForSystemSelectClosed( + fixture: Fixture, + ownerKey: String, + cursorX: Int, + cursorY: Int, + ) { repeat(30) { if (!SelectRuntime.systemEngine.isOpenFor(ownerKey)) return Thread.sleep(5) @@ -342,10 +361,11 @@ class InspectorPointerAlignmentTests { assertFalse(SelectRuntime.systemEngine.isOpenFor(ownerKey)) } - private fun findRowByProperty(fixture: Fixture, property: StyleProperty): InspectorStyleEditorRowSnapshot { - return fixture.inspector.overlayStyleEditorRows().firstOrNull { it.property == property } + private fun findRowByProperty(fixture: Fixture, property: StyleProperty): InspectorStyleEditorRowSnapshot = + fixture.inspector + .overlayStyleEditorRows() + .firstOrNull { it.property == property } ?: error("expected row for property ${property.key}") - } private fun visibleControlRect(fixture: Fixture, row: InspectorStyleEditorRowSnapshot): Rect { val bodyScrollY = fixture.inspector.panelScrollOffsetY @@ -353,22 +373,24 @@ class InspectorPointerAlignmentTests { row.controlRect.x, row.controlRect.y - bodyScrollY, row.controlRect.width, - row.controlRect.height + row.controlRect.height, ) } private fun inspectedRoot(withManyChildren: Boolean): ContainerNode { val root = ContainerNode(key = "root") root.bounds = Rect(0, 0, 1280, 720) - val target = ContainerNode(key = "target").apply { - bounds = Rect(980, 140, 120, 30) - } + val target = + ContainerNode(key = "target").apply { + bounds = Rect(980, 140, 120, 30) + } target.applyParent(root) if (withManyChildren) { repeat(24) { index -> - ContainerNode(key = "child-$index").apply { - bounds = Rect(980, 170 + index * 14, 120, 10) - }.applyParent(target) + ContainerNode(key = "child-$index") + .apply { + bounds = Rect(980, 170 + index * 14, 120, 10) + }.applyParent(target) } } StyleEngine.setInspectorOverrideLiteral(target, StyleProperty.BACKGROUND_COLOR, "#FF112233").getOrThrow() @@ -381,8 +403,6 @@ class InspectorPointerAlignmentTests { val root: ContainerNode, var revision: Long, var viewportWidth: Int, - var viewportHeight: Int + var viewportHeight: Int, ) } - - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorTextEditingDomMigrationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorTextEditingDomMigrationTests.kt index d3a0794..15e7e3d 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorTextEditingDomMigrationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorTextEditingDomMigrationTests.kt @@ -1,11 +1,5 @@ package org.dreamfinity.dsgl.core.overlay.system -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.colorpicker.ColorPickerRuntime import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent @@ -24,13 +18,22 @@ import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleProperty +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue class InspectorTextEditingDomMigrationTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } private val clipboard = RecordingClipboardAccess() @@ -47,11 +50,12 @@ class InspectorTextEditingDomMigrationTests { @Test fun `inspector live text editing is dom-first and controller edit session stays inactive`() { val fixture = openInspectorAndSelectTarget() - val stringInput = findVisibleInputNode( - host = fixture.host, - inspector = fixture.inspector, - keyPrefix = "dsgl-system-inspector-editor-input-" - ) + val stringInput = + findVisibleInputNode( + host = fixture.host, + inspector = fixture.inspector, + keyPrefix = "dsgl-system-inspector-editor-input-", + ) val focused = focusInputByClick(fixture.host, stringInput) val nearEndX = (stringInput.bounds.x + stringInput.bounds.width - 3).coerceAtLeast(stringInput.bounds.x + 1) @@ -68,11 +72,12 @@ class InspectorTextEditingDomMigrationTests { ClipboardBridge.install(clipboard) val fixture = openInspectorAndSelectTarget() - val input = findVisibleInputNode( - host = fixture.host, - inspector = fixture.inspector, - keyPrefix = "dsgl-system-inspector-editor-input-" - ) + val input = + findVisibleInputNode( + host = fixture.host, + inspector = fixture.inspector, + keyPrefix = "dsgl-system-inspector-editor-input-", + ) val focused = focusInputByClick(fixture.host, input) val y = focused.second @@ -108,11 +113,12 @@ class InspectorTextEditingDomMigrationTests { ClipboardBridge.install(clipboard) val fixture = openInspectorAndSelectTarget() - val numericInput = findVisibleInputNode( - host = fixture.host, - inspector = fixture.inspector, - keyPrefix = "dsgl-system-inspector-editor-numeric-input-width" - ) + val numericInput = + findVisibleInputNode( + host = fixture.host, + inspector = fixture.inspector, + keyPrefix = "dsgl-system-inspector-editor-numeric-input-width", + ) focusInputByClick(fixture.host, numericInput) assertEquals(numericInput.key, FocusManager.focusedNode()?.key) @@ -142,11 +148,12 @@ class InspectorTextEditingDomMigrationTests { val fixture = openInspectorAndSelectTarget() var revision = fixture.nextRevision - val firstInput = findVisibleInputNode( - host = fixture.host, - inspector = fixture.inspector, - keyPrefix = "dsgl-system-inspector-editor-numeric-input-width" - ) + val firstInput = + findVisibleInputNode( + host = fixture.host, + inspector = fixture.inspector, + keyPrefix = "dsgl-system-inspector-editor-numeric-input-width", + ) val inputKey = firstInput.key ?: error("expected keyed inspector input") val focused = focusInputByClick(fixture.host, firstInput) val focusX = focused.first @@ -163,11 +170,12 @@ class InspectorTextEditingDomMigrationTests { assertEquals(inputKey, FocusManager.focusedNode()?.key) } - val refreshed = findVisibleInputNode( - host = fixture.host, - inspector = fixture.inspector, - keyPrefix = "dsgl-system-inspector-editor-numeric-input-width" - ) + val refreshed = + findVisibleInputNode( + host = fixture.host, + inspector = fixture.inspector, + keyPrefix = "dsgl-system-inspector-editor-numeric-input-width", + ) assertEquals(inputKey, refreshed.key) assertEquals("42", refreshed.text) @@ -178,11 +186,12 @@ class InspectorTextEditingDomMigrationTests { assertTrue(fixture.host.handleKeyDown(0, '7')) renderInspectorFrame(fixture, revision++, focusX, focusY) - val refreshedAgain = findVisibleInputNode( - host = fixture.host, - inspector = fixture.inspector, - keyPrefix = "dsgl-system-inspector-editor-numeric-input-width" - ) + val refreshedAgain = + findVisibleInputNode( + host = fixture.host, + inspector = fixture.inspector, + keyPrefix = "dsgl-system-inspector-editor-numeric-input-width", + ) assertEquals("37", refreshedAgain.text) assertNull(fixture.inspector.debugActiveEditBuffer()) } @@ -208,25 +217,42 @@ class InspectorTextEditingDomMigrationTests { inspector.toggle() host.onInputFrame(1280, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false, + ) host.render(ctx, 1280, 720) host.handleMouseDown(984, 144, MouseButton.LEFT) host.handleMouseUp(984, 144, MouseButton.LEFT) inspector.setPickMode(false) - host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 984, cursorY = 144, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 2L, + cursorX = 984, + cursorY = 144, + inspectorPointerCaptured = false, + ) host.render(ctx, 1280, 720) return Fixture(inspector, host, root, target, nextRevision = 3L) } - private fun renderInspectorFrame(fixture: Fixture, revision: Long, cursorX: Int, cursorY: Int) { + private fun renderInspectorFrame( + fixture: Fixture, + revision: Long, + cursorX: Int, + cursorY: Int, + ) { fixture.host.syncFrame( inspectedRoot = fixture.root, inspectedLayoutRevision = revision, cursorX = cursorX, cursorY = cursorY, - inspectorPointerCaptured = fixture.inspector.isPointerCaptured + inspectorPointerCaptured = fixture.inspector.isPointerCaptured, ) fixture.host.render(ctx, 1280, 720) } @@ -234,15 +260,17 @@ class InspectorTextEditingDomMigrationTests { private fun findVisibleInputNode(host: SystemOverlayHost, inspector: InspectorController, keyPrefix: String): TextInputNode { val inspectorNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector entry missing") val contentRect = inspector.overlayContentRect() - val candidates = collectNodes(inspectorNode) - .filterIsInstance() - .filter { (it.key?.toString() ?: "").startsWith(keyPrefix) } - - val visible = candidates.firstOrNull { node -> - val probeX = node.bounds.x + 2 - val probeY = node.bounds.y + (node.bounds.height / 2).coerceAtLeast(1) - contentRect.contains(probeX, probeY) - } + val candidates = + collectNodes(inspectorNode) + .filterIsInstance() + .filter { (it.key?.toString() ?: "").startsWith(keyPrefix) } + + val visible = + candidates.firstOrNull { node -> + val probeX = node.bounds.x + 2 + val probeY = node.bounds.y + (node.bounds.height / 2).coerceAtLeast(1) + contentRect.contains(probeX, probeY) + } return visible ?: candidates.firstOrNull() ?: error("expected inspector input for prefix '$keyPrefix'") } @@ -266,9 +294,10 @@ class InspectorTextEditingDomMigrationTests { private fun inspectedRoot(): Pair { val root = ContainerNode(key = "root") root.bounds = Rect(0, 0, 1280, 720) - val target = ContainerNode(key = "target").apply { - bounds = Rect(980, 140, 120, 30) - } + val target = + ContainerNode(key = "target").apply { + bounds = Rect(980, 140, 120, 30) + } target.applyParent(root) StyleEngine.setInspectorOverrideLiteral(target, StyleProperty.BACKGROUND_COLOR, "#FF112233").getOrThrow() return root to target @@ -276,6 +305,7 @@ class InspectorTextEditingDomMigrationTests { private fun collectNodes(root: DOMNode): List { val out = ArrayList() + fun walk(node: DOMNode) { out += node node.children.forEach(::walk) @@ -289,7 +319,7 @@ class InspectorTextEditingDomMigrationTests { val host: SystemOverlayHost, val root: ContainerNode, val target: ContainerNode, - val nextRevision: Long + val nextRevision: Long, ) private class RecordingClipboardAccess : ClipboardAccess { @@ -302,5 +332,3 @@ class InspectorTextEditingDomMigrationTests { } } } - - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt index 6ec7fc9..be3c06d 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt @@ -1,19 +1,12 @@ package org.dreamfinity.dsgl.core.overlay.system -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotEquals -import kotlin.test.assertNotNull -import kotlin.test.assertSame -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.colorpicker.ColorFormatMode import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupRequest import org.dreamfinity.dsgl.core.colorpicker.ColorPickerRuntime -import org.dreamfinity.dsgl.core.colorpicker.ColorPickerStyle import org.dreamfinity.dsgl.core.colorpicker.ColorPickerState -import org.dreamfinity.dsgl.core.colorpicker.RgbaColor +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerStyle import org.dreamfinity.dsgl.core.colorpicker.RgbChannelOrder +import org.dreamfinity.dsgl.core.colorpicker.RgbaColor import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode @@ -22,16 +15,27 @@ import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope +import org.dreamfinity.dsgl.core.render.RenderCommand +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import kotlin.test.assertSame +import kotlin.test.assertTrue class SystemOverlayColorPickerEntryTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } + @Test fun `system picker popup lifecycle is entry owned and stable`() { val host = SystemOverlayHost(InspectorController()) @@ -87,14 +91,20 @@ class SystemOverlayColorPickerEntryTests { ownerScope = OverlayOwnerScope.Application, anchorRect = Rect(240, 210, 20, 18), title = "App Popup", - state = popupState() - ) + state = popupState(), + ), ) assertTrue(ColorPickerRuntime.engine.isOpenFor(appOwner)) pickerHost.open(anchorRect = Rect(40, 42, 20, 18), title = "Popup", state = popupState()) host.onInputFrame(960, 720) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 44, cursorY = 48, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 44, + cursorY = 48, + inspectorPointerCaptured = false, + ) val node = host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry node missing") val styleTypes = collectStyleTypes(node) @@ -106,7 +116,13 @@ class SystemOverlayColorPickerEntryTests { assertTrue(ColorPickerRuntime.engine.isOpenFor(appOwner)) pickerHost.close() - host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 44, cursorY = 48, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 2L, + cursorX = 44, + cursorY = 48, + inspectorPointerCaptured = false, + ) assertFalse(host.isSystemColorPickerOpen()) assertTrue(ColorPickerRuntime.engine.isOpenFor(appOwner)) } finally { @@ -135,13 +151,25 @@ class SystemOverlayColorPickerEntryTests { assertTrue(stateBefore.dragSession.active) host.handleMouseMove(startX + 50, startY + 30) - host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = startX + 50, cursorY = startY + 30, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 2L, + cursorX = startX + 50, + cursorY = startY + 30, + inspectorPointerCaptured = false, + ) val midState = host.debugEntryState(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry state missing") val panelMid = midState.panelState.currentRectOrNull() ?: error("panel missing") assertNotEquals(panelBefore.x, panelMid.x) host.handleMouseMove(startX + 90, startY + 60) - host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = startX + 90, cursorY = startY + 60, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 3L, + cursorX = startX + 90, + cursorY = startY + 60, + inspectorPointerCaptured = false, + ) val movingNode = host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry node missing") val movingState = host.debugEntryState(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry state missing") val panelAfter = movingState.panelState.currentRectOrNull() ?: error("panel missing") @@ -151,7 +179,13 @@ class SystemOverlayColorPickerEntryTests { assertNotEquals(panelMid.x, panelAfter.x) assertTrue(host.handleMouseUp(startX + 90, startY + 60, MouseButton.LEFT)) - host.syncFrame(root, inspectedLayoutRevision = 4L, cursorX = startX + 90, cursorY = startY + 60, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 4L, + cursorX = startX + 90, + cursorY = startY + 60, + inspectorPointerCaptured = false, + ) val finalState = host.debugEntryState(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry state missing") val panelFinal = finalState.panelState.currentRectOrNull() ?: error("panel missing") assertFalse(finalState.dragSession.active) @@ -167,7 +201,13 @@ class SystemOverlayColorPickerEntryTests { pickerHost.open(anchorRect = Rect(120, 100, 20, 18), title = "Popup", state = popupState()) host.onInputFrame(1200, 800) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 126, cursorY = 108, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 126, + cursorY = 108, + inspectorPointerCaptured = false, + ) val initialNode = host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry node missing") val header = host.debugSystemColorPickerHeaderRect() ?: error("header missing") @@ -184,7 +224,7 @@ class SystemOverlayColorPickerEntryTests { inspectedLayoutRevision = 2L + step, cursorX = mx, cursorY = my, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) val node = host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry node missing") val state = host.debugEntryState(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry state missing") @@ -204,7 +244,13 @@ class SystemOverlayColorPickerEntryTests { host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 84, cursorY = 92, inspectorPointerCaptured = false) val closeRect = host.debugSystemColorPickerCloseRect() ?: error("close rect missing") assertTrue(host.handleMouseDown(closeRect.x + 1, closeRect.y + 1, MouseButton.LEFT)) - host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = closeRect.x + 1, cursorY = closeRect.y + 1, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 2L, + cursorX = closeRect.x + 1, + cursorY = closeRect.y + 1, + inspectorPointerCaptured = false, + ) assertFalse(host.isSystemColorPickerOpen()) assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerPopup)) } @@ -218,7 +264,13 @@ class SystemOverlayColorPickerEntryTests { pickerHost.open(anchorRect = anchor, title = "Popup", state = popupState()) host.onInputFrame(1200, 800) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 364, cursorY = 226, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 364, + cursorY = 226, + inspectorPointerCaptured = false, + ) val state = host.debugEntryState(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry state missing") val panel = state.panelState.currentRectOrNull() ?: error("panel missing") @@ -228,7 +280,6 @@ class SystemOverlayColorPickerEntryTests { assertTrue(panel.y >= 8) } - @Test fun `system picker entry mounts native body subtree without command bridge`() { val host = SystemOverlayHost(InspectorController()) @@ -258,7 +309,7 @@ class SystemOverlayColorPickerEntryTests { anchorRect = Rect(80, 90, 20, 18), title = "Popup", state = popupState(), - onPreview = { previews += it } + onPreview = { previews += it }, ) host.onInputFrame(1200, 800) host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 88, cursorY = 98, inspectorPointerCaptured = false) @@ -274,9 +325,21 @@ class SystemOverlayColorPickerEntryTests { assertTrue(host.handleMouseDown(startX, startY, MouseButton.LEFT)) host.handleMouseMove(midX, midY) - host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = midX, cursorY = midY, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 2L, + cursorX = midX, + cursorY = midY, + inspectorPointerCaptured = false, + ) host.handleMouseMove(endX, endY) - host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = endX, cursorY = endY, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 3L, + cursorX = endX, + cursorY = endY, + inspectorPointerCaptured = false, + ) assertTrue(host.handleMouseUp(endX, endY, MouseButton.LEFT)) val state = host.debugSystemColorPickerState() ?: error("state missing") @@ -290,10 +353,11 @@ class SystemOverlayColorPickerEntryTests { val pickerHost = host.systemInspectorColorPickerPopupHost() val root = inspectedRoot() val initial = popupState() - val updated = initial.copy( - color = RgbaColor(0.92f, 0.16f, 0.24f, 1f), - previous = initial.color - ) + val updated = + initial.copy( + color = RgbaColor(0.92f, 0.16f, 0.24f, 1f), + previous = initial.color, + ) pickerHost.open(anchorRect = Rect(80, 90, 20, 18), title = "Popup", state = initial) host.onInputFrame(1200, 800) @@ -367,7 +431,13 @@ class SystemOverlayColorPickerEntryTests { val hueY = hue.y + hue.height / 2 assertTrue(host.handleMouseDown(hueStartX, hueY, MouseButton.LEFT)) host.handleMouseMove(hueEndX, hueY) - host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = hueEndX, cursorY = hueY, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 2L, + cursorX = hueEndX, + cursorY = hueY, + inspectorPointerCaptured = false, + ) assertTrue(host.handleMouseUp(hueEndX, hueY, MouseButton.LEFT)) val alphaStartX = alpha.x + alpha.width / 2 @@ -375,7 +445,13 @@ class SystemOverlayColorPickerEntryTests { val alphaY = alpha.y + alpha.height / 2 assertTrue(host.handleMouseDown(alphaStartX, alphaY, MouseButton.LEFT)) host.handleMouseMove(alphaEndX, alphaY) - host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = alphaEndX, cursorY = alphaY, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 3L, + cursorX = alphaEndX, + cursorY = alphaY, + inspectorPointerCaptured = false, + ) assertTrue(host.handleMouseUp(alphaEndX, alphaY, MouseButton.LEFT)) val changed = host.debugSystemColorPickerState() ?: error("state missing") @@ -391,23 +467,45 @@ class SystemOverlayColorPickerEntryTests { pickerHost.open(anchorRect = Rect(120, 120, 20, 18), title = "Popup", state = popupState()) host.onInputFrame(1200, 800) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 128, cursorY = 128, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 128, + cursorY = 128, + inspectorPointerCaptured = false, + ) val initialLayout = host.debugSystemColorPickerBodyLayout() ?: error("layout missing") val modeSelect = initialLayout.modeSelectRect assertTrue(host.handleMouseDown(modeSelect.x + 2, modeSelect.y + 2, MouseButton.LEFT)) - host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = modeSelect.x + 2, cursorY = modeSelect.y + 2, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 2L, + cursorX = modeSelect.x + 2, + cursorY = modeSelect.y + 2, + inspectorPointerCaptured = false, + ) val expandedLayout = host.debugSystemColorPickerBodyLayout() ?: error("layout missing") - val hslOption = expandedLayout.modeOptions.firstOrNull { it.mode == ColorFormatMode.HSL } ?: error("HSL option missing") + val hslOption = + expandedLayout.modeOptions.firstOrNull { it.mode == ColorFormatMode.HSL } ?: error("HSL option missing") assertTrue(host.handleMouseDown(hslOption.rect.x + 2, hslOption.rect.y + 2, MouseButton.LEFT)) - host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = hslOption.rect.x + 2, cursorY = hslOption.rect.y + 2, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 3L, + cursorX = hslOption.rect.x + 2, + cursorY = + hslOption.rect.y + 2, + inspectorPointerCaptured = false, + ) val modeChanged = host.debugSystemColorPickerState() ?: error("state missing") assertEquals(ColorFormatMode.HSL, modeChanged.mode) val hslLayout = host.debugSystemColorPickerBodyLayout() ?: error("layout missing") val saturationInput = hslLayout.inputSlots.firstOrNull { it.key == "s" } ?: error("s input missing") - assertTrue(host.handleMouseDown(saturationInput.inputRect.x + 2, saturationInput.inputRect.y + 2, MouseButton.LEFT)) + assertTrue( + host.handleMouseDown(saturationInput.inputRect.x + 2, saturationInput.inputRect.y + 2, MouseButton.LEFT), + ) assertTrue(host.handleKeyDown(KeyCodes.HOME, 0.toChar())) repeat(4) { assertTrue(host.handleKeyDown(KeyCodes.DELETE, 0.toChar())) @@ -429,7 +527,13 @@ class SystemOverlayColorPickerEntryTests { pickerHost.open(anchorRect = Rect(120, 120, 20, 18), title = "Popup", state = popupState()) host.onInputFrame(1200, 800) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 128, cursorY = 128, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 128, + cursorY = 128, + inspectorPointerCaptured = false, + ) val initialLayout = host.debugSystemColorPickerBodyLayout() ?: error("layout missing") val redInput = initialLayout.inputSlots.firstOrNull { it.key == "r" } ?: error("r input missing") @@ -437,7 +541,13 @@ class SystemOverlayColorPickerEntryTests { val argbButton = initialLayout.argbOrderRect ?: error("argb button missing") assertTrue(host.handleMouseDown(argbButton.x + 2, argbButton.y + 2, MouseButton.LEFT)) - host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = argbButton.x + 2, cursorY = argbButton.y + 2, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 2L, + cursorX = argbButton.x + 2, + cursorY = argbButton.y + 2, + inspectorPointerCaptured = false, + ) assertEquals("dsgl-system-color-picker-input-value-1", FocusManager.focusedNode()?.key) assertTrue(host.handleKeyDown(KeyCodes.HOME, 0.toChar())) @@ -453,7 +563,6 @@ class SystemOverlayColorPickerEntryTests { assertEquals("dsgl-system-color-picker-input-value-1", FocusManager.focusedNode()?.key) } - @Test fun `system picker mode dropdown is mounted in transient lane and stays interactive`() { val host = SystemOverlayHost(InspectorController()) @@ -462,24 +571,45 @@ class SystemOverlayColorPickerEntryTests { pickerHost.open(anchorRect = Rect(120, 120, 20, 18), title = "Popup", state = popupState()) host.onInputFrame(1200, 800) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 128, cursorY = 128, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 128, + cursorY = 128, + inspectorPointerCaptured = false, + ) assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerTransient)) val initialLayout = host.debugSystemColorPickerBodyLayout() ?: error("layout missing") val modeSelect = initialLayout.modeSelectRect assertTrue(host.handleMouseDown(modeSelect.x + 2, modeSelect.y + 2, MouseButton.LEFT)) - host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = modeSelect.x + 2, cursorY = modeSelect.y + 2, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 2L, + cursorX = modeSelect.x + 2, + cursorY = modeSelect.y + 2, + inspectorPointerCaptured = false, + ) assertTrue(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerTransient)) - val transientNode = host.debugEntryNode(SystemOverlayEntryId.ColorPickerTransient) ?: error("transient node missing") + val transientNode = + host.debugEntryNode(SystemOverlayEntryId.ColorPickerTransient) ?: error("transient node missing") val transientStyleTypes = collectStyleTypes(transientNode) assertTrue(transientStyleTypes.contains("dsgl-system-color-picker-native-mode-dropdown-overlay")) val expandedLayout = host.debugSystemColorPickerBodyLayout() ?: error("expanded layout missing") - val hslOption = expandedLayout.modeOptions.firstOrNull { it.mode == ColorFormatMode.HSL } ?: error("HSL option missing") + val hslOption = + expandedLayout.modeOptions.firstOrNull { it.mode == ColorFormatMode.HSL } ?: error("HSL option missing") assertTrue(host.handleMouseDown(hslOption.rect.x + 2, hslOption.rect.y + 2, MouseButton.LEFT)) - host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = hslOption.rect.x + 2, cursorY = hslOption.rect.y + 2, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 3L, + cursorX = hslOption.rect.x + 2, + cursorY = + hslOption.rect.y + 2, + inspectorPointerCaptured = false, + ) assertEquals(ColorFormatMode.HSL, host.debugSystemColorPickerState()?.mode) assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerTransient)) @@ -493,12 +623,24 @@ class SystemOverlayColorPickerEntryTests { pickerHost.open(anchorRect = Rect(140, 140, 20, 18), title = "Popup", state = popupState()) host.onInputFrame(1200, 800) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 146, cursorY = 146, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 146, + cursorY = 146, + inspectorPointerCaptured = false, + ) val layout = host.debugSystemColorPickerBodyLayout() ?: error("layout missing") val pipette = layout.pipetteRect assertTrue(host.handleMouseDown(pipette.x + 2, pipette.y + 2, MouseButton.LEFT)) - host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = pipette.x + 2, cursorY = pipette.y + 2, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 2L, + cursorX = pipette.x + 2, + cursorY = pipette.y + 2, + inspectorPointerCaptured = false, + ) val mounted = host.debugMountedEntryIds() assertTrue(mounted.contains(SystemOverlayEntryId.ColorPickerPopup)) @@ -509,12 +651,17 @@ class SystemOverlayColorPickerEntryTests { val moveConsumed = host.handleMouseMove(pipette.x + 40, pipette.y + 40) assertTrue(moveConsumed) - host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = pipette.x + 40, cursorY = pipette.y + 40, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 3L, + cursorX = pipette.x + 40, + cursorY = pipette.y + 40, + inspectorPointerCaptured = false, + ) assertTrue(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerPopup)) } - @Test fun `system picker pipette transient entry emits visible tooltip commands`() { val host = SystemOverlayHost(InspectorController()) @@ -528,50 +675,73 @@ class SystemOverlayColorPickerEntryTests { anchorRect = Rect(140, 140, 20, 18), title = "Popup", state = popupState(), - style = ColorPickerStyle( - eyedropperGridSize = 5, - eyedropperCellSize = 3, - eyedropperGridOverlayEnabled = true, - eyedropperGridOverlayColor = gridColor, - checkerLightColor = checkerLight, - checkerDarkColor = checkerDark - ) + style = + ColorPickerStyle( + eyedropperGridSize = 5, + eyedropperCellSize = 3, + eyedropperGridOverlayEnabled = true, + eyedropperGridOverlayColor = gridColor, + checkerLightColor = checkerLight, + checkerDarkColor = checkerDark, + ), ) host.onInputFrame(1200, 800) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 146, cursorY = 146, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 146, + cursorY = 146, + inspectorPointerCaptured = false, + ) val layout = host.debugSystemColorPickerBodyLayout() ?: error("layout missing") val pipette = layout.pipetteRect assertTrue(host.handleMouseDown(pipette.x + 2, pipette.y + 2, MouseButton.LEFT)) host.handleMouseMove(pipette.x + 32, pipette.y + 28) - host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = pipette.x + 32, cursorY = pipette.y + 28, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 2L, + cursorX = pipette.x + 32, + cursorY = pipette.y + 28, + inspectorPointerCaptured = false, + ) host.render(ctx, 1200, 800) val commands = host.paint(ctx) assertTrue(commands.any { it is RenderCommand.CaptureScreenRegion }) assertTrue(commands.any { it is RenderCommand.DrawCapturedScreenRegion }) - assertTrue(commands.none { command -> - command is RenderCommand.DrawRect && command.width == 3 && command.height == 3 - }) + assertTrue( + commands.none { command -> + command is RenderCommand.DrawRect && command.width == 3 && command.height == 3 + }, + ) assertTrue(commands.any { it is RenderCommand.DrawCheckerboard }) - assertTrue(commands.none { command -> - command is RenderCommand.DrawRect && (command.color == checkerLight || command.color == checkerDark) - }) + assertTrue( + commands.none { command -> + command is RenderCommand.DrawRect && (command.color == checkerLight || command.color == checkerDark) + }, + ) val capturedRegion = commands.filterIsInstance().single() val gridOverlay = capturedRegion.gridOverlay ?: error("grid overlay missing") assertEquals(5, gridOverlay.columns) assertEquals(5, gridOverlay.rows) assertEquals(3, gridOverlay.magnification) assertEquals(gridColor, gridOverlay.color) - assertTrue(commands.none { command -> - command is RenderCommand.DrawRect && command.color == gridColor - }) - assertTrue(commands.any { command -> - command is RenderCommand.DrawText && command.text.startsWith("Mode:") - }) + assertTrue( + commands.none { command -> + command is RenderCommand.DrawRect && command.color == gridColor + }, + ) + assertTrue( + commands.any { command -> + command is RenderCommand.DrawText && command.text.startsWith("Mode:") + }, + ) } + private fun collectStyleTypes(root: DOMNode): Set { val out = LinkedHashSet() + fun walk(node: DOMNode) { out += node.styleType node.children.forEach(::walk) @@ -579,35 +749,35 @@ class SystemOverlayColorPickerEntryTests { walk(root) return out } - private fun popupState(): ColorPickerState { - return ColorPickerState( + + private fun popupState(): ColorPickerState = + ColorPickerState( color = RgbaColor(0.3f, 0.5f, 0.7f, 1f), previous = RgbaColor(0.3f, 0.5f, 0.7f, 1f), mode = ColorFormatMode.RGB, alphaEnabled = true, - closeOnSelect = false + closeOnSelect = false, ) - } private fun inspectedRoot(): ContainerNode { val root = ContainerNode(key = "root") root.bounds = Rect(0, 0, 1200, 800) - ContainerNode(key = "child").apply { - bounds = Rect(16, 18, 120, 30) - }.applyParent(root) + ContainerNode(key = "child") + .apply { + bounds = Rect(16, 18, 120, 30) + }.applyParent(root) return root } - private fun resolveRectFillColor(commands: List, rect: Rect): Int? { - return commands.asReversed().asSequence() + private fun resolveRectFillColor(commands: List, rect: Rect): Int? = + commands + .asReversed() + .asSequence() .filterIsInstance() .firstOrNull { command -> command.x == rect.x && - command.y == rect.y && - command.width == rect.width && - command.height == rect.height - } - ?.color - } + command.y == rect.y && + command.width == rect.width && + command.height == rect.height + }?.color } - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayDomBridgeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayDomBridgeTests.kt index f6b9668..29a2e00 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayDomBridgeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayDomBridgeTests.kt @@ -1,10 +1,5 @@ package org.dreamfinity.dsgl.core.overlay.system -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertSame -import kotlin.test.assertNotNull -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode @@ -13,25 +8,34 @@ import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.inspector.internal.SystemInspectorOverlayNode +import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertSame +import kotlin.test.assertTrue class SystemOverlayDomBridgeTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @Test fun `renderer maps legacy commands to dom nodes`() { val host = ContainerNode(stackLayout = true, key = "host") - val commands = listOf( - RenderCommand.DrawRect(10, 12, 30, 14, 0xFF112233.toInt()), - RenderCommand.DrawText("Hello", 18, 20, 0xFFEEDDCC.toInt()) - ) + val commands = + listOf( + RenderCommand.DrawRect(10, 12, 30, 14, 0xFF112233.toInt()), + RenderCommand.DrawText("Hello", 18, 20, 0xFFEEDDCC.toInt()), + ) SystemOverlayCommandDslRenderer.rebuildInto(host, commands, "test") @@ -42,14 +46,16 @@ class SystemOverlayDomBridgeTests { @Test fun `renderer reuses raw nodes instead of recreating them`() { val host = ContainerNode(stackLayout = true, key = "host") - val first = listOf( - RenderCommand.DrawRect(2, 4, 12, 10, 0xFF223344.toInt()), - RenderCommand.DrawText("A", 6, 7, 0xFFFFFFFF.toInt()) - ) - val second = listOf( - RenderCommand.DrawRect(2, 4, 12, 10, 0xFF556677.toInt()), - RenderCommand.DrawText("B", 6, 7, 0xFFFFFFFF.toInt()) - ) + val first = + listOf( + RenderCommand.DrawRect(2, 4, 12, 10, 0xFF223344.toInt()), + RenderCommand.DrawText("A", 6, 7, 0xFFFFFFFF.toInt()), + ) + val second = + listOf( + RenderCommand.DrawRect(2, 4, 12, 10, 0xFF556677.toInt()), + RenderCommand.DrawText("B", 6, 7, 0xFFFFFFFF.toInt()), + ) SystemOverlayCommandDslRenderer.rebuildInto(host, first, "reuse") val firstNode0 = host.children[0] @@ -64,12 +70,14 @@ class SystemOverlayDomBridgeTests { fun `system inspector overlay creates native dom children from controller frame`() { val controller = InspectorController() controller.toggle() - val root = ContainerNode(key = "root").apply { - bounds = Rect(0, 0, 420, 280) - } - ContainerNode(key = "child").apply { - bounds = Rect(16, 18, 120, 28) - }.applyParent(root) + val root = + ContainerNode(key = "root").apply { + bounds = Rect(0, 0, 420, 280) + } + ContainerNode(key = "child") + .apply { + bounds = Rect(16, 18, 120, 28) + }.applyParent(root) val overlay = SystemInspectorOverlayNode(controller) overlay.bindInspectedTree(root, layoutRevision = 1L) @@ -84,12 +92,14 @@ class SystemOverlayDomBridgeTests { fun `system inspector overlay retains focused native input across frame rebuild`() { val controller = InspectorController() controller.toggle() - val root = ContainerNode(key = "root").apply { - bounds = Rect(0, 0, 1280, 720) - } - ContainerNode(key = "target").apply { - bounds = Rect(980, 140, 120, 30) - }.applyParent(root) + val root = + ContainerNode(key = "root").apply { + bounds = Rect(0, 0, 1280, 720) + } + ContainerNode(key = "target") + .apply { + bounds = Rect(980, 140, 120, 30) + }.applyParent(root) val overlay = SystemInspectorOverlayNode(controller) controller.onLayoutCommitted(root, 1L) @@ -131,12 +141,14 @@ class SystemOverlayDomBridgeTests { assertEquals(focusedKey, focusedAfterRebuild.key) assertTrue(focusedAfterRebuild !== initialInput) } + @Test fun `system inspector overlay mounts only while inspector is active`() { val controller = InspectorController() - val root = ContainerNode(key = "root").apply { - bounds = Rect(0, 0, 420, 280) - } + val root = + ContainerNode(key = "root").apply { + bounds = Rect(0, 0, 420, 280) + } val overlay = SystemInspectorOverlayNode(controller) overlay.bindInspectedTree(root, layoutRevision = 1L) @@ -153,8 +165,3 @@ class SystemOverlayDomBridgeTests { assertTrue(overlay.children.isEmpty()) } } - - - - - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntryInfrastructureTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntryInfrastructureTests.kt index 171c603..2baf845 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntryInfrastructureTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntryInfrastructureTests.kt @@ -1,12 +1,5 @@ package org.dreamfinity.dsgl.core.overlay.system -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertNotSame -import kotlin.test.assertSame -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.colorpicker.ColorFormatMode import org.dreamfinity.dsgl.core.colorpicker.ColorPickerState import org.dreamfinity.dsgl.core.colorpicker.RgbaColor @@ -17,14 +10,26 @@ import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelDragSession import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelDragType import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelState +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNotSame +import kotlin.test.assertSame +import kotlin.test.assertTrue class SystemOverlayEntryInfrastructureTests { @Test fun `system overlay host exposes explicit persistent entries`() { val host = SystemOverlayHost(InspectorController()) assertEquals( - listOf(SystemOverlayEntryId.Inspector, SystemOverlayEntryId.ColorPickerPopup, SystemOverlayEntryId.ColorPickerTransient, SystemOverlayEntryId.PanelDemo), - host.debugRegisteredEntryIds() + listOf( + SystemOverlayEntryId.Inspector, + SystemOverlayEntryId.ColorPickerPopup, + SystemOverlayEntryId.ColorPickerTransient, + SystemOverlayEntryId.PanelDemo, + ), + host.debugRegisteredEntryIds(), ) } @@ -62,14 +67,26 @@ class SystemOverlayEntryInfrastructureTests { val root = inspectedRoot() pickerHost.open(anchorRect = Rect(36, 44, 20, 18), title = "Popup", state = popupState()) try { - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 40, cursorY = 42, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 40, + cursorY = 42, + inspectorPointerCaptured = false, + ) val firstState = host.debugEntryState(SystemOverlayEntryId.ColorPickerPopup) ?: error("state missing") val firstNode = host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) ?: error("node missing") val firstRect = firstState.panelState.currentRectOrNull() assertNotNull(firstRect) assertTrue(firstState.active) - host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 60, cursorY = 65, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 2L, + cursorX = 60, + cursorY = 65, + inspectorPointerCaptured = false, + ) val secondState = host.debugEntryState(SystemOverlayEntryId.ColorPickerPopup) ?: error("state missing") val secondNode = host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) ?: error("node missing") val secondRect = secondState.panelState.currentRectOrNull() ?: error("panel rect missing") @@ -96,10 +113,16 @@ class SystemOverlayEntryInfrastructureTests { inspector.toggle() pickerHost.open(anchorRect = Rect(36, 44, 20, 18), title = "Popup", state = popupState()) try { - host.syncFrame(root, inspectedLayoutRevision = 10L, cursorX = 20, cursorY = 18, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 10L, + cursorX = 20, + cursorY = 18, + inspectorPointerCaptured = false, + ) assertEquals( listOf(SystemOverlayEntryId.Inspector, SystemOverlayEntryId.ColorPickerPopup), - host.debugMountedEntryIds() + host.debugMountedEntryIds(), ) } finally { inspector.deactivate() @@ -118,7 +141,7 @@ class SystemOverlayEntryInfrastructureTests { type = OverlayPanelDragType.PanelMove, pointerX = 100, pointerY = 120, - panelState = panelState + panelState = panelState, ) assertTrue(session.active) assertEquals(SystemOverlayEntryId.ColorPickerPopup, session.ownerId) @@ -167,22 +190,22 @@ class SystemOverlayEntryInfrastructureTests { assertEquals(0, host.debugTransientSessionCount()) } - private fun popupState(): ColorPickerState { - return ColorPickerState( + private fun popupState(): ColorPickerState = + ColorPickerState( color = RgbaColor(0.3f, 0.5f, 0.7f, 1f), previous = RgbaColor(0.3f, 0.5f, 0.7f, 1f), mode = ColorFormatMode.RGB, alphaEnabled = true, - closeOnSelect = false + closeOnSelect = false, ) - } private fun inspectedRoot(): ContainerNode { val root = ContainerNode(key = "root") root.bounds = Rect(0, 0, 800, 600) - ContainerNode(key = "child").apply { - bounds = Rect(16, 18, 120, 30) - }.applyParent(root) + ContainerNode(key = "child") + .apply { + bounds = Rect(16, 18, 120, 30) + }.applyParent(root) return root } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt index fd3e123..922401e 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt @@ -25,11 +25,14 @@ import java.nio.file.Files import kotlin.test.* class SystemOverlayInspectorNativeEntryTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -41,15 +44,18 @@ class SystemOverlayInspectorNativeEntryTests { @Test fun `inspector migration removes intermediate native overlay model classes`() { - val loadResult = runCatching { - Class.forName("org.dreamfinity.dsgl.core.inspector.InspectorNativeOverlayModel") - } + val loadResult = + runCatching { + Class.forName("org.dreamfinity.dsgl.core.inspector.InspectorNativeOverlayModel") + } assertTrue(loadResult.isFailure) } @Test fun `inspector controller no longer exposes manual append overlay commands path`() { - val methodNames = InspectorController::class.java.methods.map { it.name } + val methodNames = + InspectorController::class.java.methods + .map { it.name } assertFalse(methodNames.contains("appendOverlayCommands")) } @@ -96,7 +102,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) @@ -122,7 +128,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) @@ -134,7 +140,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 2L, cursorX = 84, cursorY = 96, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) @@ -145,10 +151,12 @@ class SystemOverlayInspectorNativeEntryTests { val inspectorNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector node missing") val directChildren = inspectorNode.children.toList() - val occluder = directChildren.firstOrNull { it.key == "dsgl-system-inspector-panel-occluder" } - ?: error("occluder node missing") - val selectedContentFill = directChildren.firstOrNull { it.key == "dsgl-system-inspector-selected-content-fill" } - ?: error("selected content fill node missing") + val occluder = + directChildren.firstOrNull { it.key == "dsgl-system-inspector-panel-occluder" } + ?: error("occluder node missing") + val selectedContentFill = + directChildren.firstOrNull { it.key == "dsgl-system-inspector-selected-content-fill" } + ?: error("selected content fill node missing") assertEquals(panelRect, occluder.bounds) assertEquals(highlight.contentRect, selectedContentFill.bounds) @@ -170,7 +178,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) @@ -187,12 +195,13 @@ class SystemOverlayInspectorNativeEntryTests { val colorAction = inspector.overlayColorPickerActionBounds(StyleProperty.BACKGROUND_COLOR) val colorAnchor = colorAction ?: Rect(80, 80, 20, 18) - val openedByClick = if (colorAction != null) { - host.handleMouseDown(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) && + val openedByClick = + if (colorAction != null) { + host.handleMouseDown(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) && host.handleMouseUp(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) - } else { - false - } + } else { + false + } if (!openedByClick) { assertTrue(inspector.debugOpenColorPickerForSelection(StyleProperty.BACKGROUND_COLOR, colorAnchor)) } @@ -202,7 +211,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 3L, cursorX = colorAnchor.x + 1, cursorY = colorAnchor.y + 1, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) assertTrue(host.isSystemColorPickerOpen()) assertEquals(OverlayOwnerScope.System, host.debugSystemColorPickerPopupOwnerScope()) @@ -226,7 +235,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) @@ -290,11 +299,13 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 1280, 720) val inspectorNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector node missing") - val panelHostNode = collectNodes(inspectorNode).firstOrNull { node -> - (node.key?.toString() ?: "").startsWith("dsgl-overlay-panel-") - } ?: error("panel host node missing") - val minimizedChipNode = collectNodes(inspectorNode).firstOrNull { it.key == "dsgl-system-inspector-chip" } - ?: error("minimized chip node missing") + val panelHostNode = + collectNodes(inspectorNode).firstOrNull { node -> + (node.key?.toString() ?: "").startsWith("dsgl-overlay-panel-") + } ?: error("panel host node missing") + val minimizedChipNode = + collectNodes(inspectorNode).firstOrNull { it.key == "dsgl-system-inspector-chip" } + ?: error("minimized chip node missing") assertEquals(0, panelHostNode.bounds.width) assertEquals(0, panelHostNode.bounds.height) @@ -334,7 +345,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 3L, cursorX = dragX, cursorY = dragY, - inspectorPointerCaptured = inspector.isPointerCaptured + inspectorPointerCaptured = inspector.isPointerCaptured, ) host.render(ctx, 1280, 720) @@ -361,7 +372,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) @@ -380,7 +391,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 3L, cursorX = wheelX, cursorY = wheelY, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 420, 280) host.paint(ctx) @@ -397,7 +408,6 @@ class SystemOverlayInspectorNativeEntryTests { assertTrue(inspector.panelScrollOffsetY >= afterWheel) } - @Test fun `scrollbar drag release over control ends capture and does not trigger control click`() { val inspector = InspectorController() @@ -412,7 +422,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) @@ -445,7 +455,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 3L, cursorX = releaseX, cursorY = releaseY + 48, - inspectorPointerCaptured = inspector.isPointerCaptured + inspectorPointerCaptured = inspector.isPointerCaptured, ) host.render(ctx, 420, 280) assertEquals(scrollAfterRelease, inspector.panelScrollOffsetY) @@ -465,7 +475,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) @@ -480,14 +490,16 @@ class SystemOverlayInspectorNativeEntryTests { val panelRect = inspector.overlayPanelRect() ?: error("panel rect missing") val modeBeforeRelease = inspector.mode - val candidatePoints = listOf( - Pair((panelRect.x + panelRect.width + 4).coerceAtMost(419), panelRect.y + 6), - Pair((panelRect.x - 4).coerceAtLeast(0), panelRect.y + 6), - Pair(panelRect.x + 6, (panelRect.y - 4).coerceAtLeast(0)), - Pair(panelRect.x + 6, (panelRect.y + panelRect.height + 4).coerceAtMost(279)) - ) - val outsidePoint = candidatePoints.firstOrNull { (x, y) -> !panelRect.contains(x, y) } - ?: error("failed to find outside release point") + val candidatePoints = + listOf( + Pair((panelRect.x + panelRect.width + 4).coerceAtMost(419), panelRect.y + 6), + Pair((panelRect.x - 4).coerceAtLeast(0), panelRect.y + 6), + Pair(panelRect.x + 6, (panelRect.y - 4).coerceAtLeast(0)), + Pair(panelRect.x + 6, (panelRect.y + panelRect.height + 4).coerceAtMost(279)), + ) + val outsidePoint = + candidatePoints.firstOrNull { (x, y) -> !panelRect.contains(x, y) } + ?: error("failed to find outside release point") val releaseX = outsidePoint.first val releaseY = outsidePoint.second @@ -506,7 +518,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 3L, cursorX = (releaseX + 32).coerceAtMost(419), cursorY = (releaseY + 32).coerceAtMost(279), - inspectorPointerCaptured = inspector.isPointerCaptured + inspectorPointerCaptured = inspector.isPointerCaptured, ) host.render(ctx, 420, 280) assertEquals(scrollAfterRelease, inspector.panelScrollOffsetY) @@ -527,8 +539,8 @@ class SystemOverlayInspectorNativeEntryTests { ownerScope = OverlayOwnerScope.Application, anchorRect = Rect(240, 210, 20, 18), title = "App Popup", - state = popupState() - ) + state = popupState(), + ), ) assertTrue(ColorPickerRuntime.engine.isOpenFor(appOwner)) @@ -539,7 +551,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) @@ -549,17 +561,18 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 2L, cursorX = 80, cursorY = 52, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) val colorAction = inspector.overlayColorPickerActionBounds(StyleProperty.BACKGROUND_COLOR) val colorAnchor = colorAction ?: Rect(80, 80, 20, 18) - val openedByClick = if (colorAction != null) { - host.handleMouseDown(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) && + val openedByClick = + if (colorAction != null) { + host.handleMouseDown(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) && host.handleMouseUp(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) - } else { - false - } + } else { + false + } if (!openedByClick) { assertTrue(inspector.debugOpenColorPickerForSelection(StyleProperty.BACKGROUND_COLOR, colorAnchor)) } @@ -568,7 +581,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 3L, cursorX = colorAnchor.x + 1, cursorY = colorAnchor.y + 1, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) assertTrue(host.isSystemColorPickerOpen()) @@ -581,7 +594,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 4L, cursorX = colorAnchor.x + 1, cursorY = colorAnchor.y + 1, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) assertFalse(host.isSystemColorPickerOpen()) assertTrue(ColorPickerRuntime.engine.isOpenFor(appOwner)) @@ -604,7 +617,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = revision, cursorX = cursorX, cursorY = cursorY, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) } @@ -618,12 +631,13 @@ class SystemOverlayInspectorNativeEntryTests { sync(revision = 2L, cursorX = 80, cursorY = 52) val colorAction = inspector.overlayColorPickerActionBounds(StyleProperty.BACKGROUND_COLOR) val colorAnchor = colorAction ?: Rect(80, 80, 20, 18) - val openedByClick = if (colorAction != null) { - host.handleMouseDown(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) && + val openedByClick = + if (colorAction != null) { + host.handleMouseDown(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) && host.handleMouseUp(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) - } else { - false - } + } else { + false + } if (!openedByClick) { assertTrue(inspector.debugOpenColorPickerForSelection(StyleProperty.BACKGROUND_COLOR, colorAnchor)) } @@ -633,13 +647,14 @@ class SystemOverlayInspectorNativeEntryTests { val layout = host.debugSystemColorPickerBodyLayout() ?: error("color picker body layout missing") val style = ColorPickerStyle() - val hoverTargets = listOf( - "dsgl-system-color-picker-mode-select" to layout.modeSelectRect, - "dsgl-system-color-picker-order-argb" to (layout.argbOrderRect ?: error("argb order rect missing")), - "dsgl-system-color-picker-button-copy" to layout.copyRect, - "dsgl-system-color-picker-button-paste" to layout.pasteRect, - "dsgl-system-color-picker-button-pipette" to layout.pipetteRect - ) + val hoverTargets = + listOf( + "dsgl-system-color-picker-mode-select" to layout.modeSelectRect, + "dsgl-system-color-picker-order-argb" to (layout.argbOrderRect ?: error("argb order rect missing")), + "dsgl-system-color-picker-button-copy" to layout.copyRect, + "dsgl-system-color-picker-button-paste" to layout.pasteRect, + "dsgl-system-color-picker-button-pipette" to layout.pipetteRect, + ) var revision = 4L hoverTargets.forEach { (key, rect) -> @@ -648,11 +663,13 @@ class SystemOverlayInspectorNativeEntryTests { assertTrue(host.handleMouseMove(hoverX, hoverY), "expected hover move to be consumed for $key") sync(revision = revision++, cursorX = hoverX, cursorY = hoverY) - val pickerNode = host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) - ?: error("color picker entry missing") - val buttonNode = collectNodes(pickerNode) - .firstOrNull { it.key?.toString() == key } as? ButtonNode - ?: error("button node missing for $key") + val pickerNode = + host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) + ?: error("color picker entry missing") + val buttonNode = + collectNodes(pickerNode) + .firstOrNull { it.key?.toString() == key } as? ButtonNode + ?: error("button node missing for $key") assertEquals(style.buttonHoverColor, buttonNode.backgroundColor, "expected hover color for $key") } } @@ -670,7 +687,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = revision, cursorX = cursorX, cursorY = cursorY, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) } @@ -685,12 +702,13 @@ class SystemOverlayInspectorNativeEntryTests { sync(revision = 2L, cursorX = 80, cursorY = 52) val colorAction = inspector.overlayColorPickerActionBounds(StyleProperty.BACKGROUND_COLOR) val colorAnchor = colorAction ?: Rect(80, 80, 20, 18) - val openedByClick = if (colorAction != null) { - host.handleMouseDown(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) && + val openedByClick = + if (colorAction != null) { + host.handleMouseDown(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) && host.handleMouseUp(colorAction.x + 1, colorAction.y + 1, MouseButton.LEFT) - } else { - false - } + } else { + false + } if (!openedByClick) { assertTrue(inspector.debugOpenColorPickerForSelection(StyleProperty.BACKGROUND_COLOR, colorAnchor)) } @@ -699,28 +717,37 @@ class SystemOverlayInspectorNativeEntryTests { assertEquals(OverlayOwnerScope.System, host.debugSystemColorPickerPopupOwnerScope()) val initialLayout = host.debugSystemColorPickerBodyLayout() ?: error("color picker body layout missing") - assertTrue(host.handleMouseDown(initialLayout.modeSelectRect.x + 2, initialLayout.modeSelectRect.y + 2, MouseButton.LEFT)) + assertTrue( + host.handleMouseDown( + initialLayout.modeSelectRect.x + 2, + initialLayout.modeSelectRect.y + 2, + MouseButton.LEFT, + ), + ) sync( revision = 4L, cursorX = initialLayout.modeSelectRect.x + 2, - cursorY = initialLayout.modeSelectRect.y + 2 + cursorY = initialLayout.modeSelectRect.y + 2, ) assertTrue(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerTransient)) val expandedLayout = host.debugSystemColorPickerBodyLayout() ?: error("expanded color picker layout missing") - val hslOption = expandedLayout.modeOptions.firstOrNull { it.mode == ColorFormatMode.HSL } - ?: error("HSL mode option missing") + val hslOption = + expandedLayout.modeOptions.firstOrNull { it.mode == ColorFormatMode.HSL } + ?: error("HSL mode option missing") val optionHoverX = hslOption.rect.x + hslOption.rect.width / 2 val optionHoverY = hslOption.rect.y + hslOption.rect.height / 2 assertTrue(host.handleMouseMove(optionHoverX, optionHoverY)) sync(revision = 5L, cursorX = optionHoverX, cursorY = optionHoverY) val style = ColorPickerStyle() - val transientNode = host.debugEntryNode(SystemOverlayEntryId.ColorPickerTransient) - ?: error("transient entry missing") - val optionNode = collectNodes(transientNode) - .firstOrNull { it.key?.toString() == "dsgl-system-color-picker-mode-option-hsl" } as? ButtonNode - ?: error("HSL option node missing") + val transientNode = + host.debugEntryNode(SystemOverlayEntryId.ColorPickerTransient) + ?: error("transient entry missing") + val optionNode = + collectNodes(transientNode) + .firstOrNull { it.key?.toString() == "dsgl-system-color-picker-mode-option-hsl" } as? ButtonNode + ?: error("HSL option node missing") assertEquals(style.buttonHoverColor, optionNode.backgroundColor) assertTrue(host.handleMouseDown(optionHoverX, optionHoverY, MouseButton.LEFT)) @@ -748,7 +775,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) @@ -759,21 +786,24 @@ class SystemOverlayInspectorNativeEntryTests { val bodyRect = inspector.overlayContentRect() val inspectorNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector node missing") - val bodyNode = collectNodes(inspectorNode) - .firstOrNull { it.key?.toString() == "dsgl-system-inspector-body" } - ?: error("inspector body node missing") + val bodyNode = + collectNodes(inspectorNode) + .firstOrNull { it.key?.toString() == "dsgl-system-inspector-body" } + ?: error("inspector body node missing") assertEquals(Overflow.Hidden, bodyNode.overflowX) assertEquals(Overflow.Auto, bodyNode.overflowY) val bodyViewport = bodyNode.overflowViewportRect() ?: bodyRect val initialCommands = host.paint(ctx) - assertTrue(initialCommands.any { command -> - command is RenderCommand.PushClip && + assertTrue( + initialCommands.any { command -> + command is RenderCommand.PushClip && command.x == bodyViewport.x && command.y == bodyViewport.y && command.width == bodyViewport.width && command.height == bodyViewport.height - }) + }, + ) assertTrue(host.handleMouseWheel(bodyRect.x + 4, bodyRect.y + 12, -120)) host.syncFrame( @@ -781,36 +811,42 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 3L, cursorX = bodyRect.x + 4, cursorY = bodyRect.y + 12, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 320, 220) - val bodyLines = collectNodes(inspectorNode).filter { node -> - if (node.display == Display.None) return@filter false - val key = node.key?.toString() ?: return@filter false - key.startsWith("dsgl-system-inspector-info-line-") || + val bodyLines = + collectNodes(inspectorNode).filter { node -> + if (node.display == Display.None) return@filter false + val key = node.key?.toString() ?: return@filter false + key.startsWith("dsgl-system-inspector-info-line-") || key.startsWith("dsgl-system-inspector-style-line-") - } + } assertTrue(bodyLines.isNotEmpty()) - assertTrue(bodyLines.any { node -> - node.bounds.y < bodyRect.y || node.bounds.y + node.bounds.height > bodyRect.y + bodyRect.height - }) + assertTrue( + bodyLines.any { node -> + node.bounds.y < bodyRect.y || node.bounds.y + node.bounds.height > bodyRect.y + bodyRect.height + }, + ) - val edgeIntersecting = bodyLines.filter { node -> - intersects(node.bounds, bodyRect) && !containsFully(bodyRect, node.bounds) - } + val edgeIntersecting = + bodyLines.filter { node -> + intersects(node.bounds, bodyRect) && !containsFully(bodyRect, node.bounds) + } assertTrue(edgeIntersecting.isNotEmpty()) assertTrue(edgeIntersecting.all { it.bounds.height >= 24 }) val scrolledCommands = host.paint(ctx) - assertTrue(scrolledCommands.any { command -> - command is RenderCommand.PushClip && + assertTrue( + scrolledCommands.any { command -> + command is RenderCommand.PushClip && command.x == bodyViewport.x && command.y == bodyViewport.y && command.width == bodyViewport.width && command.height == bodyViewport.height - }) + }, + ) } @Test @@ -827,21 +863,22 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) val commands = host.paint(ctx) val bodyRect = inspector.overlayContentRect() assertTrue(bodyRect.width > 0 && bodyRect.height > 0) - val baselineInfoRendered = commands.any { command -> - command is RenderCommand.DrawText && + val baselineInfoRendered = + commands.any { command -> + command is RenderCommand.DrawText && command.text.contains("F12 toggle") && command.x >= bodyRect.x && command.x <= bodyRect.x + bodyRect.width && command.y >= bodyRect.y && command.y <= bodyRect.y + bodyRect.height - } + } assertTrue(baselineInfoRendered) } @@ -859,7 +896,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) @@ -880,18 +917,21 @@ class SystemOverlayInspectorNativeEntryTests { var latestInteractiveNodes: List = emptyList() repeat(24) { val inspectorNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector node missing") - val interactiveNodes = collectNodes(inspectorNode).filter { node -> - if (node.display == Display.None) return@filter false - val key = node.key?.toString() ?: return@filter false - isInteractiveInspectorControlKey(key) - } + val interactiveNodes = + collectNodes(inspectorNode).filter { node -> + if (node.display == Display.None) return@filter false + val key = node.key?.toString() ?: return@filter false + isInteractiveInspectorControlKey(key) + } latestInteractiveNodes = interactiveNodes - edgeNode = interactiveNodes.firstOrNull { node -> - intersects(node.bounds, bodyRect) && !containsFully( - bodyRect, - node.bounds - ) - } + edgeNode = + interactiveNodes.firstOrNull { node -> + intersects(node.bounds, bodyRect) && + !containsFully( + bodyRect, + node.bounds, + ) + } hiddenNode = interactiveNodes.firstOrNull { node -> !intersects(node.bounds, bodyRect) } visibleNode = interactiveNodes.firstOrNull { node -> containsFully(bodyRect, node.bounds) } if (edgeNode != null && visibleNode != null) return@repeat @@ -901,7 +941,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = revision, cursorX = wheelX, cursorY = wheelY, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 320, 213) revision += 1L @@ -909,24 +949,26 @@ class SystemOverlayInspectorNativeEntryTests { val hiddenTarget = edgeNode ?: hiddenNode ?: latestInteractiveNodes.firstOrNull { node -> !intersects(node.bounds, bodyRect) } - ?: error("failed to find hidden interactive inspector control") + ?: error("failed to find hidden interactive inspector control") val visibleTarget = visibleNode ?: latestInteractiveNodes.firstOrNull { node -> intersects(node.bounds, bodyRect) } ?: edgeNode - val hiddenX = if (edgeNode != null) { - maxOf(hiddenTarget.bounds.x, bodyRect.x) + 2 - } else { - hiddenTarget.bounds.x + (hiddenTarget.bounds.width / 2).coerceAtLeast(1) - } - val hiddenY = if (edgeNode != null) { - if (hiddenTarget.bounds.y < bodyRect.y) { - hiddenTarget.bounds.y + 1 + val hiddenX = + if (edgeNode != null) { + maxOf(hiddenTarget.bounds.x, bodyRect.x) + 2 } else { - hiddenTarget.bounds.y + hiddenTarget.bounds.height - 1 + hiddenTarget.bounds.x + (hiddenTarget.bounds.width / 2).coerceAtLeast(1) + } + val hiddenY = + if (edgeNode != null) { + if (hiddenTarget.bounds.y < bodyRect.y) { + hiddenTarget.bounds.y + 1 + } else { + hiddenTarget.bounds.y + hiddenTarget.bounds.height - 1 + } + } else { + hiddenTarget.bounds.y + (hiddenTarget.bounds.height / 2).coerceAtLeast(1) } - } else { - hiddenTarget.bounds.y + (hiddenTarget.bounds.height / 2).coerceAtLeast(1) - } assertFalse(bodyRect.contains(hiddenX, hiddenY)) assertTrue(hiddenTarget.bounds.contains(hiddenX, hiddenY)) @@ -960,7 +1002,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) @@ -970,8 +1012,9 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 420, 280) val inspectorNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector node missing") - val bodyNode = collectNodes(inspectorNode).firstOrNull { it.key == "dsgl-system-inspector-body" } - ?: error("inspector body node missing") + val bodyNode = + collectNodes(inspectorNode).firstOrNull { it.key == "dsgl-system-inspector-body" } + ?: error("inspector body node missing") val scrollState = bodyNode.scrollContainerState() assertTrue(scrollState.axisY.scrollContainer) @@ -1005,7 +1048,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) @@ -1018,22 +1061,25 @@ class SystemOverlayInspectorNativeEntryTests { val inspectorNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector node missing") val bodyRect = inspector.overlayContentRect() val allNodes = collectNodes(inspectorNode) - val interactiveNode = allNodes.firstOrNull { node -> - val key = node.key?.toString() ?: return@firstOrNull false - val interactiveControl = key.startsWith("dsgl-system-inspector-editor-input-") || - key.startsWith("dsgl-system-inspector-editor-numeric-input-") || - key.startsWith("dsgl-system-inspector-editor-select-") || - key.startsWith("dsgl-system-inspector-editor-color-preview-") - if (!interactiveControl) return@firstOrNull false - val probeX = node.bounds.x + 2 - val probeY = node.bounds.y + (node.bounds.height / 2).coerceAtLeast(1) - bodyRect.contains(probeX, probeY) - } - val wheelNode = interactiveNode ?: allNodes.firstOrNull { node -> - val probeX = node.bounds.x + 2 - val probeY = node.bounds.y + (node.bounds.height / 2).coerceAtLeast(1) - bodyRect.contains(probeX, probeY) - } ?: error("visible inspector body content node missing") + val interactiveNode = + allNodes.firstOrNull { node -> + val key = node.key?.toString() ?: return@firstOrNull false + val interactiveControl = + key.startsWith("dsgl-system-inspector-editor-input-") || + key.startsWith("dsgl-system-inspector-editor-numeric-input-") || + key.startsWith("dsgl-system-inspector-editor-select-") || + key.startsWith("dsgl-system-inspector-editor-color-preview-") + if (!interactiveControl) return@firstOrNull false + val probeX = node.bounds.x + 2 + val probeY = node.bounds.y + (node.bounds.height / 2).coerceAtLeast(1) + bodyRect.contains(probeX, probeY) + } + val wheelNode = + interactiveNode ?: allNodes.firstOrNull { node -> + val probeX = node.bounds.x + 2 + val probeY = node.bounds.y + (node.bounds.height / 2).coerceAtLeast(1) + bodyRect.contains(probeX, probeY) + } ?: error("visible inspector body content node missing") val wheelX = wheelNode.bounds.x + 2 val wheelY = wheelNode.bounds.y + (wheelNode.bounds.height / 2).coerceAtLeast(1) @@ -1045,7 +1091,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 3L, cursorX = wheelX, cursorY = wheelY, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 420, 280) host.paint(ctx) @@ -1066,7 +1112,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) assertTrue(host.handleMouseDown(984, 144, MouseButton.LEFT)) @@ -1087,7 +1133,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 3L, cursorX = wheelX, cursorY = wheelY, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 420, 280) host.paint(ctx) @@ -1109,7 +1155,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) host.paint(ctx) @@ -1131,7 +1177,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 3L + step, cursorX = wheelX, cursorY = wheelY, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 420, 280) host.paint(ctx) @@ -1142,7 +1188,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 20L + settle, cursorX = wheelX, cursorY = wheelY, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 420, 280) host.paint(ctx) @@ -1159,7 +1205,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 40L + step, cursorX = wheelX, cursorY = wheelY, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 420, 280) host.paint(ctx) @@ -1173,7 +1219,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 60L + settle, cursorX = wheelX, cursorY = wheelY, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 420, 280) host.paint(ctx) @@ -1181,7 +1227,7 @@ class SystemOverlayInspectorNativeEntryTests { } assertTrue( scrolledUp < scrolledDown, - "expected upward wheel to reduce scroll: down=$scrolledDown up=$scrolledUp" + "expected upward wheel to reduce scroll: down=$scrolledDown up=$scrolledUp", ) } @@ -1199,7 +1245,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) host.paint(ctx) @@ -1225,7 +1271,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 3L, cursorX = dragX, cursorY = dragStartY + 18, - inspectorPointerCaptured = inspector.isPointerCaptured + inspectorPointerCaptured = inspector.isPointerCaptured, ) host.render(ctx, 420, 280) host.paint(ctx) @@ -1238,7 +1284,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 4L, cursorX = dragX, cursorY = dragStartY + 42, - inspectorPointerCaptured = inspector.isPointerCaptured + inspectorPointerCaptured = inspector.isPointerCaptured, ) host.render(ctx, 420, 280) host.paint(ctx) @@ -1250,12 +1296,13 @@ class SystemOverlayInspectorNativeEntryTests { @Test fun `inspector style boundary stays isolated from application stylesheet`() { - val stylesDir = createTempStylesDir( - """ - text { color: #FF00FF00; } - div { background-color: #FFFF00FF; } - """.trimIndent() - ) + val stylesDir = + createTempStylesDir( + """ + text { color: #FF00FF00; } + div { background-color: #FFFF00FF; } + """.trimIndent(), + ) StyleEngine.setStylesDirectory(stylesDir) StyleEngine.forceReloadStylesheets() @@ -1271,13 +1318,14 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) val commands = host.paint(ctx) - val headerTexts = commands - .filterIsInstance() - .filter { it.text.startsWith("Inspector") } + val headerTexts = + commands + .filterIsInstance() + .filter { it.text.startsWith("Inspector") } assertTrue(headerTexts.isNotEmpty()) assertTrue(headerTexts.none { it.color == 0xFF00FF00.toInt() }) @@ -1287,10 +1335,12 @@ class SystemOverlayInspectorNativeEntryTests { private fun inspectedRoot(): ContainerNode { val root = ContainerNode(key = "root") root.bounds = Rect(0, 0, 1280, 720) - ContainerNode(key = "target").apply { - bounds = Rect(980, 140, 120, 30) - }.applyParent(root) - StyleEngine.setInspectorOverrideLiteral(root.children.first(), StyleProperty.BACKGROUND_COLOR, "#FF112233") + ContainerNode(key = "target") + .apply { + bounds = Rect(980, 140, 120, 30) + }.applyParent(root) + StyleEngine + .setInspectorOverrideLiteral(root.children.first(), StyleProperty.BACKGROUND_COLOR, "#FF112233") .getOrThrow() return root } @@ -1298,13 +1348,16 @@ class SystemOverlayInspectorNativeEntryTests { private fun inspectedRootWithManyChildren(): ContainerNode { val root = ContainerNode(key = "root") root.bounds = Rect(0, 0, 1800, 1200) - val selected = ContainerNode(key = "target").apply { - bounds = Rect(980, 140, 260, 180) - }.applyParent(root) + val selected = + ContainerNode(key = "target") + .apply { + bounds = Rect(980, 140, 260, 180) + }.applyParent(root) repeat(60) { index -> - ContainerNode(key = "child-$index").apply { - bounds = Rect(980, 180 + index * 12, 180, 10) - }.applyParent(selected) + ContainerNode(key = "child-$index") + .apply { + bounds = Rect(980, 180 + index * 12, 180, 10) + }.applyParent(selected) } StyleEngine.setInspectorOverrideLiteral(selected, StyleProperty.BACKGROUND_COLOR, "#FF112233").getOrThrow() return root @@ -1313,52 +1366,51 @@ class SystemOverlayInspectorNativeEntryTests { private fun inspectedRootMovedUnderPanel(): ContainerNode { val root = ContainerNode(key = "root") root.bounds = Rect(0, 0, 1280, 720) - val selected = ContainerNode(key = "target").apply { - bounds = Rect(72, 84, 180, 80) - }.applyParent(root) + val selected = + ContainerNode(key = "target") + .apply { + bounds = Rect(72, 84, 180, 80) + }.applyParent(root) StyleEngine.setInspectorOverrideLiteral(selected, StyleProperty.BACKGROUND_COLOR, "#FF112233").getOrThrow() return root } - private fun popupState(): ColorPickerState { - return ColorPickerState( + private fun popupState(): ColorPickerState = + ColorPickerState( color = RgbaColor(0.3f, 0.5f, 0.7f, 1f), previous = RgbaColor(0.3f, 0.5f, 0.7f, 1f), mode = ColorFormatMode.RGB, alphaEnabled = true, - closeOnSelect = false + closeOnSelect = false, ) - } - private fun intersects(a: Rect, b: Rect): Boolean { - return a.x < b.x + b.width && - a.x + a.width > b.x && - a.y < b.y + b.height && - a.y + a.height > b.y - } - - private fun containsFully(outer: Rect, inner: Rect): Boolean { - return inner.x >= outer.x && - inner.y >= outer.y && - inner.x + inner.width <= outer.x + outer.width && - inner.y + inner.height <= outer.y + outer.height - } - - private fun isInteractiveInspectorControlKey(key: String): Boolean { - return key == "dsgl-system-inspector-parent-row" || - key.startsWith("dsgl-system-inspector-child-row-") || - key.startsWith("dsgl-system-inspector-editor-reset-") || - key.startsWith("dsgl-system-inspector-editor-select-") || - key.startsWith("dsgl-system-inspector-editor-dec-") || - key.startsWith("dsgl-system-inspector-editor-inc-") || - key.startsWith("dsgl-system-inspector-editor-unit-") || - key.startsWith("dsgl-system-inspector-editor-color-preview-") || - key == "dsgl-system-inspector-reset-node" || - key == "dsgl-system-inspector-clear-all" - } + private fun intersects(a: Rect, b: Rect): Boolean = + a.x < b.x + b.width && + a.x + a.width > b.x && + a.y < b.y + b.height && + a.y + a.height > b.y + + private fun containsFully(outer: Rect, inner: Rect): Boolean = + inner.x >= outer.x && + inner.y >= outer.y && + inner.x + inner.width <= outer.x + outer.width && + inner.y + inner.height <= outer.y + outer.height + + private fun isInteractiveInspectorControlKey(key: String): Boolean = + key == "dsgl-system-inspector-parent-row" || + key.startsWith("dsgl-system-inspector-child-row-") || + key.startsWith("dsgl-system-inspector-editor-reset-") || + key.startsWith("dsgl-system-inspector-editor-select-") || + key.startsWith("dsgl-system-inspector-editor-dec-") || + key.startsWith("dsgl-system-inspector-editor-inc-") || + key.startsWith("dsgl-system-inspector-editor-unit-") || + key.startsWith("dsgl-system-inspector-editor-color-preview-") || + key == "dsgl-system-inspector-reset-node" || + key == "dsgl-system-inspector-clear-all" private fun collectNodes(root: DOMNode): List { val out = ArrayList() + fun walk(node: DOMNode) { out += node node.children.forEach(::walk) @@ -1369,6 +1421,7 @@ class SystemOverlayInspectorNativeEntryTests { private fun collectStyleTypes(root: DOMNode): Set { val out = LinkedHashSet() + fun walk(node: DOMNode) { out += node.styleType node.children.forEach(::walk) @@ -1397,7 +1450,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) host.paint(ctx) @@ -1419,7 +1472,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 3L, cursorX = wheelX, cursorY = wheelY, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 420, 280) host.paint(ctx) @@ -1441,7 +1494,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) host.paint(ctx) @@ -1469,7 +1522,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 3L + step, cursorX = dragX, cursorY = nextY, - inspectorPointerCaptured = inspector.isPointerCaptured + inspectorPointerCaptured = inspector.isPointerCaptured, ) host.render(ctx, 420, 280) host.paint(ctx) @@ -1477,11 +1530,11 @@ class SystemOverlayInspectorNativeEntryTests { val currentThumbY = inspector.overlayScrollbarThumbRect().y assertTrue( currentScroll >= previousScroll, - "scroll regressed: prev=$previousScroll current=$currentScroll step=$step" + "scroll regressed: prev=$previousScroll current=$currentScroll step=$step", ) assertTrue( currentThumbY >= previousThumbY, - "thumb regressed: prev=$previousThumbY current=$currentThumbY step=$step" + "thumb regressed: prev=$previousThumbY current=$currentThumbY step=$step", ) previousScroll = currentScroll previousThumbY = currentThumbY @@ -1497,7 +1550,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 20L + idx, cursorX = dragX, cursorY = startY, - inspectorPointerCaptured = inspector.isPointerCaptured + inspectorPointerCaptured = inspector.isPointerCaptured, ) host.render(ctx, 420, 280) host.paint(ctx) @@ -1520,7 +1573,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 1L, cursorX = 984, cursorY = 144, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) host.paint(ctx) @@ -1547,7 +1600,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 20L + step, cursorX = dragX, cursorY = nextY, - inspectorPointerCaptured = inspector.isPointerCaptured + inspectorPointerCaptured = inspector.isPointerCaptured, ) host.render(ctx, 420, 280) host.paint(ctx) @@ -1555,11 +1608,11 @@ class SystemOverlayInspectorNativeEntryTests { val currentThumbY = inspector.overlayScrollbarThumbRect().y assertTrue( currentScroll >= previousScroll, - "scroll regressed: prev=$previousScroll current=$currentScroll step=$step" + "scroll regressed: prev=$previousScroll current=$currentScroll step=$step", ) assertTrue( currentThumbY >= previousThumbY, - "thumb regressed: prev=$previousThumbY current=$currentThumbY step=$step" + "thumb regressed: prev=$previousThumbY current=$currentThumbY step=$step", ) previousScroll = currentScroll previousThumbY = currentThumbY @@ -1575,7 +1628,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectedLayoutRevision = 40L + idx, cursorX = dragX, cursorY = boundaryY, - inspectorPointerCaptured = inspector.isPointerCaptured + inspectorPointerCaptured = inspector.isPointerCaptured, ) host.render(ctx, 420, 280) host.paint(ctx) @@ -1586,8 +1639,3 @@ class SystemOverlayInspectorNativeEntryTests { assertTrue(host.handleMouseUp(dragX, startY + 2000, MouseButton.LEFT)) } } - - - - - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayPanelDemoEntryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayPanelDemoEntryTests.kt index 15c79d4..d6ab3db 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayPanelDemoEntryTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayPanelDemoEntryTests.kt @@ -1,11 +1,5 @@ package org.dreamfinity.dsgl.core.overlay.system -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertSame -import kotlin.test.assertTrue import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Rect @@ -13,13 +7,22 @@ import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.render.RenderCommand +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertSame +import kotlin.test.assertTrue class SystemOverlayPanelDemoEntryTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 9 - override fun measureText(text: String): Int = text.length * 6 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } @Test fun `panel panel demo entry toggles mounts and keeps stable identity while open`() { @@ -28,20 +31,38 @@ class SystemOverlayPanelDemoEntryTests { host.onInputFrame(1280, 720) host.togglePanelDemo(anchorX = 160, anchorY = 120) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 162, cursorY = 122, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 162, + cursorY = 122, + inspectorPointerCaptured = false, + ) val firstNode = host.debugEntryNode(SystemOverlayEntryId.PanelDemo) ?: error("node missing") val firstState = host.debugEntryState(SystemOverlayEntryId.PanelDemo) ?: error("state missing") assertTrue(firstState.active) assertTrue(host.debugMountedEntryIds().contains(SystemOverlayEntryId.PanelDemo)) - host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 170, cursorY = 134, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 2L, + cursorX = 170, + cursorY = 134, + inspectorPointerCaptured = false, + ) val secondNode = host.debugEntryNode(SystemOverlayEntryId.PanelDemo) ?: error("node missing") val secondState = host.debugEntryState(SystemOverlayEntryId.PanelDemo) ?: error("state missing") assertSame(firstNode, secondNode) assertSame(firstState, secondState) host.togglePanelDemo(anchorX = 160, anchorY = 120) - host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = 170, cursorY = 134, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 3L, + cursorX = 170, + cursorY = 134, + inspectorPointerCaptured = false, + ) assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.PanelDemo)) } @@ -52,27 +73,47 @@ class SystemOverlayPanelDemoEntryTests { host.onInputFrame(1280, 720) host.togglePanelDemo(anchorX = 220, anchorY = 160) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 224, cursorY = 166, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 224, + cursorY = 166, + inspectorPointerCaptured = false, + ) host.render(ctx, 1280, 720) val state = host.debugEntryState(SystemOverlayEntryId.PanelDemo) ?: error("state missing") val before = state.panelState.currentRectOrNull() ?: error("panel missing") - val node = host.debugEntryNode(SystemOverlayEntryId.PanelDemo) as? SystemOverlayPanelDemoNode - ?: error("demo node missing") + val node = + host.debugEntryNode(SystemOverlayEntryId.PanelDemo) as? SystemOverlayPanelDemoNode + ?: error("demo node missing") val buttonRect = node.buttonRect() ?: error("button rect missing") val headerStartX = before.x + 10 val headerStartY = before.y + 10 assertTrue(host.handleMouseDown(headerStartX, headerStartY, MouseButton.LEFT)) assertTrue(host.handleMouseMove(headerStartX + 60, headerStartY + 30)) - host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = headerStartX + 60, cursorY = headerStartY + 30, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 2L, + cursorX = headerStartX + 60, + cursorY = headerStartY + 30, + inspectorPointerCaptured = false, + ) val moved = state.panelState.currentRectOrNull() ?: error("panel missing") assertTrue(moved.x > before.x) assertTrue(host.handleMouseUp(headerStartX + 60, headerStartY + 30, MouseButton.LEFT)) - host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = moved.x + 8, cursorY = moved.y + 8, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 3L, + cursorX = moved.x + 8, + cursorY = moved.y + 8, + inspectorPointerCaptured = false, + ) host.render(ctx, 1280, 720) - val updatedNode = host.debugEntryNode(SystemOverlayEntryId.PanelDemo) as? SystemOverlayPanelDemoNode - ?: error("demo node missing") + val updatedNode = + host.debugEntryNode(SystemOverlayEntryId.PanelDemo) as? SystemOverlayPanelDemoNode + ?: error("demo node missing") val movedButtonRect = updatedNode.buttonRect() ?: error("button rect missing") assertTrue(host.handleMouseDown(movedButtonRect.x + 1, movedButtonRect.y + 1, MouseButton.LEFT)) host.syncFrame( @@ -80,7 +121,7 @@ class SystemOverlayPanelDemoEntryTests { inspectedLayoutRevision = 4L, cursorX = movedButtonRect.x + 1, cursorY = movedButtonRect.y + 1, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) assertEquals(1, updatedNode.currentButtonClicks()) @@ -94,19 +135,37 @@ class SystemOverlayPanelDemoEntryTests { host.onInputFrame(1280, 720) host.togglePanelDemo(anchorX = 280, anchorY = 180) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 282, cursorY = 182, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 282, + cursorY = 182, + inspectorPointerCaptured = false, + ) val state = host.debugEntryState(SystemOverlayEntryId.PanelDemo) ?: error("state missing") val rect = state.panelState.currentRectOrNull() ?: error("panel missing") val closeX = rect.x + rect.width - 4 - 16 + 1 val closeY = rect.y + 4 + 1 assertTrue(host.handleMouseDown(closeX, closeY, MouseButton.LEFT)) - host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = closeX, cursorY = closeY, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 2L, + cursorX = closeX, + cursorY = closeY, + inspectorPointerCaptured = false, + ) assertFalse(host.isOverlayPanelDemoOpen()) assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.PanelDemo)) host.togglePanelDemo(anchorX = 280, anchorY = 180) - host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = 284, cursorY = 184, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 3L, + cursorX = 284, + cursorY = 184, + inspectorPointerCaptured = false, + ) assertTrue(host.isOverlayPanelDemoOpen()) assertTrue(host.debugMountedEntryIds().contains(SystemOverlayEntryId.PanelDemo)) } @@ -118,11 +177,18 @@ class SystemOverlayPanelDemoEntryTests { host.onInputFrame(1280, 720) host.togglePanelDemo(anchorX = 260, anchorY = 170) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 260, cursorY = 170, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 260, + cursorY = 170, + inspectorPointerCaptured = false, + ) host.render(ctx, 1280, 720) - val initialNode = host.debugEntryNode(SystemOverlayEntryId.PanelDemo) as? SystemOverlayPanelDemoNode - ?: error("demo node missing") + val initialNode = + host.debugEntryNode(SystemOverlayEntryId.PanelDemo) as? SystemOverlayPanelDemoNode + ?: error("demo node missing") val state = host.debugEntryState(SystemOverlayEntryId.PanelDemo) ?: error("state missing") val initialRect = state.panelState.currentRectOrNull() ?: error("panel missing") @@ -136,14 +202,15 @@ class SystemOverlayPanelDemoEntryTests { inspectedLayoutRevision = 2L, cursorX = firstDragStartX + 40, cursorY = firstDragStartY + 20, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) val movedRect = state.panelState.currentRectOrNull() ?: error("panel missing") assertTrue(movedRect.x > initialRect.x) - val movedNode = host.debugEntryNode(SystemOverlayEntryId.PanelDemo) as? SystemOverlayPanelDemoNode - ?: error("demo node missing") + val movedNode = + host.debugEntryNode(SystemOverlayEntryId.PanelDemo) as? SystemOverlayPanelDemoNode + ?: error("demo node missing") assertSame(initialNode, movedNode) val buttonRect = movedNode.buttonRect() ?: error("button missing") @@ -153,7 +220,7 @@ class SystemOverlayPanelDemoEntryTests { inspectedLayoutRevision = 3L, cursorX = buttonRect.x + 1, cursorY = buttonRect.y + 1, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) assertEquals(1, movedNode.currentButtonClicks()) @@ -168,7 +235,7 @@ class SystemOverlayPanelDemoEntryTests { inspectedLayoutRevision = 4L, cursorX = secondDragStartX + 30, cursorY = secondDragStartY + 10, - inspectorPointerCaptured = false + inspectorPointerCaptured = false, ) val secondMovedRect = state.panelState.currentRectOrNull() ?: error("panel missing") @@ -177,13 +244,26 @@ class SystemOverlayPanelDemoEntryTests { val closeX = secondMovedRect.x + secondMovedRect.width - 4 - 16 + 1 val closeY = secondMovedRect.y + 4 + 1 assertTrue(host.handleMouseDown(closeX, closeY, MouseButton.LEFT)) - host.syncFrame(root, inspectedLayoutRevision = 5L, cursorX = closeX, cursorY = closeY, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 5L, + cursorX = closeX, + cursorY = closeY, + inspectorPointerCaptured = false, + ) assertFalse(host.isOverlayPanelDemoOpen()) host.togglePanelDemo(anchorX = 260, anchorY = 170) - host.syncFrame(root, inspectedLayoutRevision = 6L, cursorX = 262, cursorY = 172, inspectorPointerCaptured = false) - val reopenedNode = host.debugEntryNode(SystemOverlayEntryId.PanelDemo) as? SystemOverlayPanelDemoNode - ?: error("demo node missing") + host.syncFrame( + root, + inspectedLayoutRevision = 6L, + cursorX = 262, + cursorY = 172, + inspectorPointerCaptured = false, + ) + val reopenedNode = + host.debugEntryNode(SystemOverlayEntryId.PanelDemo) as? SystemOverlayPanelDemoNode + ?: error("demo node missing") assertSame(initialNode, reopenedNode) assertTrue(host.isOverlayPanelDemoOpen()) } @@ -195,7 +275,13 @@ class SystemOverlayPanelDemoEntryTests { host.render(ctx, 1280, 720) host.togglePanelDemo(anchorX = 460, anchorY = 320) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 460, cursorY = 320, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 460, + cursorY = 320, + inspectorPointerCaptured = false, + ) val state = host.debugEntryState(SystemOverlayEntryId.PanelDemo) ?: error("state missing") val rect = state.panelState.currentRectOrNull() ?: error("panel missing") @@ -210,11 +296,18 @@ class SystemOverlayPanelDemoEntryTests { host.onInputFrame(1280, 720) host.togglePanelDemo(anchorX = 180, anchorY = 140) - host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 182, cursorY = 142, inspectorPointerCaptured = false) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 182, + cursorY = 142, + inspectorPointerCaptured = false, + ) host.render(ctx, 1280, 720) - val node = host.debugEntryNode(SystemOverlayEntryId.PanelDemo) as? SystemOverlayPanelDemoNode - ?: error("demo node missing") + val node = + host.debugEntryNode(SystemOverlayEntryId.PanelDemo) as? SystemOverlayPanelDemoNode + ?: error("demo node missing") assertTrue(node.children.any { it.styleType == "dsgl-overlay-panel" }) assertTrue(node.children.none { it.styleType == "dsgl-system-raw-render-command" }) } @@ -222,9 +315,10 @@ class SystemOverlayPanelDemoEntryTests { private fun inspectedRoot(): ContainerNode { val root = ContainerNode(key = "root") root.bounds = Rect(0, 0, 1280, 720) - ContainerNode(key = "child").apply { - bounds = Rect(20, 20, 120, 32) - }.applyParent(root) + ContainerNode(key = "child") + .apply { + bounds = Rect(20, 20, 120, 32) + }.applyParent(root) return root } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayStyleIsolationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayStyleIsolationTests.kt index 197a259..da062e4 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayStyleIsolationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayStyleIsolationTests.kt @@ -27,19 +27,21 @@ class SystemOverlayStyleIsolationTests { @Test fun `system overlay scope ignores user stylesheet rules`() { - val stylesDir = createTempStylesDir( - """ - * { color: #FF5500; } - probe { color: #00CCAA; } - .app probe { color: #1133DD; } - """.trimIndent() - ) + val stylesDir = + createTempStylesDir( + """ + * { color: #FF5500; } + probe { color: #00CCAA; } + .app probe { color: #1133DD; } + """.trimIndent(), + ) StyleEngine.setStylesDirectory(stylesDir) StyleEngine.forceReloadStylesheets() - val appRoot = ContainerNode(key = "app-root").apply { - addClass("app") - } + val appRoot = + ContainerNode(key = "app-root").apply { + addClass("app") + } val appProbe = ProbeNode(key = "app-probe").applyParent(appRoot) val appOverlayRoot = ApplicationOverlayRootNode() val appOverlayProbe = ProbeNode(key = "app-overlay-probe").applyParent(appOverlayRoot) @@ -57,7 +59,7 @@ class SystemOverlayStyleIsolationTests { """ * { color: #33AA55; } probe { color: #AA22EE; } - """.trimIndent() + """.trimIndent(), ) StyleEngine.forceReloadStylesheets() StyleEngine.applyStylesRecursively(appRoot, StyleApplicationScope.Application) @@ -75,7 +77,7 @@ class SystemOverlayStyleIsolationTests { } private class ProbeNode( - key: Any? + key: Any?, ) : DOMNode(key) { override val styleType: String = "probe" val defaultColor: Int = 0xFFABCDEF.toInt() @@ -83,14 +85,17 @@ class SystemOverlayStyleIsolationTests { override fun measure(ctx: UiMeasureContext): Size = Size(10, 10) - override fun render(ctx: UiMeasureContext, x: Int, y: Int, width: Int, height: Int) { + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { bounds = Rect(x, y, width, height) } - override fun buildRenderCommands( - ctx: UiMeasureContext, - out: MutableList - ) = Unit + override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) = Unit override fun defaultForegroundColor(): Int = defaultColor diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/popup/FloatingPaneDragModelTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/popup/FloatingPaneDragModelTests.kt index a5c7e1e..ff09e39 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/popup/FloatingPaneDragModelTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/popup/FloatingPaneDragModelTests.kt @@ -13,13 +13,14 @@ class FloatingPaneDragModelTests { val start = Rect(100, 80, 200, 120) drag.begin(mouseX = 130, mouseY = 100, rect = start) - val next = drag.update( - mouseX = 20, - mouseY = 10, - viewportWidth = 500, - viewportHeight = 400, - clamp = ::clampRect - ) + val next = + drag.update( + mouseX = 20, + mouseY = 10, + viewportWidth = 500, + viewportHeight = 400, + clamp = ::clampRect, + ) assertEquals(2, next.x) assertEquals(2, next.y) @@ -32,13 +33,14 @@ class FloatingPaneDragModelTests { val start = Rect(40, 50, 120, 90) drag.begin(mouseX = 60, mouseY = 70, rect = start) - val next = drag.update( - mouseX = 61, - mouseY = 71, - viewportWidth = 400, - viewportHeight = 300, - clamp = ::clampRect - ) + val next = + drag.update( + mouseX = 61, + mouseY = 71, + viewportWidth = 400, + viewportHeight = 300, + clamp = ::clampRect, + ) assertEquals(41, next.x) assertEquals(51, next.y) @@ -54,7 +56,7 @@ class FloatingPaneDragModelTests { x = rect.x.coerceIn(minX, maxX), y = rect.y.coerceIn(minY, maxY), width = rect.width, - height = rect.height + height = rect.height, ) } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/ref/UseRefHookRuntimeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/ref/UseRefHookRuntimeTests.kt index 29ff8cc..24dab34 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/ref/UseRefHookRuntimeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/ref/UseRefHookRuntimeTests.kt @@ -63,9 +63,10 @@ class UseRefHookRuntimeTests { renderWithHookSession(window) window.useBetaBranch = true - val error = assertFailsWith { - renderWithHookSession(window) - } + val error = + assertFailsWith { + renderWithHookSession(window) + } assertTrue(error.message?.contains("Hook signature mismatch") == true) assertTrue(error.message?.contains("inputRef") == true) @@ -94,9 +95,10 @@ class UseRefHookRuntimeTests { window.beginRenderBuild() window.render() - val error = assertFailsWith { - window.endRenderBuild() - } + val error = + assertFailsWith { + window.endRenderBuild() + } assertEquals(error.message?.contains("Storage-backed hook 'useRef'"), true) assertEquals(error.message?.contains("delegated property syntax"), true) @@ -106,17 +108,15 @@ class UseRefHookRuntimeTests { fun `useRef outside active render fails loudly`() { val window = RefProbeWindow() - val error = assertFailsWith { - window.useRef() - } + val error = + assertFailsWith { + window.useRef() + } assertEquals(error.message?.contains("outside active component render"), true) } - private fun renderWithHookSession( - window: DsglWindow, - mode: HookRenderSessionMode = HookRenderSessionMode.Normal - ): DomTree { + private fun renderWithHookSession(window: DsglWindow, mode: HookRenderSessionMode = HookRenderSessionMode.Normal): DomTree { window.beginRenderBuild(mode) return try { window.render() @@ -185,7 +185,11 @@ class UseRefHookRuntimeTests { } } - private data class Alpha(val value: String) + private data class Alpha( + val value: String, + ) - private data class Beta(val value: String) + private data class Beta( + val value: String, + ) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/render/RenderCommandDrawTextTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/render/RenderCommandDrawTextTests.kt index 3892629..9128bb6 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/render/RenderCommandDrawTextTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/render/RenderCommandDrawTextTests.kt @@ -9,34 +9,36 @@ import kotlin.test.assertSame class RenderCommandDrawTextTests { @Test fun `withColor preserves all draw text fields except color`() { - val spans = listOf( - RenderCommand.TextStyleSpan( - start = 0, - end = 4, - color = 0xFF11AA33.toInt(), + val spans = + listOf( + RenderCommand.TextStyleSpan( + start = 0, + end = 4, + color = 0xFF11AA33.toInt(), + bold = true, + italic = true, + underline = true, + strikethrough = false, + obfuscated = true, + ), + ) + val original = + RenderCommand.DrawText( + text = "Demo", + x = 12, + y = 34, + color = 0xFF445566.toInt(), + fontId = "ubuntu", + fontSize = 16, + textFormatting = TextFormatting.Minecraft, bold = true, - italic = true, + italic = false, underline = true, - strikethrough = false, - obfuscated = true + strikethrough = true, + obfuscated = false, + textStyleSpans = spans, + sourceKey = "node.demo", ) - ) - val original = RenderCommand.DrawText( - text = "Demo", - x = 12, - y = 34, - color = 0xFF445566.toInt(), - fontId = "ubuntu", - fontSize = 16, - textFormatting = TextFormatting.Minecraft, - bold = true, - italic = false, - underline = true, - strikethrough = true, - obfuscated = false, - textStyleSpans = spans, - sourceKey = "node.demo" - ) val recolored = original.withColor(0xAA778899.toInt()) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectEngineTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectEngineTests.kt index 1c735d5..da9dc66 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectEngineTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectEngineTests.kt @@ -8,27 +8,33 @@ import org.dreamfinity.dsgl.core.render.RenderCommand import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue -import kotlin.test.assertNotEquals class SelectEngineTests { private class FakeClock( - var now: Long = 0L + var now: Long = 0L, ) : SelectClock { override fun nowMs(): Long = now + fun advance(ms: Long) { now += ms } } - private val ctx = object : UiMeasureContext { - override fun measureText(text: String): Int = text.length * 6 - override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * 6 - override val fontHeight: Int = 9 - override fun fontHeight(fontId: String?, fontSize: Int?): Int = 9 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override fun measureText(text: String): Int = text.length * 6 + + override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * 6 + + override val fontHeight: Int = 9 + + override fun fontHeight(fontId: String?, fontSize: Int?): Int = 9 + + override fun paint(commands: List) = Unit + } @Test fun `popup placement clamps and flips above when bottom space is not enough`() { @@ -39,14 +45,15 @@ class SelectEngineTests { SelectStyle( openDurationMs = 1L, closeDurationMs = 1L, - viewportPadding = 6 - ) + viewportPadding = 6, + ), ) - val model = selectModel { - repeat(16) { index -> - option(id = "id.$index", label = "Option $index") + val model = + selectModel { + repeat(16) { index -> + option(id = "id.$index", label = "Option $index") + } } - } val anchor = Rect(180, 96, 72, 18) engine.open( SelectOpenRequest( @@ -55,8 +62,8 @@ class SelectEngineTests { entries = model.entries, selectedId = "id.0", anchorRect = anchor, - closeOnSelect = true - ) + closeOnSelect = true, + ), ) engine.onFrame(ctx, viewportWidth = 260, viewportHeight = 120, viewportScale = 1f) @@ -74,10 +81,11 @@ class SelectEngineTests { val owner = "select.state" val engine = SelectEngine(clock = clock) engine.setStyle(SelectStyle(openDurationMs = 1L, closeDurationMs = 10L)) - val model = selectModel { - option("a", "A") - option("b", "B") - } + val model = + selectModel { + option("a", "A") + option("b", "B") + } engine.open( SelectOpenRequest( owner = owner, @@ -85,8 +93,8 @@ class SelectEngineTests { entries = model.entries, selectedId = "a", anchorRect = Rect(30, 30, 90, 18), - closeOnSelect = true - ) + closeOnSelect = true, + ), ) engine.onFrame(ctx, 320, 180, 1f) clock.advance(2L) @@ -106,8 +114,8 @@ class SelectEngineTests { entries = model.entries, selectedId = "a", anchorRect = Rect(30, 30, 90, 18), - closeOnSelect = true - ) + closeOnSelect = true, + ), ) engine.onFrame(ctx, 320, 180, 1f) assertTrue(engine.handleKeyDown(KeyCodes.ESCAPE)) @@ -122,11 +130,12 @@ class SelectEngineTests { val owner = "select.keyboard" val engine = SelectEngine(clock = clock) engine.setStyle(SelectStyle(openDurationMs = 1L, closeDurationMs = 1L)) - val model = selectModel { - option("a", "Alpha") - option("b", "Beta") { enabled(false) } - option("c", "Charlie") - } + val model = + selectModel { + option("a", "Alpha") + option("b", "Beta") { enabled(false) } + option("c", "Charlie") + } var selected: String? = null engine.open( SelectOpenRequest( @@ -136,8 +145,8 @@ class SelectEngineTests { selectedId = "a", anchorRect = Rect(24, 24, 96, 18), closeOnSelect = true, - onSelect = { selected = it } - ) + onSelect = { selected = it }, + ), ) engine.onFrame(ctx, 320, 180, 1f) @@ -155,12 +164,13 @@ class SelectEngineTests { val owner = "select.typeahead" val engine = SelectEngine(clock = clock) engine.setStyle(SelectStyle(openDurationMs = 1L, closeDurationMs = 1L, typeAheadResetMs = 300L)) - val model = selectModel { - option("apple", "Apple") - option("banana", "Banana") - option("blueberry", "Blueberry") - option("cherry", "Cherry") - } + val model = + selectModel { + option("apple", "Apple") + option("banana", "Banana") + option("blueberry", "Blueberry") + option("cherry", "Cherry") + } var selected: String? = null engine.open( SelectOpenRequest( @@ -170,8 +180,8 @@ class SelectEngineTests { selectedId = null, anchorRect = Rect(10, 10, 90, 18), closeOnSelect = true, - onSelect = { selected = it } - ) + onSelect = { selected = it }, + ), ) engine.onFrame(ctx, 320, 180, 1f) @@ -186,10 +196,11 @@ class SelectEngineTests { val owner = "select.animation" val engine = SelectEngine(clock = clock) engine.setStyle(SelectStyle(openDurationMs = 100L, closeDurationMs = 80L)) - val model = selectModel { - option("one", "One") - option("two", "Two") - } + val model = + selectModel { + option("one", "One") + option("two", "Two") + } engine.open( SelectOpenRequest( owner = owner, @@ -197,8 +208,8 @@ class SelectEngineTests { entries = model.entries, selectedId = null, anchorRect = Rect(20, 20, 96, 18), - closeOnSelect = true - ) + closeOnSelect = true, + ), ) engine.onFrame(ctx, 320, 180, 1f) assertTrue(engine.snapshot().animationProgress <= 0.01f) @@ -227,10 +238,11 @@ class SelectEngineTests { fun `overlay consumes pointer before base dispatch when select is open`() { val owner = "select.overlay.order" val engine = SelectEngine() - val model = selectModel { - option("x", "X") - option("y", "Y") - } + val model = + selectModel { + option("x", "X") + option("y", "Y") + } engine.open( SelectOpenRequest( owner = owner, @@ -238,8 +250,8 @@ class SelectEngineTests { entries = model.entries, selectedId = "x", anchorRect = Rect(26, 26, 88, 18), - closeOnSelect = true - ) + closeOnSelect = true, + ), ) engine.onFrame(ctx, 320, 180, 1f) val panel = engine.debugPanelRect(owner) @@ -253,11 +265,12 @@ class SelectEngineTests { val owner = "select.wheel.scroll" val engine = SelectEngine() engine.setStyle(SelectStyle(openDurationMs = 1L, closeDurationMs = 1L, wheelStepRows = 1)) - val model = selectModel { - repeat(40) { index -> - option("id-$index", "Item $index") + val model = + selectModel { + repeat(40) { index -> + option("id-$index", "Item $index") + } } - } engine.open( SelectOpenRequest( owner = owner, @@ -265,8 +278,8 @@ class SelectEngineTests { entries = model.entries, selectedId = "id-0", anchorRect = Rect(20, 20, 90, 18), - closeOnSelect = true - ) + closeOnSelect = true, + ), ) engine.onFrame(ctx, 320, 140, 1f) val panel = engine.debugPanelRect(owner) @@ -291,14 +304,15 @@ class SelectEngineTests { closeDurationMs = 1L, maxPanelHeightPadding = 8, scrollbarTrackColor = trackColor, - scrollbarThumbColor = thumbColor - ) + scrollbarThumbColor = thumbColor, + ), ) - val overflowModel = selectModel { - repeat(50) { index -> - option("id-$index", "Option $index") + val overflowModel = + selectModel { + repeat(50) { index -> + option("id-$index", "Option $index") + } } - } overflowEngine.open( SelectOpenRequest( owner = "select.scrollbar.overflow", @@ -306,8 +320,8 @@ class SelectEngineTests { entries = overflowModel.entries, selectedId = "id-0", anchorRect = Rect(18, 16, 100, 18), - closeOnSelect = true - ) + closeOnSelect = true, + ), ) val overflowCommands = mutableListOf() overflowEngine.appendOverlayCommands(ctx, 240, 120, overflowCommands) @@ -324,14 +338,15 @@ class SelectEngineTests { closeDurationMs = 1L, maxPanelHeightPadding = 8, scrollbarTrackColor = trackColor, - scrollbarThumbColor = thumbColor - ) + scrollbarThumbColor = thumbColor, + ), ) - val noOverflowModel = selectModel { - option("a", "A") - option("b", "B") - option("c", "C") - } + val noOverflowModel = + selectModel { + option("a", "A") + option("b", "B") + option("c", "C") + } noOverflowEngine.open( SelectOpenRequest( owner = "select.scrollbar.fit", @@ -339,8 +354,8 @@ class SelectEngineTests { entries = noOverflowModel.entries, selectedId = "a", anchorRect = Rect(18, 16, 100, 18), - closeOnSelect = true - ) + closeOnSelect = true, + ), ) val noOverflowCommands = mutableListOf() noOverflowEngine.appendOverlayCommands(ctx, 240, 200, noOverflowCommands) @@ -360,14 +375,15 @@ class SelectEngineTests { openDurationMs = 1L, closeDurationMs = 1L, maxPanelHeightPadding = 8, - scrollbarWidth = 8 - ) + scrollbarWidth = 8, + ), ) - val model = selectModel { - repeat(64) { index -> - option("id-$index", "Option $index") + val model = + selectModel { + repeat(64) { index -> + option("id-$index", "Option $index") + } } - } engine.open( SelectOpenRequest( owner = owner, @@ -375,8 +391,8 @@ class SelectEngineTests { entries = model.entries, selectedId = "id-0", anchorRect = Rect(20, 20, 120, 18), - closeOnSelect = true - ) + closeOnSelect = true, + ), ) engine.onFrame(ctx, 300, 140, 1f) val panel = engine.debugPanelRect(owner) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectMeasurementCacheTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectMeasurementCacheTests.kt index 7495e6e..68a8979 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectMeasurementCacheTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectMeasurementCacheTests.kt @@ -7,41 +7,49 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue class SelectMeasurementCacheTests { - private val ctx = object : UiMeasureContext { - override fun measureText(text: String): Int = text.length * 7 - override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * 7 - override val fontHeight: Int = 9 - override fun fontHeight(fontId: String?, fontSize: Int?): Int = 9 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override fun measureText(text: String): Int = text.length * 7 + + override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * 7 + + override val fontHeight: Int = 9 + + override fun fontHeight(fontId: String?, fontSize: Int?): Int = 9 + + override fun paint(commands: List) = Unit + } @Test fun `measurement cache reuses computation when inputs unchanged`() { val cache = SelectMeasurementCache() - val model = selectModel { - option("a", "Alpha") - option("b", "Beta") - } + val model = + selectModel { + option("a", "Alpha") + option("b", "Beta") + } val style = SelectStyle() - val first = cache.measure( - modelToken = model.token, - entries = model.entries, - style = style, - ctx = ctx, - dpiScale = 1f, - fontId = null, - fontSize = null - ) - val second = cache.measure( - modelToken = model.token, - entries = model.entries, - style = style, - ctx = ctx, - dpiScale = 1f, - fontId = null, - fontSize = null - ) + val first = + cache.measure( + modelToken = model.token, + entries = model.entries, + style = style, + ctx = ctx, + dpiScale = 1f, + fontId = null, + fontSize = null, + ) + val second = + cache.measure( + modelToken = model.token, + entries = model.entries, + style = style, + ctx = ctx, + dpiScale = 1f, + fontId = null, + fontSize = null, + ) assertEquals(1L, cache.computeCount) assertEquals(first.panelWidth, second.panelWidth) @@ -50,15 +58,17 @@ class SelectMeasurementCacheTests { @Test fun `measurement cache invalidates when style or options change`() { val cache = SelectMeasurementCache() - val base = selectModel { - option("a", "Alpha") - option("b", "Beta") - } - val updated = selectModel { - option("a", "Alpha") - option("b", "Beta-Updated") - option("c", "Charlie") - } + val base = + selectModel { + option("a", "Alpha") + option("b", "Beta") + } + val updated = + selectModel { + option("a", "Alpha") + option("b", "Beta-Updated") + option("c", "Charlie") + } val styleA = SelectStyle(rowPaddingX = 6) val styleB = SelectStyle(rowPaddingX = 10) @@ -69,7 +79,7 @@ class SelectMeasurementCacheTests { ctx = ctx, dpiScale = 1f, fontId = null, - fontSize = null + fontSize = null, ) val afterBase = cache.computeCount cache.measure( @@ -79,7 +89,7 @@ class SelectMeasurementCacheTests { ctx = ctx, dpiScale = 1f, fontId = null, - fontSize = null + fontSize = null, ) val afterStyleChange = cache.computeCount cache.measure( @@ -89,7 +99,7 @@ class SelectMeasurementCacheTests { ctx = ctx, dpiScale = 1f, fontId = null, - fontSize = null + fontSize = null, ) val afterEntriesChange = cache.computeCount @@ -101,23 +111,25 @@ class SelectMeasurementCacheTests { @Test fun `measurement includes group indentation and marker column in panel width`() { val cache = SelectMeasurementCache() - val model = selectModel { - group("Citrus") { - option("orange", "Orange") - option("lemon", "Lemon") + val model = + selectModel { + group("Citrus") { + option("orange", "Orange") + option("lemon", "Lemon") + } } - } val style = SelectStyle(groupIndentX = 14, markerColumnWidth = 12, markerGap = 8) - val measurement = cache.measure( - modelToken = model.token, - entries = model.entries, - style = style, - ctx = ctx, - dpiScale = 1f, - fontId = null, - fontSize = null - ) + val measurement = + cache.measure( + modelToken = model.token, + entries = model.entries, + style = style, + ctx = ctx, + dpiScale = 1f, + fontId = null, + fontSize = null, + ) assertTrue(measurement.panelWidth >= style.minPanelWidth) assertTrue(measurement.panelWidth > 60) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntimeOwnershipBridgeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntimeOwnershipBridgeTests.kt index b727d9b..26c01d7 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntimeOwnershipBridgeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntimeOwnershipBridgeTests.kt @@ -45,16 +45,14 @@ class SelectRuntimeOwnershipBridgeTests { assertTrue(SelectRuntime.systemEngine.isOpenFor(owner)) } - private fun request(owner: Any, scope: OverlayOwnerScope): SelectOpenRequest { - return SelectOpenRequest( + private fun request(owner: Any, scope: OverlayOwnerScope): SelectOpenRequest = + SelectOpenRequest( owner = owner, modelToken = 1L, entries = listOf(SelectEntry.Option("a", labelProvider = { "Alpha" })), selectedId = "a", anchorRect = Rect(10, 10, 100, 20), closeOnSelect = true, - ownerScope = scope + ownerScope = scope, ) - } } - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/CssLengthTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/CssLengthTests.kt index 7cd058f..8365161 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/CssLengthTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/CssLengthTests.kt @@ -19,17 +19,19 @@ class CssLengthTests { @Test fun `unitless zero is accepted but non-zero is rejected`() { assertEquals(CssLength.ZERO_PX, parseCssLength("0")) - val error = assertFailsWith { - parseCssLength("12") - } + val error = + assertFailsWith { + parseCssLength("12") + } assertTrue(error.message?.contains("Expected explicit unit") == true) } @Test fun `unknown units are rejected`() { - val error = assertFailsWith { - parseCssLength("1q") - } + val error = + assertFailsWith { + parseCssLength("1q") + } assertTrue(error.message?.contains("Unknown length unit") == true) } @@ -42,10 +44,11 @@ class CssLengthTests { @Test fun `parses spacing shorthand with mixed units`() { - val insets = parseSpacingLengthShorthand( - raw = "1em 8px 2vh 5%", - allowNegative = false - ) + val insets = + parseSpacingLengthShorthand( + raw = "1em 8px 2vh 5%", + allowNegative = false, + ) assertEquals(CssUnit.Em, insets.top.unit) assertEquals(CssUnit.Px, insets.right.unit) @@ -55,19 +58,22 @@ class CssLengthTests { @Test fun `resolves percent against horizontal and vertical axes`() { - val context = LengthResolveContext( - viewportWidthPx = 400f, - viewportHeightPx = 200f, - containingBlockWidthPx = 240f, - containingBlockHeightPx = 80f, - currentFontSizePx = 10f, - inheritedFontSizePx = 12f - ) - - val horizontal = CssLength(50f, CssUnit.Percent) - .resolvePx(context, LengthPercentBase.ContainerWidth) - val vertical = CssLength(50f, CssUnit.Percent) - .resolvePx(context, LengthPercentBase.ContainerHeight) + val context = + LengthResolveContext( + viewportWidthPx = 400f, + viewportHeightPx = 200f, + containingBlockWidthPx = 240f, + containingBlockHeightPx = 80f, + currentFontSizePx = 10f, + inheritedFontSizePx = 12f, + ) + + val horizontal = + CssLength(50f, CssUnit.Percent) + .resolvePx(context, LengthPercentBase.ContainerWidth) + val vertical = + CssLength(50f, CssUnit.Percent) + .resolvePx(context, LengthPercentBase.ContainerHeight) assertEquals(120f, horizontal) assertEquals(40f, vertical) @@ -75,10 +81,11 @@ class CssLengthTests { @Test fun `resolves vw and vh against viewport`() { - val context = LengthResolveContext( - viewportWidthPx = 320f, - viewportHeightPx = 180f - ) + val context = + LengthResolveContext( + viewportWidthPx = 320f, + viewportHeightPx = 180f, + ) val vw = CssLength(10f, CssUnit.Vw).resolvePx(context, LengthPercentBase.ContainerWidth) val vh = CssLength(20f, CssUnit.Vh).resolvePx(context, LengthPercentBase.ContainerHeight) @@ -89,22 +96,26 @@ class CssLengthTests { @Test fun `em uses current font size and inherited base for font-size percent`() { - val context = LengthResolveContext( - viewportWidthPx = 0f, - viewportHeightPx = 0f, - containingBlockWidthPx = 0f, - containingBlockHeightPx = 0f, - rootFontSizePx = 24f, - currentFontSizePx = 12f, - inheritedFontSizePx = 20f - ) - - val rem = CssLength(0.5f, CssUnit.Rem) - .resolvePx(context, LengthPercentBase.ContainerWidth) - val paddingEm = CssLength(1.5f, CssUnit.Em) - .resolvePx(context, LengthPercentBase.ContainerWidth) - val fontPercent = CssLength(125f, CssUnit.Percent) - .resolvePx(context, LengthPercentBase.InheritedFontSize) + val context = + LengthResolveContext( + viewportWidthPx = 0f, + viewportHeightPx = 0f, + containingBlockWidthPx = 0f, + containingBlockHeightPx = 0f, + rootFontSizePx = 24f, + currentFontSizePx = 12f, + inheritedFontSizePx = 20f, + ) + + val rem = + CssLength(0.5f, CssUnit.Rem) + .resolvePx(context, LengthPercentBase.ContainerWidth) + val paddingEm = + CssLength(1.5f, CssUnit.Em) + .resolvePx(context, LengthPercentBase.ContainerWidth) + val fontPercent = + CssLength(125f, CssUnit.Percent) + .resolvePx(context, LengthPercentBase.InheritedFontSize) assertEquals(12f, rem) assertEquals(18f, paddingEm) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/DssParserTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/DssParserTests.kt index e5236bd..38d30ec 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/DssParserTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/DssParserTests.kt @@ -6,16 +6,17 @@ import kotlin.test.* class DssParserTests { @Test fun parsesSelectorsAndDeclarations() { - val data = DssParser.parse( - """ - :root { --primary: #3E6B9E; --fg: #FFFFFF; } - button { padding: 6px 10px; background-color: #222222; } - .accent { border-width: 1px; } - button.primary:hover { color: #FFF; } - #dangerAction:disabled { background-color: #A34343; } - """.trimIndent(), - "selectors.dss" - ) + val data = + DssParser.parse( + """ + :root { --primary: #3E6B9E; --fg: #FFFFFF; } + button { padding: 6px 10px; background-color: #222222; } + .accent { border-width: 1px; } + button.primary:hover { color: #FFF; } + #dangerAction:disabled { background-color: #A34343; } + """.trimIndent(), + "selectors.dss", + ) assertEquals(4, data.rules.size, "Expected 4 non-root rules.") assertEquals("#3E6B9E", data.rootVariables["--primary"]) @@ -44,29 +45,35 @@ class DssParserTests { @Test fun parsesVariableReferences() { - val data = DssParser.parse( - """ - :root { --primary: #3E6B9E; } - button.primary { background-color: var(--primary); } - """.trimIndent(), - "vars.dss" - ) + val data = + DssParser.parse( + """ + :root { --primary: #3E6B9E; } + button.primary { background-color: var(--primary); } + """.trimIndent(), + "vars.dss", + ) - val expr = data.rules.single().declarations.get(StyleProperty.BACKGROUND_COLOR) + val expr = + data.rules + .single() + .declarations + .get(StyleProperty.BACKGROUND_COLOR) val variableRef = assertIs(expr) assertEquals("--primary", variableRef.name) } @Test fun assignsStableSourceOrder() { - val data = DssParser.parse( - """ - button { width: 10px; } - .accent { width: 11px; } - #idOne { width: 12px; } - """.trimIndent(), - "order.dss" - ) + val data = + DssParser.parse( + """ + button { width: 10px; } + .accent { width: 11px; } + #idOne { width: 12px; } + """.trimIndent(), + "order.dss", + ) assertEquals(0, data.rules[0].sourceOrder) assertEquals(1, data.rules[1].sourceOrder) @@ -75,45 +82,52 @@ class DssParserTests { @Test fun ignoresBlockComments() { - val data = DssParser.parse( - """ - /* comment before rules */ - .accent { width: 10px; } /* inline comment */ - """.trimIndent(), - "comments.dss" - ) + val data = + DssParser.parse( + """ + /* comment before rules */ + .accent { width: 10px; } /* inline comment */ + """.trimIndent(), + "comments.dss", + ) assertEquals(1, data.rules.size) - assertEquals("accent", data.rules[0].selector.className) + assertEquals( + "accent", + data.rules[0] + .selector.className, + ) } @Test fun rejectsVariableOutsideRoot() { - val error = assertFailsWith { - DssParser.parse( - """ - .card { - --primary: #123456; - } - """.trimIndent(), - "bad-vars.dss" - ) - } + val error = + assertFailsWith { + DssParser.parse( + """ + .card { + --primary: #123456; + } + """.trimIndent(), + "bad-vars.dss", + ) + } assertTrue(error.message?.contains("only supported inside :root") == true) } @Test fun rejectsUnsupportedPropertyWithLocation() { - val error = assertFailsWith { - DssParser.parse( - """ - .card { - unsupportedProp: 10; - } - """.trimIndent(), - "bad-prop.dss" - ) - } + val error = + assertFailsWith { + DssParser.parse( + """ + .card { + unsupportedProp: 10; + } + """.trimIndent(), + "bad-prop.dss", + ) + } assertEquals("bad-prop.dss", error.path) assertTrue(error.line >= 2) assertTrue(error.column >= 1) @@ -121,16 +135,17 @@ class DssParserTests { @Test fun rejectsInvalidSpacingShorthand() { - val error = assertFailsWith { - DssParser.parse( - """ - button { - padding: 1px 2px 3px 4px 5px; - } - """.trimIndent(), - "bad-spacing.dss" - ) - } + val error = + assertFailsWith { + DssParser.parse( + """ + button { + padding: 1px 2px 3px 4px 5px; + } + """.trimIndent(), + "bad-spacing.dss", + ) + } assertTrue(error.message?.contains("supports 1, 2, 3, or 4 values") == true) } @@ -142,7 +157,11 @@ class DssParserTests { file.writeText("button { height: 20px; }") val data = DssParser.parse(file) assertEquals(1, data.rules.size) - assertEquals("button", data.rules[0].selector.typeName) + assertEquals( + "button", + data.rules[0] + .selector.typeName, + ) assertEquals(file.path, data.source) } finally { tempDir.deleteRecursively() @@ -151,24 +170,25 @@ class DssParserTests { @Test fun parsesDisplayFlexAndGridProperties() { - val data = DssParser.parse( - """ - .layout { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - gap: 6px; - text-wrap: nowrap; - } - .grid { - display: grid; - grid-columns: 4; - grid-column-span: 2; - } - """.trimIndent(), - "display.dss" - ) + val data = + DssParser.parse( + """ + .layout { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 6px; + text-wrap: nowrap; + } + .grid { + display: grid; + grid-columns: 4; + grid-column-span: 2; + } + """.trimIndent(), + "display.dss", + ) val layoutRule = data.rules.first { it.selector.className == "layout" } assertIs(layoutRule.declarations.get(StyleProperty.DISPLAY)) @@ -186,61 +206,106 @@ class DssParserTests { @Test fun parsesCombinatorsAndSpecificity() { - val data = DssParser.parse( - """ - .panel .item { color: #FFFFFF; } - .panel > .item { color: #EEEEEE; } - .item + .item { color: #DDDDDD; } - .item ~ .item { color: #CCCCCC; } - #root.toolbar .btn:hover { color: #DDDDDD; } - """.trimIndent(), - "combinators.dss" - ) + val data = + DssParser.parse( + """ + .panel .item { color: #FFFFFF; } + .panel > .item { color: #EEEEEE; } + .item + .item { color: #DDDDDD; } + .item ~ .item { color: #CCCCCC; } + #root.toolbar .btn:hover { color: #DDDDDD; } + """.trimIndent(), + "combinators.dss", + ) assertEquals(5, data.rules.size) - assertTrue(data.rules[0].selector.hasCombinators) - assertEquals(StyleCombinator.Descendant, data.rules[0].selector.steps[1].combinatorToLeft) - assertEquals(StyleCombinator.Child, data.rules[1].selector.steps[1].combinatorToLeft) - assertEquals(StyleCombinator.AdjacentSibling, data.rules[2].selector.steps[1].combinatorToLeft) - assertEquals(StyleCombinator.GeneralSibling, data.rules[3].selector.steps[1].combinatorToLeft) - assertEquals(1, data.rules[4].selector.specificity.idCount) - assertEquals(3, data.rules[4].selector.specificity.classLikeCount) + assertTrue( + data.rules[0] + .selector.hasCombinators, + ) + assertEquals( + StyleCombinator.Descendant, + data.rules[0] + .selector.steps[1] + .combinatorToLeft, + ) + assertEquals( + StyleCombinator.Child, + data.rules[1] + .selector.steps[1] + .combinatorToLeft, + ) + assertEquals( + StyleCombinator.AdjacentSibling, + data.rules[2] + .selector.steps[1] + .combinatorToLeft, + ) + assertEquals( + StyleCombinator.GeneralSibling, + data.rules[3] + .selector.steps[1] + .combinatorToLeft, + ) + assertEquals( + 1, + data.rules[4] + .selector.specificity.idCount, + ) + assertEquals( + 3, + data.rules[4] + .selector.specificity.classLikeCount, + ) } @Test fun `parses universal selector`() { - val data = DssParser.parse( - """ - * { padding: 2px; } - """.trimIndent(), - "universal.dss" - ) + val data = + DssParser.parse( + """ + * { padding: 2px; } + """.trimIndent(), + "universal.dss", + ) assertEquals(1, data.rules.size) - assertTrue(data.rules[0].selector.steps.single().part.universal) + assertTrue( + data.rules[0] + .selector.steps + .single() + .part.universal, + ) } @Test fun `parses open pseudo state`() { - val data = DssParser.parse( - """ - select:open { border-color: #7CB6FF; } - """.trimIndent(), - "open-pseudo.dss" - ) + val data = + DssParser.parse( + """ + select:open { border-color: #7CB6FF; } + """.trimIndent(), + "open-pseudo.dss", + ) assertEquals(1, data.rules.size) - assertEquals(StylePseudoState.OPEN, data.rules.single().selector.pseudoState) + assertEquals( + StylePseudoState.OPEN, + data.rules + .single() + .selector.pseudoState, + ) } @Test fun parsesImportantDeclarationFlag() { - val data = DssParser.parse( - """ - .btn { color: #111111 !important; } - """.trimIndent(), - "important.dss" - ) + val data = + DssParser.parse( + """ + .btn { color: #111111 !important; } + """.trimIndent(), + "important.dss", + ) val rule = data.rules.single() assertTrue(rule.declarations.isImportant(StyleProperty.FOREGROUND_COLOR)) @@ -248,96 +313,123 @@ class DssParserTests { @Test fun `foreground-color alias is accepted and warns once`() { - val data = DssParser.parse( - """ - .card { foreground-color: #ff0000; } - .other { foreground-color: #00ff00; } - """.trimIndent(), - "alias.dss" - ) + val data = + DssParser.parse( + """ + .card { foreground-color: #ff0000; } + .other { foreground-color: #00ff00; } + """.trimIndent(), + "alias.dss", + ) assertEquals(2, data.rules.size) - assertIs(data.rules[0].declarations.get(StyleProperty.FOREGROUND_COLOR)) + assertIs( + data.rules[0] + .declarations + .get(StyleProperty.FOREGROUND_COLOR), + ) assertEquals(1, data.warnings.size) - assertTrue(data.warnings.single().contains("foreground-color")) + assertTrue( + data.warnings + .single() + .contains("foreground-color"), + ) } @Test fun `root block supports variables and root selector declarations`() { - val data = DssParser.parse( - """ - :root { --base: #222222; color: #abcdef; } - """.trimIndent(), - "root-rule.dss" - ) + val data = + DssParser.parse( + """ + :root { --base: #222222; color: #abcdef; } + """.trimIndent(), + "root-rule.dss", + ) assertEquals("#222222", data.rootVariables["--base"]) assertEquals(1, data.rules.size) - assertEquals("dsgl-root", data.rules.single().selector.typeName) + assertEquals( + "dsgl-root", + data.rules + .single() + .selector.typeName, + ) } - @Test fun `parses z-index as unitless integer`() { - val data = DssParser.parse( - """ - .panel { z-index: 5; } - """.trimIndent(), - "z-index.dss" - ) + val data = + DssParser.parse( + """ + .panel { z-index: 5; } + """.trimIndent(), + "z-index.dss", + ) - val expression = data.rules.single().declarations.get(StyleProperty.Z_INDEX) + val expression = + data.rules + .single() + .declarations + .get(StyleProperty.Z_INDEX) val literal = assertIs(expression) assertEquals("5", literal.value) } @Test fun `rejects unitized z-index literal`() { - val error = assertFailsWith { - DssParser.parse( - """ - .panel { z-index: 5px; } - """.trimIndent(), - "z-index-bad.dss" - ) - } + val error = + assertFailsWith { + DssParser.parse( + """ + .panel { z-index: 5px; } + """.trimIndent(), + "z-index-bad.dss", + ) + } assertTrue(error.message?.contains("Expected number") == true) } @Test fun `parses position enum values`() { - val data = DssParser.parse( - """ - .a { position: static; } - .b { position: relative; } - .c { position: absolute; } - .d { position: fixed; } - .e { position: sticky; } - """.trimIndent(), - "position.dss" - ) + val data = + DssParser.parse( + """ + .a { position: static; } + .b { position: relative; } + .c { position: absolute; } + .d { position: fixed; } + .e { position: sticky; } + """.trimIndent(), + "position.dss", + ) assertEquals(5, data.rules.size) - val values = data.rules.mapNotNull { rule -> - (rule.declarations.get(StyleProperty.POSITION) as? StyleExpression.Literal)?.value - } + val values = + data.rules.mapNotNull { rule -> + (rule.declarations.get(StyleProperty.POSITION) as? StyleExpression.Literal)?.value + } assertEquals(listOf("static", "relative", "absolute", "fixed", "sticky"), values) } + @Test fun `parses offset properties through length-like grammar`() { - val data = DssParser.parse( - """ - .panel { - left: 10px; - top: 1.5em; - right: auto; - bottom: 0; - } - """.trimIndent(), - "offsets.dss" - ) + val data = + DssParser.parse( + """ + .panel { + left: 10px; + top: 1.5em; + right: auto; + bottom: 0; + } + """.trimIndent(), + "offsets.dss", + ) - val declarations = data.rules.single().declarations + val declarations = + data.rules + .single() + .declarations assertEquals("10px", (declarations.get(StyleProperty.LEFT) as? StyleExpression.Literal)?.value) assertEquals("1.5em", (declarations.get(StyleProperty.TOP) as? StyleExpression.Literal)?.value) assertEquals("auto", (declarations.get(StyleProperty.RIGHT) as? StyleExpression.Literal)?.value) @@ -346,57 +438,62 @@ class DssParserTests { @Test fun `rejects invalid offset literal`() { - val error = assertFailsWith { - DssParser.parse( - """ - .panel { left: nope; } - """.trimIndent(), - "offsets-bad.dss" - ) - } + val error = + assertFailsWith { + DssParser.parse( + """ + .panel { left: nope; } + """.trimIndent(), + "offsets-bad.dss", + ) + } assertTrue(error.message?.contains("Expected CSS length") == true) } + @Test fun `unitless non-zero length literal is rejected`() { - val error = assertFailsWith { - DssParser.parse( - """ - .panel { margin: 12; padding: 4px; } - """.trimIndent(), - "unitless-length.dss" - ) - } + val error = + assertFailsWith { + DssParser.parse( + """ + .panel { margin: 12; padding: 4px; } + """.trimIndent(), + "unitless-length.dss", + ) + } assertTrue(error.message?.contains("Expected explicit unit") == true) } @Test fun `parses line-height grammar values`() { - val data = DssParser.parse( - """ - .text-a { line-height: normal; } - .text-b { line-height: 24px; } - .text-c { line-height: 1.5em; } - """.trimIndent(), - "line-height.dss" - ) + val data = + DssParser.parse( + """ + .text-a { line-height: normal; } + .text-b { line-height: 24px; } + .text-c { line-height: 1.5em; } + """.trimIndent(), + "line-height.dss", + ) - val values = data.rules.mapNotNull { rule -> - (rule.declarations.get(StyleProperty.LINE_HEIGHT) as? StyleExpression.Literal)?.value - } + val values = + data.rules.mapNotNull { rule -> + (rule.declarations.get(StyleProperty.LINE_HEIGHT) as? StyleExpression.Literal)?.value + } assertEquals(listOf("normal", "24px", "1.5em"), values) } @Test fun `rejects invalid line-height literal`() { - val error = assertFailsWith { - DssParser.parse( - """ - .text { line-height: nope; } - """.trimIndent(), - "line-height-bad.dss" - ) - } + val error = + assertFailsWith { + DssParser.parse( + """ + .text { line-height: nope; } + """.trimIndent(), + "line-height-bad.dss", + ) + } assertTrue(error.message?.contains("Expected CSS length") == true) - }} - - + } +} diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/LineHeightStyleContractTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/LineHeightStyleContractTests.kt index 0a36d95..22d5005 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/LineHeightStyleContractTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/LineHeightStyleContractTests.kt @@ -27,9 +27,10 @@ class LineHeightStyleContractTests { @Test fun `line-height reaches computed style from inline declarations`() { val node = ContainerNode(key = "line-height-node") - node.inlineStyleDeclarations = StyleDeclarations().apply { - set(StyleProperty.LINE_HEIGHT, StyleExpression.Literal("22px")) - } + node.inlineStyleDeclarations = + StyleDeclarations().apply { + set(StyleProperty.LINE_HEIGHT, StyleExpression.Literal("22px")) + } StyleEngine.clearCache() StyleEngine.applyStylesRecursively(node) @@ -43,9 +44,10 @@ class LineHeightStyleContractTests { @Test fun `line-height inherits through computed style`() { val root = ContainerNode(key = "root") - root.inlineStyleDeclarations = StyleDeclarations().apply { - set(StyleProperty.LINE_HEIGHT, StyleExpression.Literal("24px")) - } + root.inlineStyleDeclarations = + StyleDeclarations().apply { + set(StyleProperty.LINE_HEIGHT, StyleExpression.Literal("24px")) + } val child = TextNode(TextSource.Static("child"), key = "child").applyParent(root) StyleEngine.clearCache() diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/OverflowSizeStyleContractTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/OverflowSizeStyleContractTests.kt index 7b810c4..592a607 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/OverflowSizeStyleContractTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/OverflowSizeStyleContractTests.kt @@ -37,15 +37,16 @@ class OverflowSizeStyleContractTests { @Test fun `overflow axis properties override shorthand-derived values`() { - val target = styledTarget( - """ - .target { - overflow: hidden auto; - overflow-x: visible; - overflow-y: scroll; - } - """.trimIndent() - ) + val target = + styledTarget( + """ + .target { + overflow: hidden auto; + overflow-x: visible; + overflow-y: scroll; + } + """.trimIndent(), + ) val style = target.appliedComputedStyleSnapshot() ?: error("Missing computed style") assertEquals(Overflow.Visible, style.overflowX) @@ -54,16 +55,17 @@ class OverflowSizeStyleContractTests { @Test fun `min and max size properties parse and persist in computed style`() { - val target = styledTarget( - """ - .target { - min-width: 120px; - min-height: 10vh; - max-width: 75%; - max-height: auto; - } - """.trimIndent() - ) + val target = + styledTarget( + """ + .target { + min-width: 120px; + min-height: 10vh; + max-width: 75%; + max-height: auto; + } + """.trimIndent(), + ) val style = target.appliedComputedStyleSnapshot() ?: error("Missing computed style") assertEquals(CssLength(120f, CssUnit.Px), style.minWidth) @@ -115,4 +117,3 @@ class OverflowSizeStyleContractTests { assertEquals(expectedY, actual.overflowY) } } - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/PositionedLayoutStyleContractTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/PositionedLayoutStyleContractTests.kt index 103ce17..e7e8497 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/PositionedLayoutStyleContractTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/PositionedLayoutStyleContractTests.kt @@ -1,7 +1,7 @@ package org.dreamfinity.dsgl.core.style -import org.dreamfinity.dsgl.core.dsl.StyleScope import org.dreamfinity.dsgl.core.dom.elements.ContainerNode +import org.dreamfinity.dsgl.core.dsl.StyleScope import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -38,23 +38,25 @@ class PositionedLayoutStyleContractTests { @Test fun `position z-index and offsets reach computed style`() { val node = ContainerNode(key = "positioned-style-node") - val expectations = listOf( - "static" to PositionMode.Static, - "relative" to PositionMode.Relative, - "absolute" to PositionMode.Absolute, - "fixed" to PositionMode.Fixed, - "sticky" to PositionMode.Sticky - ) + val expectations = + listOf( + "static" to PositionMode.Static, + "relative" to PositionMode.Relative, + "absolute" to PositionMode.Absolute, + "fixed" to PositionMode.Fixed, + "sticky" to PositionMode.Sticky, + ) expectations.forEach { (literal, expectedPosition) -> - node.inlineStyleDeclarations = StyleDeclarations().apply { - set(StyleProperty.POSITION, StyleExpression.Literal(literal)) - set(StyleProperty.LEFT, StyleExpression.Literal("10px")) - set(StyleProperty.TOP, StyleExpression.Literal("2em")) - set(StyleProperty.RIGHT, StyleExpression.Literal("auto")) - set(StyleProperty.BOTTOM, StyleExpression.Literal("0")) - set(StyleProperty.Z_INDEX, StyleExpression.Literal("13")) - } + node.inlineStyleDeclarations = + StyleDeclarations().apply { + set(StyleProperty.POSITION, StyleExpression.Literal(literal)) + set(StyleProperty.LEFT, StyleExpression.Literal("10px")) + set(StyleProperty.TOP, StyleExpression.Literal("2em")) + set(StyleProperty.RIGHT, StyleExpression.Literal("auto")) + set(StyleProperty.BOTTOM, StyleExpression.Literal("0")) + set(StyleProperty.Z_INDEX, StyleExpression.Literal("13")) + } StyleEngine.clearCache() StyleEngine.applyStylesRecursively(node) val computed = node.appliedComputedStyleSnapshot() @@ -79,4 +81,3 @@ class PositionedLayoutStyleContractTests { assertEquals("sticky", literal) } } - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleCascadeCombinatorTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleCascadeCombinatorTests.kt index a6c5f0b..26d6c8d 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleCascadeCombinatorTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleCascadeCombinatorTests.kt @@ -25,7 +25,7 @@ class StyleCascadeCombinatorTests { installStylesheet( """ .panel .item { color: #00AA00; } - """.trimIndent() + """.trimIndent(), ) val root = ContainerNode(key = "root") @@ -48,7 +48,7 @@ class StyleCascadeCombinatorTests { installStylesheet( """ .panel > .item { color: #3355AA; } - """.trimIndent() + """.trimIndent(), ) val root = ContainerNode(key = "root") @@ -72,7 +72,7 @@ class StyleCascadeCombinatorTests { text { color: #111111; } .btn { color: #222222; } #target { color: #333333; } - """.trimIndent() + """.trimIndent(), ) val root = ContainerNode(key = "root") @@ -90,7 +90,7 @@ class StyleCascadeCombinatorTests { """ .btn { color: #111111; } .btn { color: #222222; } - """.trimIndent() + """.trimIndent(), ) val root = ContainerNode(key = "root") @@ -107,7 +107,7 @@ class StyleCascadeCombinatorTests { """ .btn { color: #111111 !important; } .btn { color: #222222; } - """.trimIndent() + """.trimIndent(), ) val root = ContainerNode(key = "root") @@ -123,7 +123,7 @@ class StyleCascadeCombinatorTests { installStylesheet( """ .parent { color: #ABCDEF; font-size: 14px; padding: 7px; } - """.trimIndent() + """.trimIndent(), ) val root = ContainerNode(key = "root") @@ -142,7 +142,7 @@ class StyleCascadeCombinatorTests { installStylesheet( """ .panel:hover .item { color: #00AAFF; } - """.trimIndent() + """.trimIndent(), ) val root = ContainerNode(key = "root") @@ -163,7 +163,7 @@ class StyleCascadeCombinatorTests { installStylesheet( """ .a + .b { color: #FF22AA; } - """.trimIndent() + """.trimIndent(), ) val root = ContainerNode(key = "root") @@ -189,7 +189,7 @@ class StyleCascadeCombinatorTests { installStylesheet( """ .a ~ .b { color: #22AAFF; } - """.trimIndent() + """.trimIndent(), ) val root = ContainerNode(key = "root") @@ -215,7 +215,7 @@ class StyleCascadeCombinatorTests { """ .a + .b .c { color: #5566FF; } .a ~ .b > .c { font-size: 14px; } - """.trimIndent() + """.trimIndent(), ) val root = ContainerNode(key = "root") @@ -238,7 +238,7 @@ class StyleCascadeCombinatorTests { """ .a ~ .b { color: #2255AA; } .b { color: #992222 !important; } - """.trimIndent() + """.trimIndent(), ) val root = ContainerNode(key = "root") @@ -257,7 +257,7 @@ class StyleCascadeCombinatorTests { """ .alias { foreground-color: #113355; } .canonical { color: #113355; } - """.trimIndent() + """.trimIndent(), ) val root = ContainerNode(key = "root") @@ -276,7 +276,7 @@ class StyleCascadeCombinatorTests { installStylesheet( """ * { align: end; } - """.trimIndent() + """.trimIndent(), ) val root = ContainerNode(key = "root") @@ -294,7 +294,7 @@ class StyleCascadeCombinatorTests { installStylesheet( """ :root { align: end; } - """.trimIndent() + """.trimIndent(), ) val root = ContainerNode(key = "root") diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleDeclarationsHashTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleDeclarationsHashTests.kt index 3a3c2e2..c91cdc2 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleDeclarationsHashTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleDeclarationsHashTests.kt @@ -7,26 +7,29 @@ import kotlin.test.assertNotEquals class StyleDeclarationsHashTests { @Test fun `stable hash is independent from insertion order`() { - val first = StyleDeclarations().apply { - set(StyleProperty.WIDTH, StyleExpression.Literal("120")) - set(StyleProperty.DISPLAY, StyleExpression.Literal("flex")) - set(StyleProperty.GAP, StyleExpression.Literal("4")) - } - val second = StyleDeclarations().apply { - set(StyleProperty.GAP, StyleExpression.Literal("4")) - set(StyleProperty.WIDTH, StyleExpression.Literal("120")) - set(StyleProperty.DISPLAY, StyleExpression.Literal("flex")) - } + val first = + StyleDeclarations().apply { + set(StyleProperty.WIDTH, StyleExpression.Literal("120")) + set(StyleProperty.DISPLAY, StyleExpression.Literal("flex")) + set(StyleProperty.GAP, StyleExpression.Literal("4")) + } + val second = + StyleDeclarations().apply { + set(StyleProperty.GAP, StyleExpression.Literal("4")) + set(StyleProperty.WIDTH, StyleExpression.Literal("120")) + set(StyleProperty.DISPLAY, StyleExpression.Literal("flex")) + } assertEquals(first.toStableHash(), second.toStableHash()) } @Test fun `stable hash changes when declaration value changes`() { - val declarations = StyleDeclarations().apply { - set(StyleProperty.WIDTH, StyleExpression.Literal("120")) - set(StyleProperty.DISPLAY, StyleExpression.Literal("block")) - } + val declarations = + StyleDeclarations().apply { + set(StyleProperty.WIDTH, StyleExpression.Literal("120")) + set(StyleProperty.DISPLAY, StyleExpression.Literal("block")) + } val before = declarations.toStableHash() declarations.set(StyleProperty.DISPLAY, StyleExpression.Literal("flex")) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleEngineIncrementalTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleEngineIncrementalTests.kt index 544dcbd..969e07e 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleEngineIncrementalTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleEngineIncrementalTests.kt @@ -66,7 +66,7 @@ class StyleEngineIncrementalTests { installStylesheet( """ .a ~ .b { color: #33AA66; } - """.trimIndent() + """.trimIndent(), ) val root = ContainerNode(key = "root") @@ -92,7 +92,7 @@ class StyleEngineIncrementalTests { installStylesheet( """ .target { color: #2288FF; } - """.trimIndent() + """.trimIndent(), ) val root = ContainerNode(key = "root") @@ -113,7 +113,7 @@ class StyleEngineIncrementalTests { installStylesheet( """ .node { color: #FFFFFF; } - """.trimIndent() + """.trimIndent(), ) val root = ContainerNode(key = "root") diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleEngineInspectionTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleEngineInspectionTests.kt index 5f1ef7c..c8ab5f7 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleEngineInspectionTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleEngineInspectionTests.kt @@ -1,15 +1,15 @@ package org.dreamfinity.dsgl.core.style -import java.io.File -import java.nio.file.Files -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertEquals import org.dreamfinity.dsgl.core.DsglColors import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.elements.TextNode import org.dreamfinity.dsgl.core.dom.elements.TextSource +import java.io.File +import java.nio.file.Files +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals class StyleEngineInspectionTests { @AfterTest @@ -21,12 +21,13 @@ class StyleEngineInspectionTests { @Test fun `inspect reports selector and inline provenance`() { - val stylesDir = createTempStylesDir( - """ - text { color: #112233; } - #sample { color: #445566; } - """.trimIndent() - ) + val stylesDir = + createTempStylesDir( + """ + text { color: #112233; } + #sample { color: #445566; } + """.trimIndent(), + ) StyleEngine.setStylesDirectory(stylesDir) StyleEngine.forceReloadStylesheets() @@ -40,7 +41,7 @@ class StyleEngineInspectionTests { node.inlineStyleDeclarations.set( StyleProperty.FOREGROUND_COLOR, - StyleExpression.Literal("#ABCDEF") + StyleExpression.Literal("#ABCDEF"), ) val inlineInspection = StyleEngine.inspect(node) val inlineSource = inlineInspection.propertySources[StyleProperty.FOREGROUND_COLOR] @@ -50,12 +51,13 @@ class StyleEngineInspectionTests { @Test fun `inspector override has highest precedence`() { - val stylesDir = createTempStylesDir( - """ - text { color: #111111; } - #sample { color: #222222; } - """.trimIndent() - ) + val stylesDir = + createTempStylesDir( + """ + text { color: #111111; } + #sample { color: #222222; } + """.trimIndent(), + ) StyleEngine.setStylesDirectory(stylesDir) StyleEngine.forceReloadStylesheets() @@ -63,7 +65,7 @@ class StyleEngineInspectionTests { node.styleId = "sample" node.inlineStyleDeclarations.set( StyleProperty.FOREGROUND_COLOR, - StyleExpression.Literal("#333333") + StyleExpression.Literal("#333333"), ) StyleEngine.setInspectorOverrideLiteral("sample", StyleProperty.FOREGROUND_COLOR, "#444444").getOrThrow() diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleLengthUnitsIntegrationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleLengthUnitsIntegrationTests.kt index 602cbff..fac536f 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleLengthUnitsIntegrationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleLengthUnitsIntegrationTests.kt @@ -11,13 +11,18 @@ import kotlin.test.Test import kotlin.test.assertEquals class StyleLengthUnitsIntegrationTests { - private val ctx = object : UiMeasureContext { - override val fontHeight: Int = 10 - override fun measureText(text: String): Int = text.length * 6 - override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * ((fontSize ?: 10) / 2).coerceAtLeast(1) - override fun fontHeight(fontId: String?, fontSize: Int?): Int = fontSize ?: 10 - override fun paint(commands: List) = Unit - } + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 10 + + override fun measureText(text: String): Int = text.length * 6 + + override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * ((fontSize ?: 10) / 2).coerceAtLeast(1) + + override fun fontHeight(fontId: String?, fontSize: Int?): Int = fontSize ?: 10 + + override fun paint(commands: List) = Unit + } @AfterTest fun cleanup() { @@ -32,7 +37,7 @@ class StyleLengthUnitsIntegrationTests { """ .parent { font-size: 20px; } .child { font-size: 1.2em; } - """.trimIndent() + """.trimIndent(), ) val root = ContainerNode(key = "root") @@ -53,7 +58,7 @@ class StyleLengthUnitsIntegrationTests { """ .parent { font-size: 20px; } .child { font-size: 10px; padding: 1.5em; } - """.trimIndent() + """.trimIndent(), ) val root = ContainerNode(key = "root") @@ -79,7 +84,7 @@ class StyleLengthUnitsIntegrationTests { height: 25%; margin: 10% 5%; } - """.trimIndent() + """.trimIndent(), ) val root = ContainerNode(key = "root") @@ -105,7 +110,7 @@ class StyleLengthUnitsIntegrationTests { height: 20vh; padding: 1vh 1vw; } - """.trimIndent() + """.trimIndent(), ) val root = ContainerNode(key = "root") @@ -130,7 +135,7 @@ class StyleLengthUnitsIntegrationTests { font-size: 0.5rem; padding: 1rem; } - """.trimIndent() + """.trimIndent(), ) val root = ContainerNode(key = "root") diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StylePropertyRegistryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StylePropertyRegistryTests.kt index adf1ac4..b23abfc 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StylePropertyRegistryTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StylePropertyRegistryTests.kt @@ -7,7 +7,10 @@ import kotlin.test.assertTrue class StylePropertyRegistryTests { @Test fun `registry defines descriptor for every style property`() { - val registered = StylePropertyRegistry.all.map { it.property }.toSet() + val registered = + StylePropertyRegistry.all + .map { it.property } + .toSet() assertEquals(StyleProperty.entries.toSet(), registered) assertEquals(StyleProperty.entries.size, StylePropertyRegistry.all.size) } @@ -51,11 +54,11 @@ class StylePropertyRegistryTests { assertEquals( LineHeightValue.Normal, - StylePropertyRegistry.parseLineHeightLiteral(StyleProperty.LINE_HEIGHT, "normal") + StylePropertyRegistry.parseLineHeightLiteral(StyleProperty.LINE_HEIGHT, "normal"), ) assertEquals( LineHeightValue.Length(CssLength(24f, CssUnit.Px)), - StylePropertyRegistry.parseLineHeightLiteral(StyleProperty.LINE_HEIGHT, "24px") + StylePropertyRegistry.parseLineHeightLiteral(StyleProperty.LINE_HEIGHT, "24px"), ) } @@ -65,7 +68,7 @@ class StylePropertyRegistryTests { StyleProperty.LEFT, StyleProperty.TOP, StyleProperty.RIGHT, - StyleProperty.BOTTOM + StyleProperty.BOTTOM, ).forEach { property -> val descriptor = StylePropertyRegistry.descriptor(property) assertEquals(StyleValueGrammarKind.LengthLike, descriptor.grammarKind) @@ -76,7 +79,10 @@ class StylePropertyRegistryTests { @Test fun `registry includes text style editable properties`() { - val properties = StylePropertyRegistry.all.map { it.property }.toSet() + val properties = + StylePropertyRegistry.all + .map { it.property } + .toSet() assertTrue(StyleProperty.FOREGROUND_COLOR in properties) assertTrue(StyleProperty.FONT_SIZE in properties) assertTrue(StyleProperty.LINE_HEIGHT in properties) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleScopeComplexDslContractTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleScopeComplexDslContractTests.kt index 8734e15..8226bfd 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleScopeComplexDslContractTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleScopeComplexDslContractTests.kt @@ -1,8 +1,8 @@ package org.dreamfinity.dsgl.core.style +import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dsl.StyleBorder import org.dreamfinity.dsgl.core.dsl.StyleScope -import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import kotlin.test.Test import kotlin.test.assertEquals @@ -165,11 +165,7 @@ class StyleScopeComplexDslContractTests { assertEquals(null, bottomValue) } - private fun assertLiteral( - node: ContainerNode, - property: StyleProperty, - expected: String - ) { + private fun assertLiteral(node: ContainerNode, property: StyleProperty, expected: String) { val value = (node.inlineStyleDeclarations.get(property) as? StyleExpression.Literal)?.value assertEquals(expected, value) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleValueParsingTransformTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleValueParsingTransformTests.kt index fb655b0..3a8b334 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleValueParsingTransformTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleValueParsingTransformTests.kt @@ -37,4 +37,3 @@ class StyleValueParsingTransformTests { } } } - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/text/MinecraftFormattingParserTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/text/MinecraftFormattingParserTests.kt index a765260..7aec3ae 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/text/MinecraftFormattingParserTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/text/MinecraftFormattingParserTests.kt @@ -1,22 +1,23 @@ package org.dreamfinity.dsgl.core.text -import org.dreamfinity.dsgl.core.font.MsdfFontMetaParser import org.dreamfinity.dsgl.core.dom.elements.support.TextLayoutEngine +import org.dreamfinity.dsgl.core.font.MsdfFontMetaParser import org.dreamfinity.dsgl.core.style.TextFormatting import org.dreamfinity.dsgl.core.style.TextWrap import kotlin.test.Test -import kotlin.test.assertFalse import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertTrue class MinecraftFormattingParserTests { @Test fun `parses legacy colors and reset into plain text with spans`() { - val parsed = MinecraftFormattingParser.parse( - text = "\u00A7aHi \u00A7bWorld\u00A7r!", - mode = TextFormatting.Minecraft - ) + val parsed = + MinecraftFormattingParser.parse( + text = "\u00A7aHi \u00A7bWorld\u00A7r!", + mode = TextFormatting.Minecraft, + ) assertEquals("Hi World!", parsed.plainText) val spans = MinecraftFormattingParser.resolveColorSpans(parsed, 0xFF336699.toInt()) @@ -67,10 +68,11 @@ class MinecraftFormattingParserTests { @Test fun `modern hex sequence is parsed when complete`() { - val parsed = MinecraftFormattingParser.parse( - text = "\u00A7x\u00A7F\u00A7F\u00A70\u00A70\u00A70\u00A70R", - mode = TextFormatting.Minecraft - ) + val parsed = + MinecraftFormattingParser.parse( + text = "\u00A7x\u00A7F\u00A7F\u00A70\u00A70\u00A70\u00A70R", + mode = TextFormatting.Minecraft, + ) assertEquals("R", parsed.plainText) val spans = MinecraftFormattingParser.resolveColorSpans(parsed, 0xAA000000.toInt()) assertEquals(1, spans.size) @@ -79,24 +81,27 @@ class MinecraftFormattingParserTests { @Test fun `format flags toggle and reset to base style`() { - val parsed = MinecraftFormattingParser.parse( - text = "\u00A7lA\u00A7oB\u00A7nC\u00A7mD\u00A7kE\u00A7rF", - mode = TextFormatting.Minecraft - ) + val parsed = + MinecraftFormattingParser.parse( + text = "\u00A7lA\u00A7oB\u00A7nC\u00A7mD\u00A7kE\u00A7rF", + mode = TextFormatting.Minecraft, + ) assertEquals("ABCDEF", parsed.plainText) - val baseFlags = TextStyleFlags( - bold = false, - italic = true, - underline = false, - strikethrough = false, - obfuscated = false - ) - val spans = MinecraftFormattingParser.resolveStyleSpans( - parsed = parsed, - baseColor = 0xFF445566.toInt(), - baseFlags = baseFlags - ) + val baseFlags = + TextStyleFlags( + bold = false, + italic = true, + underline = false, + strikethrough = false, + obfuscated = false, + ) + val spans = + MinecraftFormattingParser.resolveStyleSpans( + parsed = parsed, + baseColor = 0xFF445566.toInt(), + baseFlags = baseFlags, + ) assertTrue(spans.isNotEmpty()) assertEquals(true, spans[0].flags.bold) assertEquals(true, spans[1].flags.bold) @@ -104,8 +109,20 @@ class MinecraftFormattingParserTests { assertEquals(true, spans[2].flags.underline) assertEquals(true, spans[3].flags.strikethrough) assertEquals(true, spans[4].flags.obfuscated) - assertEquals(true, spans.last().flags.italic, "§r should reset to base italic=true") - assertEquals(false, spans.last().flags.bold, "§r should reset bold to base bold=false") + assertEquals( + true, + spans + .last() + .flags.italic, + "§r should reset to base italic=true", + ) + assertEquals( + false, + spans + .last() + .flags.bold, + "§r should reset bold to base bold=false", + ) } @Test @@ -117,64 +134,70 @@ class MinecraftFormattingParserTests { assertEquals("ABC", plain) val baseWidth = meta.measureTextWidth(plain, 12) - val extra = TextStyleMetrics.boldExtraPxForRange( - plainText = plain, - spans = parsed.spans, - baseFlags = TextStyleFlags.NONE - ) + val extra = + TextStyleMetrics.boldExtraPxForRange( + plainText = plain, + spans = parsed.spans, + baseFlags = TextStyleFlags.NONE, + ) assertEquals(BOLD_ADVANCE_EXTRA_PX, extra) assertEquals(baseWidth + BOLD_ADVANCE_EXTRA_PX, baseWidth + extra) } @Test fun `obfuscation selector is deterministic and time-varying`() { - val fixedA = ObfuscationTextSelector.selectCandidateIndex( - sourceKey = "node.key", - lineIndex = 1, - glyphIndexInLine = 4, - timeSlice = 10L, - originalCodepoint = 'A'.code, - candidateCount = 16 - ) - val fixedB = ObfuscationTextSelector.selectCandidateIndex( - sourceKey = "node.key", - lineIndex = 1, - glyphIndexInLine = 4, - timeSlice = 10L, - originalCodepoint = 'A'.code, - candidateCount = 16 - ) - val changed = ObfuscationTextSelector.selectCandidateIndex( - sourceKey = "node.key", - lineIndex = 1, - glyphIndexInLine = 4, - timeSlice = 11L, - originalCodepoint = 'A'.code, - candidateCount = 16 - ) - assertEquals(fixedA, fixedB) - assertTrue(fixedA in 0 until 16) - assertTrue(changed in 0 until 16) - val rowA = (0 until 16).map { index -> + val fixedA = ObfuscationTextSelector.selectCandidateIndex( sourceKey = "node.key", - lineIndex = 0, - glyphIndexInLine = index, - timeSlice = 25L, - originalCodepoint = 'X'.code, - candidateCount = 32 + lineIndex = 1, + glyphIndexInLine = 4, + timeSlice = 10L, + originalCodepoint = 'A'.code, + candidateCount = 16, ) - } - val rowB = (0 until 16).map { index -> + val fixedB = ObfuscationTextSelector.selectCandidateIndex( sourceKey = "node.key", - lineIndex = 0, - glyphIndexInLine = index, - timeSlice = 26L, - originalCodepoint = 'X'.code, - candidateCount = 32 + lineIndex = 1, + glyphIndexInLine = 4, + timeSlice = 10L, + originalCodepoint = 'A'.code, + candidateCount = 16, ) - } + val changed = + ObfuscationTextSelector.selectCandidateIndex( + sourceKey = "node.key", + lineIndex = 1, + glyphIndexInLine = 4, + timeSlice = 11L, + originalCodepoint = 'A'.code, + candidateCount = 16, + ) + assertEquals(fixedA, fixedB) + assertTrue(fixedA in 0 until 16) + assertTrue(changed in 0 until 16) + val rowA = + (0 until 16).map { index -> + ObfuscationTextSelector.selectCandidateIndex( + sourceKey = "node.key", + lineIndex = 0, + glyphIndexInLine = index, + timeSlice = 25L, + originalCodepoint = 'X'.code, + candidateCount = 32, + ) + } + val rowB = + (0 until 16).map { index -> + ObfuscationTextSelector.selectCandidateIndex( + sourceKey = "node.key", + lineIndex = 0, + glyphIndexInLine = index, + timeSlice = 26L, + originalCodepoint = 'X'.code, + candidateCount = 32, + ) + } assertTrue(rowA.toSet().size >= 8, "Obfuscated row should produce varied symbols") val changedSlots = rowA.indices.count { rowA[it] != rowB[it] } assertTrue(changedSlots >= 8, "Obfuscated symbols should refresh across time slices") @@ -186,28 +209,31 @@ class MinecraftFormattingParserTests { fun `decoration spans can be split per wrapped line`() { val rawMeta = loadResource("fonts/minecraft/MinecraftDefault-Regular-meta.json") val meta = MsdfFontMetaParser.parse(rawMeta) - val parsed = MinecraftFormattingParser.parse( - text = "\u00A7nUnderline decoration wraps across multiple lines in a narrow panel", - mode = TextFormatting.Minecraft - ) + val parsed = + MinecraftFormattingParser.parse( + text = "\u00A7nUnderline decoration wraps across multiple lines in a narrow panel", + mode = TextFormatting.Minecraft, + ) val fontSize = 12 val lineHeight = meta.lineHeightPx(fontSize) - val layout = TextLayoutEngine.layout( - text = parsed.plainText, - maxWidth = 72, - wrap = TextWrap.Wrap, - fontHeight = lineHeight, - measureText = { value -> meta.measureTextWidth(value, fontSize) } - ) + val layout = + TextLayoutEngine.layout( + text = parsed.plainText, + maxWidth = 72, + wrap = TextWrap.Wrap, + fontHeight = lineHeight, + measureText = { value -> meta.measureTextWidth(value, fontSize) }, + ) assertTrue(layout.lines.size >= 2) layout.lines.forEach { line -> - val lineSpans = MinecraftFormattingParser.resolveStyleSpans( - parsed = parsed, - baseColor = 0xFFFFFFFF.toInt(), - baseFlags = TextStyleFlags.NONE, - rangeStart = line.startIndex, - rangeEnd = line.endIndexExclusive - ) + val lineSpans = + MinecraftFormattingParser.resolveStyleSpans( + parsed = parsed, + baseColor = 0xFFFFFFFF.toInt(), + baseFlags = TextStyleFlags.NONE, + rangeStart = line.startIndex, + rangeEnd = line.endIndexExclusive, + ) assertTrue(lineSpans.all { it.start >= 0 && it.end <= line.text.length }) if (line.text.isNotEmpty()) { assertTrue(lineSpans.any { it.flags.underline }) @@ -221,4 +247,3 @@ class MinecraftFormattingParserTests { return stream.bufferedReader(Charsets.UTF_8).use { it.readText() } } } - diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/text/TextDecorationLayoutTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/text/TextDecorationLayoutTests.kt index 48cd5a5..5f5e0b8 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/text/TextDecorationLayoutTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/text/TextDecorationLayoutTests.kt @@ -9,16 +9,18 @@ class TextDecorationLayoutTests { fun `converts y-up font metrics to y-down screen coordinates`() { val fontPx = 16 val scalePx = TextDecorationLayout.scalePx(fontPx = fontPx, emSize = 1f) - val baseline = TextDecorationLayout.baselineY( - lineTopY = 10f, - ascenderEm = 0.932f, - scalePx = scalePx - ) - val underlineY = TextDecorationLayout.screenYFromYUpMetric( - baselineY = baseline, - metricYUpEm = -0.162f, - scalePx = scalePx - ) + val baseline = + TextDecorationLayout.baselineY( + lineTopY = 10f, + ascenderEm = 0.932f, + scalePx = scalePx, + ) + val underlineY = + TextDecorationLayout.screenYFromYUpMetric( + baselineY = baseline, + metricYUpEm = -0.162f, + scalePx = scalePx, + ) assertEquals(24.912f, baseline, 0.0001f) assertTrue(underlineY > baseline) @@ -26,32 +28,36 @@ class TextDecorationLayoutTests { @Test fun `uses fallback thickness and position for tiny underline metrics`() { - val line = TextVisualLine( - lineIndex = 0, - lineTopY = 0f, - baselineY = TextDecorationLayout.baselineY( + val line = + TextVisualLine( + lineIndex = 0, lineTopY = 0f, + baselineY = + TextDecorationLayout.baselineY( + lineTopY = 0f, + ascenderEm = 0.694f, + scalePx = TextDecorationLayout.scalePx(fontPx = 14, emSize = 1f), + ), + lineHeightPx = 11f, + glyphStartIndex = 0, + glyphEndIndexExclusive = 1, + ) + val metrics = + DecorationFontMetrics( + emSize = 1f, + lineHeightEm = 0.746f, ascenderEm = 0.694f, - scalePx = TextDecorationLayout.scalePx(fontPx = 14, emSize = 1f) - ), - lineHeightPx = 11f, - glyphStartIndex = 0, - glyphEndIndexExclusive = 1 - ) - val metrics = DecorationFontMetrics( - emSize = 1f, - lineHeightEm = 0.746f, - ascenderEm = 0.694f, - descenderEm = 0f, - underlineYEm = -0.0015f, - underlineThicknessEm = 0.0015f - ) + descenderEm = 0f, + underlineYEm = -0.0015f, + underlineThicknessEm = 0.0015f, + ) - val resolved = TextDecorationLayout.resolveLineMetrics( - line = line, - fontMetrics = metrics, - fontPx = 14 - ) + val resolved = + TextDecorationLayout.resolveLineMetrics( + line = line, + fontMetrics = metrics, + fontPx = 14, + ) assertTrue(resolved.underlineThickness >= 1f) assertTrue(resolved.underlineY > line.baselineY) @@ -59,45 +65,50 @@ class TextDecorationLayoutTests { @Test fun `splits decoration segments per visual line`() { - val metrics = DecorationFontMetrics( - emSize = 1f, - lineHeightEm = 1f, - ascenderEm = 0.8f, - descenderEm = -0.2f, - underlineYEm = -0.1f, - underlineThicknessEm = 0.05f - ) - val lines = listOf( - TextVisualLine( - lineIndex = 0, - lineTopY = 0f, - baselineY = 12f, - lineHeightPx = 16f, - glyphStartIndex = 0, - glyphEndIndexExclusive = 2 - ), - TextVisualLine( - lineIndex = 1, - lineTopY = 16f, - baselineY = 28f, - lineHeightPx = 16f, - glyphStartIndex = 2, - glyphEndIndexExclusive = 4 + val metrics = + DecorationFontMetrics( + emSize = 1f, + lineHeightEm = 1f, + ascenderEm = 0.8f, + descenderEm = -0.2f, + underlineYEm = -0.1f, + underlineThicknessEm = 0.05f, + ) + val lines = + listOf( + TextVisualLine( + lineIndex = 0, + lineTopY = 0f, + baselineY = 12f, + lineHeightPx = 16f, + glyphStartIndex = 0, + glyphEndIndexExclusive = 2, + ), + TextVisualLine( + lineIndex = 1, + lineTopY = 16f, + baselineY = 28f, + lineHeightPx = 16f, + glyphStartIndex = 2, + glyphEndIndexExclusive = 4, + ), + ) + val glyphs = + listOf( + GlyphDecorationSample(0, 0, 0f, 8f, 0xFFFFFFFF.toInt(), underline = true, strikethrough = false), + GlyphDecorationSample(0, 1, 8f, 16f, 0xFFFFFFFF.toInt(), underline = true, strikethrough = false), + GlyphDecorationSample(1, 2, 0f, 7f, 0xFFFFFFFF.toInt(), underline = true, strikethrough = false), + GlyphDecorationSample(1, 3, 7f, 14f, 0xFFFFFFFF.toInt(), underline = true, strikethrough = false), ) - ) - val glyphs = listOf( - GlyphDecorationSample(0, 0, 0f, 8f, 0xFFFFFFFF.toInt(), underline = true, strikethrough = false), - GlyphDecorationSample(0, 1, 8f, 16f, 0xFFFFFFFF.toInt(), underline = true, strikethrough = false), - GlyphDecorationSample(1, 2, 0f, 7f, 0xFFFFFFFF.toInt(), underline = true, strikethrough = false), - GlyphDecorationSample(1, 3, 7f, 14f, 0xFFFFFFFF.toInt(), underline = true, strikethrough = false) - ) - val quads = TextDecorationLayout.buildDecorationQuads( - lines = lines, - glyphs = glyphs, - fontMetrics = metrics, - fontPx = 16 - ).filter { it.type == DecorationType.Underline } + val quads = + TextDecorationLayout + .buildDecorationQuads( + lines = lines, + glyphs = glyphs, + fontMetrics = metrics, + fontPx = 16, + ).filter { it.type == DecorationType.Underline } assertEquals(2, quads.size) assertEquals(0, quads[0].lineIndex) @@ -106,38 +117,42 @@ class TextDecorationLayoutTests { @Test fun `splits segments when style color changes mid-line`() { - val metrics = DecorationFontMetrics( - emSize = 1f, - lineHeightEm = 1f, - ascenderEm = 0.8f, - descenderEm = -0.2f, - underlineYEm = -0.1f, - underlineThicknessEm = 0.05f - ) - val lines = listOf( - TextVisualLine( - lineIndex = 0, - lineTopY = 0f, - baselineY = 12f, - lineHeightPx = 16f, - glyphStartIndex = 0, - glyphEndIndexExclusive = 3 + val metrics = + DecorationFontMetrics( + emSize = 1f, + lineHeightEm = 1f, + ascenderEm = 0.8f, + descenderEm = -0.2f, + underlineYEm = -0.1f, + underlineThicknessEm = 0.05f, + ) + val lines = + listOf( + TextVisualLine( + lineIndex = 0, + lineTopY = 0f, + baselineY = 12f, + lineHeightPx = 16f, + glyphStartIndex = 0, + glyphEndIndexExclusive = 3, + ), ) - ) val colorA = 0xFFFF0000.toInt() val colorB = 0xFF00FF00.toInt() - val glyphs = listOf( - GlyphDecorationSample(0, 0, 0f, 6f, colorA, underline = true, strikethrough = false), - GlyphDecorationSample(0, 1, 6f, 12f, colorB, underline = true, strikethrough = false), - GlyphDecorationSample(0, 2, 12f, 18f, colorB, underline = true, strikethrough = true) - ) + val glyphs = + listOf( + GlyphDecorationSample(0, 0, 0f, 6f, colorA, underline = true, strikethrough = false), + GlyphDecorationSample(0, 1, 6f, 12f, colorB, underline = true, strikethrough = false), + GlyphDecorationSample(0, 2, 12f, 18f, colorB, underline = true, strikethrough = true), + ) - val quads = TextDecorationLayout.buildDecorationQuads( - lines = lines, - glyphs = glyphs, - fontMetrics = metrics, - fontPx = 16 - ) + val quads = + TextDecorationLayout.buildDecorationQuads( + lines = lines, + glyphs = glyphs, + fontMetrics = metrics, + fontPx = 16, + ) val underline = quads.filter { it.type == DecorationType.Underline } val strike = quads.filter { it.type == DecorationType.Strikethrough } @@ -147,4 +162,3 @@ class TextDecorationLayoutTests { assertEquals(1, strike.size) } } - diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e69de29..3bb84c5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -0,0 +1,7 @@ +[versions] +ktlint-plugin = "14.0.1" +detekt-plugin = "1.23.8" + +[plugins] +ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint-plugin" } +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt-plugin" } From c0759d101fb0bfaa3a34f5150aaf39c294b8213c Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Fri, 24 Apr 2026 15:24:23 +0300 Subject: [PATCH 34/78] adding and configuring detekt to project (without code fixing for now); --- .../dsgl-linter.conventions.gradle.kts | 18 - adapters/mc-forge-1-7-10/build.gradle.kts | 1 + .../mc-forge-1-7-10/demo/build.gradle.kts | 1 + build-logic/build.gradle.kts | 1 + ...sgl-static-analysis.conventions.gradle.kts | 28 + build.gradle.kts | 66 +- config/detekt/detekt.yml | 793 ++++++++++++++++++ core/build.gradle.kts | 1 + 8 files changed, 877 insertions(+), 32 deletions(-) delete mode 100644 adapters/mc-forge-1-7-10/adapter-build-logic/dsgl-linter.conventions.gradle.kts create mode 100644 build-logic/src/main/kotlin/dsgl-static-analysis.conventions.gradle.kts create mode 100644 config/detekt/detekt.yml diff --git a/adapters/mc-forge-1-7-10/adapter-build-logic/dsgl-linter.conventions.gradle.kts b/adapters/mc-forge-1-7-10/adapter-build-logic/dsgl-linter.conventions.gradle.kts deleted file mode 100644 index 78c3063..0000000 --- a/adapters/mc-forge-1-7-10/adapter-build-logic/dsgl-linter.conventions.gradle.kts +++ /dev/null @@ -1,18 +0,0 @@ -plugins { - id("org.jlleitschuh.gradle.ktlint") -} - -ktlint { - version.set("1.5.0") - outputToConsole.set(true) - coloredOutput.set(true) - reporters { - reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.PLAIN) - reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE) - } - filter { - exclude("**/generated/**") - exclude("**/build/**") - include("**/*.kt") - } -} \ No newline at end of file diff --git a/adapters/mc-forge-1-7-10/build.gradle.kts b/adapters/mc-forge-1-7-10/build.gradle.kts index 8a0e02c..c6424dc 100644 --- a/adapters/mc-forge-1-7-10/build.gradle.kts +++ b/adapters/mc-forge-1-7-10/build.gradle.kts @@ -3,6 +3,7 @@ plugins { id("dsgl-mc-forge-1-7-10.conventions") id("dsgl-releaseable-module.conventions") id("dsgl-linter.conventions") + id("dsgl-static-analysis.conventions") } dsglRelease { diff --git a/adapters/mc-forge-1-7-10/demo/build.gradle.kts b/adapters/mc-forge-1-7-10/demo/build.gradle.kts index 1daa4bb..bdcb4de 100644 --- a/adapters/mc-forge-1-7-10/demo/build.gradle.kts +++ b/adapters/mc-forge-1-7-10/demo/build.gradle.kts @@ -2,6 +2,7 @@ plugins { id("dsgl-mc-adapter.conventions") id("dsgl-mc-forge-1-7-10.conventions") id("dsgl-linter.conventions") + id("dsgl-static-analysis.conventions") } val modId: String by project diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index d2eb1b8..8fc6d32 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") implementation("org.jetbrains.dokka:org.jetbrains.dokka.gradle.plugin:2.1.0") implementation(libs.plugins.ktlint.toDep()) + implementation(libs.plugins.detekt.toDep()) } fun Provider.toDep() = diff --git a/build-logic/src/main/kotlin/dsgl-static-analysis.conventions.gradle.kts b/build-logic/src/main/kotlin/dsgl-static-analysis.conventions.gradle.kts new file mode 100644 index 0000000..ed2542d --- /dev/null +++ b/build-logic/src/main/kotlin/dsgl-static-analysis.conventions.gradle.kts @@ -0,0 +1,28 @@ +import org.gradle.api.JavaVersion + +plugins { + id("io.gitlab.arturbosch.detekt") +} + +detekt { + buildUponDefaultConfig = true + allRules = false + config.setFrom(rootProject.files("config/detekt/detekt.yml")) + baseline = rootProject.file("config/detekt/baseline-${project.name}.xml") + parallel = true +} + +tasks.withType().configureEach { + jvmTarget = "1.8" + reports { + html.required.set(true) + sarif.required.set(true) + xml.required.set(false) + txt.required.set(false) + } + exclude("**/generated/**", "**/build/**") +} + +tasks.withType().configureEach { + jvmTarget = "1.8" +} diff --git a/build.gradle.kts b/build.gradle.kts index de60cb2..0daa4d8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -80,7 +80,7 @@ tasks.register("generateMsdfAtlases") { val rgbaArgs = commonArgs + listOf( "-format", "rgba", "-imageout", rgbaAtlasOutArg, - "-json", jsonOutArg + "-json", jsonOutArg, ) println("Generating png atlas for $fontArg") @@ -94,7 +94,7 @@ tasks.register("generateMsdfAtlases") { if (genPNGResult.exitValue != 0) { throw GradleException( "msdf-atlas-gen failed for '$fontArg' with exit code ${genPNGResult.exitValue}. " + - "Expected outputs: '$pngAtlasOutArg', '$jsonOutArg'" + "Expected outputs: '$pngAtlasOutArg', '$jsonOutArg'", ) } @@ -109,7 +109,7 @@ tasks.register("generateMsdfAtlases") { if (genRGBAResult.exitValue != 0) { throw GradleException( "msdf-atlas-gen failed for '$fontArg' with exit code ${genRGBAResult.exitValue}. " + - "Expected outputs: '$pngAtlasOutArg', '$jsonOutArg'" + "Expected outputs: '$pngAtlasOutArg', '$jsonOutArg'", ) } @@ -191,7 +191,7 @@ tasks.register("compressMsdfRgbaAtlases") { tmpOut.toPath(), out.toPath(), StandardCopyOption.REPLACE_EXISTING, - StandardCopyOption.ATOMIC_MOVE + StandardCopyOption.ATOMIC_MOVE, ) logger.lifecycle("Flip+deflate: ${rgba.name} -> ${out.name} (${rgba.length()} -> ${out.length()} bytes)") @@ -225,7 +225,7 @@ data class BumpConfig( val versionFile: File, val versionKey: String = "version", val syncedKeys: List = emptyList(), - val publishTaskPath: String + val publishTaskPath: String, ) data class BumpSummary( @@ -235,7 +235,7 @@ data class BumpSummary( val newVersion: String, val updatedKeys: List, val publishedModules: List, - val coordinates: List + val coordinates: List, ) val semVerRegex = Regex("""^(\d+)\.(\d+)\.(\d+)$""") @@ -244,7 +244,7 @@ val coreBumpConfig = BumpConfig( projectPath = ":core", versionFile = rootProject.file("core/gradle.properties"), versionKey = "moduleVersion", - publishTaskPath = ":core:publishToMavenLocal" + publishTaskPath = ":core:publishToMavenLocal", ) val mc1710BumpConfig = BumpConfig( target = BumpTarget.MC1710, @@ -252,13 +252,13 @@ val mc1710BumpConfig = BumpConfig( versionFile = rootProject.file("adapters/mc-forge-1-7-10/gradle.properties"), versionKey = "moduleVersion", syncedKeys = listOf("modVersion"), - publishTaskPath = ":adapters:mc-forge-1-7-10:publishToMavenLocal" + publishTaskPath = ":adapters:mc-forge-1-7-10:publishToMavenLocal", ) fun parseVersion(version: String): Triple { val match = semVerRegex.matchEntire(version) ?: throw GradleException( - "Version '$version' is not strict SemVer (MAJOR.MINOR.PATCH integers only)." + "Version '$version' is not strict SemVer (MAJOR.MINOR.PATCH integers only).", ) val major = match.groupValues[1].toInt() val minor = match.groupValues[2].toInt() @@ -315,7 +315,7 @@ fun ensurePublishTaskConfigured(config: BumpConfig) { if (project == null || project.tasks.findByName(taskName) == null) { throw GradleException( "Publishing is not configured for '${config.publishTaskPath}'. " + - "Ensure maven-publish and publishToMavenLocal are configured for ${config.projectPath}." + "Ensure maven-publish and publishToMavenLocal are configured for ${config.projectPath}.", ) } } @@ -330,7 +330,7 @@ fun applyRuntimeVersion(config: BumpConfig, newVersion: String): Pair() @@ -342,7 +342,7 @@ fun applyRuntimeVersion(config: BumpConfig, newVersion: String): Pair $newVersion") @@ -450,6 +450,44 @@ tasks.register("runDemoClient") { dependsOn(":adapters:mc-forge-1-7-10:demo:runClient") } +tasks.register("detektAll") { + group = "verification" + description = "Run detekt across all modules that have detekt applied" + + doLast { + val violatingModules = project.subprojects + .filter { it.plugins.hasPlugin("io.gitlab.arturbosch.detekt") } + .filter { subproject -> + val sarifFile = subproject.file("build/reports/detekt/detekt.sarif") + sarifFile.exists() && sarifFile.readText().contains("\"ruleId\"") + } + .map { it.path } + + if (violatingModules.isNotEmpty()) { + throw GradleException( + "detekt found violations in: ${violatingModules.joinToString(", ")}. " + + "See each module's build/reports/detekt/ for details.", + ) + } + } +} + +subprojects { + plugins.withId("io.gitlab.arturbosch.detekt") { + val detektTask = tasks.named("detekt") + rootProject.tasks.named("detektAll").configure { + dependsOn(detektTask) + } + gradle.taskGraph.whenReady { + if (hasTask(":detektAll")) { + detektTask.configure { + (this as? VerificationTask)?.ignoreFailures = true + } + } + } + } +} + tasks.register("installGitHooks") { group = "setup" description = "Configure git to use hooks from .githooks/. Run once after cloning." diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml new file mode 100644 index 0000000..cebb486 --- /dev/null +++ b/config/detekt/detekt.yml @@ -0,0 +1,793 @@ +build: + maxIssues: 0 + excludeCorrectable: false + weights: + # complexity: 2 + # LongParameterList: 1 + # style: 1 + # comments: 1 + +config: + validation: true + warningsAsErrors: false + checkExhaustiveness: false + # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' + excludes: '' + +processors: + active: true + exclude: + - 'DetektProgressListener' + # - 'KtFileCountProcessor' + # - 'PackageCountProcessor' + # - 'ClassCountProcessor' + # - 'FunctionCountProcessor' + # - 'PropertyCountProcessor' + # - 'ProjectComplexityProcessor' + # - 'ProjectCognitiveComplexityProcessor' + # - 'ProjectLLOCProcessor' + # - 'ProjectCLOCProcessor' + # - 'ProjectLOCProcessor' + # - 'ProjectSLOCProcessor' + # - 'LicenseHeaderLoaderExtension' + +console-reports: + active: true + exclude: + - 'ProjectStatisticsReport' + - 'ComplexityReport' + - 'NotificationReport' + - 'FindingsReport' + - 'FileBasedFindingsReport' + # - 'LiteFindingsReport' + +output-reports: + active: true + exclude: + # - 'TxtOutputReport' + # - 'XmlOutputReport' + # - 'HtmlOutputReport' + # - 'MdOutputReport' + # - 'SarifOutputReport' + +comments: + active: true + AbsentOrWrongFileLicense: + active: false + licenseTemplateFile: 'license.template' + licenseTemplateIsRegex: false + CommentOverPrivateFunction: + active: false + CommentOverPrivateProperty: + active: false + DeprecatedBlockTag: + active: false + EndOfSentenceFormat: + active: false + endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' + KDocReferencesNonPublicProperty: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + OutdatedDocumentation: + active: false + matchTypeParameters: true + matchDeclarationsOrder: true + allowParamOnConstructorProperties: false + UndocumentedPublicClass: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + searchInProtectedClass: false + ignoreDefaultCompanionObject: false + UndocumentedPublicFunction: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + searchProtectedFunction: false + UndocumentedPublicProperty: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + searchProtectedProperty: false + +complexity: + active: true + CognitiveComplexMethod: + active: false + threshold: 15 + ComplexCondition: + active: true + threshold: 4 + ComplexInterface: + active: false + threshold: 10 + includeStaticDeclarations: false + includePrivateDeclarations: false + ignoreOverloaded: false + CyclomaticComplexMethod: + active: true + threshold: 15 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + ignoreNestingFunctions: false + nestingFunctions: + - 'also' + - 'apply' + - 'forEach' + - 'isNotNull' + - 'ifNull' + - 'let' + - 'run' + - 'use' + - 'with' + LabeledExpression: + active: false + ignoredLabels: [ ] + LargeClass: + active: true + threshold: 600 + LongMethod: + active: true + threshold: 80 # DSL builder functions are naturally long + LongParameterList: + active: true + functionThreshold: 10 # DSL props objects have many params + constructorThreshold: 7 + ignoreDefaultParameters: false + ignoreDataClasses: true + ignoreAnnotatedParameter: [ ] + MethodOverloading: + active: false + threshold: 6 + NamedArguments: + active: false + threshold: 3 + ignoreArgumentsMatchingNames: false + NestedBlockDepth: + active: true + threshold: 4 + NestedScopeFunctions: + active: false + threshold: 1 + functions: + - 'kotlin.apply' + - 'kotlin.run' + - 'kotlin.with' + - 'kotlin.let' + - 'kotlin.also' + ReplaceSafeCallChainWithRun: + active: false + StringLiteralDuplication: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + threshold: 3 + ignoreAnnotation: true + excludeStringsWithLessThan5Characters: true + ignoreStringsRegex: '$^' + TooManyFunctions: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + thresholdInFiles: 25 # UI DSLs have lots of small functions + thresholdInClasses: 11 + thresholdInInterfaces: 11 + thresholdInObjects: 11 + thresholdInEnums: 11 + ignoreDeprecated: false + ignorePrivate: false + ignoreOverridden: false + ignoreAnnotatedFunctions: [ ] + +coroutines: + active: true + GlobalCoroutineUsage: + active: false + InjectDispatcher: + active: true + dispatcherNames: + - 'IO' + - 'Default' + - 'Unconfined' + RedundantSuspendModifier: + active: true + SleepInsteadOfDelay: + active: true + SuspendFunSwallowedCancellation: + active: false + SuspendFunWithCoroutineScopeReceiver: + active: false + SuspendFunWithFlowReturnType: + active: true + +empty-blocks: + active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: '_|(ignore|expected).*' + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: true + ignoreOverridden: false + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKtFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyTryBlock: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true + +exceptions: + active: true + ExceptionRaisedInUnexpectedLocation: + active: true + methodNames: + - 'equals' + - 'finalize' + - 'hashCode' + - 'toString' + InstanceOfCheckForException: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + NotImplementedDeclaration: + active: false + ObjectExtendsThrowable: + active: false + PrintStackTrace: + active: true + RethrowCaughtException: + active: true + ReturnFromFinally: + active: true + ignoreLabeled: false + SwallowedException: + active: true + ignoredExceptionTypes: + - 'InterruptedException' + - 'MalformedURLException' + - 'NumberFormatException' + - 'ParseException' + allowedExceptionNameRegex: '_|(ignore|expected).*' + ThrowingExceptionFromFinally: + active: true + ThrowingExceptionInMain: + active: false + ThrowingExceptionsWithoutMessageOrCause: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + exceptions: + - 'ArrayIndexOutOfBoundsException' + - 'Exception' + - 'IllegalArgumentException' + - 'IllegalMonitorStateException' + - 'IllegalStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + ThrowingNewInstanceOfSameException: + active: true + TooGenericExceptionCaught: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + exceptionNames: + - 'ArrayIndexOutOfBoundsException' + - 'Error' + - 'Exception' + - 'IllegalMonitorStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + allowedExceptionNameRegex: '_|(ignore|expected).*' + TooGenericExceptionThrown: + active: true + exceptionNames: + - 'Error' + - 'Exception' + - 'RuntimeException' + - 'Throwable' + +naming: + active: true + BooleanPropertyNaming: + active: false + allowedPattern: '^(is|has|are)' + ClassNaming: + active: true + classPattern: '[A-Z][a-zA-Z0-9]*' + ConstructorParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: false + forbiddenName: [ ] + FunctionMaxLength: + active: false + maximumFunctionNameLength: 30 + FunctionMinLength: + active: false + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + functionPattern: '[a-z][a-zA-Z0-9]*' + excludeClassPattern: '$^' + FunctionParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + InvalidPackageDeclaration: + active: true + rootPackage: '' + requireRootInDeclaration: false + LambdaParameterNaming: + active: false + parameterPattern: '[a-z][A-Za-z0-9]*|_' + MatchingDeclarationName: + active: true + mustBeFirst: true + multiplatformTargets: + - 'ios' + - 'android' + - 'js' + - 'jvm' + - 'native' + - 'iosArm64' + - 'iosX64' + - 'macosX64' + - 'mingwX64' + - 'linuxX64' + MemberNameEqualsClassName: + active: true + ignoreOverridden: true + NoNameShadowing: + active: true + NonBooleanPropertyPrefixedWithIs: + active: false + ObjectPropertyNaming: + active: true + constantPattern: '[A-Za-z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' + PackageNaming: + active: true + packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' + TopLevelPropertyNaming: + active: true + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' + VariableMaxLength: + active: false + maximumVariableNameLength: 64 + VariableMinLength: + active: false + minimumVariableNameLength: 1 + VariableNaming: + active: true + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + +performance: + active: true + ArrayPrimitive: + active: true + CouldBeSequence: + active: false + threshold: 3 + ForEachOnRange: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + SpreadOperator: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + UnnecessaryPartOfBinaryExpression: + active: false + UnnecessaryTemporaryInstantiation: + active: true + +potential-bugs: + active: true + AvoidReferentialEquality: + active: true + forbiddenTypePatterns: + - 'kotlin.String' + CastNullableToNonNullableType: + active: false + CastToNullableType: + active: false + Deprecation: + active: false + DontDowncastCollectionTypes: + active: false + DoubleMutabilityForCollection: + active: true + mutableTypes: + - 'kotlin.collections.MutableList' + - 'kotlin.collections.MutableMap' + - 'kotlin.collections.MutableSet' + - 'java.util.ArrayList' + - 'java.util.LinkedHashSet' + - 'java.util.HashSet' + - 'java.util.LinkedHashMap' + - 'java.util.HashMap' + ElseCaseInsteadOfExhaustiveWhen: + active: false + ignoredSubjectTypes: [ ] + EqualsAlwaysReturnsTrueOrFalse: + active: true + EqualsWithHashCodeExist: + active: true + ExitOutsideMain: + active: false + ExplicitGarbageCollectionCall: + active: true + HasPlatformType: + active: true + IgnoredReturnValue: + active: true + restrictToConfig: true + returnValueAnnotations: + - 'CheckResult' + - '*.CheckResult' + - 'CheckReturnValue' + - '*.CheckReturnValue' + ignoreReturnValueAnnotations: + - 'CanIgnoreReturnValue' + - '*.CanIgnoreReturnValue' + returnValueTypes: + - 'kotlin.sequences.Sequence' + - 'kotlinx.coroutines.flow.*Flow' + - 'java.util.stream.*Stream' + ignoreFunctionCall: [ ] + ImplicitDefaultLocale: + active: true + ImplicitUnitReturnType: + active: false + allowExplicitReturnType: true + InvalidRange: + active: true + IteratorHasNextCallsNextMethod: + active: true + IteratorNotThrowingNoSuchElementException: + active: true + LateinitUsage: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + ignoreOnClassesPattern: '' + MapGetWithNotNullAssertionOperator: + active: true + MissingPackageDeclaration: + active: false + excludes: [ '**/*.kts' ] + NullCheckOnMutableProperty: + active: false + NullableToStringCall: + active: false + PropertyUsedBeforeDeclaration: + active: false + UnconditionalJumpStatementInLoop: + active: false + UnnecessaryNotNullCheck: + active: false + UnnecessaryNotNullOperator: + active: true + UnnecessarySafeCall: + active: true + UnreachableCatchBlock: + active: true + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + UnsafeCast: + active: true + UnusedUnaryOperator: + active: true + UselessPostfixExpression: + active: true + WrongEqualsTypeParameter: + active: true + +style: + active: true + AlsoCouldBeApply: + active: false + BracesOnIfStatements: + active: false + singleLine: 'never' + multiLine: 'always' + BracesOnWhenStatements: + active: false + singleLine: 'necessary' + multiLine: 'consistent' + CanBeNonNullable: + active: false + CascadingCallWrapping: + active: false + includeElvis: true + ClassOrdering: + active: false + CollapsibleIfStatements: + active: false + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: + - 'to' + allowOperators: false + DataClassShouldBeImmutable: + active: false + DestructuringDeclarationWithTooManyEntries: + active: true + maxDestructuringEntries: 3 + DoubleNegativeLambda: + active: false + negativeFunctions: + - reason: 'Use `takeIf` instead.' + value: 'takeUnless' + - reason: 'Use `all` instead.' + value: 'none' + negativeFunctionNameParts: + - 'not' + - 'non' + EqualsNullCall: + active: true + EqualsOnSignatureLine: + active: false + ExplicitCollectionElementAccessMethod: + active: false + ExplicitItLambdaParameter: + active: true + ExpressionBodySyntax: + active: false + includeLineWrapping: false + ForbiddenAnnotation: + active: false + annotations: + - reason: 'it is a java annotation. Use `Suppress` instead.' + value: 'java.lang.SuppressWarnings' + - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' + value: 'java.lang.Deprecated' + - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' + value: 'java.lang.annotation.Documented' + - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' + value: 'java.lang.annotation.Target' + - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' + value: 'java.lang.annotation.Retention' + - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' + value: 'java.lang.annotation.Repeatable' + - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265' + value: 'java.lang.annotation.Inherited' + ForbiddenComment: + active: true + comments: + - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' + value: 'FIXME:' + - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' + value: 'STOPSHIP:' + - reason: 'Forbidden TODO todo marker in comment, please do the changes.' + value: 'TODO:' + allowedPatterns: '' + ForbiddenImport: + active: false + imports: [ ] + forbiddenPatterns: '' + ForbiddenMethodCall: + active: false + methods: + - reason: 'print does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.print' + - reason: 'println does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.println' + ForbiddenSuppress: + active: false + rules: [ ] + ForbiddenVoid: + active: true + ignoreOverridden: false + ignoreUsageInGenerics: false + FunctionOnlyReturningConstant: + active: true + ignoreOverridableFunction: true + ignoreActualFunction: true + excludedFunctions: [ ] + LoopWithTooManyJumpStatements: + active: true + maxJumpCount: 1 + MagicNumber: + active: true + ignoreNumbers: [ '-1', '0', '1', '2', '3', '4', '8', '10', '16', '100', '255' ] + ignorePropertyDeclaration: true + ignoreEnums: true + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts' ] + ignoreHashCodeFunction: true + ignoreLocalVariableDeclaration: false + ignoreAnnotation: false + ignoreNamedArgument: true + ignoreRanges: false + ignoreExtensionFunctions: true + MandatoryBracesLoops: + active: false + MaxChainedCallsOnSameLine: + active: false + maxChainedCalls: 5 + MaxLineLength: + active: true + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: true + excludeRawStrings: true + MayBeConst: + active: true + ModifierOrder: + active: true + MultilineLambdaItParameter: + active: false + MultilineRawStringIndentation: + active: false + indentSize: 4 + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + NestedClassesVisibility: + active: true + NewLineAtEndOfFile: + active: true + NoTabs: + active: false + NullableBooleanCheck: + active: false + ObjectLiteralToLambda: + active: true + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: false + PreferToOverPairSyntax: + active: false + ProtectedMemberInFinalClass: + active: true + RedundantExplicitType: + active: false + RedundantHigherOrderMapUsage: + active: true + RedundantVisibilityModifierRule: + active: false + ReturnCount: + active: true + max: 6 + excludedFunctions: + - 'equals' + excludeLabeled: false + excludeReturnFromLambda: true + excludeGuardClauses: false + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: true + SpacingBetweenPackageAndImports: + active: false + StringShouldBeRawString: + active: false + maxEscapedCharacterCount: 2 + ignoredCharacters: [ ] + ThrowsCount: + active: true + max: 2 + excludeGuardClauses: false + TrailingWhitespace: + active: false + TrimMultilineRawString: + active: false + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + UnderscoresInNumericLiterals: + active: false + acceptableLength: 4 + allowNonStandardGrouping: false + UnnecessaryAbstractClass: + active: true + UnnecessaryAnnotationUseSiteTarget: + active: false + UnnecessaryApply: + active: true + UnnecessaryBackticks: + active: false + UnnecessaryBracesAroundTrailingLambda: + active: false + UnnecessaryFilter: + active: true + UnnecessaryInheritance: + active: true + UnnecessaryInnerClass: + active: false + UnnecessaryLet: + active: false + UnnecessaryParentheses: + active: false + allowForUnclearPrecedence: false + UntilInsteadOfRangeTo: + active: false + UnusedImports: + active: false + UnusedParameter: + active: true + allowedNames: 'ignored|expected' + UnusedPrivateClass: + active: true + UnusedPrivateMember: + active: true + allowedNames: '' + UnusedPrivateProperty: + active: true + allowedNames: '_|ignored|expected|serialVersionUID' + UseAnyOrNoneInsteadOfFind: + active: true + UseArrayLiteralsInAnnotations: + active: true + UseCheckNotNull: + active: true + UseCheckOrError: + active: true + UseDataClass: + active: false + allowVars: false + UseEmptyCounterpart: + active: false + UseIfEmptyOrIfBlank: + active: false + UseIfInsteadOfWhen: + active: false + ignoreWhenContainingVariableDeclaration: false + UseIsNullOrEmpty: + active: true + UseLet: + active: false + UseOrEmpty: + active: true + UseRequire: + active: true + UseRequireNotNull: + active: true + UseSumOfInsteadOfFlatMapSize: + active: false + UselessCallOnNotNull: + active: true + UtilityClassWithPublicConstructor: + active: true + VarCouldBeVal: + active: true + ignoreLateinitVar: false + WildcardImport: + active: false + excludeImports: + - 'java.util.*' diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 26e0766..fcd8aaf 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("dsgl-core.conventions") id("dsgl-linter.conventions") + id("dsgl-static-analysis.conventions") id("dsgl-releaseable-module.conventions") id("org.jetbrains.kotlin.plugin.serialization") jacoco From f01d87dcab2788377c1b824913c3e9b33e84c5c7 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sat, 25 Apr 2026 02:18:57 +0300 Subject: [PATCH 35/78] applying detekt rules with a refactoring; --- .editorconfig | 12 +- .../mc-forge-1-7-10/demo/build.gradle.kts | 10 +- .../dsgl/mcForge1710/DsglClientHotkeys.kt | 2 +- .../mcForge1710/DsglMc1710ModContainer.kt | 2 +- .../dsgl/mcForge1710/demo/ShowcaseWindow.kt | 62 ++++---- .../demo/sections/AnimationsSection.kt | 15 +- .../demo/sections/CssCascadeSection.kt | 42 +++-- .../demo/sections/DisplaySection.kt | 28 ++-- .../demo/sections/FocusRebuildSection.kt | 18 +-- .../demo/sections/InputEventsSection.kt | 39 +++-- .../demo/sections/InputsGallerySection.kt | 13 +- .../demo/sections/InspectorSection.kt | 17 +- .../demo/sections/InteractionsSection.kt | 77 ++++----- .../demo/sections/LayoutDebugSection.kt | 34 ++-- .../demo/sections/LayoutStyleSection.kt | 57 +++---- .../demo/sections/McFeaturesSection.kt | 24 +-- .../demo/sections/PositionedLayoutSection.kt | 42 ++--- ...itionedLayoutStickyDemoIntegrationTests.kt | 13 -- .../dsgl/mcForge1710/DsglScreenHost.kt | 23 ++- .../dsgl/mcForge1710/Mc1710UiAdapter.kt | 91 +++-------- .../dsgl/mcForge1710/text/MsdfTextRenderer.kt | 25 ++- .../StickyControlClipAlignmentTests.kt | 14 +- .../kotlin/dsgl-core.conventions.gradle.kts | 4 + .../dsgl-mc-adapter.conventions.gradle.kts | 22 +-- ...sgl-static-analysis.conventions.gradle.kts | 11 +- config/detekt/detekt.yml | 4 +- .../org/dreamfinity/dsgl/core/DomTree.kt | 9 +- .../org/dreamfinity/dsgl/core/DsglWindow.kt | 4 +- .../dsgl/core/colorpicker/ColorTextCodec.kt | 15 +- .../SystemColorPickerPopupBodyNode.kt | 146 +++++++----------- .../dsgl/core/components/modal/ModalDsl.kt | 17 +- .../core/contextmenu/ContextMenuEngine.kt | 4 + .../core/debug/OverlayDebugControlHost.kt | 1 + .../dnd/{DndBindings.kt => DndListeners.kt} | 0 .../core/dnd/internal/DefaultDndEngine.kt | 4 + .../org/dreamfinity/dsgl/core/dom/DOMNode.kt | 26 ++-- .../dsgl/core/dom/elements/ContainerNode.kt | 6 +- .../dsgl/core/dom/elements/DateInputNode.kt | 2 +- .../dom/elements/support/TextLayoutEngine.kt | 1 + .../dsgl/core/dom/reconcile/DomReconciler.kt | 20 +-- .../dreamfinity/dsgl/core/dsl/StyleScope.kt | 1 + .../org/dreamfinity/dsgl/core/dsl/TextDsl.kt | 2 + .../org/dreamfinity/dsgl/core/dsl/UiScope.kt | 4 +- .../dsgl/core/font/FontRegistry.kt | 10 +- .../dsgl/core/font/MsdfFontMetaParser.kt | 16 +- .../dsgl/core/hooks/ComponentHookRuntime.kt | 19 ++- .../dreamfinity/dsgl/core/hooks/UseContext.kt | 2 + .../dreamfinity/dsgl/core/hooks/UseEffect.kt | 2 + .../dsgl/core/hooks/ref/ElementHandle.kt | 3 +- .../core/inspector/InspectorController.kt | 17 +- .../internal/SystemInspectorOverlayNode.kt | 3 +- .../core/overlay/OverlayLayerContracts.kt | 1 + .../core/overlay/input/LayerDomInputRouter.kt | 2 +- .../overlay/system/SystemOverlayEntries.kt | 1 + .../dsgl/core/select/SelectEngine.kt | 2 +- .../dreamfinity/dsgl/core/style/DssParser.kt | 16 +- .../dsgl/core/style/StyleEngine.kt | 3 - .../dsgl/core/style/StyleSelector.kt | 1 + .../dsgl/core/style/StyleValueParsing.kt | 1 + .../core/text/MinecraftFormattingParser.kt | 1 + .../org/dreamfinity/dsgl/core/GlyphsTests.kt | 1 - .../dsgl/core/UseEffectHookRuntimeTests.kt | 2 +- .../dsgl/core/UseReducerHookRuntimeTests.kt | 7 +- .../dsgl/core/UseStateHookRuntimeTests.kt | 7 +- .../PositionedLayoutStickyBehaviorTests.kt | 36 ++--- .../core/dom/ScrollReactiveSmoothTests.kt | 9 +- .../dom/ScrollbarRenderingInteractionTests.kt | 41 +++-- .../core/dom/elements/InlineLayoutTests.kt | 3 +- .../TextLineSpaceReservationBaselineTests.kt | 3 +- ...PerformanceHotPathCharacterizationTests.kt | 43 +++--- .../dsgl/core/font/MsdfFontTests.kt | 15 +- .../inspector/InspectorControllerTests.kt | 11 -- .../overlay/input/LayerDomInputRouterTests.kt | 1 + .../system/InspectorPointerAlignmentTests.kt | 21 --- .../InspectorTextEditingDomMigrationTests.kt | 3 +- 75 files changed, 558 insertions(+), 708 deletions(-) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/{DndBindings.kt => DndListeners.kt} (100%) diff --git a/.editorconfig b/.editorconfig index dad72e9..14b468b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -23,18 +23,18 @@ indent_size = 2 max_line_length = 120 ktlint_code_style = ktlint_official ktlint_experimental = disabled -ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ -ij_kotlin_packages_to_use_import_on_demand = org.dreamfinity.dsgl.core.animation.*,org.dreamfinity.dsgl.core.colorpicker.*,org.dreamfinity.dsgl.core.dnd.*,org.dreamfinity.dsgl.core.dom.elements.*,org.dreamfinity.dsgl.core.dom.elements.support.*,org.dreamfinity.dsgl.core.dom.layout.*,org.dreamfinity.dsgl.core.dsl.*,org.dreamfinity.dsgl.core.event.*,org.dreamfinity.dsgl.core.font.*,org.dreamfinity.dsgl.core.inspector.*,org.dreamfinity.dsgl.core.style.*,org.dreamfinity.dsgl.core.text.*,org.lwjgl.opengl.* -ij_kotlin_allow_trailing_comma = true -ij_kotlin_allow_trailing_comma_on_call_site = true -ij_kotlin_indent_before_arrow_on_new_line = false ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 4 ktlint_function_signature_body_expression_wrapping = multiline ktlint_class_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 1 ktlint_chain_method_rule_force_multiline_when_chain_operator_count_greater_or_equal_than = 3 ktlint_function_naming_ignore_when_annotated_with = Composable,DsglDsl ktlint_ignore_back_ticked_identifier = true -ktlint_standard_multiline-expression-wrapping = enabled + +ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ +ij_kotlin_packages_to_use_import_on_demand = org.dreamfinity.dsgl.core.animation.*,org.dreamfinity.dsgl.core.colorpicker.*,org.dreamfinity.dsgl.core.dnd.*,org.dreamfinity.dsgl.core.dom.elements.*,org.dreamfinity.dsgl.core.dom.elements.support.*,org.dreamfinity.dsgl.core.dom.layout.*,org.dreamfinity.dsgl.core.dsl.*,org.dreamfinity.dsgl.core.event.*,org.dreamfinity.dsgl.core.font.*,org.dreamfinity.dsgl.core.inspector.*,org.dreamfinity.dsgl.core.style.*,org.dreamfinity.dsgl.core.text.*,org.lwjgl.opengl.* +ij_kotlin_allow_trailing_comma = true +# ij_kotlin_allow_trailing_comma_on_call_site = true # disabled due to breaking DSL callsite by IDE code linter +ij_kotlin_indent_before_arrow_on_new_line = false ij_formatter_tags_enabled = true ij_formatter_off_tag = @formatter:off ij_formatter_on_tag = @formatter:on diff --git a/adapters/mc-forge-1-7-10/demo/build.gradle.kts b/adapters/mc-forge-1-7-10/demo/build.gradle.kts index bdcb4de..6989213 100644 --- a/adapters/mc-forge-1-7-10/demo/build.gradle.kts +++ b/adapters/mc-forge-1-7-10/demo/build.gradle.kts @@ -95,7 +95,7 @@ val generateModMetadata by tasks.registering { const val MOD_CREDITS: String = "${tokens["modCredits"]}" const val MOD_ICON: String = "${tokens["modIcon"]}" } - """.trimIndent(), + """.trimIndent() + System.lineSeparator(), ) } } @@ -156,6 +156,10 @@ tasks.named("dokkaGeneratePublicationHtml") { dependsOn(generateModMetadata) } +tasks.matching { it.name.startsWith("runKtlintCheckOver") || it.name.startsWith("runKtlintFormatOver") }.configureEach { + dependsOn(generateModMetadata) +} + tasks.named("processResources") { inputs.properties(baseModMetadataTokens) inputs.property("modVersion", providers.provider { currentModVersion() }) @@ -188,6 +192,10 @@ listOf( } } +tasks.named("test") { + mustRunAfter(":adapters:mc-forge-1-7-10:reobf") +} + tasks.withType().configureEach { enabled = false } diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglClientHotkeys.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglClientHotkeys.kt index f6f6354..d75608f 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglClientHotkeys.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglClientHotkeys.kt @@ -35,7 +35,7 @@ object DsglClientHotkeys { } @SubscribeEvent - fun onKeyInput(event: InputEvent.KeyInputEvent) { + fun onKeyInput(_event: InputEvent.KeyInputEvent) { when { openShowcaseKey.isPressed -> Minecraft diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglMc1710ModContainer.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglMc1710ModContainer.kt index d49a153..e972ab9 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglMc1710ModContainer.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglMc1710ModContainer.kt @@ -17,7 +17,7 @@ import net.minecraft.client.Minecraft ) class DsglMc1710ModContainer { @Mod.EventHandler - fun onInit(event: FMLInitializationEvent) { + fun onInit(_event: FMLInitializationEvent) { if (FMLCommonHandler .instance() .side.isClient diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/ShowcaseWindow.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/ShowcaseWindow.kt index aaa996b..da83566 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/ShowcaseWindow.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/ShowcaseWindow.kt @@ -18,7 +18,7 @@ import org.dreamfinity.dsgl.core.style.FlexDirection import org.dreamfinity.dsgl.core.style.JustifyContent import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.mcForge1710.McItemStackRef -import org.dreamfinity.dsgl.mcForge1710.demo.sections.McFeaturesShellProps +import org.dreamfinity.dsgl.mcForge1710.demo.sections.McFeaturesSection import org.dreamfinity.dsgl.mcForge1710.demo.sections.animationsSection import org.dreamfinity.dsgl.mcForge1710.demo.sections.colorPickerSection import org.dreamfinity.dsgl.mcForge1710.demo.sections.contextMenuSection @@ -328,7 +328,7 @@ class ShowcaseWindow : DsglWindow() { DemoSection.MC_FEATURES -> mcFeaturesSection( props = - McFeaturesShellProps( + McFeaturesSection( viewportWidthPx = viewportWidthPx, viewportHeightPx = viewportHeightPx, mediaReady = mediaReady, @@ -409,7 +409,7 @@ class ShowcaseWindow : DsglWindow() { checklistPage = (checklistPage + delta).coerceIn(0, pageCount - 1) } - internal fun requestManualInvalidate(reason: String) { + internal fun requestManualInvalidate(_reason: String) { invalidate() } @@ -473,7 +473,7 @@ class ShowcaseWindow : DsglWindow() { val content = file.readText() appendInfo("Stylesheet loaded by $source") return content - } catch (ex: Exception) { + } catch (ex: java.io.IOException) { appendLog("Stylesheet load failed: ${ex.javaClass.simpleName}", 0xFFFF9A66.toInt()) throw ex } @@ -485,7 +485,7 @@ class ShowcaseWindow : DsglWindow() { file.parentFile?.mkdirs() file.writeText(content) appendInfo("Stylesheet saved by $source") - } catch (ex: Exception) { + } catch (ex: java.io.IOException) { appendLog("Stylesheet save failed: ${ex.javaClass.simpleName}", 0xFFFF9A66.toInt()) throw ex } @@ -520,7 +520,7 @@ class ShowcaseWindow : DsglWindow() { writeDemoDocumentIcon(File(dataDir, "dsgl/demo/document.png")) mediaReady = true appendInfo("Prepared local file:// and cached http image assets") - } catch (ex: Exception) { + } catch (ex: java.io.IOException) { mediaReady = false appendLog("Media prep failed: ${ex.javaClass.simpleName}", 0xFFFF9A66.toInt()) } @@ -540,7 +540,7 @@ class ShowcaseWindow : DsglWindow() { --danger: #A34343; --fg: #E9F1FF; } - + button { border-width: 1px; border-color: #000000; @@ -573,7 +573,7 @@ class ShowcaseWindow : DsglWindow() { border-color: #555555; color: #8E8E8E; } - + .style-card { margin: 2px 0px 0px 0px; background-color: #2A3440; @@ -581,39 +581,39 @@ class ShowcaseWindow : DsglWindow() { border-width: 1px; padding: 4px; } - + .accent { background-color: #3F5A70; } - + button.primary { background-color: var(--primary); color: var(--fg); } - + #dangerAction { background-color: var(--danger); color: #FFFFFFFF; } - + #hoverActiveTarget:hover { background-color: #365F7D; } - + #hoverActiveTarget:active { background-color: #274356; } - + #focusInput:focus { border-color: var(--accent); border-width: 2px; } - + #disabledTarget:disabled { background-color: #444444; color: #999999; } - + .vars-demo { background-color: #213348; border-color: var(--accent); @@ -769,7 +769,7 @@ class ShowcaseWindow : DsglWindow() { if (created) { StyleEngine.forceReloadStylesheets() } - } catch (ex: Exception) { + } catch (ex: java.io.IOException) { appendLog("Stylesheet prep failed: ${ex.javaClass.simpleName}", 0xFFFF9A66.toInt()) } } @@ -809,61 +809,61 @@ class ShowcaseWindow : DsglWindow() { border-width: 1px; padding: 4px; } - + .cascade-demo-root.dark { color: #FFE4C7; } - + .cascade-demo-root.light { color: #D7E8FF; } - + .cascade-demo-root .panel { background-color: #1E2935; border-width: 1px; border-color: #516071; padding: 3px; } - + .cascade-demo-root .panel .item { color: #7EC8FF; } - + .cascade-demo-root .panel > .item { color: #9BE66F; } - + .cascade-demo-root .btn { background-color: #4A5568; color: #FFFFFFFF; border-color: #1F2937; border-width: 1px; } - + .cascade-demo-root #primary.btn { background-color: #2B6CB0; } - + .cascade-demo-root .order-target { color: #F56565; } - + .cascade-demo-root .order-target { color: #48BB78; } - + .cascade-demo-root .important-target { color: #DD6B20 !important; } - + .cascade-demo-root .important-target { color: #3182CE; } - + .cascade-demo-root.rule-a .toggle-target { color: #D69E2E; } - + .cascade-demo-root.rule-b .toggle-target { color: #63B3ED; } @@ -923,7 +923,7 @@ class ShowcaseWindow : DsglWindow() { """.trimIndent(), ) StyleEngine.forceReloadStylesheets() - } catch (ex: Exception) { + } catch (ex: java.io.IOException) { appendLog("Cascade stylesheet prep failed: ${ex.javaClass.simpleName}", 0xFFFF9A66.toInt()) } } diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/AnimationsSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/AnimationsSection.kt index 34ee3e4..8b66019 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/AnimationsSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/AnimationsSection.kt @@ -94,15 +94,12 @@ fun UiScope.animationsSection(onInfo: (String) -> Unit) { flexDirection = FlexDirection.Row } }) { - button( - if (animationsToggle) "Retarget: ON" else "Retarget: OFF", - { - onMouseClick = { - animationsToggle = !animationsToggle - onInfo("Animation retarget toggle=$animationsToggle") - } - }, - ) + button(if (animationsToggle) "Retarget: ON" else "Retarget: OFF", { + onMouseClick = { + animationsToggle = !animationsToggle + onInfo("Animation retarget toggle=$animationsToggle") + } + }) button(if (animationsPaused) "Play" else "Pause", { onMouseClick = { animationsPaused = !animationsPaused diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/CssCascadeSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/CssCascadeSection.kt index bfba9fc..4bc54a4 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/CssCascadeSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/CssCascadeSection.kt @@ -65,16 +65,13 @@ fun UiScope.cssCascadeCombinatorsSection(onLogHook: (String, Event, String?) -> flexDirection = FlexDirection.Row } }) { - button( - if (cascadeParentDark) "Parent class: dark" else "Parent class: light", - { - key = "section.cssCascade.toggleParentClass" - onMouseClick = { event -> - cascadeParentDark = !cascadeParentDark - onLogHook("css.cascade.toggle.parentClass", event, "dark=$cascadeParentDark") - } - }, - ) + button(if (cascadeParentDark) "Parent class: dark" else "Parent class: light", { + key = "section.cssCascade.toggleParentClass" + onMouseClick = { event -> + cascadeParentDark = !cascadeParentDark + onLogHook("css.cascade.toggle.parentClass", event, "dark=$cascadeParentDark") + } + }) button(if (cascadeRuleAEnabled) "Rule block: A" else "Rule block: B", { key = "section.cssCascade.toggleRuleBlock" onMouseClick = { event -> @@ -175,20 +172,17 @@ fun UiScope.cssCascadeCombinatorsSection(onLogHook: (String, Event, String?) -> flexDirection = FlexDirection.Column } }) { - button( - if (cascadeAdjacentSourceEnabled) "Source class: ON" else "Source class: OFF", - { - key = "section.cssCascade.adj.toggleSource" - onMouseClick = { event -> - cascadeAdjacentSourceEnabled = !cascadeAdjacentSourceEnabled - onLogHook( - "css.cascade.adj.toggleSource", - event, - "enabled=$cascadeAdjacentSourceEnabled", - ) - } - }, - ) + button(if (cascadeAdjacentSourceEnabled) "Source class: ON" else "Source class: OFF", { + key = "section.cssCascade.adj.toggleSource" + onMouseClick = { event -> + cascadeAdjacentSourceEnabled = !cascadeAdjacentSourceEnabled + onLogHook( + "css.cascade.adj.toggleSource", + event, + "enabled=$cascadeAdjacentSourceEnabled", + ) + } + }) button(if (cascadeAdjacentSwapOrder) "Order: swapped" else "Order: default", { key = "section.cssCascade.adj.swap" onMouseClick = { event -> diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/DisplaySection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/DisplaySection.kt index 92ade1f..ed22ccf 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/DisplaySection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/DisplaySection.kt @@ -245,15 +245,12 @@ fun UiScope.displaySection(onInfo: (String) -> Unit, onLogHook: (String, Event, flexDirection = FlexDirection.Row } }) { - button( - if (displayShowHidden) "Target visible" else "Target hidden", - { - onMouseClick = { - displayShowHidden = !displayShowHidden - onInfo("Display.none visible=$displayShowHidden") - } - }, - ) + button(if (displayShowHidden) "Target visible" else "Target hidden", { + onMouseClick = { + displayShowHidden = !displayShowHidden + onInfo("Display.none visible=$displayShowHidden") + } + }) text( "targetClicks=$displayNoneClicks (should not change while hidden)", { style = { color = DEMO_MUTED } }, @@ -313,12 +310,9 @@ fun UiScope.displaySection(onInfo: (String) -> Unit, onLogHook: (String, Event, displayFlexAlignIndex = (alignIndex + 1) % ALIGN_OPTIONS.size } }) - button( - if (displayGridLargeGap) "gap: large" else "gap: compact", - { - onMouseClick = { displayGridLargeGap = !displayGridLargeGap } - }, - ) + button(if (displayGridLargeGap) "gap: large" else "gap: compact", { + onMouseClick = { displayGridLargeGap = !displayGridLargeGap } + }) } text("Row uses fixed-size items so justify spacing is easier to compare.", { style = { color = DEMO_MUTED } @@ -489,8 +483,8 @@ private fun UiScope.dot( private fun UiScope.flexRowCell( keyPart: String, label: String, - widthPx: Int, - paddingPx: Int, + @Suppress("UnusedParameter") widthPx: Int, + @Suppress("UnusedParameter") paddingPx: Int, color: Int, ) { div({ diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/FocusRebuildSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/FocusRebuildSection.kt index 5008c71..1ed63ba 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/FocusRebuildSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/FocusRebuildSection.kt @@ -32,16 +32,14 @@ fun UiScope.focusRebuildSection( onManualInvalidate(reason) } - div( - { - key = "section.focusRebuild" - style = { - gap = 4.px - display = Display.Flex - flexDirection = FlexDirection.Column - } - }, - ) { + div({ + key = "section.focusRebuild" + style = { + gap = 4.px + display = Display.Flex + flexDirection = FlexDirection.Column + } + }) { text("Stable key focus test: focus first field, press Enter to rebuild, keep typing.") text("Unstable key field changes key version and demonstrates focus/key instability.", { style = { color = DEMO_MUTED } diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputEventsSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputEventsSection.kt index bcfef74..e7a87a9 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputEventsSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputEventsSection.kt @@ -106,27 +106,24 @@ fun UiScope.inputEventsSection(onLogHook: (String, Event, String?) -> Unit) { } }) { text("Text input") - input( - InputType.Text(value = inputEventTextValue, placeholder = "Type then blur"), - { - key = "inputEvents.text" - style = { width = 100.percent } - onFocusGain = { event: FocusGainEvent -> - appendInputEvent("text", "focus", inputEventTextValue, event) - } - onFocusLose = { event: FocusLoseEvent -> - appendInputEvent("text", "blur", inputEventTextValue, event) - } - onInput = { event: InputEvent -> - inputEventTextValue = event.value - appendInputEvent("text", "input", event.value, event) - } - onValueChange = { event: ValueChangedEvent -> - inputEventTextValue = event.value - appendInputEvent("text", "change", event.value, event) - } - }, - ) + input(InputType.Text(value = inputEventTextValue, placeholder = "Type then blur"), { + key = "inputEvents.text" + style = { width = 100.percent } + onFocusGain = { event: FocusGainEvent -> + appendInputEvent("text", "focus", inputEventTextValue, event) + } + onFocusLose = { event: FocusLoseEvent -> + appendInputEvent("text", "blur", inputEventTextValue, event) + } + onInput = { event: InputEvent -> + inputEventTextValue = event.value + appendInputEvent("text", "input", event.value, event) + } + onValueChange = { event: ValueChangedEvent -> + inputEventTextValue = event.value + appendInputEvent("text", "change", event.value, event) + } + }) text("Textarea") textarea({ diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt index d242636..6b59c18 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt @@ -377,14 +377,11 @@ fun UiScope.inputsGallerySection(clippingScrollDemoText: String, onClippingScrol option("alt", "Alternative") } - button( - if (selectDynamicAlt) "Use option set A" else "Use option set B", - { - onMouseClick = { - selectDynamicAlt = !selectDynamicAlt - } - }, - ) + button(if (selectDynamicAlt) "Use option set A" else "Use option set B", { + onMouseClick = { + selectDynamicAlt = !selectDynamicAlt + } + }) text("Dynamic options") select({ key = "input.select.dynamic" diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InspectorSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InspectorSection.kt index a783ee4..c8648c5 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InspectorSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InspectorSection.kt @@ -74,16 +74,13 @@ fun UiScope.inspectorSection(onInfo: (String) -> Unit) { onInfo("Inspector sample: behind counter=$inspectorBehindClickCounter") } }) - input( - InputType.Text(inspectorInputValue, "Focusable input"), - { - style = { flexGrow = 1f } - key = "inspector.sample.input" - onInput = { event -> - inspectorInputValue = event.value - } - }, - ) + input(InputType.Text(inspectorInputValue, "Focusable input"), { + style = { flexGrow = 1f } + key = "inspector.sample.input" + onInput = { event -> + inspectorInputValue = event.value + } + }) } div({ key = "inspector.sample.grid" diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InteractionsSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InteractionsSection.kt index 0f197be..91c8f2b 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InteractionsSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InteractionsSection.kt @@ -144,41 +144,35 @@ fun UiScope.interactionsSection(onInfo: (String) -> Unit, onLogHook: (String, Ev flexDirection = FlexDirection.Column } }) { - input( - InputType.Text(placeholder = "onKeyDown/onKeyUp"), - { - key = "interactions.key.downUp" - style = { width = 100.percent } - onKeyDown = { event -> - keyDownCount += 1 - if (event.keyCode == KeyCodes.ENTER) { - enterActionCount += 1 - onLogHook("onKeyDown", event, "enterAction") - } else { - onLogHook("onKeyDown", event, null) - } + input(InputType.Text(placeholder = "onKeyDown/onKeyUp"), { + key = "interactions.key.downUp" + style = { width = 100.percent } + onKeyDown = { event -> + keyDownCount += 1 + if (event.keyCode == KeyCodes.ENTER) { + enterActionCount += 1 + onLogHook("onKeyDown", event, "enterAction") + } else { + onLogHook("onKeyDown", event, null) } - onKeyUp = { event -> - keyUpCount += 1 - onLogHook("onKeyUp", event, null) - } - }, - ) - input( - InputType.Text(placeholder = "onKeyPressed/onKeyReleased"), - { - key = "interactions.key.aliases" - style = { width = 100.percent } - onKeyPressed = { event -> - keyPressedCount += 1 - onLogHook("onKeyPressed", event, null) - } - onKeyReleased = { event -> - keyReleasedCount += 1 - onLogHook("onKeyReleased", event, null) - } - }, - ) + } + onKeyUp = { event -> + keyUpCount += 1 + onLogHook("onKeyUp", event, null) + } + }) + input(InputType.Text(placeholder = "onKeyPressed/onKeyReleased"), { + key = "interactions.key.aliases" + style = { width = 100.percent } + onKeyPressed = { event -> + keyPressedCount += 1 + onLogHook("onKeyPressed", event, null) + } + onKeyReleased = { event -> + keyReleasedCount += 1 + onLogHook("onKeyReleased", event, null) + } + }) } text( @@ -193,15 +187,12 @@ fun UiScope.interactionsSection(onInfo: (String) -> Unit, onLogHook: (String, Ev flexDirection = FlexDirection.Row } }) { - button( - if (cancellationEnabled) "Cancel child click: ON" else "Cancel child click: OFF", - { - onMouseClick = { - cancellationEnabled = !cancellationEnabled - onInfo("Interactions: cancellation=$cancellationEnabled") - } - }, - ) + button(if (cancellationEnabled) "Cancel child click: ON" else "Cancel child click: OFF", { + onMouseClick = { + cancellationEnabled = !cancellationEnabled + onInfo("Interactions: cancellation=$cancellationEnabled") + } + }) text( "Parent=$cancellationParentHits Child=$cancellationChildHits", { style = { color = DEMO_MUTED } }, diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutDebugSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutDebugSection.kt index 61c1078..9ec23eb 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutDebugSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutDebugSection.kt @@ -43,26 +43,20 @@ fun UiScope.layoutDebugSection(onClearLogs: () -> Unit, onInfo: (String) -> Unit flexDirection = FlexDirection.Row } }) { - button( - if (layoutDebugStrict) "strict: on" else "strict: off", - { - onMouseClick = { - layoutDebugStrict = !layoutDebugStrict - LayoutDebug.strictBounds = layoutDebugStrict - onInfo("LayoutDebug.strict=$layoutDebugStrict") - } - }, - ) - button( - if (layoutDebugDraw) "draw bounds: on" else "draw bounds: off", - { - onMouseClick = { - layoutDebugDraw = !layoutDebugDraw - LayoutDebug.drawBounds = layoutDebugDraw - onInfo("LayoutDebug.drawBounds=$layoutDebugDraw") - } - }, - ) + button(if (layoutDebugStrict) "strict: on" else "strict: off", { + onMouseClick = { + layoutDebugStrict = !layoutDebugStrict + LayoutDebug.strictBounds = layoutDebugStrict + onInfo("LayoutDebug.strict=$layoutDebugStrict") + } + }) + button(if (layoutDebugDraw) "draw bounds: on" else "draw bounds: off", { + onMouseClick = { + layoutDebugDraw = !layoutDebugDraw + LayoutDebug.drawBounds = layoutDebugDraw + onInfo("LayoutDebug.drawBounds=$layoutDebugDraw") + } + }) button("clear logs", { onMouseClick = { onClearLogs() } }) diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutStyleSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutStyleSection.kt index 5f9bff6..ece510b 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutStyleSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutStyleSection.kt @@ -70,15 +70,12 @@ fun UiScope.layoutStyleSection(onInfo: (String) -> Unit, onLogHook: (String, Eve } }, ) - button( - if (styleFixedSize) "Size: Fixed" else "Size: Auto", - { - onMouseClick = { - styleFixedSize = !styleFixedSize - onInfo("Layout: fixedSize=$styleFixedSize") - } - }, - ) + button(if (styleFixedSize) "Size: Fixed" else "Size: Auto", { + onMouseClick = { + styleFixedSize = !styleFixedSize + onInfo("Layout: fixedSize=$styleFixedSize") + } + }) } div({ @@ -147,24 +144,15 @@ fun UiScope.layoutStyleSection(onInfo: (String) -> Unit, onLogHook: (String, Eve flexDirection = FlexDirection.Row } }) { - button( - if (styleUseMargin) "Margin ON" else "Margin OFF", - { - onMouseClick = { styleUseMargin = !styleUseMargin } - }, - ) - button( - if (styleUsePadding) "Padding ON" else "Padding OFF", - { - onMouseClick = { styleUsePadding = !styleUsePadding } - }, - ) - button( - if (styleUseBorder) "Border ON" else "Border OFF", - { - onMouseClick = { styleUseBorder = !styleUseBorder } - }, - ) + button(if (styleUseMargin) "Margin ON" else "Margin OFF", { + onMouseClick = { styleUseMargin = !styleUseMargin } + }) + button(if (styleUsePadding) "Padding ON" else "Padding OFF", { + onMouseClick = { styleUsePadding = !styleUsePadding } + }) + button(if (styleUseBorder) "Border ON" else "Border OFF", { + onMouseClick = { styleUseBorder = !styleUseBorder } + }) } div({ @@ -206,15 +194,12 @@ fun UiScope.layoutStyleSection(onInfo: (String) -> Unit, onLogHook: (String, Eve flexDirection = FlexDirection.Row } }) { - button( - if (stackOverlayEnabled) "Stack Overlay ON" else "Stack Overlay OFF", - { - onMouseClick = { - stackOverlayEnabled = !stackOverlayEnabled - onInfo("Layout: stackOverlay=$stackOverlayEnabled") - } - }, - ) + button(if (stackOverlayEnabled) "Stack Overlay ON" else "Stack Overlay OFF", { + onMouseClick = { + stackOverlayEnabled = !stackOverlayEnabled + onInfo("Layout: stackOverlay=$stackOverlayEnabled") + } + }) button("Reset Overlay", { onMouseClick = { layoutOverlayX = 8 diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/McFeaturesSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/McFeaturesSection.kt index f1d7b1b..d051077 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/McFeaturesSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/McFeaturesSection.kt @@ -1,19 +1,16 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections import org.dreamfinity.dsgl.core.DsglColors -import org.dreamfinity.dsgl.core.dom.elements.InputType +import org.dreamfinity.dsgl.core.dom.elements.* import org.dreamfinity.dsgl.core.dsl.* -import org.dreamfinity.dsgl.core.event.Event +import org.dreamfinity.dsgl.core.event.* import org.dreamfinity.dsgl.core.hooks.useState -import org.dreamfinity.dsgl.core.style.AlignItems -import org.dreamfinity.dsgl.core.style.Display -import org.dreamfinity.dsgl.core.style.FlexDirection -import org.dreamfinity.dsgl.core.style.JustifyContent +import org.dreamfinity.dsgl.core.style.* import org.dreamfinity.dsgl.mcForge1710.McItemStackRef import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED import kotlin.math.roundToLong -data class McFeaturesShellProps( +data class McFeaturesSection( val viewportWidthPx: Int, val viewportHeightPx: Int, val mediaReady: Boolean, @@ -31,7 +28,7 @@ data class McFeaturesShellProps( val onLogHook: (String, Event, String?) -> Unit, ) -fun UiScope.mcFeaturesSection(props: McFeaturesShellProps) { +fun UiScope.mcFeaturesSection(props: McFeaturesSection) { var itemRotY by useState(160.0) var itemRotX by useState(-11.0) @@ -57,9 +54,11 @@ fun UiScope.mcFeaturesSection(props: McFeaturesShellProps) { } }) { text( - "DSGL viewport=${props.viewportWidthPx}x${props.viewportHeightPx}px, guiScale=${props.guiScaleLabel( - guiScaleValue, - )}", + "DSGL viewport=${props.viewportWidthPx}x${props.viewportHeightPx}px, guiScale=${ + props.guiScaleLabel( + guiScaleValue, + ) + }", { style = { color = DsglColors.WHITE } }, ) text( @@ -147,7 +146,8 @@ fun UiScope.mcFeaturesSection(props: McFeaturesShellProps) { width = 1.px color = 0xFF3F4B56.toInt() } - backgroundColor = if ((row + col) % 2 == 0) 0xFF1F2D38.toInt() else 0xFF243544.toInt() + backgroundColor = + if ((row + col) % 2 == 0) 0xFF1F2D38.toInt() else 0xFF243544.toInt() } }) {} } diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutSection.kt index 3c99eac..c1ac119 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutSection.kt @@ -640,12 +640,9 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { gap = 3.px } }) { - button( - if (positionedDemoTieSwap) "tie order: second->first" else "tie order: first->second", - { - onMouseClick = { positionedDemoTieSwap = !positionedDemoTieSwap } - }, - ) + button(if (positionedDemoTieSwap) "tie order: second->first" else "tie order: first->second", { + onMouseClick = { positionedDemoTieSwap = !positionedDemoTieSwap } + }) text( "same z, later DOM child should win overlap hit", { style = { color = DEMO_MUTED } }, @@ -775,15 +772,12 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { text("positioned z=-100") } } - text( - "mixed clicks static=$positionedDemoMixedStaticClicks, positioned=$positionedDemoMixedPositionedClicks", - { - style = { - color = DEMO_MUTED - minHeight = 1.em - } - }, - ) + text("mixed clicks static=$positionedDemoMixedStaticClicks, positioned=$positionedDemoMixedPositionedClicks", { + style = { + color = DEMO_MUTED + minHeight = 1.em + } + }) repeat(40) { div({ style = { @@ -1313,18 +1307,12 @@ private fun UiScope.controls(props: ControlsProps) { button("mode=${props.demoMode.name.lowercase()}", { onMouseClick = { props.onModeCycle() } }) - button( - if (props.useLeft) "h: left first" else "h: right fallback", - { - onMouseClick = { props.onToggleUseLeft() } - }, - ) - button( - if (props.useTop) "v: top first" else "v: bottom fallback", - { - onMouseClick = { props.onToggleUseTop() } - }, - ) + button(if (props.useLeft) "h: left first" else "h: right fallback", { + onMouseClick = { props.onToggleUseLeft() } + }) + button(if (props.useTop) "v: top first" else "v: bottom fallback", { + onMouseClick = { props.onToggleUseTop() } + }) button("Reset", { onMouseClick = { props.onReset() } }) diff --git a/adapters/mc-forge-1-7-10/demo/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutStickyDemoIntegrationTests.kt b/adapters/mc-forge-1-7-10/demo/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutStickyDemoIntegrationTests.kt index a8d1c39..663b306 100644 --- a/adapters/mc-forge-1-7-10/demo/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutStickyDemoIntegrationTests.kt +++ b/adapters/mc-forge-1-7-10/demo/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutStickyDemoIntegrationTests.kt @@ -274,19 +274,6 @@ class PositionedLayoutStickyDemoIntegrationTests { error("Field '$fieldName' not found on ${clazz.name}") } - private fun findMethod(clazz: Class<*>, methodName: String, parameterTypes: Array>): Method { - var current: Class<*>? = clazz - while (current != null) { - val method = - current.declaredMethods.firstOrNull { - it.name == methodName && it.parameterTypes.contentEquals(parameterTypes) - } - if (method != null) return method - current = current.superclass - } - error("Method '$methodName' not found on ${clazz.name}") - } - private fun findMethodByNameAndArity(clazz: Class<*>, methodName: String, arity: Int): Method { var current: Class<*>? = clazz while (current != null) { diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index a3a0ef9..d0a1738 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -368,7 +368,9 @@ abstract class DsglScreenHost( val commands = try { tree.paint(adapter, applyStyles = !stylesAlreadyApplied) - } catch (error: Throwable) { + } catch ( + @Suppress("TooGenericExceptionCaught") error: Throwable, + ) { logPipelineError( key = "draw.paint", message = "[DSGL] Paint pipeline failed; rendering previous committed frame: ${error.message}", @@ -401,7 +403,9 @@ abstract class DsglScreenHost( return try { applicationOverlayHost.render(adapter, lastWidth, lastHeight) applicationOverlayHost.paint(adapter) - } catch (error: Throwable) { + } catch ( + @Suppress("TooGenericExceptionCaught") error: Throwable, + ) { logPipelineError( key = "draw.applicationOverlay", message = "[DSGL] Application overlay paint failed; skipping app overlay frame: ${error.message}", @@ -429,7 +433,9 @@ abstract class DsglScreenHost( return try { systemOverlayHost.render(adapter, lastWidth, lastHeight) systemOverlayHost.paint(adapter) - } catch (error: Throwable) { + } catch ( + @Suppress("TooGenericExceptionCaught") error: Throwable, + ) { logPipelineError( key = "draw.systemOverlay", message = "[DSGL] System overlay paint failed; skipping system overlay frame: ${error.message}", @@ -632,6 +638,7 @@ abstract class DsglScreenHost( needsRender = true } + @Suppress("EmptyFunctionBlock") override fun requestRedraw() { } @@ -690,7 +697,9 @@ abstract class DsglScreenHost( window.commitRenderBuild() tracePhase("rebuild.end") true - } catch (error: Throwable) { + } catch ( + @Suppress("TooGenericExceptionCaught") error: Throwable, + ) { window.discardRenderBuild() logPipelineError( key = "rebuild", @@ -727,7 +736,7 @@ abstract class DsglScreenHost( } } - throw IllegalStateException( + error( "Hot-reload hook remount recovery exceeded $maxAttempts attempts: ${lastRemountRequest?.message}", ) } @@ -1624,7 +1633,9 @@ abstract class DsglScreenHost( layoutRevision++ inspector.onLayoutCommitted(tree.root, layoutRevision) true - } catch (error: Throwable) { + } catch ( + @Suppress("TooGenericExceptionCaught") error: Throwable, + ) { logPipelineError( key = "layout.$phase", message = "[DSGL] Layout commit failed in $phase; keeping previous frame: ${error.message}", diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/Mc1710UiAdapter.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/Mc1710UiAdapter.kt index 5b8653c..1c16434 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/Mc1710UiAdapter.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/Mc1710UiAdapter.kt @@ -7,9 +7,9 @@ import net.minecraft.client.renderer.texture.DynamicTexture import net.minecraft.item.ItemBlock import net.minecraft.item.ItemStack import net.minecraft.util.ResourceLocation -import org.dreamfinity.dsgl.core.dom.layout.FontLineMetrics -import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext -import org.dreamfinity.dsgl.core.font.FontRegistry +import org.dreamfinity.dsgl.core.colorpicker.* +import org.dreamfinity.dsgl.core.dom.layout.* +import org.dreamfinity.dsgl.core.font.* import org.dreamfinity.dsgl.core.host.Viewport import org.dreamfinity.dsgl.core.host.dsglRectToGlScissor import org.dreamfinity.dsgl.core.render.RenderCommand @@ -390,7 +390,9 @@ class Mc1710UiAdapter( GL11.glVertex2f(-1f, 1f) GL11.glEnd() renderingSucceeded = true - } catch (error: Throwable) { + } catch ( + @Suppress("TooGenericExceptionCaught") error: Throwable, + ) { if (readbackDiagnosticsVerbose) { logRateLimited( key = "magnifier:capture:error", @@ -717,7 +719,7 @@ class Mc1710UiAdapter( ) if (linkStatus == GL11.GL_FALSE) { val info = ARBShaderObjects.glGetInfoLogARB(program, 4096) - throw IllegalStateException("Magnifier shader link failed: $info") + error("Magnifier shader link failed: $info") } val shader = MagnifierCaptureShader( @@ -731,7 +733,9 @@ class Mc1710UiAdapter( ) magnifierCaptureShader = shader shader - } catch (error: Throwable) { + } catch ( + @Suppress("TooGenericExceptionCaught") error: Throwable, + ) { magnifierCaptureShaderInitFailed = true if (readbackDiagnosticsVerbose) { logRateLimited( @@ -756,7 +760,7 @@ class Mc1710UiAdapter( ) if (compileStatus == GL11.GL_FALSE) { val info = ARBShaderObjects.glGetInfoLogARB(shader, 4096) - throw IllegalStateException("Magnifier shader compile failed: $info") + error("Magnifier shader compile failed: $info") } return shader } @@ -862,6 +866,7 @@ class Mc1710UiAdapter( ReadbackApi.OpenGl30 -> GL30.glCheckFramebufferStatus(GL30.GL_DRAW_FRAMEBUFFER) == GL30.GL_FRAMEBUFFER_COMPLETE + ReadbackApi.ArbFramebufferObject -> ARBFramebufferObject.glCheckFramebufferStatus( ARBFramebufferObject.GL_DRAW_FRAMEBUFFER, @@ -960,6 +965,7 @@ class Mc1710UiAdapter( } /** Executes DSGL render commands using Minecraft rendering APIs. */ + @Suppress("LoopWithTooManyJumpStatements") override fun paint(commands: List) { paintsCount++ opacityStack.clear() @@ -1039,7 +1045,9 @@ class Mc1710UiAdapter( "[DSGL] Skipping DrawText due linkage error in text renderer: " + "${error.message}", ) - } catch (error: Throwable) { + } catch ( + @Suppress("TooGenericExceptionCaught") error: Throwable, + ) { logRateLimited( key = "drawText:runtime", message = "[DSGL] Skipping DrawText due renderer error: ${error.message}", @@ -1242,27 +1250,6 @@ class Mc1710UiAdapter( GL11.glColor4f(1f, 1f, 1f, 1f) } - private fun drawVerticalGradientRect( - x: Int, - y: Int, - width: Int, - height: Int, - topColor: Int, - bottomColor: Int, - ) { - if (width <= 0 || height <= 0) return - GL11.glDisable(GL11.GL_TEXTURE_2D) - GL11.glDisable(GL11.GL_ALPHA) - GL11.glEnable(GL11.GL_BLEND) - GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA) - GL11.glShadeModel(GL11.GL_SMOOTH) - drawVerticalGradientRectRaw(x, y, width, height, topColor, bottomColor) - GL11.glEnable(GL11.GL_ALPHA) - GL11.glShadeModel(GL11.GL_FLAT) - GL11.glEnable(GL11.GL_TEXTURE_2D) - GL11.glColor4f(1f, 1f, 1f, 1f) - } - private inline fun drawGradientBlock(block: () -> Unit) { GL11.glDisable(GL11.GL_TEXTURE_2D) GL11.glDisable(GL11.GL_DEPTH_TEST) @@ -1319,42 +1306,9 @@ class Mc1710UiAdapter( GL11.glEnd() } - private fun drawBilinearGradientRect( - x: Int, - y: Int, - width: Int, - height: Int, - topLeftColor: Int, - topRightColor: Int, - bottomRightColor: Int, - bottomLeftColor: Int, - ) { - if (width <= 0 || height <= 0) return - GL11.glDisable(GL11.GL_TEXTURE_2D) - GL11.glEnable(GL11.GL_BLEND) - GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA) - GL11.glShadeModel(GL11.GL_SMOOTH) - GL11.glBegin(GL11.GL_QUADS) - glColor(topLeftColor) - GL11.glVertex2f(x.toFloat(), y.toFloat()) - glColor(bottomLeftColor) - GL11.glVertex2f(x.toFloat(), (y + height).toFloat()) - glColor(bottomRightColor) - GL11.glVertex2f((x + width).toFloat(), (y + height).toFloat()) - glColor(topRightColor) - GL11.glVertex2f((x + width).toFloat(), y.toFloat()) - GL11.glEnd() - GL11.glShadeModel(GL11.GL_FLAT) - GL11.glEnable(GL11.GL_TEXTURE_2D) - GL11.glColor4f(1f, 1f, 1f, 1f) - } - private fun glColor(argb: Int) { - val a = ((argb ushr 24) and 0xFF) / 255f - val r = ((argb ushr 16) and 0xFF) / 255f - val g = ((argb ushr 8) and 0xFF) / 255f - val b = (argb and 0xFF) / 255f - GL11.glColor4f(r, g, b, a) + val color = RgbaColor.fromArgbInt(argb) + GL11.glColor4f(color.r, color.g, color.b, color.a) } private fun hsvToArgbInt(hueDeg: Float, saturation: Float, value: Float): Int { @@ -1533,9 +1487,11 @@ class Mc1710UiAdapter( ReadbackApi.ArbFramebufferObject -> readBuffer in ARBFramebufferObject.GL_COLOR_ATTACHMENT0..(ARBFramebufferObject.GL_COLOR_ATTACHMENT0 + 15) + ReadbackApi.ExtFramebufferObject -> readBuffer in EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT..(EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT + 15) + ReadbackApi.Legacy -> false } @@ -1554,6 +1510,7 @@ class Mc1710UiAdapter( ARBFramebufferObject.GL_COLOR_ATTACHMENT0, EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT, -> "GL_COLOR_ATTACHMENT0" + else -> { val hex = Integer.toHexString(value).uppercase() "0x$hex" @@ -1705,7 +1662,7 @@ class Mc1710UiAdapter( } } true - } catch (ex: Exception) { + } catch (_: java.io.IOException) { false } @@ -1721,7 +1678,9 @@ class Mc1710UiAdapter( val location = mc.textureManager.getDynamicTextureLocation(name, texture) imageCache[cacheKey] = location location - } catch (ex: Exception) { + } catch ( + @Suppress("TooGenericExceptionCaught", "SwallowedException") _: Exception, + ) { null } } diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/text/MsdfTextRenderer.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/text/MsdfTextRenderer.kt index 8f29b9f..bca16e0 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/text/MsdfTextRenderer.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/text/MsdfTextRenderer.kt @@ -121,14 +121,6 @@ internal class MsdfTextRenderer { var textureUploadBytes: Long = 0L, ) - private data class DecorationSegment( - var startX: Float, - var endX: Float, - val y: Float, - val thickness: Float, - val color: Int, - ) - private data class ObfuscationBuckets( val byAdvanceBucket: Map>, val expandedByAdvanceBucket: Map>, @@ -196,6 +188,7 @@ internal class MsdfTextRenderer { fun lineHeight(fontId: String?, fontSize: Int?): Int = FontRegistry.lineHeight(fontId, fontSize) + @Suppress("UnusedParameter") fun fontLineMetrics(fontId: String?, fontSize: Int?): FontLineMetrics? { val font = FontRegistry.get(fontId) ?: return null val metrics = font.meta.metrics @@ -1019,8 +1012,9 @@ internal class MsdfTextRenderer { val width = bitmap.width.coerceAtLeast(1) val height = bitmap.height.coerceAtLeast(1) if (width > maxTextureSize || height > maxTextureSize) { - throw IllegalStateException( - "Atlas '${font.descriptor.fontId}' is ${width}x$height, exceeds GL_MAX_TEXTURE_SIZE=$maxTextureSize", + error( + "Atlas '${font.descriptor.fontId}' is ${width}x$height, " + + "exceeds GL_MAX_TEXTURE_SIZE=$maxTextureSize", ) } val buffer = BufferUtils.createByteBuffer(width * height * 4) @@ -1049,10 +1043,9 @@ internal class MsdfTextRenderer { } if (glError != GL11.GL_NO_ERROR) { GL11.glDeleteTextures(textureId) - throw IllegalStateException( - "glTexImage2D failed for '${font.descriptor.fontId}' (${width}x$height), glError=0x${ - glError.toString(16) - }", + error( + "glTexImage2D failed for '${font.descriptor.fontId}' (${width}x$height), " + + "glError=0x${glError.toString(16)}", ) } debugCounters.textureUploads += 1 @@ -1116,7 +1109,7 @@ internal class MsdfTextRenderer { ) if (linkStatus == GL11.GL_FALSE) { val info = ARBShaderObjects.glGetInfoLogARB(program, 4096) - throw IllegalStateException("Program link failed: $info") + error("Program link failed: $info") } return program } @@ -1132,7 +1125,7 @@ internal class MsdfTextRenderer { ) if (compileStatus == GL11.GL_FALSE) { val info = ARBShaderObjects.glGetInfoLogARB(shader, 4096) - throw IllegalStateException("Shader compile failed: $info") + error("Shader compile failed: $info") } return shader } diff --git a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/StickyControlClipAlignmentTests.kt b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/StickyControlClipAlignmentTests.kt index 9a1f791..9ae73a2 100644 --- a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/StickyControlClipAlignmentTests.kt +++ b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/StickyControlClipAlignmentTests.kt @@ -246,7 +246,13 @@ class StickyControlClipAlignmentTests { is RenderCommand.PopTransform -> transform.pop() is RenderCommand.PushClip -> { val transformed = transform.resolveClipRect(command.x, command.y, command.width, command.height) - val raw = GuiClipRect(command.x, command.y, command.width.coerceAtLeast(0), command.height.coerceAtLeast(0)) + val raw = + GuiClipRect( + command.x, + command.y, + command.width.coerceAtLeast(0), + command.height.coerceAtLeast(0), + ) pushClips += ObservedClipPush(raw = raw, transformed = transformed) clipStack.addLast(transformed) } @@ -286,7 +292,8 @@ class StickyControlClipAlignmentTests { assertNotNull(observed.activeClip, "Expected active clip while drawing '$text'") assertTrue( contains(observed.activeClip, observed.x, observed.y), - "Expected transformed clip to contain transformed text point for '$text': point=(${observed.x},${observed.y}) clip=${observed.activeClip}", + "Expected transformed clip to contain transformed text point for '$text': " + + "point=(${observed.x},${observed.y}) clip=${observed.activeClip}", ) } @@ -303,7 +310,8 @@ class StickyControlClipAlignmentTests { assertNotNull(caret.activeClip) assertTrue( containsRect(caret.activeClip, caret.transformed), - "Expected caret rect to be clipped by transformed active clip: caret=${caret.transformed} clip=${caret.activeClip}", + "Expected caret rect to be clipped by transformed active clip: " + + "caret=${caret.transformed} clip=${caret.activeClip}", ) } diff --git a/build-logic/src/main/kotlin/dsgl-core.conventions.gradle.kts b/build-logic/src/main/kotlin/dsgl-core.conventions.gradle.kts index 6ec3ab9..d9a6774 100644 --- a/build-logic/src/main/kotlin/dsgl-core.conventions.gradle.kts +++ b/build-logic/src/main/kotlin/dsgl-core.conventions.gradle.kts @@ -68,3 +68,7 @@ tasks.named("sourcesJar") { tasks.named("dokkaGeneratePublicationHtml") { dependsOn(generateDsglCoreMetadata) } + +tasks.matching { it.name.startsWith("runKtlintCheckOver") || it.name.startsWith("runKtlintFormatOver") }.configureEach { + dependsOn(generateDsglCoreMetadata) +} diff --git a/build-logic/src/main/kotlin/dsgl-mc-adapter.conventions.gradle.kts b/build-logic/src/main/kotlin/dsgl-mc-adapter.conventions.gradle.kts index a7db37b..474670c 100644 --- a/build-logic/src/main/kotlin/dsgl-mc-adapter.conventions.gradle.kts +++ b/build-logic/src/main/kotlin/dsgl-mc-adapter.conventions.gradle.kts @@ -1,11 +1,5 @@ -import org.gradle.api.publish.maven.MavenPublication -import org.gradle.api.tasks.SourceSetContainer -import org.gradle.api.tasks.bundling.Jar -import org.gradle.api.GradleException -import org.gradle.kotlin.dsl.the -import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension import org.dreamfinity.buildlogic.toKotlinPackageSegmentFromProjectName -import java.io.File +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension import java.util.Properties plugins { @@ -30,13 +24,13 @@ fun readRequiredModuleVersion(file: File): String { val publishEnabled = properties.getProperty("publishEnabled")?.trim()?.let { raw -> raw.toBooleanStrictOrNull() ?: throw GradleException( - "Property 'publishEnabled' in ${file.path} must be true or false." + "Property 'publishEnabled' in ${file.path} must be true or false.", ) } ?: true if (!publishEnabled) { logger.lifecycle( "Skipping DsglAdapterMetadata generation for ${project.path}: " + - "publishEnabled=false and moduleVersion is not set." + "publishEnabled=false and moduleVersion is not set.", ) return "" } @@ -62,7 +56,7 @@ val generateDsglAdapterMetadata = tasks.register("generateDsglAdapterMetadata") } val outputRoot = metadataGeneratedSourcesDir.get().asFile val outputFile = outputRoot.resolve( - packageName.replace('.', '/') + "/DsglAdapterMetadata.kt" + packageName.replace('.', '/') + "/DsglAdapterMetadata.kt", ) outputFile.parentFile.mkdirs() outputFile.writeText( @@ -72,7 +66,7 @@ val generateDsglAdapterMetadata = tasks.register("generateDsglAdapterMetadata") object DsglAdapterMetadata { const val VERSION: String = "$moduleVersion" } - """.trimIndent() + System.lineSeparator() + """.trimIndent() + System.lineSeparator(), ) } } @@ -95,12 +89,18 @@ tasks.named("dokkaGeneratePublicationHtml") { dependsOn(generateDsglAdapterMetadata) } +tasks.matching { it.name.startsWith("runKtlintCheckOver") || it.name.startsWith("runKtlintFormatOver") }.configureEach { + dependsOn(generateDsglAdapterMetadata) +} + val devJar = tasks.register("devJar") { + description = "Compiled JAR with deobfuscated names" from(sourceSets["main"].output) archiveClassifier.set("dev") } val devSourcesJar = tasks.register("devSourcesJar") { + description = "Source code JAR with deobfuscated names" dependsOn(generateDsglAdapterMetadata) from(sourceSets["main"].allSource) archiveClassifier.set("dev-sources") diff --git a/build-logic/src/main/kotlin/dsgl-static-analysis.conventions.gradle.kts b/build-logic/src/main/kotlin/dsgl-static-analysis.conventions.gradle.kts index ed2542d..07d4007 100644 --- a/build-logic/src/main/kotlin/dsgl-static-analysis.conventions.gradle.kts +++ b/build-logic/src/main/kotlin/dsgl-static-analysis.conventions.gradle.kts @@ -1,19 +1,20 @@ -import org.gradle.api.JavaVersion - plugins { id("io.gitlab.arturbosch.detekt") } +val detektBaselineFile = rootProject.file("config/detekt/baseline-${project.name}.xml") + detekt { buildUponDefaultConfig = true allRules = false config.setFrom(rootProject.files("config/detekt/detekt.yml")) - baseline = rootProject.file("config/detekt/baseline-${project.name}.xml") + baseline = detektBaselineFile parallel = true } tasks.withType().configureEach { jvmTarget = "1.8" + baseline.set(detektBaselineFile) reports { html.required.set(true) sarif.required.set(true) @@ -23,6 +24,10 @@ tasks.withType().configureEach { exclude("**/generated/**", "**/build/**") } +tasks.withType().configureEach { + baseline.set(detektBaselineFile) +} + tasks.withType().configureEach { jvmTarget = "1.8" } diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index cebb486..39c0b40 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -337,7 +337,7 @@ naming: excludeClassPattern: '$^' FunctionParameterNaming: active: true - parameterPattern: '[a-z][A-Za-z0-9]*' + parameterPattern: '_?[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' InvalidPackageDeclaration: active: true @@ -741,7 +741,7 @@ style: active: false UnusedParameter: active: true - allowedNames: 'ignored|expected' + allowedNames: 'ignored|expected|_.*' UnusedPrivateClass: active: true UnusedPrivateMember: diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/DomTree.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/DomTree.kt index 5cc2b1c..35e764f 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/DomTree.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/DomTree.kt @@ -263,8 +263,13 @@ class DomTree( paintBuffer.clear() paintBuffer.addAll(stagingPaintBuffer) commandsDirty = false - changed - } catch (error: Throwable) { + true + } catch ( + @Suppress("TooGenericExceptionCaught") error: Throwable, + ) { + // Paint rebuild is a defensive boundary: any element can throw during chunk building. + // Swallow and keep the previous frame's commands to avoid tearing down the UI. + // TODO(Veritaris): Add some error display val now = System.currentTimeMillis() if (now - lastPaintBuildErrorMs >= 2_000L) { lastPaintBuildErrorMs = now diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/DsglWindow.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/DsglWindow.kt index e3d1446..0782eef 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/DsglWindow.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/DsglWindow.kt @@ -1,8 +1,7 @@ package org.dreamfinity.dsgl.core import org.dreamfinity.dsgl.core.dom.DOMNode -import org.dreamfinity.dsgl.core.dsl.UiScope -import org.dreamfinity.dsgl.core.dsl.ui +import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.hooks.ComponentHookRuntime import org.dreamfinity.dsgl.core.hooks.HookRenderSessionMode import org.dreamfinity.dsgl.core.host.DsglWindowHost @@ -29,6 +28,7 @@ abstract class DsglWindow { } /** Records the open time for date/time controls. */ + @Suppress("UnusedParameter", "EmptyFunctionBlock") fun markOpened(instant: Instant, zoneId: ZoneId) { } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorTextCodec.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorTextCodec.kt index 6ba4791..27e7a24 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorTextCodec.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorTextCodec.kt @@ -141,8 +141,7 @@ object ColorTextCodec { val values = parseFunctionArgs(raw, prefix) ?: return null if (prefix == "rgb" && values.size != 3) return null if ((prefix == "rgba" || prefix == "argb") && values.size != 4) return null - if (values.size !in 3..4) return null - val (r, g, b, a, order) = + val parsed = if (prefix == "argb") { val a = parseAlphaComponent(values[0]) ?: return null val r = parseRgbComponent(values[1]) ?: return null @@ -157,9 +156,9 @@ object ColorTextCodec { RgbParseResult(r, g, b, a, if (values.size == 4) RgbChannelOrder.RGBA else null) } return ParsedColorText( - color = RgbaColor(r, g, b, a).normalized(), + color = RgbaColor(parsed.r, parsed.g, parsed.b, parsed.a).normalized(), detectedMode = ColorFormatMode.RGB, - detectedRgbOrder = order, + detectedRgbOrder = parsed.order, ) } @@ -223,11 +222,11 @@ object ColorTextCodec { val value = raw.trim() return if (value.endsWith("%")) { val p = value.dropLast(1).toFloatOrNull() ?: return null - if (p < 0f || p > 100f) return null + if (p !in 0f..100f) return null p / 100f } else { val number = value.toFloatOrNull() ?: return null - if (number < 0f || number > 255f) return null + if (number !in 0f..255f) return null number / 255f } } @@ -236,7 +235,7 @@ object ColorTextCodec { val value = raw.trim() return if (value.endsWith("%")) { val p = value.dropLast(1).toFloatOrNull() ?: return null - if (p < 0f || p > 100f) return null + if (p !in 0f..100f) return null p / 100f } else { val f = value.toFloatOrNull() ?: return null @@ -266,7 +265,7 @@ object ColorTextCodec { val value = raw.trim() return if (value.endsWith("%")) { val p = value.dropLast(1).toFloatOrNull() ?: return null - if (p < 0f || p > 100f) return null + if (p !in 0f..100f) return null p / 100f } else { val f = value.toFloatOrNull() ?: return null diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt index 129af3a..a99fd82 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt @@ -25,79 +25,51 @@ internal class SystemColorPickerPopupBodyNode( private var focusedSemanticInputKey: String? = null private val modeSelectButton: ButtonNode = - scope.button( - "", - { - this.key = "dsgl-system-color-picker-mode-select" - }, - ) + scope.button("", { + this.key = "dsgl-system-color-picker-mode-select" + }) private val rgbaOrderButton: ButtonNode = - scope.button( - "RGBA", - { - this.key = "dsgl-system-color-picker-order-rgba" - }, - ) + scope.button("RGBA", { + this.key = "dsgl-system-color-picker-order-rgba" + }) private val argbOrderButton: ButtonNode = - scope.button( - "ARGB", - { - this.key = "dsgl-system-color-picker-order-argb" - }, - ) + scope.button("ARGB", { + this.key = "dsgl-system-color-picker-order-argb" + }) private val colorFieldNode: ColorFieldSurfaceNode = - scope.colorField( - { - this.key = "dsgl-system-color-picker-surface-field" - }, - ) + scope.colorField({ + this.key = "dsgl-system-color-picker-surface-field" + }) private val hueSliderNode: HueSurfaceNode = - scope.hueSlider( - { - this.key = "dsgl-system-color-picker-surface-hue" - }, - ) + scope.hueSlider({ + this.key = "dsgl-system-color-picker-surface-hue" + }) private val alphaSliderNode: AlphaSurfaceNode = - scope.alphaSlider( - { - this.key = "dsgl-system-color-picker-surface-alpha" - }, - ) + scope.alphaSlider({ + this.key = "dsgl-system-color-picker-surface-alpha" + }) private val previousSwatchNode: ColorSwatchSurfaceNode = - scope.colorSwatch( - { - this.key = "dsgl-system-color-picker-swatch-previous" - }, - ) + scope.colorSwatch({ + this.key = "dsgl-system-color-picker-swatch-previous" + }) private val currentSwatchNode: ColorSwatchSurfaceNode = - scope.colorSwatch( - { - this.key = "dsgl-system-color-picker-swatch-current" - }, - ) + scope.colorSwatch({ + this.key = "dsgl-system-color-picker-swatch-current" + }) private val copyButton: ButtonNode = - scope.button( - "Copy", - { - this.key = "dsgl-system-color-picker-button-copy" - }, - ) + scope.button("Copy", { + this.key = "dsgl-system-color-picker-button-copy" + }) private val pasteButton: ButtonNode = - scope.button( - "Paste", - { - this.key = "dsgl-system-color-picker-button-paste" - }, - ) + scope.button("Paste", { + this.key = "dsgl-system-color-picker-button-paste" + }) private val pipetteButton: ButtonNode = - scope.button( - "Pipette", - { - this.key = "dsgl-system-color-picker-button-pipette" - }, - ) + scope.button("Pipette", { + this.key = "dsgl-system-color-picker-button-pipette" + }) private val inputLabelNodes: List = (0 until MAX_INPUT_SLOTS).map { index -> @@ -120,12 +92,10 @@ internal class SystemColorPickerPopupBodyNode( private val recentSwatchNodes: List = (0 until RECENT_SWATCH_COUNT).map { index -> - scope.colorSwatch( - { - allowEmpty = true - this.key = "dsgl-system-color-picker-recent-$index" - }, - ) + scope.colorSwatch({ + allowEmpty = true + this.key = "dsgl-system-color-picker-recent-$index" + }) } private var appliedStyle: ColorPickerStyle? = null @@ -969,36 +939,26 @@ internal class SystemColorPickerEyedropperOverlayNode( key = "dsgl-system-color-picker-eyedropper-capture", ).applyParent(this) private val shadowNode: ContainerNode = - scope.div( - { - this.key = "dsgl-system-color-picker-eyedropper-shadow" - }, - ) + scope.div({ + this.key = "dsgl-system-color-picker-eyedropper-shadow" + }) private val panelNode: ContainerNode = - scope.div( - { - this.key = "dsgl-system-color-picker-eyedropper-panel" - }, - ) + scope.div({ + this.key = "dsgl-system-color-picker-eyedropper-panel" + }) private val magnifierDrawNode: EyedropperMagnifierDrawNode = - scope.eyedropperMagnifier( - { - this.key = "dsgl-system-color-picker-eyedropper-magnifier" - }, - ) + scope.eyedropperMagnifier({ + this.key = "dsgl-system-color-picker-eyedropper-magnifier" + }) private val centerNode: ContainerNode = - scope.div( - { - this.key = "dsgl-system-color-picker-eyedropper-center" - }, - ) + scope.div({ + this.key = "dsgl-system-color-picker-eyedropper-center" + }) private val swatchNode: ColorSwatchSurfaceNode = - scope.colorSwatch( - { - allowEmpty = false - this.key = "dsgl-system-color-picker-eyedropper-swatch" - }, - ) + scope.colorSwatch({ + allowEmpty = false + this.key = "dsgl-system-color-picker-eyedropper-swatch" + }) private val modeTextNode: TextNode = createOverlayTextNode( key = "dsgl-system-color-picker-eyedropper-mode", diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt index c780c54..2e80042 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt @@ -326,16 +326,13 @@ fun promptModal( modalTitle(title) } modalBody { - input( - InputType.Text(value = value, placeholder = "Enter value"), - { - this.key = "modal.prompt.input.$key" - style = { width = 150.px } - onInput = { event -> - onValueInput(event.value) - } - }, - ) + input(InputType.Text(value = value, placeholder = "Enter value"), { + this.key = "modal.prompt.input.$key" + style = { width = 150.px } + onInput = { event -> + onValueInput(event.value) + } + }) } modalFooter { button(cancelText, { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngine.kt index 2a9ccb6..6f090b6 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngine.kt @@ -467,6 +467,10 @@ class ContextMenuEngine( return false } + // Per-level layout pass mutates each level's measurement, panelRect, and scrollOffset in + // place; the two early-out `break`s (missing parent, missing anchor rect) trim the level + // stack before exiting, which a filter/map chain cannot express. + @Suppress("LoopWithTooManyJumpStatements") private fun ensureLayout() { val ctx = lastMeasureContext ?: return if (!isOpen()) return diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt index c42b2f7..981b4c0 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt @@ -136,6 +136,7 @@ class OverlayDebugControlHost( return currentLayout.panelRect.contains(mouseX, mouseY) } + @Suppress("FunctionOnlyReturningConstant", "UnusedParameter") fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = false fun clearRefs() { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndBindings.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndListeners.kt similarity index 100% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndBindings.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndListeners.kt diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngine.kt index f2cd2e7..42befc1 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngine.kt @@ -430,6 +430,10 @@ object DefaultDndEngine : DndEngine { notifyDragOver(active) } + // Single pass that both collects drop candidates and records whether the drag source + // appeared in the hover chain; folding this into a filter+any pair would walk the chain + // twice and split closely coupled state across two expressions. + @Suppress("LoopWithTooManyJumpStatements") private fun resolveDropTarget( root: DOMNode, active: ActiveSession, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/DOMNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/DOMNode.kt index c0efbc9..168a75d 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/DOMNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/DOMNode.kt @@ -1,25 +1,19 @@ package org.dreamfinity.dsgl.core.dom import org.dreamfinity.dsgl.core.DsglColors -import org.dreamfinity.dsgl.core.animation.AnimationSpec -import org.dreamfinity.dsgl.core.animation.StyleAnimationEngine -import org.dreamfinity.dsgl.core.animation.TransitionSpec +import org.dreamfinity.dsgl.core.animation.* import org.dreamfinity.dsgl.core.debug.ScrollPerformanceCounters import org.dreamfinity.dsgl.core.dnd.* import org.dreamfinity.dsgl.core.dom.layout.* import org.dreamfinity.dsgl.core.dom.text.ResolvedTextMetrics -import org.dreamfinity.dsgl.core.dsl.ComponentProps -import org.dreamfinity.dsgl.core.dsl.StyleScope +import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.* -import org.dreamfinity.dsgl.core.font.FontRegistry +import org.dreamfinity.dsgl.core.font.* import org.dreamfinity.dsgl.core.hooks.ref.ElementHandle import org.dreamfinity.dsgl.core.hooks.ref.RefTarget import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.* -import org.dreamfinity.dsgl.core.text.MinecraftFormattingParser -import org.dreamfinity.dsgl.core.text.ParsedText -import org.dreamfinity.dsgl.core.text.TextStyleFlags -import org.dreamfinity.dsgl.core.text.TextStyleMetrics +import org.dreamfinity.dsgl.core.text.* import kotlin.math.roundToInt data class NodeStyleApplyResult( @@ -553,11 +547,12 @@ abstract class DOMNode( } /** Measures the node's desired size. */ + @Suppress("UnusedParameter") internal fun resolveLayoutStyleValues(ctx: UiMeasureContext, parentContentWidth: Int?, parentContentHeight: Int?) { if (appliedComputedStyle == null) { return } - val context = lengthResolveContext(ctx, parentContentWidth, parentContentHeight) + val context = lengthResolveContext(parentContentWidth, parentContentHeight) margin = marginStyleValue.resolveToInsets(context) padding = paddingStyleValue.resolveToInsets(context) val borderWidthPx = @@ -812,13 +807,14 @@ abstract class DOMNode( return deltaX to deltaY } + @Suppress("UnusedParameter") internal fun resolveFlexBasisForAxis( ctx: UiMeasureContext, parentContentWidth: Int?, parentContentHeight: Int?, axis: FlexDirection, ): Int? { - val context = lengthResolveContext(ctx, parentContentWidth, parentContentHeight) + val context = lengthResolveContext(parentContentWidth, parentContentHeight) val percentBase = if (axis == FlexDirection.Row) { LengthPercentBase.ContainerWidth @@ -831,11 +827,7 @@ abstract class DOMNode( ?.coerceAtLeast(0) } - private fun lengthResolveContext( - ctx: UiMeasureContext, - parentContentWidth: Int?, - parentContentHeight: Int?, - ): LengthResolveContext { + private fun lengthResolveContext(parentContentWidth: Int?, parentContentHeight: Int?): LengthResolveContext { val rootFontSizePx = rootNode().resolveComputedFontSizePx().toFloat() val inheritedFontSizePx = ( diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ContainerNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ContainerNode.kt index eb5ff96..3b3936a 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ContainerNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ContainerNode.kt @@ -2,10 +2,7 @@ package org.dreamfinity.dsgl.core.dom.elements import org.dreamfinity.dsgl.core.debug.ScrollPerformanceCounters import org.dreamfinity.dsgl.core.dom.DOMNode -import org.dreamfinity.dsgl.core.dom.layout.Insets -import org.dreamfinity.dsgl.core.dom.layout.Rect -import org.dreamfinity.dsgl.core.dom.layout.Size -import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.dom.layout.* import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.* import kotlin.math.roundToInt @@ -994,6 +991,7 @@ class ContainerNode( return contentY + child.margin.top + verticalOffset.coerceAtLeast(0) } + @Suppress("UnusedParameter") private fun renderContainedChild( ctx: UiMeasureContext, child: DOMNode, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/DateInputNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/DateInputNode.kt index 15831b6..950fd55 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/DateInputNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/DateInputNode.kt @@ -67,7 +67,7 @@ class DateInputNode( try { val dateTime = LocalDateTime.parse(text, formatter) dateTime.atZone(this.zoneId).toInstant() - } catch (ex: Exception) { + } catch (_: java.time.format.DateTimeParseException) { null } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/TextLayoutEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/TextLayoutEngine.kt index 83e2250..5b65a6d 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/TextLayoutEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/support/TextLayoutEngine.kt @@ -176,6 +176,7 @@ object TextLayoutEngine { substringSliceCalls.set(0L) } + @Suppress("LoopWithTooManyJumpStatements") private fun buildLines( text: String, maxWidth: Int?, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/DomReconciler.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/DomReconciler.kt index 0e3ecc1..10a3be3 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/DomReconciler.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/reconcile/DomReconciler.kt @@ -127,13 +127,7 @@ object DomReconciler { if (templateKey != null) { val identity = ChildIdentity(templateKey, template.javaClass) val queue = keyed[identity] ?: return null - while (queue.isNotEmpty()) { - val candidate = queue.removeFirst() - if (!consumed[candidate]) { - return candidate - } - } - return null + return generateSequence { queue.removeFirstOrNull() }.firstOrNull { !consumed[it] } } if (index < oldChildren.size) { @@ -143,15 +137,11 @@ object DomReconciler { } } - for (candidateIndex in oldChildren.indices) { - if (consumed[candidateIndex]) continue - val candidate = oldChildren[candidateIndex] - if (candidate.key != null) continue - if (canReuse(candidate, template)) { - return candidateIndex - } + return oldChildren.indices.firstOrNull { candidateIndex -> + !consumed[candidateIndex] && + oldChildren[candidateIndex].key == null && + canReuse(oldChildren[candidateIndex], template) } - return null } private fun canReuse(current: DOMNode, template: DOMNode): Boolean { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/StyleScope.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/StyleScope.kt index eac6b30..9906ce1 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/StyleScope.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/StyleScope.kt @@ -773,6 +773,7 @@ class StyleScope internal constructor( setExpression(StyleProperty.ALIGN, variable) } + @Suppress("FunctionNaming") fun `var`(name: String): StyleExpression.VariableRef { val normalized = if (name.startsWith("--")) name else "--$name" return StyleExpression.VariableRef(normalized) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/TextDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/TextDsl.kt index 0aa3d9b..2e3279e 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/TextDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/TextDsl.kt @@ -1,3 +1,5 @@ +@file:Suppress("MatchingDeclarationName") + package org.dreamfinity.dsgl.core.dsl import org.dreamfinity.dsgl.core.dom.elements.TextNode diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/UiScope.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/UiScope.kt index 122f5ec..146cb66 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/UiScope.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/UiScope.kt @@ -42,9 +42,7 @@ class UiScope internal constructor( ) { @PublishedApi internal fun requireHookOwnerWindow(): DsglWindow = - ownerWindow ?: throw IllegalStateException( - "Hook APIs require a UiScope owned by a DsglWindow render session.", - ) + ownerWindow ?: error("Hook APIs require a UiScope owned by a DsglWindow render session.") internal fun childScope(childParent: DOMNode): UiScope = UiScope( diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/FontRegistry.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/FontRegistry.kt index 3bb18d5..93c378f 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/FontRegistry.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/FontRegistry.kt @@ -70,7 +70,7 @@ class AtlasPayload internal constructor( decodedBitmap?.let { return it } synchronized(this) { decodedBitmap?.let { return it } - val bytes = encodedPngBytes ?: throw IllegalStateException("Missing atlas payload bytes") + val bytes = encodedPngBytes ?: error("Missing atlas payload bytes") val decoded = decodeAtlasBitmap(bytes) decodedBitmap = decoded encodedPngBytes = null @@ -112,7 +112,7 @@ class AtlasPayload internal constructor( val image = ByteArrayInputStream(bytes).use { input -> ImageIO.read(input) - } ?: throw IllegalStateException("Atlas payload is neither deflated rgba nor PNG") + } ?: error("Atlas payload is neither deflated rgba nor PNG") val width = image.width.coerceAtLeast(1) val height = image.height.coerceAtLeast(1) @@ -337,7 +337,8 @@ object FontRegistry { val durationMs = ((System.nanoTime() - startedAt) / 1_000_000L).coerceAtLeast(0L) println( "[DSGL-MSDF] preload summary: jar=$jarDiscovered external=$externalDiscovered " + - "override=$externalOverrodeJar invalidExternal=$invalidExternalPackages loaded=$loadedFonts/$totalFonts in ${durationMs}ms", + "override=$externalOverrodeJar invalidExternal=$invalidExternalPackages " + + "loaded=$loadedFonts/$totalFonts in ${durationMs}ms", ) return FontPreloadSummary( @@ -732,9 +733,10 @@ object FontRegistry { val meta = runCatching { MsdfFontMetaParser.parse(metaRaw) } .onFailure { error -> + val reason = error.message ?: error.javaClass.simpleName failFontLoad( descriptor, - "Failed to parse metadata '${descriptor.metaPath}': ${error.message ?: error.javaClass.simpleName}", + "Failed to parse metadata '${descriptor.metaPath}': $reason", ) }.getOrNull() ?: return null diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/MsdfFontMetaParser.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/MsdfFontMetaParser.kt index 479f804..92d0432 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/MsdfFontMetaParser.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/font/MsdfFontMetaParser.kt @@ -59,9 +59,7 @@ object MsdfFontMetaParser { ) val previous = glyphsByIndex.putIfAbsent(resolvedGlyphIndex, runtimeGlyph) - if (previous != null) { - throw IllegalArgumentException("Duplicate glyph index $resolvedGlyphIndex in metadata") - } + require(previous == null) { "Duplicate glyph index $resolvedGlyphIndex in metadata" } val codepoint = runtimeGlyph.codepoint if (codepoint != null && !glyphsByCodepoint.containsKey(codepoint)) { @@ -109,15 +107,9 @@ object MsdfFontMetaParser { } private fun validate(meta: MsdfMetaJson) { - if (meta.atlas.width <= 0 || meta.atlas.height <= 0) { - throw IllegalArgumentException("atlas.width and atlas.height must be > 0") - } - if (meta.metrics.lineHeight <= 0f) { - throw IllegalArgumentException("metrics.lineHeight must be > 0") - } - if (meta.glyphs.isEmpty()) { - throw IllegalArgumentException("glyphs list must not be empty") - } + require(meta.atlas.width > 0 && meta.atlas.height > 0) { "atlas.width and atlas.height must be > 0" } + require(meta.metrics.lineHeight > 0f) { "metrics.lineHeight must be > 0" } + require(meta.glyphs.isNotEmpty()) { "glyphs list must not be empty" } } private fun MsdfPlaneBoundsJson.toRuntime(): MsdfPlaneBounds = diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ComponentHookRuntime.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ComponentHookRuntime.kt index 981d8e4..b21ad0e 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ComponentHookRuntime.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ComponentHookRuntime.kt @@ -329,6 +329,7 @@ internal class ComponentHookContext( fun entryCount(): Int = entriesByPath.size + @Suppress("ThrowsCount") private fun resolvePath( path: HookPath, kind: HookEntryKind, @@ -469,7 +470,7 @@ internal class ComponentHookRuntime { private val pendingStorageHookBindings: MutableList = arrayListOf() private val renderEffectRegistrations: MutableMap = linkedMapOf() - @Suppress("ktlint:standard:max-line-length", "ktlint:standard:property-wrapping") + @Suppress("ktlint:standard:max-line-length", "ktlint:standard:property-wrapping", "MaxLineLength") private val committedEffectsByComponent: MutableMap> = linkedMapOf() private var pendingEffectCommitBatch: PendingEffectCommitBatch? = null @@ -615,6 +616,7 @@ internal class ComponentHookRuntime { ) } + @Suppress("ThrowsCount") fun leaveComponentInstance() { ensureActiveRender() if (componentStack.size <= 1) { @@ -1046,7 +1048,9 @@ internal class ComponentHookRuntime { ) { try { cleanup() - } catch (error: Throwable) { + } catch ( + @Suppress("TooGenericExceptionCaught") error: Throwable, + ) { println( "[DSGL][Hooks] Effect cleanup failed at path '$path' in component '${componentId.debugPath()}' " + "during $reason: ${error.message}", @@ -1151,6 +1155,7 @@ internal class ComponentHookRuntime { throw HookUsageException("Hook runtime internal error: no non-inferred frame in component stack.") } + @Suppress("ThrowsCount") private fun popInferredFrameForTransition() { val frame = componentStack.lastOrNull() @@ -1176,7 +1181,8 @@ internal class ComponentHookRuntime { ComponentFrameOrigin.Inferred -> popInferredFrameForTransition() ComponentFrameOrigin.Explicit -> { throw HookUsageException( - "Invalid nested component scope behavior: render ended with unbalanced explicit component scopes.", + "Invalid nested component scope behavior: " + + "render ended with unbalanced explicit component scopes.", ) } @@ -1229,11 +1235,15 @@ internal class ComponentHookRuntime { return childFrame } + // Stack-frame walker mutates three independent pieces of state (innerToOuter list, + // leafCallSite, foundRenderBoundary flag) while classifying each frame; skip-frame and + // stop-at-boundary are distinct control flows that don't collapse into a filter chain. + @Suppress("LoopWithTooManyJumpStatements") private fun inferComponentSnapshotFromCallContext(): InferenceSnapshot { val innerToOuter: MutableList = arrayListOf() var leafCallSite: HookCallSite? = null var foundRenderBoundary = false - val trace = Throwable().stackTrace + val trace = Throwable("hook-call-context-inference").stackTrace for (frame in trace) { if (isInferenceFrameworkFrame(frame)) { continue @@ -1327,6 +1337,7 @@ internal class ComponentHookRuntime { ) } + @Suppress("ThrowsCount") private fun maybeAdvanceInferredSiblingFrame(frame: ComponentFrame): ComponentFrame { if (frame.origin != ComponentFrameOrigin.Inferred) { return frame diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseContext.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseContext.kt index 69344c6..8e40ff3 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseContext.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseContext.kt @@ -1,3 +1,5 @@ +@file:Suppress("MatchingDeclarationName") + package org.dreamfinity.dsgl.core.hooks import org.dreamfinity.dsgl.core.dsl.UiScope diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseEffect.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseEffect.kt index 7abba1d..320eec1 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseEffect.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/UseEffect.kt @@ -1,3 +1,5 @@ +@file:Suppress("MatchingDeclarationName") + package org.dreamfinity.dsgl.core.hooks import org.dreamfinity.dsgl.core.DsglWindow diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ref/ElementHandle.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ref/ElementHandle.kt index cffc455..5e89efe 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ref/ElementHandle.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ref/ElementHandle.kt @@ -30,8 +30,9 @@ internal class NodeElementHandle( FocusManager.requestFocus(focusable) } + @Suppress("ForbiddenComment") override fun scrollIntoView() { - // TODO: add scroll container integration when generic scrolling API is available. + // TODO(Veritaris): add scroll container integration when generic scrolling API is available. } fun detach() { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt index 18b0962..25e713b 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt @@ -9,7 +9,6 @@ import org.dreamfinity.dsgl.core.colorpicker.internal.SystemColorPickerPanelMana import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.elements.TextEditState import org.dreamfinity.dsgl.core.dom.elements.support.TextEditOps -import org.dreamfinity.dsgl.core.dom.layout.Insets import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.KeyInput @@ -2270,8 +2269,13 @@ class InspectorController( StyleProperty.OPACITY -> formatFloatLiteral(style.opacity) } - private fun spacingLiteral(value: LengthInsets): String = - "${value.top.toCssLiteral()} ${value.right.toCssLiteral()} ${value.bottom.toCssLiteral()} ${value.left.toCssLiteral()}" + private fun spacingLiteral(value: LengthInsets): String { + val top = value.top.toCssLiteral() + val right = value.right.toCssLiteral() + val bottom = value.bottom.toCssLiteral() + val left = value.left.toCssLiteral() + return "$top $right $bottom $left" + } private fun pxLiteral(value: Int): String = "${value}px" @@ -2600,15 +2604,8 @@ class InspectorController( return "${node.styleType}[$key]" } - private fun pathToken(node: DOMNode): String { - val key = node.key?.toString() ?: "?" - return "${node.styleType}:$key" - } - private fun rectLabel(rect: Rect): String = "${rect.x},${rect.y},${rect.width}x${rect.height}" - private fun spacingLabel(value: Insets): String = "${value.top}/${value.right}/${value.bottom}/${value.left}" - private fun colorLabel(color: Int): String { val hex = color diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt index 2fbda9a..99e0753 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt @@ -801,7 +801,7 @@ internal class SystemInspectorOverlayNode( } InspectorEditorKind.StringInput -> { - renderStyleEditorStringInput(scope, parentNode, ctx, bodyScrollY, row) + renderStyleEditorStringInput(parentNode, ctx, bodyScrollY, row) renderStyleEditorColorPreview(scope, ctx, bodyScrollY, row, index) } @@ -903,7 +903,6 @@ internal class SystemInspectorOverlayNode( } private fun renderStyleEditorStringInput( - scope: UiScope, parentNode: DOMNode, ctx: UiMeasureContext, bodyScrollY: Int, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContracts.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContracts.kt index a4d581d..ea413c7 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContracts.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContracts.kt @@ -37,6 +37,7 @@ object OverlayLayerContracts { OverlayOwnerScope.System -> UiLayerId.SystemOverlay } + @Suppress("UnusedParameter") fun resolveTransientLayer(ownerScope: OverlayOwnerScope, cursorX: Int, cursorY: Int): UiLayerId = resolveTransientLayer(ownerScope) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouter.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouter.kt index f27f5e4..cc70244 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouter.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouter.kt @@ -282,7 +282,7 @@ class LayerDomInputRouter( val hovered = hoverTarget if (hovered != null) return hovered val focused = FocusManager.focusedNode() - return if (focused is TextAreaNode) focused else null + return focused as? TextAreaNode } private fun bubbleGenericWheel( diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt index 37a62e3..1432510 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt @@ -80,6 +80,7 @@ class SystemOverlayTransientOwnershipRegistry { SystemOverlayTransientSession(ownerToken = ownerToken) } + @Suppress("UnusedParameter") fun resolve(ownerToken: Any, cursorX: Int, cursorY: Int): SystemOverlayTransientSession = resolve(ownerToken) fun release(ownerToken: Any): Boolean = sessions.remove(ownerToken) != null diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt index 97cbe57..e7a151e 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt @@ -411,7 +411,7 @@ class SelectEngine( return true } - fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { + fun handleMouseUp(_mouseX: Int, _mouseY: Int, button: MouseButton): Boolean { val current = popup ?: return false if (visibilityState == VisibilityState.Hidden) return false if (button == MouseButton.LEFT && current.scrollbarDragging) { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/DssParser.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/DssParser.kt index 4fca186..4104bc3 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/DssParser.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/DssParser.kt @@ -7,7 +7,8 @@ class DssParseException( val line: Int, val column: Int, message: String, -) : RuntimeException("$path:$line:$column $message") + cause: Throwable? = null, +) : RuntimeException("$path:$line:$column $message", cause) object DssParser { private val importantSuffixRegex = Regex("(?i)\\s*!important\\s*$") @@ -19,6 +20,7 @@ object DssParser { return parse(text, file.path) } + @Suppress("ThrowsCount") fun parse(sourceText: String, sourceName: String = ""): StylesheetData { val text = stripBlockComments(sourceText) var index = 0 @@ -70,7 +72,7 @@ object DssParser { try { StyleSelector.parse(selectorText) } catch (ex: IllegalArgumentException) { - throw parseError(sourceName, text, selectorStart, ex.message ?: "Invalid selector.") + throw parseError(sourceName, text, selectorStart, ex.message ?: "Invalid selector.", ex) } } if (selector != null) { @@ -92,6 +94,7 @@ object DssParser { ) } + @Suppress("ThrowsCount") private fun parseDeclarations( sourceName: String, text: String, @@ -175,8 +178,10 @@ object DssParser { literal = expression.value, warningReporter = warnings, ) - } catch (ex: Exception) { - throw parseError(sourceName, text, valueStart, ex.message ?: "Invalid value.") + } catch ( + @Suppress("TooGenericExceptionCaught") ex: RuntimeException, + ) { + throw parseError(sourceName, text, valueStart, ex.message ?: "Invalid value.", ex) } } declarations.set(property, expression, important = important) @@ -204,6 +209,7 @@ object DssParser { source: String, index: Int, message: String, + cause: Throwable? = null, ): DssParseException { val safeIndex = index.coerceIn(0, source.length) var line = 1 @@ -216,7 +222,7 @@ object DssParser { col++ } } - return DssParseException(path, line, col, message) + return DssParseException(path, line, col, message, cause) } private fun stripBlockComments(source: String): String { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt index 5bc4cfe..ca26c5a 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt @@ -251,7 +251,6 @@ object StyleEngine { val parentComputed = node.parent?.appliedComputedStyleSnapshot() val winners = resolveCascadeWinners( - node = node, candidates = candidates, inline = node.inlineStyleDeclarations, inspector = inspector, @@ -635,7 +634,6 @@ object StyleEngine { val candidates = matchingCandidates(node, snapshot.index) val winners = resolveCascadeWinners( - node = node, candidates = candidates, inline = node.inlineStyleDeclarations, inspector = if (allowInspectorOverrides) inspectorOverrides[inspectorOverrideTarget(node)] else null, @@ -668,7 +666,6 @@ object StyleEngine { } private fun resolveCascadeWinners( - node: DOMNode, candidates: List, inline: StyleDeclarations, inspector: StyleDeclarations?, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleSelector.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleSelector.kt index 76c297e..06baa5b 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleSelector.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleSelector.kt @@ -108,6 +108,7 @@ data class StyleSelector( private val classRegex = Regex("^[a-zA-Z0-9_-]+$") private val idRegex = Regex("^[a-zA-Z0-9_-]+$") + @Suppress("LoopWithTooManyJumpStatements") fun parse(rawSelector: String): StyleSelector { val trimmed = rawSelector.trim() require(trimmed.isNotEmpty()) { "Selector cannot be empty." } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleValueParsing.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleValueParsing.kt index 12e022c..58342fb 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleValueParsing.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleValueParsing.kt @@ -23,6 +23,7 @@ fun parseSpacingShorthand(raw: String): Insets = allowNegative = true, ).resolveToInsets(LengthResolveContext()) +@Suppress("UnusedParameter") fun parseSpacingLengthShorthand( raw: String, allowNegative: Boolean, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/text/MinecraftFormattingParser.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/text/MinecraftFormattingParser.kt index 6cb5477..8bd4d78 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/text/MinecraftFormattingParser.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/text/MinecraftFormattingParser.kt @@ -203,6 +203,7 @@ object MinecraftFormattingParser { return out } + @Suppress("LoopWithTooManyJumpStatements") private fun parseMinecraft(text: String): ParsedText { val plain = StringBuilder(text.length) val spans = ArrayList(8) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/GlyphsTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/GlyphsTests.kt index e71b21e..1e8839e 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/GlyphsTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/GlyphsTests.kt @@ -19,6 +19,5 @@ class GlyphsTests { val glyph = gv.getGlyphCode(i) usedGlyphs.add(glyph) } - val x = 1 } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseEffectHookRuntimeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseEffectHookRuntimeTests.kt index fdea4cb..e6f3506 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseEffectHookRuntimeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseEffectHookRuntimeTests.kt @@ -219,7 +219,7 @@ class UseEffectHookRuntimeTests { events += "run" onDispose { events += "cleanup" } } - throw IllegalStateException("forced render failure") + error("forced render failure") } } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseReducerHookRuntimeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseReducerHookRuntimeTests.kt index a1fda70..198b38d 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseReducerHookRuntimeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseReducerHookRuntimeTests.kt @@ -7,11 +7,7 @@ import org.dreamfinity.dsgl.core.hooks.HookUsageException import org.dreamfinity.dsgl.core.hooks.useReducer import org.dreamfinity.dsgl.core.host.DsglWindowHost import org.dreamfinity.dsgl.core.host.Viewport -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertTrue -import kotlin.test.fail +import kotlin.test.* class UseReducerHookRuntimeTests { @Test @@ -230,6 +226,7 @@ class UseReducerHookRuntimeTests { rebuildRequests += 1 } + @Suppress("EmptyFunctionBlock") override fun requestRedraw() { } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseStateHookRuntimeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseStateHookRuntimeTests.kt index 54cd966..30240aa 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseStateHookRuntimeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/UseStateHookRuntimeTests.kt @@ -8,11 +8,7 @@ import org.dreamfinity.dsgl.core.hooks.HookUsageException import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.core.host.DsglWindowHost import org.dreamfinity.dsgl.core.host.Viewport -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertTrue -import kotlin.test.fail +import kotlin.test.* class UseStateHookRuntimeTests { @Test @@ -310,6 +306,7 @@ class UseStateHookRuntimeTests { rebuildRequests += 1 } + @Suppress("EmptyFunctionBlock") override fun requestRedraw() { } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutStickyBehaviorTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutStickyBehaviorTests.kt index 56e1f30..7523682 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutStickyBehaviorTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutStickyBehaviorTests.kt @@ -487,7 +487,8 @@ class PositionedLayoutStickyBehaviorTests { assertTrue(overlapDrawIndex >= 0, "Expected overlap draw command") assertTrue( stickyDrawIndex > overlapDrawIndex, - "Expected sticky draw after overlap draw, but stickyDrawIndex=$stickyDrawIndex overlapDrawIndex=$overlapDrawIndex", + "Expected sticky draw after overlap draw, but " + + "stickyDrawIndex=$stickyDrawIndex overlapDrawIndex=$overlapDrawIndex", ) } @@ -883,12 +884,11 @@ class PositionedLayoutStickyBehaviorTests { height = 180 overflowY = Overflow.Auto }.applyParent(root) - val outerSpacer = - ContainerNode(key = "sticky-nested-outer-spacer") - .apply { - width = 200 - height = 48 - }.applyParent(outer) + ContainerNode(key = "sticky-nested-outer-spacer") + .apply { + width = 200 + height = 48 + }.applyParent(outer) val inner = ContainerNode(key = "sticky-nested-inner") .apply { @@ -913,18 +913,16 @@ class PositionedLayoutStickyBehaviorTests { StyleProperty.TOP to "0px", ) }.applyParent(inner) - val innerFiller = - ContainerNode(key = "sticky-nested-inner-filler") - .apply { - width = 200 - height = 260 - }.applyParent(inner) - val outerFiller = - ContainerNode(key = "sticky-nested-outer-filler") - .apply { - width = 200 - height = 220 - }.applyParent(outer) + ContainerNode(key = "sticky-nested-inner-filler") + .apply { + width = 200 + height = 260 + }.applyParent(inner) + ContainerNode(key = "sticky-nested-outer-filler") + .apply { + width = 200 + height = 220 + }.applyParent(outer) val tree = DomTree(root) tree.render(ctx, 320, 260) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollReactiveSmoothTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollReactiveSmoothTests.kt index 0a19847..c250608 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollReactiveSmoothTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollReactiveSmoothTests.kt @@ -102,7 +102,8 @@ class ScrollReactiveSmoothTests { val expectedButtonY = state.viewportRect.y - state.scrollY + fixture.button.margin.top assertTrue( kotlin.math.abs(expectedButtonY - fixture.button.bounds.y) <= 1, - "expectedButtonY=$expectedButtonY actualButtonY=${fixture.button.bounds.y} scrollY=${state.scrollY} viewportY=${state.viewportRect.y}", + "expectedButtonY=$expectedButtonY actualButtonY=${fixture.button.bounds.y} " + + "scrollY=${state.scrollY} viewportY=${state.viewportRect.y}", ) previousScroll = state.scrollY previousThumb = thumbY @@ -211,7 +212,8 @@ class ScrollReactiveSmoothTests { val expectedButtonY = state.viewportRect.y - state.scrollY + fixture.button.margin.top assertTrue( kotlin.math.abs(expectedButtonY - fixture.button.bounds.y) <= 1, - "expectedButtonY=$expectedButtonY actualButtonY=${fixture.button.bounds.y} scrollY=${state.scrollY} viewportY=${state.viewportRect.y}", + "expectedButtonY=$expectedButtonY actualButtonY=${fixture.button.bounds.y} " + + "scrollY=${state.scrollY} viewportY=${state.viewportRect.y}", ) assertEquals(state.scrollY, debug.resolvedY) assertTrue(kotlin.math.abs(debug.displayedY - debug.resolvedY.toDouble()) <= 1.0) @@ -345,7 +347,8 @@ class ScrollReactiveSmoothTests { val debug = fixture.viewport.debugScrollAnimationState() assertTrue( kotlin.math.abs(expectedScroll - state.scrollY) <= 1, - "expectedScroll=$expectedScroll actualScroll=${state.scrollY} moveY=$moveY baseline=$baseline stateMax=${state.maxScrollY}", + "expectedScroll=$expectedScroll actualScroll=${state.scrollY} " + + "moveY=$moveY baseline=$baseline stateMax=${state.maxScrollY}", ) assertEquals(state.scrollY, debug.resolvedY) assertEquals(debug.displayedY, debug.resolvedY.toDouble()) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollbarRenderingInteractionTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollbarRenderingInteractionTests.kt index 32f3c18..8740610 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollbarRenderingInteractionTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollbarRenderingInteractionTests.kt @@ -39,7 +39,7 @@ class ScrollbarRenderingInteractionTests { @Test fun `generic scrollbar rendering emits track and thumb when present`() { - val (_, viewport, _, _) = + val viewport = createFixture( overflowX = Overflow.Visible, overflowY = Overflow.Scroll, @@ -47,7 +47,7 @@ class ScrollbarRenderingInteractionTests { viewportHeight = 70, contentWidth = 90, contentHeight = 40, - ) + ).viewport val commands = selfCommands(viewport) val vertical = viewport.debugScrollbarVisualState().vertical @@ -76,7 +76,7 @@ class ScrollbarRenderingInteractionTests { @Test fun `thumb geometry stays synchronized with scroll offsets`() { - val (_, viewport, _, _) = + val viewport = createFixture( overflowX = Overflow.Visible, overflowY = Overflow.Auto, @@ -84,7 +84,7 @@ class ScrollbarRenderingInteractionTests { viewportHeight = 70, contentWidth = 90, contentHeight = 260, - ) + ).viewport val initialState = viewport.scrollContainerState() val initialThumb = @@ -120,7 +120,7 @@ class ScrollbarRenderingInteractionTests { @Test fun `wheel scrolling updates generic container scroll state`() { - val (root, viewport, wheelTarget, router) = + val fixture = createFixture( overflowX = Overflow.Visible, overflowY = Overflow.Auto, @@ -129,6 +129,10 @@ class ScrollbarRenderingInteractionTests { contentWidth = 90, contentHeight = 260, ) + val root = fixture.root + val viewport = fixture.viewport + val wheelTarget = fixture.wheelTarget + val router = fixture.router val wheelX = wheelTarget.bounds.x + 2 val wheelY = wheelTarget.bounds.y + 2 @@ -144,7 +148,7 @@ class ScrollbarRenderingInteractionTests { @Test fun `thumb drag updates scroll offset through generic pointer capture`() { - val (_, viewport, _, router) = + val fixture = createFixture( overflowX = Overflow.Visible, overflowY = Overflow.Auto, @@ -153,6 +157,8 @@ class ScrollbarRenderingInteractionTests { contentWidth = 90, contentHeight = 320, ) + val viewport = fixture.viewport + val router = fixture.router val visual = viewport.debugScrollbarVisualState().vertical @@ -180,7 +186,7 @@ class ScrollbarRenderingInteractionTests { Overflow.Auto to true, ) modes.forEach { (mode, expectedVisible) -> - val (_, viewport, _, _) = + val viewport = createFixture( overflowX = Overflow.Visible, overflowY = mode, @@ -188,7 +194,7 @@ class ScrollbarRenderingInteractionTests { viewportHeight = 70, contentWidth = 90, contentHeight = 240, - ) + ).viewport val commands = selfCommands(viewport) val vertical = viewport.debugScrollbarVisualState().vertical @@ -203,7 +209,7 @@ class ScrollbarRenderingInteractionTests { @Test fun `horizontal auto scrollbar appears when content width exceeds viewport`() { - val (_, viewport, _, _) = + val viewport = createFixture( overflowX = Overflow.Auto, overflowY = Overflow.Hidden, @@ -211,7 +217,7 @@ class ScrollbarRenderingInteractionTests { viewportHeight = 634, contentWidth = 826, contentHeight = 620, - ) + ).viewport val commands = selfCommands(viewport) val horizontal = viewport.debugScrollbarVisualState().horizontal @@ -230,7 +236,7 @@ class ScrollbarRenderingInteractionTests { @Test fun `normal wheel scrolls only vertical axis`() { - val (_, viewport, wheelTarget, router) = + val fixture = createFixture( overflowX = Overflow.Auto, overflowY = Overflow.Auto, @@ -239,6 +245,9 @@ class ScrollbarRenderingInteractionTests { contentWidth = 280, contentHeight = 260, ) + val viewport = fixture.viewport + val wheelTarget = fixture.wheelTarget + val router = fixture.router KeyModifiers.sync(shift = false, control = false, meta = false) val wheelX = wheelTarget.bounds.x + 2 @@ -253,7 +262,7 @@ class ScrollbarRenderingInteractionTests { @Test fun `shift plus wheel scrolls only horizontal axis`() { - val (_, viewport, wheelTarget, router) = + val fixture = createFixture( overflowX = Overflow.Auto, overflowY = Overflow.Auto, @@ -262,6 +271,9 @@ class ScrollbarRenderingInteractionTests { contentWidth = 280, contentHeight = 260, ) + val viewport = fixture.viewport + val wheelTarget = fixture.wheelTarget + val router = fixture.router KeyModifiers.sync(shift = true, control = false, meta = false) val wheelX = wheelTarget.bounds.x + 2 @@ -329,7 +341,7 @@ class ScrollbarRenderingInteractionTests { @Test fun `wheel scroll keeps thumb position synchronized`() { - val (_, viewport, wheelTarget, router) = + val fixture = createFixture( overflowX = Overflow.Visible, overflowY = Overflow.Auto, @@ -338,6 +350,9 @@ class ScrollbarRenderingInteractionTests { contentWidth = 90, contentHeight = 320, ) + val viewport = fixture.viewport + val wheelTarget = fixture.wheelTarget + val router = fixture.router KeyModifiers.sync(shift = false, control = false, meta = false) val beforeVisual = diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/InlineLayoutTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/InlineLayoutTests.kt index 4ea9acb..99f0302 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/InlineLayoutTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/InlineLayoutTests.kt @@ -421,7 +421,8 @@ class InlineLayoutTests { } assertTrue( maxOuterWidth <= contentWidth, - "Inline child outer width should fit content width: maxOuterWidth=$maxOuterWidth, contentWidth=$contentWidth", + "Inline child outer width should fit content width: " + + "maxOuterWidth=$maxOuterWidth, contentWidth=$contentWidth", ) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextLineSpaceReservationBaselineTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextLineSpaceReservationBaselineTests.kt index 6c3507d..82ac8b3 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextLineSpaceReservationBaselineTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextLineSpaceReservationBaselineTests.kt @@ -211,7 +211,8 @@ class TextLineSpaceReservationBaselineTests { ) assertTrue( withWorkaround.bounds.height >= ordinaryRow.bounds.height, - "Workaround may still increase explicit minimums, but ordinary line-box reservation no longer depends on it.", + "Workaround may still increase explicit minimums, but ordinary line-box reservation " + + "no longer depends on it.", ) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextPerformanceHotPathCharacterizationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextPerformanceHotPathCharacterizationTests.kt index 844b8fc..b629c63 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextPerformanceHotPathCharacterizationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextPerformanceHotPathCharacterizationTests.kt @@ -165,19 +165,19 @@ class TextPerformanceHotPathCharacterizationTests { width = 180 height = 120 } - val node = - TextNode( - textSource = - TextSource.Static( - "Wrapped text baseline path should repeatedly measure many ranges while fitting lines for wrapping.", - ), - key = "text.hotpath.wrap", - ).apply { - width = 120 - fontId = FontRegistry.FONT_MINECRAFT - fontSize = 16 - textWrap = TextWrap.Wrap - }.applyParent(root) + TextNode( + textSource = + TextSource.Static( + "Wrapped text baseline path should repeatedly measure many ranges " + + "while fitting lines for wrapping.", + ), + key = "text.hotpath.wrap", + ).apply { + width = 120 + fontId = FontRegistry.FONT_MINECRAFT + fontSize = 16 + textWrap = TextWrap.Wrap + }.applyParent(root) val tree = DomTree(root) tree.render(ctx, 180, 120) @@ -465,7 +465,8 @@ class TextPerformanceHotPathCharacterizationTests { TextNode( textSource = TextSource.Static( - "Row $index wraps repeatedly to characterize current range-based text measurement under scroll-heavy updates.", + "Row $index wraps repeatedly to characterize current range-based text " + + "measurement under scroll-heavy updates.", ), key = "scroll-hot-text-$index", ).apply { @@ -734,13 +735,11 @@ class TextPerformanceHotPathCharacterizationTests { Character.isValidCodePoint(cp) && !primary.canDisplay(cp) && fallback.canDisplay(cp) }?.let { return it } - for (cp in 0x20..0x2FFF) { - if (!Character.isValidCodePoint(cp)) continue - if (cp in 0xD800..0xDFFF) continue - if (!primary.canDisplay(cp) && fallback.canDisplay(cp)) { - return cp - } - } - error("Could not find deterministic fallback-only codepoint for characterization test") + return (0x20..0x2FFF).firstOrNull { cp -> + Character.isValidCodePoint(cp) && + cp !in 0xD800..0xDFFF && + !primary.canDisplay(cp) && + fallback.canDisplay(cp) + } ?: error("Could not find deterministic fallback-only codepoint for characterization test") } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/font/MsdfFontTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/font/MsdfFontTests.kt index eedd5a9..a24e0fd 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/font/MsdfFontTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/font/MsdfFontTests.kt @@ -426,16 +426,13 @@ class MsdfFontTests { assertTrue(decoded.rgbaBytes.contentEquals(expected)) } - private fun findFallbackOnlyCodepoint(primary: Font, fallback: Font): Int? { - for (cp in 0x20..0x10FFFF) { - if (!Character.isValidCodePoint(cp)) continue - if (cp in 0xD800..0xDFFF) continue - if (!primary.canDisplay(cp) && fallback.canDisplay(cp)) { - return cp - } + private fun findFallbackOnlyCodepoint(primary: Font, fallback: Font): Int? = + (0x20..0x10FFFF).firstOrNull { cp -> + Character.isValidCodePoint(cp) && + cp !in 0xD800..0xDFFF && + !primary.canDisplay(cp) && + fallback.canDisplay(cp) } - return null - } private fun loadResource(path: String): String { val stream = javaClass.classLoader.getResourceAsStream(path) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorControllerTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorControllerTests.kt index 7b7aa04..5ee7124 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorControllerTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorControllerTests.kt @@ -9,7 +9,6 @@ import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.input.ClipboardAccess import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleExpression import org.dreamfinity.dsgl.core.style.StyleProperty @@ -565,16 +564,6 @@ class InspectorControllerTests { bounds = Rect(x, y, width, height) } - private class RecordingClipboardAccess : ClipboardAccess { - var contents: String = "" - - override fun readText(): String = contents - - override fun writeText(value: String) { - contents = value - } - } - private class RecordingInspectorColorPickerHost : InspectorColorPickerHost { var lastOpen: OpenCall? = null private var open: Boolean = false diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouterTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouterTests.kt index b8af560..ad736c8 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouterTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouterTests.kt @@ -31,6 +31,7 @@ class LayerDomInputRouterTests { override fun measureText(text: String): Int = text.length * 6 + @Suppress("EmptyFunctionBlock") override fun paint(commands: List) {} } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt index a94a264..a7449b9 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt @@ -265,27 +265,6 @@ class InspectorPointerAlignmentTests { } } - private fun findVisibleSelectRowWithoutScrolling(fixture: Fixture): InspectorStyleEditorRowSnapshot { - val rows = - fixture.inspector.overlayStyleEditorRows().filter { row -> - row.editorKind == InspectorEditorKind.EnumSelect || row.editorKind == InspectorEditorKind.FontSelect - } - val contentRect = fixture.inspector.overlayContentRect() - val bodyScrollY = fixture.inspector.panelScrollOffsetY - return rows.firstOrNull { row -> - val rect = - Rect( - row.controlRect.x, - row.controlRect.y - bodyScrollY, - row.controlRect.width, - row.controlRect.height, - ) - val centerX = rect.x + (rect.width / 2).coerceAtLeast(1) - val centerY = rect.y + (rect.height / 2).coerceAtLeast(1) - contentRect.contains(centerX, centerY) - } ?: error("expected visible inspector select row without scrolling") - } - private fun findOrScrollToVisibleSelectRow(fixture: Fixture): InspectorStyleEditorRowSnapshot { repeat(120) { val rows = diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorTextEditingDomMigrationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorTextEditingDomMigrationTests.kt index 15e7e3d..970723b 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorTextEditingDomMigrationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorTextEditingDomMigrationTests.kt @@ -258,7 +258,8 @@ class InspectorTextEditingDomMigrationTests { } private fun findVisibleInputNode(host: SystemOverlayHost, inspector: InspectorController, keyPrefix: String): TextInputNode { - val inspectorNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector entry missing") + val inspectorNode = + host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector entry missing") val contentRect = inspector.overlayContentRect() val candidates = collectNodes(inspectorNode) From 0a1f0c953c10c5e0f05029deeb14bf7d1eff4e44 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sat, 25 Apr 2026 15:01:02 +0300 Subject: [PATCH 36/78] fixing "MaxLineLength" errors; excluding tests from detekt processing - we increased limit to 160 in ktlint, but detekt does not have this feature thus excluding this directories; --- .../dsgl/mcForge1710/demo/ShowcaseWindow.kt | 4 +- .../demo/sections/CssCascadeSection.kt | 3 +- .../demo/sections/DragDropSection.kt | 9 ++- .../demo/sections/InputsGallerySection.kt | 3 +- .../demo/sections/InteractionsSection.kt | 7 ++- .../demo/sections/LayoutDebugSection.kt | 3 +- .../demo/sections/MsdfFontsSection.kt | 34 ++++++----- .../demo/sections/OverflowScrollSection.kt | 5 +- .../demo/sections/PositionedLayoutSection.kt | 3 +- .../demo/sections/TextWrapSection.kt | 9 ++- .../demo/support/EventFormatting.kt | 20 +++++-- ...sgl-static-analysis.conventions.gradle.kts | 8 +-- config/detekt/detekt.yml | 2 + .../core/contextmenu/ContextMenuEngine.kt | 56 +++++++++---------- 14 files changed, 101 insertions(+), 65 deletions(-) diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/ShowcaseWindow.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/ShowcaseWindow.kt index da83566..9720ad8 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/ShowcaseWindow.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/ShowcaseWindow.kt @@ -124,7 +124,9 @@ class ShowcaseWindow : DsglWindow() { }) { text("DSGL Showcase Window", { style = { color = DsglColors.WHITE } }) text( - "renderPasses=$renderPasses section=${selectedSection.title} viewport=${viewportWidth}x$viewportHeight", + "renderPasses=$renderPasses " + + "section=${selectedSection.title} " + + "viewport=${viewportWidth}x$viewportHeight", { style = { color = DEMO_MUTED diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/CssCascadeSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/CssCascadeSection.kt index 4bc54a4..484eeba 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/CssCascadeSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/CssCascadeSection.kt @@ -50,7 +50,8 @@ fun UiScope.cssCascadeCombinatorsSection(onLogHook: (String, Event, String?) -> } }) { text( - "CSS-like cascade demo: descendant/child/sibling selectors, specificity, source order, !important, inheritance.", + "CSS-like cascade demo: descendant/child/sibling selectors, specificity, " + + "source order, !important, inheritance.", ) text( "Use the controls to toggle classes, swap siblings, and insert/remove items.", diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/DragDropSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/DragDropSection.kt index e4e4dbe..478c264 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/DragDropSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/DragDropSection.kt @@ -531,18 +531,21 @@ fun UiScope.dragNDropSection( }) { text("Drag preview modes: ORIGINAL (detached source) and GHOST (overlay preview).") text( - "active=${state.activeItem} mode=${monitor.mode?.name ?: "none"} effect=${state.dropEffect} hover=${state.hoverZone}", + "active=${state.activeItem} mode=${monitor.mode?.name ?: "none"} " + + "effect=${state.dropEffect} hover=${state.hoverZone}", { style = { color = DEMO_MUTED } }, ) text("types=${state.transferTypes} dragTicks=${state.dragTickCount} action=${state.lastAction}", { style = { color = DEMO_MUTED } }) text( - "debug active=${monitor.sourceKey ?: "none"} over=${state.debugOverId} container=${state.debugOverContainerId}", + "debug active=${monitor.sourceKey ?: "none"} over=${state.debugOverId} " + + "container=${state.debugOverContainerId}", { style = { color = DEMO_MUTED } }, ) text( - "candidates=${state.debugCandidatesCount} insert=${state.debugInsertPosition} excludeActive=${state.debugExcludesActiveCard}", + "candidates=${state.debugCandidatesCount} insert=${state.debugInsertPosition} " + + "excludeActive=${state.debugExcludesActiveCard}", { style = { color = DEMO_MUTED } }, ) div({ diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt index 6b59c18..da11ecd 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt @@ -405,7 +405,8 @@ fun UiScope.inputsGallerySection(clippingScrollDemoText: String, onClippingScrol } } text( - "Select state: basic=${selectBasicValue ?: "-"} many=${selectManyValue ?: "-"} dynamic=${selectDynamicValue ?: "-"}", + "Select state: basic=${selectBasicValue ?: "-"} many=${selectManyValue ?: "-"} " + + "dynamic=${selectDynamicValue ?: "-"}", { style = { color = DEMO_MUTED } }, ) diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InteractionsSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InteractionsSection.kt index 91c8f2b..037e0c1 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InteractionsSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InteractionsSection.kt @@ -132,7 +132,9 @@ fun UiScope.interactionsSection(onInfo: (String) -> Unit, onLogHook: (String, Ev }) { text("Move, click, drag and wheel here") text( - "E$mouseEnterCount L$mouseLeaveCount O$mouseOverCount M$mouseMoveCount D$mouseDownCount/$mouseUpCount C$mouseClickCount G$mouseDragCount W$mouseWheelCount", + "E$mouseEnterCount L$mouseLeaveCount O$mouseOverCount M$mouseMoveCount " + + "D$mouseDownCount/$mouseUpCount C$mouseClickCount G$mouseDragCount " + + "W$mouseWheelCount", { style = { color = DEMO_MUTED } }, ) } @@ -176,7 +178,8 @@ fun UiScope.interactionsSection(onInfo: (String) -> Unit, onLogHook: (String, Ev } text( - "Key counters: down=$keyDownCount up=$keyUpCount pressed=$keyPressedCount released=$keyReleasedCount enter=$enterActionCount", + "Key counters: down=$keyDownCount up=$keyUpCount pressed=$keyPressedCount " + + "released=$keyReleasedCount enter=$enterActionCount", { style = { color = DEMO_MUTED } }, ) diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutDebugSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutDebugSection.kt index 9ec23eb..65fff7c 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutDebugSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutDebugSection.kt @@ -62,7 +62,8 @@ fun UiScope.layoutDebugSection(onClearLogs: () -> Unit, onInfo: (String) -> Unit }) } text( - "validatorViolations=${LayoutDebug.lastViolationCount} strict=${LayoutDebug.strictBounds} draw=${LayoutDebug.drawBounds}", + "validatorViolations=${LayoutDebug.lastViolationCount} " + + "strict=${LayoutDebug.strictBounds} draw=${LayoutDebug.drawBounds}", { style = { color = DEMO_MUTED } }, ) diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/MsdfFontsSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/MsdfFontsSection.kt index 5872faa..49ace92 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/MsdfFontsSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/MsdfFontsSection.kt @@ -1,16 +1,10 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections -import org.dreamfinity.dsgl.core.dom.elements.InputType +import org.dreamfinity.dsgl.core.dom.elements.* import org.dreamfinity.dsgl.core.dsl.* -import org.dreamfinity.dsgl.core.font.FontRegistry +import org.dreamfinity.dsgl.core.font.* import org.dreamfinity.dsgl.core.hooks.useState -import org.dreamfinity.dsgl.core.style.Display -import org.dreamfinity.dsgl.core.style.FlexDirection -import org.dreamfinity.dsgl.core.style.FontStyle -import org.dreamfinity.dsgl.core.style.FontWeight -import org.dreamfinity.dsgl.core.style.TextDecoration -import org.dreamfinity.dsgl.core.style.TextFormatting -import org.dreamfinity.dsgl.core.style.TextWrap +import org.dreamfinity.dsgl.core.style.* import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED import org.dreamfinity.dsgl.mcForge1710.text.MsdfRuntimeDebugSettings @@ -23,14 +17,21 @@ private val COLOR_PRESETS = ) private const val SAMPLE_PARAGRAPH = - "MSDF/MTSDF text rendering demo in DSGL. This paragraph should wrap cleanly in a fixed-width panel and respect font switches, opacity, and size." + "MSDF/MTSDF text rendering demo in DSGL. This paragraph should wrap cleanly " + + "in a fixed-width panel and respect font switches, opacity, and size." private const val SAMPLE_WORD = "\u4ED6\u65B9\u3001\u6210\u7E3E\u8A55long_unbroken_word_to_force_hard_break_ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789" private const val SAMPLE_SPACES_A = "Hello world" + +@Suppress("MaxLineLength") private const val LONG_CH_SENTENCE = "\u592A\u9633\u6652\u5F97\u58A8\u9ED1\u7684\u6E05\u7626\u7684\u8138\u4E0A\uFF0C\u6709\u4E00\u5BF9\u7A0D\u7A0D\u6D3C\u8FDB\u53BB\u7684\u5927\u5927\u7684\u53CC\u773C\u76AE\u513F\u773C\u775B\uFF0C\u7709\u6BDB\u7EC6\u800C\u659C\uFF0C\u9ED1\u91CC\u5E26\u9EC4\u7684\u5934\u53D1\u7528\u82B1\u5E03\u6761\u5B50\u624E\u4E24\u6761\u77ED\u8FAB\u5B50\uFF0C\u8863\u670D\u90FD\u5F88\u65E7\uFF0C\u53F3\u88E4\u811A\u4E0A\u7684\u4E00\u4E2A\u7834\u6D1E\u522B\u4E00\u652F\u522B\u9488\uFF0C\u6625\u590F\u79CB\u4E09\u5B63\u90FD\u6253\u8D64\u811A\uFF0C\u53EA\u6709\u4E0A\u5C71\u6293\u67F4\u79BE\u7684\u65F6\u8282\uFF0C\u6015\u523A\u7834\u811A\u677F\uFF0C\u624D\u7A7F\u53CC\u978B\u5B50\uFF0C\u4F46\u4E00\u4E0B\u5C71\u5C31\u8131\u4E86\u3002" + +@Suppress("MaxLineLength") private const val LONG_JP_SENTENCE = "\u4ED6\u65B9\u3001\u6210\u7E3E\u8A55\u4FA1\u306E\u7518\u3044\u6388\u696D\u304C\u9AD8\u304F\u8A55\u4FA1\u3055\u308C\u305F\u308A\u3001\u4EBA\u6C17\u53D6\u308A\u306B\u8D70\u308B\u6559\u5E2B\u304C\u51FA\u305F\u308A\u3057\u3001\u6210\u7E3E\u306E\u5B89\u58F2\u308A\u3084\u5927\u5B66\u6559\u5E2B\u306E\u30EC\u30D9\u30EB\u30C0\u30A6\u30F3\u3068\u3044\u3046\u5F0A\u5BB3\u3092\u3082\u305F\u3089\u3059\u6050\u308C\u304C\u3042\u308B\u3001\u306A\u3069\u306E\u53CD\u7701\u610F\u898B\u3082\u3042\u308B." + +@Suppress("MaxLineLength") private const val LONG_KR_SENTENCE = "\uC800\uB294 \uC624\uB298 \uC544\uCE68\uC5D0 \uCE74\uD398\uC5D0\uC11C \uCE5C\uAD6C\uB791 \uD55C\uAD6D\uC5B4 \uACF5\uBD80\uB97C \uD558\uACE0 \uB098\uC11C \uB3C4\uC11C\uAD00\uC5D0 \uAC08 \uAC70\uC608\uC694." @@ -66,7 +67,8 @@ fun UiScope.msdfFontsSection(onInfo: (String) -> Unit) { }) { text("MSDF Fonts") text( - "All DSGL DrawText commands go through MSDF/MTSDF rendering. Switch font/size/color/opacity and verify wrapping.", + "All DSGL DrawText commands go through MSDF/MTSDF rendering. Switch " + + "font/size/color/opacity and verify wrapping.", { style = { color = DEMO_MUTED } }, ) text("DREAMFINITY", { style = { fontId = "telegrafico" } }) @@ -177,11 +179,17 @@ fun UiScope.msdfFontsSection(onInfo: (String) -> Unit) { ) text( - "fontId=${selectedFont.fontId} source=${selectedFont.source.name.lowercase()} fontSize=$fontSize opacity=$textOpacity panelWidth=$panelWidthPercent% formatting=${formattingMode.name.lowercase()} guides=$msdfShowBaselineGuides", + "fontId=${selectedFont.fontId} " + + "source=${selectedFont.source.name.lowercase()} " + + "fontSize=$fontSize " + + "opacity=$textOpacity " + + "panelWidth=$panelWidthPercent% " + + "formatting=${formattingMode.name.lowercase()} guides=$msdfShowBaselineGuides", { style = { this.color = DEMO_MUTED } }, ) text( - "Drop external font packages into /dsgl/fonts//.ttf + -meta.json + -mtsdf.png and restart.", + "Drop external font packages into /dsgl/fonts//.ttf + " + + "-meta.json + -mtsdf.png and restart.", { style = { color = DEMO_MUTED diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/OverflowScrollSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/OverflowScrollSection.kt index aa2aab6..8bcc1e1 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/OverflowScrollSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/OverflowScrollSection.kt @@ -262,7 +262,10 @@ private fun UiScope.overflowDemoCard( text(title) text(note, { style = { color = DEMO_MUTED } }) text( - "viewport=${viewportWidth}x$viewportHeight content=${contentWidth}x$contentHeight overflow-x=${overflowX.label()} overflow-y=${overflowY.label()}", + "viewport=${viewportWidth}x$viewportHeight " + + "content=${contentWidth}x$contentHeight " + + "overflow-x=${overflowX.label()} " + + "overflow-y=${overflowY.label()}", { style = { color = DEMO_MUTED } }, ) diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutSection.kt index c1ac119..708eb17 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutSection.kt @@ -552,7 +552,8 @@ fun UiScope.positionedLayoutSection(viewportWidthPx: Int) { } }) { text( - "H. Sticky: in-flow slot + visual stick with per-axis nearest scroll container and direct-parent clamp.", + "H. Sticky: in-flow slot + visual stick with per-axis nearest scroll " + + "container and direct-parent clamp.", ) text( "Inspector target key: positioned.sticky.xy.target", diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/TextWrapSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/TextWrapSection.kt index 474da41..8e3562c 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/TextWrapSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/TextWrapSection.kt @@ -1,18 +1,17 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections -import org.dreamfinity.dsgl.core.dom.elements.InputType +import org.dreamfinity.dsgl.core.dom.elements.* import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.hooks.useState -import org.dreamfinity.dsgl.core.style.Display -import org.dreamfinity.dsgl.core.style.FlexDirection -import org.dreamfinity.dsgl.core.style.TextWrap +import org.dreamfinity.dsgl.core.style.* import org.dreamfinity.dsgl.mcForge1710.demo.support.DEMO_MUTED private const val WRAP_SAMPLE_TEXT = "This sentence demonstrates style.textWrap on text and button labels inside a fixed-width panel." private const val WRAP_SAMPLE_WORD = "long_unbroken_word_to_force_hard_break_1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ" private const val WRAP_TEXTAREA_SAMPLE = - "Textarea sample: long_unbroken_word_to_force_hard_break_1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ\nSecond line with spaces for normal wrapping." + "Textarea sample: long_unbroken_word_to_force_hard_break_1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ\n" + + "Second line with spaces for normal wrapping." fun UiScope.textWrapSection(onInfo: (String) -> Unit) { val minWidth = 96 diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/EventFormatting.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/EventFormatting.kt index 15b7ad2..90e466a 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/EventFormatting.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/EventFormatting.kt @@ -26,21 +26,27 @@ fun formatEventLine(hookName: String, event: Event, note: String? = null): Strin is FocusLoseEvent -> "next=${event.nextTargetKey ?: "none"}" is InputEvent -> "value=${event.value} parsed=${event.parsedValue ?: "null"}" is ValueChangedEvent -> "value=${event.value} parsed=${event.parsedValue ?: "null"}" - is DragStartEvent -> "source=${event.sourceKey ?: "none"} types=${event.dataTransfer.types.joinToString( - ",", - )}" + is DragStartEvent -> + "source=${event.sourceKey ?: "none"} types=${ + event.dataTransfer.types.joinToString( + ",", + ) + }" + is DragEvent -> { val effect = event.dataTransfer.dropEffect.name .lowercase() "source=${event.sourceKey ?: "none"} effect=$effect" } + is DragEndEvent -> { val effect = event.finalDropEffect.name .lowercase() "drop=${event.didDrop} effect=$effect target=${event.dropTargetKey ?: "none"}" } + is DragEnterEvent -> "source=${event.sourceKey ?: "none"}" is DragOverEvent -> { val effect = @@ -49,13 +55,19 @@ fun formatEventLine(hookName: String, event: Event, note: String? = null): Strin val accepted = event.dropAccepted || event.cancelled "source=${event.sourceKey ?: "none"} effect=$effect accepted=$accepted" } + is DragLeaveEvent -> "source=${event.sourceKey ?: "none"}" is DropEvent -> "source=${event.sourceKey ?: "none"} types=${event.dataTransfer.types.joinToString(",")}" else -> "" } val notePart = if (note.isNullOrBlank()) "" else " note=$note" val raw = - "$hookName ${event.type.name} target=$targetKey $coords $payload shift=${KeyModifiers.shiftDown} ctrl=${KeyModifiers.controlDown} meta=${KeyModifiers.metaDown} shortcut=${KeyModifiers.shortcutDown}$notePart" + "$hookName ${event.type.name} " + + "target=$targetKey $coords $payload " + + "shift=${KeyModifiers.shiftDown} " + + "ctrl=${KeyModifiers.controlDown} " + + "meta=${KeyModifiers.metaDown} " + + "shortcut=${KeyModifiers.shortcutDown}$notePart" return truncateForPanel(raw, 118) } diff --git a/build-logic/src/main/kotlin/dsgl-static-analysis.conventions.gradle.kts b/build-logic/src/main/kotlin/dsgl-static-analysis.conventions.gradle.kts index 07d4007..714505f 100644 --- a/build-logic/src/main/kotlin/dsgl-static-analysis.conventions.gradle.kts +++ b/build-logic/src/main/kotlin/dsgl-static-analysis.conventions.gradle.kts @@ -2,19 +2,19 @@ plugins { id("io.gitlab.arturbosch.detekt") } -val detektBaselineFile = rootProject.file("config/detekt/baseline-${project.name}.xml") +//val detektBaselineFile = rootProject.file("config/detekt/baseline-${project.name}.xml") detekt { buildUponDefaultConfig = true allRules = false config.setFrom(rootProject.files("config/detekt/detekt.yml")) - baseline = detektBaselineFile +// baseline = detektBaselineFile parallel = true } tasks.withType().configureEach { jvmTarget = "1.8" - baseline.set(detektBaselineFile) +// baseline.set(detektBaselineFile) reports { html.required.set(true) sarif.required.set(true) @@ -25,7 +25,7 @@ tasks.withType().configureEach { } tasks.withType().configureEach { - baseline.set(detektBaselineFile) +// baseline.set(detektBaselineFile) } tasks.withType().configureEach { diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index 39c0b40..d97ee2a 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -641,6 +641,8 @@ style: MaxLineLength: active: true maxLineLength: 120 + excludes: + - "**/test/**" excludePackageStatements: true excludeImportStatements: true excludeCommentStatements: true diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngine.kt index 6f090b6..af1cc3e 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngine.kt @@ -1,12 +1,9 @@ package org.dreamfinity.dsgl.core.contextmenu -import org.dreamfinity.dsgl.core.dom.layout.Rect -import org.dreamfinity.dsgl.core.dom.layout.Size -import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext -import org.dreamfinity.dsgl.core.event.KeyCodes -import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.dom.layout.* +import org.dreamfinity.dsgl.core.event.* import org.dreamfinity.dsgl.core.render.RenderCommand -import org.dreamfinity.dsgl.core.style.StyleEngine +import org.dreamfinity.dsgl.core.style.* class ContextMenuEngine( private val clock: ContextMenuClock = SystemContextMenuClock, @@ -220,30 +217,32 @@ class ContextMenuEngine( ) val isHovered = index == level.hoveredIndex val isSelected = index == level.selectedIndex - if (isHovered) { - out += - RenderCommand.DrawRect( - itemRect.x, - itemRect.y, - itemRect.width, - itemRect.height, - style.itemHoverBackgroundColor, - ) - } else if (isSelected) { - out += - RenderCommand.DrawRect( - itemRect.x, - itemRect.y, - itemRect.width, - itemRect.height, - style.itemSelectedBackgroundColor, - ) - } + val nextCommand = + when { + isHovered -> + RenderCommand.DrawRect( + itemRect.x, + itemRect.y, + itemRect.width, + itemRect.height, + style.itemHoverBackgroundColor, + ) + + isSelected -> + RenderCommand.DrawRect( + itemRect.x, + itemRect.y, + itemRect.width, + itemRect.height, + style.itemSelectedBackgroundColor, + ) + else -> null + } + nextCommand?.let { out += it } val textY = itemRect.y + ((itemRect.height - fontHeight).coerceAtLeast(0) / 2) val baseX = itemRect.x + style.rowPaddingX - val indicatorX = baseX - val labelX = indicatorX + measurement.indicatorWidth + style.contentSpacing + val labelX = baseX + measurement.indicatorWidth + style.contentSpacing val textColor = if (snapshot.enabled) style.itemTextColor else style.disabledTextColor val indicatorText = @@ -257,7 +256,7 @@ class ContextMenuEngine( out += RenderCommand.DrawText( text = indicatorText, - x = indicatorX, + x = baseX, y = textY, color = indicatorColor, fontId = fontId, @@ -279,6 +278,7 @@ class ContextMenuEngine( when { snapshot.kind == ContextMenuMeasurementCache.KIND_SUBMENU && snapshot.hint.isNullOrEmpty() -> ContextMenuGlyphs.SUBMENU_ARROW + else -> snapshot.hint } if (!hintText.isNullOrEmpty()) { From 14b32f2cd8841d86cbac90e599c17787fc048eb9 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sat, 25 Apr 2026 18:41:31 +0300 Subject: [PATCH 37/78] adding pre-commit hook for detekt, adding baselines for all modules (:core, :adapter:mc-forge-1-7-10 and it's demo); --- .githooks/pre-commit | 20 +- .../mc-forge-1-7-10/demo/detekt-baseline.xml | 207 ++++++++ adapters/mc-forge-1-7-10/detekt-baseline.xml | 70 +++ core/detekt-baseline.xml | 461 ++++++++++++++++++ 4 files changed, 755 insertions(+), 3 deletions(-) create mode 100644 adapters/mc-forge-1-7-10/demo/detekt-baseline.xml create mode 100644 adapters/mc-forge-1-7-10/detekt-baseline.xml create mode 100644 core/detekt-baseline.xml diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 39361e9..f6dc4bb 100644 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,7 +1,7 @@ #!/bin/sh -# Runs ktlintFormat on staged Kotlin files before commit. -# Auto-correctable violations are fixed and re-staged automatically. -# Non-auto-correctable violations abort the commit. +# Runs ktlintFormat and detektAll on staged Kotlin files before commit. +# ktlint auto-correctable violations are fixed and re-staged automatically. +# Non-auto-correctable ktlint violations or detekt violations abort the commit. exec 1>&2 @@ -29,4 +29,18 @@ if [ $STATUS -ne 0 ]; then fi echo "[pre-commit] ktlintFormat passed." + +echo "[pre-commit] Running detektAll..." + +./gradlew detektAll +STATUS=$? + +if [ $STATUS -ne 0 ]; then + echo "" + echo "[pre-commit] detektAll found violations." + echo "[pre-commit] Fix them manually or update the baseline, then re-run git commit." + exit 1 +fi + +echo "[pre-commit] detektAll passed." exit 0 \ No newline at end of file diff --git a/adapters/mc-forge-1-7-10/demo/detekt-baseline.xml b/adapters/mc-forge-1-7-10/demo/detekt-baseline.xml new file mode 100644 index 0000000..caec4a7 --- /dev/null +++ b/adapters/mc-forge-1-7-10/demo/detekt-baseline.xml @@ -0,0 +1,207 @@ + + + + + CyclomaticComplexMethod:AnimationsSection.kt$fun UiScope.animationsSection(onInfo: (String) -> Unit) + CyclomaticComplexMethod:CapabilityChecklistCatalog.kt$CapabilityChecklistCatalog$fun capabilitiesForSection(section: DemoSection): Set<CapabilityId> + CyclomaticComplexMethod:ContextMenuSection.kt$fun UiScope.contextMenuSection(onInfo: (String) -> Unit) + CyclomaticComplexMethod:CssCascadeSection.kt$fun UiScope.cssCascadeCombinatorsSection(onLogHook: (String, Event, String?) -> Unit) + CyclomaticComplexMethod:DragDropSection.kt$fun UiScope.dragNDropSection( onInfo: (String) -> Unit, onClearLogs: () -> Unit, onLogHook: (String, Event, String?) -> Unit, ) + CyclomaticComplexMethod:EventFormatting.kt$fun formatEventLine(hookName: String, event: Event, note: String? = null): String + CyclomaticComplexMethod:InputsGallerySection.kt$fun UiScope.inputsGallerySection(clippingScrollDemoText: String, onClippingScrollDemoTextChange: (String) -> Unit) + CyclomaticComplexMethod:InteractionsSection.kt$fun UiScope.interactionsSection(onInfo: (String) -> Unit, onLogHook: (String, Event, String?) -> Unit) + CyclomaticComplexMethod:LayoutStyleSection.kt$fun UiScope.layoutStyleSection(onInfo: (String) -> Unit, onLogHook: (String, Event, String?) -> Unit) + CyclomaticComplexMethod:McFeaturesSection.kt$fun UiScope.mcFeaturesSection(props: McFeaturesSection) + CyclomaticComplexMethod:ShowcaseWindow.kt$ShowcaseWindow$override fun render(): DomTree + LargeClass:ShowcaseWindow.kt$ShowcaseWindow : DsglWindow + LongMethod:AnimationsSection.kt$fun UiScope.animationsSection(onInfo: (String) -> Unit) + LongMethod:CapabilityChecklistCatalog.kt$CapabilityChecklistCatalog$fun capabilitiesForSection(section: DemoSection): Set<CapabilityId> + LongMethod:ColorPickerSection.kt$fun UiScope.colorPickerSection() + LongMethod:ContextMenuSection.kt$fun UiScope.contextMenuEntryTile(file: ContextMenuDemoFile) + LongMethod:ContextMenuSection.kt$fun UiScope.contextMenuSection(onInfo: (String) -> Unit) + LongMethod:CssCascadeSection.kt$fun UiScope.cssCascadeCombinatorsSection(onLogHook: (String, Event, String?) -> Unit) + LongMethod:DisplaySection.kt$fun UiScope.displaySection(onInfo: (String) -> Unit, onLogHook: (String, Event, String?) -> Unit) + LongMethod:DragDropSection.kt$fun UiScope.dragNDropSection( onInfo: (String) -> Unit, onClearLogs: () -> Unit, onLogHook: (String, Event, String?) -> Unit, ) + LongMethod:DragDropSection.kt$private fun UiScope.originalModeReorder( state: DndSectionState, sourceKey: Any?, onStart: (DndDemoItem, DragStartEvent) -> Unit, onDrag: (DragEvent) -> Unit, onEnd: (DragEndEvent) -> Unit, onLaneOver: (DragOverEvent) -> Unit, onLaneLeave: () -> Unit, onLaneDrop: (DropEvent) -> Unit, onCardOver: (String, Boolean, DragOverEvent) -> Unit, onCardDrop: (String, Boolean, DropEvent) -> Unit, laneIndicatorForCard: (String, Any?) -> DndLaneIndicator, shouldShowLaneAppendGap: (Any?) -> Boolean, ) + LongMethod:FocusRebuildSection.kt$fun UiScope.focusRebuildSection( renderPasses: Int, onManualInvalidate: (String) -> Unit, onInfo: (String) -> Unit, onLogHook: (String, Event, String?) -> Unit, ) + LongMethod:HooksSection.kt$private fun UiScope.overviewUseRef(onInfo: (String) -> Unit, onLogHook: (String, Event, String?) -> Unit) + LongMethod:InputEventsSection.kt$fun UiScope.inputEventsSection(onLogHook: (String, Event, String?) -> Unit) + LongMethod:InputsGallerySection.kt$fun UiScope.inputsGallerySection(clippingScrollDemoText: String, onClippingScrollDemoTextChange: (String) -> Unit) + LongMethod:InspectorSection.kt$fun UiScope.inspectorSection(onInfo: (String) -> Unit) + LongMethod:InteractionsSection.kt$fun UiScope.interactionsSection(onInfo: (String) -> Unit, onLogHook: (String, Event, String?) -> Unit) + LongMethod:LayoutDebugSection.kt$fun UiScope.layoutDebugSection(onClearLogs: () -> Unit, onInfo: (String) -> Unit) + LongMethod:LayoutStyleSection.kt$fun UiScope.layoutStyleSection(onInfo: (String) -> Unit, onLogHook: (String, Event, String?) -> Unit) + LongMethod:McFeaturesSection.kt$fun UiScope.mcFeaturesSection(props: McFeaturesSection) + LongMethod:MsdfFontsSection.kt$fun UiScope.msdfFontsSection(onInfo: (String) -> Unit) + LongMethod:OverflowScrollSection.kt$fun UiScope.overflowScrollSection(onInfo: (String) -> Unit) + LongMethod:OverflowScrollSection.kt$private fun UiScope.overflowDemoCard( title: String, note: String, viewportWidth: Int, viewportHeight: Int, contentWidth: Int, contentHeight: Int, overflowX: Overflow, overflowY: Overflow, keyPrefix: String, onVisibleClick: () -> Unit, onEdgeClick: () -> Unit, ) + LongMethod:PositionedLayoutSection.kt$fun UiScope.positionedLayoutSection(viewportWidthPx: Int) + LongMethod:PositionedLayoutSection.kt$private fun UiScope.controls(props: ControlsProps) + LongMethod:PositionedLayoutSection.kt$private fun UiScope.stickyHorizontalGroup() + LongMethod:PositionedLayoutSection.kt$private fun UiScope.stickyVerticalGroup( onSetLastHover: (String) -> Unit, onSetLastClick: (String) -> Unit, stickyTopClicks: Int, onStickyTopClick: () -> Unit, ) + LongMethod:ShowcaseWindow.kt$ShowcaseWindow$override fun render(): DomTree + LongMethod:ShowcaseWindow.kt$ShowcaseWindow$private fun prepareCascadeStylesheet() + LongMethod:ShowcaseWindow.kt$ShowcaseWindow$private fun prepareDemoStylesheet() + LongMethod:StylesheetsSection.kt$fun UiScope.stylesheetsSection( onLogHook: (String, Event, String?) -> Unit, onInfo: (String) -> Unit, loadStylesheetText: () -> String, saveStylesheetText: (String) -> Unit, onReloadStylesheets: () -> Unit, ) + LongMethod:TextEditingSection.kt$fun UiScope.textEditingSection(onLogHook: (String, Event, String?) -> Unit) + LongMethod:TextWrapSection.kt$fun UiScope.textWrapSection(onInfo: (String) -> Unit) + LongParameterList:DragDropSection.kt$( state: DndSectionState, boxKey: Any, title: String, boxId: String, color: Int, cards: List<DndDemoItem>, onStart: (DndDemoItem, DragStartEvent) -> Unit, onDrag: (DragEvent) -> Unit, onEnd: (DragEndEvent) -> Unit, onBoxOver: (String, DragOverEvent) -> Unit, onBoxDrop: (String, DropEvent) -> Unit, onHoverZone: (String) -> Unit, onLogHook: (String, Event, String?) -> Unit, ) + LongParameterList:DragDropSection.kt$( state: DndSectionState, sourceKey: Any?, onStart: (DndDemoItem, DragStartEvent) -> Unit, onDrag: (DragEvent) -> Unit, onEnd: (DragEndEvent) -> Unit, onLaneOver: (DragOverEvent) -> Unit, onLaneLeave: () -> Unit, onLaneDrop: (DropEvent) -> Unit, onCardOver: (String, Boolean, DragOverEvent) -> Unit, onCardDrop: (String, Boolean, DropEvent) -> Unit, laneIndicatorForCard: (String, Any?) -> DndLaneIndicator, shouldShowLaneAppendGap: (Any?) -> Boolean, ) + LongParameterList:OverflowScrollSection.kt$( title: String, note: String, viewportWidth: Int, viewportHeight: Int, contentWidth: Int, contentHeight: Int, overflowX: Overflow, overflowY: Overflow, keyPrefix: String, onVisibleClick: () -> Unit, onEdgeClick: () -> Unit, ) + MagicNumber:AnimationsSection.kt$0.5f + MagicNumber:AnimationsSection.kt$0.65f + MagicNumber:AnimationsSection.kt$1.08f + MagicNumber:AnimationsSection.kt$12f + MagicNumber:AnimationsSection.kt$1400L + MagicNumber:AnimationsSection.kt$17L + MagicNumber:AnimationsSection.kt$200 + MagicNumber:AnimationsSection.kt$20f + MagicNumber:AnimationsSection.kt$220 + MagicNumber:AnimationsSection.kt$260 + MagicNumber:AnimationsSection.kt$6000 + MagicNumber:AnimationsSection.kt$67L + MagicNumber:AnimationsSection.kt$83L + MagicNumber:AnimationsSection.kt$8f + MagicNumber:ColorPickerSection.kt$0.19f + MagicNumber:ColorPickerSection.kt$0.28f + MagicNumber:ColorPickerSection.kt$0.29f + MagicNumber:ColorPickerSection.kt$0.31f + MagicNumber:ColorPickerSection.kt$0.41f + MagicNumber:ColorPickerSection.kt$0.45f + MagicNumber:ColorPickerSection.kt$0.46f + MagicNumber:ColorPickerSection.kt$0.52f + MagicNumber:ColorPickerSection.kt$0.73f + MagicNumber:ColorPickerSection.kt$0.82f + MagicNumber:ColorPickerSection.kt$0.88f + MagicNumber:ColorPickerSection.kt$0.8f + MagicNumber:ColorPickerSection.kt$0.91f + MagicNumber:ColorPickerSection.kt$0.96f + MagicNumber:ColorPickerSection.kt$0.9f + MagicNumber:ContextMenuSection.kt$0x66000000 + MagicNumber:ContextMenuSection.kt$11L + MagicNumber:ContextMenuSection.kt$12L + MagicNumber:ContextMenuSection.kt$13L + MagicNumber:ContextMenuSection.kt$14L + MagicNumber:ContextMenuSection.kt$15L + MagicNumber:ContextMenuSection.kt$17L + MagicNumber:ContextMenuSection.kt$18 + MagicNumber:ContextMenuSection.kt$18L + MagicNumber:ContextMenuSection.kt$19 + MagicNumber:ContextMenuSection.kt$24 + MagicNumber:ContextMenuSection.kt$5 + MagicNumber:ContextMenuSection.kt$5L + MagicNumber:ContextMenuSection.kt$6 + MagicNumber:ContextMenuSection.kt$6L + MagicNumber:ContextMenuSection.kt$7 + MagicNumber:ContextMenuSection.kt$7L + MagicNumber:ContextMenuSection.kt$9L + MagicNumber:DisplaySection.kt$0x00040401 + MagicNumber:DisplaySection.kt$0x000A0A00 + MagicNumber:DisplaySection.kt$0xFF3A4B60 + MagicNumber:DisplaySection.kt$0xFF3D5873 + MagicNumber:DisplaySection.kt$132L + MagicNumber:DisplaySection.kt$14 + MagicNumber:DisplaySection.kt$20 + MagicNumber:DisplaySection.kt$26 + MagicNumber:DisplaySection.kt$320 + MagicNumber:DisplaySection.kt$6 + MagicNumber:DisplaySection.kt$96 + MagicNumber:DragDropSection.kt$0x2219222B + MagicNumber:DragDropSection.kt$0x2A9EC4E3 + MagicNumber:DragDropSection.kt$0x44333F4D + MagicNumber:DragDropSection.kt$0x44405058 + MagicNumber:DragDropSection.kt$0x553A4452 + MagicNumber:DragDropSection.kt$0xFF + MagicNumber:DragDropSection.kt$12 + MagicNumber:DragDropSection.kt$24 + MagicNumber:DragDropSection.kt$4.0 + MagicNumber:DragDropSection.kt$5 + MagicNumber:DragDropSection.kt$96.0 + MagicNumber:DragNDropWindow.kt$32 + MagicNumber:EventFormatting.kt$118 + MagicNumber:EventFormatting.kt$32 + MagicNumber:FocusRebuildSection.kt$127 + MagicNumber:HooksSection.kt$10007 + MagicNumber:HooksSection.kt$5 + MagicNumber:HooksSection.kt$5000 + MagicNumber:InputEventsSection.kt$35L + MagicNumber:InputsGallerySection.kt$35L + MagicNumber:InputsGallerySection.kt$9 + MagicNumber:InspectorSection.kt$0x22496699 + MagicNumber:InspectorSection.kt$0x3338424F + MagicNumber:InspectorSection.kt$0x665A9CE0 + MagicNumber:InspectorSection.kt$6 + MagicNumber:InteractionsSection.kt$6 + MagicNumber:LayoutDebugSection.kt$148L + MagicNumber:LayoutDebugSection.kt$320 + MagicNumber:LayoutDebugSection.kt$96 + MagicNumber:LayoutStyleSection.kt$148 + MagicNumber:LayoutStyleSection.kt$24 + MagicNumber:LayoutStyleSection.kt$26 + MagicNumber:LayoutStyleSection.kt$92 + MagicNumber:McFeaturesSection.kt$11.0 + MagicNumber:McFeaturesSection.kt$160.0 + MagicNumber:McFeaturesSection.kt$18 + MagicNumber:McFeaturesSection.kt$20 + MagicNumber:McFeaturesSection.kt$360.0 + MagicNumber:McFeaturesSection.kt$360L + MagicNumber:McFeaturesSection.kt$5f + MagicNumber:McFeaturesSection.kt$89.0 + MagicNumber:McFeaturesSection.kt$89L + MagicNumber:MsdfFontsSection.kt$15L + MagicNumber:MsdfFontsSection.kt$48 + MagicNumber:MsdfFontsSection.kt$6 + MagicNumber:MsdfFontsSection.kt$9L + MagicNumber:OverflowScrollSection.kt$118L + MagicNumber:OverflowScrollSection.kt$126L + MagicNumber:OverflowScrollSection.kt$132L + MagicNumber:OverflowScrollSection.kt$180 + MagicNumber:OverflowScrollSection.kt$260 + MagicNumber:OverflowScrollSection.kt$420 + MagicNumber:OverflowScrollSection.kt$48 + MagicNumber:OverflowScrollSection.kt$56 + MagicNumber:OverflowScrollSection.kt$60 + MagicNumber:OverflowScrollSection.kt$76L + MagicNumber:OverflowScrollSection.kt$88 + MagicNumber:PositionedLayoutSection.kt$100 + MagicNumber:PositionedLayoutSection.kt$104 + MagicNumber:PositionedLayoutSection.kt$112 + MagicNumber:PositionedLayoutSection.kt$12 + MagicNumber:PositionedLayoutSection.kt$14 + MagicNumber:PositionedLayoutSection.kt$14L + MagicNumber:PositionedLayoutSection.kt$18 + MagicNumber:PositionedLayoutSection.kt$18L + MagicNumber:PositionedLayoutSection.kt$236 + MagicNumber:PositionedLayoutSection.kt$24 + MagicNumber:PositionedLayoutSection.kt$24L + MagicNumber:PositionedLayoutSection.kt$26L + MagicNumber:PositionedLayoutSection.kt$28 + MagicNumber:PositionedLayoutSection.kt$40 + MagicNumber:PositionedLayoutSection.kt$5 + MagicNumber:PositionedLayoutSection.kt$58 + MagicNumber:PositionedLayoutSection.kt$6 + MagicNumber:PositionedLayoutSection.kt$7 + MagicNumber:PositionedLayoutSection.kt$72 + MagicNumber:PositionedLayoutSection.kt$999 + MagicNumber:ShowcaseWindow.kt$ShowcaseWindow$0.35f + MagicNumber:ShowcaseWindow.kt$ShowcaseWindow$1.08f + MagicNumber:ShowcaseWindow.kt$ShowcaseWindow$12 + MagicNumber:ShowcaseWindow.kt$ShowcaseWindow$13 + MagicNumber:ShowcaseWindow.kt$ShowcaseWindow$14 + MagicNumber:ShowcaseWindow.kt$ShowcaseWindow$15 + MagicNumber:ShowcaseWindow.kt$ShowcaseWindow$17 + MagicNumber:ShowcaseWindow.kt$ShowcaseWindow$18 + MagicNumber:ShowcaseWindow.kt$ShowcaseWindow$180f + MagicNumber:ShowcaseWindow.kt$ShowcaseWindow$360f + MagicNumber:ShowcaseWindow.kt$ShowcaseWindow$40 + MagicNumber:ShowcaseWindow.kt$ShowcaseWindow$5 + MagicNumber:ShowcaseWindow.kt$ShowcaseWindow$50f + MagicNumber:ShowcaseWindow.kt$ShowcaseWindow$7 + MagicNumber:ShowcaseWindow.kt$ShowcaseWindow$9 + MagicNumber:TextWrapSection.kt$176L + MagicNumber:TextWrapSection.kt$320 + MagicNumber:TextWrapSection.kt$96 + ReturnCount:ContextMenuSection.kt$fun contextMenuCanDropIntoDirectory(entryId: String, destinationDirectoryId: String): Boolean + TooManyFunctions:ShowcaseWindow.kt$ShowcaseWindow : DsglWindow + + diff --git a/adapters/mc-forge-1-7-10/detekt-baseline.xml b/adapters/mc-forge-1-7-10/detekt-baseline.xml new file mode 100644 index 0000000..3ae6052 --- /dev/null +++ b/adapters/mc-forge-1-7-10/detekt-baseline.xml @@ -0,0 +1,70 @@ + + + + + ComplexCondition:DsglScreenHost.kt$DsglScreenHost$!inspectorBlocks && !contextMenuBlocks && !selectBlocks && !systemSelectBlocks && !colorPickerBlocks + ComplexCondition:DsglScreenHost.kt$DsglScreenHost$inspectorBlocks || contextMenuBlocks || selectBlocks || systemSelectBlocks || colorPickerBlocks + ComplexCondition:DsglScreenHost.kt$DsglScreenHost$lastWidth > 0 && lastHeight > 0 && (rootBounds.width <= 0 || rootBounds.height <= 0) + ComplexCondition:Mc1710UiAdapter.kt$Mc1710UiAdapter$x < 0 || y < 0 || x >= viewport.width || y >= viewport.height + ComplexCondition:MsdfTextRenderer.kt$MsdfTextRenderer.SegmentBuffer$last >= 0 && kind[last] == segmentKind && color[last] == segmentColor && kotlin.math.abs(endX[last] - segmentStartX) <= 0.51f && kotlin.math.abs(y[last] - segmentY) <= 0.51f && kotlin.math.abs(thickness[last] - segmentThickness) <= 0.1f + CyclomaticComplexMethod:DsglScreenHost.kt$DsglScreenHost$private fun consumeApplicationOverlayPointerEvent( mouseX: Int, mouseY: Int, dWheel: Int, mouseButton: Int, mappedButton: MouseButton?, buttonPressed: Boolean, ): Boolean + CyclomaticComplexMethod:DsglScreenHost.kt$DsglScreenHost$private fun consumeSystemOverlayPointerEvent( mouseX: Int, mouseY: Int, dWheel: Int, mouseButton: Int, mappedButton: MouseButton?, buttonPressed: Boolean, ): Boolean + CyclomaticComplexMethod:DsglScreenHost.kt$DsglScreenHost$private fun handleKeyboardKeyDown( keyCode: Int, keyChar: Char, inspectorMouseX: Int, inspectorMouseY: Int, ): Boolean + CyclomaticComplexMethod:DsglScreenHost.kt$DsglScreenHost$private fun updateFrameInteractionState( tree: DomTree, dtSeconds: Double, dsglMouseX: Int, dsglMouseY: Int, appOverlayInputEnabled: Boolean, systemOverlayInputEnabled: Boolean, inspectorBlocks: Boolean, ) + CyclomaticComplexMethod:Mc1710UiAdapter.kt$Mc1710UiAdapter$@Suppress("LoopWithTooManyJumpStatements") override fun paint(commands: List<RenderCommand>) + CyclomaticComplexMethod:MsdfTextRenderer.kt$MsdfTextRenderer$fun draw(command: RenderCommand.DrawText, opacityMultiplier: Float) + CyclomaticComplexMethod:MsdfTextRenderer.kt$MsdfTextRenderer$private fun drawDecorationSegments(segments: SegmentBuffer, includeDebug: Boolean) + LargeClass:DsglScreenHost.kt$DsglScreenHost : GuiScreenDsglWindowHost + LargeClass:Mc1710UiAdapter.kt$Mc1710UiAdapter : UiMeasureContext + LargeClass:MsdfTextRenderer.kt$MsdfTextRenderer + LongMethod:Mc1710UiAdapter.kt$Mc1710UiAdapter$@Suppress("LoopWithTooManyJumpStatements") override fun paint(commands: List<RenderCommand>) + LongMethod:Mc1710UiAdapter.kt$Mc1710UiAdapter$private fun captureScreenRegion(command: RenderCommand.CaptureScreenRegion, viewport: Viewport) + LongMethod:MsdfTextRenderer.kt$MsdfTextRenderer$fun draw(command: RenderCommand.DrawText, opacityMultiplier: Float) + MagicNumber:DsglScreenHost.kt$DsglScreenHost$0.25 + MagicNumber:DsglScreenHost.kt$DsglScreenHost$1_000_000_000.0 + MagicNumber:DsglScreenHost.kt$DsglScreenHost$2_000L + MagicNumber:DsglScreenHost.kt$DsglScreenHost$60.0 + MagicNumber:Mc1710UiAdapter.kt$Mc1710UiAdapter$0.1f + MagicNumber:Mc1710UiAdapter.kt$Mc1710UiAdapter$0.5f + MagicNumber:Mc1710UiAdapter.kt$Mc1710UiAdapter$0.999f + MagicNumber:Mc1710UiAdapter.kt$Mc1710UiAdapter$0x00FF_FFFF + MagicNumber:Mc1710UiAdapter.kt$Mc1710UiAdapter$0xFF + MagicNumber:Mc1710UiAdapter.kt$Mc1710UiAdapter$0xFFFF_FFFFL + MagicNumber:Mc1710UiAdapter.kt$Mc1710UiAdapter$1000.0 + MagicNumber:Mc1710UiAdapter.kt$Mc1710UiAdapter$120f + MagicNumber:Mc1710UiAdapter.kt$Mc1710UiAdapter$15 + MagicNumber:Mc1710UiAdapter.kt$Mc1710UiAdapter$180f + MagicNumber:Mc1710UiAdapter.kt$Mc1710UiAdapter$24 + MagicNumber:Mc1710UiAdapter.kt$Mc1710UiAdapter$240f + MagicNumber:Mc1710UiAdapter.kt$Mc1710UiAdapter$300f + MagicNumber:Mc1710UiAdapter.kt$Mc1710UiAdapter$31 + MagicNumber:Mc1710UiAdapter.kt$Mc1710UiAdapter$32 + MagicNumber:Mc1710UiAdapter.kt$Mc1710UiAdapter$360f + MagicNumber:Mc1710UiAdapter.kt$Mc1710UiAdapter$3_000L + MagicNumber:Mc1710UiAdapter.kt$Mc1710UiAdapter$4096 + MagicNumber:Mc1710UiAdapter.kt$Mc1710UiAdapter$6 + MagicNumber:Mc1710UiAdapter.kt$Mc1710UiAdapter$60f + MagicNumber:Mc1710UiAdapter.kt$Mc1710UiAdapter$8.0 + MagicNumber:MsdfTextRenderer.kt$MsdfTextRenderer$0.5f + MagicNumber:MsdfTextRenderer.kt$MsdfTextRenderer$0.999f + MagicNumber:MsdfTextRenderer.kt$MsdfTextRenderer$0x00FF_FFFF + MagicNumber:MsdfTextRenderer.kt$MsdfTextRenderer$0xFF + MagicNumber:MsdfTextRenderer.kt$MsdfTextRenderer$1_000L + MagicNumber:MsdfTextRenderer.kt$MsdfTextRenderer$1_000_000_000.0 + MagicNumber:MsdfTextRenderer.kt$MsdfTextRenderer$24 + MagicNumber:MsdfTextRenderer.kt$MsdfTextRenderer$31 + MagicNumber:MsdfTextRenderer.kt$MsdfTextRenderer$32 + MagicNumber:MsdfTextRenderer.kt$MsdfTextRenderer$3_000L + MagicNumber:MsdfTextRenderer.kt$MsdfTextRenderer$4096 + MagicNumber:MsdfTextRenderer.kt$MsdfTextRenderer$64 + MagicNumber:MsdfTextRenderer.kt$MsdfTextRenderer.SegmentBuffer$0.1f + MagicNumber:MsdfTextRenderer.kt$MsdfTextRenderer.SegmentBuffer$0.51f + NestedBlockDepth:Mc1710UiAdapter.kt$Mc1710UiAdapter$@Suppress("LoopWithTooManyJumpStatements") override fun paint(commands: List<RenderCommand>) + NestedBlockDepth:MsdfTextRenderer.kt$MsdfTextRenderer$fun draw(command: RenderCommand.DrawText, opacityMultiplier: Float) + ReturnCount:DsglScreenHost.kt$DsglScreenHost$private fun consumeApplicationOverlayPointerEvent( mouseX: Int, mouseY: Int, dWheel: Int, mouseButton: Int, mappedButton: MouseButton?, buttonPressed: Boolean, ): Boolean + ReturnCount:DsglScreenHost.kt$DsglScreenHost$private fun consumeSystemOverlayPointerEvent( mouseX: Int, mouseY: Int, dWheel: Int, mouseButton: Int, mappedButton: MouseButton?, buttonPressed: Boolean, ): Boolean + TooManyFunctions:DsglScreenHost.kt$DsglScreenHost : GuiScreenDsglWindowHost + TooManyFunctions:Mc1710UiAdapter.kt$Mc1710UiAdapter : UiMeasureContext + TooManyFunctions:MsdfTextRenderer.kt$MsdfTextRenderer + + diff --git a/core/detekt-baseline.xml b/core/detekt-baseline.xml new file mode 100644 index 0000000..7433ad4 --- /dev/null +++ b/core/detekt-baseline.xml @@ -0,0 +1,461 @@ + + + + + ComplexCondition:ColorPickerInlineNode.kt$ColorPickerInlineNode$!force && currentArgb == syncedColorArgb && previousArgb == syncedPreviousArgb && mode == syncedMode && alphaEnabled == syncedAlphaEnabled && closeOnSelect == syncedCloseOnSelect + ComplexCondition:ColorPickerPopupRuntime.kt$ColorPickerPopupEngine$previous.anchorRect != request.anchorRect || previous.width != request.width || previous.style != request.style || previous.state.alphaEnabled != request.state.alphaEnabled + ComplexCondition:DOMNode.kt$DOMNode$border.top <= 0 && border.right <= 0 && border.bottom <= 0 && border.left <= 0 + ComplexCondition:DOMNode.kt$DOMNode$relativeOffsetX == 0 && relativeOffsetY == 0 && stickyOffsetX == 0 && stickyOffsetY == 0 + ComplexCondition:DomTree.kt$DomTree$( !laidOut || styleReport.layoutDirty || scrollInvalidation.layoutDirty || requiresSystemOverlayScrollLayoutFallback ) && lastWidth > 0 && lastHeight > 0 + ComplexCondition:FontRegistry.kt$FontRegistry$end > start && end < text.length && Character.isLowSurrogate(text[end]) && Character.isHighSurrogate(text[end - 1]) + ComplexCondition:FontRegistry.kt$FontRegistry$start > 0 && start < text.length && Character.isLowSurrogate(text[start]) && Character.isHighSurrogate(text[start - 1]) + ComplexCondition:InspectorController.kt$InspectorController$!dragMoved && ( kotlin.math.abs(resized.width - dragStartRect.width) >= 2 || kotlin.math.abs(resized.height - dragStartRect.height) >= 2 || kotlin.math.abs(resized.x - dragStartRect.x) >= 2 || kotlin.math.abs(resized.y - dragStartRect.y) >= 2 ) + ComplexCondition:InspectorController.kt$InspectorController$!hoverDirty && lastHoverMouseX == mouseX && lastHoverMouseY == mouseY && lastHoverLayoutVersion == layoutVersion + ComplexCondition:InspectorController.kt$InspectorController$a.width <= 0 || a.height <= 0 || b.width <= 0 || b.height <= 0 + ComplexCondition:InspectorController.kt$InspectorController$cached != null && cached.key == key && cached.nodeClass == klass && cached.layoutVersion == layoutVersion + ComplexCondition:InspectorController.kt$InspectorController$endedMode == DragMode.MinimizedMove && clickLike && panelState == InspectorPanelState.Minimized && minimizedBounds.contains(mouseX, mouseY) + ComplexCondition:InspectorController.kt$InspectorController$numberText.isEmpty() || numberText == "-" || numberText == "." || numberText == "-." + ComplexCondition:LayerDomInputRouter.kt$LayerDomInputRouter$node.onMouseDown != null || node.onMouseUp != null || node.onMouseClick != null || node.onMouseDrag != null || node.onMouseWheel != null || node.onMouseMove != null || node.onKeyDown != null || node.onKeyUp != null + ComplexCondition:ModalRuntime.kt$ModalRuntime$(needsFocusOnTop || (topMost.trapFocus && focusOutsideTop)) && !FocusManager.requestFocusFirstInSubtree(topDialogKey) + ComplexCondition:SelectNode.kt$SelectNode$!controlled && uncontrolledValue == null && value != null && optionExists(value) + ComplexCondition:SingleLineInputNode.kt$SingleLineInputNode$!showPlaceholder && focused && !styleDisabled && editState.isCaretVisible(caretBlinkPeriodMs) + ComplexCondition:StyleSelector.kt$StyleSelector.Companion$index < token.length && (token[index].isLetterOrDigit() || token[index] == '_' || token[index] == '-') + ComplexCondition:StyleSelector.kt$StyleSelector.Companion$index < trimmed.length && !trimmed[index].isWhitespace() && trimmed[index] != '>' && trimmed[index] != '+' && trimmed[index] != '~' + ComplexCondition:SystemColorPickerCustomSurfaceNodes.kt$ColorFieldSurfaceNode$style != typedTemplate.style || hueDeg != typedTemplate.hueDeg || saturation != typedTemplate.saturation || brightness != typedTemplate.brightness + ComplexCondition:SystemColorPickerCustomSurfaceNodes.kt$ColorFieldSurfaceNode$this.style != style || this.hueDeg != hueDeg || saturation != nextSaturation || brightness != nextBrightness + ComplexCondition:SystemColorPickerCustomSurfaceNodes.kt$EyedropperMagnifierDrawNode$this.columns != nextColumns || this.rows != nextRows || this.magnification != nextMagnification || this.gridEnabled != gridEnabled || this.gridColor != gridColor + ComplexCondition:TextAreaNode.kt$TextAreaNode$!showPlaceholder && focused && !styleDisabled && editState.isCaretVisible(caretBlinkPeriodMs) + ComplexCondition:TextDecorationLayout.kt$TextDecorationLayout$last != null && last.type == quad.type && last.lineIndex == quad.lineIndex && kotlin.math.abs(last.xEnd - quad.xStart) <= 0.51f && kotlin.math.abs(last.y - quad.y) <= 0.51f && kotlin.math.abs(last.thickness - quad.thickness) <= 0.1f && last.color == quad.color + CyclomaticComplexMethod:ColorPickerController.kt$ColorPickerController$fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean + CyclomaticComplexMethod:ColorPickerController.kt$ColorPickerController$fun handleMouseDown( globalX: Int, globalY: Int, button: MouseButton, layout: ColorPickerLayout, ): Boolean + CyclomaticComplexMethod:ColorPickerController.kt$ColorPickerController$private fun applyInputDraftValue(key: String, rawValue: String): Boolean + CyclomaticComplexMethod:ColorTextCodec.kt$ColorTextCodec$private fun parseHsbLike(raw: String): RgbaColor? + CyclomaticComplexMethod:ColorTextCodec.kt$ColorTextCodec$private fun parseRgbLike(raw: String): ParsedColorText? + CyclomaticComplexMethod:ComponentHookRuntime.kt$ComponentHookRuntime$private fun applyEffectCommitBatch(batch: PendingEffectCommitBatch) + CyclomaticComplexMethod:ContainerNode.kt$ContainerNode$private fun measureBlock(ctx: UiMeasureContext, children: List<DOMNode>, wrapWidth: Int?): Size + CyclomaticComplexMethod:ContainerNode.kt$ContainerNode$private fun measureWithConstraint(ctx: UiMeasureContext, constrainedContentWidth: Int?): Size + CyclomaticComplexMethod:ContainerNode.kt$ContainerNode$private fun renderFlex(ctx: UiMeasureContext, children: List<DOMNode>) + CyclomaticComplexMethod:ContainerNode.kt$ContainerNode$private fun renderGrid(ctx: UiMeasureContext, children: List<DOMNode>) + CyclomaticComplexMethod:ContextMenuEngine.kt$ContextMenuEngine$@Suppress("LoopWithTooManyJumpStatements") private fun ensureLayout() + CyclomaticComplexMethod:ContextMenuEngine.kt$ContextMenuEngine$fun appendOverlayCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, out: MutableList<RenderCommand>, ) + CyclomaticComplexMethod:ContextMenuEngine.kt$ContextMenuEngine$fun handleKeyDown(keyCode: Int): Boolean + CyclomaticComplexMethod:ContextMenuMeasurementCache.kt$ContextMenuMeasurementCache$fun measure( menuToken: Long, entries: List<MenuEntry>, style: ContextMenuStyle, fontId: String?, fontSize: Int?, ctx: UiMeasureContext, dpiScale: Float, ): Measurement + CyclomaticComplexMethod:CssLength.kt$fun parseCssLength(raw: String, allowUnitlessZero: Boolean = true): CssLength + CyclomaticComplexMethod:DOMNode.kt$DOMNode$fun scrollContainerState(): ScrollContainerState + CyclomaticComplexMethod:DOMNode.kt$DOMNode$internal fun applyComputedStyle(style: ComputedStyle): NodeStyleApplyResult + CyclomaticComplexMethod:DOMNode.kt$DOMNode$internal fun restoreScrollSessionSnapshot(snapshot: ScrollSessionSnapshot?) + CyclomaticComplexMethod:DOMNode.kt$DOMNode$private fun advanceScrollAnimationAxis( scrollContainer: Boolean, maxScroll: Int, vertical: Boolean, dtSeconds: Double, ): Boolean + CyclomaticComplexMethod:DOMNode.kt$DOMNode$private fun resolveScrollbarResolution( overflowX: Overflow, overflowY: Overflow, contentExtent: Size, baseViewportWidth: Int, baseViewportHeight: Int, ): ScrollbarResolution + CyclomaticComplexMethod:DefaultDndEngine.kt$DefaultDndEngine$private fun shiftCommand(command: RenderCommand, dx: Int, dy: Int): RenderCommand + CyclomaticComplexMethod:DomTree.kt$DomTree$fun paint(ctx: UiMeasureContext, applyStyles: Boolean = true): List<RenderCommand> + CyclomaticComplexMethod:DssParser.kt$DssParser$@Suppress("ThrowsCount") private fun parseDeclarations( sourceName: String, text: String, fromIndex: Int, declarations: StyleDeclarations, rootVars: MutableMap<String, String>, allowVariables: Boolean, warnings: ParseWarnings, ): Int + CyclomaticComplexMethod:EventBus.kt$EventBus$fun post(event: Event) + CyclomaticComplexMethod:FontRegistry.kt$FontRegistry$private fun buildShapingSegments( text: String, startIndex: Int, endIndexExclusive: Int, primary: LoadedMsdfFont, fallback: LoadedMsdfFont?, ): List<MutableShapingSegment> + CyclomaticComplexMethod:InlineLayoutTests.kt$InlineLayoutTests$private fun createDisplayInlineScenario(rootWidth: Int, inlineContentWidth: Int): Pair<ContainerNode, ContainerNode> + CyclomaticComplexMethod:InspectorController.kt$InspectorController$fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean + CyclomaticComplexMethod:InspectorController.kt$InspectorController$private fun applyStyleEdit( selected: DOMNode, property: StyleProperty, operation: EditOperation, step: Float, payload: String?, actionBounds: Rect, ) + CyclomaticComplexMethod:InspectorController.kt$InspectorController$private fun buildExpandedDomSnapshot( root: DOMNode, viewportWidth: Int, viewportHeight: Int, ): InspectorDomSnapshot + CyclomaticComplexMethod:InspectorController.kt$InspectorController$private fun literalFromComputed(style: ComputedStyle, property: StyleProperty): String + CyclomaticComplexMethod:InspectorController.kt$InspectorController$private fun updateExpandedDrag( mouseX: Int, mouseY: Int, viewportWidth: Int, viewportHeight: Int, ) + CyclomaticComplexMethod:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$fun build(context: InspectorStyleEditorSnapshotBuildContext): InspectorStyleEditorSnapshotBuildResult + CyclomaticComplexMethod:KeyInput.kt$KeyInput$fun applyShift(ch: Char, shiftDown: Boolean): Char + CyclomaticComplexMethod:MinecraftFormattingParser.kt$MinecraftFormattingParser$@Suppress("LoopWithTooManyJumpStatements") private fun parseMinecraft(text: String): ParsedText + CyclomaticComplexMethod:ModalDsl.kt$fun UiScope.modalHost(modals: List<ModalSpec>, modalKey: String = "modal.host", content: UiScope.() -> Unit) + CyclomaticComplexMethod:MsdfFontMetaParser.kt$MsdfFontMetaParser$fun parse(rawJson: String): MsdfFontMeta + CyclomaticComplexMethod:OverlayPanel.kt$OverlayPanel$private fun buildResizedRect(viewportWidth: Int, viewportHeight: Int): Rect + CyclomaticComplexMethod:SelectEngine.kt$SelectEngine$fun appendOverlayCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, out: MutableList<RenderCommand>, ) + CyclomaticComplexMethod:SingleLineInputNode.kt$SingleLineInputNode$override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList<RenderCommand>) + CyclomaticComplexMethod:StyleEngine.kt$StyleEngine$private fun applyProperty( current: ComputedStyle, parentComputed: ComputedStyle?, property: StyleProperty, expression: StyleExpression, variables: Map<String, String>, rootFontSizePx: Int, ): ComputedStyle + CyclomaticComplexMethod:StyleEngine.kt$StyleEngine$private fun selectorLabel(selector: StyleSelector): String + CyclomaticComplexMethod:StyleEngine.kt$StyleEngine$private fun selectorPartMatches(node: DOMNode, part: StyleSelectorPart): Boolean + CyclomaticComplexMethod:StyleSelector.kt$StyleSelector.Companion$@Suppress("LoopWithTooManyJumpStatements") fun parse(rawSelector: String): StyleSelector + CyclomaticComplexMethod:StyleSelector.kt$StyleSelector.Companion$private fun parsePartToken(token: String, rawSelector: String): StyleSelectorPart + CyclomaticComplexMethod:StyleValueParsing.kt$fun validateLiteralForProperty( property: StyleProperty, literal: String, warningReporter: StyleWarningReporter? = null, deprecatedLengthWarningKey: String = "deprecated.unitless-length", ) + CyclomaticComplexMethod:StylesheetManager.kt$StylesheetManager$@Synchronized fun pollForChanges(force: Boolean = false) + CyclomaticComplexMethod:SystemOverlayInspectorNativeEntryTests.kt$SystemOverlayInspectorNativeEntryTests$@Test fun `inspector clipped body blocks hidden row input and accepts visible portion`() + CyclomaticComplexMethod:SystemOverlayInspectorNativeEntryTests.kt$SystemOverlayInspectorNativeEntryTests$@Test fun `inspector native body content remains clipped in narrow viewport`() + CyclomaticComplexMethod:TextAreaNode.kt$TextAreaNode$override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList<RenderCommand>) + CyclomaticComplexMethod:TextAreaNode.kt$TextAreaNode$private fun handleKey(event: KeyboardKeyDownEvent) + CyclomaticComplexMethod:TextEditShortcutDispatcher.kt$TextEditShortcutDispatcher$fun dispatch(event: KeyboardKeyDownEvent, callbacks: TextShortcutCallbacks): Boolean + LargeClass:ColorPickerController.kt$ColorPickerController + LargeClass:ColorPickerPopupEngineTests.kt$ColorPickerPopupEngineTests + LargeClass:ComponentHookRuntime.kt$ComponentHookRuntime + LargeClass:ContainerNode.kt$ContainerNode : DOMNode + LargeClass:ContextMenuEngine.kt$ContextMenuEngine : ContextMenuHost + LargeClass:DOMNode.kt$DOMNode + LargeClass:DefaultDndEngine.kt$DefaultDndEngine : DndEngine + LargeClass:FontRegistry.kt$FontRegistry + LargeClass:InspectorController.kt$InspectorController + LargeClass:PositionedLayoutStickyBehaviorTests.kt$PositionedLayoutStickyBehaviorTests + LargeClass:SelectEngine.kt$SelectEngine : SelectHost + LargeClass:StyleEngine.kt$StyleEngine + LargeClass:StyleScope.kt$StyleScope : CssLengthUnitsDsl + LargeClass:SystemColorPickerPopupBodyNode.kt$SystemColorPickerPopupBodyNode : DOMNode + LargeClass:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode : DOMNode + LargeClass:SystemOverlayColorPickerEntryTests.kt$SystemOverlayColorPickerEntryTests + LargeClass:SystemOverlayInspectorNativeEntryTests.kt$SystemOverlayInspectorNativeEntryTests + LargeClass:TextAreaNode.kt$TextAreaNode : DOMNode + LargeClass:TextPerformanceHotPathCharacterizationTests.kt$TextPerformanceHotPathCharacterizationTests + LongMethod:ColorPickerController.kt$ColorPickerController$fun appendCommands( layout: ColorPickerLayout, out: MutableList<RenderCommand>, nowMs: Long = System.currentTimeMillis(), ) + LongMethod:ColorPickerController.kt$ColorPickerController$fun appendEyedropperOverlay(viewportWidth: Int, viewportHeight: Int, out: MutableList<RenderCommand>) + LongMethod:ColorPickerController.kt$ColorPickerController$fun buildLayout(bounds: Rect): ColorPickerLayout + LongMethod:ColorPickerController.kt$ColorPickerController$fun handleMouseDown( globalX: Int, globalY: Int, button: MouseButton, layout: ColorPickerLayout, ): Boolean + LongMethod:ComponentHookRuntime.kt$ComponentHookRuntime$private fun applyEffectCommitBatch(batch: PendingEffectCommitBatch) + LongMethod:ContainerNode.kt$ContainerNode$private fun renderFlex(ctx: UiMeasureContext, children: List<DOMNode>) + LongMethod:ContainerNode.kt$ContainerNode$private fun renderGrid(ctx: UiMeasureContext, children: List<DOMNode>) + LongMethod:ContextMenuEngine.kt$ContextMenuEngine$fun appendOverlayCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, out: MutableList<RenderCommand>, ) + LongMethod:ContextMenuMeasurementCache.kt$ContextMenuMeasurementCache$fun measure( menuToken: Long, entries: List<MenuEntry>, style: ContextMenuStyle, fontId: String?, fontSize: Int?, ctx: UiMeasureContext, dpiScale: Float, ): Measurement + LongMethod:DOMNode.kt$DOMNode$@Suppress("UnusedParameter") internal fun resolveLayoutStyleValues(ctx: UiMeasureContext, parentContentWidth: Int?, parentContentHeight: Int?) + LongMethod:DOMNode.kt$DOMNode$fun scrollContainerState(): ScrollContainerState + LongMethod:DOMNode.kt$DOMNode$internal fun applyComputedStyle(style: ComputedStyle): NodeStyleApplyResult + LongMethod:DOMNode.kt$DOMNode$internal fun syncBaseFrom(template: DOMNode) + LongMethod:DndHooks.kt$internal fun DsglWindow.useSortable( id: String, nodeKey: Any = id, containerId: String, items: List<String>, data: Any? = null, previewMode: DragPreviewMode = DragPreviewMode.ORIGINAL, hideSourceWhileDragging: Boolean = true, ): Sortable + LongMethod:DomTree.kt$DomTree$fun paint(ctx: UiMeasureContext, applyStyles: Boolean = true): List<RenderCommand> + LongMethod:DssParser.kt$DssParser$@Suppress("ThrowsCount") private fun parseDeclarations( sourceName: String, text: String, fromIndex: Int, declarations: StyleDeclarations, rootVars: MutableMap<String, String>, allowVariables: Boolean, warnings: ParseWarnings, ): Int + LongMethod:InlineLayoutTests.kt$InlineLayoutTests$private fun createDisplayInlineScenario(rootWidth: Int, inlineContentWidth: Int): Pair<ContainerNode, ContainerNode> + LongMethod:InspectorController.kt$InspectorController$private fun buildExpandedDomSnapshot( root: DOMNode, viewportWidth: Int, viewportHeight: Int, ): InspectorDomSnapshot + LongMethod:InspectorController.kt$InspectorController$private fun literalFromComputed(style: ComputedStyle, property: StyleProperty): String + LongMethod:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$fun build(context: InspectorStyleEditorSnapshotBuildContext): InspectorStyleEditorSnapshotBuildResult + LongMethod:ModalDsl.kt$fun UiScope.modalHost(modals: List<ModalSpec>, modalKey: String = "modal.host", content: UiScope.() -> Unit) + LongMethod:MsdfFontMetaParser.kt$MsdfFontMetaParser$fun parse(rawJson: String): MsdfFontMeta + LongMethod:OverlayPanel.kt$OverlayPanel$private fun buildResizedRect(viewportWidth: Int, viewportHeight: Int): Rect + LongMethod:PositionedLayoutStickyBehaviorTests.kt$PositionedLayoutStickyBehaviorTests$@Test fun `non-sticky positioned modes remain unchanged with sticky enabled`() + LongMethod:SelectEngine.kt$SelectEngine$fun appendOverlayCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, out: MutableList<RenderCommand>, ) + LongMethod:StyleEngine.kt$StyleEngine$private fun applyProperty( current: ComputedStyle, parentComputed: ComputedStyle?, property: StyleProperty, expression: StyleExpression, variables: Map<String, String>, rootFontSizePx: Int, ): ComputedStyle + LongMethod:StyleValueParsing.kt$fun validateLiteralForProperty( property: StyleProperty, literal: String, warningReporter: StyleWarningReporter? = null, deprecatedLengthWarningKey: String = "deprecated.unitless-length", ) + LongMethod:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$private fun renderExpanded(ctx: UiMeasureContext, snapshot: InspectorDomSnapshot, viewportRect: Rect) + LongMethod:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$private fun renderHighlights(scope: UiScope, ctx: UiMeasureContext) + LongMethod:SystemOverlayInspectorNativeEntryTests.kt$SystemOverlayInspectorNativeEntryTests$@Test fun `inspector clipped body blocks hidden row input and accepts visible portion`() + LongMethod:SystemOverlayInspectorNativeEntryTests.kt$SystemOverlayInspectorNativeEntryTests$@Test fun `inspector wheel scrolling remains symmetric across rebuilds`() + LongMethod:SystemOverlayPanelDemoEntryTests.kt$SystemOverlayPanelDemoEntryTests$@Test fun `panel panel demo remains stable across open drag body click drag close reopen sequence`() + LongMethod:TextPerformanceHotPathCharacterizationTests.kt$TextPerformanceHotPathCharacterizationTests$@Test fun `cache boundaries are explicit for wrapped layout paths`() + LongParameterList:ColorPickerInlineNode.kt$ColorPickerInlineNode$( controlled: Boolean = false, value: RgbaColor? = null, defaultValue: RgbaColor = RgbaColor.WHITE, previousValue: RgbaColor? = null, mode: ColorFormatMode = ColorFormatMode.HEX, alphaEnabled: Boolean = true, key: Any? = null, ) + LongParameterList:ColorPickerPopupPaneNode.kt$ColorPickerPopupPaneNode$( controlled: Boolean = false, value: RgbaColor? = null, defaultValue: RgbaColor = RgbaColor.WHITE, previousValue: RgbaColor? = null, mode: ColorFormatMode = ColorFormatMode.HEX, alphaEnabled: Boolean = true, key: Any? = null, ) + LongParameterList:ColorPickerPopupRuntime.kt$ColorPickerPopupManager$( ownerScope: OverlayOwnerScope = OverlayOwnerScope.Application, anchorRect: Rect, title: String, state: ColorPickerState, style: ColorPickerStyle = ColorPickerStyle(), width: Int = 320, draggable: Boolean = true, closeOnOutsideClick: Boolean = false, onPreview: ((RgbaColor) -> Unit)? = null, onChange: ((RgbaColor) -> Unit)? = null, onCommit: ((RgbaColor) -> Unit)? = null, onClose: (() -> Unit)? = null, ) + LongParameterList:ComponentProps.kt$ComponentProps$( var style: StyleScope.() -> Unit = {}, var key: Any? = null, var id: String? = null, var className: String = "", var classes: Set<String> = emptySet(), var disabled: Boolean = false, var draggable: Boolean = false, var droppable: Boolean = false, var dragPreviewMode: DragPreviewMode = DragPreviewMode.GHOST, var hideSourceWhileDragging: Boolean = false, var dragPreview: (DragPreviewScope.() -> Unit)? = null, var dragPlaceholder: (PlaceholderScope.() -> Unit)? = null, var ref: RefTarget<ElementHandle>? = null, var onMouseEnter: ((MouseEnterEvent) -> Unit)? = null, var onMouseLeave: ((MouseLeaveEvent) -> Unit)? = null, var onMouseOver: ((MouseOverEvent) -> Unit)? = null, var onMouseMove: ((MouseMoveEvent) -> Unit)? = null, var onMouseDown: ((MouseDownEvent) -> Unit)? = null, var onMouseUp: ((MouseUpEvent) -> Unit)? = null, var onMouseClick: ((MouseClickEvent) -> Unit)? = null, var onMouseDrag: ((MouseDragEvent) -> Unit)? = null, var onMouseWheel: ((MouseWheelEvent) -> Unit)? = null, var onKeyDown: ((KeyboardKeyDownEvent) -> Unit)? = null, var onKeyUp: ((KeyboardKeyUpEvent) -> Unit)? = null, var onKeyPressed: ((KeyboardKeyDownEvent) -> Unit)? = null, var onKeyReleased: ((KeyboardKeyUpEvent) -> Unit)? = null, var onFocusGain: ((FocusGainEvent) -> Unit)? = null, var onFocusLose: ((FocusLoseEvent) -> Unit)? = null, var onInput: ((InputEvent) -> Unit)? = null, var onValueChange: ((ValueChangedEvent) -> Unit)? = null, var onDragStart: ((DragStartEvent) -> Unit)? = null, var onDrag: ((DragEvent) -> Unit)? = null, var onDragEnd: ((DragEndEvent) -> Unit)? = null, var onDragEnter: ((DragEnterEvent) -> Unit)? = null, var onDragOver: ((DragOverEvent) -> Unit)? = null, var onDragLeave: ((DragLeaveEvent) -> Unit)? = null, var onDrop: ((DropEvent) -> Unit)? = null, ) + LongParameterList:ContainerNode.kt$ContainerNode$( ctx: UiMeasureContext, child: DOMNode, parentContentX: Int, parentContentY: Int, parentContentWidth: Int, parentContentHeight: Int, desiredX: Int, desiredY: Int, desiredWidth: Int, desiredHeight: Int, ) + LongParameterList:DndHooks.kt$( id: String, nodeKey: Any = id, type: String = "default", data: Any? = null, previewMode: DragPreviewMode = DragPreviewMode.GHOST, hideSourceWhileDragging: Boolean = false, renderPreview: (DragPreviewScope.() -> Unit)? = null, renderPlaceholder: (PlaceholderScope.() -> Unit)? = null, onDragStart: ((DragStartEvent) -> Unit)? = null, onDrag: ((DragEvent) -> Unit)? = null, onDragEnd: ((DragEndEvent) -> Unit)? = null, ) + LongParameterList:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$( x: Int, y: Int, width: Int, options: List<String>, property: StyleProperty, unitSelect: Boolean, pointerProjectionScrollY: Int, rowHeightPx: Int, viewportWidth: Int, viewportHeight: Int, mouseX: Int, mouseY: Int, currentScrollIndex: Int, ) + LongParameterList:InspectorStyleEditorSnapshotBuilderTests.kt$InspectorStyleEditorSnapshotBuilderTests$( selected: ContainerNode, inspection: org.dreamfinity.dsgl.core.style.StyleInspection, panelRect: Rect = Rect(20, 20, 360, 260), editableProperties: List<StyleProperty>, pointerProjectionScrollY: Int = 0, mouseX: Int = 180, mouseY: Int = 120, openValueSelectProperty: StyleProperty? = null, openUnitSelectProperty: StyleProperty? = null, openValueSelectScrollIndex: Int = 0, openUnitSelectScrollIndex: Int = 0, ) + LongParameterList:SelectNode.kt$SelectNode$( model: SelectModel, controlled: Boolean = false, value: String? = null, defaultValue: String? = null, closeOnSelect: Boolean = true, ownerScope: OverlayOwnerScope = OverlayOwnerScope.Application, key: Any? = null, ) + LongParameterList:SystemColorPickerPanelManager.kt$InspectorColorPickerHost$( anchorRect: Rect, title: String, state: ColorPickerState, style: ColorPickerStyle = ColorPickerStyle(), width: Int = 320, draggable: Boolean = true, closeOnOutsideClick: Boolean = false, onPreview: ((RgbaColor) -> Unit)? = null, onChange: ((RgbaColor) -> Unit)? = null, onCommit: ((RgbaColor) -> Unit)? = null, onClose: (() -> Unit)? = null, ) + MagicNumber:AnimationModel.kt$ColorAnimatable$0xFF + MagicNumber:AnimationModel.kt$ColorAnimatable$24 + MagicNumber:AnimationModel.kt$TransformAnimatable$180f + MagicNumber:AnimationModel.kt$TransformAnimatable$360f + MagicNumber:CheckboxGroupNode.kt$CheckboxGroupNode$6 + MagicNumber:ColorPickerController.kt$ColorPickerController$0x33222A34 + MagicNumber:ColorPickerController.kt$ColorPickerController$12 + MagicNumber:ColorPickerController.kt$ColorPickerController$156 + MagicNumber:ColorPickerController.kt$ColorPickerController$20 + MagicNumber:ColorPickerController.kt$ColorPickerController$24 + MagicNumber:ColorPickerController.kt$ColorPickerController$32 + MagicNumber:ColorPickerController.kt$ColorPickerController$360f + MagicNumber:ColorPickerController.kt$ColorPickerController$40 + MagicNumber:ColorPickerController.kt$ColorPickerController$44 + MagicNumber:ColorPickerController.kt$ColorPickerController$5 + MagicNumber:ColorPickerController.kt$ColorPickerController$500L + MagicNumber:ColorPickerController.kt$ColorPickerController$6 + MagicNumber:ColorPickerController.kt$ColorPickerController$64 + MagicNumber:ColorPickerController.kt$ColorPickerController$7 + MagicNumber:ColorPickerController.kt$ColorPickerController$84 + MagicNumber:ColorPickerPopupGeometry.kt$ColorPickerPopupGeometry$20 + MagicNumber:ColorPickerPopupPaneNode.kt$ColorPickerPopupPaneNode$120 + MagicNumber:ColorPickerPopupPaneNode.kt$ColorPickerPopupPaneNode$24 + MagicNumber:ColorPickerPopupPaneNode.kt$ColorPickerPopupPaneNode$42 + MagicNumber:ColorPickerPopupRuntime.kt$ColorPickerPopupEngine$18 + MagicNumber:ColorPickerPopupRuntime.kt$ColorPickerPopupEngine$20 + MagicNumber:ColorPickerPopupRuntime.kt$ColorPickerPopupEngine$220 + MagicNumber:ColorTextCodec.kt$ColorTextCodec$0.5f + MagicNumber:ColorTextCodec.kt$ColorTextCodec$0xFF + MagicNumber:ColorTextCodec.kt$ColorTextCodec$1000f + MagicNumber:ColorTextCodec.kt$ColorTextCodec$1e-6f + MagicNumber:ColorTextCodec.kt$ColorTextCodec$24 + MagicNumber:ColorTextCodec.kt$ColorTextCodec$360 + MagicNumber:ColorTextCodec.kt$ColorTextCodec$360f + MagicNumber:ColorTextCodec.kt$ColorTextCodec$6 + MagicNumber:ColorTypes.kt$ColorConversions$0.5f + MagicNumber:ColorTypes.kt$ColorConversions$120f + MagicNumber:ColorTypes.kt$ColorConversions$1e-6f + MagicNumber:ColorTypes.kt$ColorConversions$240f + MagicNumber:ColorTypes.kt$ColorConversions$360f + MagicNumber:ColorTypes.kt$ColorConversions$5f + MagicNumber:ColorTypes.kt$ColorConversions$60f + MagicNumber:ColorTypes.kt$ColorConversions$6f + MagicNumber:ColorTypes.kt$HslColor$360f + MagicNumber:ColorTypes.kt$HsvColor$360f + MagicNumber:ColorTypes.kt$RgbaColor$0.5f + MagicNumber:ColorTypes.kt$RgbaColor$24 + MagicNumber:ColorTypes.kt$RgbaColor.Companion$0xFF + MagicNumber:ColorTypes.kt$RgbaColor.Companion$24 + MagicNumber:ContainerNode.kt$ContainerNode$0xFFFF_FFFFL + MagicNumber:ContainerNode.kt$ContainerNode$32 + MagicNumber:ContextMenuMeasurementCache.kt$ContextMenuMeasurementCache$14 + MagicNumber:ContextMenuMeasurementCache.kt$ContextMenuMeasurementCache$31 + MagicNumber:DOMNode.kt$DOMNode$0.8f + MagicNumber:DOMNode.kt$DOMNode$0.999f + MagicNumber:DOMNode.kt$DOMNode$120 + MagicNumber:DOMNode.kt$DOMNode$31L + MagicNumber:DateInputNode.kt$DateInputNode$13 + MagicNumber:DateInputNode.kt$DateInputNode$5 + MagicNumber:DefaultDndEngine.kt$DefaultDndEngine$12 + MagicNumber:DefaultDndEngine.kt$DefaultDndEngine$14 + MagicNumber:DefaultDndEngine.kt$DefaultDndEngine$32 + MagicNumber:DefaultDndEngine.kt$DefaultDndEngine$48 + MagicNumber:DefaultDndEngine.kt$DefaultDndEngine$6 + MagicNumber:DomTree.kt$DomTree$0.2 + MagicNumber:DomTree.kt$DomTree$0.999f + MagicNumber:DomTree.kt$DomTree$1_000_000_000.0 + MagicNumber:DomTree.kt$DomTree$240.0 + MagicNumber:DomTree.kt$DomTree$2_000L + MagicNumber:DomTree.kt$DomTree$31L + MagicNumber:DomTree.kt$DomTree$60.0 + MagicNumber:DragPresentation.kt$PlaceholderScope$5 + MagicNumber:Easing.kt$CubicBezierEasing$0.5 + MagicNumber:Easing.kt$CubicBezierEasing$18 + MagicNumber:Easing.kt$CubicBezierEasing$1e-5 + MagicNumber:FontRegistry.kt$AtlasPayload$0xFF + MagicNumber:FontRegistry.kt$AtlasPayload$24 + MagicNumber:FontRegistry.kt$FontRegistry$0.6f + MagicNumber:FontRegistry.kt$FontRegistry$1_000_000L + MagicNumber:FontRegistry.kt$FontRegistry$3_000L + MagicNumber:InspectorController.kt$InspectorController$0.56f + MagicNumber:InspectorController.kt$InspectorController$12 + MagicNumber:InspectorController.kt$InspectorController$120 + MagicNumber:InspectorController.kt$InspectorController$128 + MagicNumber:InspectorController.kt$InspectorController$140 + MagicNumber:InspectorController.kt$InspectorController$160 + MagicNumber:InspectorController.kt$InspectorController$18 + MagicNumber:InspectorController.kt$InspectorController$20 + MagicNumber:InspectorController.kt$InspectorController$24 + MagicNumber:InspectorController.kt$InspectorController$26 + MagicNumber:InspectorController.kt$InspectorController$264 + MagicNumber:InspectorController.kt$InspectorController$32 + MagicNumber:InspectorController.kt$InspectorController$40 + MagicNumber:InspectorController.kt$InspectorController$44 + MagicNumber:InspectorController.kt$InspectorController$5 + MagicNumber:InspectorController.kt$InspectorController$6 + MagicNumber:InspectorController.kt$InspectorController$64 + MagicNumber:InspectorController.kt$InspectorController$86 + MagicNumber:InspectorController.kt$InspectorController$96 + MagicNumber:InspectorEditorRegistry.kt$InspectorEditorRegistry$6 + MagicNumber:InspectorPresentationSupport.kt$InspectorPresentationSupport$0.56f + MagicNumber:InspectorPresentationSupport.kt$InspectorPresentationSupport$6 + MagicNumber:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$0.40f + MagicNumber:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$12 + MagicNumber:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$140 + MagicNumber:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$148 + MagicNumber:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$160 + MagicNumber:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$18 + MagicNumber:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$20 + MagicNumber:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$22 + MagicNumber:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$24 + MagicNumber:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$28 + MagicNumber:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$30 + MagicNumber:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$34 + MagicNumber:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$36 + MagicNumber:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$40 + MagicNumber:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$6 + MagicNumber:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$64 + MagicNumber:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$68 + MagicNumber:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$80 + MagicNumber:ItemStackNode.kt$ItemStackNode$11.0 + MagicNumber:LayoutValidator.kt$LayoutValidator$0x6688C15B + MagicNumber:LayoutValidator.kt$LayoutValidator$0x7700AEEF + MagicNumber:MeasuredTextRangeWidthSource.kt$MeasuredTextRangeWidthSource$0xFFFF_FFFFL + MagicNumber:MeasuredTextRangeWidthSource.kt$MeasuredTextRangeWidthSource$32 + MagicNumber:MeasuredTextRangeWidthSource.kt$MeasuredTextRangeWidthSource.Companion$31 + MagicNumber:MediaDsl.kt$ItemStackProps$11.0 + MagicNumber:MinecraftFormattingParser.kt$MinecraftFormattingParser$0x00FF_FFFF + MagicNumber:MinecraftFormattingParser.kt$MinecraftFormattingParser$0xFF + MagicNumber:MinecraftFormattingParser.kt$MinecraftFormattingParser$13 + MagicNumber:MinecraftFormattingParser.kt$MinecraftFormattingParser$14 + MagicNumber:MinecraftFormattingParser.kt$MinecraftFormattingParser$24 + MagicNumber:MinecraftFormattingParser.kt$MinecraftFormattingParser$6 + MagicNumber:ModalDsl.kt$132 + MagicNumber:ModalDsl.kt$184 + MagicNumber:ModalDsl.kt$232 + MagicNumber:ModalDsl.kt$6 + MagicNumber:MsdfFontMeta.kt$MsdfFontMeta$0.25f + MagicNumber:MsdfFontMeta.kt$MsdfFontMeta$0.5f + MagicNumber:MsdfFontMeta.kt$MsdfFontMeta$0x00A0 + MagicNumber:MsdfFontMeta.kt$MsdfFontMeta.Companion$0xFFFF_FFFFL + MagicNumber:MsdfFontMeta.kt$MsdfFontMeta.Companion$32 + MagicNumber:ObfuscationTextSelector.kt$ObfuscationTextSelector$0x7FFF_FFFFL + MagicNumber:ObfuscationTextSelector.kt$ObfuscationTextSelector$17 + MagicNumber:ObfuscationTextSelector.kt$ObfuscationTextSelector$31 + MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlHost$120 + MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlHost$176 + MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlHost$18 + MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlHost$24 + MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlHost$300 + MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlHost$34 + MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlHost$56 + MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlHost$96 + MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlRootNode$0x5F000000 + MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlRootNode$14 + MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlRootNode$18 + MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlRootNode$20 + MagicNumber:OverlayLayerDebugState.kt$OverlayLayerDebugState$1000.0 + MagicNumber:OverlayPanel.kt$OverlayPanel$6 + MagicNumber:RadioGroupNode.kt$RadioGroupNode$6 + MagicNumber:RangeInputNode.kt$RangeInputNode$12 + MagicNumber:RangeInputNode.kt$RangeInputNode$120 + MagicNumber:SelectEngine.kt$SelectEngine$0.001f + MagicNumber:SelectEngine.kt$SelectEngine$0.999f + MagicNumber:SelectMeasurementCache.kt$SelectMeasurementCache$14 + MagicNumber:SelectMeasurementCache.kt$SelectMeasurementCache$31 + MagicNumber:SelectNode.kt$SelectNode$31L + MagicNumber:SelectNode.kt$SelectNode$6 + MagicNumber:SelectStyle.kt$SelectStyle$4f + MagicNumber:SingleLineInputNode.kt$SingleLineInputNode$17L + MagicNumber:SingleLineInputNode.kt$SingleLineInputNode$31L + MagicNumber:SingleLineInputNode.kt$SingleLineInputNode$6 + MagicNumber:StyleAnimationEngine.kt$StyleAnimationEngine$1000.0 + MagicNumber:StyleAnimationEngine.kt$StyleAnimationEngine$1e-6f + MagicNumber:StyleEngine.kt$StyleEngine$20 + MagicNumber:StyleEngine.kt$StyleEngine$31 + MagicNumber:StyleEngine.kt$StyleEngine$5 + MagicNumber:StyleEngine.kt$StyleEngine$6 + MagicNumber:StyleModel.kt$StyleDeclarations$31 + MagicNumber:StyleScope.kt$StyleScope$0xFFFFFFFFL + MagicNumber:StyleValueParsing.kt$0xFF + MagicNumber:StyleValueParsing.kt$24 + MagicNumber:StyleValueParsing.kt$6 + MagicNumber:SystemColorPickerCustomSurfaceNodes.kt$ColorFieldSurfaceNode$5 + MagicNumber:SystemColorPickerCustomSurfaceNodes.kt$ColorFieldSurfaceNode$7 + MagicNumber:SystemColorPickerCustomSurfaceNodes.kt$ColorSwatchSurfaceNode$0x33222A34 + MagicNumber:SystemColorPickerCustomSurfaceNodes.kt$HueSurfaceNode$360f + MagicNumber:SystemColorPickerPopupBodyNode.kt$SystemColorPickerEyedropperOverlayNode$6 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x18212C39 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x1B293746 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x1E263241 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x22313D4B + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x2A425164 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x2A465968 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x2A4E3F56 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x3346596E + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x334D5D70 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x3A47A0FF + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x4426A69A + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x444285F4 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x44F3B33D + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x55394654 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x553F4A57 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x663F4A57 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x66FF5252 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x775E738C + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x77607084 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x777A5C84 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$12 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$14 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$160 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$18 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$20 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$22 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$24 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$264 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$32 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$34 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$36 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$40 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$5 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$58 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$6 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$64 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$86 + MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$96 + MagicNumber:SystemOverlayHost.kt$SystemOverlayHost.OverlayPanelDemoOverlayEntry$190 + MagicNumber:SystemOverlayHost.kt$SystemOverlayHost.OverlayPanelDemoOverlayEntry$300 + MagicNumber:SystemOverlayPanelDemoNode.kt$SystemOverlayPanelDemoNode.DemoBodyNode$120 + MagicNumber:SystemOverlayPanelDemoNode.kt$SystemOverlayPanelDemoNode.DemoBodyNode$24 + MagicNumber:SystemOverlayPanelDemoNode.kt$SystemOverlayPanelDemoNode.DemoBodyNode$44 + MagicNumber:SystemOverlayPanelDemoNode.kt$SystemOverlayPanelDemoNode.DemoBodyNode$6 + MagicNumber:TextAreaNode.kt$TextAreaNode$17L + MagicNumber:TextAreaNode.kt$TextAreaNode$31L + MagicNumber:TextAreaNode.kt$TextAreaNode$6 + MagicNumber:TextDecorationLayout.kt$TextDecorationLayout$0.06f + MagicNumber:TextDecorationLayout.kt$TextDecorationLayout$0.08f + MagicNumber:TextDecorationLayout.kt$TextDecorationLayout$0.1f + MagicNumber:TextDecorationLayout.kt$TextDecorationLayout$0.30f + MagicNumber:TextDecorationLayout.kt$TextDecorationLayout$0.51f + MagicNumber:TextDecorationLayout.kt$TextDecorationLayout$0.5f + MagicNumber:TextEditOps.kt$TextEditOps$127 + MagicNumber:TextLayoutEngine.kt$TextLayoutEngine$31 + MagicNumber:TextStyleMetrics.kt$TextStyleMetrics$0x00A0 + NestedBlockDepth:ComponentHookRuntime.kt$ComponentHookRuntime$private fun applyEffectCommitBatch(batch: PendingEffectCommitBatch) + NestedBlockDepth:ContainerNode.kt$ContainerNode$private fun computeGridPlacements(children: List<DOMNode>, columns: Int): List<GridPlacement> + NestedBlockDepth:ContextMenuEngine.kt$ContextMenuEngine$fun appendOverlayCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, out: MutableList<RenderCommand>, ) + NestedBlockDepth:DssParser.kt$DssParser$@Suppress("ThrowsCount") private fun parseDeclarations( sourceName: String, text: String, fromIndex: Int, declarations: StyleDeclarations, rootVars: MutableMap<String, String>, allowVariables: Boolean, warnings: ParseWarnings, ): Int + NestedBlockDepth:EventBus.kt$EventBus$fun post(event: Event) + NestedBlockDepth:FontRegistry.kt$AtlasPayload$private fun decodeDeflatedRgba(bytes: ByteArray): AtlasBitmap? + NestedBlockDepth:SelectEngine.kt$SelectEngine$fun appendOverlayCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, out: MutableList<RenderCommand>, ) + NestedBlockDepth:StylesheetManager.kt$StylesheetManager$@Synchronized fun pollForChanges(force: Boolean = false) + ReturnCount:ColorPickerController.kt$ColorPickerController$fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean + ReturnCount:ColorPickerController.kt$ColorPickerController$fun handleMouseDown( globalX: Int, globalY: Int, button: MouseButton, layout: ColorPickerLayout, ): Boolean + ReturnCount:ColorPickerController.kt$ColorPickerController$private fun applyInputDraftValue(key: String, rawValue: String): Boolean + ReturnCount:ColorPickerPopupRuntime.kt$ColorPickerPopupEngine$fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean + ReturnCount:ColorTextCodec.kt$ColorTextCodec$private fun parseHsbLike(raw: String): RgbaColor? + ReturnCount:ColorTextCodec.kt$ColorTextCodec$private fun parseHslLike(raw: String): RgbaColor? + ReturnCount:ColorTextCodec.kt$ColorTextCodec$private fun parseRgbLike(raw: String): ParsedColorText? + ReturnCount:ComponentHookRuntime.kt$ComponentHookRuntime$private fun isInferenceFrameworkFrame(frame: StackTraceElement): Boolean + ReturnCount:ComponentHookRuntime.kt$ComponentHookRuntime$private fun normalizeInferenceMethodName(rawMethodName: String): String? + ReturnCount:ContextMenuEngine.kt$ContextMenuEngine$fun handleKeyDown(keyCode: Int): Boolean + ReturnCount:DOMNode.kt$DOMNode$open fun handleGenericWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean + ReturnCount:FontRegistry.kt$FontRegistry$private fun load(descriptor: MsdfFontResource): LoadedMsdfFont? + ReturnCount:InspectorController.kt$InspectorController$fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean + ReturnCount:InspectorController.kt$InspectorController$fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean + ReturnCount:InspectorController.kt$InspectorController$fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean + ReturnCount:InspectorController.kt$InspectorController$private fun resolveResizeDragMode(mouseX: Int, mouseY: Int): DragMode + ReturnCount:LayerDomInputRouter.kt$LayerDomInputRouter$private fun collectHoverChainLocal( root: DOMNode, mouseX: Int, mouseY: Int, parentTransform: AffineTransform2D, parentInputClipRect: Rect?, out: MutableList<DOMNode>, ): Boolean + ReturnCount:OverlayPanel.kt$OverlayPanel$fun handleMouseDown( mouseX: Int, mouseY: Int, button: MouseButton, includeCloseButton: Boolean = true, ): Boolean + ReturnCount:OverlayPanel.kt$OverlayPanel$private fun resolveResizeHandle(panelRect: Rect, mouseX: Int, mouseY: Int): OverlayPanelResizeHandle? + ReturnCount:SelectEngine.kt$SelectEngine$fun handleKeyDown(keyCode: Int, keyChar: Char = 0.toChar()): Boolean + ReturnCount:SelectEngine.kt$SelectEngine$fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean + ReturnCount:SelectEngine.kt$SelectEngine$private fun entryAt(current: PopupState, mouseX: Int, mouseY: Int): Int + ReturnCount:TextEditShortcutDispatcher.kt$TextEditShortcutDispatcher$fun dispatch(event: KeyboardKeyDownEvent, callbacks: TextShortcutCallbacks): Boolean + TooManyFunctions:ButtonNode.kt$ButtonNode : DOMNode + TooManyFunctions:CheckboxGroupNode.kt$CheckboxGroupNode : DOMNode + TooManyFunctions:ColorPickerController.kt$ColorPickerController + TooManyFunctions:ColorPickerInlineNode.kt$ColorPickerInlineNode : DOMNode + TooManyFunctions:ColorPickerPopupPaneNode.kt$ColorPickerPopupPaneNode : DOMNode + TooManyFunctions:ColorPickerPopupRuntime.kt$ColorPickerPopupEngine : ColorPickerPopupHost + TooManyFunctions:ColorTextCodec.kt$ColorTextCodec + TooManyFunctions:ComponentHookRuntime.kt$ComponentHookRuntime + TooManyFunctions:ContainerNode.kt$ContainerNode : DOMNode + TooManyFunctions:ContextMenuDsl.kt$ContextMenuSubmenuBuilder + TooManyFunctions:ContextMenuEngine.kt$ContextMenuEngine : ContextMenuHost + TooManyFunctions:DOMNode.kt$DOMNode + TooManyFunctions:DefaultDndEngine.kt$DefaultDndEngine : DndEngine + TooManyFunctions:DomTree.kt$DomTree + TooManyFunctions:DsglWindow.kt$DsglWindow + TooManyFunctions:EventBus.kt$EventBus + TooManyFunctions:FocusManager.kt$FocusManager + TooManyFunctions:FontRegistry.kt$FontRegistry + TooManyFunctions:InspectorController.kt$InspectorController + TooManyFunctions:InspectorEditorRegistry.kt$InspectorEditorRegistry + TooManyFunctions:LayerDomInputRouter.kt$LayerDomInputRouter + TooManyFunctions:LayoutValidator.kt$LayoutValidator + TooManyFunctions:MsdfFontMeta.kt$MsdfFontMeta + TooManyFunctions:OverlayDebugControlHost.kt$OverlayDebugControlHost + TooManyFunctions:OverlayPanel.kt$OverlayPanel + TooManyFunctions:PositionedLayoutModel.kt$PositionedLayoutModel + TooManyFunctions:RadioGroupNode.kt$RadioGroupNode : DOMNode + TooManyFunctions:RangeInputNode.kt$RangeInputNode : DOMNode + TooManyFunctions:ScrollPerformanceCounters.kt$ScrollPerformanceCounters + TooManyFunctions:SelectEngine.kt$SelectEngine : SelectHost + TooManyFunctions:SelectNode.kt$SelectNode : DOMNode + TooManyFunctions:SingleLineInputNode.kt$SingleLineInputNode : DOMNode + TooManyFunctions:StyleAnimationEngine.kt$StyleAnimationEngine + TooManyFunctions:StyleEngine.kt$StyleEngine + TooManyFunctions:StyleScope.kt$StyleScope : CssLengthUnitsDsl + TooManyFunctions:StyleValueParsing.kt$org.dreamfinity.dsgl.core.style.StyleValueParsing.kt + TooManyFunctions:SystemColorPickerPopupBodyNode.kt$SystemColorPickerEyedropperOverlayNode : DOMNode + TooManyFunctions:SystemColorPickerPopupBodyNode.kt$SystemColorPickerPopupBodyNode : DOMNode + TooManyFunctions:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode : DOMNode + TooManyFunctions:SystemOverlayHost.kt$SystemOverlayHost : OverlayLayerHost + TooManyFunctions:SystemOverlayHost.kt$SystemOverlayHost$ColorPickerOverlayEntry : SystemOverlayEntryInspectorColorPickerHost + TooManyFunctions:TextAreaNode.kt$TextAreaNode : DOMNode + TooManyFunctions:ToggleNode.kt$ToggleNode : DOMNode + + From b866ca23109f8668180256328f2e7a2e90a6cf7f Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sat, 25 Apr 2026 22:35:05 +0300 Subject: [PATCH 38/78] making debug overlay to implement OverlayHost; --- .../dsgl/mcForge1710/DsglScreenHost.kt | 2 +- .../core/debug/OverlayDebugControlHost.kt | 69 ++++++++++--------- .../debug/OverlayDebugControlHostTests.kt | 16 ++--- 3 files changed, 46 insertions(+), 41 deletions(-) diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index d0a1738..89dba5e 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -462,7 +462,7 @@ abstract class DsglScreenHost( private fun collectDebugOverlayCommands(): List = runCatching { - debugOverlayHost.render(lastWidth, lastHeight) + debugOverlayHost.render(adapter, lastWidth, lastHeight) debugOverlayHost.paint(adapter) }.getOrElse { emptyList() diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt index 981b4c0..7f1dcad 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt @@ -15,6 +15,8 @@ import org.dreamfinity.dsgl.core.dsl.button import org.dreamfinity.dsgl.core.dsl.div import org.dreamfinity.dsgl.core.dsl.text import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.overlay.OverlayLayerHost +import org.dreamfinity.dsgl.core.overlay.UiLayerId import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.StyleApplicationScope @@ -32,17 +34,19 @@ internal data class OverlayDebugControlLayout( val resetRect: Rect, ) +private data class OverlayDebugToggleSnapshot( + val applicationOverlayRenderEnabled: Boolean, + val applicationOverlayTintEnabled: Boolean, + val applicationOverlayInputEnabled: Boolean, + val systemOverlayRenderEnabled: Boolean, + val systemOverlayTintEnabled: Boolean, + val systemOverlayInputEnabled: Boolean, +) + class OverlayDebugControlHost( private val state: OverlayLayerDebugState = OverlayLayerDebugState, -) { - private data class ToggleSnapshot( - val applicationOverlayRenderEnabled: Boolean, - val applicationOverlayTintEnabled: Boolean, - val applicationOverlayInputEnabled: Boolean, - val systemOverlayRenderEnabled: Boolean, - val systemOverlayTintEnabled: Boolean, - val systemOverlayInputEnabled: Boolean, - ) +) : OverlayLayerHost { + override val layerId: UiLayerId = UiLayerId.Debug private var viewportWidth: Int = 1 private var viewportHeight: Int = 1 @@ -53,23 +57,24 @@ class OverlayDebugControlHost( root = rootNode, styleScope = StyleApplicationScope.SystemOverlay, ) - private var lastToggleSnapshot: ToggleSnapshot? = null + private var lastToggleSnapshot: OverlayDebugToggleSnapshot? = null - fun render(viewportWidth: Int, viewportHeight: Int) { - this.viewportWidth = viewportWidth.coerceAtLeast(1) - this.viewportHeight = viewportHeight.coerceAtLeast(1) + @Suppress("UnusedParameter") + override fun render(ctx: UiMeasureContext, width: Int, height: Int) { + viewportWidth = width.coerceAtLeast(1) + viewportHeight = height.coerceAtLeast(1) if (!state.controlsEnabled) { layout = null lastToggleSnapshot = null return } - layout = buildLayout(this.viewportWidth, this.viewportHeight) + layout = buildLayout(viewportWidth, viewportHeight) } - fun paint(ctx: UiMeasureContext): List { + override fun paint(ctx: UiMeasureContext): List { val currentLayout = layout ?: return emptyList() val snapshot = state.snapshot() - val toggleSnapshot = snapshot.toggleSnapshot() + val toggleSnapshot = snapshot.toDebugToggleSnapshot() if (lastToggleSnapshot != toggleSnapshot) { tree.invalidateRenderCommandChunks() lastToggleSnapshot = toggleSnapshot @@ -79,12 +84,12 @@ class OverlayDebugControlHost( return tree.paint(ctx, applyStyles = true) } - fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean { + override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean { val currentLayout = layout ?: return false return currentLayout.panelRect.contains(mouseX, mouseY) } - fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { + override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { val currentLayout = layout ?: return false if (!currentLayout.panelRect.contains(mouseX, mouseY)) { return false @@ -124,22 +129,22 @@ class OverlayDebugControlHost( return true } - fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { + override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { val currentLayout = layout ?: return false if (button != MouseButton.LEFT) return false return currentLayout.panelRect.contains(mouseX, mouseY) } - fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean { + override fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean { val currentLayout = layout ?: return false if (delta == 0) return false return currentLayout.panelRect.contains(mouseX, mouseY) } @Suppress("FunctionOnlyReturningConstant", "UnusedParameter") - fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = false + override fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = false - fun clearRefs() { + override fun clearRefs() { layout = null lastToggleSnapshot = null tree.clearRefs() @@ -182,18 +187,18 @@ class OverlayDebugControlHost( ), ) } - - private fun OverlayLayerDebugSnapshot.toggleSnapshot(): ToggleSnapshot = - ToggleSnapshot( - applicationOverlayRenderEnabled = applicationOverlayRenderEnabled, - applicationOverlayTintEnabled = applicationOverlayTintEnabled, - applicationOverlayInputEnabled = applicationOverlayInputEnabled, - systemOverlayRenderEnabled = systemOverlayRenderEnabled, - systemOverlayTintEnabled = systemOverlayTintEnabled, - systemOverlayInputEnabled = systemOverlayInputEnabled, - ) } +private fun OverlayLayerDebugSnapshot.toDebugToggleSnapshot(): OverlayDebugToggleSnapshot = + OverlayDebugToggleSnapshot( + applicationOverlayRenderEnabled = applicationOverlayRenderEnabled, + applicationOverlayTintEnabled = applicationOverlayTintEnabled, + applicationOverlayInputEnabled = applicationOverlayInputEnabled, + systemOverlayRenderEnabled = systemOverlayRenderEnabled, + systemOverlayTintEnabled = systemOverlayTintEnabled, + systemOverlayInputEnabled = systemOverlayInputEnabled, + ) + private class OverlayDebugControlRootNode( key: Any? = "dsgl-overlay-debug-root", ) : DOMNode(key) { diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHostTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHostTests.kt index d3e4051..aea2804 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHostTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHostTests.kt @@ -37,7 +37,7 @@ class OverlayDebugControlHostTests { OverlayLayerDebugState.systemOverlayInputEnabled = false val host = OverlayDebugControlHost() - host.render(960, 540) + host.render(ctx, 960, 540) val layout = host.debugLayout() val commands = host.paint(ctx) @@ -56,7 +56,7 @@ class OverlayDebugControlHostTests { OverlayLayerDebugState.systemOverlayInputEnabled = false val host = OverlayDebugControlHost() - host.render(960, 540) + host.render(ctx, 960, 540) val layout = host.debugLayout() ?: error("layout missing") assertTrue(host.handleMouseDown(layout.resetRect.x + 2, layout.resetRect.y + 2, MouseButton.LEFT)) @@ -72,7 +72,7 @@ class OverlayDebugControlHostTests { OverlayLayerDebugState.resetAll() val host = OverlayDebugControlHost() - host.render(960, 540) + host.render(ctx, 960, 540) val layout = host.debugLayout() ?: error("layout missing") assertTrue( @@ -104,7 +104,7 @@ class OverlayDebugControlHostTests { OverlayLayerDebugState.updateFrameTiming(0.025) val host = OverlayDebugControlHost() - host.render(960, 540) + host.render(ctx, 960, 540) host.paint(ctx) val commands = host.paint(ctx) val drawTexts = @@ -135,7 +135,7 @@ class OverlayDebugControlHostTests { OverlayLayerDebugState.resetAll() val host = OverlayDebugControlHost() - host.render(960, 540) + host.render(ctx, 960, 540) val layout = host.debugLayout() ?: error("layout missing") val initialText = host @@ -153,7 +153,7 @@ class OverlayDebugControlHostTests { ), ) - host.render(960, 540) + host.render(ctx, 960, 540) val updatedText = host .paint(ctx) @@ -179,11 +179,11 @@ class OverlayDebugControlHostTests { fun `controls visibility obeys debug-only toggle`() { OverlayLayerDebugState.setControlsEnabledTestOverride(false) val host = OverlayDebugControlHost() - host.render(960, 540) + host.render(ctx, 960, 540) assertTrue(host.paint(ctx).isEmpty()) OverlayLayerDebugState.setControlsEnabledTestOverride(true) - host.render(960, 540) + host.render(ctx, 960, 540) assertTrue(host.paint(ctx).isNotEmpty()) } From 418b28aba0d0800378872959f87743cb11cb950c Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sat, 25 Apr 2026 23:54:42 +0300 Subject: [PATCH 39/78] changing frame input ownership; --- .../org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt | 9 +++++++-- .../dreamfinity/dsgl/core/overlay/OverlayLayerHost.kt | 2 ++ .../dsgl/core/overlay/system/SystemOverlayHost.kt | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index 89dba5e..a594f04 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -31,6 +31,7 @@ import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.inspector.InspectorMode import org.dreamfinity.dsgl.core.overlay.ApplicationOverlayHost import org.dreamfinity.dsgl.core.overlay.OverlayLayerContracts +import org.dreamfinity.dsgl.core.overlay.OverlayLayerHost import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.overlay.UiLayerId import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayHost @@ -748,7 +749,7 @@ abstract class DsglScreenHost( control = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL), meta = Keyboard.isKeyDown(Keyboard.KEY_LMETA) || Keyboard.isKeyDown(Keyboard.KEY_RMETA), ) - systemOverlayHost.onInputFrame(lastWidth, lastHeight) + runOverlayInputFrame(systemOverlayHost) ColorPickerRuntime.engine.onFrame(lastWidth, lastHeight) val keyCode = Keyboard.getEventKey() val keyChar = Keyboard.getEventCharacter() @@ -920,7 +921,7 @@ abstract class DsglScreenHost( viewportHeight = lastHeight, viewportScale = 1f, ) - systemOverlayHost.onInputFrame(lastWidth, lastHeight) + runOverlayInputFrame(systemOverlayHost) inspectorPointerCaptured = inspector.isPointerCaptured systemOverlayHost.syncFrame( inspectedRoot = tree.root, @@ -933,6 +934,10 @@ abstract class DsglScreenHost( refreshActiveColorSamplerOwner(tree.root) } + private fun runOverlayInputFrame(host: OverlayLayerHost) { + host.onInputFrame(lastWidth, lastHeight) + } + private fun consumeOverlayPointerPhase(inputEvent: MouseInputEvent): Boolean { val appPressMove = inputEvent.mouseButton == -1 && eventButton != -1 if (!appPressMove && diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerHost.kt index 6a11dfa..e57aa98 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerHost.kt @@ -7,6 +7,8 @@ import org.dreamfinity.dsgl.core.render.RenderCommand interface OverlayLayerHost { val layerId: UiLayerId + fun onInputFrame(viewportWidth: Int, viewportHeight: Int) {} + fun render(ctx: UiMeasureContext, width: Int, height: Int) fun paint(ctx: UiMeasureContext): List diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt index 1ccc386..af201c5 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt @@ -76,7 +76,7 @@ class SystemOverlayHost( fun isOverlayPanelDemoOpen(): Boolean = overlayPanelDemoEntry.isOpen() - fun onInputFrame(viewportWidth: Int, viewportHeight: Int) { + override fun onInputFrame(viewportWidth: Int, viewportHeight: Int) { knownViewportWidth = viewportWidth.coerceAtLeast(1) knownViewportHeight = viewportHeight.coerceAtLeast(1) rootNode.setViewportBounds(knownViewportWidth, knownViewportHeight) From d6e3d7d0787576497a3223c718064ffd4860d73a Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sun, 26 Apr 2026 12:27:40 +0300 Subject: [PATCH 40/78] refactoring context menu handling; --- .../dsgl/mcForge1710/DsglScreenHost.kt | 40 +++++++++------ .../core/overlay/ApplicationOverlayHost.kt | 50 +++++++++++++++++++ 2 files changed, 74 insertions(+), 16 deletions(-) diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index a594f04..bbf4dd7 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -9,7 +9,6 @@ import org.dreamfinity.dsgl.core.DsglWindow import org.dreamfinity.dsgl.core.HotReloadBridge import org.dreamfinity.dsgl.core.animation.StyleAnimationEngine import org.dreamfinity.dsgl.core.colorpicker.* -import org.dreamfinity.dsgl.core.contextmenu.ContextMenuRuntime import org.dreamfinity.dsgl.core.debug.OverlayDebugControlHost import org.dreamfinity.dsgl.core.debug.OverlayLayerDebugState import org.dreamfinity.dsgl.core.dnd.DndRuntime @@ -34,6 +33,15 @@ import org.dreamfinity.dsgl.core.overlay.OverlayLayerContracts import org.dreamfinity.dsgl.core.overlay.OverlayLayerHost import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.overlay.UiLayerId +import org.dreamfinity.dsgl.core.overlay.appendContextMenuOverlayCommands +import org.dreamfinity.dsgl.core.overlay.closeContextMenus +import org.dreamfinity.dsgl.core.overlay.contextMenuOnFrame +import org.dreamfinity.dsgl.core.overlay.handleContextMenuKeyDown +import org.dreamfinity.dsgl.core.overlay.handleContextMenuMouseDown +import org.dreamfinity.dsgl.core.overlay.handleContextMenuMouseMove +import org.dreamfinity.dsgl.core.overlay.handleContextMenuMouseUp +import org.dreamfinity.dsgl.core.overlay.handleContextMenuMouseWheel +import org.dreamfinity.dsgl.core.overlay.isContextMenuOpen import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayHost import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.select.SelectRuntime @@ -389,7 +397,7 @@ abstract class DsglScreenHost( } private fun syncFeatureRuntimeFrame(tree: DomTree, dsglMouseX: Int, dsglMouseY: Int) { - ContextMenuRuntime.engine.onFrame(adapter, lastWidth, lastHeight, 1f) + applicationOverlayHost.contextMenuOnFrame(adapter, lastWidth, lastHeight, 1f) SelectRuntime.applicationEngine.onFrame(adapter, lastWidth, lastHeight, 1f) SelectRuntime.systemEngine.onFrame(adapter, lastWidth, lastHeight, 1f) ColorPickerRuntime.engine.onFrame(lastWidth, lastHeight) @@ -478,7 +486,7 @@ abstract class DsglScreenHost( systemOverlayInputEnabled: Boolean, inspectorBlocks: Boolean, ) { - val contextMenuBlocks = appOverlayInputEnabled && !inspectorBlocks && ContextMenuRuntime.engine.isOpen() + val contextMenuBlocks = appOverlayInputEnabled && !inspectorBlocks && applicationOverlayHost.isContextMenuOpen() val selectBlocks = appOverlayInputEnabled && !inspectorBlocks && SelectRuntime.applicationEngine.isOpen() val systemSelectBlocks = systemOverlayInputEnabled && SelectRuntime.systemEngine.isOpen() val inlineSamplerOwnsSession = activeColorSamplerOwner is ActiveColorSamplerOwner.Inline @@ -537,11 +545,11 @@ abstract class DsglScreenHost( lastHeight, applicationOverlayCommandsBuffer, ) - ContextMenuRuntime.engine.appendOverlayCommands( - adapter, - lastWidth, - lastHeight, - applicationOverlayCommandsBuffer, + applicationOverlayHost.appendContextMenuOverlayCommands( + measureContext = adapter, + viewportWidth = lastWidth, + viewportHeight = lastHeight, + out = applicationOverlayCommandsBuffer, ) ColorPickerRuntime.engine.appendOverlayCommands(applicationOverlayCommandsBuffer) appendInlineColorPickerOverlayCommands(applicationOverlayCommandsBuffer) @@ -605,7 +613,7 @@ abstract class DsglScreenHost( DndRuntime.engine.cancelActiveDrag() ColorPickerRuntime.engine.closeAll() SelectRuntime.host.closeAll() - ContextMenuRuntime.engine.closeAll() + applicationOverlayHost.closeContextMenus() clearActiveTarget() flushPendingCleanup() clearHoverChainStates() @@ -651,7 +659,7 @@ abstract class DsglScreenHost( val height = viewport.height lastViewport = viewport if (force || width != lastWidth || height != lastHeight) { - ContextMenuRuntime.engine.closeAll() + applicationOverlayHost.closeContextMenus() lastWidth = width lastHeight = height needsLayout = true @@ -903,7 +911,7 @@ abstract class DsglScreenHost( private fun syncMouseInputFrame(tree: DomTree, inputEvent: MouseInputEvent) { inspector.onCursorMoved(inputEvent.mouseX, inputEvent.mouseY) - ContextMenuRuntime.engine.onFrame( + applicationOverlayHost.contextMenuOnFrame( measureContext = adapter, viewportWidth = lastWidth, viewportHeight = lastHeight, @@ -1117,7 +1125,7 @@ abstract class DsglScreenHost( if (SelectRuntime.applicationEngine.handleKeyDown(keyCode, keyChar)) { return true } - if (ContextMenuRuntime.engine.handleKeyDown(keyCode)) { + if (applicationOverlayHost.handleContextMenuKeyDown(keyCode)) { return true } return false @@ -1290,7 +1298,7 @@ abstract class DsglScreenHost( return true } - if (dWheel != 0 && ContextMenuRuntime.engine.handleMouseWheel(mouseX, mouseY, dWheel)) { + if (dWheel != 0 && applicationOverlayHost.handleContextMenuMouseWheel(mouseX, mouseY, dWheel)) { return true } if (dWheel != 0 && SelectRuntime.applicationEngine.handleMouseWheel(mouseX, mouseY, dWheel)) { @@ -1299,9 +1307,9 @@ abstract class DsglScreenHost( if (mouseButton != -1 && mappedButton != null) { val consumedByContextMenu = if (buttonPressed) { - ContextMenuRuntime.engine.handleMouseDown(mouseX, mouseY, mappedButton) + applicationOverlayHost.handleContextMenuMouseDown(mouseX, mouseY, mappedButton) } else { - ContextMenuRuntime.engine.handleMouseUp(mouseX, mouseY, mappedButton) + applicationOverlayHost.handleContextMenuMouseUp(mouseX, mouseY, mappedButton) } if (consumedByContextMenu) { return true @@ -1317,7 +1325,7 @@ abstract class DsglScreenHost( } return false } - if (mouseButton == -1 && ContextMenuRuntime.engine.handleMouseMove(mouseX, mouseY)) { + if (mouseButton == -1 && applicationOverlayHost.handleContextMenuMouseMove(mouseX, mouseY)) { return true } if (mouseButton == -1 && SelectRuntime.applicationEngine.handleMouseMove(mouseX, mouseY)) { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt index 19786f6..e8b95d9 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt @@ -1,6 +1,7 @@ package org.dreamfinity.dsgl.core.overlay import org.dreamfinity.dsgl.core.DomTree +import org.dreamfinity.dsgl.core.contextmenu.ContextMenuRuntime import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton @@ -40,3 +41,52 @@ class ApplicationOverlayHost : OverlayLayerHost { internal fun debugRootBounds(): Rect = rootNode.bounds } + +fun ApplicationOverlayHost.contextMenuOnFrame( + measureContext: UiMeasureContext, + viewportWidth: Int, + viewportHeight: Int, + viewportScale: Float, +) { + ContextMenuRuntime.engine.onFrame( + measureContext = measureContext, + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + viewportScale = viewportScale, + ) +} + +fun ApplicationOverlayHost.appendContextMenuOverlayCommands( + measureContext: UiMeasureContext, + viewportWidth: Int, + viewportHeight: Int, + out: MutableList, +) { + ContextMenuRuntime.engine.appendOverlayCommands( + measureContext = measureContext, + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + out = out, + ) +} + +fun ApplicationOverlayHost.closeContextMenus() { + ContextMenuRuntime.engine.closeAll() +} + +fun ApplicationOverlayHost.isContextMenuOpen(): Boolean = ContextMenuRuntime.engine.isOpen() + +fun ApplicationOverlayHost.handleContextMenuMouseMove(mouseX: Int, mouseY: Int): Boolean = + ContextMenuRuntime.engine.handleMouseMove(mouseX, mouseY) + +fun ApplicationOverlayHost.handleContextMenuMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + ContextMenuRuntime.engine.handleMouseDown(mouseX, mouseY, button) + +fun ApplicationOverlayHost.handleContextMenuMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + ContextMenuRuntime.engine.handleMouseUp(mouseX, mouseY, button) + +fun ApplicationOverlayHost.handleContextMenuMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = + ContextMenuRuntime.engine.handleMouseWheel(mouseX, mouseY, delta) + +fun ApplicationOverlayHost.handleContextMenuKeyDown(keyCode: Int): Boolean = + ContextMenuRuntime.engine.handleKeyDown(keyCode) From 3b9bf3b27a400738fd133be29eb9bc80e4278b31 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sun, 26 Apr 2026 12:49:34 +0300 Subject: [PATCH 41/78] refactoring application select handling; --- .../dsgl/mcForge1710/DsglScreenHost.kt | 35 ++++++++------ .../core/overlay/ApplicationOverlayHost.kt | 46 +++++++++++++++++++ 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index bbf4dd7..4e51ea4 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -33,14 +33,22 @@ import org.dreamfinity.dsgl.core.overlay.OverlayLayerContracts import org.dreamfinity.dsgl.core.overlay.OverlayLayerHost import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.overlay.UiLayerId +import org.dreamfinity.dsgl.core.overlay.appendApplicationSelectOverlayCommands import org.dreamfinity.dsgl.core.overlay.appendContextMenuOverlayCommands +import org.dreamfinity.dsgl.core.overlay.applicationSelectOnFrame import org.dreamfinity.dsgl.core.overlay.closeContextMenus import org.dreamfinity.dsgl.core.overlay.contextMenuOnFrame +import org.dreamfinity.dsgl.core.overlay.handleApplicationSelectKeyDown +import org.dreamfinity.dsgl.core.overlay.handleApplicationSelectMouseDown +import org.dreamfinity.dsgl.core.overlay.handleApplicationSelectMouseMove +import org.dreamfinity.dsgl.core.overlay.handleApplicationSelectMouseUp +import org.dreamfinity.dsgl.core.overlay.handleApplicationSelectMouseWheel import org.dreamfinity.dsgl.core.overlay.handleContextMenuKeyDown import org.dreamfinity.dsgl.core.overlay.handleContextMenuMouseDown import org.dreamfinity.dsgl.core.overlay.handleContextMenuMouseMove import org.dreamfinity.dsgl.core.overlay.handleContextMenuMouseUp import org.dreamfinity.dsgl.core.overlay.handleContextMenuMouseWheel +import org.dreamfinity.dsgl.core.overlay.isApplicationSelectOpen import org.dreamfinity.dsgl.core.overlay.isContextMenuOpen import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayHost import org.dreamfinity.dsgl.core.render.RenderCommand @@ -398,7 +406,7 @@ abstract class DsglScreenHost( private fun syncFeatureRuntimeFrame(tree: DomTree, dsglMouseX: Int, dsglMouseY: Int) { applicationOverlayHost.contextMenuOnFrame(adapter, lastWidth, lastHeight, 1f) - SelectRuntime.applicationEngine.onFrame(adapter, lastWidth, lastHeight, 1f) + applicationOverlayHost.applicationSelectOnFrame(adapter, lastWidth, lastHeight, 1f) SelectRuntime.systemEngine.onFrame(adapter, lastWidth, lastHeight, 1f) ColorPickerRuntime.engine.onFrame(lastWidth, lastHeight) ColorPickerRuntime.engine.onCursorPosition(dsglMouseX, dsglMouseY) @@ -487,7 +495,8 @@ abstract class DsglScreenHost( inspectorBlocks: Boolean, ) { val contextMenuBlocks = appOverlayInputEnabled && !inspectorBlocks && applicationOverlayHost.isContextMenuOpen() - val selectBlocks = appOverlayInputEnabled && !inspectorBlocks && SelectRuntime.applicationEngine.isOpen() + val selectBlocks = + appOverlayInputEnabled && !inspectorBlocks && applicationOverlayHost.isApplicationSelectOpen() val systemSelectBlocks = systemOverlayInputEnabled && SelectRuntime.systemEngine.isOpen() val inlineSamplerOwnsSession = activeColorSamplerOwner is ActiveColorSamplerOwner.Inline val colorPickerBlocks = @@ -539,11 +548,11 @@ abstract class DsglScreenHost( lastHeight, applicationOverlayCommandsBuffer, ) - SelectRuntime.applicationEngine.appendOverlayCommands( - adapter, - lastWidth, - lastHeight, - applicationOverlayCommandsBuffer, + applicationOverlayHost.appendApplicationSelectOverlayCommands( + measureContext = adapter, + viewportWidth = lastWidth, + viewportHeight = lastHeight, + out = applicationOverlayCommandsBuffer, ) applicationOverlayHost.appendContextMenuOverlayCommands( measureContext = adapter, @@ -917,7 +926,7 @@ abstract class DsglScreenHost( viewportHeight = lastHeight, viewportScale = 1f, ) - SelectRuntime.applicationEngine.onFrame( + applicationOverlayHost.applicationSelectOnFrame( measureContext = adapter, viewportWidth = lastWidth, viewportHeight = lastHeight, @@ -1122,7 +1131,7 @@ abstract class DsglScreenHost( if (applicationOverlayHost.handleKeyDown(keyCode, keyChar)) { return true } - if (SelectRuntime.applicationEngine.handleKeyDown(keyCode, keyChar)) { + if (applicationOverlayHost.handleApplicationSelectKeyDown(keyCode, keyChar)) { return true } if (applicationOverlayHost.handleContextMenuKeyDown(keyCode)) { @@ -1301,7 +1310,7 @@ abstract class DsglScreenHost( if (dWheel != 0 && applicationOverlayHost.handleContextMenuMouseWheel(mouseX, mouseY, dWheel)) { return true } - if (dWheel != 0 && SelectRuntime.applicationEngine.handleMouseWheel(mouseX, mouseY, dWheel)) { + if (dWheel != 0 && applicationOverlayHost.handleApplicationSelectMouseWheel(mouseX, mouseY, dWheel)) { return true } if (mouseButton != -1 && mappedButton != null) { @@ -1316,9 +1325,9 @@ abstract class DsglScreenHost( } val consumedBySelect = if (buttonPressed) { - SelectRuntime.applicationEngine.handleMouseDown(mouseX, mouseY, mappedButton) + applicationOverlayHost.handleApplicationSelectMouseDown(mouseX, mouseY, mappedButton) } else { - SelectRuntime.applicationEngine.handleMouseUp(mouseX, mouseY, mappedButton) + applicationOverlayHost.handleApplicationSelectMouseUp(mouseX, mouseY, mappedButton) } if (consumedBySelect) { return true @@ -1328,7 +1337,7 @@ abstract class DsglScreenHost( if (mouseButton == -1 && applicationOverlayHost.handleContextMenuMouseMove(mouseX, mouseY)) { return true } - if (mouseButton == -1 && SelectRuntime.applicationEngine.handleMouseMove(mouseX, mouseY)) { + if (mouseButton == -1 && applicationOverlayHost.handleApplicationSelectMouseMove(mouseX, mouseY)) { return true } return false diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt index e8b95d9..3d257e7 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt @@ -6,6 +6,7 @@ import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.render.RenderCommand +import org.dreamfinity.dsgl.core.select.SelectRuntime import org.dreamfinity.dsgl.core.style.StyleApplicationScope class ApplicationOverlayHost : OverlayLayerHost { @@ -90,3 +91,48 @@ fun ApplicationOverlayHost.handleContextMenuMouseWheel(mouseX: Int, mouseY: Int, fun ApplicationOverlayHost.handleContextMenuKeyDown(keyCode: Int): Boolean = ContextMenuRuntime.engine.handleKeyDown(keyCode) + +fun ApplicationOverlayHost.applicationSelectOnFrame( + measureContext: UiMeasureContext, + viewportWidth: Int, + viewportHeight: Int, + viewportScale: Float, +) { + SelectRuntime.applicationEngine.onFrame( + measureContext = measureContext, + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + viewportScale = viewportScale, + ) +} + +fun ApplicationOverlayHost.appendApplicationSelectOverlayCommands( + measureContext: UiMeasureContext, + viewportWidth: Int, + viewportHeight: Int, + out: MutableList, +) { + SelectRuntime.applicationEngine.appendOverlayCommands( + measureContext = measureContext, + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + out = out, + ) +} + +fun ApplicationOverlayHost.isApplicationSelectOpen(): Boolean = SelectRuntime.applicationEngine.isOpen() + +fun ApplicationOverlayHost.handleApplicationSelectKeyDown(keyCode: Int, keyChar: Char): Boolean = + SelectRuntime.applicationEngine.handleKeyDown(keyCode, keyChar) + +fun ApplicationOverlayHost.handleApplicationSelectMouseMove(mouseX: Int, mouseY: Int): Boolean = + SelectRuntime.applicationEngine.handleMouseMove(mouseX, mouseY) + +fun ApplicationOverlayHost.handleApplicationSelectMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + SelectRuntime.applicationEngine.handleMouseDown(mouseX, mouseY, button) + +fun ApplicationOverlayHost.handleApplicationSelectMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + SelectRuntime.applicationEngine.handleMouseUp(mouseX, mouseY, button) + +fun ApplicationOverlayHost.handleApplicationSelectMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = + SelectRuntime.applicationEngine.handleMouseWheel(mouseX, mouseY, delta) From 2b9d2c301032bbd9a8960b86184f32fd0db1104f Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sun, 26 Apr 2026 13:12:29 +0300 Subject: [PATCH 42/78] reworking how system overlay ownership is taken; --- .../dsgl/mcForge1710/DsglScreenHost.kt | 18 ++++---- .../core/overlay/system/SystemOverlayHost.kt | 46 +++++++++++++++++++ 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index 4e51ea4..1772155 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -407,7 +407,7 @@ abstract class DsglScreenHost( private fun syncFeatureRuntimeFrame(tree: DomTree, dsglMouseX: Int, dsglMouseY: Int) { applicationOverlayHost.contextMenuOnFrame(adapter, lastWidth, lastHeight, 1f) applicationOverlayHost.applicationSelectOnFrame(adapter, lastWidth, lastHeight, 1f) - SelectRuntime.systemEngine.onFrame(adapter, lastWidth, lastHeight, 1f) + systemOverlayHost.systemSelectOnFrame(adapter, lastWidth, lastHeight, 1f) ColorPickerRuntime.engine.onFrame(lastWidth, lastHeight) ColorPickerRuntime.engine.onCursorPosition(dsglMouseX, dsglMouseY) refreshActiveColorSamplerOwner(tree.root) @@ -468,7 +468,7 @@ abstract class DsglScreenHost( systemOverlayCommandsBuffer.clear() systemOverlayCommandsBuffer.addAll(systemOverlayCommands) if (systemOverlayRenderEnabled) { - SelectRuntime.systemEngine.appendOverlayCommands( + systemOverlayHost.appendSystemSelectOverlayCommands( adapter, lastWidth, lastHeight, @@ -497,7 +497,7 @@ abstract class DsglScreenHost( val contextMenuBlocks = appOverlayInputEnabled && !inspectorBlocks && applicationOverlayHost.isContextMenuOpen() val selectBlocks = appOverlayInputEnabled && !inspectorBlocks && applicationOverlayHost.isApplicationSelectOpen() - val systemSelectBlocks = systemOverlayInputEnabled && SelectRuntime.systemEngine.isOpen() + val systemSelectBlocks = systemOverlayInputEnabled && systemOverlayHost.isSystemSelectOpen() val inlineSamplerOwnsSession = activeColorSamplerOwner is ActiveColorSamplerOwner.Inline val colorPickerBlocks = !inspectorBlocks && @@ -932,7 +932,7 @@ abstract class DsglScreenHost( viewportHeight = lastHeight, viewportScale = 1f, ) - SelectRuntime.systemEngine.onFrame( + systemOverlayHost.systemSelectOnFrame( measureContext = adapter, viewportWidth = lastWidth, viewportHeight = lastHeight, @@ -1105,7 +1105,7 @@ abstract class DsglScreenHost( inspectorMouseX: Int, inspectorMouseY: Int, ): Boolean { - if (SelectRuntime.systemEngine.handleKeyDown(keyCode, keyChar)) { + if (systemOverlayHost.handleSystemSelectKeyDown(keyCode, keyChar)) { return true } if (systemOverlayHost.handleKeyDown(keyCode, keyChar)) { @@ -1222,7 +1222,7 @@ abstract class DsglScreenHost( mappedButton: MouseButton?, buttonPressed: Boolean, ): Boolean { - if (dWheel != 0 && SelectRuntime.systemEngine.handleMouseWheel(mouseX, mouseY, dWheel)) { + if (dWheel != 0 && systemOverlayHost.handleSystemSelectMouseWheel(mouseX, mouseY, dWheel)) { return true } if (dWheel != 0 && systemOverlayHost.handleMouseWheel(mouseX, mouseY, dWheel)) { @@ -1231,9 +1231,9 @@ abstract class DsglScreenHost( if (mouseButton != -1 && mappedButton != null) { val consumedBySystemSelect = if (buttonPressed) { - SelectRuntime.systemEngine.handleMouseDown(mouseX, mouseY, mappedButton) + systemOverlayHost.handleSystemSelectMouseDown(mouseX, mouseY, mappedButton) } else { - SelectRuntime.systemEngine.handleMouseUp(mouseX, mouseY, mappedButton) + systemOverlayHost.handleSystemSelectMouseUp(mouseX, mouseY, mappedButton) } if (consumedBySystemSelect) { return true @@ -1247,7 +1247,7 @@ abstract class DsglScreenHost( if (consumedBySystemOverlay) { return true } - } else if (mouseButton == -1 && SelectRuntime.systemEngine.handleMouseMove(mouseX, mouseY)) { + } else if (mouseButton == -1 && systemOverlayHost.handleSystemSelectMouseMove(mouseX, mouseY)) { return true } else if (mouseButton == -1 && systemOverlayHost.handleMouseMove(mouseX, mouseY)) { return true diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt index af201c5..55befbe 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt @@ -22,6 +22,7 @@ import org.dreamfinity.dsgl.core.overlay.input.dispatchManualThenDomFallback import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanel import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelStyle import org.dreamfinity.dsgl.core.render.RenderCommand +import org.dreamfinity.dsgl.core.select.SelectRuntime import org.dreamfinity.dsgl.core.style.StyleApplicationScope class SystemOverlayHost( @@ -76,6 +77,51 @@ class SystemOverlayHost( fun isOverlayPanelDemoOpen(): Boolean = overlayPanelDemoEntry.isOpen() + fun systemSelectOnFrame( + measureContext: UiMeasureContext, + viewportWidth: Int, + viewportHeight: Int, + viewportScale: Float, + ) { + SelectRuntime.systemEngine.onFrame( + measureContext = measureContext, + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + viewportScale = viewportScale, + ) + } + + fun appendSystemSelectOverlayCommands( + measureContext: UiMeasureContext, + viewportWidth: Int, + viewportHeight: Int, + out: MutableList, + ) { + SelectRuntime.systemEngine.appendOverlayCommands( + measureContext = measureContext, + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + out = out, + ) + } + + fun isSystemSelectOpen(): Boolean = SelectRuntime.systemEngine.isOpen() + + fun handleSystemSelectKeyDown(keyCode: Int, keyChar: Char): Boolean = + SelectRuntime.systemEngine.handleKeyDown(keyCode, keyChar) + + fun handleSystemSelectMouseMove(mouseX: Int, mouseY: Int): Boolean = + SelectRuntime.systemEngine.handleMouseMove(mouseX, mouseY) + + fun handleSystemSelectMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + SelectRuntime.systemEngine.handleMouseDown(mouseX, mouseY, button) + + fun handleSystemSelectMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + SelectRuntime.systemEngine.handleMouseUp(mouseX, mouseY, button) + + fun handleSystemSelectMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = + SelectRuntime.systemEngine.handleMouseWheel(mouseX, mouseY, delta) + override fun onInputFrame(viewportWidth: Int, viewportHeight: Int) { knownViewportWidth = viewportWidth.coerceAtLeast(1) knownViewportHeight = viewportHeight.coerceAtLeast(1) From 2b0593a111b517b0fafae9a519eb1c7172a98cbe Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sun, 26 Apr 2026 13:23:13 +0300 Subject: [PATCH 43/78] clean-up swatch render; --- .../SystemColorPickerPopupBodyNode.kt | 82 ++++++++++++------- 1 file changed, 51 insertions(+), 31 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt index a99fd82..30cae18 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt @@ -90,14 +90,6 @@ internal class SystemColorPickerPopupBodyNode( .also { node -> configureInputValueNode(index, node) } } - private val recentSwatchNodes: List = - (0 until RECENT_SWATCH_COUNT).map { index -> - scope.colorSwatch({ - allowEmpty = true - this.key = "dsgl-system-color-picker-recent-$index" - }) - } - private var appliedStyle: ColorPickerStyle? = null fun focusInputSlot(index: Int, mouseX: Int, mouseY: Int): Boolean { @@ -197,13 +189,6 @@ internal class SystemColorPickerPopupBodyNode( val modeDropdownOpen: Boolean, ) - private data class RecentSwatchRenderState( - val layout: ColorPickerLayout, - val style: ColorPickerStyle, - val recentColors: List, - val hoveredRecent: Int, - ) - private data class InputRowsRenderState( val controller: ColorPickerController, val layout: ColorPickerLayout, @@ -460,32 +445,64 @@ internal class SystemColorPickerPopupBodyNode( hoverY: Int, recentColors: List, ) { - val renderState = - RecentSwatchRenderState( - layout = layout, + val hoveredRecent = layout.recentRects.indexOfFirst { it.contains(hoverX, hoverY) } + val recentSwatchNodes = + composeRecentSwatchNodes( style = style, recentColors = recentColors, - hoveredRecent = layout.recentRects.indexOfFirst { it.contains(hoverX, hoverY) }, + hoveredRecent = hoveredRecent, ) for (index in 0 until RECENT_SWATCH_COUNT) { - renderRecentSwatch(ctx, renderState, index) + renderRecentSwatch(ctx, layout, recentSwatchNodes, index) + } + } + + private fun composeRecentSwatchNodes( + style: ColorPickerStyle, + recentColors: List, + hoveredRecent: Int, + ): List { + removeRecentSwatchSectionNodes() + return buildList(RECENT_SWATCH_COUNT) { + for (index in 0 until RECENT_SWATCH_COUNT) { + val node = + scope.colorSwatch({ + allowEmpty = true + key = recentSwatchNodeKey(index) + palette = style + color = recentColors.getOrNull(index) + highlighted = index == hoveredRecent + }) + add(node) + } } } - private fun renderRecentSwatch(ctx: UiMeasureContext, state: RecentSwatchRenderState, index: Int) { + private fun removeRecentSwatchSectionNodes() { + val iterator = children.iterator() + while (iterator.hasNext()) { + val child = iterator.next() + val key = child.key as? String + val isRecentSwatchNode = key?.startsWith(RECENT_SWATCH_KEY_PREFIX) == true + if (isRecentSwatchNode) { + child.parent = null + iterator.remove() + } + } + } + + private fun renderRecentSwatch( + ctx: UiMeasureContext, + layout: ColorPickerLayout, + recentSwatchNodes: List, + index: Int, + ) { val swatchNode = recentSwatchNodes[index] - val swatchRect = - state.layout.recentRects - .getOrNull(index) + val swatchRect = layout.recentRects.getOrNull(index) if (swatchRect == null) { renderNode(ctx, swatchNode, null) return } - swatchNode.bind( - style = state.style, - color = state.recentColors.getOrNull(index), - highlighted = index == state.hoveredRecent, - ) renderNode(ctx, swatchNode, swatchRect) } @@ -703,8 +720,11 @@ internal class SystemColorPickerPopupBodyNode( } private companion object { - const val MAX_INPUT_SLOTS: Int = 4 - const val RECENT_SWATCH_COUNT: Int = 64 + private const val MAX_INPUT_SLOTS: Int = 4 + private const val RECENT_SWATCH_COUNT: Int = 64 + private const val RECENT_SWATCH_KEY_PREFIX: String = "dsgl-system-color-picker-recent-" + + private fun recentSwatchNodeKey(index: Int): String = "$RECENT_SWATCH_KEY_PREFIX$index" } } From 0f1be280e0008030f9c26ecb53ede0a4323c7e30 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sun, 26 Apr 2026 15:45:05 +0300 Subject: [PATCH 44/78] preparing to unifying the color picker; --- .../internal/SystemColorPickerOverlayNode.kt | 8 +++++--- .../internal/SystemColorPickerPopupBodyNode.kt | 4 ++++ .../dsgl/core/overlay/system/SystemOverlayHost.kt | 12 ++++++------ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerOverlayNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerOverlayNode.kt index c4d4d51..e6f30f3 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerOverlayNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerOverlayNode.kt @@ -9,7 +9,7 @@ import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanel import org.dreamfinity.dsgl.core.style.Display -internal class SystemColorPickerOverlayNode( +internal class ColorPickerPopupOverlayNode( private val popupEngine: ColorPickerPopupEngine, private val overlayPanel: OverlayPanel, key: Any? = "dsgl-system-color-picker", @@ -20,8 +20,8 @@ internal class SystemColorPickerOverlayNode( private var cursorY: Int = 0 private val panelNode: DOMNode = overlayPanel.node().applyParent(this) - private val bodyNode: SystemColorPickerPopupBodyNode = - SystemColorPickerPopupBodyNode(popupEngine = popupEngine).also(overlayPanel::setBodyContent) + private val bodyNode: ColorPickerPopupBodyNode = + ColorPickerPopupBodyNode(popupEngine = popupEngine).also(overlayPanel::setBodyContent) fun updateCursor(mouseX: Int, mouseY: Int) { cursorX = mouseX @@ -53,3 +53,5 @@ internal class SystemColorPickerOverlayNode( panelNode.render(ctx, x, y, width, height) } } + +internal typealias SystemColorPickerOverlayNode = ColorPickerPopupOverlayNode diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt index 30cae18..97e38ae 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt @@ -1187,3 +1187,7 @@ internal class SystemColorPickerEyedropperOverlayNode( } } } + +internal typealias ColorPickerPopupBodyNode = SystemColorPickerPopupBodyNode + +internal typealias ColorPickerTransientOverlayNode = SystemColorPickerTransientOverlayNode diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt index 55befbe..968d500 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt @@ -2,9 +2,9 @@ package org.dreamfinity.dsgl.core.overlay.system import org.dreamfinity.dsgl.core.DomTree import org.dreamfinity.dsgl.core.colorpicker.* +import org.dreamfinity.dsgl.core.colorpicker.internal.ColorPickerPopupOverlayNode +import org.dreamfinity.dsgl.core.colorpicker.internal.ColorPickerTransientOverlayNode import org.dreamfinity.dsgl.core.colorpicker.internal.InspectorColorPickerHost -import org.dreamfinity.dsgl.core.colorpicker.internal.SystemColorPickerOverlayNode -import org.dreamfinity.dsgl.core.colorpicker.internal.SystemColorPickerTransientOverlayNode import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.elements.SingleLineInputNode import org.dreamfinity.dsgl.core.dom.layout.Rect @@ -372,13 +372,13 @@ class SystemOverlayHost( panelState = state.panelState, dragSession = state.dragSession, ) - override val node: SystemColorPickerOverlayNode = - SystemColorPickerOverlayNode( + override val node: ColorPickerPopupOverlayNode = + ColorPickerPopupOverlayNode( popupEngine = popupEngine, overlayPanel = overlayPanel, ) - private val transientNode: SystemColorPickerTransientOverlayNode = - SystemColorPickerTransientOverlayNode(popupEngine = popupEngine) + private val transientNode: ColorPickerTransientOverlayNode = + ColorPickerTransientOverlayNode(popupEngine = popupEngine) private var draggable: Boolean = true private var viewportWidth: Int = 1 private var viewportHeight: Int = 1 From 008f017a8c4057fd6eba9b030d74bd3212766f61 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sun, 26 Apr 2026 16:39:38 +0300 Subject: [PATCH 45/78] moving colour picker popup to separate mount; --- .../internal/ColorPickerPopupMount.kt | 31 ++++++ .../core/overlay/system/SystemOverlayHost.kt | 97 +++++++++---------- 2 files changed, 77 insertions(+), 51 deletions(-) create mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/ColorPickerPopupMount.kt diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/ColorPickerPopupMount.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/ColorPickerPopupMount.kt new file mode 100644 index 0000000..01322b5 --- /dev/null +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/ColorPickerPopupMount.kt @@ -0,0 +1,31 @@ +package org.dreamfinity.dsgl.core.colorpicker.internal + +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupEngine +import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanel +import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelDragSession +import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelState + +internal class ColorPickerPopupMount( + ownerId: Any, + panelState: OverlayPanelState, + dragSession: OverlayPanelDragSession, + initialOwnerToken: Any = Any(), +) { + val ownerToken: Any = initialOwnerToken + val popupEngine: ColorPickerPopupEngine = ColorPickerPopupEngine() + val overlayPanel: OverlayPanel = + OverlayPanel( + ownerId = ownerId, + panelState = panelState, + dragSession = dragSession, + ) + + val node: ColorPickerPopupOverlayNode = + ColorPickerPopupOverlayNode( + popupEngine = popupEngine, + overlayPanel = overlayPanel, + ) + + val transientNode: ColorPickerTransientOverlayNode = + ColorPickerTransientOverlayNode(popupEngine = popupEngine) +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt index 968d500..b0421e1 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt @@ -2,8 +2,8 @@ package org.dreamfinity.dsgl.core.overlay.system import org.dreamfinity.dsgl.core.DomTree import org.dreamfinity.dsgl.core.colorpicker.* +import org.dreamfinity.dsgl.core.colorpicker.internal.ColorPickerPopupMount import org.dreamfinity.dsgl.core.colorpicker.internal.ColorPickerPopupOverlayNode -import org.dreamfinity.dsgl.core.colorpicker.internal.ColorPickerTransientOverlayNode import org.dreamfinity.dsgl.core.colorpicker.internal.InspectorColorPickerHost import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.elements.SingleLineInputNode @@ -364,21 +364,13 @@ class SystemOverlayHost( order = 200, lane = SystemOverlayLane.PanelContent, ) - private val ownerToken: Any = Any() - private val popupEngine: ColorPickerPopupEngine = ColorPickerPopupEngine() - private val overlayPanel: OverlayPanel = - OverlayPanel( + private val popupMount: ColorPickerPopupMount = + ColorPickerPopupMount( ownerId = state.id, panelState = state.panelState, dragSession = state.dragSession, ) - override val node: ColorPickerPopupOverlayNode = - ColorPickerPopupOverlayNode( - popupEngine = popupEngine, - overlayPanel = overlayPanel, - ) - private val transientNode: ColorPickerTransientOverlayNode = - ColorPickerTransientOverlayNode(popupEngine = popupEngine) + override val node: ColorPickerPopupOverlayNode = popupMount.node private var draggable: Boolean = true private var viewportWidth: Int = 1 private var viewportHeight: Int = 1 @@ -387,39 +379,39 @@ class SystemOverlayHost( override fun sync(frame: SystemOverlayFrameContext) { node.updateCursor(frame.cursorX, frame.cursorY) - state.active = popupEngine.isOpenFor(ownerToken) + state.active = popupMount.popupEngine.isOpenFor(popupMount.ownerToken) if (!state.active) { state.panelState.hide() state.dragSession.end() return } - overlayPanel.configure( - title = popupEngine.debugTitle(ownerToken) ?: "Color Picker", + popupMount.overlayPanel.configure( + title = popupMount.popupEngine.debugTitle(popupMount.ownerToken) ?: "Color Picker", draggable = draggable, style = - popupEngine - .debugStyle(ownerToken) + popupMount.popupEngine + .debugStyle(popupMount.ownerToken) ?.let { toOverlayPanelStyle(it) } ?: OverlayPanelStyle(), onClose = ::close, ) - val panelRect = popupEngine.debugPanelRect(ownerToken) + val panelRect = popupMount.popupEngine.debugPanelRect(popupMount.ownerToken) if (panelRect != null) { - overlayPanel.syncPanelRect(panelRect) + popupMount.overlayPanel.syncPanelRect(panelRect) } else { state.panelState.show() - overlayPanel.syncPanelRect(state.panelState.currentRectOrNull()) + popupMount.overlayPanel.syncPanelRect(state.panelState.currentRectOrNull()) } - if (overlayPanel.handleMouseMove( + if (popupMount.overlayPanel.handleMouseMove( mouseX = frame.cursorX, mouseY = frame.cursorY, viewportWidth = viewportWidth, viewportHeight = viewportHeight, ) { rect -> - popupEngine.forcePanelRect(ownerToken, rect) + popupMount.popupEngine.forcePanelRect(popupMount.ownerToken, rect) } ) { - popupEngine.onCursorPosition(frame.cursorX, frame.cursorY) + popupMount.popupEngine.onCursorPosition(frame.cursorX, frame.cursorY) } node.syncInputFocusForDomEditing() } @@ -427,60 +419,60 @@ class SystemOverlayHost( override fun onInputFrame(viewportWidth: Int, viewportHeight: Int) { this.viewportWidth = viewportWidth this.viewportHeight = viewportHeight - popupEngine.onFrame(viewportWidth, viewportHeight) + popupMount.popupEngine.onFrame(viewportWidth, viewportHeight) } override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean { if (!state.active) return false - popupEngine.onCursorPosition(mouseX, mouseY) - if (overlayPanel.handleMouseMove( + popupMount.popupEngine.onCursorPosition(mouseX, mouseY) + if (popupMount.overlayPanel.handleMouseMove( mouseX = mouseX, mouseY = mouseY, viewportWidth = viewportWidth, viewportHeight = viewportHeight, ) { rect -> - popupEngine.forcePanelRect(ownerToken, rect) + popupMount.popupEngine.forcePanelRect(popupMount.ownerToken, rect) } ) { - popupEngine.onCursorPosition(mouseX, mouseY) + popupMount.popupEngine.onCursorPosition(mouseX, mouseY) return true } - return popupEngine.handleMouseMove(mouseX, mouseY) + return popupMount.popupEngine.handleMouseMove(mouseX, mouseY) } override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { if (!state.active) return false - if (overlayPanel.handleMouseDown(mouseX, mouseY, button)) { + if (popupMount.overlayPanel.handleMouseDown(mouseX, mouseY, button)) { return true } - if (popupEngine.shouldRouteSystemInputSlotMouseDownToDom(mouseX, mouseY, button)) { - return popupEngine.focusSystemInputSlotForDomEditing(mouseX, mouseY) { index -> + if (popupMount.popupEngine.shouldRouteSystemInputSlotMouseDownToDom(mouseX, mouseY, button)) { + return popupMount.popupEngine.focusSystemInputSlotForDomEditing(mouseX, mouseY) { index -> node.focusInputSlot(index, mouseX, mouseY) } } - return popupEngine.handleMouseDown(mouseX, mouseY, button) + return popupMount.popupEngine.handleMouseDown(mouseX, mouseY, button) } override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { if (!state.active) return false - if (overlayPanel.handleMouseUp( + if (popupMount.overlayPanel.handleMouseUp( mouseX = mouseX, mouseY = mouseY, button = button, viewportWidth = viewportWidth, viewportHeight = viewportHeight, ) { rect -> - popupEngine.forcePanelRect(ownerToken, rect) + popupMount.popupEngine.forcePanelRect(popupMount.ownerToken, rect) } ) { return true } - return popupEngine.handleMouseUp(mouseX, mouseY, button) + return popupMount.popupEngine.handleMouseUp(mouseX, mouseY, button) } override fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean { if (!state.active) return false - return popupEngine.handleMouseWheel(mouseX, mouseY, delta) + return popupMount.popupEngine.handleMouseWheel(mouseX, mouseY, delta) } override fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean { @@ -488,11 +480,11 @@ class SystemOverlayHost( if (shouldRouteSystemTextInputKeyDownToDom()) { return false } - return popupEngine.handleKeyDown(keyCode, keyChar) + return popupMount.popupEngine.handleKeyDown(keyCode, keyChar) } private fun shouldRouteSystemTextInputKeyDownToDom(): Boolean { - if (popupEngine.debugOwnerScope(ownerToken) != OverlayOwnerScope.System) return false + if (popupMount.popupEngine.debugOwnerScope(popupMount.ownerToken) != OverlayOwnerScope.System) return false val focused = FocusManager.focusedNode() ?: return false if (focused !is SingleLineInputNode) return false val key = focused.key as? String ?: return false @@ -513,9 +505,9 @@ class SystemOverlayHost( onClose: (() -> Unit)?, ) { this.draggable = draggable - popupEngine.open( + popupMount.popupEngine.open( ColorPickerPopupRequest( - owner = ownerToken, + owner = popupMount.ownerToken, ownerScope = OverlayOwnerScope.System, anchorRect = anchorRect, title = title, @@ -533,34 +525,37 @@ class SystemOverlayHost( } override fun close() { - popupEngine.close(ownerToken) + popupMount.popupEngine.close(popupMount.ownerToken) state.dragSession.end() state.panelState.hide() state.active = false } - override fun isOpen(): Boolean = popupEngine.isOpenFor(ownerToken) + override fun isOpen(): Boolean = popupMount.popupEngine.isOpenFor(popupMount.ownerToken) - fun transientOverlayNode(): DOMNode = transientNode + fun transientOverlayNode(): DOMNode = popupMount.transientNode fun isTransientActive(): Boolean { - val controller = popupEngine.debugController(ownerToken) ?: return false + val controller = popupMount.popupEngine.debugController(popupMount.ownerToken) ?: return false return controller.viewModeDropdownOpen() || controller.isEyedropperActive() } - fun debugHeaderRect(): Rect? = overlayPanel.headerRect() + fun debugHeaderRect(): Rect? = popupMount.overlayPanel.headerRect() - fun debugCloseRect(): Rect? = overlayPanel.closeRect() + fun debugCloseRect(): Rect? = popupMount.overlayPanel.closeRect() - fun debugBodyLayout(): ColorPickerLayout? = popupEngine.debugBodyLayout(ownerToken) + fun debugBodyLayout(): ColorPickerLayout? = popupMount.popupEngine.debugBodyLayout(popupMount.ownerToken) - fun debugState(): ColorPickerState? = popupEngine.debugController(ownerToken)?.snapshot() + fun debugState(): ColorPickerState? = + popupMount.popupEngine + .debugController(popupMount.ownerToken) + ?.snapshot() fun captureEyedropperSample() { - popupEngine.captureEyedropperSample() + popupMount.popupEngine.captureEyedropperSample() } - fun debugOwnerScope(): OverlayOwnerScope? = popupEngine.debugOwnerScope(ownerToken) + fun debugOwnerScope(): OverlayOwnerScope? = popupMount.popupEngine.debugOwnerScope(popupMount.ownerToken) } private class ColorPickerTransientOverlayEntry( From 640d92fe6758ff7ea07aebbf1aebd63e0e02b506 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sun, 26 Apr 2026 16:54:31 +0300 Subject: [PATCH 46/78] changing how overlay input is routed; --- .../dsgl/mcForge1710/DsglScreenHost.kt | 2 + .../core/overlay/ApplicationOverlayHost.kt | 26 +++++++++--- .../overlay/ApplicationOverlayRootNode.kt | 1 + .../overlay/LiveLayerInteractionPathTests.kt | 42 +++++++++++++++++++ 4 files changed, 66 insertions(+), 5 deletions(-) diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index 1772155..33172b3 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -766,6 +766,7 @@ abstract class DsglScreenHost( control = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL), meta = Keyboard.isKeyDown(Keyboard.KEY_LMETA) || Keyboard.isKeyDown(Keyboard.KEY_RMETA), ) + runOverlayInputFrame(applicationOverlayHost) runOverlayInputFrame(systemOverlayHost) ColorPickerRuntime.engine.onFrame(lastWidth, lastHeight) val keyCode = Keyboard.getEventKey() @@ -938,6 +939,7 @@ abstract class DsglScreenHost( viewportHeight = lastHeight, viewportScale = 1f, ) + runOverlayInputFrame(applicationOverlayHost) runOverlayInputFrame(systemOverlayHost) inspectorPointerCaptured = inspector.isPointerCaptured systemOverlayHost.syncFrame( diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt index 3d257e7..80c4ce7 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt @@ -5,6 +5,7 @@ import org.dreamfinity.dsgl.core.contextmenu.ContextMenuRuntime import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.select.SelectRuntime import org.dreamfinity.dsgl.core.style.StyleApplicationScope @@ -18,6 +19,17 @@ class ApplicationOverlayHost : OverlayLayerHost { root = rootNode, styleScope = StyleApplicationScope.Application, ) + private val domInputRouter: LayerDomInputRouter = + LayerDomInputRouter( + rootProvider = { rootNode }, + ) + + override fun onInputFrame(viewportWidth: Int, viewportHeight: Int) { + rootNode.setViewportBounds( + width = viewportWidth.coerceAtLeast(1), + height = viewportHeight.coerceAtLeast(1), + ) + } override fun render(ctx: UiMeasureContext, width: Int, height: Int) { rootNode.setViewportBounds(width, height) @@ -26,18 +38,22 @@ class ApplicationOverlayHost : OverlayLayerHost { override fun paint(ctx: UiMeasureContext): List = tree.paint(ctx, applyStyles = true) - override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = false + override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = domInputRouter.handleMouseMove(mouseX, mouseY) - override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = false + override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + domInputRouter.handleMouseDown(mouseX, mouseY, button) - override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = false + override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + domInputRouter.handleMouseUp(mouseX, mouseY, button) - override fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = false + override fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = + domInputRouter.handleMouseWheel(mouseX, mouseY, delta) - override fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = false + override fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = domInputRouter.handleKeyDown(keyCode, keyChar) override fun clearRefs() { tree.clearRefs() + domInputRouter.clear() } internal fun debugRootBounds(): Rect = rootNode.bounds diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayRootNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayRootNode.kt index 5669606..26f4ea3 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayRootNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayRootNode.kt @@ -31,6 +31,7 @@ class ApplicationOverlayRootNode( internal fun setViewportBounds(width: Int, height: Int) { viewportWidth = width.coerceAtLeast(0) viewportHeight = height.coerceAtLeast(0) + bounds = Rect(0, 0, viewportWidth, viewportHeight) } override fun measure(ctx: UiMeasureContext): Size { diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt index dbc888d..50c0053 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt @@ -1,6 +1,8 @@ package org.dreamfinity.dsgl.core.overlay +import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent +import org.dreamfinity.dsgl.core.dom.elements.ButtonNode import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext @@ -177,6 +179,40 @@ class LiveLayerInteractionPathTests { assertFalse(appRootReceived) } + @Test + fun `application overlay host dom bridge consumes mounted node and blocks app-root fallthrough`() { + val applicationOverlayHost = ApplicationOverlayHost() + applicationOverlayHost.onInputFrame(1280, 720) + var clicks = 0 + ButtonNode("Overlay", key = "app-overlay-button") + .apply { + bounds = Rect(40, 44, 120, 24) + onClick { clicks += 1 } + }.applyParent(applicationOverlayRoot(applicationOverlayHost)) + + assertTrue(applicationOverlayHost.handleMouseDown(50, 50, MouseButton.LEFT)) + assertTrue(applicationOverlayHost.handleMouseUp(50, 50, MouseButton.LEFT)) + assertEquals(1, clicks) + + val harness = + LiveLayerInputHarness( + debugHandler = { _, _, _ -> false }, + systemOverlayHandler = { _, _, _ -> false }, + applicationOverlayHandler = { x, y, button -> + applicationOverlayHost.handleMouseDown(x, y, button) + }, + ) + var appRootReceived = false + val consumedBy = + harness.dispatchMouseDown(50, 50, MouseButton.LEFT) { + appRootReceived = true + true + } + + assertEquals(UiLayerId.ApplicationOverlay, consumedBy) + assertFalse(appRootReceived) + } + @Test fun `rendered system overlay content is reachable through same live interaction path`() { val systemHost = SystemOverlayHost(InspectorController()) @@ -246,4 +282,10 @@ class LiveLayerInteractionPathTests { }, ) } + + private fun applicationOverlayRoot(host: ApplicationOverlayHost): DOMNode { + val field = ApplicationOverlayHost::class.java.getDeclaredField("rootNode") + field.isAccessible = true + return field.get(host) as DOMNode + } } From 304fdc6412a0c16ae5148ee2468046857353347e Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sun, 26 Apr 2026 20:07:19 +0300 Subject: [PATCH 47/78] unifying colour picker logic with semantic action methods; --- .../core/colorpicker/ColorPickerController.kt | 206 ++++++++++++------ 1 file changed, 139 insertions(+), 67 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt index d72e409..9ce2547 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt @@ -175,23 +175,20 @@ class ColorPickerController( internal fun handleDomInputDraft(key: String, value: String): Boolean { domFocusedInputKey = key domLastFocusedInputKey = key - return applyInputDraftValue(key, value) + return semanticApplyInputDraftValue(key, value) } internal fun commitDomInputEdit(key: String, value: String): Boolean { domFocusedInputKey = key domLastFocusedInputKey = key - val applied = applyInputDraftValue(key, value) - commitCurrentColor() - clearInputEdit() - return applied + return semanticCommitInputEdit(key, value) } internal fun cancelDomInputEdit(key: String) { if (domFocusedInputKey == key) { domFocusedInputKey = null } - clearInputEdit() + semanticCancelInputEdit() } internal fun resolveDomInputValue(key: String): String = inputValues()[key].orEmpty() @@ -204,6 +201,110 @@ class ColorPickerController( return key } + internal fun semanticSetMode(mode: ColorFormatMode) { + state = state.copy(mode = mode) + modeDropdownOpen = false + requestDomInputFocusResync() + clearInputEdit() + } + + internal fun semanticSetRgbOrder(order: RgbChannelOrder) { + state = state.copy(rgbOrder = order) + modeDropdownOpen = false + requestDomInputFocusResync() + clearInputEdit() + } + + internal fun semanticToggleModeDropdown() { + modeDropdownOpen = !modeDropdownOpen + clearInputEdit() + } + + internal fun semanticCloseModeDropdown() { + modeDropdownOpen = false + } + + internal fun semanticUpdateFromField( + globalX: Int, + globalY: Int, + rect: Rect, + commit: Boolean, + ) { + updateFromField(globalX, globalY, rect, commit) + } + + internal fun semanticUpdateFromHue(globalX: Int, rect: Rect, commit: Boolean) { + updateFromHue(globalX, rect, commit) + } + + internal fun semanticUpdateFromAlpha(globalX: Int, rect: Rect, commit: Boolean) { + updateFromAlpha(globalX, rect, commit) + } + + internal fun semanticPreviewPreviousSwatch() { + applyColor(state.previous, notifyPreview = true, commit = false) + } + + internal fun semanticCommitCurrentColor() { + commitCurrentColor() + } + + internal fun semanticCopyCurrentColor() { + ColorClipboardSupport.copy(state.color, state.mode, state.alphaEnabled, state.rgbOrder) + } + + internal fun semanticPasteFromClipboard(): Boolean { + val parsed = ColorClipboardSupport.paste() ?: return false + val next = if (state.alphaEnabled) parsed.color else parsed.color.copy(a = 1f) + applyColor(next, notifyPreview = true, commit = false) + state = + state.copy( + mode = parsed.detectedMode, + rgbOrder = parsed.detectedRgbOrder ?: state.rgbOrder, + ) + return true + } + + internal fun semanticBeginEyedropper() { + beginEyedropper() + } + + internal fun semanticCancelEyedropper() { + cancelEyedropper() + } + + internal fun semanticAcceptEyedropperSelection() { + commitCurrentColor() + eyedropperActive = false + } + + internal fun semanticBeginInputEdit(key: String) { + interaction.textInput.begin(key, inputValues()[key].orEmpty()) + } + + internal fun semanticPreviewRecentSwatch(index: Int): Boolean { + val color = recentHistory.snapshot().getOrNull(index) ?: return false + applyColor(color, notifyPreview = true, commit = false) + return true + } + + internal fun semanticApplyInputDraftValue(key: String, value: String): Boolean = applyInputDraftValue(key, value) + + internal fun semanticCommitInputEdit(key: String, value: String): Boolean { + val applied = semanticApplyInputDraftValue(key, value) + semanticCommitCurrentColor() + semanticCancelInputEdit() + return applied + } + + internal fun semanticCancelInputEdit() { + clearInputEdit() + } + + internal fun semanticRequestClose() { + onRequestClose?.invoke() + } + fun beginEyedropper() { if (!state.alphaEnabled) { eyedropperBaseColor = state.color.copy(a = 1f) @@ -732,19 +833,19 @@ class ColorPickerController( } when (dragTarget) { ColorPickerDragTarget.Field -> { - updateFromField(globalX, globalY, layout.colorFieldRect, commit = false) + semanticUpdateFromField(globalX, globalY, layout.colorFieldRect, commit = false) return true } ColorPickerDragTarget.Hue -> { - updateFromHue(globalX, layout.hueRect, commit = false) + semanticUpdateFromHue(globalX, layout.hueRect, commit = false) return true } ColorPickerDragTarget.Alpha -> { val alphaRect = layout.alphaRect if (alphaRect != null) { - updateFromAlpha(globalX, alphaRect, commit = false) + semanticUpdateFromAlpha(globalX, alphaRect, commit = false) } return true } @@ -766,13 +867,12 @@ class ColorPickerController( if (eyedropperActive) { return when (button) { MouseButton.LEFT -> { - commitCurrentColor() - eyedropperActive = false + semanticAcceptEyedropperSelection() true } MouseButton.RIGHT -> { - cancelEyedropper() + semanticCancelEyedropper() true } @@ -795,100 +895,78 @@ class ColorPickerController( null } if (modeOptionHit != null) { - state = state.copy(mode = modeOptionHit.mode) - modeDropdownOpen = false - requestDomInputFocusResync() - clearInputEdit() + semanticSetMode(modeOptionHit.mode) return true } if (layout.rgbaOrderRect?.contains(globalX, globalY) == true) { - state = state.copy(rgbOrder = RgbChannelOrder.RGBA) - modeDropdownOpen = false - requestDomInputFocusResync() - clearInputEdit() + semanticSetRgbOrder(RgbChannelOrder.RGBA) return true } if (layout.argbOrderRect?.contains(globalX, globalY) == true) { - state = state.copy(rgbOrder = RgbChannelOrder.ARGB) - modeDropdownOpen = false - requestDomInputFocusResync() - clearInputEdit() + semanticSetRgbOrder(RgbChannelOrder.ARGB) return true } if (layout.modeSelectRect.contains(globalX, globalY)) { - modeDropdownOpen = !modeDropdownOpen - clearInputEdit() + semanticToggleModeDropdown() return true } if (!layout.bounds.contains(globalX, globalY)) { - modeDropdownOpen = false - clearInputEdit() + semanticCloseModeDropdown() + semanticCancelInputEdit() return false } - modeDropdownOpen = false + semanticCloseModeDropdown() if (layout.colorFieldRect.contains(globalX, globalY)) { dragTarget = ColorPickerDragTarget.Field - clearInputEdit() - updateFromField(globalX, globalY, layout.colorFieldRect, commit = false) + semanticCancelInputEdit() + semanticUpdateFromField(globalX, globalY, layout.colorFieldRect, commit = false) return true } if (layout.hueRect.contains(globalX, globalY)) { dragTarget = ColorPickerDragTarget.Hue - clearInputEdit() - updateFromHue(globalX, layout.hueRect, commit = false) + semanticCancelInputEdit() + semanticUpdateFromHue(globalX, layout.hueRect, commit = false) return true } if (layout.alphaRect?.contains(globalX, globalY) == true) { dragTarget = ColorPickerDragTarget.Alpha - clearInputEdit() - updateFromAlpha(globalX, layout.alphaRect, commit = false) + semanticCancelInputEdit() + semanticUpdateFromAlpha(globalX, layout.alphaRect, commit = false) return true } if (layout.previousSwatchRect.contains(globalX, globalY)) { - applyColor(state.previous, notifyPreview = true, commit = false) + semanticPreviewPreviousSwatch() return true } if (layout.currentSwatchRect.contains(globalX, globalY)) { - commitCurrentColor() + semanticCommitCurrentColor() return true } if (layout.copyRect.contains(globalX, globalY)) { - ColorClipboardSupport.copy(state.color, state.mode, state.alphaEnabled, state.rgbOrder) + semanticCopyCurrentColor() return true } if (layout.pasteRect.contains(globalX, globalY)) { - val parsed = ColorClipboardSupport.paste() - if (parsed != null) { - val next = if (state.alphaEnabled) parsed.color else parsed.color.copy(a = 1f) - applyColor(next, notifyPreview = true, commit = false) - state = - state.copy( - mode = parsed.detectedMode, - rgbOrder = parsed.detectedRgbOrder ?: state.rgbOrder, - ) - } + semanticPasteFromClipboard() return true } if (layout.pipetteRect.contains(globalX, globalY)) { - beginEyedropper() + semanticBeginEyedropper() return true } val inputHit = layout.inputSlots.firstOrNull { it.inputRect.contains(globalX, globalY) } if (inputHit != null) { - interaction.textInput.begin(inputHit.key, inputValues()[inputHit.key].orEmpty()) + semanticBeginInputEdit(inputHit.key) return true } - clearInputEdit() + semanticCancelInputEdit() val recentIndex = layout.recentRects.indexOfFirst { it.contains(globalX, globalY) } if (recentIndex >= 0) { - val color = recentHistory.snapshot().getOrNull(recentIndex) - if (color != null) { - applyColor(color, notifyPreview = true, commit = false) - } + semanticPreviewRecentSwatch(recentIndex) return true } @@ -901,7 +979,7 @@ class ColorPickerController( val dragged = interaction.hasActiveDragTarget() interaction.clearDragTarget() if (dragged) { - commitCurrentColor() + semanticCommitCurrentColor() return true } return eyedropperActive @@ -909,28 +987,22 @@ class ColorPickerController( fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean { if (eyedropperActive && keyCode == KeyCodes.ESCAPE) { - cancelEyedropper() + semanticCancelEyedropper() return true } if (KeyModifiers.shortcutDown && keyCode == KeyCodes.C) { - ColorClipboardSupport.copy(state.color, state.mode, state.alphaEnabled, state.rgbOrder) + semanticCopyCurrentColor() return true } if (KeyModifiers.shortcutDown && keyCode == KeyCodes.V) { - val parsed = ColorClipboardSupport.paste() ?: return true - applyColor(parsed.color, notifyPreview = true, commit = false) - state = - state.copy( - mode = parsed.detectedMode, - rgbOrder = parsed.detectedRgbOrder ?: state.rgbOrder, - ) + semanticPasteFromClipboard() return true } val key = activeInputKey ?: run { if (keyCode == KeyCodes.ESCAPE) { if (modeDropdownOpen) { - modeDropdownOpen = false + semanticCloseModeDropdown() return true } } @@ -938,7 +1010,7 @@ class ColorPickerController( } when (keyCode) { KeyCodes.ESCAPE -> { - clearInputEdit() + semanticCancelInputEdit() return true } From 44d1f9867c3e40e369e86ef3fbc9c65ce1b03505 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sun, 26 Apr 2026 21:11:20 +0300 Subject: [PATCH 48/78] adding semantic actions for color picker controller; --- .../colorpicker/ColorPickerPopupRuntime.kt | 18 +++++++++ .../internal/SystemColorPickerOverlayNode.kt | 8 ++++ .../SystemColorPickerPopupBodyNode.kt | 38 +++++++++++++++++++ .../core/overlay/system/SystemOverlayHost.kt | 20 ++++++++++ .../SystemOverlayColorPickerEntryTests.kt | 29 ++++++++++++++ 5 files changed, 113 insertions(+) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt index cc4ad57..7cdbd1e 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt @@ -401,6 +401,24 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { .any { slot -> slot.inputRect.contains(mouseX, mouseY) } } + fun shouldRouteSystemBodyIntentMouseDownToDom(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { + val current = popup ?: return false + if (current.request.ownerScope != OverlayOwnerScope.System) return false + if (button != MouseButton.LEFT) return false + if (current.controller.isEyedropperActive()) return false + refreshLayout(current) + return current.layout.previousSwatchRect + .contains(mouseX, mouseY) || + current.layout.currentSwatchRect + .contains(mouseX, mouseY) || + current.layout.copyRect + .contains(mouseX, mouseY) || + current.layout.pasteRect + .contains(mouseX, mouseY) || + current.layout.pipetteRect + .contains(mouseX, mouseY) + } + fun focusSystemInputSlotForDomEditing(mouseX: Int, mouseY: Int, focusInputByIndex: (Int) -> Boolean): Boolean { val current = popup ?: return false if (current.request.ownerScope != OverlayOwnerScope.System) return false diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerOverlayNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerOverlayNode.kt index e6f30f3..4aef90b 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerOverlayNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerOverlayNode.kt @@ -18,6 +18,7 @@ internal class ColorPickerPopupOverlayNode( private var cursorX: Int = 0 private var cursorY: Int = 0 + private var domInputRoutingReady: Boolean = false private val panelNode: DOMNode = overlayPanel.node().applyParent(this) private val bodyNode: ColorPickerPopupBodyNode = @@ -34,6 +35,12 @@ internal class ColorPickerPopupOverlayNode( bodyNode.syncFocusedInputForModeOrOrderChange() } + fun isDomInputRoutingReady(): Boolean = domInputRoutingReady + + fun resetDomInputRoutingReadiness() { + domInputRoutingReady = false + } + override fun measure(ctx: UiMeasureContext): Size = Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) @@ -50,6 +57,7 @@ internal class ColorPickerPopupOverlayNode( val panelRect = overlayPanel.panelRect() bodyNode.display = if (panelRect == null) Display.None else Display.Block + domInputRoutingReady = bodyNode.display == Display.Block panelNode.render(ctx, x, y, width, height) } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt index 97e38ae..1741d60 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt @@ -92,6 +92,10 @@ internal class SystemColorPickerPopupBodyNode( private var appliedStyle: ColorPickerStyle? = null + init { + bindSemanticBodyClickHandlers() + } + fun focusInputSlot(index: Int, mouseX: Int, mouseY: Int): Boolean { val inputNode = inputValueNodes.getOrNull(index) ?: return false if (inputNode.display == Display.None) return false @@ -549,6 +553,40 @@ internal class SystemColorPickerPopupBodyNode( } } + private fun bindSemanticBodyClickHandlers() { + val left = MouseButton.LEFT + previousSwatchNode.onMouseDown = { event -> + if (event.mouseButton == left) { + popupEngine.debugActiveController()?.semanticPreviewPreviousSwatch() + event.cancelled = true + } + } + currentSwatchNode.onMouseDown = { event -> + if (event.mouseButton == left) { + popupEngine.debugActiveController()?.semanticCommitCurrentColor() + event.cancelled = true + } + } + copyButton.onMouseDown = { event -> + if (event.mouseButton == left) { + popupEngine.debugActiveController()?.semanticCopyCurrentColor() + event.cancelled = true + } + } + pasteButton.onMouseDown = { event -> + if (event.mouseButton == left) { + popupEngine.debugActiveController()?.semanticPasteFromClipboard() + event.cancelled = true + } + } + pipetteButton.onMouseDown = { event -> + if (event.mouseButton == left) { + popupEngine.debugActiveController()?.semanticBeginEyedropper() + event.cancelled = true + } + } + } + private fun resyncFocusedInputForModeOrOrderChange(controller: ColorPickerController, layout: ColorPickerLayout) { val focusedIndex = inputValueNodes.indexOf(FocusManager.focusedNode()) val focusedSlotKey = if (focusedIndex >= 0) inputSemanticKeys.getOrNull(focusedIndex) else null diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt index b0421e1..8351203 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt @@ -374,6 +374,7 @@ class SystemOverlayHost( private var draggable: Boolean = true private var viewportWidth: Int = 1 private var viewportHeight: Int = 1 + private var domDelegatedBodyPressActive: Boolean = false override fun enablesDomInputFallbackRouting(): Boolean = true @@ -383,6 +384,8 @@ class SystemOverlayHost( if (!state.active) { state.panelState.hide() state.dragSession.end() + node.resetDomInputRoutingReadiness() + domDelegatedBodyPressActive = false return } popupMount.overlayPanel.configure( @@ -424,6 +427,7 @@ class SystemOverlayHost( override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean { if (!state.active) return false + if (domDelegatedBodyPressActive) return false popupMount.popupEngine.onCursorPosition(mouseX, mouseY) if (popupMount.overlayPanel.handleMouseMove( mouseX = mouseX, @@ -442,6 +446,9 @@ class SystemOverlayHost( override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { if (!state.active) return false + if (button != MouseButton.LEFT) { + domDelegatedBodyPressActive = false + } if (popupMount.overlayPanel.handleMouseDown(mouseX, mouseY, button)) { return true } @@ -450,11 +457,22 @@ class SystemOverlayHost( node.focusInputSlot(index, mouseX, mouseY) } } + if ( + node.isDomInputRoutingReady() && + popupMount.popupEngine.shouldRouteSystemBodyIntentMouseDownToDom(mouseX, mouseY, button) + ) { + domDelegatedBodyPressActive = true + return false + } return popupMount.popupEngine.handleMouseDown(mouseX, mouseY, button) } override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { if (!state.active) return false + if (domDelegatedBodyPressActive && button == MouseButton.LEFT) { + domDelegatedBodyPressActive = false + return false + } if (popupMount.overlayPanel.handleMouseUp( mouseX = mouseX, mouseY = mouseY, @@ -529,6 +547,8 @@ class SystemOverlayHost( state.dragSession.end() state.panelState.hide() state.active = false + node.resetDomInputRoutingReadiness() + domDelegatedBodyPressActive = false } override fun isOpen(): Boolean = popupMount.popupEngine.isOpenFor(popupMount.ownerToken) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt index be3c06d..4b84006 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt @@ -347,6 +347,35 @@ class SystemOverlayColorPickerEntryTests { assertNotEquals(0.3f, state.color.r) } + @Test + fun `system picker current swatch click commits once without double apply`() { + val host = SystemOverlayHost(InspectorController()) + val pickerHost = host.systemInspectorColorPickerPopupHost() + val root = inspectedRoot() + var commits = 0 + + pickerHost.open( + anchorRect = Rect(80, 90, 20, 18), + title = "Popup", + state = + ColorPickerState( + color = RgbaColor(0.3f, 0.5f, 0.7f, 1f), + previous = RgbaColor(0.1f, 0.2f, 0.3f, 1f), + mode = ColorFormatMode.RGB, + alphaEnabled = true, + closeOnSelect = false, + ), + onCommit = { commits += 1 }, + ) + host.onInputFrame(1200, 800) + host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 88, cursorY = 98, inspectorPointerCaptured = false) + + val currentSwatch = host.debugSystemColorPickerBodyLayout()?.currentSwatchRect ?: error("swatch rect missing") + assertTrue(host.handleMouseDown(currentSwatch.x + 2, currentSwatch.y + 2, MouseButton.LEFT)) + assertTrue(host.handleMouseUp(currentSwatch.x + 2, currentSwatch.y + 2, MouseButton.LEFT)) + assertEquals(1, commits) + } + @Test fun `system picker sync state updates current swatch without drag nudge`() { val host = SystemOverlayHost(InspectorController()) From 6a347c1aeea687dd4d431989d0a24d1ca5f92a38 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sun, 26 Apr 2026 23:08:33 +0300 Subject: [PATCH 49/78] adding test for recent swatch preview and updating logic to prevent double apply; --- .../colorpicker/ColorPickerPopupRuntime.kt | 4 +- .../SystemColorPickerPopupBodyNode.kt | 8 +++ .../SystemOverlayColorPickerEntryTests.kt | 65 +++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt index 7cdbd1e..73711cb 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt @@ -416,7 +416,9 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { current.layout.pasteRect .contains(mouseX, mouseY) || current.layout.pipetteRect - .contains(mouseX, mouseY) + .contains(mouseX, mouseY) || + current.layout.recentRects + .any { rect -> rect.contains(mouseX, mouseY) } } fun focusSystemInputSlotForDomEditing(mouseX: Int, mouseY: Int, focusInputByIndex: (Int) -> Boolean): Boolean { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt index 1741d60..5595fb1 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt @@ -476,6 +476,14 @@ internal class SystemColorPickerPopupBodyNode( palette = style color = recentColors.getOrNull(index) highlighted = index == hoveredRecent + onMouseDown = { event -> + if (event.mouseButton == MouseButton.LEFT) { + val controller = popupEngine.debugActiveController() + controller?.semanticCancelInputEdit() + controller?.semanticPreviewRecentSwatch(index) + event.cancelled = true + } + } }) add(node) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt index 4b84006..c284392 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt @@ -376,6 +376,71 @@ class SystemOverlayColorPickerEntryTests { assertEquals(1, commits) } + @Test + fun `system picker recent swatch click previews once without double apply`() { + val host = SystemOverlayHost(InspectorController()) + val pickerHost = host.systemInspectorColorPickerPopupHost() + val root = inspectedRoot() + val initial = popupState() + val previews = ArrayList() + + pickerHost.open( + anchorRect = Rect(80, 90, 20, 18), + title = "Popup", + state = initial, + onPreview = { previews += it }, + ) + host.onInputFrame(1200, 800) + host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 88, cursorY = 98, inspectorPointerCaptured = false) + + val layout = host.debugSystemColorPickerBodyLayout() ?: error("layout missing") + val field = layout.colorFieldRect + val startX = field.x + 2 + val startY = field.y + 2 + val endX = field.x + field.width - 2 + val endY = field.y + field.height - 2 + + assertTrue(host.handleMouseDown(startX, startY, MouseButton.LEFT)) + host.handleMouseMove(endX, endY) + host.syncFrame( + root, + inspectedLayoutRevision = 2L, + cursorX = endX, + cursorY = endY, + inspectorPointerCaptured = false, + ) + assertTrue(host.handleMouseUp(endX, endY, MouseButton.LEFT)) + host.syncFrame( + root, + inspectedLayoutRevision = 3L, + cursorX = endX, + cursorY = endY, + inspectorPointerCaptured = false, + ) + + val stateAfterDrag = host.debugSystemColorPickerState() ?: error("state missing") + assertNotEquals(initial.color.toArgbInt(), stateAfterDrag.color.toArgbInt()) + val previewCountBeforeRecentClick = previews.size + + host.render(ctx, 1200, 800) + val recentRect = + host + .debugSystemColorPickerBodyLayout() + ?.recentRects + ?.getOrNull(1) + ?: error("recent swatch rect missing") + assertTrue(host.handleMouseDown(recentRect.x + 1, recentRect.y + 1, MouseButton.LEFT)) + assertTrue(host.handleMouseUp(recentRect.x + 1, recentRect.y + 1, MouseButton.LEFT)) + assertEquals(previewCountBeforeRecentClick + 1, previews.size) + assertEquals( + initial.color.toArgbInt(), + host + .debugSystemColorPickerState() + ?.color + ?.toArgbInt(), + ) + } + @Test fun `system picker sync state updates current swatch without drag nudge`() { val host = SystemOverlayHost(InspectorController()) From 207a37a0dc04ea2a101f086d357bbe883bc5e34e Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Fri, 8 May 2026 21:53:33 +0300 Subject: [PATCH 50/78] reworking swatches rebuild; --- .../mc-forge-1-7-10/demo/build.gradle.kts | 2 + .../mc-forge-1-7-10/demo/gradle.properties | 1 + .../colorpicker/ColorPickerPopupRuntime.kt | 95 ++++++++++++++++--- .../SystemColorPickerPopupBodyNode.kt | 87 +++++++++-------- 4 files changed, 130 insertions(+), 55 deletions(-) diff --git a/adapters/mc-forge-1-7-10/demo/build.gradle.kts b/adapters/mc-forge-1-7-10/demo/build.gradle.kts index 6989213..0e9319d 100644 --- a/adapters/mc-forge-1-7-10/demo/build.gradle.kts +++ b/adapters/mc-forge-1-7-10/demo/build.gradle.kts @@ -23,6 +23,7 @@ val rebuildTrace: String by project val perfDebug: String by project val dsglOverlayDebug: String by project val dsglOverlayControls: String by project +val dsglColorPickerDebugCounters: String by project val hotReloadAgentLibraryName: String? by project val baseModMetadataTokens = @@ -111,6 +112,7 @@ tasks { "-Ddsgl.perf.debug=$perfDebug", "-Ddsgl.overlay.debug=$dsglOverlayDebug", "-Ddsgl.overlay.controls=$dsglOverlayControls", + "-Ddsgl.colorPicker.debugCounters=$dsglColorPickerDebugCounters", ) if (hotReload.toBoolean()) { diff --git a/adapters/mc-forge-1-7-10/demo/gradle.properties b/adapters/mc-forge-1-7-10/demo/gradle.properties index e47993f..d0e0d33 100644 --- a/adapters/mc-forge-1-7-10/demo/gradle.properties +++ b/adapters/mc-forge-1-7-10/demo/gradle.properties @@ -28,6 +28,7 @@ rebuildTrace=false perfDebug=false dsglOverlayDebug=true dsglOverlayControls=true +dsglColorPickerDebugCounters=false startParameter.offline=true diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt index 73711cb..bda0c19 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt @@ -1,5 +1,6 @@ package org.dreamfinity.dsgl.core.colorpicker +import org.dreamfinity.dsgl.core.colorpicker.internal.ColorPickerDebugCounters import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.MouseButton @@ -55,6 +56,12 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { private val headerHeight: Int = 26 private val panelPadding: Int = 6 private val positionStore: ColorPickerPopupPositionStore = ColorPickerPopupPositionStore() + private val debugCountersEnabled: Boolean = + java.lang.Boolean + .getBoolean("dsgl.colorPicker.debugCounters") + private val debugReportIntervalMs: Long = 4000L + private val nanosPerMillisecond: Double = 1_000_000.0 + private var debugNextReportAtMs: Long = 0L override fun open(request: ColorPickerPopupRequest) { val current = popup @@ -78,6 +85,7 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { val initialY = rememberedPanel?.y ?: request.anchorRect.y val initialRect = Rect(initialX, initialY, request.width.coerceAtLeast(220), 1) val initialBody = Rect(initialRect.x + panelPadding, initialRect.y + headerHeight + panelPadding, 1, 1) + ColorPickerDebugCounters.onBuildLayoutCall(request.ownerScope == OverlayOwnerScope.System) val initialLayout = controller.buildLayout(initialBody) val state = PopupState( @@ -93,6 +101,10 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { popup = state bindController(state) relayout(state, keepPosition = rememberedPanel != null) + if (debugCountersEnabled) { + ColorPickerDebugCounters.reset() + debugNextReportAtMs = System.currentTimeMillis() + debugReportIntervalMs + } } fun sync(request: ColorPickerPopupRequest) { @@ -133,6 +145,9 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { current.request.onClose ?.invoke() popup = null + if (debugCountersEnabled) { + debugNextReportAtMs = 0L + } } override fun closeAll() { @@ -142,6 +157,9 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { current.request.onClose ?.invoke() popup = null + if (debugCountersEnabled) { + debugNextReportAtMs = 0L + } } override fun isOpenFor(owner: Any): Boolean { @@ -211,6 +229,12 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { internal fun debugIsDraggingPopup(): Boolean = popup?.dragModel?.dragging == true + internal fun debugResetCounters() { + ColorPickerDebugCounters.reset() + } + + internal fun debugCountersSnapshot(): ColorPickerDebugCounters.Snapshot = ColorPickerDebugCounters.snapshot() + internal fun forcePanelRect(owner: Any, panelRect: Rect) { val current = popup ?: return if (current.owner != owner) return @@ -226,6 +250,7 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { } fun onFrame(viewportWidth: Int, viewportHeight: Int) { + reportDebugCountersIfDue() if (this.viewportWidth != viewportWidth || this.viewportHeight != viewportHeight) { this.viewportWidth = viewportWidth this.viewportHeight = viewportHeight @@ -397,8 +422,11 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { if (current.request.ownerScope != OverlayOwnerScope.System) return false if (button != MouseButton.LEFT) return false if (current.controller.isEyedropperActive()) return false - return current.layout.inputSlots - .any { slot -> slot.inputRect.contains(mouseX, mouseY) } + val hit = + current.layout.inputSlots + .any { slot -> slot.inputRect.contains(mouseX, mouseY) } + ColorPickerDebugCounters.onRouteSystemInputSlotCheck(hit) + return hit } fun shouldRouteSystemBodyIntentMouseDownToDom(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { @@ -407,18 +435,21 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { if (button != MouseButton.LEFT) return false if (current.controller.isEyedropperActive()) return false refreshLayout(current) - return current.layout.previousSwatchRect - .contains(mouseX, mouseY) || - current.layout.currentSwatchRect - .contains(mouseX, mouseY) || - current.layout.copyRect - .contains(mouseX, mouseY) || - current.layout.pasteRect + val hit = + current.layout.previousSwatchRect .contains(mouseX, mouseY) || - current.layout.pipetteRect - .contains(mouseX, mouseY) || - current.layout.recentRects - .any { rect -> rect.contains(mouseX, mouseY) } + current.layout.currentSwatchRect + .contains(mouseX, mouseY) || + current.layout.copyRect + .contains(mouseX, mouseY) || + current.layout.pasteRect + .contains(mouseX, mouseY) || + current.layout.pipetteRect + .contains(mouseX, mouseY) || + current.layout.recentRects + .any { rect -> rect.contains(mouseX, mouseY) } + ColorPickerDebugCounters.onRouteSystemBodyIntentCheck(hit) + return hit } fun focusSystemInputSlotForDomEditing(mouseX: Int, mouseY: Int, focusInputByIndex: (Int) -> Boolean): Boolean { @@ -512,10 +543,13 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { state.headerRect = frame.headerRect state.bodyRect = frame.bodyRect state.closeRect = frame.closeRect + ColorPickerDebugCounters.onBuildLayoutCall(state.request.ownerScope == OverlayOwnerScope.System) state.layout = state.controller.buildLayout(frame.bodyRect) } private fun refreshLayout(state: PopupState) { + ColorPickerDebugCounters.onRefreshLayoutCall(state.request.ownerScope == OverlayOwnerScope.System) + ColorPickerDebugCounters.onBuildLayoutCall(state.request.ownerScope == OverlayOwnerScope.System) state.layout = state.controller.buildLayout(state.bodyRect) } @@ -534,6 +568,41 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { a.rgbOrder == b.rgbOrder && a.alphaEnabled == b.alphaEnabled && a.closeOnSelect == b.closeOnSelect + + private fun reportDebugCountersIfDue() { + if (!debugCountersEnabled) return + val current = popup ?: return + val now = System.currentTimeMillis() + if (debugNextReportAtMs == 0L) { + debugNextReportAtMs = now + debugReportIntervalMs + return + } + if (now < debugNextReportAtMs) return + + val snapshot = ColorPickerDebugCounters.snapshot() + val composeMs = nanosToMsString(snapshot.recentSwatchComposeNanos) + val removeMs = nanosToMsString(snapshot.recentSwatchRemoveNanos) + println( + "dsgl.colorPicker.debugCounters " + + "ownerScope=${current.request.ownerScope} " + + "recentComposeCalls=${snapshot.recentSwatchGridComposeCalls} " + + "recentCreated=${snapshot.recentSwatchNodesCreated} " + + "recentRemoved=${snapshot.recentSwatchNodesRemoved} " + + "recentComposeMs=$composeMs " + + "recentRemoveMs=$removeMs " + + "recentSnapshotReads=${snapshot.recentColorsSnapshotReads} " + + "refreshLayoutCalls=${snapshot.refreshLayoutCalls} " + + "buildLayoutCalls=${snapshot.buildLayoutCalls} " + + "bodyIntentChecks=${snapshot.routeSystemBodyIntentChecks} " + + "bodyIntentHits=${snapshot.routeSystemBodyIntentHits} " + + "renderInvalidations=${snapshot.renderInvalidationCalls}", + ) + ColorPickerDebugCounters.reset() + debugNextReportAtMs = now + debugReportIntervalMs + } + + private fun nanosToMsString(nanos: Long): String = + String.format(java.util.Locale.ROOT, "%.3f", nanos / nanosPerMillisecond) } class ColorPickerPopupManager( diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt index 5595fb1..4d7fdbe 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt @@ -57,6 +57,21 @@ internal class SystemColorPickerPopupBodyNode( scope.colorSwatch({ this.key = "dsgl-system-color-picker-swatch-current" }) + private val recentSwatchNodes: List = + (0 until RECENT_SWATCH_COUNT).map { index -> + scope.colorSwatch({ + allowEmpty = true + this.key = recentSwatchNodeKey(index) + onMouseDown = { event -> + if (event.mouseButton == MouseButton.LEFT) { + val controller = popupEngine.debugActiveController() + controller?.semanticCancelInputEdit() + controller?.semanticPreviewRecentSwatch(index) + event.cancelled = true + } + } + }) + } private val copyButton: ButtonNode = scope.button("Copy", { @@ -148,6 +163,7 @@ internal class SystemColorPickerPopupBodyNode( val modeDropdownOpen = controller.viewModeDropdownOpen() val inputValues = controller.viewInputValues() + ColorPickerDebugCounters.onRecentColorsSnapshotRead() val recentColors = controller.viewRecentColors() val definitionsByKey = controller.viewInputDefinitions().associate { it.first to it.second } @@ -466,41 +482,23 @@ internal class SystemColorPickerPopupBodyNode( recentColors: List, hoveredRecent: Int, ): List { - removeRecentSwatchSectionNodes() - return buildList(RECENT_SWATCH_COUNT) { - for (index in 0 until RECENT_SWATCH_COUNT) { - val node = - scope.colorSwatch({ - allowEmpty = true - key = recentSwatchNodeKey(index) - palette = style - color = recentColors.getOrNull(index) - highlighted = index == hoveredRecent - onMouseDown = { event -> - if (event.mouseButton == MouseButton.LEFT) { - val controller = popupEngine.debugActiveController() - controller?.semanticCancelInputEdit() - controller?.semanticPreviewRecentSwatch(index) - event.cancelled = true - } - } - }) - add(node) - } - } - } - - private fun removeRecentSwatchSectionNodes() { - val iterator = children.iterator() - while (iterator.hasNext()) { - val child = iterator.next() - val key = child.key as? String - val isRecentSwatchNode = key?.startsWith(RECENT_SWATCH_KEY_PREFIX) == true - if (isRecentSwatchNode) { - child.parent = null - iterator.remove() - } + val composeStartNanos = System.nanoTime() + for (index in 0 until RECENT_SWATCH_COUNT) { + val node = recentSwatchNodes[index] + node.bind( + style = style, + color = recentColors.getOrNull(index), + highlighted = index == hoveredRecent, + ) } + val composeDurationNanos = System.nanoTime() - composeStartNanos + ColorPickerDebugCounters.onRecentSwatchCompose( + createdNodes = 0, + removedNodes = 0, + composeDurationNanos = composeDurationNanos, + removeDurationNanos = 0L, + ) + return recentSwatchNodes } private fun renderRecentSwatch( @@ -552,7 +550,7 @@ internal class SystemColorPickerPopupBodyNode( val restoredValue = controller.resolveDomInputValue(key) if (inputNode.text != restoredValue) { inputNode.text = restoredValue - inputNode.requestRenderCommandsInvalidation() + requestRenderCommandsInvalidationTracked(inputNode) } event.cancelled = true } @@ -685,7 +683,7 @@ internal class SystemColorPickerPopupBodyNode( changed = true } if (changed) { - button.requestRenderCommandsInvalidation() + requestRenderCommandsInvalidationTracked(button) } } @@ -700,7 +698,7 @@ internal class SystemColorPickerPopupBodyNode( changed = true } if (changed) { - node.requestRenderCommandsInvalidation() + requestRenderCommandsInvalidationTracked(node) } } @@ -744,7 +742,7 @@ internal class SystemColorPickerPopupBodyNode( changed = true } if (changed) { - node.requestRenderCommandsInvalidation() + requestRenderCommandsInvalidationTracked(node) } } @@ -964,7 +962,7 @@ internal class SystemColorPickerModeDropdownOverlayNode( changed = true } if (changed) { - button.requestRenderCommandsInvalidation() + requestRenderCommandsInvalidationTracked(button) } } @@ -979,7 +977,7 @@ internal class SystemColorPickerModeDropdownOverlayNode( changed = true } if (changed) { - node.requestRenderCommandsInvalidation() + requestRenderCommandsInvalidationTracked(node) } } @@ -1191,7 +1189,7 @@ internal class SystemColorPickerEyedropperOverlayNode( changed = true } if (changed) { - node.requestRenderCommandsInvalidation() + requestRenderCommandsInvalidationTracked(node) } } @@ -1206,7 +1204,7 @@ internal class SystemColorPickerEyedropperOverlayNode( changed = true } if (changed) { - node.requestRenderCommandsInvalidation() + requestRenderCommandsInvalidationTracked(node) } } @@ -1237,3 +1235,8 @@ internal class SystemColorPickerEyedropperOverlayNode( internal typealias ColorPickerPopupBodyNode = SystemColorPickerPopupBodyNode internal typealias ColorPickerTransientOverlayNode = SystemColorPickerTransientOverlayNode + +private fun requestRenderCommandsInvalidationTracked(node: DOMNode) { + ColorPickerDebugCounters.onRenderInvalidationCall() + node.requestRenderCommandsInvalidation() +} From e0b5a70060b305832c0d22863553bbd4bd6639cf Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Fri, 8 May 2026 22:37:32 +0300 Subject: [PATCH 51/78] a little workaround to ensure layout; --- .../colorpicker/ColorPickerPopupRuntime.kt | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt index bda0c19..af021cb 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt @@ -37,6 +37,14 @@ data class ColorPickerPopupRequest( ) class ColorPickerPopupEngine : ColorPickerPopupHost { + private data class LayoutDirtyKey( + val bodyRect: Rect, + val mode: ColorFormatMode, + val rgbOrder: RgbChannelOrder, + val alphaEnabled: Boolean, + val modeDropdownOpen: Boolean, + ) + private data class PopupState( val owner: Any, var request: ColorPickerPopupRequest, @@ -46,6 +54,7 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { var bodyRect: Rect, var closeRect: Rect, var layout: ColorPickerLayout, + var layoutDirtyKey: LayoutDirtyKey? = null, val dragModel: FloatingPaneDragModel = FloatingPaneDragModel(), var consumedEyedropperPress: Boolean = false, ) @@ -543,14 +552,37 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { state.headerRect = frame.headerRect state.bodyRect = frame.bodyRect state.closeRect = frame.closeRect - ColorPickerDebugCounters.onBuildLayoutCall(state.request.ownerScope == OverlayOwnerScope.System) - state.layout = state.controller.buildLayout(frame.bodyRect) + rebuildLayout(state) } private fun refreshLayout(state: PopupState) { ColorPickerDebugCounters.onRefreshLayoutCall(state.request.ownerScope == OverlayOwnerScope.System) + ensureLayoutUpToDate(state) + } + + private fun ensureLayoutUpToDate(state: PopupState) { + val nextKey = resolveLayoutDirtyKey(state) + if (state.layoutDirtyKey == nextKey) { + return + } + rebuildLayout(state) + } + + private fun rebuildLayout(state: PopupState) { ColorPickerDebugCounters.onBuildLayoutCall(state.request.ownerScope == OverlayOwnerScope.System) state.layout = state.controller.buildLayout(state.bodyRect) + state.layoutDirtyKey = resolveLayoutDirtyKey(state) + } + + private fun resolveLayoutDirtyKey(state: PopupState): LayoutDirtyKey { + val snapshot = state.controller.snapshot() + return LayoutDirtyKey( + bodyRect = state.bodyRect, + mode = snapshot.mode, + rgbOrder = snapshot.rgbOrder, + alphaEnabled = snapshot.alphaEnabled, + modeDropdownOpen = state.controller.viewModeDropdownOpen(), + ) } private fun drawBorder(out: MutableList, rect: Rect, color: Int) { From 327f77859cb64a8b8dd460f1aae82254237024c2 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sat, 9 May 2026 00:26:39 +0300 Subject: [PATCH 52/78] adding tests for colour picker mode and RGB order interactions, and implementing semantic actions without double apply; --- .../colorpicker/ColorPickerPopupRuntime.kt | 8 + .../internal/ColorPickerDebugCounters.kt | 172 ++++++++++++++++++ .../SystemColorPickerPopupBodyNode.kt | 24 +++ .../SystemOverlayColorPickerEntryTests.kt | 150 +++++++++++++++ 4 files changed, 354 insertions(+) create mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/ColorPickerDebugCounters.kt diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt index af021cb..ec062ac 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt @@ -455,6 +455,14 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { .contains(mouseX, mouseY) || current.layout.pipetteRect .contains(mouseX, mouseY) || + current.layout.rgbaOrderRect + ?.contains(mouseX, mouseY) == true || + current.layout.argbOrderRect + ?.contains(mouseX, mouseY) == true || + current.layout.modeSelectRect + .contains(mouseX, mouseY) || + current.layout.modeOptionsRect + ?.contains(mouseX, mouseY) == true || current.layout.recentRects .any { rect -> rect.contains(mouseX, mouseY) } ColorPickerDebugCounters.onRouteSystemBodyIntentCheck(hit) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/ColorPickerDebugCounters.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/ColorPickerDebugCounters.kt new file mode 100644 index 0000000..50d1006 --- /dev/null +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/ColorPickerDebugCounters.kt @@ -0,0 +1,172 @@ +package org.dreamfinity.dsgl.core.colorpicker.internal + +internal object ColorPickerDebugCounters { + @Volatile + private var enabled: Boolean = + java.lang.Boolean + .getBoolean("dsgl.colorPicker.debugCounters") + + @Volatile + private var recentSwatchGridComposeCalls: Long = 0L + + @Volatile + private var recentSwatchNodesCreated: Long = 0L + + @Volatile + private var recentSwatchNodesRemoved: Long = 0L + + @Volatile + private var recentSwatchComposeNanos: Long = 0L + + @Volatile + private var recentSwatchRemoveNanos: Long = 0L + + @Volatile + private var recentColorsSnapshotReads: Long = 0L + + @Volatile + private var refreshLayoutCalls: Long = 0L + + @Volatile + private var refreshLayoutSystemCalls: Long = 0L + + @Volatile + private var buildLayoutCalls: Long = 0L + + @Volatile + private var buildLayoutSystemCalls: Long = 0L + + @Volatile + private var routeSystemInputSlotChecks: Long = 0L + + @Volatile + private var routeSystemInputSlotHits: Long = 0L + + @Volatile + private var routeSystemBodyIntentChecks: Long = 0L + + @Volatile + private var routeSystemBodyIntentHits: Long = 0L + + @Volatile + private var renderInvalidationCalls: Long = 0L + + data class Snapshot( + val recentSwatchGridComposeCalls: Long, + val recentSwatchNodesCreated: Long, + val recentSwatchNodesRemoved: Long, + val recentSwatchComposeNanos: Long, + val recentSwatchRemoveNanos: Long, + val recentColorsSnapshotReads: Long, + val refreshLayoutCalls: Long, + val refreshLayoutSystemCalls: Long, + val buildLayoutCalls: Long, + val buildLayoutSystemCalls: Long, + val routeSystemInputSlotChecks: Long, + val routeSystemInputSlotHits: Long, + val routeSystemBodyIntentChecks: Long, + val routeSystemBodyIntentHits: Long, + val renderInvalidationCalls: Long, + ) + + fun reset() { + recentSwatchGridComposeCalls = 0L + recentSwatchNodesCreated = 0L + recentSwatchNodesRemoved = 0L + recentSwatchComposeNanos = 0L + recentSwatchRemoveNanos = 0L + recentColorsSnapshotReads = 0L + refreshLayoutCalls = 0L + refreshLayoutSystemCalls = 0L + buildLayoutCalls = 0L + buildLayoutSystemCalls = 0L + routeSystemInputSlotChecks = 0L + routeSystemInputSlotHits = 0L + routeSystemBodyIntentChecks = 0L + routeSystemBodyIntentHits = 0L + renderInvalidationCalls = 0L + } + + fun snapshot(): Snapshot = + Snapshot( + recentSwatchGridComposeCalls = recentSwatchGridComposeCalls, + recentSwatchNodesCreated = recentSwatchNodesCreated, + recentSwatchNodesRemoved = recentSwatchNodesRemoved, + recentSwatchComposeNanos = recentSwatchComposeNanos, + recentSwatchRemoveNanos = recentSwatchRemoveNanos, + recentColorsSnapshotReads = recentColorsSnapshotReads, + refreshLayoutCalls = refreshLayoutCalls, + refreshLayoutSystemCalls = refreshLayoutSystemCalls, + buildLayoutCalls = buildLayoutCalls, + buildLayoutSystemCalls = buildLayoutSystemCalls, + routeSystemInputSlotChecks = routeSystemInputSlotChecks, + routeSystemInputSlotHits = routeSystemInputSlotHits, + routeSystemBodyIntentChecks = routeSystemBodyIntentChecks, + routeSystemBodyIntentHits = routeSystemBodyIntentHits, + renderInvalidationCalls = renderInvalidationCalls, + ) + + fun onRecentSwatchCompose( + createdNodes: Int, + removedNodes: Int, + composeDurationNanos: Long, + removeDurationNanos: Long, + ) { + if (!enabled) return + recentSwatchGridComposeCalls += 1L + if (createdNodes > 0) { + recentSwatchNodesCreated += createdNodes.toLong() + } + if (removedNodes > 0) { + recentSwatchNodesRemoved += removedNodes.toLong() + } + if (composeDurationNanos > 0L) { + recentSwatchComposeNanos += composeDurationNanos + } + if (removeDurationNanos > 0L) { + recentSwatchRemoveNanos += removeDurationNanos + } + } + + fun onRecentColorsSnapshotRead() { + if (!enabled) return + recentColorsSnapshotReads += 1L + } + + fun onRefreshLayoutCall(systemOwner: Boolean) { + if (!enabled) return + refreshLayoutCalls += 1L + if (systemOwner) { + refreshLayoutSystemCalls += 1L + } + } + + fun onBuildLayoutCall(systemOwner: Boolean) { + if (!enabled) return + buildLayoutCalls += 1L + if (systemOwner) { + buildLayoutSystemCalls += 1L + } + } + + fun onRouteSystemInputSlotCheck(hit: Boolean) { + if (!enabled) return + routeSystemInputSlotChecks += 1L + if (hit) { + routeSystemInputSlotHits += 1L + } + } + + fun onRouteSystemBodyIntentCheck(hit: Boolean) { + if (!enabled) return + routeSystemBodyIntentChecks += 1L + if (hit) { + routeSystemBodyIntentHits += 1L + } + } + + fun onRenderInvalidationCall() { + if (!enabled) return + renderInvalidationCalls += 1L + } +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt index 4d7fdbe..56c254a 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt @@ -591,6 +591,24 @@ internal class SystemColorPickerPopupBodyNode( event.cancelled = true } } + rgbaOrderButton.onMouseDown = { event -> + if (event.mouseButton == left) { + popupEngine.debugActiveController()?.semanticSetRgbOrder(RgbChannelOrder.RGBA) + event.cancelled = true + } + } + argbOrderButton.onMouseDown = { event -> + if (event.mouseButton == left) { + popupEngine.debugActiveController()?.semanticSetRgbOrder(RgbChannelOrder.ARGB) + event.cancelled = true + } + } + modeSelectButton.onMouseDown = { event -> + if (event.mouseButton == left) { + popupEngine.debugActiveController()?.semanticToggleModeDropdown() + event.cancelled = true + } + } } private fun resyncFocusedInputForModeOrOrderChange(controller: ColorPickerController, layout: ColorPickerLayout) { @@ -816,6 +834,12 @@ internal class SystemColorPickerModeDropdownOverlayNode( mode.name, { this.key = "dsgl-system-color-picker-mode-option-${mode.name.lowercase()}" + onMouseDown = { event -> + if (event.mouseButton == MouseButton.LEFT) { + popupEngine.debugActiveController()?.semanticSetMode(mode) + event.cancelled = true + } + } }, ) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt index c284392..69e7ceb 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt @@ -657,6 +657,156 @@ class SystemOverlayColorPickerEntryTests { assertEquals("dsgl-system-color-picker-input-value-1", FocusManager.focusedNode()?.key) } + @Test + fun `system picker rgb order buttons use dom semantic actions without double apply`() { + val host = SystemOverlayHost(InspectorController()) + val pickerHost = host.systemInspectorColorPickerPopupHost() + val root = inspectedRoot() + var previews = 0 + var commits = 0 + + pickerHost.open( + anchorRect = Rect(120, 120, 20, 18), + title = "Popup", + state = popupState().copy(mode = ColorFormatMode.RGB, rgbOrder = RgbChannelOrder.RGBA), + onPreview = { previews += 1 }, + onCommit = { commits += 1 }, + ) + host.onInputFrame(1200, 800) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 128, + cursorY = 128, + inspectorPointerCaptured = false, + ) + + val initialLayout = host.debugSystemColorPickerBodyLayout() ?: error("layout missing") + val redInput = initialLayout.inputSlots.firstOrNull { it.key == "r" } ?: error("r input missing") + assertTrue(host.handleMouseDown(redInput.inputRect.x + 2, redInput.inputRect.y + 2, MouseButton.LEFT)) + host.render(ctx, 1200, 800) + + val argbButton = initialLayout.argbOrderRect ?: error("argb button missing") + assertTrue(host.handleMouseDown(argbButton.x + 2, argbButton.y + 2, MouseButton.LEFT)) + assertTrue(host.handleMouseUp(argbButton.x + 2, argbButton.y + 2, MouseButton.LEFT)) + host.syncFrame( + root, + inspectedLayoutRevision = 2L, + cursorX = argbButton.x + 2, + cursorY = argbButton.y + 2, + inspectorPointerCaptured = false, + ) + + val updated = host.debugSystemColorPickerState() ?: error("state missing") + assertEquals(RgbChannelOrder.ARGB, updated.rgbOrder) + assertEquals(ColorFormatMode.RGB, updated.mode) + assertEquals(0, previews) + assertEquals(0, commits) + assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerTransient)) + } + + @Test + fun `system picker mode trigger toggles dropdown through dom path without double apply`() { + val host = SystemOverlayHost(InspectorController()) + val pickerHost = host.systemInspectorColorPickerPopupHost() + val root = inspectedRoot() + + pickerHost.open(anchorRect = Rect(120, 120, 20, 18), title = "Popup", state = popupState()) + host.onInputFrame(1200, 800) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 128, + cursorY = 128, + inspectorPointerCaptured = false, + ) + + val initialLayout = host.debugSystemColorPickerBodyLayout() ?: error("layout missing") + val modeSelect = initialLayout.modeSelectRect + assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerTransient)) + + assertTrue(host.handleMouseDown(modeSelect.x + 2, modeSelect.y + 2, MouseButton.LEFT)) + assertTrue(host.handleMouseUp(modeSelect.x + 2, modeSelect.y + 2, MouseButton.LEFT)) + host.syncFrame( + root, + inspectedLayoutRevision = 2L, + cursorX = modeSelect.x + 2, + cursorY = modeSelect.y + 2, + inspectorPointerCaptured = false, + ) + + assertTrue(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerTransient)) + assertNotNull(host.debugSystemColorPickerBodyLayout()?.modeOptionsRect) + + assertTrue(host.handleMouseDown(modeSelect.x + 2, modeSelect.y + 2, MouseButton.LEFT)) + assertTrue(host.handleMouseUp(modeSelect.x + 2, modeSelect.y + 2, MouseButton.LEFT)) + host.syncFrame( + root, + inspectedLayoutRevision = 3L, + cursorX = modeSelect.x + 2, + cursorY = modeSelect.y + 2, + inspectorPointerCaptured = false, + ) + + assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerTransient)) + assertTrue(host.debugSystemColorPickerBodyLayout()?.modeOptionsRect == null) + } + + @Test + fun `system picker mode option click changes mode and closes dropdown via dom path`() { + val host = SystemOverlayHost(InspectorController()) + val pickerHost = host.systemInspectorColorPickerPopupHost() + val root = inspectedRoot() + var previews = 0 + var commits = 0 + + pickerHost.open( + anchorRect = Rect(120, 120, 20, 18), + title = "Popup", + state = popupState(), + onPreview = { previews += 1 }, + onCommit = { commits += 1 }, + ) + host.onInputFrame(1200, 800) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 128, + cursorY = 128, + inspectorPointerCaptured = false, + ) + + val initialLayout = host.debugSystemColorPickerBodyLayout() ?: error("layout missing") + val modeSelect = initialLayout.modeSelectRect + assertTrue(host.handleMouseDown(modeSelect.x + 2, modeSelect.y + 2, MouseButton.LEFT)) + assertTrue(host.handleMouseUp(modeSelect.x + 2, modeSelect.y + 2, MouseButton.LEFT)) + host.syncFrame( + root, + inspectedLayoutRevision = 2L, + cursorX = modeSelect.x + 2, + cursorY = modeSelect.y + 2, + inspectorPointerCaptured = false, + ) + + val expandedLayout = host.debugSystemColorPickerBodyLayout() ?: error("expanded layout missing") + val hslOption = + expandedLayout.modeOptions.firstOrNull { it.mode == ColorFormatMode.HSL } ?: error("HSL option missing") + assertTrue(host.handleMouseDown(hslOption.rect.x + 2, hslOption.rect.y + 2, MouseButton.LEFT)) + assertTrue(host.handleMouseUp(hslOption.rect.x + 2, hslOption.rect.y + 2, MouseButton.LEFT)) + host.syncFrame( + root, + inspectedLayoutRevision = 3L, + cursorX = hslOption.rect.x + 2, + cursorY = hslOption.rect.y + 2, + inspectorPointerCaptured = false, + ) + + assertEquals(ColorFormatMode.HSL, host.debugSystemColorPickerState()?.mode) + assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerTransient)) + assertEquals(0, previews) + assertEquals(0, commits) + } + @Test fun `system picker mode dropdown is mounted in transient lane and stays interactive`() { val host = SystemOverlayHost(InspectorController()) From 81c078e05a770a2b584556028169da60b0d2dba6 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sat, 9 May 2026 18:23:09 +0300 Subject: [PATCH 53/78] preparing to a layered structure rework - introducing domains and surfaces; --- .../core/overlay/OverlayLayerContracts.kt | 50 +++++++++++++++++++ .../overlay/OverlayLayerContractsTests.kt | 48 ++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContracts.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContracts.kt index ea413c7..1f0d04f 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContracts.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContracts.kt @@ -14,6 +14,22 @@ enum class OverlayOwnerScope { System, } +internal enum class ScreenDomainId { + Application, + System, + Debug, +} + +internal enum class ScreenDomainSurfaceRole { + Root, + Portal, +} + +internal data class ScreenDomainSurface( + val domain: ScreenDomainId, + val role: ScreenDomainSurfaceRole, +) + object OverlayLayerContracts { val paintOrder: List = listOf( @@ -31,12 +47,46 @@ object OverlayLayerContracts { UiLayerId.ApplicationRoot, ) + internal val paintSurfaces: List = paintOrder.map(::domainSurfaceForLayer) + + internal val inputSurfaces: List = inputPriority.map(::domainSurfaceForLayer) + fun resolveTransientLayer(ownerScope: OverlayOwnerScope): UiLayerId = when (ownerScope) { OverlayOwnerScope.Application -> UiLayerId.ApplicationOverlay OverlayOwnerScope.System -> UiLayerId.SystemOverlay } + internal fun domainSurfaceForLayer(layer: UiLayerId): ScreenDomainSurface = + when (layer) { + UiLayerId.ApplicationRoot -> + ScreenDomainSurface( + domain = ScreenDomainId.Application, + role = ScreenDomainSurfaceRole.Root, + ) + + UiLayerId.ApplicationOverlay -> + ScreenDomainSurface( + domain = ScreenDomainId.Application, + role = ScreenDomainSurfaceRole.Portal, + ) + + UiLayerId.SystemOverlay -> + ScreenDomainSurface( + domain = ScreenDomainId.System, + role = ScreenDomainSurfaceRole.Portal, + ) + + UiLayerId.Debug -> + ScreenDomainSurface( + domain = ScreenDomainId.Debug, + role = ScreenDomainSurfaceRole.Root, + ) + } + + internal fun portalSurfaceForOwner(ownerScope: OverlayOwnerScope): ScreenDomainSurface = + domainSurfaceForLayer(resolveTransientLayer(ownerScope)) + @Suppress("UnusedParameter") fun resolveTransientLayer(ownerScope: OverlayOwnerScope, cursorX: Int, cursorY: Int): UiLayerId = resolveTransientLayer(ownerScope) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContractsTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContractsTests.kt index eff2c41..be05741 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContractsTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContractsTests.kt @@ -26,6 +26,54 @@ class OverlayLayerContractsTests { ) } + @Test + fun `paint surfaces map current layer order to domain surfaces`() { + assertEquals( + listOf( + ScreenDomainSurface(ScreenDomainId.Application, ScreenDomainSurfaceRole.Root), + ScreenDomainSurface(ScreenDomainId.Application, ScreenDomainSurfaceRole.Portal), + ScreenDomainSurface(ScreenDomainId.System, ScreenDomainSurfaceRole.Portal), + ScreenDomainSurface(ScreenDomainId.Debug, ScreenDomainSurfaceRole.Root), + ), + OverlayLayerContracts.paintSurfaces, + ) + } + + @Test + fun `input surfaces map current input priority to domain surfaces`() { + assertEquals( + listOf( + ScreenDomainSurface(ScreenDomainId.Debug, ScreenDomainSurfaceRole.Root), + ScreenDomainSurface(ScreenDomainId.System, ScreenDomainSurfaceRole.Portal), + ScreenDomainSurface(ScreenDomainId.Application, ScreenDomainSurfaceRole.Portal), + ScreenDomainSurface(ScreenDomainId.Application, ScreenDomainSurfaceRole.Root), + ), + OverlayLayerContracts.inputSurfaces, + ) + } + + @Test + fun `owner scope resolves to compatible portal domain surfaces`() { + assertEquals( + ScreenDomainSurface(ScreenDomainId.Application, ScreenDomainSurfaceRole.Portal), + OverlayLayerContracts.portalSurfaceForOwner(OverlayOwnerScope.Application), + ) + assertEquals( + ScreenDomainSurface(ScreenDomainId.System, ScreenDomainSurfaceRole.Portal), + OverlayLayerContracts.portalSurfaceForOwner(OverlayOwnerScope.System), + ) + } + + @Test + fun `debug layer maps to debug domain root without changing layer behavior`() { + assertEquals( + ScreenDomainSurface(ScreenDomainId.Debug, ScreenDomainSurfaceRole.Root), + OverlayLayerContracts.domainSurfaceForLayer(UiLayerId.Debug), + ) + assertEquals(UiLayerId.Debug, OverlayLayerContracts.paintOrder.last()) + assertEquals(UiLayerId.Debug, OverlayLayerContracts.inputPriority.first()) + } + @Test fun `firstInputConsumer respects configured input priority`() { val consumed = From 94b715fd5c67e344051fe7dc4beeba22c157417d Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sat, 9 May 2026 22:00:50 +0300 Subject: [PATCH 54/78] introducing portal host contracts and adding tests for overlay interaction layers; --- .../dsgl/core/overlay/PortalHostContracts.kt | 178 ++++++++++ .../core/overlay/PortalHostContractsTests.kt | 305 ++++++++++++++++++ 2 files changed, 483 insertions(+) create mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContracts.kt create mode 100644 core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContractsTests.kt diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContracts.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContracts.kt new file mode 100644 index 0000000..b40b016 --- /dev/null +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContracts.kt @@ -0,0 +1,178 @@ +package org.dreamfinity.dsgl.core.overlay + +import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.render.RenderCommand + +internal data class PortalEntryId( + val value: String, +) { + init { + require(value.isNotBlank()) { "Portal entry id must not be blank." } + } +} + +internal data class PortalEntryOrder( + val zIndex: Int, + val sequence: Int = 0, +) : Comparable { + override fun compareTo(other: PortalEntryOrder): Int = + compareValuesBy(this, other, PortalEntryOrder::zIndex, PortalEntryOrder::sequence) +} + +internal data class PortalEntryBounds( + val viewportBounds: Rect, + val entryBounds: Rect, +) { + init { + require(viewportBounds.width > 0 && viewportBounds.height > 0) { + "Portal viewport bounds must be explicit and non-empty." + } + require(entryBounds.width > 0 && entryBounds.height > 0) { + "Portal entry bounds must be explicit and non-empty." + } + } +} + +internal data class PortalEntryPlacement( + val anchorBounds: Rect?, + val bounds: PortalEntryBounds, +) + +internal enum class PortalDismissPolicy { + None, + OutsidePointerDown, + EscapeOrOutsidePointerDown, +} + +internal enum class PortalInputPolicy { + None, + DomOnly, + ManualOnly, + ManualThenDomFallback, +} + +internal enum class PortalFocusPolicy { + Preserve, + RequestFocus, + TrapFocus, +} + +internal data class PortalEntryState( + val id: PortalEntryId, + val ownerToken: Any, + val surface: ScreenDomainSurface, + val order: PortalEntryOrder, + val dismissPolicy: PortalDismissPolicy = PortalDismissPolicy.None, + val inputPolicy: PortalInputPolicy = PortalInputPolicy.DomOnly, + val focusPolicy: PortalFocusPolicy = PortalFocusPolicy.Preserve, +) { + var active: Boolean = false + internal set + var placement: PortalEntryPlacement? = null + internal set + + fun activate(placement: PortalEntryPlacement) { + this.placement = placement + active = true + } + + fun deactivate() { + active = false + placement = null + } +} + +internal data class PortalFrameContext( + val viewportBounds: Rect, +) { + init { + require(viewportBounds.width > 0 && viewportBounds.height > 0) { + "Portal frame viewport bounds must be explicit and non-empty." + } + } +} + +internal interface PortalEntry { + val state: PortalEntryState + val node: DOMNode? + + fun onInputFrame(context: PortalFrameContext) = Unit + + fun render(ctx: UiMeasureContext, width: Int, height: Int) = Unit + + fun paint(ctx: UiMeasureContext): List = emptyList() + + fun clearRefs() = Unit + + fun close() { + state.deactivate() + } + + fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = false + + fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = false + + fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = false + + fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = false + + fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = false +} + +internal class PortalHost( + val surface: ScreenDomainSurface, +) { + private val entriesById: LinkedHashMap = LinkedHashMap() + + fun register(entry: PortalEntry) { + require(entry.state.surface == surface) { + "Portal entry ${entry.state.id.value} belongs to ${entry.state.surface}, not $surface." + } + require(entriesById.putIfAbsent(entry.state.id, entry) == null) { + "Portal entry ${entry.state.id.value} is already registered." + } + } + + fun unregister(id: PortalEntryId): Boolean { + val entry = entriesById.remove(id) ?: return false + entry.clearRefs() + entry.close() + return true + } + + fun entriesInPaintOrder(): List = + entriesById.values + .filter { it.state.active } + .sortedBy { it.state.order } + + fun entriesInInputOrder(): List = entriesInPaintOrder().asReversed() + + fun onInputFrame(context: PortalFrameContext) { + entriesById.values.forEach { it.onInputFrame(context) } + } + + fun render(ctx: UiMeasureContext, width: Int, height: Int) { + entriesInPaintOrder().forEach { it.render(ctx, width, height) } + } + + fun paint(ctx: UiMeasureContext): List = entriesInPaintOrder().flatMap { it.paint(ctx) } + + fun clearRefs() { + entriesById.values.forEach { entry -> + entry.clearRefs() + entry.close() + } + entriesById.clear() + } + + fun dispatchInput(handler: (PortalEntry) -> Boolean): Boolean = entriesInInputOrder().any(handler) +} + +internal data class OverlayLayerPortalHostAdapter( + val layerHost: OverlayLayerHost, +) { + val surface: ScreenDomainSurface = OverlayLayerContracts.domainSurfaceForLayer(layerHost.layerId) +} diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContractsTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContractsTests.kt new file mode 100644 index 0000000..0346f69 --- /dev/null +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContractsTests.kt @@ -0,0 +1,305 @@ +package org.dreamfinity.dsgl.core.overlay + +import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.event.FocusManager +import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.inspector.InspectorController +import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayHost +import org.dreamfinity.dsgl.core.render.RenderCommand +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class PortalHostContractsTests { + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } + + @AfterTest + fun cleanup() { + FocusManager.clearFocus() + } + + @Test + fun `multiple portal entries preserve deterministic paint and input order`() { + val host = PortalHost(applicationSurface()) + val low = FakePortalEntry("low", FakePortalEntryConfig(order = PortalEntryOrder(zIndex = 10))) + val high = FakePortalEntry("high", FakePortalEntryConfig(order = PortalEntryOrder(zIndex = 20))) + val sameZLater = + FakePortalEntry( + "same-z-later", + FakePortalEntryConfig(order = PortalEntryOrder(zIndex = 10, sequence = 1)), + ) + host.register(high) + host.register(sameZLater) + host.register(low) + + low.activate() + high.activate() + sameZLater.activate() + + assertEquals(listOf("low", "same-z-later", "high"), host.entriesInPaintOrder().map { it.state.id.value }) + assertEquals(listOf("high", "same-z-later", "low"), host.entriesInInputOrder().map { it.state.id.value }) + } + + @Test + fun `entry cleanup removes refs and active state`() { + val host = PortalHost(applicationSurface()) + val entry = FakePortalEntry("entry") + host.register(entry) + entry.activate() + + assertTrue(host.unregister(entry.state.id)) + + assertEquals(1, entry.clearRefsCalls) + assertEquals(1, entry.closeCalls) + assertFalse(entry.state.active) + assertNull(entry.state.placement) + assertTrue(host.entriesInPaintOrder().isEmpty()) + assertFalse(host.unregister(entry.state.id)) + } + + @Test + fun `higher priority input consumes before lower priority entries`() { + val host = PortalHost(applicationSurface()) + val low = FakePortalEntry("low", FakePortalEntryConfig(order = PortalEntryOrder(zIndex = 1))) + val high = + FakePortalEntry( + id = "high", + config = + FakePortalEntryConfig( + order = PortalEntryOrder(zIndex = 2), + consumeMouseDown = true, + ), + ) + host.register(low) + host.register(high) + low.activate() + high.activate() + + assertTrue(host.dispatchInput { it.handleMouseDown(24, 28, MouseButton.LEFT) }) + + assertEquals(1, high.mouseDownCalls) + assertEquals(0, low.mouseDownCalls) + } + + @Test + fun `input falls through portal entries until one consumes`() { + val host = PortalHost(applicationSurface()) + val low = + FakePortalEntry( + id = "low", + config = + FakePortalEntryConfig( + order = PortalEntryOrder(zIndex = 1), + consumeMouseDown = true, + ), + ) + val high = FakePortalEntry("high", FakePortalEntryConfig(order = PortalEntryOrder(zIndex = 2))) + host.register(low) + host.register(high) + low.activate() + high.activate() + + assertTrue(host.dispatchInput { it.handleMouseDown(24, 28, MouseButton.LEFT) }) + + assertEquals(1, high.mouseDownCalls) + assertEquals(1, low.mouseDownCalls) + } + + @Test + fun `bounds and viewport are explicit and never default to hidden origin placement`() { + val valid = + PortalEntryPlacement( + anchorBounds = Rect(12, 14, 20, 18), + bounds = + PortalEntryBounds( + viewportBounds = Rect(0, 0, 320, 240), + entryBounds = Rect(18, 24, 120, 80), + ), + ) + val entry = FakePortalEntry("entry") + entry.state.activate(valid) + + assertEquals(valid, entry.state.placement) + assertFailsWith { + PortalEntryBounds( + viewportBounds = Rect(0, 0, 0, 240), + entryBounds = Rect(18, 24, 120, 80), + ) + } + assertFailsWith { + PortalEntryBounds( + viewportBounds = Rect(0, 0, 320, 240), + entryBounds = Rect(0, 0, 0, 0), + ) + } + assertFailsWith { + PortalFrameContext(viewportBounds = Rect(0, 0, 0, 0)) + } + } + + @Test + fun `focus policy declares preserve request and trap without changing global focus by default`() { + val preserve = FakePortalEntry("preserve", FakePortalEntryConfig(focusPolicy = PortalFocusPolicy.Preserve)) + val request = FakePortalEntry("request", FakePortalEntryConfig(focusPolicy = PortalFocusPolicy.RequestFocus)) + val trap = FakePortalEntry("trap", FakePortalEntryConfig(focusPolicy = PortalFocusPolicy.TrapFocus)) + + preserve.activate() + request.activate() + trap.activate() + + assertEquals(PortalFocusPolicy.Preserve, preserve.state.focusPolicy) + assertEquals(PortalFocusPolicy.RequestFocus, request.state.focusPolicy) + assertEquals(PortalFocusPolicy.TrapFocus, trap.state.focusPolicy) + assertNull(FocusManager.focusedNode()) + } + + @Test + fun `overlay layer adapter maps current application and system hosts to portal surfaces`() { + assertEquals( + ScreenDomainSurface(ScreenDomainId.Application, ScreenDomainSurfaceRole.Portal), + OverlayLayerPortalHostAdapter(ApplicationOverlayHost()).surface, + ) + assertEquals( + ScreenDomainSurface(ScreenDomainId.System, ScreenDomainSurfaceRole.Portal), + OverlayLayerPortalHostAdapter(SystemOverlayHost(InspectorController())).surface, + ) + } + + @Test + fun `portal host rejects entries for another domain surface`() { + val host = PortalHost(applicationSurface()) + val systemEntry = + FakePortalEntry( + id = "system", + config = + FakePortalEntryConfig( + surface = ScreenDomainSurface(ScreenDomainId.System, ScreenDomainSurfaceRole.Portal), + ), + ) + + assertFailsWith { + host.register(systemEntry) + } + } + + @Test + fun `render and paint use active entries in paint order`() { + val host = PortalHost(applicationSurface()) + val renderOrder = ArrayList() + val low = + FakePortalEntry( + id = "low", + config = + FakePortalEntryConfig( + order = PortalEntryOrder(zIndex = 1), + paintColor = 0xFF000001.toInt(), + renderOrder = renderOrder, + ), + ) + val high = + FakePortalEntry( + id = "high", + config = + FakePortalEntryConfig( + order = PortalEntryOrder(zIndex = 2), + paintColor = 0xFF000002.toInt(), + renderOrder = renderOrder, + ), + ) + host.register(high) + host.register(low) + low.activate() + high.activate() + + host.render(ctx, 320, 240) + val commands = host.paint(ctx) + + assertEquals(listOf("low", "high"), renderOrder) + assertEquals( + listOf(0xFF000001.toInt(), 0xFF000002.toInt()), + commands.map { (it as RenderCommand.DrawRect).color }, + ) + } + + private fun applicationSurface(): ScreenDomainSurface = ScreenDomainSurface(ScreenDomainId.Application, ScreenDomainSurfaceRole.Portal) + + private class FakePortalEntry( + id: String, + private val config: FakePortalEntryConfig = FakePortalEntryConfig(), + ) : PortalEntry { + override val state: PortalEntryState = + PortalEntryState( + id = PortalEntryId(id), + ownerToken = config.ownerToken, + surface = config.surface, + order = config.order, + focusPolicy = config.focusPolicy, + ) + override val node: DOMNode? = null + var clearRefsCalls: Int = 0 + private set + var closeCalls: Int = 0 + private set + var mouseDownCalls: Int = 0 + private set + var renderCalls: Int = 0 + private set + + fun activate() { + state.activate( + PortalEntryPlacement( + anchorBounds = Rect(10, 10, 20, 20), + bounds = + PortalEntryBounds( + viewportBounds = Rect(0, 0, 320, 240), + entryBounds = Rect(12, 12, 100, 80), + ), + ), + ) + } + + override fun render(ctx: UiMeasureContext, width: Int, height: Int) { + renderCalls += 1 + config.renderOrder?.add(state.id.value) + } + + override fun paint(ctx: UiMeasureContext): List = listOf(RenderCommand.DrawRect(0, 0, 1, 1, config.paintColor)) + + override fun clearRefs() { + clearRefsCalls += 1 + } + + override fun close() { + closeCalls += 1 + super.close() + } + + override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { + mouseDownCalls += 1 + return config.consumeMouseDown + } + } + + private data class FakePortalEntryConfig( + val ownerToken: Any = Any(), + val surface: ScreenDomainSurface = ScreenDomainSurface(ScreenDomainId.Application, ScreenDomainSurfaceRole.Portal), + val order: PortalEntryOrder = PortalEntryOrder(zIndex = 0), + val focusPolicy: PortalFocusPolicy = PortalFocusPolicy.Preserve, + val consumeMouseDown: Boolean = false, + val paintColor: Int = 0xFFFFFFFF.toInt(), + val renderOrder: MutableList? = null, + ) +} From bf69ec2aeda7aa9a636c6e4879d902cb77ea1661 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sat, 9 May 2026 23:35:30 +0300 Subject: [PATCH 55/78] wiring orchestrator with screenhost; --- .../dsgl/mcForge1710/DsglScreenHost.kt | 41 +++++- .../ScreenDomainSurfaceOrchestrator.kt | 63 +++++++++ .../DsglScreenHostDomainOrchestrationTests.kt | 121 ++++++++++++++++++ 3 files changed, 219 insertions(+), 6 deletions(-) create mode 100644 adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/ScreenDomainSurfaceOrchestrator.kt create mode 100644 adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index 33172b3..36940d2 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -119,6 +119,7 @@ abstract class DsglScreenHost( private val applicationOverlayHost: ApplicationOverlayHost = ApplicationOverlayHost() private val systemOverlayHost: SystemOverlayHost = SystemOverlayHost(inspector) private val debugOverlayHost: OverlayDebugControlHost = OverlayDebugControlHost() + private val domainOrchestrator: ScreenDomainSurfaceOrchestrator = ScreenDomainSurfaceOrchestrator() private val colorSamplerOwnershipRouter: ActiveColorSamplerOwnershipRouter = ActiveColorSamplerOwnershipRouter() private var activeColorSamplerOwner: ActiveColorSamplerOwner = ActiveColorSamplerOwner.None private var activeInlineColorSamplerNode: ColorPickerInlineNode? = null @@ -572,11 +573,11 @@ abstract class DsglScreenHost( rebuiltThisFrame: Boolean, layoutCommittedThisFrame: Boolean, ) { - OverlayLayerContracts.composePaintCommands( + domainOrchestrator.composePaintCommands( applicationRoot = commands, - applicationOverlay = applicationOverlayCommandsBuffer, - systemOverlay = systemOverlayCommandsBuffer, - debug = debugOverlayCommands, + applicationPortal = applicationOverlayCommandsBuffer, + systemPortal = systemOverlayCommandsBuffer, + debugRoot = debugOverlayCommands, out = stagingCommandsBuffer, shouldRenderLayer = OverlayLayerDebugState::isRenderEnabled, ) @@ -1080,7 +1081,7 @@ abstract class DsglScreenHost( inspectorMouseY: Int, ): Boolean { val consumedBy = - OverlayLayerContracts.firstInputConsumer( + domainOrchestrator.firstInputConsumer( canConsume = { layer -> when (layer) { UiLayerId.Debug -> debugOverlayHost.handleKeyDown(keyCode, keyChar) @@ -1151,7 +1152,7 @@ abstract class DsglScreenHost( val mappedButton = mapButton(mouseButton) val buttonPressed = Mouse.getEventButtonState() val consumedBy = - OverlayLayerContracts.firstInputConsumer( + domainOrchestrator.firstInputConsumer( canConsume = { layer -> when (layer) { UiLayerId.Debug -> @@ -1480,6 +1481,34 @@ abstract class DsglScreenHost( internal fun debugRebuildIfNeededForTests(): Boolean = rebuildIfNeeded() + internal fun debugComposeDomainPaintCommandsForTests( + applicationRoot: List, + applicationPortal: List, + systemPortal: List, + debugRoot: List, + shouldRenderLayer: (UiLayerId) -> Boolean = { true }, + ): List { + val out = ArrayList() + domainOrchestrator.composePaintCommands( + applicationRoot = applicationRoot, + applicationPortal = applicationPortal, + systemPortal = systemPortal, + debugRoot = debugRoot, + out = out, + shouldRenderLayer = shouldRenderLayer, + ) + return out + } + + internal fun debugFirstDomainInputConsumerForTests( + canConsume: (UiLayerId) -> Boolean, + isLayerInputEnabled: (UiLayerId) -> Boolean = { true }, + ): UiLayerId? = + domainOrchestrator.firstInputConsumer( + canConsume = canConsume, + isLayerInputEnabled = isLayerInputEnabled, + ) + private fun setDragCapture(target: DOMNode) { dragCaptureTarget = target dragCaptureKey = target.key diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/ScreenDomainSurfaceOrchestrator.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/ScreenDomainSurfaceOrchestrator.kt new file mode 100644 index 0000000..814e6cf --- /dev/null +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/ScreenDomainSurfaceOrchestrator.kt @@ -0,0 +1,63 @@ +package org.dreamfinity.dsgl.mcForge1710 + +import org.dreamfinity.dsgl.core.overlay.OverlayLayerContracts +import org.dreamfinity.dsgl.core.overlay.UiLayerId +import org.dreamfinity.dsgl.core.render.RenderCommand + +internal class ScreenDomainSurfaceOrchestrator { + private val paintSurfaces: List = + OverlayLayerContracts.paintOrder.map(RuntimeDomainSurface::fromLayer) + private val inputSurfaces: List = + OverlayLayerContracts.inputPriority.map(RuntimeDomainSurface::fromLayer) + + fun composePaintCommands( + applicationRoot: List, + applicationPortal: List, + systemPortal: List, + debugRoot: List, + out: MutableList, + shouldRenderLayer: (UiLayerId) -> Boolean = { true }, + ) { + out.clear() + paintSurfaces.forEach { surface -> + if (!shouldRenderLayer(surface.layer)) return@forEach + when (surface) { + RuntimeDomainSurface.ApplicationRoot -> out.addAll(applicationRoot) + RuntimeDomainSurface.ApplicationPortal -> out.addAll(applicationPortal) + RuntimeDomainSurface.SystemPortal -> out.addAll(systemPortal) + RuntimeDomainSurface.DebugRoot -> out.addAll(debugRoot) + } + } + } + + fun firstInputConsumer( + canConsume: (UiLayerId) -> Boolean, + isLayerInputEnabled: (UiLayerId) -> Boolean = { true }, + ): UiLayerId? { + inputSurfaces.forEach { surface -> + if (!isLayerInputEnabled(surface.layer)) return@forEach + if (canConsume(surface.layer)) return surface.layer + } + return null + } +} + +private enum class RuntimeDomainSurface( + val layer: UiLayerId, +) { + ApplicationRoot(UiLayerId.ApplicationRoot), + ApplicationPortal(UiLayerId.ApplicationOverlay), + SystemPortal(UiLayerId.SystemOverlay), + DebugRoot(UiLayerId.Debug), + ; + + companion object { + fun fromLayer(layer: UiLayerId): RuntimeDomainSurface = + when (layer) { + UiLayerId.ApplicationRoot -> ApplicationRoot + UiLayerId.ApplicationOverlay -> ApplicationPortal + UiLayerId.SystemOverlay -> SystemPortal + UiLayerId.Debug -> DebugRoot + } + } +} diff --git a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt new file mode 100644 index 0000000..415156c --- /dev/null +++ b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt @@ -0,0 +1,121 @@ +package org.dreamfinity.dsgl.mcForge1710 + +import org.dreamfinity.dsgl.core.DomTree +import org.dreamfinity.dsgl.core.DsglWindow +import org.dreamfinity.dsgl.core.dom.elements.ContainerNode +import org.dreamfinity.dsgl.core.overlay.UiLayerId +import org.dreamfinity.dsgl.core.render.RenderCommand +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class DsglScreenHostDomainOrchestrationTests { + @Test + fun `host domain paint orchestration preserves current render order`() { + val host = createHost() + + val commands = + host.debugComposeDomainPaintCommandsForTests( + applicationRoot = listOf(command(1)), + applicationPortal = listOf(command(2)), + systemPortal = listOf(command(3)), + debugRoot = listOf(command(4)), + ) + + assertEquals(listOf(1, 2, 3, 4), commandColors(commands)) + } + + @Test + fun `host domain paint orchestration preserves render debug skips`() { + val host = createHost() + + val commands = + host.debugComposeDomainPaintCommandsForTests( + applicationRoot = listOf(command(1)), + applicationPortal = listOf(command(2)), + systemPortal = listOf(command(3)), + debugRoot = listOf(command(4)), + shouldRenderLayer = { layer -> layer != UiLayerId.ApplicationOverlay }, + ) + + assertEquals(listOf(1, 3, 4), commandColors(commands)) + } + + @Test + fun `host domain input orchestration preserves current priority`() { + val host = createHost() + val visited = ArrayList() + + val consumed = + host.debugFirstDomainInputConsumerForTests( + canConsume = { layer -> + visited += layer + layer == UiLayerId.ApplicationRoot + }, + ) + + assertEquals(UiLayerId.ApplicationRoot, consumed) + assertEquals( + listOf(UiLayerId.Debug, UiLayerId.SystemOverlay, UiLayerId.ApplicationOverlay, UiLayerId.ApplicationRoot), + visited, + ) + } + + @Test + fun `host domain input orchestration blocks lower domains after consumption`() { + val host = createHost() + val visited = ArrayList() + + val consumed = + host.debugFirstDomainInputConsumerForTests( + canConsume = { layer -> + visited += layer + layer == UiLayerId.SystemOverlay + }, + ) + + assertEquals(UiLayerId.SystemOverlay, consumed) + assertEquals(listOf(UiLayerId.Debug, UiLayerId.SystemOverlay), visited) + } + + @Test + fun `host domain input orchestration preserves debug input disables`() { + val host = createHost() + val visited = ArrayList() + + val consumed = + host.debugFirstDomainInputConsumerForTests( + canConsume = { layer -> + visited += layer + layer == UiLayerId.Debug || layer == UiLayerId.ApplicationOverlay + }, + isLayerInputEnabled = { layer -> layer != UiLayerId.Debug }, + ) + + assertEquals(UiLayerId.ApplicationOverlay, consumed) + assertEquals(listOf(UiLayerId.SystemOverlay, UiLayerId.ApplicationOverlay), visited) + } + + @Test + fun `host domain input orchestration returns null when no surface consumes`() { + val host = createHost() + + val consumed = host.debugFirstDomainInputConsumerForTests(canConsume = { false }) + + assertNull(consumed) + } + + private fun createHost(): DsglScreenHost = + object : DsglScreenHost( + object : DsglWindow() { + override fun render(): DomTree = DomTree(ContainerNode(key = "root")) + }, + ) {} + + private fun command(color: Int): RenderCommand = RenderCommand.DrawRect(0, 0, 1, 1, color) + + private fun commandColors(commands: List): List = + commands.map { command -> + (command as RenderCommand.DrawRect).color + } +} From 616d08f98c49d744135db9702396009858883ddf Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sun, 10 May 2026 00:00:18 +0300 Subject: [PATCH 56/78] wiring application overlay host with portals host as temporary adapter; --- .../core/overlay/ApplicationOverlayHost.kt | 172 +++++++++++++++++- .../overlay/LiveLayerInteractionPathTests.kt | 100 ++++++++++ 2 files changed, 262 insertions(+), 10 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt index 80c4ce7..8b6eb77 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt @@ -1,7 +1,9 @@ package org.dreamfinity.dsgl.core.overlay import org.dreamfinity.dsgl.core.DomTree +import org.dreamfinity.dsgl.core.contextmenu.ContextMenuEngine import org.dreamfinity.dsgl.core.contextmenu.ContextMenuRuntime +import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton @@ -23,6 +25,8 @@ class ApplicationOverlayHost : OverlayLayerHost { LayerDomInputRouter( rootProvider = { rootNode }, ) + internal val contextMenuPortal: ContextMenuPortalController = + ContextMenuPortalController(ContextMenuRuntime.engine) override fun onInputFrame(viewportWidth: Int, viewportHeight: Int) { rootNode.setViewportBounds( @@ -54,18 +58,167 @@ class ApplicationOverlayHost : OverlayLayerHost { override fun clearRefs() { tree.clearRefs() domInputRouter.clear() + contextMenuPortal.close() } internal fun debugRootBounds(): Rect = rootNode.bounds } +internal class ContextMenuPortalController( + private val engine: ContextMenuEngine, +) { + private val portalHost: PortalHost = + PortalHost(OverlayLayerContracts.portalSurfaceForOwner(OverlayOwnerScope.Application)) + private val entry: ContextMenuPortalEntry = ContextMenuPortalEntry(engine) + + init { + portalHost.register(entry) + } + + fun onFrame( + measureContext: UiMeasureContext, + viewportWidth: Int, + viewportHeight: Int, + viewportScale: Float, + ) { + entry.onFrame(measureContext, viewportWidth, viewportHeight, viewportScale) + } + + fun appendCommands( + measureContext: UiMeasureContext, + viewportWidth: Int, + viewportHeight: Int, + out: MutableList, + ) { + entry.updatePaintContext(measureContext, viewportWidth, viewportHeight) + out += portalHost.paint(measureContext) + } + + fun close() { + entry.close() + } + + fun isOpen(): Boolean = engine.isOpen() + + fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = + portalHost.dispatchInput { it.handleMouseMove(mouseX, mouseY) } + + fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + portalHost.dispatchInput { it.handleMouseDown(mouseX, mouseY, button) } + + fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + portalHost.dispatchInput { it.handleMouseUp(mouseX, mouseY, button) } + + fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = + portalHost.dispatchInput { it.handleMouseWheel(mouseX, mouseY, delta) } + + fun handleKeyDown(keyCode: Int): Boolean = + portalHost.dispatchInput { + it.handleKeyDown(keyCode, Char.MIN_VALUE) + } +} + +private class ContextMenuPortalEntry( + private val engine: ContextMenuEngine, +) : PortalEntry { + override val state: PortalEntryState = + PortalEntryState( + id = PortalEntryId("application.context-menu"), + ownerToken = engine, + surface = OverlayLayerContracts.portalSurfaceForOwner(OverlayOwnerScope.Application), + order = PortalEntryOrder(zIndex = 0), + dismissPolicy = PortalDismissPolicy.EscapeOrOutsidePointerDown, + inputPolicy = PortalInputPolicy.ManualOnly, + focusPolicy = PortalFocusPolicy.Preserve, + ) + override val node: DOMNode? = null + private var viewportWidth: Int = 1 + private var viewportHeight: Int = 1 + private var measureContext: UiMeasureContext? = null + + fun onFrame( + measureContext: UiMeasureContext, + viewportWidth: Int, + viewportHeight: Int, + viewportScale: Float, + ) { + updatePaintContext(measureContext, viewportWidth, viewportHeight) + engine.onFrame( + measureContext = measureContext, + viewportWidth = this.viewportWidth, + viewportHeight = this.viewportHeight, + viewportScale = viewportScale, + ) + syncActivePlacement() + } + + fun updatePaintContext(measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int) { + this.measureContext = measureContext + this.viewportWidth = viewportWidth.coerceAtLeast(1) + this.viewportHeight = viewportHeight.coerceAtLeast(1) + } + + override fun paint(ctx: UiMeasureContext): List { + if (!engine.isOpen()) { + state.deactivate() + return emptyList() + } + val commands = ArrayList() + engine.appendOverlayCommands( + measureContext = measureContext ?: ctx, + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + out = commands, + ) + syncActivePlacement() + return commands + } + + override fun close() { + engine.closeAll() + state.deactivate() + } + + override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = engine.handleMouseMove(mouseX, mouseY) + + override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + engine.handleMouseDown(mouseX, mouseY, button).also { syncActivePlacement() } + + override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + engine.handleMouseUp(mouseX, mouseY, button).also { syncActivePlacement() } + + override fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = + engine.handleMouseWheel(mouseX, mouseY, delta).also { syncActivePlacement() } + + override fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = + engine.handleKeyDown(keyCode).also { syncActivePlacement() } + + private fun syncActivePlacement() { + if (!engine.isOpen()) { + state.deactivate() + return + } + val panelRect = engine.debugPanelRect(0) ?: return + state.activate( + PortalEntryPlacement( + anchorBounds = null, + bounds = + PortalEntryBounds( + viewportBounds = Rect(0, 0, viewportWidth.coerceAtLeast(1), viewportHeight.coerceAtLeast(1)), + entryBounds = panelRect, + ), + ), + ) + } +} + fun ApplicationOverlayHost.contextMenuOnFrame( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, viewportScale: Float, ) { - ContextMenuRuntime.engine.onFrame( + contextMenuPortal.onFrame( measureContext = measureContext, viewportWidth = viewportWidth, viewportHeight = viewportHeight, @@ -79,7 +232,7 @@ fun ApplicationOverlayHost.appendContextMenuOverlayCommands( viewportHeight: Int, out: MutableList, ) { - ContextMenuRuntime.engine.appendOverlayCommands( + contextMenuPortal.appendCommands( measureContext = measureContext, viewportWidth = viewportWidth, viewportHeight = viewportHeight, @@ -88,25 +241,24 @@ fun ApplicationOverlayHost.appendContextMenuOverlayCommands( } fun ApplicationOverlayHost.closeContextMenus() { - ContextMenuRuntime.engine.closeAll() + contextMenuPortal.close() } -fun ApplicationOverlayHost.isContextMenuOpen(): Boolean = ContextMenuRuntime.engine.isOpen() +fun ApplicationOverlayHost.isContextMenuOpen(): Boolean = contextMenuPortal.isOpen() fun ApplicationOverlayHost.handleContextMenuMouseMove(mouseX: Int, mouseY: Int): Boolean = - ContextMenuRuntime.engine.handleMouseMove(mouseX, mouseY) + contextMenuPortal.handleMouseMove(mouseX, mouseY) fun ApplicationOverlayHost.handleContextMenuMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = - ContextMenuRuntime.engine.handleMouseDown(mouseX, mouseY, button) + contextMenuPortal.handleMouseDown(mouseX, mouseY, button) fun ApplicationOverlayHost.handleContextMenuMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = - ContextMenuRuntime.engine.handleMouseUp(mouseX, mouseY, button) + contextMenuPortal.handleMouseUp(mouseX, mouseY, button) fun ApplicationOverlayHost.handleContextMenuMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = - ContextMenuRuntime.engine.handleMouseWheel(mouseX, mouseY, delta) + contextMenuPortal.handleMouseWheel(mouseX, mouseY, delta) -fun ApplicationOverlayHost.handleContextMenuKeyDown(keyCode: Int): Boolean = - ContextMenuRuntime.engine.handleKeyDown(keyCode) +fun ApplicationOverlayHost.handleContextMenuKeyDown(keyCode: Int): Boolean = contextMenuPortal.handleKeyDown(keyCode) fun ApplicationOverlayHost.applicationSelectOnFrame( measureContext: UiMeasureContext, diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt index 50c0053..9045ff8 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt @@ -1,17 +1,21 @@ package org.dreamfinity.dsgl.core.overlay +import org.dreamfinity.dsgl.core.contextmenu.ContextMenuRuntime +import org.dreamfinity.dsgl.core.contextmenu.contextMenu import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ButtonNode import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayEntryId import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayHost import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayPanelDemoNode import org.dreamfinity.dsgl.core.render.RenderCommand +import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -28,6 +32,11 @@ class LiveLayerInteractionPathTests { override fun paint(commands: List) = Unit } + @AfterTest + fun cleanupContextMenuRuntime() { + ContextMenuRuntime.engine.closeAll() + } + @Test fun `runtime layer path resolves in debug system app-overlay app-root order`() { val callOrder = ArrayList(4) @@ -213,6 +222,97 @@ class LiveLayerInteractionPathTests { assertFalse(appRootReceived) } + @Test + fun `application context menu is rendered and consumed through application portal path`() { + val applicationOverlayHost = ApplicationOverlayHost() + applicationOverlayHost.onInputFrame(320, 180) + var actionHits = 0 + ContextMenuRuntime.host.openAtCursor( + contextMenu(id = "portal.context") { + item("Run") { + onClick { actionHits += 1 } + } + }, + x = 24, + y = 24, + ) + + applicationOverlayHost.contextMenuOnFrame(ctx, 320, 180, 1f) + val commands = ArrayList() + applicationOverlayHost.appendContextMenuOverlayCommands(ctx, 320, 180, commands) + val firstEntryRect = ContextMenuRuntime.engine.debugEntryRect(levelIndex = 0, entryIndex = 0) + assertNotNull(firstEntryRect) + + val consumedByMenu = + applicationOverlayHost.handleContextMenuMouseDown( + mouseX = firstEntryRect.x + 1, + mouseY = firstEntryRect.y + 1, + button = MouseButton.LEFT, + ) + + assertTrue(commands.isNotEmpty()) + assertTrue(consumedByMenu) + assertEquals(1, actionHits) + assertFalse(applicationOverlayHost.isContextMenuOpen()) + } + + @Test + fun `application context menu portal blocks app-root fallthrough on outside dismiss`() { + val applicationOverlayHost = ApplicationOverlayHost() + applicationOverlayHost.onInputFrame(320, 180) + ContextMenuRuntime.host.openAtCursor( + contextMenu(id = "portal.dismiss") { + item("Run") + item("Build") + }, + x = 24, + y = 24, + ) + applicationOverlayHost.contextMenuOnFrame(ctx, 320, 180, 1f) + val panel = ContextMenuRuntime.engine.debugPanelRect(0) + assertNotNull(panel) + val outsideX = panel.x + panel.width + 24 + val outsideY = panel.y + panel.height + 24 + + val harness = + LiveLayerInputHarness( + debugHandler = { _, _, _ -> false }, + systemOverlayHandler = { _, _, _ -> false }, + applicationOverlayHandler = { x, y, button -> + applicationOverlayHost.handleContextMenuMouseDown(x, y, button) + }, + ) + var appRootReceived = false + val consumedBy = + harness.dispatchMouseDown(outsideX, outsideY, MouseButton.LEFT) { + appRootReceived = true + true + } + + assertEquals(UiLayerId.ApplicationOverlay, consumedBy) + assertFalse(appRootReceived) + assertFalse(applicationOverlayHost.isContextMenuOpen()) + } + + @Test + fun `application context menu portal consumes wheel and escape while open`() { + val applicationOverlayHost = ApplicationOverlayHost() + applicationOverlayHost.onInputFrame(320, 180) + ContextMenuRuntime.host.openAtCursor( + contextMenu(id = "portal.keyboard") { + item("Run") + item("Build") + }, + x = 24, + y = 24, + ) + applicationOverlayHost.contextMenuOnFrame(ctx, 320, 180, 1f) + + assertTrue(applicationOverlayHost.handleContextMenuMouseWheel(26, 26, -120)) + assertTrue(applicationOverlayHost.handleContextMenuKeyDown(KeyCodes.ESCAPE)) + assertFalse(applicationOverlayHost.isContextMenuOpen()) + } + @Test fun `rendered system overlay content is reachable through same live interaction path`() { val systemHost = SystemOverlayHost(InspectorController()) From 2291b2f5a17788ddcc9ae13c7a72744d05baac17 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sun, 10 May 2026 15:01:18 +0300 Subject: [PATCH 57/78] wiring select with a portalHost; --- .../core/overlay/ApplicationOverlayHost.kt | 24 ++- .../core/overlay/system/SystemOverlayHost.kt | 24 ++- .../core/select/SelectPortalController.kt | 178 +++++++++++++++++ .../overlay/LiveLayerInteractionPathTests.kt | 185 ++++++++++++++++++ 4 files changed, 395 insertions(+), 16 deletions(-) create mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt index 8b6eb77..11cbe7f 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt @@ -9,6 +9,7 @@ import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand +import org.dreamfinity.dsgl.core.select.SelectPortalController import org.dreamfinity.dsgl.core.select.SelectRuntime import org.dreamfinity.dsgl.core.style.StyleApplicationScope @@ -27,6 +28,12 @@ class ApplicationOverlayHost : OverlayLayerHost { ) internal val contextMenuPortal: ContextMenuPortalController = ContextMenuPortalController(ContextMenuRuntime.engine) + internal val applicationSelectPortal: SelectPortalController = + SelectPortalController( + engine = SelectRuntime.applicationEngine, + ownerScope = OverlayOwnerScope.Application, + entryId = "application.select", + ) override fun onInputFrame(viewportWidth: Int, viewportHeight: Int) { rootNode.setViewportBounds( @@ -59,6 +66,7 @@ class ApplicationOverlayHost : OverlayLayerHost { tree.clearRefs() domInputRouter.clear() contextMenuPortal.close() + applicationSelectPortal.close() } internal fun debugRootBounds(): Rect = rootNode.bounds @@ -266,7 +274,7 @@ fun ApplicationOverlayHost.applicationSelectOnFrame( viewportHeight: Int, viewportScale: Float, ) { - SelectRuntime.applicationEngine.onFrame( + applicationSelectPortal.onFrame( measureContext = measureContext, viewportWidth = viewportWidth, viewportHeight = viewportHeight, @@ -280,7 +288,7 @@ fun ApplicationOverlayHost.appendApplicationSelectOverlayCommands( viewportHeight: Int, out: MutableList, ) { - SelectRuntime.applicationEngine.appendOverlayCommands( + applicationSelectPortal.appendCommands( measureContext = measureContext, viewportWidth = viewportWidth, viewportHeight = viewportHeight, @@ -288,19 +296,19 @@ fun ApplicationOverlayHost.appendApplicationSelectOverlayCommands( ) } -fun ApplicationOverlayHost.isApplicationSelectOpen(): Boolean = SelectRuntime.applicationEngine.isOpen() +fun ApplicationOverlayHost.isApplicationSelectOpen(): Boolean = applicationSelectPortal.isOpen() fun ApplicationOverlayHost.handleApplicationSelectKeyDown(keyCode: Int, keyChar: Char): Boolean = - SelectRuntime.applicationEngine.handleKeyDown(keyCode, keyChar) + applicationSelectPortal.handleKeyDown(keyCode, keyChar) fun ApplicationOverlayHost.handleApplicationSelectMouseMove(mouseX: Int, mouseY: Int): Boolean = - SelectRuntime.applicationEngine.handleMouseMove(mouseX, mouseY) + applicationSelectPortal.handleMouseMove(mouseX, mouseY) fun ApplicationOverlayHost.handleApplicationSelectMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = - SelectRuntime.applicationEngine.handleMouseDown(mouseX, mouseY, button) + applicationSelectPortal.handleMouseDown(mouseX, mouseY, button) fun ApplicationOverlayHost.handleApplicationSelectMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = - SelectRuntime.applicationEngine.handleMouseUp(mouseX, mouseY, button) + applicationSelectPortal.handleMouseUp(mouseX, mouseY, button) fun ApplicationOverlayHost.handleApplicationSelectMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = - SelectRuntime.applicationEngine.handleMouseWheel(mouseX, mouseY, delta) + applicationSelectPortal.handleMouseWheel(mouseX, mouseY, delta) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt index 8351203..5f4eb2b 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt @@ -22,6 +22,7 @@ import org.dreamfinity.dsgl.core.overlay.input.dispatchManualThenDomFallback import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanel import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelStyle import org.dreamfinity.dsgl.core.render.RenderCommand +import org.dreamfinity.dsgl.core.select.SelectPortalController import org.dreamfinity.dsgl.core.select.SelectRuntime import org.dreamfinity.dsgl.core.style.StyleApplicationScope @@ -41,6 +42,12 @@ class SystemOverlayHost( ) private val transientOwnershipRegistry: SystemOverlayTransientOwnershipRegistry = SystemOverlayTransientOwnershipRegistry() + private val systemSelectPortal: SelectPortalController = + SelectPortalController( + engine = SelectRuntime.systemEngine, + ownerScope = OverlayOwnerScope.System, + entryId = "system.select", + ) private val tree: DomTree = DomTree( root = rootNode, @@ -83,7 +90,7 @@ class SystemOverlayHost( viewportHeight: Int, viewportScale: Float, ) { - SelectRuntime.systemEngine.onFrame( + systemSelectPortal.onFrame( measureContext = measureContext, viewportWidth = viewportWidth, viewportHeight = viewportHeight, @@ -97,7 +104,7 @@ class SystemOverlayHost( viewportHeight: Int, out: MutableList, ) { - SelectRuntime.systemEngine.appendOverlayCommands( + systemSelectPortal.appendCommands( measureContext = measureContext, viewportWidth = viewportWidth, viewportHeight = viewportHeight, @@ -105,22 +112,22 @@ class SystemOverlayHost( ) } - fun isSystemSelectOpen(): Boolean = SelectRuntime.systemEngine.isOpen() + fun isSystemSelectOpen(): Boolean = systemSelectPortal.isOpen() fun handleSystemSelectKeyDown(keyCode: Int, keyChar: Char): Boolean = - SelectRuntime.systemEngine.handleKeyDown(keyCode, keyChar) + systemSelectPortal.handleKeyDown(keyCode, keyChar) fun handleSystemSelectMouseMove(mouseX: Int, mouseY: Int): Boolean = - SelectRuntime.systemEngine.handleMouseMove(mouseX, mouseY) + systemSelectPortal.handleMouseMove(mouseX, mouseY) fun handleSystemSelectMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = - SelectRuntime.systemEngine.handleMouseDown(mouseX, mouseY, button) + systemSelectPortal.handleMouseDown(mouseX, mouseY, button) fun handleSystemSelectMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = - SelectRuntime.systemEngine.handleMouseUp(mouseX, mouseY, button) + systemSelectPortal.handleMouseUp(mouseX, mouseY, button) fun handleSystemSelectMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = - SelectRuntime.systemEngine.handleMouseWheel(mouseX, mouseY, delta) + systemSelectPortal.handleMouseWheel(mouseX, mouseY, delta) override fun onInputFrame(viewportWidth: Int, viewportHeight: Int) { knownViewportWidth = viewportWidth.coerceAtLeast(1) @@ -197,6 +204,7 @@ class SystemOverlayHost( transientOwnershipRegistry.clear() colorPickerEntry.close() overlayPanelDemoEntry.close() + systemSelectPortal.close() domInputRouter.clear() } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt new file mode 100644 index 0000000..56a5c54 --- /dev/null +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt @@ -0,0 +1,178 @@ +package org.dreamfinity.dsgl.core.select + +import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.overlay.OverlayLayerContracts +import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope +import org.dreamfinity.dsgl.core.overlay.PortalDismissPolicy +import org.dreamfinity.dsgl.core.overlay.PortalEntry +import org.dreamfinity.dsgl.core.overlay.PortalEntryBounds +import org.dreamfinity.dsgl.core.overlay.PortalEntryId +import org.dreamfinity.dsgl.core.overlay.PortalEntryOrder +import org.dreamfinity.dsgl.core.overlay.PortalEntryPlacement +import org.dreamfinity.dsgl.core.overlay.PortalEntryState +import org.dreamfinity.dsgl.core.overlay.PortalFocusPolicy +import org.dreamfinity.dsgl.core.overlay.PortalHost +import org.dreamfinity.dsgl.core.overlay.PortalInputPolicy +import org.dreamfinity.dsgl.core.render.RenderCommand + +internal class SelectPortalController( + private val engine: SelectEngine, + ownerScope: OverlayOwnerScope, + entryId: String, +) { + private val portalHost: PortalHost = + PortalHost(OverlayLayerContracts.portalSurfaceForOwner(ownerScope)) + private val entry: SelectPortalEntry = + SelectPortalEntry( + engine = engine, + ownerScope = ownerScope, + entryId = entryId, + ) + + init { + portalHost.register(entry) + } + + fun onFrame( + measureContext: UiMeasureContext, + viewportWidth: Int, + viewportHeight: Int, + viewportScale: Float, + ) { + entry.onFrame(measureContext, viewportWidth, viewportHeight, viewportScale) + } + + fun appendCommands( + measureContext: UiMeasureContext, + viewportWidth: Int, + viewportHeight: Int, + out: MutableList, + ) { + entry.updatePaintContext(measureContext, viewportWidth, viewportHeight) + entry.syncActivePlacement() + out += portalHost.paint(measureContext) + } + + fun close() { + entry.close() + } + + fun isOpen(): Boolean = engine.isOpen() + + fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = + portalHost.dispatchInput { it.handleMouseMove(mouseX, mouseY) } + + fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + portalHost.dispatchInput { it.handleMouseDown(mouseX, mouseY, button) } + + fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + portalHost.dispatchInput { it.handleMouseUp(mouseX, mouseY, button) } + + fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = + portalHost.dispatchInput { it.handleMouseWheel(mouseX, mouseY, delta) } + + fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = + portalHost.dispatchInput { it.handleKeyDown(keyCode, keyChar) } +} + +private class SelectPortalEntry( + private val engine: SelectEngine, + ownerScope: OverlayOwnerScope, + entryId: String, +) : PortalEntry { + override val state: PortalEntryState = + PortalEntryState( + id = PortalEntryId(entryId), + ownerToken = engine, + surface = OverlayLayerContracts.portalSurfaceForOwner(ownerScope), + order = PortalEntryOrder(zIndex = 0), + dismissPolicy = PortalDismissPolicy.EscapeOrOutsidePointerDown, + inputPolicy = PortalInputPolicy.ManualOnly, + focusPolicy = PortalFocusPolicy.Preserve, + ) + override val node: DOMNode? = null + private var viewportWidth: Int = 1 + private var viewportHeight: Int = 1 + private var measureContext: UiMeasureContext? = null + + fun onFrame( + measureContext: UiMeasureContext, + viewportWidth: Int, + viewportHeight: Int, + viewportScale: Float, + ) { + updatePaintContext(measureContext, viewportWidth, viewportHeight) + engine.onFrame( + measureContext = measureContext, + viewportWidth = this.viewportWidth, + viewportHeight = this.viewportHeight, + viewportScale = viewportScale, + ) + syncActivePlacement() + } + + fun updatePaintContext(measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int) { + this.measureContext = measureContext + this.viewportWidth = viewportWidth.coerceAtLeast(1) + this.viewportHeight = viewportHeight.coerceAtLeast(1) + } + + override fun paint(ctx: UiMeasureContext): List { + if (!engine.isOpen()) { + state.deactivate() + return emptyList() + } + val commands = ArrayList() + engine.appendOverlayCommands( + measureContext = measureContext ?: ctx, + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + out = commands, + ) + syncActivePlacement() + return commands + } + + override fun close() { + engine.closeAll() + state.deactivate() + } + + override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = + engine.handleMouseMove(mouseX, mouseY).also { syncActivePlacement() } + + override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + engine.handleMouseDown(mouseX, mouseY, button).also { syncActivePlacement() } + + override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + engine.handleMouseUp(mouseX, mouseY, button).also { syncActivePlacement() } + + override fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = + engine.handleMouseWheel(mouseX, mouseY, delta).also { syncActivePlacement() } + + override fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = + engine.handleKeyDown(keyCode, keyChar).also { syncActivePlacement() } + + fun syncActivePlacement() { + if (!engine.isOpen()) { + state.deactivate() + return + } + val owner = engine.snapshot().owner ?: return + val panelRect = engine.debugPanelRect(owner) ?: return + val anchorRect = engine.debugAnchorRect(owner) + state.activate( + PortalEntryPlacement( + anchorBounds = anchorRect, + bounds = + PortalEntryBounds( + viewportBounds = Rect(0, 0, viewportWidth, viewportHeight), + entryBounds = panelRect, + ), + ), + ) + } +} diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt index 9045ff8..c5048da 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt @@ -15,6 +15,10 @@ import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayEntryId import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayHost import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayPanelDemoNode import org.dreamfinity.dsgl.core.render.RenderCommand +import org.dreamfinity.dsgl.core.select.SelectEntry +import org.dreamfinity.dsgl.core.select.SelectOpenRequest +import org.dreamfinity.dsgl.core.select.SelectRuntime +import org.dreamfinity.dsgl.core.select.selectModel import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals @@ -35,6 +39,7 @@ class LiveLayerInteractionPathTests { @AfterTest fun cleanupContextMenuRuntime() { ContextMenuRuntime.engine.closeAll() + SelectRuntime.host.closeAll() } @Test @@ -313,6 +318,154 @@ class LiveLayerInteractionPathTests { assertFalse(applicationOverlayHost.isContextMenuOpen()) } + @Test + fun `application select is rendered and consumed through application portal path`() { + val applicationOverlayHost = ApplicationOverlayHost() + applicationOverlayHost.onInputFrame(320, 180) + var selected: String? = null + val owner = "application-select-portal" + SelectRuntime.host.open(selectRequest(owner, OverlayOwnerScope.Application) { selected = it }) + + applicationOverlayHost.applicationSelectOnFrame(ctx, 320, 180, 1f) + val commands = ArrayList() + applicationOverlayHost.appendApplicationSelectOverlayCommands(ctx, 320, 180, commands) + val panel = SelectRuntime.applicationEngine.debugPanelRect(owner) + assertNotNull(panel) + + val style = SelectRuntime.applicationEngine.currentStyle() + val consumed = + applicationOverlayHost.handleApplicationSelectMouseDown( + mouseX = panel.x + style.panelPaddingX + 1, + mouseY = panel.y + style.panelPaddingY + 1, + button = MouseButton.LEFT, + ) + + assertTrue(commands.isNotEmpty()) + assertTrue(consumed) + assertEquals("a", selected) + } + + @Test + fun `application select portal blocks app-root fallthrough on outside dismiss`() { + val applicationOverlayHost = ApplicationOverlayHost() + applicationOverlayHost.onInputFrame(320, 180) + val owner = "application-select-dismiss" + SelectRuntime.host.open(selectRequest(owner, OverlayOwnerScope.Application)) + applicationOverlayHost.applicationSelectOnFrame(ctx, 320, 180, 1f) + val panel = SelectRuntime.applicationEngine.debugPanelRect(owner) + assertNotNull(panel) + val outsideX = panel.x + panel.width + 24 + val outsideY = panel.y + panel.height + 24 + val harness = + LiveLayerInputHarness( + debugHandler = { _, _, _ -> false }, + systemOverlayHandler = { _, _, _ -> false }, + applicationOverlayHandler = { x, y, button -> + applicationOverlayHost.handleApplicationSelectMouseDown(x, y, button) + }, + ) + + var appRootReceived = false + val consumedBy = + harness.dispatchMouseDown(outsideX, outsideY, MouseButton.LEFT) { + appRootReceived = true + true + } + + assertEquals(UiLayerId.ApplicationOverlay, consumedBy) + assertFalse(appRootReceived) + } + + @Test + fun `application select portal consumes wheel typeahead and escape`() { + val applicationOverlayHost = ApplicationOverlayHost() + applicationOverlayHost.onInputFrame(320, 120) + val owner = "application-select-keyboard" + var selected: String? = null + SelectRuntime.host.open( + selectRequest( + owner = owner, + ownerScope = OverlayOwnerScope.Application, + entries = + listOf( + SelectEntry.Option("a", labelProvider = { "Alpha" }), + SelectEntry.Option("b", labelProvider = { "Beta" }), + SelectEntry.Option("c", labelProvider = { "Charlie" }), + SelectEntry.Option("d", labelProvider = { "Delta" }), + SelectEntry.Option("e", labelProvider = { "Echo" }), + SelectEntry.Option("f", labelProvider = { "Foxtrot" }), + ), + onSelect = { selected = it }, + ), + ) + applicationOverlayHost.applicationSelectOnFrame(ctx, 320, 120, 1f) + val panel = SelectRuntime.applicationEngine.debugPanelRect(owner) + assertNotNull(panel) + + assertTrue(applicationOverlayHost.handleApplicationSelectMouseWheel(panel.x + 2, panel.y + 2, -120)) + assertTrue(applicationOverlayHost.handleApplicationSelectKeyDown(0, 'd')) + assertTrue(applicationOverlayHost.handleApplicationSelectKeyDown(KeyCodes.ENTER, Char.MIN_VALUE)) + assertEquals("d", selected) + + SelectRuntime.host.open(selectRequest(owner, OverlayOwnerScope.Application)) + applicationOverlayHost.applicationSelectOnFrame(ctx, 320, 120, 1f) + assertTrue(applicationOverlayHost.handleApplicationSelectKeyDown(KeyCodes.ESCAPE, Char.MIN_VALUE)) + } + + @Test + fun `system select is rendered and consumed through system portal path`() { + val systemHost = SystemOverlayHost(InspectorController()) + systemHost.onInputFrame(320, 180) + val owner = "system-select-portal" + var selected: String? = null + SelectRuntime.host.open(selectRequest(owner, OverlayOwnerScope.System) { selected = it }) + + systemHost.systemSelectOnFrame(ctx, 320, 180, 1f) + val commands = ArrayList() + systemHost.appendSystemSelectOverlayCommands(ctx, 320, 180, commands) + val panel = SelectRuntime.systemEngine.debugPanelRect(owner) + assertNotNull(panel) + val style = SelectRuntime.systemEngine.currentStyle() + + val harness = + LiveLayerInputHarness( + debugHandler = { _, _, _ -> false }, + systemOverlayHandler = { x, y, button -> + systemHost.handleSystemSelectMouseDown(x, y, button) + }, + applicationOverlayHandler = { _, _, _ -> false }, + ) + var appRootReceived = false + val consumedBy = + harness.dispatchMouseDown( + panel.x + style.panelPaddingX + 1, + panel.y + style.panelPaddingY + 1, + MouseButton.LEFT, + ) { + appRootReceived = true + true + } + + assertTrue(commands.isNotEmpty()) + assertEquals(UiLayerId.SystemOverlay, consumedBy) + assertFalse(appRootReceived) + assertEquals("a", selected) + assertFalse(SelectRuntime.applicationEngine.isOpenFor(owner)) + } + + @Test + fun `select owner migration preserves application system routing`() { + val owner = "select-owner-migration" + SelectRuntime.host.open(selectRequest(owner, OverlayOwnerScope.Application)) + assertTrue(SelectRuntime.applicationEngine.isOpenFor(owner)) + assertFalse(SelectRuntime.systemEngine.isOpenFor(owner)) + + SelectRuntime.host.open(selectRequest(owner, OverlayOwnerScope.System)) + + assertFalse(SelectRuntime.applicationEngine.isOpenFor(owner)) + assertTrue(SelectRuntime.systemEngine.isOpenFor(owner)) + } + @Test fun `rendered system overlay content is reachable through same live interaction path`() { val systemHost = SystemOverlayHost(InspectorController()) @@ -360,6 +513,38 @@ class LiveLayerInteractionPathTests { return root } + private fun selectRequest( + owner: Any, + ownerScope: OverlayOwnerScope, + entries: List = + listOf( + SelectEntry.Option("a", labelProvider = { "Alpha" }), + SelectEntry.Option("b", labelProvider = { "Beta" }), + ), + onSelect: ((String) -> Unit)? = null, + ): SelectOpenRequest { + val model = + selectModel(id = "live-layer-select") { + entries.forEach { entry -> + when (entry) { + is SelectEntry.Option -> option(entry.id, entry.labelProvider) + is SelectEntry.Group -> group(entry.labelProvider, entry.id) {} + is SelectEntry.Separator -> separator(entry.id) + } + } + } + return SelectOpenRequest( + owner = owner, + modelToken = model.token, + entries = entries, + selectedId = null, + anchorRect = Rect(24, 24, 100, 18), + closeOnSelect = true, + onSelect = onSelect, + ownerScope = ownerScope, + ) + } + private class LiveLayerInputHarness( private val debugHandler: (Int, Int, MouseButton) -> Boolean, private val systemOverlayHandler: (Int, Int, MouseButton) -> Boolean, From 6a2478dc5fc2b1d1dc99ae00a8a9f04ba2e35720 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sun, 10 May 2026 15:40:35 +0300 Subject: [PATCH 58/78] wiring color picker with portalHost; --- .../dsgl/mcForge1710/DsglScreenHost.kt | 61 ++++-- .../ColorPickerPortalController.kt | 195 ++++++++++++++++++ .../ApplicationColorPickerPortalExtensions.kt | 63 ++++++ .../core/overlay/ApplicationOverlayHost.kt | 5 + .../overlay/LiveLayerInteractionPathTests.kt | 137 ++++++++++++ 5 files changed, 446 insertions(+), 15 deletions(-) create mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPortalController.kt create mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationColorPickerPortalExtensions.kt diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index 36940d2..d5c6771 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -33,11 +33,19 @@ import org.dreamfinity.dsgl.core.overlay.OverlayLayerContracts import org.dreamfinity.dsgl.core.overlay.OverlayLayerHost import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.overlay.UiLayerId +import org.dreamfinity.dsgl.core.overlay.appendApplicationColorPickerOverlayCommands import org.dreamfinity.dsgl.core.overlay.appendApplicationSelectOverlayCommands import org.dreamfinity.dsgl.core.overlay.appendContextMenuOverlayCommands +import org.dreamfinity.dsgl.core.overlay.applicationColorPickerOnFrame import org.dreamfinity.dsgl.core.overlay.applicationSelectOnFrame +import org.dreamfinity.dsgl.core.overlay.captureApplicationColorPickerEyedropperSample import org.dreamfinity.dsgl.core.overlay.closeContextMenus import org.dreamfinity.dsgl.core.overlay.contextMenuOnFrame +import org.dreamfinity.dsgl.core.overlay.handleApplicationColorPickerKeyDown +import org.dreamfinity.dsgl.core.overlay.handleApplicationColorPickerMouseDown +import org.dreamfinity.dsgl.core.overlay.handleApplicationColorPickerMouseMove +import org.dreamfinity.dsgl.core.overlay.handleApplicationColorPickerMouseUp +import org.dreamfinity.dsgl.core.overlay.handleApplicationColorPickerMouseWheel import org.dreamfinity.dsgl.core.overlay.handleApplicationSelectKeyDown import org.dreamfinity.dsgl.core.overlay.handleApplicationSelectMouseDown import org.dreamfinity.dsgl.core.overlay.handleApplicationSelectMouseMove @@ -48,6 +56,8 @@ import org.dreamfinity.dsgl.core.overlay.handleContextMenuMouseDown import org.dreamfinity.dsgl.core.overlay.handleContextMenuMouseMove import org.dreamfinity.dsgl.core.overlay.handleContextMenuMouseUp import org.dreamfinity.dsgl.core.overlay.handleContextMenuMouseWheel +import org.dreamfinity.dsgl.core.overlay.hasActiveApplicationColorPickerEyedropper +import org.dreamfinity.dsgl.core.overlay.isApplicationColorPickerOpen import org.dreamfinity.dsgl.core.overlay.isApplicationSelectOpen import org.dreamfinity.dsgl.core.overlay.isContextMenuOpen import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayHost @@ -409,8 +419,7 @@ abstract class DsglScreenHost( applicationOverlayHost.contextMenuOnFrame(adapter, lastWidth, lastHeight, 1f) applicationOverlayHost.applicationSelectOnFrame(adapter, lastWidth, lastHeight, 1f) systemOverlayHost.systemSelectOnFrame(adapter, lastWidth, lastHeight, 1f) - ColorPickerRuntime.engine.onFrame(lastWidth, lastHeight) - ColorPickerRuntime.engine.onCursorPosition(dsglMouseX, dsglMouseY) + applicationOverlayHost.applicationColorPickerOnFrame(lastWidth, lastHeight, dsglMouseX, dsglMouseY) refreshActiveColorSamplerOwner(tree.root) } @@ -504,7 +513,11 @@ abstract class DsglScreenHost( !inspectorBlocks && ( (systemOverlayInputEnabled && systemOverlayHost.isSystemColorPickerOpen()) || - (appOverlayInputEnabled && ColorPickerRuntime.engine.isOpen() && !inlineSamplerOwnsSession) + ( + appOverlayInputEnabled && + applicationOverlayHost.isApplicationColorPickerOpen() && + !inlineSamplerOwnsSession + ) ) if (!inspectorBlocks && !contextMenuBlocks && !selectBlocks && !systemSelectBlocks && !colorPickerBlocks) { DndRuntime.engine.onMouseMove(tree.root, dsglMouseX, dsglMouseY) @@ -561,7 +574,12 @@ abstract class DsglScreenHost( viewportHeight = lastHeight, out = applicationOverlayCommandsBuffer, ) - ColorPickerRuntime.engine.appendOverlayCommands(applicationOverlayCommandsBuffer) + applicationOverlayHost.appendApplicationColorPickerOverlayCommands( + measureContext = adapter, + viewportWidth = lastWidth, + viewportHeight = lastHeight, + out = applicationOverlayCommandsBuffer, + ) appendInlineColorPickerOverlayCommands(applicationOverlayCommandsBuffer) } } @@ -769,7 +787,12 @@ abstract class DsglScreenHost( ) runOverlayInputFrame(applicationOverlayHost) runOverlayInputFrame(systemOverlayHost) - ColorPickerRuntime.engine.onFrame(lastWidth, lastHeight) + applicationOverlayHost.applicationColorPickerOnFrame( + viewportWidth = lastWidth, + viewportHeight = lastHeight, + mouseX = if (lastMoveX == Int.MIN_VALUE) lastMouseX else lastMoveX, + mouseY = if (lastMoveY == Int.MIN_VALUE) lastMouseY else lastMoveY, + ) val keyCode = Keyboard.getEventKey() val keyChar = Keyboard.getEventCharacter() val inspectorMouseX = if (lastMoveX == Int.MIN_VALUE) lastMouseX else lastMoveX @@ -950,7 +973,12 @@ abstract class DsglScreenHost( cursorY = inputEvent.mouseY, inspectorPointerCaptured = inspectorPointerCaptured, ) - ColorPickerRuntime.engine.onFrame(lastWidth, lastHeight) + applicationOverlayHost.applicationColorPickerOnFrame( + viewportWidth = lastWidth, + viewportHeight = lastHeight, + mouseX = inputEvent.mouseX, + mouseY = inputEvent.mouseY, + ) refreshActiveColorSamplerOwner(tree.root) } @@ -1128,7 +1156,7 @@ abstract class DsglScreenHost( } private fun consumeApplicationOverlayKeyDown(keyCode: Int, keyChar: Char): Boolean { - if (ColorPickerRuntime.engine.handleKeyDown(keyCode, keyChar)) { + if (applicationOverlayHost.handleApplicationColorPickerKeyDown(keyCode, keyChar)) { return true } if (applicationOverlayHost.handleKeyDown(keyCode, keyChar)) { @@ -1275,20 +1303,23 @@ abstract class DsglScreenHost( ): Boolean { val inlineSamplerOwnsSession = activeColorSamplerOwner is ActiveColorSamplerOwner.Inline if (!inlineSamplerOwnsSession) { - if (dWheel != 0 && ColorPickerRuntime.engine.handleMouseWheel(mouseX, mouseY, dWheel)) { + if (dWheel != 0 && applicationOverlayHost.handleApplicationColorPickerMouseWheel(mouseX, mouseY, dWheel)) { return true } if (mouseButton != -1 && mappedButton != null) { val consumedByColorPicker = if (buttonPressed) { - ColorPickerRuntime.engine.handleMouseDown(mouseX, mouseY, mappedButton) + applicationOverlayHost.handleApplicationColorPickerMouseDown(mouseX, mouseY, mappedButton) } else { - ColorPickerRuntime.engine.handleMouseUp(mouseX, mouseY, mappedButton) + applicationOverlayHost.handleApplicationColorPickerMouseUp(mouseX, mouseY, mappedButton) } if (consumedByColorPicker) { return true } - } else if (mouseButton == -1 && ColorPickerRuntime.engine.handleMouseMove(mouseX, mouseY)) { + } else if ( + mouseButton == -1 && + applicationOverlayHost.handleApplicationColorPickerMouseMove(mouseX, mouseY) + ) { return true } } @@ -1377,7 +1408,7 @@ abstract class DsglScreenHost( } activeColorSamplerOwner = colorSamplerOwnershipRouter.update( - popupEyedropperActive = ColorPickerRuntime.engine.hasActiveEyedropper(), + popupEyedropperActive = applicationOverlayHost.hasActiveApplicationColorPickerEyedropper(), inlineActiveTokens = inlineByToken.keys.toSet(), ) activeInlineColorSamplerNode = @@ -1433,7 +1464,7 @@ abstract class DsglScreenHost( return } when (activeColorSamplerOwner) { - ActiveColorSamplerOwner.Popup -> ColorPickerRuntime.engine.captureEyedropperSample() + ActiveColorSamplerOwner.Popup -> applicationOverlayHost.captureApplicationColorPickerEyedropperSample() is ActiveColorSamplerOwner.Inline -> { val inline = activeInlineColorSamplerNode if (inline != null && inline.wantsGlobalPointerInput()) { @@ -1442,8 +1473,8 @@ abstract class DsglScreenHost( } ActiveColorSamplerOwner.None -> { - if (ColorPickerRuntime.engine.hasActiveEyedropper()) { - ColorPickerRuntime.engine.captureEyedropperSample() + if (applicationOverlayHost.hasActiveApplicationColorPickerEyedropper()) { + applicationOverlayHost.captureApplicationColorPickerEyedropperSample() } } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPortalController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPortalController.kt new file mode 100644 index 0000000..8a3736d --- /dev/null +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPortalController.kt @@ -0,0 +1,195 @@ +package org.dreamfinity.dsgl.core.colorpicker + +import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.overlay.OverlayLayerContracts +import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope +import org.dreamfinity.dsgl.core.overlay.PortalDismissPolicy +import org.dreamfinity.dsgl.core.overlay.PortalEntry +import org.dreamfinity.dsgl.core.overlay.PortalEntryBounds +import org.dreamfinity.dsgl.core.overlay.PortalEntryId +import org.dreamfinity.dsgl.core.overlay.PortalEntryOrder +import org.dreamfinity.dsgl.core.overlay.PortalEntryPlacement +import org.dreamfinity.dsgl.core.overlay.PortalEntryState +import org.dreamfinity.dsgl.core.overlay.PortalFocusPolicy +import org.dreamfinity.dsgl.core.overlay.PortalHost +import org.dreamfinity.dsgl.core.overlay.PortalInputPolicy +import org.dreamfinity.dsgl.core.render.RenderCommand + +internal class ColorPickerPortalController( + private val engine: ColorPickerPopupEngine, +) { + private val portalHost: PortalHost = + PortalHost(OverlayLayerContracts.portalSurfaceForOwner(OverlayOwnerScope.Application)) + private val entry: ColorPickerPortalEntry = ColorPickerPortalEntry(engine) + + init { + portalHost.register(entry) + } + + fun onFrame( + viewportWidth: Int, + viewportHeight: Int, + mouseX: Int, + mouseY: Int, + ) { + entry.onFrame(viewportWidth, viewportHeight, mouseX, mouseY) + } + + fun appendCommands( + measureContext: UiMeasureContext, + viewportWidth: Int, + viewportHeight: Int, + out: MutableList, + ) { + entry.updatePaintContext(viewportWidth, viewportHeight) + entry.syncActivePlacement() + out += portalHost.paint(measureContext) + } + + fun close() { + entry.close() + } + + val isOpen: Boolean + get() = entry.isApplicationPopupOpen + + val hasActiveEyedropper: Boolean + get() = isOpen && engine.hasActiveEyedropper() + + fun captureEyedropperSample() { + if (isOpen) { + engine.captureEyedropperSample() + } + } + + fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = + portalHost.dispatchInput { it.handleMouseMove(mouseX, mouseY) } + + fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + portalHost.dispatchInput { it.handleMouseDown(mouseX, mouseY, button) } + + fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + portalHost.dispatchInput { it.handleMouseUp(mouseX, mouseY, button) } + + fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = + portalHost.dispatchInput { it.handleMouseWheel(mouseX, mouseY, delta) } + + fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = + portalHost.dispatchInput { it.handleKeyDown(keyCode, keyChar) } +} + +private class ColorPickerPortalEntry( + private val engine: ColorPickerPopupEngine, +) : PortalEntry { + override val state: PortalEntryState = + PortalEntryState( + id = PortalEntryId("application.color-picker"), + ownerToken = engine, + surface = OverlayLayerContracts.portalSurfaceForOwner(OverlayOwnerScope.Application), + order = PortalEntryOrder(zIndex = 0), + dismissPolicy = PortalDismissPolicy.EscapeOrOutsidePointerDown, + inputPolicy = PortalInputPolicy.ManualOnly, + focusPolicy = PortalFocusPolicy.Preserve, + ) + override val node: DOMNode? = null + private var viewportWidth: Int = 1 + private var viewportHeight: Int = 1 + + fun onFrame( + viewportWidth: Int, + viewportHeight: Int, + mouseX: Int, + mouseY: Int, + ) { + updatePaintContext(viewportWidth, viewportHeight) + if (!isApplicationPopupOpen) { + state.deactivate() + return + } + engine.onFrame(this.viewportWidth, this.viewportHeight) + engine.onCursorPosition(mouseX, mouseY) + syncActivePlacement() + } + + fun updatePaintContext(viewportWidth: Int, viewportHeight: Int) { + this.viewportWidth = viewportWidth.coerceAtLeast(1) + this.viewportHeight = viewportHeight.coerceAtLeast(1) + } + + override fun paint(ctx: UiMeasureContext): List { + if (!isApplicationPopupOpen) { + state.deactivate() + return emptyList() + } + val commands = ArrayList() + engine.appendOverlayCommands(commands) + syncActivePlacement() + return commands + } + + override fun close() { + if (isApplicationPopupOpen) { + engine.closeAll() + } + state.deactivate() + } + + override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = + if (isApplicationPopupOpen) { + engine.handleMouseMove(mouseX, mouseY).also { syncActivePlacement() } + } else { + false + } + + override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + if (isApplicationPopupOpen) { + engine.handleMouseDown(mouseX, mouseY, button).also { syncActivePlacement() } + } else { + false + } + + override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + if (isApplicationPopupOpen) { + engine.handleMouseUp(mouseX, mouseY, button).also { syncActivePlacement() } + } else { + false + } + + override fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = + if (isApplicationPopupOpen) { + engine.handleMouseWheel(mouseX, mouseY, delta).also { syncActivePlacement() } + } else { + false + } + + override fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = + if (isApplicationPopupOpen) { + engine.handleKeyDown(keyCode, keyChar).also { syncActivePlacement() } + } else { + false + } + + fun syncActivePlacement() { + if (!isApplicationPopupOpen) { + state.deactivate() + return + } + val panelRect = engine.debugActivePanelRect() ?: return + state.activate( + PortalEntryPlacement( + anchorBounds = null, + bounds = + PortalEntryBounds( + viewportBounds = Rect(0, 0, viewportWidth, viewportHeight), + entryBounds = panelRect, + ), + ), + ) + } + + val isApplicationPopupOpen: Boolean + get() = engine.isOpen() && engine.debugActiveOwnerScope() == OverlayOwnerScope.Application +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationColorPickerPortalExtensions.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationColorPickerPortalExtensions.kt new file mode 100644 index 0000000..2c07964 --- /dev/null +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationColorPickerPortalExtensions.kt @@ -0,0 +1,63 @@ +package org.dreamfinity.dsgl.core.overlay + +import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.render.RenderCommand + +fun ApplicationOverlayHost.applicationColorPickerOnFrame( + viewportWidth: Int, + viewportHeight: Int, + mouseX: Int, + mouseY: Int, +) { + applicationColorPickerPortal.onFrame( + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + mouseX = mouseX, + mouseY = mouseY, + ) +} + +fun ApplicationOverlayHost.appendApplicationColorPickerOverlayCommands( + measureContext: UiMeasureContext, + viewportWidth: Int, + viewportHeight: Int, + out: MutableList, +) { + applicationColorPickerPortal.appendCommands( + measureContext = measureContext, + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + out = out, + ) +} + +fun ApplicationOverlayHost.isApplicationColorPickerOpen(): Boolean = applicationColorPickerPortal.isOpen + +fun ApplicationOverlayHost.hasActiveApplicationColorPickerEyedropper(): Boolean = + applicationColorPickerPortal.hasActiveEyedropper + +fun ApplicationOverlayHost.captureApplicationColorPickerEyedropperSample() { + applicationColorPickerPortal.captureEyedropperSample() +} + +fun ApplicationOverlayHost.handleApplicationColorPickerKeyDown(keyCode: Int, keyChar: Char): Boolean = + applicationColorPickerPortal.handleKeyDown(keyCode, keyChar) + +fun ApplicationOverlayHost.handleApplicationColorPickerMouseMove(mouseX: Int, mouseY: Int): Boolean = + applicationColorPickerPortal.handleMouseMove(mouseX, mouseY) + +fun ApplicationOverlayHost.handleApplicationColorPickerMouseDown( + mouseX: Int, + mouseY: Int, + button: MouseButton, +): Boolean = applicationColorPickerPortal.handleMouseDown(mouseX, mouseY, button) + +fun ApplicationOverlayHost.handleApplicationColorPickerMouseUp( + mouseX: Int, + mouseY: Int, + button: MouseButton, +): Boolean = applicationColorPickerPortal.handleMouseUp(mouseX, mouseY, button) + +fun ApplicationOverlayHost.handleApplicationColorPickerMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = + applicationColorPickerPortal.handleMouseWheel(mouseX, mouseY, delta) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt index 11cbe7f..9e986be 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt @@ -1,6 +1,8 @@ package org.dreamfinity.dsgl.core.overlay import org.dreamfinity.dsgl.core.DomTree +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPortalController +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerRuntime import org.dreamfinity.dsgl.core.contextmenu.ContextMenuEngine import org.dreamfinity.dsgl.core.contextmenu.ContextMenuRuntime import org.dreamfinity.dsgl.core.dom.DOMNode @@ -34,6 +36,8 @@ class ApplicationOverlayHost : OverlayLayerHost { ownerScope = OverlayOwnerScope.Application, entryId = "application.select", ) + internal val applicationColorPickerPortal: ColorPickerPortalController = + ColorPickerPortalController(ColorPickerRuntime.engine) override fun onInputFrame(viewportWidth: Int, viewportHeight: Int) { rootNode.setViewportBounds( @@ -67,6 +71,7 @@ class ApplicationOverlayHost : OverlayLayerHost { domInputRouter.clear() contextMenuPortal.close() applicationSelectPortal.close() + applicationColorPickerPortal.close() } internal fun debugRootBounds(): Rect = rootNode.bounds diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt index c5048da..5c62278 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt @@ -1,5 +1,11 @@ package org.dreamfinity.dsgl.core.overlay +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupRequest +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerRuntime +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerState +import org.dreamfinity.dsgl.core.colorpicker.RgbaColor +import org.dreamfinity.dsgl.core.colorpicker.ScreenColorSampler +import org.dreamfinity.dsgl.core.colorpicker.ScreenColorSamplerBridge import org.dreamfinity.dsgl.core.contextmenu.ContextMenuRuntime import org.dreamfinity.dsgl.core.contextmenu.contextMenu import org.dreamfinity.dsgl.core.dom.DOMNode @@ -23,6 +29,7 @@ import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -39,6 +46,8 @@ class LiveLayerInteractionPathTests { @AfterTest fun cleanupContextMenuRuntime() { ContextMenuRuntime.engine.closeAll() + ColorPickerRuntime.engine.closeAll() + ScreenColorSamplerBridge.install(null) SelectRuntime.host.closeAll() } @@ -412,6 +421,125 @@ class LiveLayerInteractionPathTests { assertTrue(applicationOverlayHost.handleApplicationSelectKeyDown(KeyCodes.ESCAPE, Char.MIN_VALUE)) } + @Test + fun `application color picker is rendered and consumed through application portal path`() { + val applicationOverlayHost = ApplicationOverlayHost() + applicationOverlayHost.onInputFrame(360, 240) + val owner = "application-color-picker-portal" + ColorPickerRuntime.host.open(colorPickerRequest(owner, OverlayOwnerScope.Application)) + + applicationOverlayHost.applicationColorPickerOnFrame(360, 240, 42, 48) + val commands = ArrayList() + applicationOverlayHost.appendApplicationColorPickerOverlayCommands(ctx, 360, 240, commands) + val layout = ColorPickerRuntime.engine.debugBodyLayout(owner) + assertNotNull(layout) + + val harness = + LiveLayerInputHarness( + debugHandler = { _, _, _ -> false }, + systemOverlayHandler = { _, _, _ -> false }, + applicationOverlayHandler = { x, y, button -> + applicationOverlayHost.handleApplicationColorPickerMouseDown(x, y, button) + }, + ) + var appRootReceived = false + val consumedBy = + harness.dispatchMouseDown( + layout.colorFieldRect.x + 4, + layout.colorFieldRect.y + 4, + MouseButton.LEFT, + ) { + appRootReceived = true + true + } + + assertTrue(commands.isNotEmpty()) + assertEquals(UiLayerId.ApplicationOverlay, consumedBy) + assertFalse(appRootReceived) + assertTrue(applicationOverlayHost.isApplicationColorPickerOpen()) + } + + @Test + fun `application color picker portal preserves drag close and eyedropper capture hooks`() { + ScreenColorSamplerBridge.install(ScreenColorSampler { x, y -> (0xFF shl 24) or (x shl 16) or (y shl 8) or 0x44 }) + val applicationOverlayHost = ApplicationOverlayHost() + applicationOverlayHost.onInputFrame(480, 320) + val owner = "application-color-picker-drag-eyedropper" + var committed: RgbaColor? = null + ColorPickerRuntime.host.open( + colorPickerRequest(owner, OverlayOwnerScope.Application) { + committed = it + }, + ) + applicationOverlayHost.applicationColorPickerOnFrame(480, 320, 120, 80) + + val panelBefore = ColorPickerRuntime.engine.debugPanelRect(owner) ?: error("panel missing") + val header = ColorPickerRuntime.engine.debugHeaderRect(owner) ?: error("header missing") + val dragStartX = header.x + 6 + val dragStartY = header.y + 6 + assertTrue(applicationOverlayHost.handleApplicationColorPickerMouseDown(dragStartX, dragStartY, MouseButton.LEFT)) + assertTrue( + applicationOverlayHost.handleApplicationColorPickerMouseMove( + dragStartX + 40, + dragStartY + 30, + ), + ) + assertTrue( + applicationOverlayHost.handleApplicationColorPickerMouseUp( + dragStartX + 40, + dragStartY + 30, + MouseButton.LEFT, + ), + ) + val panelAfter = ColorPickerRuntime.engine.debugPanelRect(owner) ?: error("panel missing") + assertNotEquals(panelBefore.x, panelAfter.x) + + val layout = ColorPickerRuntime.engine.debugBodyLayout(owner) ?: error("layout missing") + assertTrue( + applicationOverlayHost.handleApplicationColorPickerMouseDown( + layout.pipetteRect.x + 2, + layout.pipetteRect.y + 2, + MouseButton.LEFT, + ), + ) + assertTrue(applicationOverlayHost.hasActiveApplicationColorPickerEyedropper()) + assertTrue(applicationOverlayHost.handleApplicationColorPickerMouseMove(25, 52)) + applicationOverlayHost.captureApplicationColorPickerEyedropperSample() + assertTrue(applicationOverlayHost.handleApplicationColorPickerMouseDown(25, 52, MouseButton.LEFT)) + assertTrue(applicationOverlayHost.handleApplicationColorPickerMouseUp(25, 52, MouseButton.LEFT)) + val expected = RgbaColor.fromArgbInt((0xFF shl 24) or (25 shl 16) or (52 shl 8) or 0x44) + assertEquals(expected.toArgbInt(), committed?.toArgbInt()) + + val closeRect = ColorPickerRuntime.engine.debugCloseRect(owner) ?: error("close missing") + assertTrue( + applicationOverlayHost.handleApplicationColorPickerMouseDown( + closeRect.x + 1, + closeRect.y + 1, + MouseButton.LEFT, + ), + ) + assertFalse(applicationOverlayHost.isApplicationColorPickerOpen()) + } + + @Test + fun `application color picker portal does not consume system owned popup`() { + val applicationOverlayHost = ApplicationOverlayHost() + applicationOverlayHost.onInputFrame(360, 240) + val owner = "system-color-picker-owner" + ColorPickerRuntime.host.open(colorPickerRequest(owner, OverlayOwnerScope.System)) + + applicationOverlayHost.applicationColorPickerOnFrame(360, 240, 42, 48) + val commands = ArrayList() + applicationOverlayHost.appendApplicationColorPickerOverlayCommands(ctx, 360, 240, commands) + val panel = ColorPickerRuntime.engine.debugPanelRect(owner) + assertNotNull(panel) + + assertTrue(ColorPickerRuntime.engine.isOpenFor(owner)) + assertFalse(applicationOverlayHost.isApplicationColorPickerOpen()) + assertFalse(applicationOverlayHost.handleApplicationColorPickerMouseDown(panel.x + 2, panel.y + 2, MouseButton.LEFT)) + assertTrue(commands.isEmpty()) + } + @Test fun `system select is rendered and consumed through system portal path`() { val systemHost = SystemOverlayHost(InspectorController()) @@ -545,6 +673,15 @@ class LiveLayerInteractionPathTests { ) } + private fun colorPickerRequest(owner: Any, ownerScope: OverlayOwnerScope, onCommit: ((RgbaColor) -> Unit)? = null): ColorPickerPopupRequest = + ColorPickerPopupRequest( + owner = owner, + ownerScope = ownerScope, + anchorRect = Rect(32, 36, 24, 18), + state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), + onCommit = onCommit, + ) + private class LiveLayerInputHarness( private val debugHandler: (Int, Int, MouseButton) -> Boolean, private val systemOverlayHandler: (Int, Int, MouseButton) -> Boolean, From 95a41f2c87a74d1b663b890aa125a6e7cb53a7c5 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Sun, 10 May 2026 22:01:28 +0300 Subject: [PATCH 59/78] wiring Inspector with portalHost; --- .../overlay/system/SystemOverlayEntries.kt | 91 +++++++++++++++++++ .../core/overlay/system/SystemOverlayHost.kt | 42 +++++++-- .../SystemOverlayEntryInfrastructureTests.kt | 17 ++++ 3 files changed, 140 insertions(+), 10 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt index 1432510..5d3dbd0 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt @@ -1,7 +1,19 @@ package org.dreamfinity.dsgl.core.overlay.system import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.overlay.OverlayLayerContracts +import org.dreamfinity.dsgl.core.overlay.PortalDismissPolicy +import org.dreamfinity.dsgl.core.overlay.PortalEntry +import org.dreamfinity.dsgl.core.overlay.PortalEntryBounds +import org.dreamfinity.dsgl.core.overlay.PortalEntryId +import org.dreamfinity.dsgl.core.overlay.PortalEntryOrder +import org.dreamfinity.dsgl.core.overlay.PortalEntryPlacement +import org.dreamfinity.dsgl.core.overlay.PortalEntryState +import org.dreamfinity.dsgl.core.overlay.PortalFocusPolicy +import org.dreamfinity.dsgl.core.overlay.PortalInputPolicy +import org.dreamfinity.dsgl.core.overlay.UiLayerId import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelDragSession import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelState import java.util.IdentityHashMap @@ -102,3 +114,82 @@ internal class SystemOverlayEntryRegistry( fun entry(id: SystemOverlayEntryId): SystemOverlayEntry? = byId[id] } + +internal class SystemOverlayPortalEntryAdapter( + private val entry: SystemOverlayEntry, +) : PortalEntry { + override val state: PortalEntryState = + PortalEntryState( + id = PortalEntryId("system.${entry.state.id.name}"), + ownerToken = entry.state, + surface = OverlayLayerContracts.domainSurfaceForLayer(UiLayerId.SystemOverlay), + order = + PortalEntryOrder( + zIndex = entry.state.lane.zOrder, + sequence = entry.state.order, + ), + dismissPolicy = PortalDismissPolicy.None, + inputPolicy = entry.inputPolicy(), + focusPolicy = PortalFocusPolicy.Preserve, + ) + override val node: DOMNode = entry.node + + val systemEntry: SystemOverlayEntry + get() = entry + + fun syncPlacement(viewportWidth: Int, viewportHeight: Int) { + if (!entry.state.active) { + state.deactivate() + return + } + state.activate( + PortalEntryPlacement( + anchorBounds = null, + bounds = + PortalEntryBounds( + viewportBounds = Rect(0, 0, viewportWidth.coerceAtLeast(1), viewportHeight.coerceAtLeast(1)), + entryBounds = entry.resolvePortalEntryBounds(viewportWidth, viewportHeight), + ), + ), + ) + } + + override fun close() { + entry.state.active = false + entry.state.panelState + .hide() + entry.state.dragSession + .end() + state.deactivate() + } + + override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = entry.handleMouseMove(mouseX, mouseY) + + override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + entry.handleMouseDown(mouseX, mouseY, button) + + override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + entry.handleMouseUp(mouseX, mouseY, button) + + override fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = + entry.handleMouseWheel(mouseX, mouseY, delta) + + override fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = entry.handleKeyDown(keyCode, keyChar) + + private fun SystemOverlayEntry.inputPolicy(): PortalInputPolicy = + when { + participatesInDomInput() || enablesDomInputFallbackRouting() -> PortalInputPolicy.ManualThenDomFallback + else -> PortalInputPolicy.ManualOnly + } + + private fun SystemOverlayEntry.resolvePortalEntryBounds(viewportWidth: Int, viewportHeight: Int): Rect { + val panelBounds = state.panelState.currentRectOrNull() + if (panelBounds != null && panelBounds.width > 0 && panelBounds.height > 0) { + return panelBounds + } + if (node.bounds.width > 0 && node.bounds.height > 0) { + return node.bounds + } + return Rect(0, 0, viewportWidth.coerceAtLeast(1), viewportHeight.coerceAtLeast(1)) + } +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt index 5f4eb2b..4ad29e9 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt @@ -14,8 +14,11 @@ import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.inspector.InspectorPanelState import org.dreamfinity.dsgl.core.inspector.internal.SystemInspectorOverlayNode +import org.dreamfinity.dsgl.core.overlay.OverlayLayerContracts import org.dreamfinity.dsgl.core.overlay.OverlayLayerHost import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope +import org.dreamfinity.dsgl.core.overlay.PortalFrameContext +import org.dreamfinity.dsgl.core.overlay.PortalHost import org.dreamfinity.dsgl.core.overlay.UiLayerId import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter import org.dreamfinity.dsgl.core.overlay.input.dispatchManualThenDomFallback @@ -40,6 +43,10 @@ class SystemOverlayHost( SystemOverlayEntryRegistry( listOf(inspectorEntry, colorPickerEntry, colorPickerTransientEntry, overlayPanelDemoEntry), ) + private val portalHost: PortalHost = + PortalHost(OverlayLayerContracts.domainSurfaceForLayer(UiLayerId.SystemOverlay)) + private val portalEntries: List = + entryRegistry.allEntries().map(::SystemOverlayPortalEntryAdapter) private val transientOwnershipRegistry: SystemOverlayTransientOwnershipRegistry = SystemOverlayTransientOwnershipRegistry() private val systemSelectPortal: SelectPortalController = @@ -70,6 +77,10 @@ class SystemOverlayHost( }, ) + init { + portalEntries.forEach(portalHost::register) + } + fun systemInspectorColorPickerPopupHost(): InspectorColorPickerHost = colorPickerEntry fun isSystemColorPickerOpen(): Boolean = colorPickerEntry.isOpen() @@ -133,6 +144,7 @@ class SystemOverlayHost( knownViewportWidth = viewportWidth.coerceAtLeast(1) knownViewportHeight = viewportHeight.coerceAtLeast(1) rootNode.setViewportBounds(knownViewportWidth, knownViewportHeight) + portalHost.onInputFrame(PortalFrameContext(Rect(0, 0, knownViewportWidth, knownViewportHeight))) entryRegistry.allEntries().forEach { entry -> entry.onInputFrame(viewportWidth, viewportHeight) } @@ -157,6 +169,9 @@ class SystemOverlayHost( entryRegistry.allEntries().forEach { entry -> entry.sync(frameContext) } + portalEntries.forEach { entry -> + entry.syncPlacement(knownViewportWidth, knownViewportHeight) + } reconcileMountedEntries() } @@ -205,6 +220,7 @@ class SystemOverlayHost( colorPickerEntry.close() overlayPanelDemoEntry.close() systemSelectPortal.close() + portalEntries.forEach { it.syncPlacement(knownViewportWidth, knownViewportHeight) } domInputRouter.clear() } @@ -214,15 +230,22 @@ class SystemOverlayHost( internal fun debugRegisteredEntryIds(): List = entryRegistry.allEntries().map { it.state.id } + internal fun debugRegisteredPortalEntryIds(): List = portalEntries.map { it.state.id.value } + + internal fun debugActivePortalEntryIds(): List = portalHost.entriesInPaintOrder().map { it.state.id.value } + internal fun debugMountedEntryIds(): List { - val entriesByNode = entryRegistry.allEntries().associateBy { it.node } + val entriesByNode = portalEntries.associateBy { it.node } val mountedNodes = buildList { addAll(rootNode.mountedLaneNodes(SystemOverlayLane.PanelContent)) addAll(rootNode.mountedLaneNodes(SystemOverlayLane.Transient)) } return mountedNodes.mapNotNull { node -> - entriesByNode[node]?.state?.id + entriesByNode[node] + ?.systemEntry + ?.state + ?.id } } @@ -249,7 +272,10 @@ class SystemOverlayHost( internal fun debugRootBounds(): Rect = rootNode.bounds private fun reconcileMountedEntries() { - val activeEntries = entryRegistry.allEntries().filter { it.state.active } + val activeEntries = + portalHost.entriesInPaintOrder().mapNotNull { + (it as? SystemOverlayPortalEntryAdapter)?.systemEntry + } val panelNodes = activeEntries .filter { it.state.lane == SystemOverlayLane.PanelContent } @@ -265,13 +291,9 @@ class SystemOverlayHost( } private fun activeEntriesTopFirst(): List = - entryRegistry - .allEntries() - .filter { it.state.active } - .sortedWith( - compareBy { it.state.lane.zOrder } - .thenBy { it.state.order }, - ).asReversed() + portalHost + .entriesInInputOrder() + .mapNotNull { (it as? SystemOverlayPortalEntryAdapter)?.systemEntry } private inline fun dispatchManualInput(handler: (SystemOverlayEntry) -> Boolean): Boolean = activeEntriesTopFirst() diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntryInfrastructureTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntryInfrastructureTests.kt index 2baf845..d740afb 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntryInfrastructureTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntryInfrastructureTests.kt @@ -31,6 +31,15 @@ class SystemOverlayEntryInfrastructureTests { ), host.debugRegisteredEntryIds(), ) + assertEquals( + listOf( + "system.Inspector", + "system.ColorPickerPopup", + "system.ColorPickerTransient", + "system.PanelDemo", + ), + host.debugRegisteredPortalEntryIds(), + ) } @Test @@ -48,6 +57,7 @@ class SystemOverlayEntryInfrastructureTests { val firstNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("node missing") assertTrue(host.debugMountedEntryIds().contains(SystemOverlayEntryId.Inspector)) assertTrue(firstState.active) + assertEquals(listOf("system.Inspector"), host.debugActivePortalEntryIds()) host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = 22, cursorY = 20, inspectorPointerCaptured = false) val secondState = host.debugEntryState(SystemOverlayEntryId.Inspector) ?: error("state missing") @@ -58,6 +68,7 @@ class SystemOverlayEntryInfrastructureTests { inspector.deactivate() host.syncFrame(root, inspectedLayoutRevision = 4L, cursorX = 22, cursorY = 20, inspectorPointerCaptured = false) assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.Inspector)) + assertFalse(host.debugActivePortalEntryIds().contains("system.Inspector")) } @Test @@ -96,12 +107,14 @@ class SystemOverlayEntryInfrastructureTests { assertEquals(firstRect.x, secondRect.x) assertEquals(firstRect.y, secondRect.y) assertTrue(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerPopup)) + assertTrue(host.debugActivePortalEntryIds().contains("system.ColorPickerPopup")) } finally { pickerHost.close() } host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = 60, cursorY = 65, inspectorPointerCaptured = false) assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerPopup)) + assertFalse(host.debugActivePortalEntryIds().contains("system.ColorPickerPopup")) } @Test @@ -124,6 +137,10 @@ class SystemOverlayEntryInfrastructureTests { listOf(SystemOverlayEntryId.Inspector, SystemOverlayEntryId.ColorPickerPopup), host.debugMountedEntryIds(), ) + assertEquals( + listOf("system.Inspector", "system.ColorPickerPopup"), + host.debugActivePortalEntryIds(), + ) } finally { inspector.deactivate() pickerHost.close() From cdc5b1d0be300b01c09e9ecc90b49f35d8a62c3a Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Mon, 11 May 2026 16:37:26 +0300 Subject: [PATCH 60/78] wiring modal portal with portalHost and integrating application overlay host functionality; adding modal-specific tests for interaction layers; --- .../dsgl/core/components/modal/ModalDsl.kt | 168 ++++--- .../modal/internal/ModalHostNode.kt | 45 ++ .../modal/internal/ModalPortalController.kt | 180 +++++++ .../components/modal/internal/ModalRuntime.kt | 191 ++++++-- .../dsgl/core/event/FocusManager.kt | 10 + .../core/overlay/ApplicationOverlayHost.kt | 5 + .../modal/ModalHostKeyboardRegressionTests.kt | 459 +++++++++++++++++- 7 files changed, 961 insertions(+), 97 deletions(-) create mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt index 2e80042..efaf0c1 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt @@ -1,7 +1,9 @@ package org.dreamfinity.dsgl.core.components.modal import org.dreamfinity.dsgl.core.components.modal.internal.ModalHostNode +import org.dreamfinity.dsgl.core.components.modal.internal.ModalPortalRootNode import org.dreamfinity.dsgl.core.components.modal.internal.ModalRuntime +import org.dreamfinity.dsgl.core.components.modal.internal.modalLifecycleKey import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.elements.InputType import org.dreamfinity.dsgl.core.dsl.* @@ -40,70 +42,25 @@ fun UiScope.modalHost(modals: List, modalKey: String = "modal.host", content() } + val portalRoot = ModalPortalRootNode("$modalKey.portal") + val portalScope = childScope(portalRoot) + hostNode.refTarget = ModalRuntime.portalHostRef(modalKey) + ModalRuntime.registerPortalTemplate(modalKey, portalRoot) + portalRoot.onKeyDown = hostNode.onKeyDown + buildModalLayers(portalScope, modals, modalKey) +} + +private fun buildModalLayers(hostScope: UiScope, modals: List, modalKey: String) { modals.forEachIndexed { index, spec -> - val isTopMost = index == modals.lastIndex - val dialogKey = ModalRuntime.dialogKey(modalKey, spec.key) - val backdropColor = - when (spec.backdrop) { - BackdropMode.True, BackdropMode.Static -> 0x88000000.toInt() - BackdropMode.False -> 0x00000000 - } - hostScope.div({ - key = "$modalKey.modal.${spec.key}.layer" - onMouseDown = { event -> - if (!isTopMost) { - event.cancelled = true - } else { - val insideDialog = isTargetInsideDialog(event.target, dialogKey) - if (!insideDialog && spec.trapFocus) { - FocusManager.requestFocusFirstInSubtree(dialogKey) - } - event.cancelled = true - } - } - onMouseClick = { event -> - if (!isTopMost) { - event.cancelled = true - } else { - val insideDialog = isTargetInsideDialog(event.target, dialogKey) - if (!insideDialog) { - if (spec.backdrop == BackdropMode.True) { - spec.onHide?.invoke() - } - event.cancelled = true - } - } - } - onMouseWheel = { event -> - val insideDialog = isTargetInsideDialog(event.target, dialogKey) - if (!insideDialog) { - event.cancelled = true - } - } - style = { - backgroundColor = backdropColor - display = Display.Flex - flexDirection = FlexDirection.Column - alignItems = AlignItems.Center - justifyContent = if (spec.centered) JustifyContent.Center else JustifyContent.Start - padding { all((if (spec.centered) 6 else 10).px) } - } - }) { - modalFrame( - spec = spec, - dialogKey = dialogKey, - scope = - ModalScope( - dismiss = spec.onHide, - isTopMost = isTopMost, - modalKey = spec.key, - ), - ) - } + hostScope.modalLayer( + spec = spec, + modalKey = modalKey, + isTopMost = index == modals.lastIndex, + ) } hostScope.div({ - key = "$modalKey.modal.lifecycle" + key = modalLifecycleKey(modalKey) ref = RefTarget { handle -> if (handle != null) { @@ -118,6 +75,68 @@ fun UiScope.modalHost(modals: List, modalKey: String = "modal.host", }) } +private fun UiScope.modalLayer(spec: ModalSpec, modalKey: String, isTopMost: Boolean) { + val dialogKey = ModalRuntime.dialogKey(modalKey, spec.key) + div({ + key = "$modalKey.modal.${spec.key}.layer" + onMouseDown = { event -> + if (!isTopMost) { + event.cancelled = true + } else { + val insideDialog = isEventInsideDialog(event.target, dialogKey, event.mouseX, event.mouseY) + if (!insideDialog && spec.trapFocus) { + FocusManager.requestFocusFirstInSubtree(dialogKey) + } + event.cancelled = true + } + } + onMouseClick = { event -> + if (!isTopMost) { + event.cancelled = true + } else { + val insideDialog = isEventInsideDialog(event.target, dialogKey, event.mouseX, event.mouseY) + if (!insideDialog) { + if (spec.backdrop == BackdropMode.True) { + spec.onHide?.invoke() + } + event.cancelled = true + } + } + } + onMouseWheel = { event -> + val insideDialog = isEventInsideDialog(event.target, dialogKey, event.mouseX, event.mouseY) + if (!insideDialog) { + event.cancelled = true + } + } + style = { + backgroundColor = spec.backdropColor() + display = Display.Flex + flexDirection = FlexDirection.Column + alignItems = AlignItems.Center + justifyContent = if (spec.centered) JustifyContent.Center else JustifyContent.Start + padding { all((if (spec.centered) 6 else 10).px) } + } + }) { + modalFrame( + spec = spec, + dialogKey = dialogKey, + scope = + ModalScope( + dismiss = spec.onHide, + isTopMost = isTopMost, + modalKey = spec.key, + ), + ) + } +} + +private fun ModalSpec.backdropColor(): Int = + when (backdrop) { + BackdropMode.True, BackdropMode.Static -> 0x88000000.toInt() + BackdropMode.False -> 0x00000000 + } + fun UiScope.modalFrame( spec: ModalSpec, dialogKey: String = "modal.dialog.${spec.key}", @@ -352,3 +371,32 @@ private fun isTargetInsideDialog(target: DOMNode?, dialogKey: String): Boolean { } return false } + +private fun isEventInsideDialog( + target: DOMNode?, + dialogKey: String, + mouseX: Int, + mouseY: Int, +): Boolean { + if (isTargetInsideDialog(target, dialogKey)) return true + val root = target?.rootAncestor() ?: return false + val dialog = findNodeByKey(root, dialogKey) ?: return false + return dialog.bounds.contains(mouseX, mouseY) +} + +private fun DOMNode.rootAncestor(): DOMNode { + var current = this + while (current.parent != null) { + current = current.parent ?: return current + } + return current +} + +private fun findNodeByKey(root: DOMNode, key: Any?): DOMNode? { + if (root.key == key) return root + root.children.forEach { child -> + val found = findNodeByKey(child, key) + if (found != null) return found + } + return null +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalHostNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalHostNode.kt index 7c95764..8ef33a2 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalHostNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalHostNode.kt @@ -70,3 +70,48 @@ internal class ModalHostNode( override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) = Unit } + +/** + * Application-portal root for modal layers. The regular modal host content stays + * in the application root; modal layers render through this full-viewport root. + */ +internal class ModalPortalRootNode( + key: Any?, +) : DOMNode(key) { + override val styleType: String = "modal-portal-root" + + override fun measure(ctx: UiMeasureContext): Size = + Size( + width = StyleEngine.viewportWidthPx().coerceAtLeast(0), + height = StyleEngine.viewportHeightPx().coerceAtLeast(0), + ) + + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { + bounds = + Rect( + x = 0, + y = 0, + width = + StyleEngine + .viewportWidthPx() + .coerceAtLeast(width) + .coerceAtLeast(0), + height = + StyleEngine + .viewportHeightPx() + .coerceAtLeast(height) + .coerceAtLeast(0), + ) + children.forEach { child -> + child.render(ctx, bounds.x, bounds.y, bounds.width, bounds.height) + } + } + + override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) = Unit +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt new file mode 100644 index 0000000..ddfebe6 --- /dev/null +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt @@ -0,0 +1,180 @@ +package org.dreamfinity.dsgl.core.components.modal.internal + +import org.dreamfinity.dsgl.core.DomTree +import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.event.EventBus +import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.overlay.OverlayLayerContracts +import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope +import org.dreamfinity.dsgl.core.overlay.PortalDismissPolicy +import org.dreamfinity.dsgl.core.overlay.PortalEntry +import org.dreamfinity.dsgl.core.overlay.PortalEntryBounds +import org.dreamfinity.dsgl.core.overlay.PortalEntryId +import org.dreamfinity.dsgl.core.overlay.PortalEntryOrder +import org.dreamfinity.dsgl.core.overlay.PortalEntryPlacement +import org.dreamfinity.dsgl.core.overlay.PortalEntryState +import org.dreamfinity.dsgl.core.overlay.PortalFocusPolicy +import org.dreamfinity.dsgl.core.overlay.PortalHost +import org.dreamfinity.dsgl.core.overlay.PortalInputPolicy + +internal class ModalPortalController { + private val portalHost: PortalHost = + PortalHost(OverlayLayerContracts.portalSurfaceForOwner(OverlayOwnerScope.Application)) + private val entriesByHostKey: LinkedHashMap = LinkedHashMap() + + fun sync(rootNode: DOMNode, viewportWidth: Int, viewportHeight: Int) { + val snapshots = ModalRuntime.portalSnapshots() + val activeHostKeys = snapshots.mapTo(LinkedHashSet()) { it.hostKey } + snapshots.forEach { snapshot -> + val entry = + entriesByHostKey.getOrPut(snapshot.hostKey) { + ModalPortalEntry(snapshot.hostKey, snapshot.root).also(portalHost::register) + } + entry.reconcile(snapshot.root) + entry.syncActive(viewportWidth, viewportHeight) + } + entriesByHostKey + .keys + .filter { it !in activeHostKeys } + .forEach { hostKey -> + val entry = entriesByHostKey.remove(hostKey) ?: return@forEach + portalHost.unregister(entry.state.id) + entry.detach() + } + reconcileMountedRoots(rootNode) + } + + fun close() { + entriesByHostKey.values.forEach { entry -> + portalHost.unregister(entry.state.id) + entry.detach() + } + entriesByHostKey.clear() + } + + fun commitActivePortals() { + portalHost + .entriesInPaintOrder() + .mapNotNull { it as? ModalPortalEntry } + .forEach { entry -> ModalRuntime.commitPortal(entry.hostKey, entry.root) } + } + + internal fun debugActivePortalEntryIds(): List = portalHost.entriesInPaintOrder().map { it.state.id.value } + + internal fun debugFindNodeByKey(key: Any?): DOMNode? = debugFindNode { node -> node.key == key } + + internal fun debugFindNode(predicate: (DOMNode) -> Boolean): DOMNode? = + portalHost + .entriesInPaintOrder() + .mapNotNull { (it as? ModalPortalEntry)?.root } + .firstNotNullOfOrNull { root -> findNode(root, predicate) } + + private fun reconcileMountedRoots(rootNode: DOMNode) { + val activeRoots = + portalHost + .entriesInPaintOrder() + .mapNotNull { (it as? ModalPortalEntry)?.root } + entriesByHostKey.values.forEach { entry -> + if (entry.root !in activeRoots) { + entry.detach() + } + } + activeRoots.forEach { root -> + if (root.parent !== rootNode) { + root.parent + ?.children + ?.remove(root) + root.parent = rootNode + } + } + rootNode.children.removeAll(activeRoots.toSet()) + rootNode.children.addAll(activeRoots) + } + + private fun findNode(root: DOMNode, predicate: (DOMNode) -> Boolean): DOMNode? { + if (predicate(root)) return root + root.children.forEach { child -> + val found = findNode(child, predicate) + if (found != null) { + return found + } + } + return null + } +} + +private class ModalPortalEntry( + val hostKey: String, + templateRoot: ModalPortalRootNode, +) : PortalEntry { + private var tree: DomTree = DomTree(templateRoot) + + val root: ModalPortalRootNode + get() = tree.root as ModalPortalRootNode + + override val state: PortalEntryState = + PortalEntryState( + id = PortalEntryId("application.modal.$hostKey"), + ownerToken = hostKey, + surface = OverlayLayerContracts.portalSurfaceForOwner(OverlayOwnerScope.Application), + order = PortalEntryOrder(zIndex = -100), + dismissPolicy = PortalDismissPolicy.None, + inputPolicy = PortalInputPolicy.DomOnly, + focusPolicy = PortalFocusPolicy.TrapFocus, + ) + override val node: DOMNode + get() = root + + fun reconcile(templateRoot: ModalPortalRootNode) { + val previousRoot = root + val parent = root.parent + val result = tree.reconcileWith(DomTree(templateRoot)) + tree.root = result.root + EventBus.run { + result.detachedRoots.forEach { detached -> detached.clearListenersDeep() } + } + if (previousRoot !== root) { + previousRoot.parent + ?.children + ?.remove(previousRoot) + previousRoot.parent = null + } + root.parent = parent + } + + fun syncActive(viewportWidth: Int, viewportHeight: Int) { + if (!ModalRuntime.shouldKeepPortalActive(hostKey)) { + state.deactivate() + return + } + state.activate( + PortalEntryPlacement( + anchorBounds = null, + bounds = + PortalEntryBounds( + viewportBounds = Rect(0, 0, viewportWidth.coerceAtLeast(1), viewportHeight.coerceAtLeast(1)), + entryBounds = Rect(0, 0, viewportWidth.coerceAtLeast(1), viewportHeight.coerceAtLeast(1)), + ), + ), + ) + } + + fun detach() { + root.parent + ?.children + ?.remove(root) + root.parent = null + } + + override fun close() { + ModalRuntime.forgetPortal(hostKey) + state.deactivate() + } + + override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = false + + override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = false + + override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = false +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalRuntime.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalRuntime.kt index 39ebf60..9fee32d 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalRuntime.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalRuntime.kt @@ -1,26 +1,40 @@ package org.dreamfinity.dsgl.core.components.modal.internal import org.dreamfinity.dsgl.core.components.modal.ModalSpec +import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.event.EventBus import org.dreamfinity.dsgl.core.event.FocusManager +import org.dreamfinity.dsgl.core.hooks.ref.ElementHandle +import org.dreamfinity.dsgl.core.hooks.ref.RefTarget import java.util.concurrent.ConcurrentHashMap +internal fun modalLifecycleKey(hostKey: String): String = "$hostKey.modal.lifecycle" + +private data class ModalMeta( + val restoreFocus: Boolean, +) + +private class ModalHostState { + var previousKeys: List = emptyList() + var previousMetaByKey: Map = emptyMap() + val restoreFocusByModalKey: MutableMap = linkedMapOf() + var pendingRestoreFocusKey: Any? = null + var pendingFocusDialogKey: String? = null + var currentModals: List = emptyList() +} + internal object ModalRuntime { - private data class ModalMeta( - val restoreFocus: Boolean, + data class PortalSnapshot( + val hostKey: String, + val root: ModalPortalRootNode, ) - private class HostState { - var previousKeys: List = emptyList() - var previousMetaByKey: Map = emptyMap() - val restoreFocusByModalKey: MutableMap = linkedMapOf() - var pendingRestoreFocusKey: Any? = null - var pendingFocusDialogKey: String? = null - } - - private val states: MutableMap = ConcurrentHashMap() + private val states: MutableMap = ConcurrentHashMap() + private val portalTemplates: MutableMap = ConcurrentHashMap() + private val portalHostRefs: MutableMap> = ConcurrentHashMap() fun onBuild(hostKey: String, modals: List) { - val state = states.getOrPut(hostKey) { HostState() } + val state = states.getOrPut(hostKey) { ModalHostState() } val currentKeys = modals.map { it.key } val previousKeys = state.previousKeys @@ -56,49 +70,156 @@ internal object ModalRuntime { } state.previousKeys = currentKeys + state.currentModals = modals state.previousMetaByKey = modals.associate { spec -> spec.key to ModalMeta(restoreFocus = spec.restoreFocus) } } - fun onCommit(hostKey: String, modals: List) { + fun onCommit(hostKey: String, modals: List, focusRoot: DOMNode? = null) { val state = states[hostKey] ?: return val topMost = modals.lastOrNull() if (topMost == null) { - val restoreKey = state.pendingRestoreFocusKey - state.pendingRestoreFocusKey = null - if (restoreKey != null) { - if (!FocusManager.requestFocusByKey(restoreKey)) { - FocusManager.clearFocus() - } - } - if (state.previousKeys.isEmpty()) { + if (commitWithoutActiveModal(state, focusRoot)) { states.remove(hostKey) } return } - val topDialogKey = dialogKey(hostKey, topMost.key) + commitWithActiveModal(hostKey, state, topMost, focusRoot) + } - val restoreKey = state.pendingRestoreFocusKey - if (restoreKey != null) { - state.pendingRestoreFocusKey = null - FocusManager.requestFocusByKey(restoreKey) + fun registerPortalTemplate(hostKey: String, root: ModalPortalRootNode) { + val previous = portalTemplates.put(hostKey, root) + if (previous != null && previous !== root && previous.parent == null) { + clearTemplateOwnedListeners(previous) } + } - val needsFocusOnTop = state.pendingFocusDialogKey == topDialogKey - val focusOutsideTop = !FocusManager.isFocusWithinSubtree(topDialogKey) - if ((needsFocusOnTop || (topMost.trapFocus && focusOutsideTop)) && - !FocusManager.requestFocusFirstInSubtree(topDialogKey) - ) { - FocusManager.requestFocusByKey(topDialogKey) - } - if (needsFocusOnTop) { - state.pendingFocusDialogKey = null + fun portalHostRef(hostKey: String): RefTarget = + portalHostRefs.getOrPut(hostKey) { + RefTarget { handle -> + if (handle == null) { + forgetPortal(hostKey) + } + } } + + fun commitPortal(hostKey: String, focusRoot: DOMNode) { + val state = states[hostKey] ?: return + onCommit(hostKey, state.currentModals, focusRoot) + } + + fun portalSnapshots(): List = + portalTemplates + .entries + .sortedBy { it.key } + .map { (hostKey, root) -> + PortalSnapshot( + hostKey = hostKey, + root = root, + ) + } + + fun shouldKeepPortalActive(hostKey: String): Boolean { + val template = portalTemplates[hostKey] ?: return false + val state = states[hostKey] + return template.children.any { it.key != modalLifecycleKey(hostKey) } || + state?.previousKeys?.isNotEmpty() == true || + state?.pendingRestoreFocusKey != null || + state?.pendingFocusDialogKey != null + } + + fun forgetPortal(hostKey: String) { + portalTemplates.remove(hostKey) + portalHostRefs.remove(hostKey) + states.remove(hostKey) } fun dialogKey(hostKey: String, modalKey: String): String = "$hostKey.modal.$modalKey.dialog" } + +private fun clearTemplateOwnedListeners(root: DOMNode) { + EventBus.run { + clearTemplateOwnedListeners( + root = root, + node = root, + ) + } +} + +private fun clearTemplateOwnedListeners(root: DOMNode, node: DOMNode) { + if (!isOwnedByTemplateRoot(root, node)) return + EventBus.run { node.clearOwnListeners() } + node.children.forEach { child -> clearTemplateOwnedListeners(root, child) } +} + +private fun isOwnedByTemplateRoot(root: DOMNode, node: DOMNode): Boolean { + if (node === root) return true + var current = node.parent + while (current != null) { + if (current === root) return true + current = current.parent + } + return false +} + +private fun commitWithoutActiveModal(state: ModalHostState, focusRoot: DOMNode?): Boolean { + val restoreKey = state.pendingRestoreFocusKey + if (restoreKey != null) { + val restored = requestFocusByKey(restoreKey, focusRoot) + if (restored || focusRoot != null) { + state.pendingRestoreFocusKey = null + } + if (!restored && focusRoot != null) { + FocusManager.clearFocus() + } + } + return state.previousKeys.isEmpty() +} + +private fun commitWithActiveModal( + hostKey: String, + state: ModalHostState, + topMost: ModalSpec, + focusRoot: DOMNode?, +) { + restorePendingFocus(state, focusRoot) + + val topDialogKey = ModalRuntime.dialogKey(hostKey, topMost.key) + val needsFocusOnTop = state.pendingFocusDialogKey == topDialogKey + val focusOutsideTop = !FocusManager.isFocusWithinSubtree(topDialogKey) + val shouldFocusTop = needsFocusOnTop || (topMost.trapFocus && focusOutsideTop) + val focusedTop = !shouldFocusTop || focusTopDialog(topDialogKey, focusRoot) + if (needsFocusOnTop && focusedTop) { + state.pendingFocusDialogKey = null + } +} + +private fun restorePendingFocus(state: ModalHostState, focusRoot: DOMNode?) { + val restoreKey = state.pendingRestoreFocusKey ?: return + val restored = requestFocusByKey(restoreKey, focusRoot) + if (restored || focusRoot != null) { + state.pendingRestoreFocusKey = null + } +} + +private fun focusTopDialog(topDialogKey: String, focusRoot: DOMNode?): Boolean = + requestFocusFirstInSubtree(topDialogKey, focusRoot) || requestFocusByKey(topDialogKey, focusRoot) + +private fun requestFocusByKey(key: Any?, focusRoot: DOMNode?): Boolean = + if (focusRoot != null) { + FocusManager.requestFocusByKey(focusRoot, key) || FocusManager.requestFocusByKey(key) + } else { + FocusManager.requestFocusByKey(key) + } + +private fun requestFocusFirstInSubtree(rootKey: Any?, focusRoot: DOMNode?): Boolean = + if (focusRoot != null) { + FocusManager.requestFocusFirstInSubtree(focusRoot, rootKey) || + FocusManager.requestFocusFirstInSubtree(rootKey) + } else { + FocusManager.requestFocusFirstInSubtree(rootKey) + } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/FocusManager.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/FocusManager.kt index a9dd297..10d0471 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/FocusManager.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/FocusManager.kt @@ -107,6 +107,11 @@ object FocusManager { fun requestFocusByKey(key: Any?): Boolean { if (key == null) return false val root = lastRoot ?: return false + return requestFocusByKey(root, key) + } + + fun requestFocusByKey(root: DOMNode, key: Any?): Boolean { + if (key == null) return false val target = findByKey(root, key) ?: return false val focusable = findFirstFocusable(target) ?: return false requestFocus(focusable) @@ -116,6 +121,11 @@ object FocusManager { fun requestFocusFirstInSubtree(rootKey: Any?): Boolean { if (rootKey == null) return false val root = lastRoot ?: return false + return requestFocusFirstInSubtree(root, rootKey) + } + + fun requestFocusFirstInSubtree(root: DOMNode, rootKey: Any?): Boolean { + if (rootKey == null) return false val subtree = findByKey(root, rootKey) ?: return false val focusable = findFirstFocusable(subtree) ?: return false requestFocus(focusable) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt index 9e986be..238139f 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt @@ -3,6 +3,7 @@ package org.dreamfinity.dsgl.core.overlay import org.dreamfinity.dsgl.core.DomTree import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPortalController import org.dreamfinity.dsgl.core.colorpicker.ColorPickerRuntime +import org.dreamfinity.dsgl.core.components.modal.internal.ModalPortalController import org.dreamfinity.dsgl.core.contextmenu.ContextMenuEngine import org.dreamfinity.dsgl.core.contextmenu.ContextMenuRuntime import org.dreamfinity.dsgl.core.dom.DOMNode @@ -38,6 +39,7 @@ class ApplicationOverlayHost : OverlayLayerHost { ) internal val applicationColorPickerPortal: ColorPickerPortalController = ColorPickerPortalController(ColorPickerRuntime.engine) + internal val modalPortal: ModalPortalController = ModalPortalController() override fun onInputFrame(viewportWidth: Int, viewportHeight: Int) { rootNode.setViewportBounds( @@ -48,7 +50,9 @@ class ApplicationOverlayHost : OverlayLayerHost { override fun render(ctx: UiMeasureContext, width: Int, height: Int) { rootNode.setViewportBounds(width, height) + modalPortal.sync(rootNode, width, height) tree.render(ctx, width, height) + modalPortal.commitActivePortals() } override fun paint(ctx: UiMeasureContext): List = tree.paint(ctx, applyStyles = true) @@ -72,6 +76,7 @@ class ApplicationOverlayHost : OverlayLayerHost { contextMenuPortal.close() applicationSelectPortal.close() applicationColorPickerPortal.close() + modalPortal.close() } internal fun debugRootBounds(): Rect = rootNode.bounds diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalHostKeyboardRegressionTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalHostKeyboardRegressionTests.kt index 387f368..7f61d5e 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalHostKeyboardRegressionTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalHostKeyboardRegressionTests.kt @@ -1,22 +1,39 @@ package org.dreamfinity.dsgl.core.components.modal import org.dreamfinity.dsgl.core.DomTree +import org.dreamfinity.dsgl.core.DsglWindow +import org.dreamfinity.dsgl.core.components.modal.internal.ModalRuntime +import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.dom.elements.ButtonNode +import org.dreamfinity.dsgl.core.dom.elements.InputType import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.dsl.button import org.dreamfinity.dsgl.core.dsl.div +import org.dreamfinity.dsgl.core.dsl.input +import org.dreamfinity.dsgl.core.dsl.text import org.dreamfinity.dsgl.core.dsl.ui import org.dreamfinity.dsgl.core.event.EventBus import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.KeyboardKeyDownEvent +import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.event.MouseClickEvent +import org.dreamfinity.dsgl.core.hooks.useState +import org.dreamfinity.dsgl.core.host.DsglWindowHost +import org.dreamfinity.dsgl.core.host.Viewport +import org.dreamfinity.dsgl.core.overlay.ApplicationOverlayHost import org.dreamfinity.dsgl.core.render.RenderCommand import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull +import kotlin.test.assertTrue class ModalHostKeyboardRegressionTests { private val trees: MutableList = ArrayList() + private val overlays: MutableList = ArrayList() + private val hostKeys: MutableSet = LinkedHashSet() private val measureContext = object : UiMeasureContext { override fun measureText(text: String): Int = text.length * 6 @@ -35,10 +52,15 @@ class ModalHostKeyboardRegressionTests { FocusManager.clearFocus() EventBus.run { trees.forEach { tree -> + tree.clearRefs() tree.root.clearListenersDeep() } } + overlays.forEach { overlay -> overlay.clearRefs() } + hostKeys.forEach(ModalRuntime::forgetPortal) trees.clear() + overlays.clear() + hostKeys.clear() } @Test @@ -84,12 +106,256 @@ class ModalHostKeyboardRegressionTests { assertEquals(1080, content.bounds.height) } - private fun buildTree(hostKey: String, modals: List): DomTree = - ui { + @Test + fun `modal layers mount through application overlay portal`() { + val hostKey = "tests.modal.host.portal" + val tree = buildTree(hostKey, listOf(basicModal())) + trees += tree + tree.render(measureContext, 320, 180) + + val modalHost = + tree.root.children + .firstOrNull() + assertNotNull(modalHost) + assertEquals(listOf("$hostKey.content"), modalHost.children.map { it.key }) + + val overlay = ApplicationOverlayHost() + overlays += overlay + overlay.render(measureContext, 320, 180) + + assertEquals(listOf("application.modal.$hostKey"), overlay.modalPortal.debugActivePortalEntryIds()) + } + + @Test + fun `modal portal blocks application root click through`() { + val hostKey = "tests.modal.host.portal.input" + val tree = buildTree(hostKey, listOf(basicModal())) + trees += tree + tree.render(measureContext, 320, 180) + + val overlay = ApplicationOverlayHost() + overlays += overlay + overlay.render(measureContext, 320, 180) + + assertTrue(overlay.handleMouseDown(4, 4, MouseButton.LEFT)) + } + + @Test + fun `modal portal does not dismiss non static modal when clicking inside dialog body`() { + val hostKey = "tests.modal.host.portal.inside.dismiss" + var hideCount = 0 + val tree = buildTree(hostKey, listOf(dismissibleBodyModal { hideCount += 1 })) + trees += tree + tree.render(measureContext, 320, 180) + + val overlay = ApplicationOverlayHost() + overlays += overlay + overlay.render(measureContext, 320, 180) + + val dialog = overlay.modalPortal.debugFindNodeByKey(ModalRuntime.dialogKey(hostKey, "modal.dismissible")) + assertNotNull(dialog) + val clickX = dialog.bounds.x + dialog.bounds.width / 2 + val clickY = dialog.bounds.y + dialog.bounds.height / 2 + + assertTrue(overlay.handleMouseDown(clickX, clickY, MouseButton.LEFT)) + assertTrue(overlay.handleMouseUp(clickX, clickY, MouseButton.LEFT)) + assertEquals(0, hideCount) + } + + @Test + fun `modal portal dismisses non static modal when clicking backdrop`() { + val hostKey = "tests.modal.host.portal.backdrop.dismiss" + var hideCount = 0 + val tree = buildTree(hostKey, listOf(dismissibleBodyModal { hideCount += 1 })) + trees += tree + tree.render(measureContext, 320, 180) + + val overlay = ApplicationOverlayHost() + overlays += overlay + overlay.render(measureContext, 320, 180) + + assertTrue(overlay.handleMouseDown(2, 2, MouseButton.LEFT)) + assertTrue(overlay.handleMouseUp(2, 2, MouseButton.LEFT)) + assertEquals(1, hideCount) + } + + @Test + fun `modal portal keeps topmost focus request on overlay commit`() { + val hostKey = "tests.modal.host.portal.focus" + val current = buildTreeWithContentInput(hostKey, emptyList()) + trees += current + current.render(measureContext, 320, 180) + FocusManager.requestFocus(requireNodeByKey(current.root, "$hostKey.content.input")) + + val withModal = buildTreeWithContentInput(hostKey, listOf(inputModal())) + trees += withModal + current.reconcileWith(withModal) + current.render(measureContext, 320, 180) + + val overlay = ApplicationOverlayHost() + overlays += overlay + overlay.render(measureContext, 320, 180) + + assertEquals("modal.input", FocusManager.focusedNode()?.key) + } + + @Test + fun `modal portal keeps underlying modal pointer interactive after top modal closes`() { + val hostKey = "tests.modal.host.portal.stack.pointer" + var stepOneClicks = 0 + val stepOne = clickableModal("step.one", "step.one.button") { stepOneClicks += 1 } + val stepTwo = clickableModal("step.two", "step.two.button") {} + val current = buildTree(hostKey, listOf(stepOne)) + trees += current + current.render(measureContext, 320, 180) + + val overlay = ApplicationOverlayHost() + overlays += overlay + overlay.render(measureContext, 320, 180) + + val stacked = buildTree(hostKey, listOf(stepOne, stepTwo)) + trees += stacked + current.reconcileWith(stacked) + current.render(measureContext, 320, 180) + overlay.render(measureContext, 320, 180) + + val popped = buildTree(hostKey, listOf(stepOne)) + trees += popped + current.reconcileWith(popped) + current.render(measureContext, 320, 180) + overlay.render(measureContext, 320, 180) + + val stepOneButton = overlay.modalPortal.debugFindNodeByKey("step.one.button") + assertNotNull(stepOneButton) + val clickX = stepOneButton.bounds.x + stepOneButton.bounds.width / 2 + val clickY = stepOneButton.bounds.y + stepOneButton.bounds.height / 2 + + assertTrue(overlay.handleMouseDown(clickX, clickY, MouseButton.LEFT)) + assertTrue(overlay.handleMouseUp(clickX, clickY, MouseButton.LEFT)) + assertEquals(1, stepOneClicks) + } + + @Test + fun `modal portal restores showcase flow modal pointer interaction after closing step two`() { + val hostKey = "tests.modal.host.portal.showcase.flow" + var modals: List = emptyList() + + fun removeModal(key: String) { + modals = modals.filterNot { modal -> modal.key == key } + } + + fun pushModal(modal: ModalSpec) { + modals += modal + } + pushModal(showcaseFlowStepOne(::pushModal, ::removeModal)) + + var tree = buildTree(hostKey, modals) + trees += tree + val overlay = ApplicationOverlayHost() + overlays += overlay + renderTreeAndOverlay(tree, overlay) + + clickOverlayButton(overlay, "Next") + assertEquals(listOf("modal.flow.1", "modal.flow.2"), modals.map { it.key }) + tree = reconcileTree(tree, buildTree(hostKey, modals)) + renderTreeAndOverlay(tree, overlay) + + clickOverlayButton(overlay, "Back to Step 1") + assertEquals(listOf("modal.flow.1"), modals.map { it.key }) + tree = reconcileTree(tree, buildTree(hostKey, modals)) + renderTreeAndOverlay(tree, overlay) + + clickOverlayButton(overlay, "Next") + assertEquals(listOf("modal.flow.1", "modal.flow.2"), modals.map { it.key }) + } + + @Test + fun `modal portal restores showcase flow pointer interaction after closing step two header button`() { + val hostKey = "tests.modal.host.portal.showcase.flow.header" + var modals: List = emptyList() + + fun removeModal(key: String) { + modals = modals.filterNot { modal -> modal.key == key } + } + + fun pushModal(modal: ModalSpec) { + modals += modal + } + pushModal(showcaseFlowStepOne(::pushModal, ::removeModal)) + + var tree = buildTree(hostKey, modals) + trees += tree + val overlay = ApplicationOverlayHost() + overlays += overlay + renderTreeAndOverlay(tree, overlay) + + clickOverlayButton(overlay, "Next") + tree = reconcileTree(tree, buildTree(hostKey, modals)) + renderTreeAndOverlay(tree, overlay) + + clickOverlayButtonInDialog( + overlay = overlay, + text = "x", + dialogKey = ModalRuntime.dialogKey(hostKey, "modal.flow.2"), + ) + assertEquals(listOf("modal.flow.1"), modals.map { it.key }) + tree = reconcileTree(tree, buildTree(hostKey, modals)) + renderTreeAndOverlay(tree, overlay) + + clickOverlayButton(overlay, "Next") + assertEquals(listOf("modal.flow.1", "modal.flow.2"), modals.map { it.key }) + } + + @Test + fun `modal portal restores hook state showcase flow pointer interaction after closing step two`() { + val window = ShowcaseFlowWindow() + val host = RecordingHost(window) + window.attachHost(host) + var tree = renderWithHookSession(window) + trees += tree + val overlay = ApplicationOverlayHost() + overlays += overlay + renderTreeAndOverlay(tree, overlay) + + clickTreeNode(tree, "open.flow") + assertTrue(host.rebuildRequests > 0) + tree = reconcileTree(tree, renderWithHookSession(window)) + renderTreeAndOverlay(tree, overlay) + + clickOverlayButton(overlay, "Next") + tree = reconcileTree(tree, renderWithHookSession(window)) + renderTreeAndOverlay(tree, overlay) + + clickOverlayButton(overlay, "Back to Step 1") + tree = reconcileTree(tree, renderWithHookSession(window)) + renderTreeAndOverlay(tree, overlay) + + clickOverlayButton(overlay, "Next") + tree = reconcileTree(tree, renderWithHookSession(window)) + renderTreeAndOverlay(tree, overlay) + + assertEquals(listOf("modal.flow.1", "modal.flow.2"), window.lastRenderedModalKeys) + } + + private fun buildTree(hostKey: String, modals: List): DomTree { + hostKeys += hostKey + return ui { modalHost(modals = modals, modalKey = hostKey) { div({ key = "$hostKey.content" }) } } + } + + private fun buildTreeWithContentInput(hostKey: String, modals: List): DomTree { + hostKeys += hostKey + return ui { + modalHost(modals = modals, modalKey = hostKey) { + input(InputType.Text(value = ""), { + key = "$hostKey.content.input" + }) + } + } + } private fun staticModal(): ModalSpec = ModalSpec( @@ -97,4 +363,193 @@ class ModalHostKeyboardRegressionTests { backdrop = BackdropMode.Static, keyboard = false, ) { _ -> } + + private fun basicModal(): ModalSpec = + ModalSpec( + key = "modal.basic", + ) { _ -> } + + private fun inputModal(): ModalSpec = + ModalSpec( + key = "modal.input", + ) { _ -> + input(InputType.Text(value = ""), { + key = "modal.input" + }) + } + + private fun dismissibleBodyModal(onHide: () -> Unit): ModalSpec = + ModalSpec( + key = "modal.dismissible", + centered = true, + onHide = onHide, + ) { _ -> + modalBody { + text("Clicking this non-interactive body area must not dismiss the modal.") + } + } + + private fun clickableModal(modalKey: String, buttonKey: String, onClick: () -> Unit): ModalSpec = + ModalSpec(key = modalKey) { _ -> + modalBody { + button("Click", { + key = buttonKey + onMouseClick = { onClick() } + }) + } + } + + private fun showcaseFlowStepOne(onPushModal: (ModalSpec) -> Unit, onRemoveModal: (String) -> Unit): ModalSpec = + ModalSpec( + key = "modal.flow.1", + onHide = { onRemoveModal("modal.flow.1") }, + ) { scope -> + modalHeader(closeButton = true, onHide = scope.dismiss) { + modalTitle("Flow Step 1") + } + modalBody { + text("Step 1 remains visible but inert when Step 2 is pushed.") + } + modalFooter { + button("Close", { + onMouseClick = { scope.dismiss?.invoke() } + }) + button("Next", { + onMouseClick = { + onPushModal(showcaseFlowStepTwo(onRemoveModal)) + } + }) + } + } + + private fun showcaseFlowStepTwo(onRemoveModal: (String) -> Unit): ModalSpec = + ModalSpec( + key = "modal.flow.2", + centered = true, + onHide = { onRemoveModal("modal.flow.2") }, + ) { scope -> + modalHeader(closeButton = true, onHide = scope.dismiss) { + modalTitle("Flow Step 2") + } + modalBody { + text("Topmost modal only. Closing returns interaction to Step 1.") + } + modalFooter { + button("Back to Step 1", { + onMouseClick = { scope.dismiss?.invoke() } + }) + } + } + + private fun renderTreeAndOverlay(tree: DomTree, overlay: ApplicationOverlayHost) { + tree.render(measureContext, 320, 180) + overlay.render(measureContext, 320, 180) + } + + private fun reconcileTree(current: DomTree, next: DomTree): DomTree { + trees += next + current.reconcileWith(next) + return current + } + + private fun clickOverlayButton(overlay: ApplicationOverlayHost, text: String) { + val button = + overlay.modalPortal.debugFindNode { node -> + node is ButtonNode && node.text == text + } + assertNotNull(button) + val clickX = button.bounds.x + button.bounds.width / 2 + val clickY = button.bounds.y + button.bounds.height / 2 + assertTrue(overlay.handleMouseDown(clickX, clickY, MouseButton.LEFT)) + assertTrue(overlay.handleMouseUp(clickX, clickY, MouseButton.LEFT)) + } + + private fun clickOverlayButtonInDialog(overlay: ApplicationOverlayHost, text: String, dialogKey: String) { + val button = + overlay.modalPortal.debugFindNode { node -> + node is ButtonNode && node.text == text && hasAncestorWithKey(node, dialogKey) + } + assertNotNull(button) + val clickX = button.bounds.x + button.bounds.width / 2 + val clickY = button.bounds.y + button.bounds.height / 2 + assertTrue(overlay.handleMouseDown(clickX, clickY, MouseButton.LEFT)) + assertTrue(overlay.handleMouseUp(clickX, clickY, MouseButton.LEFT)) + } + + private fun hasAncestorWithKey(node: DOMNode, key: Any?): Boolean { + var current: DOMNode? = node + while (current != null) { + if (current.key == key) return true + current = current.parent + } + return false + } + + private fun renderWithHookSession(window: DsglWindow): DomTree { + window.beginRenderBuild() + return try { + window.render() + } finally { + window.endRenderBuild() + window.commitRenderBuild() + } + } + + private inner class ShowcaseFlowWindow : DsglWindow() { + var lastRenderedModalKeys: List = emptyList() + + override fun render(): DomTree = + ui { + var modals by useState(emptyList()) + lastRenderedModalKeys = modals.map { it.key } + + fun removeModal(key: String) { + modals = modals.filterNot { modal -> modal.key == key } + } + + fun pushModal(modal: ModalSpec) { + modals += modal + } + modalHost(modals = modals, modalKey = "tests.modal.host.portal.hook.showcase") { + button("Open flow step 1", { + key = "open.flow" + onMouseClick = { pushModal(showcaseFlowStepOne(::pushModal, ::removeModal)) } + }) + } + } + } + + private class RecordingHost( + override val window: DsglWindow, + ) : DsglWindowHost { + var rebuildRequests: Int = 0 + + override fun requestRebuild(reason: String?) { + rebuildRequests += 1 + } + + override fun requestRedraw() = Unit + + override fun getViewport(): Viewport = Viewport(width = 320, height = 180) + } + + private fun clickTreeNode(tree: DomTree, key: Any) { + val node = requireNodeByKey(tree.root, key) + val clickX = node.bounds.x + node.bounds.width / 2 + val clickY = node.bounds.y + node.bounds.height / 2 + EventBus.post( + MouseClickEvent(clickX, clickY, MouseButton.LEFT).also { event -> + event.target = node + }, + ) + } + + private fun requireNodeByKey(root: DOMNode, key: Any): DOMNode { + if (root.key == key) return root + root.children.forEach { child -> + val found = runCatching { requireNodeByKey(child, key) }.getOrNull() + if (found != null) return found + } + error("Missing node key=$key") + } } From 5c8e74a53ad27cd687785d237b15e4d4aa18837c Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Mon, 11 May 2026 20:00:03 +0300 Subject: [PATCH 61/78] wiring debug scope and overlay debug control with portalHost; --- .../org/dreamfinity/dsgl/core/DomTree.kt | 22 ++++++++--------- .../core/debug/OverlayDebugControlHost.kt | 5 +++- .../dsgl/core/style/StyleApplicationScope.kt | 1 + .../dsgl/core/style/StyleEngine.kt | 7 ++++-- .../debug/OverlayDebugControlHostTests.kt | 8 +++++++ .../SystemOverlayStyleIsolationTests.kt | 24 +++++++++++++++++++ 6 files changed, 52 insertions(+), 15 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/DomTree.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/DomTree.kt index 35e764f..3b564eb 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/DomTree.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/DomTree.kt @@ -124,7 +124,7 @@ class DomTree( laidOut && lastWidth > 0 && lastHeight > 0 && - styleScope != StyleApplicationScope.SystemOverlay && + styleScope == StyleApplicationScope.Application && !styleReport.layoutDirty && !styleReport.visualDirty && scrollInvalidation.visualDirty && @@ -142,17 +142,15 @@ class DomTree( ScrollPerformanceCounters.recordScrollVisualGeometryRefresh(translatedNodes) ScrollPerformanceCounters.recordStickyVisualRefresh(resolvedStickyNodes) } - val requiresSystemOverlayScrollLayoutFallback = - styleScope == StyleApplicationScope.SystemOverlay && scrollInvalidation.visualDirty - if (( - !laidOut || - styleReport.layoutDirty || - scrollInvalidation.layoutDirty || - requiresSystemOverlayScrollLayoutFallback - ) && - lastWidth > 0 && - lastHeight > 0 - ) { + val requiresIsolatedScopeScrollLayoutFallback = + styleScope != StyleApplicationScope.Application && scrollInvalidation.visualDirty + val requiresLayoutPass = + !laidOut || + styleReport.layoutDirty || + scrollInvalidation.layoutDirty || + requiresIsolatedScopeScrollLayoutFallback + val hasKnownViewport = lastWidth > 0 && lastHeight > 0 + if (requiresLayoutPass && hasKnownViewport) { val layoutStartNanos = System.nanoTime() root.resolveLayoutStyleValues( ctx = ctx, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt index 7f1dcad..90c5258 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt @@ -55,7 +55,7 @@ class OverlayDebugControlHost( private val tree: DomTree = DomTree( root = rootNode, - styleScope = StyleApplicationScope.SystemOverlay, + styleScope = StyleApplicationScope.Debug, ) private var lastToggleSnapshot: OverlayDebugToggleSnapshot? = null @@ -152,6 +152,9 @@ class OverlayDebugControlHost( internal fun debugLayout(): OverlayDebugControlLayout? = layout + internal val debugStyleScope: StyleApplicationScope + get() = StyleApplicationScope.Debug + private fun buildLayout(viewportWidth: Int, viewportHeight: Int): OverlayDebugControlLayout { val panelWidth = 300 val panelHeight = 176 + 56 diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleApplicationScope.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleApplicationScope.kt index be9a721..cf97075 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleApplicationScope.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleApplicationScope.kt @@ -3,4 +3,5 @@ package org.dreamfinity.dsgl.core.style enum class StyleApplicationScope { Application, SystemOverlay, + Debug, } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt index ca26c5a..06453ea 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt @@ -376,6 +376,7 @@ object StyleEngine { } StyleApplicationScope.SystemOverlay -> 0L + StyleApplicationScope.Debug -> 0L } return base xor (pseudoStateVersion shl 3) xor @@ -755,7 +756,9 @@ object StyleEngine { private fun snapshotForScope(scope: StyleApplicationScope): StylesheetSnapshot = when (scope) { StyleApplicationScope.Application -> StylesheetManager.snapshot() - StyleApplicationScope.SystemOverlay -> + StyleApplicationScope.SystemOverlay, + StyleApplicationScope.Debug, + -> StylesheetSnapshot( version = Long.MIN_VALUE, index = RuleIndex.EMPTY, @@ -764,7 +767,7 @@ object StyleEngine { } private fun resolvedVariables(snapshot: StylesheetSnapshot, scope: StyleApplicationScope): Map { - if (scope == StyleApplicationScope.SystemOverlay) { + if (scope != StyleApplicationScope.Application) { return emptyMap() } val variables = linkedMapOf() diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHostTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHostTests.kt index aea2804..f168379 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHostTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHostTests.kt @@ -4,6 +4,7 @@ import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.overlay.UiLayerId import org.dreamfinity.dsgl.core.render.RenderCommand +import org.dreamfinity.dsgl.core.style.StyleApplicationScope import java.util.Locale import kotlin.test.AfterTest import kotlin.test.Test @@ -47,6 +48,13 @@ class OverlayDebugControlHostTests { assertTrue(host.handleMouseUp(layout.panelRect.x + 3, layout.panelRect.y + 3, MouseButton.LEFT)) } + @Test + fun `debug control host uses explicit debug style scope`() { + val host = OverlayDebugControlHost() + + assertEquals(StyleApplicationScope.Debug, host.debugStyleScope) + } + @Test fun `reset all restores overlay render and input toggles`() { OverlayLayerDebugState.setControlsEnabledTestOverride(true) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayStyleIsolationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayStyleIsolationTests.kt index da062e4..b19e74c 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayStyleIsolationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayStyleIsolationTests.kt @@ -70,6 +70,30 @@ class SystemOverlayStyleIsolationTests { assertEquals(DsglColors.TEXT, systemProbe.appliedColor) } + @Test + fun `debug scope ignores user stylesheet rules`() { + val stylesDir = + createTempStylesDir( + """ + * { color: #FF5500; } + probe { color: #00CCAA; } + """.trimIndent(), + ) + StyleEngine.setStylesDirectory(stylesDir) + StyleEngine.forceReloadStylesheets() + + val appRoot = ContainerNode(key = "app-root") + val appProbe = ProbeNode(key = "app-probe").applyParent(appRoot) + val debugRoot = ContainerNode(key = "debug-root") + val debugProbe = ProbeNode(key = "debug-probe").applyParent(debugRoot) + + StyleEngine.applyStylesRecursively(appRoot, StyleApplicationScope.Application) + StyleEngine.applyStylesRecursively(debugRoot, StyleApplicationScope.Debug) + + assertEquals(0xFF00CCAA.toInt(), appProbe.appliedColor) + assertEquals(DsglColors.TEXT, debugProbe.appliedColor) + } + private fun createTempStylesDir(dss: String): File { val root = Files.createTempDirectory("dsgl-system-style-").toFile() root.resolve("test.dss").writeText(dss) From db50661b1ed7021ecdd17ae7e530ce563c37e17e Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Mon, 11 May 2026 23:05:07 +0300 Subject: [PATCH 62/78] removing migrated runtime facades; adding follow-up cleanup task; --- .../dsgl/mcForge1710/demo/ShowcaseWindow.kt | 4 +- .../examples/cookbook/ModalStackWindow.kt | 4 +- .../demo/sections/ContextMenuSection.kt | 4 +- .../demo/sections/InputsGallerySection.kt | 4 +- .../demo/sections/ModalsSection.kt | 2 +- .../mcForge1710/demo/support/DemoSection.kt | 2 +- .../dsgl/mcForge1710/DsglScreenHost.kt | 207 ++++++---------- core/detekt-baseline.xml | 6 +- .../colorpicker/ColorPickerPopupRuntime.kt | 5 +- .../ColorPickerPortalController.kt | 11 +- .../dsgl/core/components/modal/ModalDsl.kt | 26 +- .../modal/internal/ModalPortalController.kt | 38 +-- .../{ModalHostNode.kt => ModalPortalNode.kt} | 8 +- ...lRuntime.kt => ModalPortalSessionStore.kt} | 72 +++--- ...untime.kt => ContextMenuPortalServices.kt} | 3 +- .../dsgl/core/dom/ContextMenuEvents.kt | 6 +- .../dom/elements/ColorPickerPopupPaneNode.kt | 12 +- .../dsgl/core/dom/elements/SelectNode.kt | 30 +-- .../internal/SystemInspectorOverlayNode.kt | 6 +- .../ApplicationColorPickerPortalExtensions.kt | 63 ----- .../core/overlay/ApplicationOverlayHost.kt | 234 ++++++++++-------- .../core/overlay/system/SystemOverlayHost.kt | 24 +- .../core/select/SelectPortalController.kt | 11 +- .../dsgl/core/select/SelectPortalServices.kt | 36 +++ .../dsgl/core/select/SelectRuntime.kt | 40 --- .../dsgl/core/DomTreeCachingTests.kt | 6 +- ...> ModalPortalHookOwnerPropagationTests.kt} | 12 +- ... => ModalPortalKeyboardRegressionTests.kt} | 44 ++-- ...sts.kt => ModalPortalSessionStoreTests.kt} | 32 +-- .../core/dom/SelectNodeOwnerScopeTests.kt | 8 +- .../dom/SelectPopupAnchoringStickyTests.kt | 14 +- .../overlay/LiveLayerInteractionPathTests.kt | 207 ++++++++-------- .../InspectorDragScrollDomMigrationTests.kt | 14 +- .../InspectorDropdownCorrectiveTests.kt | 32 +-- .../system/InspectorInputPathBaselineTests.kt | 46 ++-- .../system/InspectorPointerAlignmentTests.kt | 30 +-- .../InspectorTextEditingDomMigrationTests.kt | 6 +- .../SystemOverlayColorPickerEntryTests.kt | 58 ++--- .../SystemOverlayEntryInfrastructureTests.kt | 4 +- .../SystemOverlayInspectorNativeEntryTests.kt | 58 ++--- .../SelectPortalServicesOwnershipTests.kt | 58 +++++ .../SelectRuntimeOwnershipBridgeTests.kt | 58 ----- docs/cookbook.md | 8 +- docs/elements-overview.md | 9 +- 44 files changed, 730 insertions(+), 832 deletions(-) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/{ModalHostNode.kt => ModalPortalNode.kt} (94%) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/{ModalRuntime.kt => ModalPortalSessionStore.kt} (75%) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/{ContextMenuRuntime.kt => ContextMenuPortalServices.kt} (61%) delete mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationColorPickerPortalExtensions.kt create mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalServices.kt delete mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntime.kt rename core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/{ModalHostHookOwnerPropagationTests.kt => ModalPortalHookOwnerPropagationTests.kt} (82%) rename core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/{ModalHostKeyboardRegressionTests.kt => ModalPortalKeyboardRegressionTests.kt} (93%) rename core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/{ModalRuntimeTests.kt => ModalPortalSessionStoreTests.kt} (74%) create mode 100644 core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalServicesOwnershipTests.kt delete mode 100644 core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntimeOwnershipBridgeTests.kt diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/ShowcaseWindow.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/ShowcaseWindow.kt index 9720ad8..83f132f 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/ShowcaseWindow.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/ShowcaseWindow.kt @@ -10,7 +10,7 @@ import org.dreamfinity.dsgl.core.DsglColors import org.dreamfinity.dsgl.core.DsglWindow import org.dreamfinity.dsgl.core.animation.keyframes import org.dreamfinity.dsgl.core.components.modal.ModalSpec -import org.dreamfinity.dsgl.core.components.modal.modalHost +import org.dreamfinity.dsgl.core.components.modal.modalPortal import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.Event import org.dreamfinity.dsgl.core.style.Display @@ -109,7 +109,7 @@ class ShowcaseWindow : DsglWindow() { renderPasses += 1 return ui { - modalHost(modals = demoModals, modalKey = "showcase.modalHost") { + modalPortal(modals = demoModals, key = "showcase.modalPortal") { div({ key = "showcase.root" style = { diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/cookbook/ModalStackWindow.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/cookbook/ModalStackWindow.kt index 9b61e57..044b491 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/cookbook/ModalStackWindow.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/examples/cookbook/ModalStackWindow.kt @@ -5,7 +5,7 @@ import org.dreamfinity.dsgl.core.components.modal.ModalSpec import org.dreamfinity.dsgl.core.components.modal.modalBody import org.dreamfinity.dsgl.core.components.modal.modalFooter import org.dreamfinity.dsgl.core.components.modal.modalHeader -import org.dreamfinity.dsgl.core.components.modal.modalHost +import org.dreamfinity.dsgl.core.components.modal.modalPortal import org.dreamfinity.dsgl.core.components.modal.modalTitle import org.dreamfinity.dsgl.core.dsl.UiScope import org.dreamfinity.dsgl.core.dsl.button @@ -29,7 +29,7 @@ private fun UiScope.modalStackRecipe() { modals = modals.filterNot { it.key == key } } - modalHost(modals = modals, modalKey = "recipe.modal.host") { + modalPortal(modals = modals, key = "recipe.modal.host") { button("Open modal", { onMouseClick = { modals += diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ContextMenuSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ContextMenuSection.kt index f266bc2..7c8f84e 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ContextMenuSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ContextMenuSection.kt @@ -1,7 +1,7 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections -import org.dreamfinity.dsgl.core.contextmenu.ContextMenuRuntime +import org.dreamfinity.dsgl.core.contextmenu.ContextMenuPortalServices import org.dreamfinity.dsgl.core.contextmenu.ContextMenuStyle import org.dreamfinity.dsgl.core.contextmenu.contextMenu import org.dreamfinity.dsgl.core.dnd.* @@ -753,7 +753,7 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { } val entries = contextMenuVisibleFiles() - ContextMenuRuntime.engine.setStyle( + ContextMenuPortalServices.engine.setStyle( ContextMenuStyle( panelPaddingX = 4, panelPaddingY = 4, diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt index da11ecd..3e6c70d 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt @@ -5,7 +5,7 @@ import org.dreamfinity.dsgl.core.dom.elements.InputType import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.hooks.useState -import org.dreamfinity.dsgl.core.select.SelectRuntime +import org.dreamfinity.dsgl.core.select.SelectPortalServices import org.dreamfinity.dsgl.core.select.SelectStyle import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection @@ -63,7 +63,7 @@ fun UiScope.inputsGallerySection(clippingScrollDemoText: String, onClippingScrol .sorted() .joinToString(",") - SelectRuntime.engine.setStyle( + SelectPortalServices.engine.setStyle( SelectStyle( panelBackgroundColor = 0xFF202A35.toInt(), panelBorderColor = 0xFF607286.toInt(), diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ModalsSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ModalsSection.kt index 717c991..2015786 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ModalsSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ModalsSection.kt @@ -154,7 +154,7 @@ private fun largeCenteredModal(onRemoveModal: (String) -> Unit): ModalSpec = } modalBody { text("Preset size: Lg; centered=true") - text("ModalHost keeps background inert while open.", { style = { color = DEMO_MUTED } }) + text("Modal portal keeps background inert while open.", { style = { color = DEMO_MUTED } }) } modalFooter { button("Done", { diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/DemoSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/DemoSection.kt index 3e1085d..8256be0 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/DemoSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/DemoSection.kt @@ -22,7 +22,7 @@ enum class DemoSection( "CSS Cascade & Combinators", "Descendant/child/sibling selectors, specificity, source order, !important, inheritance", ), - MODALS("Modals", "Declarative stacked modal host (RB-inspired)"), + MODALS("Modals", "Declarative stacked modal portal (RB-inspired)"), CONTEXT_MENU("Context Menu", "Right-click nested menus with overlay-first hit testing"), INPUTS("Inputs Gallery", "All input factory variants and textarea"), INPUT_EVENTS("Input Events", "HTML-like onFocus/onBlur/onInput/onChange"), diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index d5c6771..3c67ccd 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -33,36 +33,20 @@ import org.dreamfinity.dsgl.core.overlay.OverlayLayerContracts import org.dreamfinity.dsgl.core.overlay.OverlayLayerHost import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.overlay.UiLayerId -import org.dreamfinity.dsgl.core.overlay.appendApplicationColorPickerOverlayCommands -import org.dreamfinity.dsgl.core.overlay.appendApplicationSelectOverlayCommands -import org.dreamfinity.dsgl.core.overlay.appendContextMenuOverlayCommands -import org.dreamfinity.dsgl.core.overlay.applicationColorPickerOnFrame -import org.dreamfinity.dsgl.core.overlay.applicationSelectOnFrame -import org.dreamfinity.dsgl.core.overlay.captureApplicationColorPickerEyedropperSample -import org.dreamfinity.dsgl.core.overlay.closeContextMenus -import org.dreamfinity.dsgl.core.overlay.contextMenuOnFrame -import org.dreamfinity.dsgl.core.overlay.handleApplicationColorPickerKeyDown -import org.dreamfinity.dsgl.core.overlay.handleApplicationColorPickerMouseDown -import org.dreamfinity.dsgl.core.overlay.handleApplicationColorPickerMouseMove -import org.dreamfinity.dsgl.core.overlay.handleApplicationColorPickerMouseUp -import org.dreamfinity.dsgl.core.overlay.handleApplicationColorPickerMouseWheel -import org.dreamfinity.dsgl.core.overlay.handleApplicationSelectKeyDown -import org.dreamfinity.dsgl.core.overlay.handleApplicationSelectMouseDown -import org.dreamfinity.dsgl.core.overlay.handleApplicationSelectMouseMove -import org.dreamfinity.dsgl.core.overlay.handleApplicationSelectMouseUp -import org.dreamfinity.dsgl.core.overlay.handleApplicationSelectMouseWheel -import org.dreamfinity.dsgl.core.overlay.handleContextMenuKeyDown -import org.dreamfinity.dsgl.core.overlay.handleContextMenuMouseDown -import org.dreamfinity.dsgl.core.overlay.handleContextMenuMouseMove -import org.dreamfinity.dsgl.core.overlay.handleContextMenuMouseUp -import org.dreamfinity.dsgl.core.overlay.handleContextMenuMouseWheel -import org.dreamfinity.dsgl.core.overlay.hasActiveApplicationColorPickerEyedropper -import org.dreamfinity.dsgl.core.overlay.isApplicationColorPickerOpen -import org.dreamfinity.dsgl.core.overlay.isApplicationSelectOpen -import org.dreamfinity.dsgl.core.overlay.isContextMenuOpen +import org.dreamfinity.dsgl.core.overlay.appendPortalOverlayCommands +import org.dreamfinity.dsgl.core.overlay.captureColorPickerEyedropperSample +import org.dreamfinity.dsgl.core.overlay.closeFloatingPortals +import org.dreamfinity.dsgl.core.overlay.handlePortalKeyDownAfterDom +import org.dreamfinity.dsgl.core.overlay.handlePortalKeyDownBeforeDom +import org.dreamfinity.dsgl.core.overlay.handlePortalPointerAfterDom +import org.dreamfinity.dsgl.core.overlay.handlePortalPointerBeforeDom +import org.dreamfinity.dsgl.core.overlay.hasActiveColorPickerEyedropper +import org.dreamfinity.dsgl.core.overlay.hasOpenColorPickerPortal +import org.dreamfinity.dsgl.core.overlay.hasOpenContextMenuPortal +import org.dreamfinity.dsgl.core.overlay.hasOpenSelectPortal +import org.dreamfinity.dsgl.core.overlay.syncPortalFrame import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayHost import org.dreamfinity.dsgl.core.render.RenderCommand -import org.dreamfinity.dsgl.core.select.SelectRuntime import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.StyleEngine import org.lwjgl.input.Keyboard @@ -416,10 +400,15 @@ abstract class DsglScreenHost( } private fun syncFeatureRuntimeFrame(tree: DomTree, dsglMouseX: Int, dsglMouseY: Int) { - applicationOverlayHost.contextMenuOnFrame(adapter, lastWidth, lastHeight, 1f) - applicationOverlayHost.applicationSelectOnFrame(adapter, lastWidth, lastHeight, 1f) - systemOverlayHost.systemSelectOnFrame(adapter, lastWidth, lastHeight, 1f) - applicationOverlayHost.applicationColorPickerOnFrame(lastWidth, lastHeight, dsglMouseX, dsglMouseY) + applicationOverlayHost.syncPortalFrame( + measureContext = adapter, + viewportWidth = lastWidth, + viewportHeight = lastHeight, + viewportScale = 1f, + mouseX = dsglMouseX, + mouseY = dsglMouseY, + ) + systemOverlayHost.syncPortalFrame(adapter, lastWidth, lastHeight, 1f) refreshActiveColorSamplerOwner(tree.root) } @@ -478,11 +467,11 @@ abstract class DsglScreenHost( systemOverlayCommandsBuffer.clear() systemOverlayCommandsBuffer.addAll(systemOverlayCommands) if (systemOverlayRenderEnabled) { - systemOverlayHost.appendSystemSelectOverlayCommands( - adapter, - lastWidth, - lastHeight, - systemOverlayCommandsBuffer, + systemOverlayHost.appendPortalOverlayCommands( + measureContext = adapter, + viewportWidth = lastWidth, + viewportHeight = lastHeight, + out = systemOverlayCommandsBuffer, ) } } @@ -504,10 +493,11 @@ abstract class DsglScreenHost( systemOverlayInputEnabled: Boolean, inspectorBlocks: Boolean, ) { - val contextMenuBlocks = appOverlayInputEnabled && !inspectorBlocks && applicationOverlayHost.isContextMenuOpen() + val contextMenuBlocks = + appOverlayInputEnabled && !inspectorBlocks && applicationOverlayHost.hasOpenContextMenuPortal() val selectBlocks = - appOverlayInputEnabled && !inspectorBlocks && applicationOverlayHost.isApplicationSelectOpen() - val systemSelectBlocks = systemOverlayInputEnabled && systemOverlayHost.isSystemSelectOpen() + appOverlayInputEnabled && !inspectorBlocks && applicationOverlayHost.hasOpenSelectPortal() + val systemSelectBlocks = systemOverlayInputEnabled && systemOverlayHost.hasOpenPortal() val inlineSamplerOwnsSession = activeColorSamplerOwner is ActiveColorSamplerOwner.Inline val colorPickerBlocks = !inspectorBlocks && @@ -515,7 +505,7 @@ abstract class DsglScreenHost( (systemOverlayInputEnabled && systemOverlayHost.isSystemColorPickerOpen()) || ( appOverlayInputEnabled && - applicationOverlayHost.isApplicationColorPickerOpen() && + applicationOverlayHost.hasOpenColorPickerPortal() && !inlineSamplerOwnsSession ) ) @@ -562,19 +552,7 @@ abstract class DsglScreenHost( lastHeight, applicationOverlayCommandsBuffer, ) - applicationOverlayHost.appendApplicationSelectOverlayCommands( - measureContext = adapter, - viewportWidth = lastWidth, - viewportHeight = lastHeight, - out = applicationOverlayCommandsBuffer, - ) - applicationOverlayHost.appendContextMenuOverlayCommands( - measureContext = adapter, - viewportWidth = lastWidth, - viewportHeight = lastHeight, - out = applicationOverlayCommandsBuffer, - ) - applicationOverlayHost.appendApplicationColorPickerOverlayCommands( + applicationOverlayHost.appendPortalOverlayCommands( measureContext = adapter, viewportWidth = lastWidth, viewportHeight = lastHeight, @@ -639,9 +617,8 @@ abstract class DsglScreenHost( ScreenColorSamplerBridge.install(null) FocusManager.clearFocus() DndRuntime.engine.cancelActiveDrag() - ColorPickerRuntime.engine.closeAll() - SelectRuntime.host.closeAll() - applicationOverlayHost.closeContextMenus() + applicationOverlayHost.closeFloatingPortals() + systemOverlayHost.clearRefs() clearActiveTarget() flushPendingCleanup() clearHoverChainStates() @@ -687,7 +664,7 @@ abstract class DsglScreenHost( val height = viewport.height lastViewport = viewport if (force || width != lastWidth || height != lastHeight) { - applicationOverlayHost.closeContextMenus() + applicationOverlayHost.closeFloatingPortals() lastWidth = width lastHeight = height needsLayout = true @@ -787,9 +764,11 @@ abstract class DsglScreenHost( ) runOverlayInputFrame(applicationOverlayHost) runOverlayInputFrame(systemOverlayHost) - applicationOverlayHost.applicationColorPickerOnFrame( + applicationOverlayHost.syncPortalFrame( + measureContext = adapter, viewportWidth = lastWidth, viewportHeight = lastHeight, + viewportScale = 1f, mouseX = if (lastMoveX == Int.MIN_VALUE) lastMouseX else lastMoveX, mouseY = if (lastMoveY == Int.MIN_VALUE) lastMouseY else lastMoveY, ) @@ -945,19 +924,15 @@ abstract class DsglScreenHost( private fun syncMouseInputFrame(tree: DomTree, inputEvent: MouseInputEvent) { inspector.onCursorMoved(inputEvent.mouseX, inputEvent.mouseY) - applicationOverlayHost.contextMenuOnFrame( - measureContext = adapter, - viewportWidth = lastWidth, - viewportHeight = lastHeight, - viewportScale = 1f, - ) - applicationOverlayHost.applicationSelectOnFrame( + applicationOverlayHost.syncPortalFrame( measureContext = adapter, viewportWidth = lastWidth, viewportHeight = lastHeight, viewportScale = 1f, + mouseX = inputEvent.mouseX, + mouseY = inputEvent.mouseY, ) - systemOverlayHost.systemSelectOnFrame( + systemOverlayHost.syncPortalFrame( measureContext = adapter, viewportWidth = lastWidth, viewportHeight = lastHeight, @@ -973,12 +948,6 @@ abstract class DsglScreenHost( cursorY = inputEvent.mouseY, inspectorPointerCaptured = inspectorPointerCaptured, ) - applicationOverlayHost.applicationColorPickerOnFrame( - viewportWidth = lastWidth, - viewportHeight = lastHeight, - mouseX = inputEvent.mouseX, - mouseY = inputEvent.mouseY, - ) refreshActiveColorSamplerOwner(tree.root) } @@ -1136,7 +1105,7 @@ abstract class DsglScreenHost( inspectorMouseX: Int, inspectorMouseY: Int, ): Boolean { - if (systemOverlayHost.handleSystemSelectKeyDown(keyCode, keyChar)) { + if (systemOverlayHost.handlePortalKeyDown(keyCode, keyChar)) { return true } if (systemOverlayHost.handleKeyDown(keyCode, keyChar)) { @@ -1156,16 +1125,13 @@ abstract class DsglScreenHost( } private fun consumeApplicationOverlayKeyDown(keyCode: Int, keyChar: Char): Boolean { - if (applicationOverlayHost.handleApplicationColorPickerKeyDown(keyCode, keyChar)) { + if (applicationOverlayHost.handlePortalKeyDownBeforeDom(keyCode, keyChar)) { return true } if (applicationOverlayHost.handleKeyDown(keyCode, keyChar)) { return true } - if (applicationOverlayHost.handleApplicationSelectKeyDown(keyCode, keyChar)) { - return true - } - if (applicationOverlayHost.handleContextMenuKeyDown(keyCode)) { + if (applicationOverlayHost.handlePortalKeyDownAfterDom(keyCode, keyChar)) { return true } return false @@ -1253,7 +1219,7 @@ abstract class DsglScreenHost( mappedButton: MouseButton?, buttonPressed: Boolean, ): Boolean { - if (dWheel != 0 && systemOverlayHost.handleSystemSelectMouseWheel(mouseX, mouseY, dWheel)) { + if (dWheel != 0 && systemOverlayHost.handlePortalMouseWheel(mouseX, mouseY, dWheel)) { return true } if (dWheel != 0 && systemOverlayHost.handleMouseWheel(mouseX, mouseY, dWheel)) { @@ -1262,9 +1228,9 @@ abstract class DsglScreenHost( if (mouseButton != -1 && mappedButton != null) { val consumedBySystemSelect = if (buttonPressed) { - systemOverlayHost.handleSystemSelectMouseDown(mouseX, mouseY, mappedButton) + systemOverlayHost.handlePortalMouseDown(mouseX, mouseY, mappedButton) } else { - systemOverlayHost.handleSystemSelectMouseUp(mouseX, mouseY, mappedButton) + systemOverlayHost.handlePortalMouseUp(mouseX, mouseY, mappedButton) } if (consumedBySystemSelect) { return true @@ -1278,7 +1244,7 @@ abstract class DsglScreenHost( if (consumedBySystemOverlay) { return true } - } else if (mouseButton == -1 && systemOverlayHost.handleSystemSelectMouseMove(mouseX, mouseY)) { + } else if (mouseButton == -1 && systemOverlayHost.handlePortalMouseMove(mouseX, mouseY)) { return true } else if (mouseButton == -1 && systemOverlayHost.handleMouseMove(mouseX, mouseY)) { return true @@ -1303,22 +1269,14 @@ abstract class DsglScreenHost( ): Boolean { val inlineSamplerOwnsSession = activeColorSamplerOwner is ActiveColorSamplerOwner.Inline if (!inlineSamplerOwnsSession) { - if (dWheel != 0 && applicationOverlayHost.handleApplicationColorPickerMouseWheel(mouseX, mouseY, dWheel)) { - return true - } - if (mouseButton != -1 && mappedButton != null) { - val consumedByColorPicker = - if (buttonPressed) { - applicationOverlayHost.handleApplicationColorPickerMouseDown(mouseX, mouseY, mappedButton) - } else { - applicationOverlayHost.handleApplicationColorPickerMouseUp(mouseX, mouseY, mappedButton) - } - if (consumedByColorPicker) { - return true - } - } else if ( - mouseButton == -1 && - applicationOverlayHost.handleApplicationColorPickerMouseMove(mouseX, mouseY) + if ( + applicationOverlayHost.handlePortalPointerBeforeDom( + mouseX = mouseX, + mouseY = mouseY, + dWheel = dWheel, + button = mappedButton, + pressed = buttonPressed, + ) ) { return true } @@ -1341,40 +1299,13 @@ abstract class DsglScreenHost( return true } - if (dWheel != 0 && applicationOverlayHost.handleContextMenuMouseWheel(mouseX, mouseY, dWheel)) { - return true - } - if (dWheel != 0 && applicationOverlayHost.handleApplicationSelectMouseWheel(mouseX, mouseY, dWheel)) { - return true - } - if (mouseButton != -1 && mappedButton != null) { - val consumedByContextMenu = - if (buttonPressed) { - applicationOverlayHost.handleContextMenuMouseDown(mouseX, mouseY, mappedButton) - } else { - applicationOverlayHost.handleContextMenuMouseUp(mouseX, mouseY, mappedButton) - } - if (consumedByContextMenu) { - return true - } - val consumedBySelect = - if (buttonPressed) { - applicationOverlayHost.handleApplicationSelectMouseDown(mouseX, mouseY, mappedButton) - } else { - applicationOverlayHost.handleApplicationSelectMouseUp(mouseX, mouseY, mappedButton) - } - if (consumedBySelect) { - return true - } - return false - } - if (mouseButton == -1 && applicationOverlayHost.handleContextMenuMouseMove(mouseX, mouseY)) { - return true - } - if (mouseButton == -1 && applicationOverlayHost.handleApplicationSelectMouseMove(mouseX, mouseY)) { - return true - } - return false + return applicationOverlayHost.handlePortalPointerAfterDom( + mouseX = mouseX, + mouseY = mouseY, + dWheel = dWheel, + button = mappedButton, + pressed = buttonPressed, + ) } private fun consumeOverlayPointerState(mouseX: Int, mouseY: Int) { @@ -1394,7 +1325,7 @@ abstract class DsglScreenHost( } init { - inspector.installColorPickerHost(systemOverlayHost.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(systemOverlayHost.systemInspectorColorPickerPortalService()) } private fun refreshActiveColorSamplerOwner(root: DOMNode?) { @@ -1408,7 +1339,7 @@ abstract class DsglScreenHost( } activeColorSamplerOwner = colorSamplerOwnershipRouter.update( - popupEyedropperActive = applicationOverlayHost.hasActiveApplicationColorPickerEyedropper(), + popupEyedropperActive = applicationOverlayHost.hasActiveColorPickerEyedropper(), inlineActiveTokens = inlineByToken.keys.toSet(), ) activeInlineColorSamplerNode = @@ -1464,7 +1395,7 @@ abstract class DsglScreenHost( return } when (activeColorSamplerOwner) { - ActiveColorSamplerOwner.Popup -> applicationOverlayHost.captureApplicationColorPickerEyedropperSample() + ActiveColorSamplerOwner.Popup -> applicationOverlayHost.captureColorPickerEyedropperSample() is ActiveColorSamplerOwner.Inline -> { val inline = activeInlineColorSamplerNode if (inline != null && inline.wantsGlobalPointerInput()) { @@ -1473,8 +1404,8 @@ abstract class DsglScreenHost( } ActiveColorSamplerOwner.None -> { - if (applicationOverlayHost.hasActiveApplicationColorPickerEyedropper()) { - applicationOverlayHost.captureApplicationColorPickerEyedropperSample() + if (applicationOverlayHost.hasActiveColorPickerEyedropper()) { + applicationOverlayHost.captureColorPickerEyedropperSample() } } } diff --git a/core/detekt-baseline.xml b/core/detekt-baseline.xml index 7433ad4..aa88a71 100644 --- a/core/detekt-baseline.xml +++ b/core/detekt-baseline.xml @@ -16,7 +16,7 @@ ComplexCondition:InspectorController.kt$InspectorController$endedMode == DragMode.MinimizedMove && clickLike && panelState == InspectorPanelState.Minimized && minimizedBounds.contains(mouseX, mouseY) ComplexCondition:InspectorController.kt$InspectorController$numberText.isEmpty() || numberText == "-" || numberText == "." || numberText == "-." ComplexCondition:LayerDomInputRouter.kt$LayerDomInputRouter$node.onMouseDown != null || node.onMouseUp != null || node.onMouseClick != null || node.onMouseDrag != null || node.onMouseWheel != null || node.onMouseMove != null || node.onKeyDown != null || node.onKeyUp != null - ComplexCondition:ModalRuntime.kt$ModalRuntime$(needsFocusOnTop || (topMost.trapFocus && focusOutsideTop)) && !FocusManager.requestFocusFirstInSubtree(topDialogKey) + ComplexCondition:ModalPortalSessionStore.kt$ModalPortalSessionStore$(needsFocusOnTop || (topMost.trapFocus && focusOutsideTop)) && !FocusManager.requestFocusFirstInSubtree(topDialogKey) ComplexCondition:SelectNode.kt$SelectNode$!controlled && uncontrolledValue == null && value != null && optionExists(value) ComplexCondition:SingleLineInputNode.kt$SingleLineInputNode$!showPlaceholder && focused && !styleDisabled && editState.isCaretVisible(caretBlinkPeriodMs) ComplexCondition:StyleSelector.kt$StyleSelector.Companion$index < token.length && (token[index].isLetterOrDigit() || token[index] == '_' || token[index] == '-') @@ -60,7 +60,7 @@ CyclomaticComplexMethod:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$fun build(context: InspectorStyleEditorSnapshotBuildContext): InspectorStyleEditorSnapshotBuildResult CyclomaticComplexMethod:KeyInput.kt$KeyInput$fun applyShift(ch: Char, shiftDown: Boolean): Char CyclomaticComplexMethod:MinecraftFormattingParser.kt$MinecraftFormattingParser$@Suppress("LoopWithTooManyJumpStatements") private fun parseMinecraft(text: String): ParsedText - CyclomaticComplexMethod:ModalDsl.kt$fun UiScope.modalHost(modals: List<ModalSpec>, modalKey: String = "modal.host", content: UiScope.() -> Unit) + CyclomaticComplexMethod:ModalDsl.kt$fun UiScope.modalPortal(modals: List<ModalSpec>, modalKey: String = "modal.host", content: UiScope.() -> Unit) CyclomaticComplexMethod:MsdfFontMetaParser.kt$MsdfFontMetaParser$fun parse(rawJson: String): MsdfFontMeta CyclomaticComplexMethod:OverlayPanel.kt$OverlayPanel$private fun buildResizedRect(viewportWidth: Int, viewportHeight: Int): Rect CyclomaticComplexMethod:SelectEngine.kt$SelectEngine$fun appendOverlayCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, out: MutableList<RenderCommand>, ) @@ -116,7 +116,7 @@ LongMethod:InspectorController.kt$InspectorController$private fun buildExpandedDomSnapshot( root: DOMNode, viewportWidth: Int, viewportHeight: Int, ): InspectorDomSnapshot LongMethod:InspectorController.kt$InspectorController$private fun literalFromComputed(style: ComputedStyle, property: StyleProperty): String LongMethod:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$fun build(context: InspectorStyleEditorSnapshotBuildContext): InspectorStyleEditorSnapshotBuildResult - LongMethod:ModalDsl.kt$fun UiScope.modalHost(modals: List<ModalSpec>, modalKey: String = "modal.host", content: UiScope.() -> Unit) + LongMethod:ModalDsl.kt$fun UiScope.modalPortal(modals: List<ModalSpec>, modalKey: String = "modal.host", content: UiScope.() -> Unit) LongMethod:MsdfFontMetaParser.kt$MsdfFontMetaParser$fun parse(rawJson: String): MsdfFontMeta LongMethod:OverlayPanel.kt$OverlayPanel$private fun buildResizedRect(viewportWidth: Int, viewportHeight: Int): Rect LongMethod:PositionedLayoutStickyBehaviorTests.kt$PositionedLayoutStickyBehaviorTests$@Test fun `non-sticky positioned modes remain unchanged with sticky enabled`() diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt index ec062ac..360932b 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt @@ -646,7 +646,7 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { } class ColorPickerPopupManager( - private val host: ColorPickerPopupHost = ColorPickerRuntime.host, + private val host: ColorPickerPopupHost = ColorPickerPortalServices.engine, private val ownerToken: Any = Any(), ) { fun open( @@ -689,7 +689,6 @@ class ColorPickerPopupManager( fun isOpen(): Boolean = host.isOpenFor(ownerToken) } -object ColorPickerRuntime { +object ColorPickerPortalServices { val engine: ColorPickerPopupEngine = ColorPickerPopupEngine() - val host: ColorPickerPopupHost = engine } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPortalController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPortalController.kt index 8a3736d..8bf9c51 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPortalController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPortalController.kt @@ -16,11 +16,12 @@ import org.dreamfinity.dsgl.core.overlay.PortalEntryState import org.dreamfinity.dsgl.core.overlay.PortalFocusPolicy import org.dreamfinity.dsgl.core.overlay.PortalHost import org.dreamfinity.dsgl.core.overlay.PortalInputPolicy +import org.dreamfinity.dsgl.core.overlay.PortalPointerDispatch import org.dreamfinity.dsgl.core.render.RenderCommand internal class ColorPickerPortalController( private val engine: ColorPickerPopupEngine, -) { +) : PortalPointerDispatch { private val portalHost: PortalHost = PortalHost(OverlayLayerContracts.portalSurfaceForOwner(OverlayOwnerScope.Application)) private val entry: ColorPickerPortalEntry = ColorPickerPortalEntry(engine) @@ -65,16 +66,16 @@ internal class ColorPickerPortalController( } } - fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = + override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = portalHost.dispatchInput { it.handleMouseMove(mouseX, mouseY) } - fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = portalHost.dispatchInput { it.handleMouseDown(mouseX, mouseY, button) } - fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = portalHost.dispatchInput { it.handleMouseUp(mouseX, mouseY, button) } - fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = + override fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = portalHost.dispatchInput { it.handleMouseWheel(mouseX, mouseY, delta) } fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt index efaf0c1..fa9ab11 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt @@ -1,8 +1,8 @@ package org.dreamfinity.dsgl.core.components.modal -import org.dreamfinity.dsgl.core.components.modal.internal.ModalHostNode +import org.dreamfinity.dsgl.core.components.modal.internal.ModalPortalAnchorNode import org.dreamfinity.dsgl.core.components.modal.internal.ModalPortalRootNode -import org.dreamfinity.dsgl.core.components.modal.internal.ModalRuntime +import org.dreamfinity.dsgl.core.components.modal.internal.ModalPortalSessionStore import org.dreamfinity.dsgl.core.components.modal.internal.modalLifecycleKey import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.elements.InputType @@ -15,13 +15,13 @@ import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection import org.dreamfinity.dsgl.core.style.JustifyContent -fun UiScope.modalHost(modals: List, modalKey: String = "modal.host", content: UiScope.() -> Unit) { - ModalRuntime.onBuild(modalKey, modals) - val hostNode = mount(ModalHostNode(modalKey)) +fun UiScope.modalPortal(modals: List, key: String = "modal.portal", content: UiScope.() -> Unit) { + ModalPortalSessionStore.onBuild(key, modals) + val hostNode = mount(ModalPortalAnchorNode(key)) hostNode.onKeyDown = { event -> val topMost = modals.lastOrNull() if (topMost != null) { - val topDialogKey = ModalRuntime.dialogKey(modalKey, topMost.key) + val topDialogKey = ModalPortalSessionStore.dialogKey(key, topMost.key) val focusInsideTop = FocusManager.isFocusWithinSubtree(topDialogKey) if (event.keyCode == KeyCodes.ESCAPE) { if (topMost.keyboard) { @@ -38,16 +38,16 @@ fun UiScope.modalHost(modals: List, modalKey: String = "modal.host", } val hostScope = childScope(hostNode) - hostScope.div({ key = "$modalKey.content" }) { + hostScope.div({ this.key = "$key.content" }) { content() } - val portalRoot = ModalPortalRootNode("$modalKey.portal") + val portalRoot = ModalPortalRootNode("$key.portal") val portalScope = childScope(portalRoot) - hostNode.refTarget = ModalRuntime.portalHostRef(modalKey) - ModalRuntime.registerPortalTemplate(modalKey, portalRoot) + hostNode.refTarget = ModalPortalSessionStore.portalHostRef(key) + ModalPortalSessionStore.registerPortalTemplate(key, portalRoot) portalRoot.onKeyDown = hostNode.onKeyDown - buildModalLayers(portalScope, modals, modalKey) + buildModalLayers(portalScope, modals, key) } private fun buildModalLayers(hostScope: UiScope, modals: List, modalKey: String) { @@ -64,7 +64,7 @@ private fun buildModalLayers(hostScope: UiScope, modals: List, modalK ref = RefTarget { handle -> if (handle != null) { - ModalRuntime.onCommit(modalKey, modals) + ModalPortalSessionStore.onCommit(modalKey, modals) } } style = { @@ -76,7 +76,7 @@ private fun buildModalLayers(hostScope: UiScope, modals: List, modalK } private fun UiScope.modalLayer(spec: ModalSpec, modalKey: String, isTopMost: Boolean) { - val dialogKey = ModalRuntime.dialogKey(modalKey, spec.key) + val dialogKey = ModalPortalSessionStore.dialogKey(modalKey, spec.key) div({ key = "$modalKey.modal.${spec.key}.layer" onMouseDown = { event -> diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt index ddfebe6..6c63c38 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt @@ -21,24 +21,24 @@ import org.dreamfinity.dsgl.core.overlay.PortalInputPolicy internal class ModalPortalController { private val portalHost: PortalHost = PortalHost(OverlayLayerContracts.portalSurfaceForOwner(OverlayOwnerScope.Application)) - private val entriesByHostKey: LinkedHashMap = LinkedHashMap() + private val entriesByPortalKey: LinkedHashMap = LinkedHashMap() fun sync(rootNode: DOMNode, viewportWidth: Int, viewportHeight: Int) { - val snapshots = ModalRuntime.portalSnapshots() - val activeHostKeys = snapshots.mapTo(LinkedHashSet()) { it.hostKey } + val snapshots = ModalPortalSessionStore.portalSnapshots() + val activePortalKeys = snapshots.mapTo(LinkedHashSet()) { it.portalKey } snapshots.forEach { snapshot -> val entry = - entriesByHostKey.getOrPut(snapshot.hostKey) { - ModalPortalEntry(snapshot.hostKey, snapshot.root).also(portalHost::register) + entriesByPortalKey.getOrPut(snapshot.portalKey) { + ModalPortalEntry(snapshot.portalKey, snapshot.root).also(portalHost::register) } entry.reconcile(snapshot.root) entry.syncActive(viewportWidth, viewportHeight) } - entriesByHostKey + entriesByPortalKey .keys - .filter { it !in activeHostKeys } - .forEach { hostKey -> - val entry = entriesByHostKey.remove(hostKey) ?: return@forEach + .filter { it !in activePortalKeys } + .forEach { portalKey -> + val entry = entriesByPortalKey.remove(portalKey) ?: return@forEach portalHost.unregister(entry.state.id) entry.detach() } @@ -46,20 +46,22 @@ internal class ModalPortalController { } fun close() { - entriesByHostKey.values.forEach { entry -> + entriesByPortalKey.values.forEach { entry -> portalHost.unregister(entry.state.id) entry.detach() } - entriesByHostKey.clear() + entriesByPortalKey.clear() } fun commitActivePortals() { portalHost .entriesInPaintOrder() .mapNotNull { it as? ModalPortalEntry } - .forEach { entry -> ModalRuntime.commitPortal(entry.hostKey, entry.root) } + .forEach { entry -> ModalPortalSessionStore.commitPortal(entry.portalKey, entry.root) } } + fun hasActivePortal(): Boolean = entriesByPortalKey.values.any { entry -> entry.state.active } + internal fun debugActivePortalEntryIds(): List = portalHost.entriesInPaintOrder().map { it.state.id.value } internal fun debugFindNodeByKey(key: Any?): DOMNode? = debugFindNode { node -> node.key == key } @@ -75,7 +77,7 @@ internal class ModalPortalController { portalHost .entriesInPaintOrder() .mapNotNull { (it as? ModalPortalEntry)?.root } - entriesByHostKey.values.forEach { entry -> + entriesByPortalKey.values.forEach { entry -> if (entry.root !in activeRoots) { entry.detach() } @@ -105,7 +107,7 @@ internal class ModalPortalController { } private class ModalPortalEntry( - val hostKey: String, + val portalKey: String, templateRoot: ModalPortalRootNode, ) : PortalEntry { private var tree: DomTree = DomTree(templateRoot) @@ -115,8 +117,8 @@ private class ModalPortalEntry( override val state: PortalEntryState = PortalEntryState( - id = PortalEntryId("application.modal.$hostKey"), - ownerToken = hostKey, + id = PortalEntryId("application.modal.$portalKey"), + ownerToken = portalKey, surface = OverlayLayerContracts.portalSurfaceForOwner(OverlayOwnerScope.Application), order = PortalEntryOrder(zIndex = -100), dismissPolicy = PortalDismissPolicy.None, @@ -144,7 +146,7 @@ private class ModalPortalEntry( } fun syncActive(viewportWidth: Int, viewportHeight: Int) { - if (!ModalRuntime.shouldKeepPortalActive(hostKey)) { + if (!ModalPortalSessionStore.shouldKeepPortalActive(portalKey)) { state.deactivate() return } @@ -168,7 +170,7 @@ private class ModalPortalEntry( } override fun close() { - ModalRuntime.forgetPortal(hostKey) + ModalPortalSessionStore.forgetPortal(portalKey) state.deactivate() } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalHostNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalNode.kt similarity index 94% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalHostNode.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalNode.kt index 8ef33a2..913df97 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalHostNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalNode.kt @@ -8,13 +8,13 @@ import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.StyleEngine /** - * Root node for modal host composition: + * Root node for modal portal composition: * child[0] is regular content and children[1..] are full-viewport modal layers. */ -internal class ModalHostNode( +internal class ModalPortalAnchorNode( key: Any?, ) : DOMNode(key) { - override val styleType: String = "modal-host" + override val styleType: String = "modal-portal" override fun measure(ctx: UiMeasureContext): Size { val content = children.firstOrNull() @@ -72,7 +72,7 @@ internal class ModalHostNode( } /** - * Application-portal root for modal layers. The regular modal host content stays + * Application-portal root for modal layers. The regular modal portal content stays * in the application root; modal layers render through this full-viewport root. */ internal class ModalPortalRootNode( diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalRuntime.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalSessionStore.kt similarity index 75% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalRuntime.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalSessionStore.kt index 9fee32d..aa4822a 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalRuntime.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalSessionStore.kt @@ -8,13 +8,13 @@ import org.dreamfinity.dsgl.core.hooks.ref.ElementHandle import org.dreamfinity.dsgl.core.hooks.ref.RefTarget import java.util.concurrent.ConcurrentHashMap -internal fun modalLifecycleKey(hostKey: String): String = "$hostKey.modal.lifecycle" +internal fun modalLifecycleKey(portalKey: String): String = "$portalKey.modal.lifecycle" private data class ModalMeta( val restoreFocus: Boolean, ) -private class ModalHostState { +private class ModalPortalState { var previousKeys: List = emptyList() var previousMetaByKey: Map = emptyMap() val restoreFocusByModalKey: MutableMap = linkedMapOf() @@ -23,18 +23,18 @@ private class ModalHostState { var currentModals: List = emptyList() } -internal object ModalRuntime { +internal object ModalPortalSessionStore { data class PortalSnapshot( - val hostKey: String, + val portalKey: String, val root: ModalPortalRootNode, ) - private val states: MutableMap = ConcurrentHashMap() + private val states: MutableMap = ConcurrentHashMap() private val portalTemplates: MutableMap = ConcurrentHashMap() private val portalHostRefs: MutableMap> = ConcurrentHashMap() - fun onBuild(hostKey: String, modals: List) { - val state = states.getOrPut(hostKey) { ModalHostState() } + fun onBuild(portalKey: String, modals: List) { + val state = states.getOrPut(portalKey) { ModalPortalState() } val currentKeys = modals.map { it.key } val previousKeys = state.previousKeys @@ -63,7 +63,7 @@ internal object ModalRuntime { val previousTop = previousKeys.lastOrNull() val currentTop = currentKeys.lastOrNull() if (currentTop != null && currentTop != previousTop) { - state.pendingFocusDialogKey = dialogKey(hostKey, currentTop) + state.pendingFocusDialogKey = dialogKey(portalKey, currentTop) } if (currentTop == null) { state.pendingFocusDialogKey = null @@ -77,68 +77,68 @@ internal object ModalRuntime { } } - fun onCommit(hostKey: String, modals: List, focusRoot: DOMNode? = null) { - val state = states[hostKey] ?: return + fun onCommit(portalKey: String, modals: List, focusRoot: DOMNode? = null) { + val state = states[portalKey] ?: return val topMost = modals.lastOrNull() if (topMost == null) { if (commitWithoutActiveModal(state, focusRoot)) { - states.remove(hostKey) + states.remove(portalKey) } return } - commitWithActiveModal(hostKey, state, topMost, focusRoot) + commitWithActiveModal(portalKey, state, topMost, focusRoot) } - fun registerPortalTemplate(hostKey: String, root: ModalPortalRootNode) { - val previous = portalTemplates.put(hostKey, root) + fun registerPortalTemplate(portalKey: String, root: ModalPortalRootNode) { + val previous = portalTemplates.put(portalKey, root) if (previous != null && previous !== root && previous.parent == null) { clearTemplateOwnedListeners(previous) } } - fun portalHostRef(hostKey: String): RefTarget = - portalHostRefs.getOrPut(hostKey) { + fun portalHostRef(portalKey: String): RefTarget = + portalHostRefs.getOrPut(portalKey) { RefTarget { handle -> if (handle == null) { - forgetPortal(hostKey) + forgetPortal(portalKey) } } } - fun commitPortal(hostKey: String, focusRoot: DOMNode) { - val state = states[hostKey] ?: return - onCommit(hostKey, state.currentModals, focusRoot) + fun commitPortal(portalKey: String, focusRoot: DOMNode) { + val state = states[portalKey] ?: return + onCommit(portalKey, state.currentModals, focusRoot) } fun portalSnapshots(): List = portalTemplates .entries .sortedBy { it.key } - .map { (hostKey, root) -> + .map { (portalKey, root) -> PortalSnapshot( - hostKey = hostKey, + portalKey = portalKey, root = root, ) } - fun shouldKeepPortalActive(hostKey: String): Boolean { - val template = portalTemplates[hostKey] ?: return false - val state = states[hostKey] - return template.children.any { it.key != modalLifecycleKey(hostKey) } || + fun shouldKeepPortalActive(portalKey: String): Boolean { + val template = portalTemplates[portalKey] ?: return false + val state = states[portalKey] + return template.children.any { it.key != modalLifecycleKey(portalKey) } || state?.previousKeys?.isNotEmpty() == true || state?.pendingRestoreFocusKey != null || state?.pendingFocusDialogKey != null } - fun forgetPortal(hostKey: String) { - portalTemplates.remove(hostKey) - portalHostRefs.remove(hostKey) - states.remove(hostKey) + fun forgetPortal(portalKey: String) { + portalTemplates.remove(portalKey) + portalHostRefs.remove(portalKey) + states.remove(portalKey) } - fun dialogKey(hostKey: String, modalKey: String): String = "$hostKey.modal.$modalKey.dialog" + fun dialogKey(portalKey: String, modalKey: String): String = "$portalKey.modal.$modalKey.dialog" } private fun clearTemplateOwnedListeners(root: DOMNode) { @@ -166,7 +166,7 @@ private fun isOwnedByTemplateRoot(root: DOMNode, node: DOMNode): Boolean { return false } -private fun commitWithoutActiveModal(state: ModalHostState, focusRoot: DOMNode?): Boolean { +private fun commitWithoutActiveModal(state: ModalPortalState, focusRoot: DOMNode?): Boolean { val restoreKey = state.pendingRestoreFocusKey if (restoreKey != null) { val restored = requestFocusByKey(restoreKey, focusRoot) @@ -181,14 +181,14 @@ private fun commitWithoutActiveModal(state: ModalHostState, focusRoot: DOMNode?) } private fun commitWithActiveModal( - hostKey: String, - state: ModalHostState, + portalKey: String, + state: ModalPortalState, topMost: ModalSpec, focusRoot: DOMNode?, ) { restorePendingFocus(state, focusRoot) - val topDialogKey = ModalRuntime.dialogKey(hostKey, topMost.key) + val topDialogKey = ModalPortalSessionStore.dialogKey(portalKey, topMost.key) val needsFocusOnTop = state.pendingFocusDialogKey == topDialogKey val focusOutsideTop = !FocusManager.isFocusWithinSubtree(topDialogKey) val shouldFocusTop = needsFocusOnTop || (topMost.trapFocus && focusOutsideTop) @@ -198,7 +198,7 @@ private fun commitWithActiveModal( } } -private fun restorePendingFocus(state: ModalHostState, focusRoot: DOMNode?) { +private fun restorePendingFocus(state: ModalPortalState, focusRoot: DOMNode?) { val restoreKey = state.pendingRestoreFocusKey ?: return val restored = requestFocusByKey(restoreKey, focusRoot) if (restored || focusRoot != null) { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuRuntime.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuPortalServices.kt similarity index 61% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuRuntime.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuPortalServices.kt index 8ce79de..65e044b 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuRuntime.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuPortalServices.kt @@ -1,6 +1,5 @@ package org.dreamfinity.dsgl.core.contextmenu -object ContextMenuRuntime { +object ContextMenuPortalServices { val engine: ContextMenuEngine = ContextMenuEngine() - val host: ContextMenuHost = engine } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEvents.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEvents.kt index 4d6722f..e11b2d1 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEvents.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEvents.kt @@ -2,12 +2,12 @@ package org.dreamfinity.dsgl.core.dom import org.dreamfinity.dsgl.core.contextmenu.ContextMenuHost import org.dreamfinity.dsgl.core.contextmenu.ContextMenuModel -import org.dreamfinity.dsgl.core.contextmenu.ContextMenuRuntime +import org.dreamfinity.dsgl.core.contextmenu.ContextMenuPortalServices import org.dreamfinity.dsgl.core.contextmenu.ContextMenuTriggerScope import org.dreamfinity.dsgl.core.event.MouseButton fun DOMNode.onContextMenu( - host: ContextMenuHost = ContextMenuRuntime.host, + host: ContextMenuHost = ContextMenuPortalServices.engine, handler: ContextMenuTriggerScope.() -> Unit, ) { val previous = onMouseDown @@ -33,7 +33,7 @@ fun DOMNode.onContextMenu( } fun DOMNode.onContextMenuModel( - host: ContextMenuHost = ContextMenuRuntime.host, + host: ContextMenuHost = ContextMenuPortalServices.engine, modelProvider: () -> ContextMenuModel, ) { onContextMenu(host = host, handler = { openMenu(modelProvider()) }) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerPopupPaneNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerPopupPaneNode.kt index 48d396a..6d12db5 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerPopupPaneNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerPopupPaneNode.kt @@ -58,8 +58,8 @@ class ColorPickerPopupPaneNode( return@addEventListener } FocusManager.requestFocus(this@ColorPickerPopupPaneNode) - if (ColorPickerRuntime.host.isOpenFor(ownerToken)) { - ColorPickerRuntime.host.close(ownerToken) + if (ColorPickerPortalServices.engine.isOpenFor(ownerToken)) { + ColorPickerPortalServices.engine.close(ownerToken) } else { openPopup() } @@ -125,7 +125,7 @@ class ColorPickerPopupPaneNode( color = textColor, ) out += - if (ColorPickerRuntime.host.isOpenFor(ownerToken)) { + if (ColorPickerPortalServices.engine.isOpenFor(ownerToken)) { drawTextCommand( ctx, text = "^", @@ -183,13 +183,13 @@ class ColorPickerPopupPaneNode( } private fun syncPopupIfOpen() { - if (!ColorPickerRuntime.host.isOpenFor(ownerToken)) return - ColorPickerRuntime.engine.sync(openRequest()) + if (!ColorPickerPortalServices.engine.isOpenFor(ownerToken)) return + ColorPickerPortalServices.engine.sync(openRequest()) setOpenState(true) } private fun openPopup() { - ColorPickerRuntime.host.open(openRequest()) + ColorPickerPortalServices.engine.open(openRequest()) setOpenState(true) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt index 206a3c9..775a018 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt @@ -12,7 +12,7 @@ import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.select.SelectEntry import org.dreamfinity.dsgl.core.select.SelectModel import org.dreamfinity.dsgl.core.select.SelectOpenRequest -import org.dreamfinity.dsgl.core.select.SelectRuntime +import org.dreamfinity.dsgl.core.select.SelectPortalServices class SelectNode( model: SelectModel, @@ -65,7 +65,7 @@ class SelectNode( var disabledTextColor: Int = 0xFF8E8E8E.toInt() var minContentWidth: Int = 92 var arrowGlyph: String = - SelectRuntime.engine + SelectPortalServices.engine .currentStyle() .arrowGlyph var arrowSpacing: Int = 8 @@ -81,8 +81,8 @@ class SelectNode( if (event.mouseButton != MouseButton.LEFT) return@addEventListener if (!this@SelectNode.containsGlobalPoint(event.mouseX, event.mouseY)) return@addEventListener FocusManager.requestFocus(this@SelectNode) - if (SelectRuntime.host.isOpenFor(ownerToken)) { - SelectRuntime.host.close(ownerToken) + if (SelectPortalServices.isOpenFor(ownerToken)) { + SelectPortalServices.close(ownerToken) } else { openPopup() } @@ -91,7 +91,7 @@ class SelectNode( this@SelectNode.addEventListener(Events.KEYDOWN) { event: KeyboardKeyDownEvent -> if (this@SelectNode.styleDisabled) return@addEventListener if (!FocusManager.isFocused(this@SelectNode)) return@addEventListener - if (SelectRuntime.host.isOpenFor(ownerToken)) return@addEventListener + if (SelectPortalServices.isOpenFor(ownerToken)) return@addEventListener when (event.keyCode) { KeyCodes.ENTER, KeyCodes.SPACE -> { openPopup() @@ -100,20 +100,20 @@ class SelectNode( KeyCodes.DOWN -> { openPopup() - SelectRuntime.engineFor(ownerScope).moveHighlight(ownerToken, 1) + SelectPortalServices.engineFor(ownerScope).moveHighlight(ownerToken, 1) event.cancelled = true } KeyCodes.UP -> { openPopup() - SelectRuntime.engineFor(ownerScope).moveHighlight(ownerToken, -1) + SelectPortalServices.engineFor(ownerScope).moveHighlight(ownerToken, -1) event.cancelled = true } } } this@SelectNode.addEventListener(Events.BLUR) { _: FocusLoseEvent -> - if (SelectRuntime.host.isOpenFor(ownerToken)) { - SelectRuntime.host.close(ownerToken) + if (SelectPortalServices.isOpenFor(ownerToken)) { + SelectPortalServices.close(ownerToken) } } } @@ -150,8 +150,8 @@ class SelectNode( } override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) { - if (styleDisabled && SelectRuntime.host.isOpenFor(ownerToken)) { - SelectRuntime.host.close(ownerToken) + if (styleDisabled && SelectPortalServices.isOpenFor(ownerToken)) { + SelectPortalServices.close(ownerToken) } syncPopup() val isFocused = FocusManager.isFocused(this) && !styleDisabled @@ -206,7 +206,7 @@ class SelectNode( var hash = 1L hash = 31L * hash + selectedLabelOrPlaceholder().hashCode() hash = 31L * hash + (selectedOptionId()?.hashCode() ?: 0) - hash = 31L * hash + if (SelectRuntime.host.isOpenFor(ownerToken)) 1L else 0L + hash = 31L * hash + if (SelectPortalServices.isOpenFor(ownerToken)) 1L else 0L return hash } @@ -246,15 +246,15 @@ class SelectNode( private fun openPopup() { if (!hasEnabledOption()) return - SelectRuntime.host.open(openRequest()) + SelectPortalServices.open(openRequest()) setOpenState(true) } private fun syncPopup() { - val open = SelectRuntime.host.isOpenFor(ownerToken) + val open = SelectPortalServices.isOpenFor(ownerToken) setOpenState(open) if (open) { - SelectRuntime.engineFor(ownerScope).sync(openRequest()) + SelectPortalServices.engineFor(ownerScope).sync(openRequest()) } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt index 99e0753..c9ab077 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.inspector.internal +package org.dreamfinity.dsgl.core.inspector.internal import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.ScrollSessionSnapshot @@ -15,7 +15,7 @@ import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanel import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelDragSession import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelState -import org.dreamfinity.dsgl.core.select.SelectRuntime +import org.dreamfinity.dsgl.core.select.SelectPortalServices import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.Overflow import org.dreamfinity.dsgl.core.style.TextWrap @@ -1034,7 +1034,7 @@ internal class SystemInspectorOverlayNode( hovered: Boolean, onSelected: (String) -> Unit, ): DOMNode { - val open = SelectRuntime.host.isOpenFor(key) + val open = SelectPortalServices.isOpenFor(key) val selectNode = scope.select( props = { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationColorPickerPortalExtensions.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationColorPickerPortalExtensions.kt deleted file mode 100644 index 2c07964..0000000 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationColorPickerPortalExtensions.kt +++ /dev/null @@ -1,63 +0,0 @@ -package org.dreamfinity.dsgl.core.overlay - -import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext -import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.render.RenderCommand - -fun ApplicationOverlayHost.applicationColorPickerOnFrame( - viewportWidth: Int, - viewportHeight: Int, - mouseX: Int, - mouseY: Int, -) { - applicationColorPickerPortal.onFrame( - viewportWidth = viewportWidth, - viewportHeight = viewportHeight, - mouseX = mouseX, - mouseY = mouseY, - ) -} - -fun ApplicationOverlayHost.appendApplicationColorPickerOverlayCommands( - measureContext: UiMeasureContext, - viewportWidth: Int, - viewportHeight: Int, - out: MutableList, -) { - applicationColorPickerPortal.appendCommands( - measureContext = measureContext, - viewportWidth = viewportWidth, - viewportHeight = viewportHeight, - out = out, - ) -} - -fun ApplicationOverlayHost.isApplicationColorPickerOpen(): Boolean = applicationColorPickerPortal.isOpen - -fun ApplicationOverlayHost.hasActiveApplicationColorPickerEyedropper(): Boolean = - applicationColorPickerPortal.hasActiveEyedropper - -fun ApplicationOverlayHost.captureApplicationColorPickerEyedropperSample() { - applicationColorPickerPortal.captureEyedropperSample() -} - -fun ApplicationOverlayHost.handleApplicationColorPickerKeyDown(keyCode: Int, keyChar: Char): Boolean = - applicationColorPickerPortal.handleKeyDown(keyCode, keyChar) - -fun ApplicationOverlayHost.handleApplicationColorPickerMouseMove(mouseX: Int, mouseY: Int): Boolean = - applicationColorPickerPortal.handleMouseMove(mouseX, mouseY) - -fun ApplicationOverlayHost.handleApplicationColorPickerMouseDown( - mouseX: Int, - mouseY: Int, - button: MouseButton, -): Boolean = applicationColorPickerPortal.handleMouseDown(mouseX, mouseY, button) - -fun ApplicationOverlayHost.handleApplicationColorPickerMouseUp( - mouseX: Int, - mouseY: Int, - button: MouseButton, -): Boolean = applicationColorPickerPortal.handleMouseUp(mouseX, mouseY, button) - -fun ApplicationOverlayHost.handleApplicationColorPickerMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = - applicationColorPickerPortal.handleMouseWheel(mouseX, mouseY, delta) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt index 238139f..1c7cedb 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt @@ -1,25 +1,31 @@ package org.dreamfinity.dsgl.core.overlay import org.dreamfinity.dsgl.core.DomTree +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupEngine import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPortalController -import org.dreamfinity.dsgl.core.colorpicker.ColorPickerRuntime +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPortalServices import org.dreamfinity.dsgl.core.components.modal.internal.ModalPortalController import org.dreamfinity.dsgl.core.contextmenu.ContextMenuEngine -import org.dreamfinity.dsgl.core.contextmenu.ContextMenuRuntime +import org.dreamfinity.dsgl.core.contextmenu.ContextMenuPortalServices import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand +import org.dreamfinity.dsgl.core.select.SelectEngine import org.dreamfinity.dsgl.core.select.SelectPortalController -import org.dreamfinity.dsgl.core.select.SelectRuntime +import org.dreamfinity.dsgl.core.select.SelectPortalServices import org.dreamfinity.dsgl.core.style.StyleApplicationScope -class ApplicationOverlayHost : OverlayLayerHost { +class ApplicationOverlayHost( + contextMenuEngine: ContextMenuEngine = ContextMenuPortalServices.engine, + selectEngine: SelectEngine = SelectPortalServices.applicationEngine, + colorPickerEngine: ColorPickerPopupEngine = ColorPickerPortalServices.engine, +) : OverlayLayerHost { override val layerId: UiLayerId = UiLayerId.ApplicationOverlay - private val rootNode: ApplicationOverlayRootNode = ApplicationOverlayRootNode() + internal val rootNode: ApplicationOverlayRootNode = ApplicationOverlayRootNode() private val tree: DomTree = DomTree( root = rootNode, @@ -30,16 +36,17 @@ class ApplicationOverlayHost : OverlayLayerHost { rootProvider = { rootNode }, ) internal val contextMenuPortal: ContextMenuPortalController = - ContextMenuPortalController(ContextMenuRuntime.engine) + ContextMenuPortalController(contextMenuEngine) internal val applicationSelectPortal: SelectPortalController = SelectPortalController( - engine = SelectRuntime.applicationEngine, + engine = selectEngine, ownerScope = OverlayOwnerScope.Application, entryId = "application.select", ) internal val applicationColorPickerPortal: ColorPickerPortalController = - ColorPickerPortalController(ColorPickerRuntime.engine) + ColorPickerPortalController(colorPickerEngine) internal val modalPortal: ModalPortalController = ModalPortalController() + private var modalPortalWasActive: Boolean = false override fun onInputFrame(viewportWidth: Int, viewportHeight: Int) { rootNode.setViewportBounds( @@ -51,6 +58,7 @@ class ApplicationOverlayHost : OverlayLayerHost { override fun render(ctx: UiMeasureContext, width: Int, height: Int) { rootNode.setViewportBounds(width, height) modalPortal.sync(rootNode, width, height) + closeStaleFloatingPortalsAfterModalOpen() tree.render(ctx, width, height) modalPortal.commitActivePortals() } @@ -77,14 +85,119 @@ class ApplicationOverlayHost : OverlayLayerHost { applicationSelectPortal.close() applicationColorPickerPortal.close() modalPortal.close() + modalPortalWasActive = false } - internal fun debugRootBounds(): Rect = rootNode.bounds + private fun closeStaleFloatingPortalsAfterModalOpen() { + val modalActive = modalPortal.hasActivePortal() + if (modalActive && !modalPortalWasActive) { + closeFloatingPortals() + } + modalPortalWasActive = modalActive + } +} + +internal fun ApplicationOverlayHost.debugRootBounds(): Rect = rootNode.bounds + +fun ApplicationOverlayHost.syncPortalFrame( + measureContext: UiMeasureContext, + viewportWidth: Int, + viewportHeight: Int, + viewportScale: Float, + mouseX: Int, + mouseY: Int, +) { + contextMenuPortal.onFrame(measureContext, viewportWidth, viewportHeight, viewportScale) + applicationSelectPortal.onFrame(measureContext, viewportWidth, viewportHeight, viewportScale) + applicationColorPickerPortal.onFrame(viewportWidth, viewportHeight, mouseX, mouseY) +} + +fun ApplicationOverlayHost.appendPortalOverlayCommands( + measureContext: UiMeasureContext, + viewportWidth: Int, + viewportHeight: Int, + out: MutableList, +) { + applicationSelectPortal.appendCommands(measureContext, viewportWidth, viewportHeight, out) + contextMenuPortal.appendCommands(measureContext, viewportWidth, viewportHeight, out) + applicationColorPickerPortal.appendCommands(measureContext, viewportWidth, viewportHeight, out) +} + +fun ApplicationOverlayHost.closeFloatingPortals() { + contextMenuPortal.close() + applicationSelectPortal.close() + applicationColorPickerPortal.close() +} + +fun ApplicationOverlayHost.hasOpenContextMenuPortal(): Boolean = contextMenuPortal.isOpen() + +fun ApplicationOverlayHost.hasOpenSelectPortal(): Boolean = applicationSelectPortal.isOpen() + +fun ApplicationOverlayHost.hasOpenColorPickerPortal(): Boolean = applicationColorPickerPortal.isOpen + +fun ApplicationOverlayHost.hasActiveColorPickerEyedropper(): Boolean = applicationColorPickerPortal.hasActiveEyedropper + +fun ApplicationOverlayHost.captureColorPickerEyedropperSample() { + applicationColorPickerPortal.captureEyedropperSample() +} + +fun ApplicationOverlayHost.handlePortalKeyDownBeforeDom(keyCode: Int, keyChar: Char): Boolean = + applicationColorPickerPortal.handleKeyDown(keyCode, keyChar) + +fun ApplicationOverlayHost.handlePortalKeyDownAfterDom(keyCode: Int, keyChar: Char): Boolean = + applicationSelectPortal.handleKeyDown(keyCode, keyChar) || + contextMenuPortal.handleKeyDown(keyCode) + +fun ApplicationOverlayHost.handlePortalPointerBeforeDom( + mouseX: Int, + mouseY: Int, + dWheel: Int, + button: MouseButton?, + pressed: Boolean, +): Boolean = handlePortalPointer(applicationColorPickerPortal, mouseX, mouseY, dWheel, button, pressed) + +fun ApplicationOverlayHost.handlePortalPointerAfterDom( + mouseX: Int, + mouseY: Int, + dWheel: Int, + button: MouseButton?, + pressed: Boolean, +): Boolean = + handlePortalPointer(contextMenuPortal, mouseX, mouseY, dWheel, button, pressed) || + handlePortalPointer(applicationSelectPortal, mouseX, mouseY, dWheel, button, pressed) + +private fun handlePortalPointer( + portal: PortalPointerDispatch, + mouseX: Int, + mouseY: Int, + dWheel: Int, + button: MouseButton?, + pressed: Boolean, +): Boolean { + if (dWheel != 0 && portal.handleMouseWheel(mouseX, mouseY, dWheel)) return true + if (button != null) { + return if (pressed) { + portal.handleMouseDown(mouseX, mouseY, button) + } else { + portal.handleMouseUp(mouseX, mouseY, button) + } + } + return portal.handleMouseMove(mouseX, mouseY) +} + +internal interface PortalPointerDispatch { + fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean + + fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean + + fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean + + fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean } internal class ContextMenuPortalController( private val engine: ContextMenuEngine, -) { +) : PortalPointerDispatch { private val portalHost: PortalHost = PortalHost(OverlayLayerContracts.portalSurfaceForOwner(OverlayOwnerScope.Application)) private val entry: ContextMenuPortalEntry = ContextMenuPortalEntry(engine) @@ -118,16 +231,16 @@ internal class ContextMenuPortalController( fun isOpen(): Boolean = engine.isOpen() - fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = + override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = portalHost.dispatchInput { it.handleMouseMove(mouseX, mouseY) } - fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = portalHost.dispatchInput { it.handleMouseDown(mouseX, mouseY, button) } - fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = portalHost.dispatchInput { it.handleMouseUp(mouseX, mouseY, button) } - fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = + override fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = portalHost.dispatchInput { it.handleMouseWheel(mouseX, mouseY, delta) } fun handleKeyDown(keyCode: Int): Boolean = @@ -229,96 +342,3 @@ private class ContextMenuPortalEntry( ) } } - -fun ApplicationOverlayHost.contextMenuOnFrame( - measureContext: UiMeasureContext, - viewportWidth: Int, - viewportHeight: Int, - viewportScale: Float, -) { - contextMenuPortal.onFrame( - measureContext = measureContext, - viewportWidth = viewportWidth, - viewportHeight = viewportHeight, - viewportScale = viewportScale, - ) -} - -fun ApplicationOverlayHost.appendContextMenuOverlayCommands( - measureContext: UiMeasureContext, - viewportWidth: Int, - viewportHeight: Int, - out: MutableList, -) { - contextMenuPortal.appendCommands( - measureContext = measureContext, - viewportWidth = viewportWidth, - viewportHeight = viewportHeight, - out = out, - ) -} - -fun ApplicationOverlayHost.closeContextMenus() { - contextMenuPortal.close() -} - -fun ApplicationOverlayHost.isContextMenuOpen(): Boolean = contextMenuPortal.isOpen() - -fun ApplicationOverlayHost.handleContextMenuMouseMove(mouseX: Int, mouseY: Int): Boolean = - contextMenuPortal.handleMouseMove(mouseX, mouseY) - -fun ApplicationOverlayHost.handleContextMenuMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = - contextMenuPortal.handleMouseDown(mouseX, mouseY, button) - -fun ApplicationOverlayHost.handleContextMenuMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = - contextMenuPortal.handleMouseUp(mouseX, mouseY, button) - -fun ApplicationOverlayHost.handleContextMenuMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = - contextMenuPortal.handleMouseWheel(mouseX, mouseY, delta) - -fun ApplicationOverlayHost.handleContextMenuKeyDown(keyCode: Int): Boolean = contextMenuPortal.handleKeyDown(keyCode) - -fun ApplicationOverlayHost.applicationSelectOnFrame( - measureContext: UiMeasureContext, - viewportWidth: Int, - viewportHeight: Int, - viewportScale: Float, -) { - applicationSelectPortal.onFrame( - measureContext = measureContext, - viewportWidth = viewportWidth, - viewportHeight = viewportHeight, - viewportScale = viewportScale, - ) -} - -fun ApplicationOverlayHost.appendApplicationSelectOverlayCommands( - measureContext: UiMeasureContext, - viewportWidth: Int, - viewportHeight: Int, - out: MutableList, -) { - applicationSelectPortal.appendCommands( - measureContext = measureContext, - viewportWidth = viewportWidth, - viewportHeight = viewportHeight, - out = out, - ) -} - -fun ApplicationOverlayHost.isApplicationSelectOpen(): Boolean = applicationSelectPortal.isOpen() - -fun ApplicationOverlayHost.handleApplicationSelectKeyDown(keyCode: Int, keyChar: Char): Boolean = - applicationSelectPortal.handleKeyDown(keyCode, keyChar) - -fun ApplicationOverlayHost.handleApplicationSelectMouseMove(mouseX: Int, mouseY: Int): Boolean = - applicationSelectPortal.handleMouseMove(mouseX, mouseY) - -fun ApplicationOverlayHost.handleApplicationSelectMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = - applicationSelectPortal.handleMouseDown(mouseX, mouseY, button) - -fun ApplicationOverlayHost.handleApplicationSelectMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = - applicationSelectPortal.handleMouseUp(mouseX, mouseY, button) - -fun ApplicationOverlayHost.handleApplicationSelectMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = - applicationSelectPortal.handleMouseWheel(mouseX, mouseY, delta) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt index 4ad29e9..0a20665 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt @@ -26,7 +26,7 @@ import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanel import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelStyle import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.select.SelectPortalController -import org.dreamfinity.dsgl.core.select.SelectRuntime +import org.dreamfinity.dsgl.core.select.SelectPortalServices import org.dreamfinity.dsgl.core.style.StyleApplicationScope class SystemOverlayHost( @@ -51,7 +51,7 @@ class SystemOverlayHost( SystemOverlayTransientOwnershipRegistry() private val systemSelectPortal: SelectPortalController = SelectPortalController( - engine = SelectRuntime.systemEngine, + engine = SelectPortalServices.systemEngine, ownerScope = OverlayOwnerScope.System, entryId = "system.select", ) @@ -81,7 +81,7 @@ class SystemOverlayHost( portalEntries.forEach(portalHost::register) } - fun systemInspectorColorPickerPopupHost(): InspectorColorPickerHost = colorPickerEntry + fun systemInspectorColorPickerPortalService(): InspectorColorPickerHost = colorPickerEntry fun isSystemColorPickerOpen(): Boolean = colorPickerEntry.isOpen() @@ -95,7 +95,7 @@ class SystemOverlayHost( fun isOverlayPanelDemoOpen(): Boolean = overlayPanelDemoEntry.isOpen() - fun systemSelectOnFrame( + fun syncPortalFrame( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, @@ -109,7 +109,7 @@ class SystemOverlayHost( ) } - fun appendSystemSelectOverlayCommands( + fun appendPortalOverlayCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, @@ -123,21 +123,19 @@ class SystemOverlayHost( ) } - fun isSystemSelectOpen(): Boolean = systemSelectPortal.isOpen() + fun hasOpenPortal(): Boolean = systemSelectPortal.isOpen() - fun handleSystemSelectKeyDown(keyCode: Int, keyChar: Char): Boolean = - systemSelectPortal.handleKeyDown(keyCode, keyChar) + fun handlePortalKeyDown(keyCode: Int, keyChar: Char): Boolean = systemSelectPortal.handleKeyDown(keyCode, keyChar) - fun handleSystemSelectMouseMove(mouseX: Int, mouseY: Int): Boolean = - systemSelectPortal.handleMouseMove(mouseX, mouseY) + fun handlePortalMouseMove(mouseX: Int, mouseY: Int): Boolean = systemSelectPortal.handleMouseMove(mouseX, mouseY) - fun handleSystemSelectMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + fun handlePortalMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = systemSelectPortal.handleMouseDown(mouseX, mouseY, button) - fun handleSystemSelectMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + fun handlePortalMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = systemSelectPortal.handleMouseUp(mouseX, mouseY, button) - fun handleSystemSelectMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = + fun handlePortalMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = systemSelectPortal.handleMouseWheel(mouseX, mouseY, delta) override fun onInputFrame(viewportWidth: Int, viewportHeight: Int) { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt index 56a5c54..fff2abc 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt @@ -16,13 +16,14 @@ import org.dreamfinity.dsgl.core.overlay.PortalEntryState import org.dreamfinity.dsgl.core.overlay.PortalFocusPolicy import org.dreamfinity.dsgl.core.overlay.PortalHost import org.dreamfinity.dsgl.core.overlay.PortalInputPolicy +import org.dreamfinity.dsgl.core.overlay.PortalPointerDispatch import org.dreamfinity.dsgl.core.render.RenderCommand internal class SelectPortalController( private val engine: SelectEngine, ownerScope: OverlayOwnerScope, entryId: String, -) { +) : PortalPointerDispatch { private val portalHost: PortalHost = PortalHost(OverlayLayerContracts.portalSurfaceForOwner(ownerScope)) private val entry: SelectPortalEntry = @@ -62,16 +63,16 @@ internal class SelectPortalController( fun isOpen(): Boolean = engine.isOpen() - fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = + override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = portalHost.dispatchInput { it.handleMouseMove(mouseX, mouseY) } - fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = portalHost.dispatchInput { it.handleMouseDown(mouseX, mouseY, button) } - fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = portalHost.dispatchInput { it.handleMouseUp(mouseX, mouseY, button) } - fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = + override fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = portalHost.dispatchInput { it.handleMouseWheel(mouseX, mouseY, delta) } fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalServices.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalServices.kt new file mode 100644 index 0000000..b7a247d --- /dev/null +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalServices.kt @@ -0,0 +1,36 @@ +package org.dreamfinity.dsgl.core.select + +import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope + +object SelectPortalServices { + val applicationEngine: SelectEngine = SelectEngine() + val systemEngine: SelectEngine = SelectEngine() + val engine: SelectEngine = applicationEngine + + fun engineFor(ownerScope: OverlayOwnerScope): SelectEngine = + when (ownerScope) { + OverlayOwnerScope.Application -> applicationEngine + OverlayOwnerScope.System -> systemEngine + } + + fun open(request: SelectOpenRequest) { + val target = engineFor(request.ownerScope) + val other = if (target === applicationEngine) systemEngine else applicationEngine + other.close(request.owner) + target.open(request) + } + + fun close(owner: Any) { + applicationEngine.close(owner) + systemEngine.close(owner) + } + + fun closeAll() { + applicationEngine.closeAll() + systemEngine.closeAll() + } + + fun isOpenFor(owner: Any): Boolean = applicationEngine.isOpenFor(owner) || systemEngine.isOpenFor(owner) + + fun isOpen(): Boolean = applicationEngine.isOpen() || systemEngine.isOpen() +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntime.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntime.kt deleted file mode 100644 index cbe2b45..0000000 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntime.kt +++ /dev/null @@ -1,40 +0,0 @@ -package org.dreamfinity.dsgl.core.select - -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope - -object SelectRuntime { - val applicationEngine: SelectEngine = SelectEngine() - val systemEngine: SelectEngine = SelectEngine() - val engine: SelectEngine = applicationEngine - val host: SelectHost = RoutedSelectHost() - - fun engineFor(ownerScope: OverlayOwnerScope): SelectEngine = - when (ownerScope) { - OverlayOwnerScope.Application -> applicationEngine - OverlayOwnerScope.System -> systemEngine - } - - private class RoutedSelectHost : SelectHost { - override fun open(request: SelectOpenRequest) { - val target = engineFor(request.ownerScope) - val other = if (target === applicationEngine) systemEngine else applicationEngine - other.close(request.owner) - target.open(request) - } - - override fun close(owner: Any) { - applicationEngine.close(owner) - systemEngine.close(owner) - } - - override fun closeAll() { - applicationEngine.closeAll() - systemEngine.closeAll() - } - - override fun isOpenFor(owner: Any): Boolean = - applicationEngine.isOpenFor(owner) || systemEngine.isOpenFor(owner) - - override fun isOpen(): Boolean = applicationEngine.isOpen() || systemEngine.isOpen() - } -} diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/DomTreeCachingTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/DomTreeCachingTests.kt index 80422ab..b6b6ace 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/DomTreeCachingTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/DomTreeCachingTests.kt @@ -1,6 +1,6 @@ package org.dreamfinity.dsgl.core -import org.dreamfinity.dsgl.core.components.modal.internal.ModalHostNode +import org.dreamfinity.dsgl.core.components.modal.internal.ModalPortalAnchorNode import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode @@ -189,8 +189,8 @@ class DomTreeCachingTests { } @Test - fun `modal host does not duplicate child commands in chunk assembly`() { - val host = ModalHostNode(key = "modal-host") + fun `modal portal does not duplicate child commands in chunk assembly`() { + val host = ModalPortalAnchorNode(key = "modal-host") CountingRectNode(color = 0xFFAA3300.toInt(), key = "content").applyParent(host) CountingRectNode(color = 0xFF0033AA.toInt(), key = "overlay").applyParent(host) val tree = DomTree(host) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalHostHookOwnerPropagationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalHookOwnerPropagationTests.kt similarity index 82% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalHostHookOwnerPropagationTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalHookOwnerPropagationTests.kt index f26b92d..9c5f8ff 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalHostHookOwnerPropagationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalHookOwnerPropagationTests.kt @@ -7,10 +7,10 @@ import org.dreamfinity.dsgl.core.hooks.useState import kotlin.test.Test import kotlin.test.assertEquals -class ModalHostHookOwnerPropagationTests { +class ModalPortalHookOwnerPropagationTests { @Test - fun `hooks inside modalHost content keep owner-bound UiScope`() { - val window = ModalHostStateWindow() + fun `hooks inside modalPortal content keep owner-bound UiScope`() { + val window = ModalPortalStateWindow() renderWithHookSession(window) assertEquals(0, window.lastSeenCount) @@ -33,15 +33,15 @@ class ModalHostHookOwnerPropagationTests { } } - private class ModalHostStateWindow : DsglWindow() { + private class ModalPortalStateWindow : DsglWindow() { var pendingMutation: Int? = null var lastSeenCount: Int = -1 override fun render(): DomTree = ui { - modalHost( + modalPortal( modals = emptyList(), - modalKey = "test.modal.host", + key = "test.modal.portal", ) { var count by useState(0) pendingMutation?.let { mutation -> diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalHostKeyboardRegressionTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalKeyboardRegressionTests.kt similarity index 93% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalHostKeyboardRegressionTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalKeyboardRegressionTests.kt index 7f61d5e..e84e506 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalHostKeyboardRegressionTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalKeyboardRegressionTests.kt @@ -2,7 +2,7 @@ package org.dreamfinity.dsgl.core.components.modal import org.dreamfinity.dsgl.core.DomTree import org.dreamfinity.dsgl.core.DsglWindow -import org.dreamfinity.dsgl.core.components.modal.internal.ModalRuntime +import org.dreamfinity.dsgl.core.components.modal.internal.ModalPortalSessionStore import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.elements.ButtonNode import org.dreamfinity.dsgl.core.dom.elements.InputType @@ -30,7 +30,7 @@ import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertTrue -class ModalHostKeyboardRegressionTests { +class ModalPortalKeyboardRegressionTests { private val trees: MutableList = ArrayList() private val overlays: MutableList = ArrayList() private val hostKeys: MutableSet = LinkedHashSet() @@ -57,7 +57,7 @@ class ModalHostKeyboardRegressionTests { } } overlays.forEach { overlay -> overlay.clearRefs() } - hostKeys.forEach(ModalRuntime::forgetPortal) + hostKeys.forEach(ModalPortalSessionStore::forgetPortal) trees.clear() overlays.clear() hostKeys.clear() @@ -65,7 +65,7 @@ class ModalHostKeyboardRegressionTests { @Test fun `escape is not cancelled after static modal closes`() { - val hostKey = "tests.modal.host.keyboard.regression" + val hostKey = "tests.modal.portal.keyboard.regression" val current = buildTree(hostKey, emptyList()) trees += current @@ -85,8 +85,8 @@ class ModalHostKeyboardRegressionTests { } @Test - fun `modal host fills root viewport bounds`() { - val tree = buildTree("tests.modal.host.layout.viewport", emptyList()) + fun `modal portal fills root viewport bounds`() { + val tree = buildTree("tests.modal.portal.layout.viewport", emptyList()) trees += tree tree.render(measureContext, 1920, 1080) @@ -108,16 +108,16 @@ class ModalHostKeyboardRegressionTests { @Test fun `modal layers mount through application overlay portal`() { - val hostKey = "tests.modal.host.portal" + val hostKey = "tests.modal.portal.portal" val tree = buildTree(hostKey, listOf(basicModal())) trees += tree tree.render(measureContext, 320, 180) - val modalHost = + val modalPortal = tree.root.children .firstOrNull() - assertNotNull(modalHost) - assertEquals(listOf("$hostKey.content"), modalHost.children.map { it.key }) + assertNotNull(modalPortal) + assertEquals(listOf("$hostKey.content"), modalPortal.children.map { it.key }) val overlay = ApplicationOverlayHost() overlays += overlay @@ -128,7 +128,7 @@ class ModalHostKeyboardRegressionTests { @Test fun `modal portal blocks application root click through`() { - val hostKey = "tests.modal.host.portal.input" + val hostKey = "tests.modal.portal.portal.input" val tree = buildTree(hostKey, listOf(basicModal())) trees += tree tree.render(measureContext, 320, 180) @@ -142,7 +142,7 @@ class ModalHostKeyboardRegressionTests { @Test fun `modal portal does not dismiss non static modal when clicking inside dialog body`() { - val hostKey = "tests.modal.host.portal.inside.dismiss" + val hostKey = "tests.modal.portal.portal.inside.dismiss" var hideCount = 0 val tree = buildTree(hostKey, listOf(dismissibleBodyModal { hideCount += 1 })) trees += tree @@ -152,7 +152,7 @@ class ModalHostKeyboardRegressionTests { overlays += overlay overlay.render(measureContext, 320, 180) - val dialog = overlay.modalPortal.debugFindNodeByKey(ModalRuntime.dialogKey(hostKey, "modal.dismissible")) + val dialog = overlay.modalPortal.debugFindNodeByKey(ModalPortalSessionStore.dialogKey(hostKey, "modal.dismissible")) assertNotNull(dialog) val clickX = dialog.bounds.x + dialog.bounds.width / 2 val clickY = dialog.bounds.y + dialog.bounds.height / 2 @@ -164,7 +164,7 @@ class ModalHostKeyboardRegressionTests { @Test fun `modal portal dismisses non static modal when clicking backdrop`() { - val hostKey = "tests.modal.host.portal.backdrop.dismiss" + val hostKey = "tests.modal.portal.portal.backdrop.dismiss" var hideCount = 0 val tree = buildTree(hostKey, listOf(dismissibleBodyModal { hideCount += 1 })) trees += tree @@ -181,7 +181,7 @@ class ModalHostKeyboardRegressionTests { @Test fun `modal portal keeps topmost focus request on overlay commit`() { - val hostKey = "tests.modal.host.portal.focus" + val hostKey = "tests.modal.portal.portal.focus" val current = buildTreeWithContentInput(hostKey, emptyList()) trees += current current.render(measureContext, 320, 180) @@ -201,7 +201,7 @@ class ModalHostKeyboardRegressionTests { @Test fun `modal portal keeps underlying modal pointer interactive after top modal closes`() { - val hostKey = "tests.modal.host.portal.stack.pointer" + val hostKey = "tests.modal.portal.portal.stack.pointer" var stepOneClicks = 0 val stepOne = clickableModal("step.one", "step.one.button") { stepOneClicks += 1 } val stepTwo = clickableModal("step.two", "step.two.button") {} @@ -237,7 +237,7 @@ class ModalHostKeyboardRegressionTests { @Test fun `modal portal restores showcase flow modal pointer interaction after closing step two`() { - val hostKey = "tests.modal.host.portal.showcase.flow" + val hostKey = "tests.modal.portal.portal.showcase.flow" var modals: List = emptyList() fun removeModal(key: String) { @@ -271,7 +271,7 @@ class ModalHostKeyboardRegressionTests { @Test fun `modal portal restores showcase flow pointer interaction after closing step two header button`() { - val hostKey = "tests.modal.host.portal.showcase.flow.header" + val hostKey = "tests.modal.portal.portal.showcase.flow.header" var modals: List = emptyList() fun removeModal(key: String) { @@ -296,7 +296,7 @@ class ModalHostKeyboardRegressionTests { clickOverlayButtonInDialog( overlay = overlay, text = "x", - dialogKey = ModalRuntime.dialogKey(hostKey, "modal.flow.2"), + dialogKey = ModalPortalSessionStore.dialogKey(hostKey, "modal.flow.2"), ) assertEquals(listOf("modal.flow.1"), modals.map { it.key }) tree = reconcileTree(tree, buildTree(hostKey, modals)) @@ -340,7 +340,7 @@ class ModalHostKeyboardRegressionTests { private fun buildTree(hostKey: String, modals: List): DomTree { hostKeys += hostKey return ui { - modalHost(modals = modals, modalKey = hostKey) { + modalPortal(modals = modals, key = hostKey) { div({ key = "$hostKey.content" }) } } @@ -349,7 +349,7 @@ class ModalHostKeyboardRegressionTests { private fun buildTreeWithContentInput(hostKey: String, modals: List): DomTree { hostKeys += hostKey return ui { - modalHost(modals = modals, modalKey = hostKey) { + modalPortal(modals = modals, key = hostKey) { input(InputType.Text(value = ""), { key = "$hostKey.content.input" }) @@ -510,7 +510,7 @@ class ModalHostKeyboardRegressionTests { fun pushModal(modal: ModalSpec) { modals += modal } - modalHost(modals = modals, modalKey = "tests.modal.host.portal.hook.showcase") { + modalPortal(modals = modals, key = "tests.modal.portal.portal.hook.showcase") { button("Open flow step 1", { key = "open.flow" onMouseClick = { pushModal(showcaseFlowStepOne(::pushModal, ::removeModal)) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalRuntimeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalSessionStoreTests.kt similarity index 74% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalRuntimeTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalSessionStoreTests.kt index a520ae7..d7e2327 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalRuntimeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalSessionStoreTests.kt @@ -1,6 +1,6 @@ package org.dreamfinity.dsgl.core.components.modal -import org.dreamfinity.dsgl.core.components.modal.internal.ModalRuntime +import org.dreamfinity.dsgl.core.components.modal.internal.ModalPortalSessionStore import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode @@ -9,7 +9,7 @@ import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals -class ModalRuntimeTests { +class ModalPortalSessionStoreTests { @AfterTest fun cleanup() { FocusManager.clearFocus() @@ -17,47 +17,47 @@ class ModalRuntimeTests { @Test fun restoresPreviousFocusWhenTopModalCloses() { - val hostKey = "tests.modal.host.restore" + val hostKey = "tests.modal.portal.restore" val contentRoot = buildRoot(hostKey, includeM1 = false, includeM2 = false) FocusManager.requestFocus(requireNodeByKey(contentRoot, "content.input")) FocusManager.retainFocus(contentRoot) val modal1 = ModalSpec(key = "m1") { _ -> } - ModalRuntime.onBuild(hostKey, listOf(modal1)) + ModalPortalSessionStore.onBuild(hostKey, listOf(modal1)) val withModal = buildRoot(hostKey, includeM1 = true, includeM2 = false) FocusManager.retainFocus(withModal) - ModalRuntime.onCommit(hostKey, listOf(modal1)) + ModalPortalSessionStore.onCommit(hostKey, listOf(modal1)) assertEquals("m1.input", FocusManager.focusedNode()?.key) - ModalRuntime.onBuild(hostKey, emptyList()) + ModalPortalSessionStore.onBuild(hostKey, emptyList()) val withoutModal = buildRoot(hostKey, includeM1 = false, includeM2 = false) FocusManager.retainFocus(withoutModal) - ModalRuntime.onCommit(hostKey, emptyList()) + ModalPortalSessionStore.onCommit(hostKey, emptyList()) assertEquals("content.input", FocusManager.focusedNode()?.key) } @Test fun focusesNewestTopmostAndRestoresUnderlyingModalFocusOnPop() { - val hostKey = "tests.modal.host.stack" + val hostKey = "tests.modal.portal.stack" val modal1 = ModalSpec(key = "m1") { _ -> } val modal2 = ModalSpec(key = "m2") { _ -> } val withM1 = buildRoot(hostKey, includeM1 = true, includeM2 = false) FocusManager.retainFocus(withM1) - ModalRuntime.onBuild(hostKey, listOf(modal1)) - ModalRuntime.onCommit(hostKey, listOf(modal1)) + ModalPortalSessionStore.onBuild(hostKey, listOf(modal1)) + ModalPortalSessionStore.onCommit(hostKey, listOf(modal1)) assertEquals("m1.input", FocusManager.focusedNode()?.key) - ModalRuntime.onBuild(hostKey, listOf(modal1, modal2)) + ModalPortalSessionStore.onBuild(hostKey, listOf(modal1, modal2)) val withM1M2 = buildRoot(hostKey, includeM1 = true, includeM2 = true) FocusManager.retainFocus(withM1M2) - ModalRuntime.onCommit(hostKey, listOf(modal1, modal2)) + ModalPortalSessionStore.onCommit(hostKey, listOf(modal1, modal2)) assertEquals("m2.input", FocusManager.focusedNode()?.key) - ModalRuntime.onBuild(hostKey, listOf(modal1)) + ModalPortalSessionStore.onBuild(hostKey, listOf(modal1)) val backToM1 = buildRoot(hostKey, includeM1 = true, includeM2 = false) FocusManager.retainFocus(backToM1) - ModalRuntime.onCommit(hostKey, listOf(modal1)) + ModalPortalSessionStore.onCommit(hostKey, listOf(modal1)) assertEquals("m1.input", FocusManager.focusedNode()?.key) } @@ -67,12 +67,12 @@ class ModalRuntimeTests { FocusableNode(key = "content.input").applyParent(this) } if (includeM1) { - ContainerNode(key = ModalRuntime.dialogKey(hostKey, "m1")).applyParent(root).apply { + ContainerNode(key = ModalPortalSessionStore.dialogKey(hostKey, "m1")).applyParent(root).apply { FocusableNode(key = "m1.input").applyParent(this) } } if (includeM2) { - ContainerNode(key = ModalRuntime.dialogKey(hostKey, "m2")).applyParent(root).apply { + ContainerNode(key = ModalPortalSessionStore.dialogKey(hostKey, "m2")).applyParent(root).apply { FocusableNode(key = "m2.input").applyParent(this) } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerScopeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerScopeTests.kt index beae55e..ebf3ffc 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerScopeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerScopeTests.kt @@ -9,7 +9,7 @@ import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand -import org.dreamfinity.dsgl.core.select.SelectRuntime +import org.dreamfinity.dsgl.core.select.SelectPortalServices import org.dreamfinity.dsgl.core.select.selectModel import kotlin.test.AfterTest import kotlin.test.Test @@ -28,7 +28,7 @@ class SelectNodeOwnerScopeTests { @AfterTest fun cleanup() { - SelectRuntime.host.closeAll() + SelectPortalServices.closeAll() } @Test @@ -62,7 +62,7 @@ class SelectNodeOwnerScopeTests { assertTrue(router.handleMouseDown(clickX, clickY, MouseButton.LEFT)) assertTrue(router.handleMouseUp(clickX, clickY, MouseButton.LEFT)) - assertFalse(SelectRuntime.applicationEngine.isOpenFor(ownerKey)) - assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) + assertFalse(SelectPortalServices.applicationEngine.isOpenFor(ownerKey)) + assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectPopupAnchoringStickyTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectPopupAnchoringStickyTests.kt index 71066e8..9dd4163 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectPopupAnchoringStickyTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectPopupAnchoringStickyTests.kt @@ -9,7 +9,7 @@ import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand -import org.dreamfinity.dsgl.core.select.SelectRuntime +import org.dreamfinity.dsgl.core.select.SelectPortalServices import org.dreamfinity.dsgl.core.select.selectModel import org.dreamfinity.dsgl.core.style.Overflow import org.dreamfinity.dsgl.core.style.StyleDeclarations @@ -37,7 +37,7 @@ class SelectPopupAnchoringStickyTests { @AfterTest fun cleanup() { - SelectRuntime.host.closeAll() + SelectPortalServices.closeAll() StyleEngine.clearAllInspectorOverrides() StyleEngine.clearCache() } @@ -96,7 +96,7 @@ class SelectPopupAnchoringStickyTests { val y = visible.y + visible.height / 2 assertTrue(fixture.router.handleMouseDown(x, y, MouseButton.LEFT)) - assertTrue(SelectRuntime.host.isOpenFor(fixture.ownerKey)) + assertTrue(SelectPortalServices.isOpenFor(fixture.ownerKey)) assertTrue(fixture.router.handleMouseUp(x, y, MouseButton.LEFT)) } @@ -106,19 +106,19 @@ class SelectPopupAnchoringStickyTests { val y = visible.y + visible.height / 2 assertTrue(fixture.router.handleMouseDown(x, y, MouseButton.LEFT)) assertTrue(fixture.router.handleMouseUp(x, y, MouseButton.LEFT)) - assertTrue(SelectRuntime.host.isOpenFor(fixture.ownerKey)) + assertTrue(SelectPortalServices.isOpenFor(fixture.ownerKey)) - SelectRuntime.engine.onFrame( + SelectPortalServices.engine.onFrame( measureContext = ctx, viewportWidth = viewportWidth, viewportHeight = viewportHeight, viewportScale = 1f, ) val anchor = - SelectRuntime.engine.debugAnchorRect(fixture.ownerKey) + SelectPortalServices.engine.debugAnchorRect(fixture.ownerKey) ?: error("Expected select anchor rect for owner=${fixture.ownerKey}") val panel = - SelectRuntime.engine.debugPanelRect(fixture.ownerKey) + SelectPortalServices.engine.debugPanelRect(fixture.ownerKey) ?: error("Expected select panel rect for owner=${fixture.ownerKey}") return PopupGeometry(anchor = anchor, panel = panel) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt index 5c62278..3493d8b 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt @@ -1,12 +1,12 @@ package org.dreamfinity.dsgl.core.overlay import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupRequest -import org.dreamfinity.dsgl.core.colorpicker.ColorPickerRuntime +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPortalServices import org.dreamfinity.dsgl.core.colorpicker.ColorPickerState import org.dreamfinity.dsgl.core.colorpicker.RgbaColor import org.dreamfinity.dsgl.core.colorpicker.ScreenColorSampler import org.dreamfinity.dsgl.core.colorpicker.ScreenColorSamplerBridge -import org.dreamfinity.dsgl.core.contextmenu.ContextMenuRuntime +import org.dreamfinity.dsgl.core.contextmenu.ContextMenuPortalServices import org.dreamfinity.dsgl.core.contextmenu.contextMenu import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent @@ -23,7 +23,7 @@ import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayPanelDemoNode import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.select.SelectEntry import org.dreamfinity.dsgl.core.select.SelectOpenRequest -import org.dreamfinity.dsgl.core.select.SelectRuntime +import org.dreamfinity.dsgl.core.select.SelectPortalServices import org.dreamfinity.dsgl.core.select.selectModel import kotlin.test.AfterTest import kotlin.test.Test @@ -44,11 +44,11 @@ class LiveLayerInteractionPathTests { } @AfterTest - fun cleanupContextMenuRuntime() { - ContextMenuRuntime.engine.closeAll() - ColorPickerRuntime.engine.closeAll() + fun cleanupContextMenuPortalServices() { + ContextMenuPortalServices.engine.closeAll() + ColorPickerPortalServices.engine.closeAll() ScreenColorSamplerBridge.install(null) - SelectRuntime.host.closeAll() + SelectPortalServices.closeAll() } @Test @@ -241,7 +241,7 @@ class LiveLayerInteractionPathTests { val applicationOverlayHost = ApplicationOverlayHost() applicationOverlayHost.onInputFrame(320, 180) var actionHits = 0 - ContextMenuRuntime.host.openAtCursor( + ContextMenuPortalServices.engine.openAtCursor( contextMenu(id = "portal.context") { item("Run") { onClick { actionHits += 1 } @@ -251,30 +251,32 @@ class LiveLayerInteractionPathTests { y = 24, ) - applicationOverlayHost.contextMenuOnFrame(ctx, 320, 180, 1f) + applicationOverlayHost.syncPortalFrame(ctx, 320, 180, 1f, 24, 24) val commands = ArrayList() - applicationOverlayHost.appendContextMenuOverlayCommands(ctx, 320, 180, commands) - val firstEntryRect = ContextMenuRuntime.engine.debugEntryRect(levelIndex = 0, entryIndex = 0) + applicationOverlayHost.appendPortalOverlayCommands(ctx, 320, 180, commands) + val firstEntryRect = ContextMenuPortalServices.engine.debugEntryRect(levelIndex = 0, entryIndex = 0) assertNotNull(firstEntryRect) val consumedByMenu = - applicationOverlayHost.handleContextMenuMouseDown( + applicationOverlayHost.handlePortalPointerAfterDom( mouseX = firstEntryRect.x + 1, mouseY = firstEntryRect.y + 1, + dWheel = 0, button = MouseButton.LEFT, + pressed = true, ) assertTrue(commands.isNotEmpty()) assertTrue(consumedByMenu) assertEquals(1, actionHits) - assertFalse(applicationOverlayHost.isContextMenuOpen()) + assertFalse(applicationOverlayHost.hasOpenContextMenuPortal()) } @Test fun `application context menu portal blocks app-root fallthrough on outside dismiss`() { val applicationOverlayHost = ApplicationOverlayHost() applicationOverlayHost.onInputFrame(320, 180) - ContextMenuRuntime.host.openAtCursor( + ContextMenuPortalServices.engine.openAtCursor( contextMenu(id = "portal.dismiss") { item("Run") item("Build") @@ -282,8 +284,8 @@ class LiveLayerInteractionPathTests { x = 24, y = 24, ) - applicationOverlayHost.contextMenuOnFrame(ctx, 320, 180, 1f) - val panel = ContextMenuRuntime.engine.debugPanelRect(0) + applicationOverlayHost.syncPortalFrame(ctx, 320, 180, 1f, 24, 24) + val panel = ContextMenuPortalServices.engine.debugPanelRect(0) assertNotNull(panel) val outsideX = panel.x + panel.width + 24 val outsideY = panel.y + panel.height + 24 @@ -293,7 +295,7 @@ class LiveLayerInteractionPathTests { debugHandler = { _, _, _ -> false }, systemOverlayHandler = { _, _, _ -> false }, applicationOverlayHandler = { x, y, button -> - applicationOverlayHost.handleContextMenuMouseDown(x, y, button) + applicationOverlayHost.handlePortalPointerAfterDom(x, y, 0, button, true) }, ) var appRootReceived = false @@ -305,14 +307,14 @@ class LiveLayerInteractionPathTests { assertEquals(UiLayerId.ApplicationOverlay, consumedBy) assertFalse(appRootReceived) - assertFalse(applicationOverlayHost.isContextMenuOpen()) + assertFalse(applicationOverlayHost.hasOpenContextMenuPortal()) } @Test fun `application context menu portal consumes wheel and escape while open`() { val applicationOverlayHost = ApplicationOverlayHost() applicationOverlayHost.onInputFrame(320, 180) - ContextMenuRuntime.host.openAtCursor( + ContextMenuPortalServices.engine.openAtCursor( contextMenu(id = "portal.keyboard") { item("Run") item("Build") @@ -320,11 +322,11 @@ class LiveLayerInteractionPathTests { x = 24, y = 24, ) - applicationOverlayHost.contextMenuOnFrame(ctx, 320, 180, 1f) + applicationOverlayHost.syncPortalFrame(ctx, 320, 180, 1f, 24, 24) - assertTrue(applicationOverlayHost.handleContextMenuMouseWheel(26, 26, -120)) - assertTrue(applicationOverlayHost.handleContextMenuKeyDown(KeyCodes.ESCAPE)) - assertFalse(applicationOverlayHost.isContextMenuOpen()) + assertTrue(applicationOverlayHost.handlePortalPointerAfterDom(26, 26, -120, null, false)) + assertTrue(applicationOverlayHost.handlePortalKeyDownAfterDom(KeyCodes.ESCAPE, Char.MIN_VALUE)) + assertFalse(applicationOverlayHost.hasOpenContextMenuPortal()) } @Test @@ -333,20 +335,22 @@ class LiveLayerInteractionPathTests { applicationOverlayHost.onInputFrame(320, 180) var selected: String? = null val owner = "application-select-portal" - SelectRuntime.host.open(selectRequest(owner, OverlayOwnerScope.Application) { selected = it }) + SelectPortalServices.open(selectRequest(owner, OverlayOwnerScope.Application) { selected = it }) - applicationOverlayHost.applicationSelectOnFrame(ctx, 320, 180, 1f) + applicationOverlayHost.syncPortalFrame(ctx, 320, 180, 1f, 0, 0) val commands = ArrayList() - applicationOverlayHost.appendApplicationSelectOverlayCommands(ctx, 320, 180, commands) - val panel = SelectRuntime.applicationEngine.debugPanelRect(owner) + applicationOverlayHost.appendPortalOverlayCommands(ctx, 320, 180, commands) + val panel = SelectPortalServices.applicationEngine.debugPanelRect(owner) assertNotNull(panel) - val style = SelectRuntime.applicationEngine.currentStyle() + val style = SelectPortalServices.applicationEngine.currentStyle() val consumed = - applicationOverlayHost.handleApplicationSelectMouseDown( + applicationOverlayHost.handlePortalPointerAfterDom( mouseX = panel.x + style.panelPaddingX + 1, mouseY = panel.y + style.panelPaddingY + 1, + dWheel = 0, button = MouseButton.LEFT, + pressed = true, ) assertTrue(commands.isNotEmpty()) @@ -359,9 +363,9 @@ class LiveLayerInteractionPathTests { val applicationOverlayHost = ApplicationOverlayHost() applicationOverlayHost.onInputFrame(320, 180) val owner = "application-select-dismiss" - SelectRuntime.host.open(selectRequest(owner, OverlayOwnerScope.Application)) - applicationOverlayHost.applicationSelectOnFrame(ctx, 320, 180, 1f) - val panel = SelectRuntime.applicationEngine.debugPanelRect(owner) + SelectPortalServices.open(selectRequest(owner, OverlayOwnerScope.Application)) + applicationOverlayHost.syncPortalFrame(ctx, 320, 180, 1f, 0, 0) + val panel = SelectPortalServices.applicationEngine.debugPanelRect(owner) assertNotNull(panel) val outsideX = panel.x + panel.width + 24 val outsideY = panel.y + panel.height + 24 @@ -370,7 +374,7 @@ class LiveLayerInteractionPathTests { debugHandler = { _, _, _ -> false }, systemOverlayHandler = { _, _, _ -> false }, applicationOverlayHandler = { x, y, button -> - applicationOverlayHost.handleApplicationSelectMouseDown(x, y, button) + applicationOverlayHost.handlePortalPointerAfterDom(x, y, 0, button, true) }, ) @@ -391,7 +395,7 @@ class LiveLayerInteractionPathTests { applicationOverlayHost.onInputFrame(320, 120) val owner = "application-select-keyboard" var selected: String? = null - SelectRuntime.host.open( + SelectPortalServices.open( selectRequest( owner = owner, ownerScope = OverlayOwnerScope.Application, @@ -407,18 +411,18 @@ class LiveLayerInteractionPathTests { onSelect = { selected = it }, ), ) - applicationOverlayHost.applicationSelectOnFrame(ctx, 320, 120, 1f) - val panel = SelectRuntime.applicationEngine.debugPanelRect(owner) + applicationOverlayHost.syncPortalFrame(ctx, 320, 120, 1f, 0, 0) + val panel = SelectPortalServices.applicationEngine.debugPanelRect(owner) assertNotNull(panel) - assertTrue(applicationOverlayHost.handleApplicationSelectMouseWheel(panel.x + 2, panel.y + 2, -120)) - assertTrue(applicationOverlayHost.handleApplicationSelectKeyDown(0, 'd')) - assertTrue(applicationOverlayHost.handleApplicationSelectKeyDown(KeyCodes.ENTER, Char.MIN_VALUE)) + assertTrue(applicationOverlayHost.handlePortalPointerAfterDom(panel.x + 2, panel.y + 2, -120, null, false)) + assertTrue(applicationOverlayHost.handlePortalKeyDownAfterDom(0, 'd')) + assertTrue(applicationOverlayHost.handlePortalKeyDownAfterDom(KeyCodes.ENTER, Char.MIN_VALUE)) assertEquals("d", selected) - SelectRuntime.host.open(selectRequest(owner, OverlayOwnerScope.Application)) - applicationOverlayHost.applicationSelectOnFrame(ctx, 320, 120, 1f) - assertTrue(applicationOverlayHost.handleApplicationSelectKeyDown(KeyCodes.ESCAPE, Char.MIN_VALUE)) + SelectPortalServices.open(selectRequest(owner, OverlayOwnerScope.Application)) + applicationOverlayHost.syncPortalFrame(ctx, 320, 120, 1f, 0, 0) + assertTrue(applicationOverlayHost.handlePortalKeyDownAfterDom(KeyCodes.ESCAPE, Char.MIN_VALUE)) } @Test @@ -426,12 +430,12 @@ class LiveLayerInteractionPathTests { val applicationOverlayHost = ApplicationOverlayHost() applicationOverlayHost.onInputFrame(360, 240) val owner = "application-color-picker-portal" - ColorPickerRuntime.host.open(colorPickerRequest(owner, OverlayOwnerScope.Application)) + ColorPickerPortalServices.engine.open(colorPickerRequest(owner, OverlayOwnerScope.Application)) - applicationOverlayHost.applicationColorPickerOnFrame(360, 240, 42, 48) + applicationOverlayHost.syncPortalFrame(ctx, 360, 240, 1f, 42, 48) val commands = ArrayList() - applicationOverlayHost.appendApplicationColorPickerOverlayCommands(ctx, 360, 240, commands) - val layout = ColorPickerRuntime.engine.debugBodyLayout(owner) + applicationOverlayHost.appendPortalOverlayCommands(ctx, 360, 240, commands) + val layout = ColorPickerPortalServices.engine.debugBodyLayout(owner) assertNotNull(layout) val harness = @@ -439,7 +443,7 @@ class LiveLayerInteractionPathTests { debugHandler = { _, _, _ -> false }, systemOverlayHandler = { _, _, _ -> false }, applicationOverlayHandler = { x, y, button -> - applicationOverlayHost.handleApplicationColorPickerMouseDown(x, y, button) + applicationOverlayHost.handlePortalPointerBeforeDom(x, y, 0, button, true) }, ) var appRootReceived = false @@ -456,7 +460,7 @@ class LiveLayerInteractionPathTests { assertTrue(commands.isNotEmpty()) assertEquals(UiLayerId.ApplicationOverlay, consumedBy) assertFalse(appRootReceived) - assertTrue(applicationOverlayHost.isApplicationColorPickerOpen()) + assertTrue(applicationOverlayHost.hasOpenColorPickerPortal()) } @Test @@ -466,59 +470,68 @@ class LiveLayerInteractionPathTests { applicationOverlayHost.onInputFrame(480, 320) val owner = "application-color-picker-drag-eyedropper" var committed: RgbaColor? = null - ColorPickerRuntime.host.open( + ColorPickerPortalServices.engine.open( colorPickerRequest(owner, OverlayOwnerScope.Application) { committed = it }, ) - applicationOverlayHost.applicationColorPickerOnFrame(480, 320, 120, 80) + applicationOverlayHost.syncPortalFrame(ctx, 480, 320, 1f, 120, 80) - val panelBefore = ColorPickerRuntime.engine.debugPanelRect(owner) ?: error("panel missing") - val header = ColorPickerRuntime.engine.debugHeaderRect(owner) ?: error("header missing") + val panelBefore = ColorPickerPortalServices.engine.debugPanelRect(owner) ?: error("panel missing") + val header = ColorPickerPortalServices.engine.debugHeaderRect(owner) ?: error("header missing") val dragStartX = header.x + 6 val dragStartY = header.y + 6 - assertTrue(applicationOverlayHost.handleApplicationColorPickerMouseDown(dragStartX, dragStartY, MouseButton.LEFT)) + assertTrue(applicationOverlayHost.handlePortalPointerBeforeDom(dragStartX, dragStartY, 0, MouseButton.LEFT, true)) assertTrue( - applicationOverlayHost.handleApplicationColorPickerMouseMove( - dragStartX + 40, - dragStartY + 30, + applicationOverlayHost.handlePortalPointerBeforeDom( + mouseX = dragStartX + 40, + mouseY = dragStartY + 30, + dWheel = 0, + button = null, + pressed = false, ), ) assertTrue( - applicationOverlayHost.handleApplicationColorPickerMouseUp( - dragStartX + 40, - dragStartY + 30, - MouseButton.LEFT, + applicationOverlayHost.handlePortalPointerBeforeDom( + mouseX = dragStartX + 40, + mouseY = dragStartY + 30, + dWheel = 0, + button = MouseButton.LEFT, + pressed = false, ), ) - val panelAfter = ColorPickerRuntime.engine.debugPanelRect(owner) ?: error("panel missing") + val panelAfter = ColorPickerPortalServices.engine.debugPanelRect(owner) ?: error("panel missing") assertNotEquals(panelBefore.x, panelAfter.x) - val layout = ColorPickerRuntime.engine.debugBodyLayout(owner) ?: error("layout missing") + val layout = ColorPickerPortalServices.engine.debugBodyLayout(owner) ?: error("layout missing") assertTrue( - applicationOverlayHost.handleApplicationColorPickerMouseDown( - layout.pipetteRect.x + 2, - layout.pipetteRect.y + 2, - MouseButton.LEFT, + applicationOverlayHost.handlePortalPointerBeforeDom( + mouseX = layout.pipetteRect.x + 2, + mouseY = layout.pipetteRect.y + 2, + dWheel = 0, + button = MouseButton.LEFT, + pressed = true, ), ) - assertTrue(applicationOverlayHost.hasActiveApplicationColorPickerEyedropper()) - assertTrue(applicationOverlayHost.handleApplicationColorPickerMouseMove(25, 52)) - applicationOverlayHost.captureApplicationColorPickerEyedropperSample() - assertTrue(applicationOverlayHost.handleApplicationColorPickerMouseDown(25, 52, MouseButton.LEFT)) - assertTrue(applicationOverlayHost.handleApplicationColorPickerMouseUp(25, 52, MouseButton.LEFT)) + assertTrue(applicationOverlayHost.hasActiveColorPickerEyedropper()) + assertTrue(applicationOverlayHost.handlePortalPointerBeforeDom(25, 52, 0, null, false)) + applicationOverlayHost.captureColorPickerEyedropperSample() + assertTrue(applicationOverlayHost.handlePortalPointerBeforeDom(25, 52, 0, MouseButton.LEFT, true)) + assertTrue(applicationOverlayHost.handlePortalPointerBeforeDom(25, 52, 0, MouseButton.LEFT, false)) val expected = RgbaColor.fromArgbInt((0xFF shl 24) or (25 shl 16) or (52 shl 8) or 0x44) assertEquals(expected.toArgbInt(), committed?.toArgbInt()) - val closeRect = ColorPickerRuntime.engine.debugCloseRect(owner) ?: error("close missing") + val closeRect = ColorPickerPortalServices.engine.debugCloseRect(owner) ?: error("close missing") assertTrue( - applicationOverlayHost.handleApplicationColorPickerMouseDown( - closeRect.x + 1, - closeRect.y + 1, - MouseButton.LEFT, + applicationOverlayHost.handlePortalPointerBeforeDom( + mouseX = closeRect.x + 1, + mouseY = closeRect.y + 1, + dWheel = 0, + button = MouseButton.LEFT, + pressed = true, ), ) - assertFalse(applicationOverlayHost.isApplicationColorPickerOpen()) + assertFalse(applicationOverlayHost.hasOpenColorPickerPortal()) } @Test @@ -526,17 +539,17 @@ class LiveLayerInteractionPathTests { val applicationOverlayHost = ApplicationOverlayHost() applicationOverlayHost.onInputFrame(360, 240) val owner = "system-color-picker-owner" - ColorPickerRuntime.host.open(colorPickerRequest(owner, OverlayOwnerScope.System)) + ColorPickerPortalServices.engine.open(colorPickerRequest(owner, OverlayOwnerScope.System)) - applicationOverlayHost.applicationColorPickerOnFrame(360, 240, 42, 48) + applicationOverlayHost.syncPortalFrame(ctx, 360, 240, 1f, 42, 48) val commands = ArrayList() - applicationOverlayHost.appendApplicationColorPickerOverlayCommands(ctx, 360, 240, commands) - val panel = ColorPickerRuntime.engine.debugPanelRect(owner) + applicationOverlayHost.appendPortalOverlayCommands(ctx, 360, 240, commands) + val panel = ColorPickerPortalServices.engine.debugPanelRect(owner) assertNotNull(panel) - assertTrue(ColorPickerRuntime.engine.isOpenFor(owner)) - assertFalse(applicationOverlayHost.isApplicationColorPickerOpen()) - assertFalse(applicationOverlayHost.handleApplicationColorPickerMouseDown(panel.x + 2, panel.y + 2, MouseButton.LEFT)) + assertTrue(ColorPickerPortalServices.engine.isOpenFor(owner)) + assertFalse(applicationOverlayHost.hasOpenColorPickerPortal()) + assertFalse(applicationOverlayHost.handlePortalPointerBeforeDom(panel.x + 2, panel.y + 2, 0, MouseButton.LEFT, true)) assertTrue(commands.isEmpty()) } @@ -546,20 +559,20 @@ class LiveLayerInteractionPathTests { systemHost.onInputFrame(320, 180) val owner = "system-select-portal" var selected: String? = null - SelectRuntime.host.open(selectRequest(owner, OverlayOwnerScope.System) { selected = it }) + SelectPortalServices.open(selectRequest(owner, OverlayOwnerScope.System) { selected = it }) - systemHost.systemSelectOnFrame(ctx, 320, 180, 1f) + systemHost.syncPortalFrame(ctx, 320, 180, 1f) val commands = ArrayList() - systemHost.appendSystemSelectOverlayCommands(ctx, 320, 180, commands) - val panel = SelectRuntime.systemEngine.debugPanelRect(owner) + systemHost.appendPortalOverlayCommands(ctx, 320, 180, commands) + val panel = SelectPortalServices.systemEngine.debugPanelRect(owner) assertNotNull(panel) - val style = SelectRuntime.systemEngine.currentStyle() + val style = SelectPortalServices.systemEngine.currentStyle() val harness = LiveLayerInputHarness( debugHandler = { _, _, _ -> false }, systemOverlayHandler = { x, y, button -> - systemHost.handleSystemSelectMouseDown(x, y, button) + systemHost.handlePortalMouseDown(x, y, button) }, applicationOverlayHandler = { _, _, _ -> false }, ) @@ -578,20 +591,20 @@ class LiveLayerInteractionPathTests { assertEquals(UiLayerId.SystemOverlay, consumedBy) assertFalse(appRootReceived) assertEquals("a", selected) - assertFalse(SelectRuntime.applicationEngine.isOpenFor(owner)) + assertFalse(SelectPortalServices.applicationEngine.isOpenFor(owner)) } @Test fun `select owner migration preserves application system routing`() { val owner = "select-owner-migration" - SelectRuntime.host.open(selectRequest(owner, OverlayOwnerScope.Application)) - assertTrue(SelectRuntime.applicationEngine.isOpenFor(owner)) - assertFalse(SelectRuntime.systemEngine.isOpenFor(owner)) + SelectPortalServices.open(selectRequest(owner, OverlayOwnerScope.Application)) + assertTrue(SelectPortalServices.applicationEngine.isOpenFor(owner)) + assertFalse(SelectPortalServices.systemEngine.isOpenFor(owner)) - SelectRuntime.host.open(selectRequest(owner, OverlayOwnerScope.System)) + SelectPortalServices.open(selectRequest(owner, OverlayOwnerScope.System)) - assertFalse(SelectRuntime.applicationEngine.isOpenFor(owner)) - assertTrue(SelectRuntime.systemEngine.isOpenFor(owner)) + assertFalse(SelectPortalServices.applicationEngine.isOpenFor(owner)) + assertTrue(SelectPortalServices.systemEngine.isOpenFor(owner)) } @Test diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt index 3f68d68..0ef5380 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt @@ -1,6 +1,6 @@ package org.dreamfinity.dsgl.core.overlay.system -import org.dreamfinity.dsgl.core.colorpicker.ColorPickerRuntime +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPortalServices import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode @@ -15,7 +15,7 @@ import org.dreamfinity.dsgl.core.inspector.InspectorEditorKind import org.dreamfinity.dsgl.core.inspector.InspectorStyleEditorRowSnapshot import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.render.RenderCommand -import org.dreamfinity.dsgl.core.select.SelectRuntime +import org.dreamfinity.dsgl.core.select.SelectPortalServices import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleProperty import kotlin.test.* @@ -34,8 +34,8 @@ class InspectorDragScrollDomMigrationTests { fun cleanup() { FocusManager.clearFocus() KeyModifiers.sync(shift = false, control = false, meta = false) - ColorPickerRuntime.engine.closeAll() - SelectRuntime.host.closeAll() + ColorPickerPortalServices.engine.closeAll() + SelectPortalServices.closeAll() StyleEngine.clearAllInspectorOverrides() StyleEngine.clearCache() } @@ -304,12 +304,12 @@ class InspectorDragScrollDomMigrationTests { fixture.host.handleMouseUp(clickX, clickY, MouseButton.LEFT) syncAndRender(fixture, clickX, clickY) - assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) + assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) fixture.host.handleMouseDown(clickX, clickY, MouseButton.LEFT) fixture.host.handleMouseUp(clickX, clickY, MouseButton.LEFT) syncAndRender(fixture, clickX, clickY) - assertFalse(SelectRuntime.systemEngine.isOpenFor(ownerKey)) + assertFalse(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) } @Test @@ -328,7 +328,7 @@ class InspectorDragScrollDomMigrationTests { private fun openInspectorAndSelectTarget(withManyChildren: Boolean): Fixture { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRoot(withManyChildren) inspector.toggle() diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt index 09e52e0..b69da26 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt @@ -1,6 +1,6 @@ package org.dreamfinity.dsgl.core.overlay.system -import org.dreamfinity.dsgl.core.colorpicker.ColorPickerRuntime +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPortalServices import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode @@ -14,7 +14,7 @@ import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.inspector.InspectorEditorKind import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.render.RenderCommand -import org.dreamfinity.dsgl.core.select.SelectRuntime +import org.dreamfinity.dsgl.core.select.SelectPortalServices import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleProperty import kotlin.test.* @@ -33,8 +33,8 @@ class InspectorDropdownCorrectiveTests { fun cleanup() { FocusManager.clearFocus() KeyModifiers.sync(shift = false, control = false, meta = false) - ColorPickerRuntime.engine.closeAll() - SelectRuntime.host.closeAll() + ColorPickerPortalServices.engine.closeAll() + SelectPortalServices.closeAll() StyleEngine.clearAllInspectorOverrides() StyleEngine.clearCache() } @@ -74,7 +74,7 @@ class InspectorDropdownCorrectiveTests { assertTrue(dispatchSystemMouseWheel(fixture, wheelX, wheelY, -120)) syncAndRender(fixture, wheelX, wheelY) - assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) + assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) assertEquals(beforePanelScroll, fixture.inspector.panelScrollOffsetY) } @@ -145,7 +145,7 @@ class InspectorDropdownCorrectiveTests { private fun openInspectorAndSelectTarget(withManyChildren: Boolean): Fixture { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRoot(withManyChildren) inspector.toggle() @@ -255,7 +255,7 @@ class InspectorDropdownCorrectiveTests { dispatchSystemMouseUp(fixture, clickX, clickY) syncAndRender(fixture, clickX, clickY) - assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) + assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) return triggerRect to ownerKey } @@ -302,7 +302,7 @@ class InspectorDropdownCorrectiveTests { dispatchSystemMouseUp(fixture, clickX, clickY) syncAndRender(fixture, clickX, clickY) - val opened = SelectRuntime.systemEngine.isOpenFor(ownerKey) + val opened = SelectPortalServices.systemEngine.isOpenFor(ownerKey) if (opened) { val popup = selectPanelRect(ownerKey, fixture) if (!requireScrollable || popup.height > triggerRect.height + 24) { @@ -322,17 +322,17 @@ class InspectorDropdownCorrectiveTests { } private fun selectPanelRect(ownerKey: String, fixture: Fixture): Rect { - SelectRuntime.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) - return SelectRuntime.systemEngine.debugPanelRect(ownerKey) + SelectPortalServices.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) + return SelectPortalServices.systemEngine.debugPanelRect(ownerKey) ?: error("expected system select popup for owner=$ownerKey") } private fun dispatchSystemMouseDown(fixture: Fixture, x: Int, y: Int): Boolean = - SelectRuntime.systemEngine.handleMouseDown(x, y, MouseButton.LEFT) || + SelectPortalServices.systemEngine.handleMouseDown(x, y, MouseButton.LEFT) || fixture.host.handleMouseDown(x, y, MouseButton.LEFT) private fun dispatchSystemMouseUp(fixture: Fixture, x: Int, y: Int): Boolean = - SelectRuntime.systemEngine.handleMouseUp(x, y, MouseButton.LEFT) || + SelectPortalServices.systemEngine.handleMouseUp(x, y, MouseButton.LEFT) || fixture.host.handleMouseUp(x, y, MouseButton.LEFT) private fun dispatchSystemMouseWheel( @@ -341,7 +341,7 @@ class InspectorDropdownCorrectiveTests { y: Int, delta: Int, ): Boolean = - SelectRuntime.systemEngine.handleMouseWheel(x, y, delta) || + SelectPortalServices.systemEngine.handleMouseWheel(x, y, delta) || fixture.host.handleMouseWheel(x, y, delta) private fun waitForSystemSelectClosed( @@ -351,12 +351,12 @@ class InspectorDropdownCorrectiveTests { cursorY: Int, ) { repeat(30) { - if (!SelectRuntime.systemEngine.isOpenFor(ownerKey)) return + if (!SelectPortalServices.systemEngine.isOpenFor(ownerKey)) return Thread.sleep(5) syncAndRender(fixture, cursorX, cursorY) - SelectRuntime.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) + SelectPortalServices.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) } - assertFalse(SelectRuntime.systemEngine.isOpenFor(ownerKey)) + assertFalse(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) } private fun focusInputByClick(fixture: Fixture, input: TextInputNode): Pair { diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorInputPathBaselineTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorInputPathBaselineTests.kt index a661449..b1808b3 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorInputPathBaselineTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorInputPathBaselineTests.kt @@ -1,6 +1,6 @@ package org.dreamfinity.dsgl.core.overlay.system -import org.dreamfinity.dsgl.core.colorpicker.ColorPickerRuntime +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPortalServices import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode @@ -15,7 +15,7 @@ import org.dreamfinity.dsgl.core.inspector.InspectorEditorKind import org.dreamfinity.dsgl.core.inspector.InspectorStyleEditorRowSnapshot import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.render.RenderCommand -import org.dreamfinity.dsgl.core.select.SelectRuntime +import org.dreamfinity.dsgl.core.select.SelectPortalServices import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleProperty import kotlin.test.* @@ -34,8 +34,8 @@ class InspectorInputPathBaselineTests { fun cleanup() { FocusManager.clearFocus() KeyModifiers.sync(shift = false, control = false, meta = false) - ColorPickerRuntime.engine.closeAll() - SelectRuntime.host.closeAll() + ColorPickerPortalServices.engine.closeAll() + SelectPortalServices.closeAll() StyleEngine.clearAllInspectorOverrides() StyleEngine.clearCache() } @@ -45,7 +45,7 @@ class InspectorInputPathBaselineTests { val fixture = openInspectorAndSelectTarget(withManyChildren = false) val (trigger, ownerKey) = openDropdownFromVisibleSelectRow(fixture) - assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) + assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) assertNotNull(selectPanelRect(ownerKey, fixture)) assertFalse(fixture.inspector.hasOpenStyleDropdown()) assertFalse(fixture.inspector.closeOpenStyleDropdowns()) @@ -74,7 +74,7 @@ class InspectorInputPathBaselineTests { fun `inspector dropdown opens and closes from dom interactions`() { val fixture = openInspectorAndSelectTarget(withManyChildren = false) val (trigger, ownerKey) = openDropdownFromVisibleSelectRow(fixture) - assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) + assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) dispatchSystemMouseDown(fixture, trigger.x + 2, trigger.y + 2) dispatchSystemMouseUp(fixture, trigger.x + 2, trigger.y + 2) @@ -91,9 +91,9 @@ class InspectorInputPathBaselineTests { val optionX = panel.x + 6 val optionY = panel.y + 10 - SelectRuntime.systemEngine.handleMouseMove(optionX, optionY) - assertTrue(SelectRuntime.systemEngine.handleMouseDown(optionX, optionY, MouseButton.LEFT)) - assertTrue(SelectRuntime.systemEngine.handleMouseUp(optionX, optionY, MouseButton.LEFT)) + SelectPortalServices.systemEngine.handleMouseMove(optionX, optionY) + assertTrue(SelectPortalServices.systemEngine.handleMouseDown(optionX, optionY, MouseButton.LEFT)) + assertTrue(SelectPortalServices.systemEngine.handleMouseUp(optionX, optionY, MouseButton.LEFT)) syncAndRender(fixture, optionX, optionY) waitForSystemSelectClosed(fixture, ownerKey, optionX, optionY) @@ -107,7 +107,7 @@ class InspectorInputPathBaselineTests { val panel = selectPanelRect(ownerKey, fixture) syncAndRender(fixture, panel.x + 2, panel.y + 2) - assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) + assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) assertFalse(fixture.inspector.hasOpenStyleDropdown()) } @@ -116,7 +116,7 @@ class InspectorInputPathBaselineTests { val fixture = openInspectorAndSelectTarget(withManyChildren = false) val (_, ownerKey) = openDropdownFromVisibleSelectRow(fixture) - assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) + assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) assertFalse(fixture.inspector.hasOpenStyleDropdown()) assertFalse(fixture.inspector.handleOpenStyleDropdownWheel(-120)) @@ -128,7 +128,7 @@ class InspectorInputPathBaselineTests { assertTrue(dispatchSystemMouseWheel(fixture, wheelX, wheelY, -120)) syncAndRender(fixture, wheelX, wheelY) - assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) + assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) assertEquals(beforePanelScroll, fixture.inspector.panelScrollOffsetY) assertFalse(fixture.inspector.hasOpenStyleDropdown()) } @@ -194,7 +194,7 @@ class InspectorInputPathBaselineTests { val wheelY = contentRect.y + 10 assertTrue(dispatchSystemMouseWheel(fixture, wheelX, wheelY, -120)) syncAndRender(fixture, wheelX, wheelY) - assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) + assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) val panelRect = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") val outsideX = (panelRect.x - 12).coerceAtLeast(1) @@ -211,7 +211,7 @@ class InspectorInputPathBaselineTests { private fun openInspectorAndSelectTarget(withManyChildren: Boolean): Fixture { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRoot(withManyChildren) inspector.toggle() @@ -331,22 +331,22 @@ class InspectorInputPathBaselineTests { dispatchSystemMouseUp(fixture, clickX, clickY) syncAndRender(fixture, clickX, clickY) - assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) + assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) return triggerRect to ownerKey } private fun selectPanelRect(ownerKey: String, fixture: Fixture): Rect { - SelectRuntime.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) - return SelectRuntime.systemEngine.debugPanelRect(ownerKey) + SelectPortalServices.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) + return SelectPortalServices.systemEngine.debugPanelRect(ownerKey) ?: error("expected system select popup for owner=$ownerKey") } private fun dispatchSystemMouseDown(fixture: Fixture, x: Int, y: Int): Boolean = - SelectRuntime.systemEngine.handleMouseDown(x, y, MouseButton.LEFT) || + SelectPortalServices.systemEngine.handleMouseDown(x, y, MouseButton.LEFT) || fixture.host.handleMouseDown(x, y, MouseButton.LEFT) private fun dispatchSystemMouseUp(fixture: Fixture, x: Int, y: Int): Boolean = - SelectRuntime.systemEngine.handleMouseUp(x, y, MouseButton.LEFT) || + SelectPortalServices.systemEngine.handleMouseUp(x, y, MouseButton.LEFT) || fixture.host.handleMouseUp(x, y, MouseButton.LEFT) private fun dispatchSystemMouseWheel( @@ -355,7 +355,7 @@ class InspectorInputPathBaselineTests { y: Int, delta: Int, ): Boolean = - SelectRuntime.systemEngine.handleMouseWheel(x, y, delta) || + SelectPortalServices.systemEngine.handleMouseWheel(x, y, delta) || fixture.host.handleMouseWheel(x, y, delta) private fun waitForSystemSelectClosed( @@ -365,12 +365,12 @@ class InspectorInputPathBaselineTests { cursorY: Int, ) { repeat(30) { - if (!SelectRuntime.systemEngine.isOpenFor(ownerKey)) return + if (!SelectPortalServices.systemEngine.isOpenFor(ownerKey)) return Thread.sleep(5) syncAndRender(fixture, cursorX, cursorY) - SelectRuntime.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) + SelectPortalServices.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) } - assertFalse(SelectRuntime.systemEngine.isOpenFor(ownerKey)) + assertFalse(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) } private fun focusInputByClick(fixture: Fixture, input: TextInputNode): Pair { diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt index a7449b9..d433072 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt @@ -1,6 +1,6 @@ package org.dreamfinity.dsgl.core.overlay.system -import org.dreamfinity.dsgl.core.colorpicker.ColorPickerRuntime +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPortalServices import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Rect @@ -12,7 +12,7 @@ import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.inspector.InspectorEditorKind import org.dreamfinity.dsgl.core.inspector.InspectorStyleEditorRowSnapshot import org.dreamfinity.dsgl.core.render.RenderCommand -import org.dreamfinity.dsgl.core.select.SelectRuntime +import org.dreamfinity.dsgl.core.select.SelectPortalServices import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleProperty import kotlin.test.AfterTest @@ -34,8 +34,8 @@ class InspectorPointerAlignmentTests { fun cleanup() { FocusManager.clearFocus() KeyModifiers.sync(shift = false, control = false, meta = false) - ColorPickerRuntime.engine.closeAll() - SelectRuntime.host.closeAll() + ColorPickerPortalServices.engine.closeAll() + SelectPortalServices.closeAll() StyleEngine.clearAllInspectorOverrides() StyleEngine.clearCache() } @@ -122,7 +122,7 @@ class InspectorPointerAlignmentTests { val ownerKey = "dsgl-system-inspector-editor-select-$rowIndex" val property = row.property val triggerRect = openDropdownFromVisibleSelectRow(fixture, row) - assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) + assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) fixture.host.handleMouseDown( triggerRect.x + 2, @@ -167,7 +167,7 @@ class InspectorPointerAlignmentTests { assertTrue(dispatchSystemMouseWheel(fixture, wheelX, wheelY, -120)) syncAndRender(fixture, wheelX, wheelY) - assertTrue(SelectRuntime.systemEngine.isOpenFor(ownerKey)) + assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) val panelRect = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") val outsideX = (panelRect.x - 12).coerceAtLeast(1) @@ -183,7 +183,7 @@ class InspectorPointerAlignmentTests { private fun openInspectorAndSelectTarget(withManyChildren: Boolean): Fixture { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRoot(withManyChildren) inspector.toggle() @@ -303,17 +303,17 @@ class InspectorPointerAlignmentTests { } private fun selectPanelRect(ownerKey: String, fixture: Fixture): Rect { - SelectRuntime.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) - return SelectRuntime.systemEngine.debugPanelRect(ownerKey) + SelectPortalServices.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) + return SelectPortalServices.systemEngine.debugPanelRect(ownerKey) ?: error("expected system select popup for owner=$ownerKey") } private fun dispatchSystemMouseDown(fixture: Fixture, x: Int, y: Int): Boolean = - SelectRuntime.systemEngine.handleMouseDown(x, y, MouseButton.LEFT) || + SelectPortalServices.systemEngine.handleMouseDown(x, y, MouseButton.LEFT) || fixture.host.handleMouseDown(x, y, MouseButton.LEFT) private fun dispatchSystemMouseUp(fixture: Fixture, x: Int, y: Int): Boolean = - SelectRuntime.systemEngine.handleMouseUp(x, y, MouseButton.LEFT) || + SelectPortalServices.systemEngine.handleMouseUp(x, y, MouseButton.LEFT) || fixture.host.handleMouseUp(x, y, MouseButton.LEFT) private fun dispatchSystemMouseWheel( @@ -322,7 +322,7 @@ class InspectorPointerAlignmentTests { y: Int, delta: Int, ): Boolean = - SelectRuntime.systemEngine.handleMouseWheel(x, y, delta) || + SelectPortalServices.systemEngine.handleMouseWheel(x, y, delta) || fixture.host.handleMouseWheel(x, y, delta) private fun waitForSystemSelectClosed( @@ -332,12 +332,12 @@ class InspectorPointerAlignmentTests { cursorY: Int, ) { repeat(30) { - if (!SelectRuntime.systemEngine.isOpenFor(ownerKey)) return + if (!SelectPortalServices.systemEngine.isOpenFor(ownerKey)) return Thread.sleep(5) syncAndRender(fixture, cursorX, cursorY) - SelectRuntime.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) + SelectPortalServices.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) } - assertFalse(SelectRuntime.systemEngine.isOpenFor(ownerKey)) + assertFalse(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) } private fun findRowByProperty(fixture: Fixture, property: StyleProperty): InspectorStyleEditorRowSnapshot = diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorTextEditingDomMigrationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorTextEditingDomMigrationTests.kt index 970723b..c35e83f 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorTextEditingDomMigrationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorTextEditingDomMigrationTests.kt @@ -1,6 +1,6 @@ package org.dreamfinity.dsgl.core.overlay.system -import org.dreamfinity.dsgl.core.colorpicker.ColorPickerRuntime +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPortalServices import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode @@ -42,7 +42,7 @@ class InspectorTextEditingDomMigrationTests { FocusManager.clearFocus() KeyModifiers.sync(shift = false, control = false, meta = false) ClipboardBridge.install(null) - ColorPickerRuntime.engine.closeAll() + ColorPickerPortalServices.engine.closeAll() StyleEngine.clearAllInspectorOverrides() StyleEngine.clearCache() } @@ -212,7 +212,7 @@ class InspectorTextEditingDomMigrationTests { private fun openInspectorAndSelectTarget(): Fixture { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val (root, target) = inspectedRoot() inspector.toggle() diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt index 69e7ceb..a666862 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt @@ -2,7 +2,7 @@ package org.dreamfinity.dsgl.core.overlay.system import org.dreamfinity.dsgl.core.colorpicker.ColorFormatMode import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupRequest -import org.dreamfinity.dsgl.core.colorpicker.ColorPickerRuntime +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPortalServices import org.dreamfinity.dsgl.core.colorpicker.ColorPickerState import org.dreamfinity.dsgl.core.colorpicker.ColorPickerStyle import org.dreamfinity.dsgl.core.colorpicker.RgbChannelOrder @@ -39,11 +39,11 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker popup lifecycle is entry owned and stable`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPopupHost() + val pickerHost = host.systemInspectorColorPickerPortalService() val root = inspectedRoot() assertFalse(host.isSystemColorPickerOpen()) - assertFalse(ColorPickerRuntime.engine.isOpen()) + assertFalse(ColorPickerPortalServices.engine.isOpen()) pickerHost.open(anchorRect = Rect(40, 42, 20, 18), title = "Popup", state = popupState()) host.onInputFrame(960, 720) @@ -53,7 +53,7 @@ class SystemOverlayColorPickerEntryTests { assertEquals(OverlayOwnerScope.System, host.debugSystemColorPickerPopupOwnerScope()) assertTrue(firstState.active) assertNotNull(firstState.panelState.currentRectOrNull()) - assertFalse(ColorPickerRuntime.engine.isOpen()) + assertFalse(ColorPickerPortalServices.engine.isOpen()) host.onInputFrame(960, 720) host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 50, cursorY = 56, inspectorPointerCaptured = false) @@ -80,12 +80,12 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker entry path stays independent from application runtime popup path`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPopupHost() + val pickerHost = host.systemInspectorColorPickerPortalService() val root = inspectedRoot() val appOwner = Any() try { - ColorPickerRuntime.engine.open( + ColorPickerPortalServices.engine.open( ColorPickerPopupRequest( owner = appOwner, ownerScope = OverlayOwnerScope.Application, @@ -94,7 +94,7 @@ class SystemOverlayColorPickerEntryTests { state = popupState(), ), ) - assertTrue(ColorPickerRuntime.engine.isOpenFor(appOwner)) + assertTrue(ColorPickerPortalServices.engine.isOpenFor(appOwner)) pickerHost.open(anchorRect = Rect(40, 42, 20, 18), title = "Popup", state = popupState()) host.onInputFrame(960, 720) @@ -113,7 +113,7 @@ class SystemOverlayColorPickerEntryTests { assertFalse(styleTypes.contains("dsgl-system-raw-render-command")) assertEquals(OverlayOwnerScope.System, host.debugSystemColorPickerPopupOwnerScope()) assertTrue(host.isSystemColorPickerOpen()) - assertTrue(ColorPickerRuntime.engine.isOpenFor(appOwner)) + assertTrue(ColorPickerPortalServices.engine.isOpenFor(appOwner)) pickerHost.close() host.syncFrame( @@ -124,17 +124,17 @@ class SystemOverlayColorPickerEntryTests { inspectorPointerCaptured = false, ) assertFalse(host.isSystemColorPickerOpen()) - assertTrue(ColorPickerRuntime.engine.isOpenFor(appOwner)) + assertTrue(ColorPickerPortalServices.engine.isOpenFor(appOwner)) } finally { pickerHost.close() - ColorPickerRuntime.engine.close(appOwner) + ColorPickerPortalServices.engine.close(appOwner) } } @Test fun `system picker popup drag uses persistent entry drag session and keeps node stable`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPopupHost() + val pickerHost = host.systemInspectorColorPickerPortalService() val root = inspectedRoot() pickerHost.open(anchorRect = Rect(80, 90, 20, 18), title = "Popup", state = popupState()) @@ -196,7 +196,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker popup survives routine sync updates without remount during drag`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPopupHost() + val pickerHost = host.systemInspectorColorPickerPortalService() val root = inspectedRoot() pickerHost.open(anchorRect = Rect(120, 100, 20, 18), title = "Popup", state = popupState()) @@ -236,7 +236,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker popup close button closes entry through panel panel`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPopupHost() + val pickerHost = host.systemInspectorColorPickerPortalService() val root = inspectedRoot() pickerHost.open(anchorRect = Rect(80, 86, 20, 18), title = "Popup", state = popupState()) @@ -258,7 +258,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker keyboard-open path uses valid viewport after input frame sync`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPopupHost() + val pickerHost = host.systemInspectorColorPickerPortalService() val root = inspectedRoot() val anchor = Rect(360, 220, 1, 1) @@ -283,7 +283,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker entry mounts native body subtree without command bridge`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPopupHost() + val pickerHost = host.systemInspectorColorPickerPortalService() val root = inspectedRoot() pickerHost.open(anchorRect = Rect(60, 70, 20, 18), title = "Popup", state = popupState()) @@ -301,7 +301,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker color field drag updates color continuously`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPopupHost() + val pickerHost = host.systemInspectorColorPickerPortalService() val root = inspectedRoot() val previews = ArrayList() @@ -350,7 +350,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker current swatch click commits once without double apply`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPopupHost() + val pickerHost = host.systemInspectorColorPickerPortalService() val root = inspectedRoot() var commits = 0 @@ -379,7 +379,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker recent swatch click previews once without double apply`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPopupHost() + val pickerHost = host.systemInspectorColorPickerPortalService() val root = inspectedRoot() val initial = popupState() val previews = ArrayList() @@ -444,7 +444,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker sync state updates current swatch without drag nudge`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPopupHost() + val pickerHost = host.systemInspectorColorPickerPortalService() val root = inspectedRoot() val initial = popupState() val updated = @@ -474,7 +474,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker sync state updates rgb order button selected visuals without drag nudge`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPopupHost() + val pickerHost = host.systemInspectorColorPickerPortalService() val root = inspectedRoot() val style = ColorPickerStyle() val initial = popupState().copy(mode = ColorFormatMode.RGB, rgbOrder = RgbChannelOrder.RGBA) @@ -508,7 +508,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker hue and alpha drag update state`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPopupHost() + val pickerHost = host.systemInspectorColorPickerPortalService() val root = inspectedRoot() pickerHost.open(anchorRect = Rect(80, 90, 20, 18), title = "Popup", state = popupState()) @@ -556,7 +556,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker text input and mode controls stay synchronized`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPopupHost() + val pickerHost = host.systemInspectorColorPickerPortalService() val root = inspectedRoot() pickerHost.open(anchorRect = Rect(120, 120, 20, 18), title = "Popup", state = popupState()) @@ -616,7 +616,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker input focus retargets by semantic key across rgb order switch`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPopupHost() + val pickerHost = host.systemInspectorColorPickerPortalService() val root = inspectedRoot() pickerHost.open(anchorRect = Rect(120, 120, 20, 18), title = "Popup", state = popupState()) @@ -660,7 +660,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker rgb order buttons use dom semantic actions without double apply`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPopupHost() + val pickerHost = host.systemInspectorColorPickerPortalService() val root = inspectedRoot() var previews = 0 var commits = 0 @@ -708,7 +708,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker mode trigger toggles dropdown through dom path without double apply`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPopupHost() + val pickerHost = host.systemInspectorColorPickerPortalService() val root = inspectedRoot() pickerHost.open(anchorRect = Rect(120, 120, 20, 18), title = "Popup", state = popupState()) @@ -755,7 +755,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker mode option click changes mode and closes dropdown via dom path`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPopupHost() + val pickerHost = host.systemInspectorColorPickerPortalService() val root = inspectedRoot() var previews = 0 var commits = 0 @@ -810,7 +810,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker mode dropdown is mounted in transient lane and stays interactive`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPopupHost() + val pickerHost = host.systemInspectorColorPickerPortalService() val root = inspectedRoot() pickerHost.open(anchorRect = Rect(120, 120, 20, 18), title = "Popup", state = popupState()) @@ -862,7 +862,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker pipette keeps system overlay visible and uses transient lane`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPopupHost() + val pickerHost = host.systemInspectorColorPickerPortalService() val root = inspectedRoot() pickerHost.open(anchorRect = Rect(140, 140, 20, 18), title = "Popup", state = popupState()) @@ -909,7 +909,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker pipette transient entry emits visible tooltip commands`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPopupHost() + val pickerHost = host.systemInspectorColorPickerPortalService() val root = inspectedRoot() val gridColor = 0x7F4C93FF val checkerLight = 0x7F0AA0A0 diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntryInfrastructureTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntryInfrastructureTests.kt index d740afb..669b650 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntryInfrastructureTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntryInfrastructureTests.kt @@ -74,7 +74,7 @@ class SystemOverlayEntryInfrastructureTests { @Test fun `color picker entry keeps panel state and identity stable across routine updates`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPopupHost() + val pickerHost = host.systemInspectorColorPickerPortalService() val root = inspectedRoot() pickerHost.open(anchorRect = Rect(36, 44, 20, 18), title = "Popup", state = popupState()) try { @@ -121,7 +121,7 @@ class SystemOverlayEntryInfrastructureTests { fun `entry ordering stays explicit when both system entries are active`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - val pickerHost = host.systemInspectorColorPickerPopupHost() + val pickerHost = host.systemInspectorColorPickerPortalService() val root = inspectedRoot() inspector.toggle() pickerHost.open(anchorRect = Rect(36, 44, 20, 18), title = "Popup", state = popupState()) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt index 922401e..5851297 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt @@ -92,7 +92,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `live inspector path is native system-overlay entry and anti-legacy guarded`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRoot() inspector.toggle() @@ -118,7 +118,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `expanded inspector paints occluder above full highlight geometry`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val initialRoot = inspectedRoot() inspector.toggle() @@ -168,7 +168,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector runtime interaction path supports selection controls and system-owned color edit`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRoot() inspector.toggle() @@ -246,7 +246,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector minimize restore and close reopen remain stable`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRoot() inspector.toggle() @@ -362,7 +362,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector native path preserves scroll and scrollbar drag behavior`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -412,7 +412,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `scrollbar drag release over control ends capture and does not trigger control click`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -465,7 +465,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `scrollbar drag release outside inspector consumes mouse up and stops capture`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -529,11 +529,11 @@ class SystemOverlayInspectorNativeEntryTests { val appOwner = Any() val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRoot() try { - ColorPickerRuntime.engine.open( + ColorPickerPortalServices.engine.open( ColorPickerPopupRequest( owner = appOwner, ownerScope = OverlayOwnerScope.Application, @@ -542,7 +542,7 @@ class SystemOverlayInspectorNativeEntryTests { state = popupState(), ), ) - assertTrue(ColorPickerRuntime.engine.isOpenFor(appOwner)) + assertTrue(ColorPickerPortalServices.engine.isOpenFor(appOwner)) inspector.toggle() host.onInputFrame(1280, 720) @@ -586,9 +586,9 @@ class SystemOverlayInspectorNativeEntryTests { assertTrue(host.isSystemColorPickerOpen()) assertEquals(OverlayOwnerScope.System, host.debugSystemColorPickerPopupOwnerScope()) - assertTrue(ColorPickerRuntime.engine.isOpenFor(appOwner)) + assertTrue(ColorPickerPortalServices.engine.isOpenFor(appOwner)) - host.systemInspectorColorPickerPopupHost().close() + host.systemInspectorColorPickerPortalService().close() host.syncFrame( root, inspectedLayoutRevision = 4L, @@ -597,10 +597,10 @@ class SystemOverlayInspectorNativeEntryTests { inspectorPointerCaptured = false, ) assertFalse(host.isSystemColorPickerOpen()) - assertTrue(ColorPickerRuntime.engine.isOpenFor(appOwner)) + assertTrue(ColorPickerPortalServices.engine.isOpenFor(appOwner)) } finally { - host.systemInspectorColorPickerPopupHost().close() - ColorPickerRuntime.engine.close(appOwner) + host.systemInspectorColorPickerPortalService().close() + ColorPickerPortalServices.engine.close(appOwner) } } @@ -608,7 +608,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector-opened system color picker top controls expose hover feedback`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRoot() fun sync(revision: Long, cursorX: Int, cursorY: Int) { @@ -678,7 +678,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector-opened system color picker mode dropdown options hover and click reliably`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRoot() fun sync(revision: Long, cursorX: Int, cursorY: Int) { @@ -765,7 +765,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector native body content remains clipped in narrow viewport`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -853,7 +853,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector expanded body renders baseline info text`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRoot() inspector.toggle() @@ -886,7 +886,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector clipped body blocks hidden row input and accepts visible portion`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -992,7 +992,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector body consumes generic scroll viewport and content state`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -1038,7 +1038,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector wheel scrolling works when hovering interactive input`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -1102,7 +1102,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector shift wheel does not consume vertical wheel path`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -1145,7 +1145,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector wheel scrolling remains symmetric across rebuilds`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -1235,7 +1235,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector thumb drag remains active across rebuild without controller pointer capture`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -1308,7 +1308,7 @@ class SystemOverlayInspectorNativeEntryTests { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRoot() inspector.toggle() @@ -1440,7 +1440,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector consumer scroll reacts on frame update without viewport resize`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -1484,7 +1484,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector consumer thumb drag remains smooth and stable on release`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -1563,7 +1563,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector consumer fast thumb drag to boundary stays stable`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPopupHost()) + inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) val root = inspectedRootWithManyChildren() inspector.toggle() diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalServicesOwnershipTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalServicesOwnershipTests.kt new file mode 100644 index 0000000..8ca1d95 --- /dev/null +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalServicesOwnershipTests.kt @@ -0,0 +1,58 @@ +package org.dreamfinity.dsgl.core.select + +import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class SelectPortalServicesOwnershipTests { + @AfterTest + fun cleanup() { + SelectPortalServices.closeAll() + } + + @Test + fun `application-scoped request opens application engine only`() { + val owner = Any() + SelectPortalServices.open(request(owner, OverlayOwnerScope.Application)) + + assertTrue(SelectPortalServices.applicationEngine.isOpenFor(owner)) + assertFalse(SelectPortalServices.systemEngine.isOpenFor(owner)) + assertTrue(SelectPortalServices.isOpenFor(owner)) + } + + @Test + fun `system-scoped request opens system engine only`() { + val owner = Any() + SelectPortalServices.open(request(owner, OverlayOwnerScope.System)) + + assertFalse(SelectPortalServices.applicationEngine.isOpenFor(owner)) + assertTrue(SelectPortalServices.systemEngine.isOpenFor(owner)) + assertTrue(SelectPortalServices.isOpenFor(owner)) + } + + @Test + fun `opening same owner in another scope switches engine ownership`() { + val owner = Any() + SelectPortalServices.open(request(owner, OverlayOwnerScope.Application)) + assertTrue(SelectPortalServices.applicationEngine.isOpenFor(owner)) + assertFalse(SelectPortalServices.systemEngine.isOpenFor(owner)) + + SelectPortalServices.open(request(owner, OverlayOwnerScope.System)) + assertFalse(SelectPortalServices.applicationEngine.isOpenFor(owner)) + assertTrue(SelectPortalServices.systemEngine.isOpenFor(owner)) + } + + private fun request(owner: Any, scope: OverlayOwnerScope): SelectOpenRequest = + SelectOpenRequest( + owner = owner, + modelToken = 1L, + entries = listOf(SelectEntry.Option("a", labelProvider = { "Alpha" })), + selectedId = "a", + anchorRect = Rect(10, 10, 100, 20), + closeOnSelect = true, + ownerScope = scope, + ) +} diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntimeOwnershipBridgeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntimeOwnershipBridgeTests.kt deleted file mode 100644 index 26c01d7..0000000 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectRuntimeOwnershipBridgeTests.kt +++ /dev/null @@ -1,58 +0,0 @@ -package org.dreamfinity.dsgl.core.select - -import org.dreamfinity.dsgl.core.dom.layout.Rect -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class SelectRuntimeOwnershipBridgeTests { - @AfterTest - fun cleanup() { - SelectRuntime.host.closeAll() - } - - @Test - fun `application-scoped request opens application engine only`() { - val owner = Any() - SelectRuntime.host.open(request(owner, OverlayOwnerScope.Application)) - - assertTrue(SelectRuntime.applicationEngine.isOpenFor(owner)) - assertFalse(SelectRuntime.systemEngine.isOpenFor(owner)) - assertTrue(SelectRuntime.host.isOpenFor(owner)) - } - - @Test - fun `system-scoped request opens system engine only`() { - val owner = Any() - SelectRuntime.host.open(request(owner, OverlayOwnerScope.System)) - - assertFalse(SelectRuntime.applicationEngine.isOpenFor(owner)) - assertTrue(SelectRuntime.systemEngine.isOpenFor(owner)) - assertTrue(SelectRuntime.host.isOpenFor(owner)) - } - - @Test - fun `opening same owner in another scope switches engine ownership`() { - val owner = Any() - SelectRuntime.host.open(request(owner, OverlayOwnerScope.Application)) - assertTrue(SelectRuntime.applicationEngine.isOpenFor(owner)) - assertFalse(SelectRuntime.systemEngine.isOpenFor(owner)) - - SelectRuntime.host.open(request(owner, OverlayOwnerScope.System)) - assertFalse(SelectRuntime.applicationEngine.isOpenFor(owner)) - assertTrue(SelectRuntime.systemEngine.isOpenFor(owner)) - } - - private fun request(owner: Any, scope: OverlayOwnerScope): SelectOpenRequest = - SelectOpenRequest( - owner = owner, - modelToken = 1L, - entries = listOf(SelectEntry.Option("a", labelProvider = { "Alpha" })), - selectedId = "a", - anchorRect = Rect(10, 10, 100, 20), - closeOnSelect = true, - ownerScope = scope, - ) -} diff --git a/docs/cookbook.md b/docs/cookbook.md index c1f2039..ad01a37 100644 --- a/docs/cookbook.md +++ b/docs/cookbook.md @@ -1,11 +1,11 @@ # Cookbook A set of practical DSGL patterns that are already used in this repository (mostly in `adapters/mc-forge-1-7-10/demo`). -It is not a generic UI cookbook; every recipe here maps to existing runtime / demo behaviour. +It is not a generic UI cookbook; every recipe here maps to existing portal/runtime and demo behaviour. ## Recipe 1: State-driven modal stack -Use a window/component state list of `ModalSpec`, and render it through `modalHost`. +Use a window/component state list of `ModalSpec`, and render it through `modalPortal`. Topmost modal is the last item in the list. ```kotlin { .kotlin .copy .select } @@ -29,7 +29,7 @@ private fun UiScope.modalStackRecipe() { modals = modals.filterNot { it.key == key } } - modalHost(modals = modals, modalKey = "recipe.modal.host") { + modalPortal(modals = modals, key = "recipe.modal.portal") { button("Open modal", { onMouseClick = { modals = modals + ModalSpec( @@ -63,7 +63,7 @@ Why this pattern: ## Recipe 2: Attach a context menu to a node Use `DOMNode.onContextMenu { ... }` and build a model with `contextMenu { ... }`. -The current runtime opens it from the right mouse down and consumes that event path. +The application portal service opens it from the right mouse down and consumes that event path. ```kotlin { .kotlin .copy .select } import org.dreamfinity.dsgl.core.DsglWindow diff --git a/docs/elements-overview.md b/docs/elements-overview.md index 7cb9bf3..06461dc 100644 --- a/docs/elements-overview.md +++ b/docs/elements-overview.md @@ -240,7 +240,8 @@ select({ Caveats: -- popup behaviour is provided by select runtime/overlay internals; this API is the supported convenience entrypoint. +- popup behaviour is provided by domain portal services; this API is the supported convenience entrypoint. +- use `ownerScope = OverlayOwnerScope.System` when a select is hosted by system-owned UI (for example inspector/system tools). - keyboard and wheel behaviour are implemented and covered by `SelectEngineTests`. ### `colorPicker(...)` and `colorPickerPopup(...)` @@ -280,7 +281,7 @@ framework contract. Public helper set: -- `modalHost(modals, modalKey) { ... }` +- `modalPortal(modals, key) { ... }` - `ModalSpec(...)` - `modalFrame`, `modalDialog`, `modalHeader`, `modalTitle`, `modalBody`, `modalFooter` - `alertModal`, `confirmModal`, `promptModal` @@ -291,7 +292,7 @@ Small example: fun UiScope.modalSample() { var modals by useState(emptyList()) - modalHost(modals = modals, modalKey = "example.modal.host") { + modalPortal(modals = modals, key = "example.modal.portal") { button("Open modal", { onMouseClick = { modals = listOf( @@ -311,7 +312,7 @@ fun UiScope.modalSample() { Caveat: -- modal focus restore/trap/topmost handling is runtime-managed and tested (`ModalRuntimeTests`). +- modal focus restore/trap/topmost handling is portal-session-managed and tested (`ModalPortalSessionStoreTests`). ### Context menu helpers From c1e208ac54a50261d6e3b0e46a42b2d4c0e4b0a8 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Tue, 12 May 2026 00:47:11 +0300 Subject: [PATCH 63/78] partially moving from layers and many-hosts to a domain model + portalsHost; --- .../demo/sections/ContextMenuSection.kt | 4 +- .../demo/sections/InputsGallerySection.kt | 4 +- .../dsgl/mcForge1710/DsglScreenHost.kt | 2 +- core/detekt-baseline.xml | 26 ++-- ...upRuntime.kt => ColorPickerPopupEngine.kt} | 17 ++- .../internal/SystemColorPickerPanelManager.kt | 4 +- .../core/contextmenu/ContextMenuEngine.kt | 2 +- ...enuHost.kt => ContextMenuPortalRequest.kt} | 10 +- .../contextmenu/ContextMenuPortalServices.kt | 5 - .../dsgl/core/dom/ContextMenuEvents.kt | 12 +- .../dom/elements/ColorPickerPopupPaneNode.kt | 13 +- .../dsgl/core/dom/elements/SelectNode.kt | 30 ++--- .../core/inspector/InspectorController.kt | 12 +- .../internal/SystemInspectorOverlayNode.kt | 4 +- .../core/overlay/ApplicationOverlayHost.kt | 9 +- .../dsgl/core/overlay/DomainPortalServices.kt | 41 +++++++ .../overlay/system/SystemOverlayEntries.kt | 2 +- .../core/overlay/system/SystemOverlayHost.kt | 18 +-- .../dsgl/core/select/SelectEngine.kt | 12 +- .../{SelectHost.kt => SelectPortalRequest.kt} | 12 -- .../dsgl/core/select/SelectPortalServices.kt | 36 ------ .../ColorPickerPopupEngineTests.kt | 26 ++-- .../dsgl/core/dom/ContextMenuEventsTests.kt | 20 ++-- .../core/dom/SelectNodeOwnerScopeTests.kt | 8 +- .../dom/SelectPopupAnchoringStickyTests.kt | 14 +-- .../inspector/InspectorControllerTests.kt | 16 +-- .../overlay/LiveLayerInteractionPathTests.kt | 80 ++++++------- .../InspectorDragScrollDomMigrationTests.kt | 13 +- .../InspectorDropdownCorrectiveTests.kt | 31 +++-- .../system/InspectorInputPathBaselineTests.kt | 45 ++++--- .../system/InspectorPointerAlignmentTests.kt | 29 +++-- .../InspectorTextEditingDomMigrationTests.kt | 6 +- .../SystemOverlayColorPickerEntryTests.kt | 112 +++++++++--------- .../SystemOverlayEntryInfrastructureTests.kt | 12 +- .../SystemOverlayInspectorNativeEntryTests.kt | 59 ++++----- .../DomainPortalServicesOwnershipTests.kt | 59 +++++++++ .../SelectPortalServicesOwnershipTests.kt | 58 --------- 37 files changed, 421 insertions(+), 442 deletions(-) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/{ColorPickerPopupRuntime.kt => ColorPickerPopupEngine.kt} (98%) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/{ContextMenuHost.kt => ContextMenuPortalRequest.kt} (81%) delete mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuPortalServices.kt create mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/DomainPortalServices.kt rename core/src/main/kotlin/org/dreamfinity/dsgl/core/select/{SelectHost.kt => SelectPortalRequest.kt} (80%) delete mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalServices.kt create mode 100644 core/src/test/kotlin/org/dreamfinity/dsgl/core/select/DomainPortalServicesOwnershipTests.kt delete mode 100644 core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalServicesOwnershipTests.kt diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ContextMenuSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ContextMenuSection.kt index 7c8f84e..18252bc 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ContextMenuSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ContextMenuSection.kt @@ -1,7 +1,6 @@ package org.dreamfinity.dsgl.mcForge1710.demo.sections -import org.dreamfinity.dsgl.core.contextmenu.ContextMenuPortalServices import org.dreamfinity.dsgl.core.contextmenu.ContextMenuStyle import org.dreamfinity.dsgl.core.contextmenu.contextMenu import org.dreamfinity.dsgl.core.dnd.* @@ -11,6 +10,7 @@ import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.hooks.useState +import org.dreamfinity.dsgl.core.overlay.DomainPortalServices import org.dreamfinity.dsgl.core.style.AlignItems import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection @@ -753,7 +753,7 @@ fun UiScope.contextMenuSection(onInfo: (String) -> Unit) { } val entries = contextMenuVisibleFiles() - ContextMenuPortalServices.engine.setStyle( + DomainPortalServices.applicationContextMenuEngine.setStyle( ContextMenuStyle( panelPaddingX = 4, panelPaddingY = 4, diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt index 3e6c70d..92057ae 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt @@ -5,7 +5,7 @@ import org.dreamfinity.dsgl.core.dom.elements.InputType import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.hooks.useState -import org.dreamfinity.dsgl.core.select.SelectPortalServices +import org.dreamfinity.dsgl.core.overlay.DomainPortalServices import org.dreamfinity.dsgl.core.select.SelectStyle import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection @@ -63,7 +63,7 @@ fun UiScope.inputsGallerySection(clippingScrollDemoText: String, onClippingScrol .sorted() .joinToString(",") - SelectPortalServices.engine.setStyle( + DomainPortalServices.applicationSelectEngine.setStyle( SelectStyle( panelBackgroundColor = 0xFF202A35.toInt(), panelBorderColor = 0xFF607286.toInt(), diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index 3c67ccd..de7db14 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -1325,7 +1325,7 @@ abstract class DsglScreenHost( } init { - inspector.installColorPickerHost(systemOverlayHost.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(systemOverlayHost.systemInspectorColorPickerService()) } private fun refreshActiveColorSamplerOwner(root: DOMNode?) { diff --git a/core/detekt-baseline.xml b/core/detekt-baseline.xml index aa88a71..a2c2ef9 100644 --- a/core/detekt-baseline.xml +++ b/core/detekt-baseline.xml @@ -3,7 +3,7 @@ ComplexCondition:ColorPickerInlineNode.kt$ColorPickerInlineNode$!force && currentArgb == syncedColorArgb && previousArgb == syncedPreviousArgb && mode == syncedMode && alphaEnabled == syncedAlphaEnabled && closeOnSelect == syncedCloseOnSelect - ComplexCondition:ColorPickerPopupRuntime.kt$ColorPickerPopupEngine$previous.anchorRect != request.anchorRect || previous.width != request.width || previous.style != request.style || previous.state.alphaEnabled != request.state.alphaEnabled + ComplexCondition:ColorPickerPopupEngine.kt$ColorPickerPopupEngine$previous.anchorRect != request.anchorRect || previous.width != request.width || previous.style != request.style || previous.state.alphaEnabled != request.state.alphaEnabled ComplexCondition:DOMNode.kt$DOMNode$border.top <= 0 && border.right <= 0 && border.bottom <= 0 && border.left <= 0 ComplexCondition:DOMNode.kt$DOMNode$relativeOffsetX == 0 && relativeOffsetY == 0 && stickyOffsetX == 0 && stickyOffsetY == 0 ComplexCondition:DomTree.kt$DomTree$( !laidOut || styleReport.layoutDirty || scrollInvalidation.layoutDirty || requiresSystemOverlayScrollLayoutFallback ) && lastWidth > 0 && lastHeight > 0 @@ -81,13 +81,13 @@ LargeClass:ColorPickerPopupEngineTests.kt$ColorPickerPopupEngineTests LargeClass:ComponentHookRuntime.kt$ComponentHookRuntime LargeClass:ContainerNode.kt$ContainerNode : DOMNode - LargeClass:ContextMenuEngine.kt$ContextMenuEngine : ContextMenuHost + LargeClass:ContextMenuEngine.kt$ContextMenuEngine : ContextMenuPortalService LargeClass:DOMNode.kt$DOMNode LargeClass:DefaultDndEngine.kt$DefaultDndEngine : DndEngine LargeClass:FontRegistry.kt$FontRegistry LargeClass:InspectorController.kt$InspectorController LargeClass:PositionedLayoutStickyBehaviorTests.kt$PositionedLayoutStickyBehaviorTests - LargeClass:SelectEngine.kt$SelectEngine : SelectHost + LargeClass:SelectEngine.kt$SelectEngine LargeClass:StyleEngine.kt$StyleEngine LargeClass:StyleScope.kt$StyleScope : CssLengthUnitsDsl LargeClass:SystemColorPickerPopupBodyNode.kt$SystemColorPickerPopupBodyNode : DOMNode @@ -131,14 +131,14 @@ LongMethod:TextPerformanceHotPathCharacterizationTests.kt$TextPerformanceHotPathCharacterizationTests$@Test fun `cache boundaries are explicit for wrapped layout paths`() LongParameterList:ColorPickerInlineNode.kt$ColorPickerInlineNode$( controlled: Boolean = false, value: RgbaColor? = null, defaultValue: RgbaColor = RgbaColor.WHITE, previousValue: RgbaColor? = null, mode: ColorFormatMode = ColorFormatMode.HEX, alphaEnabled: Boolean = true, key: Any? = null, ) LongParameterList:ColorPickerPopupPaneNode.kt$ColorPickerPopupPaneNode$( controlled: Boolean = false, value: RgbaColor? = null, defaultValue: RgbaColor = RgbaColor.WHITE, previousValue: RgbaColor? = null, mode: ColorFormatMode = ColorFormatMode.HEX, alphaEnabled: Boolean = true, key: Any? = null, ) - LongParameterList:ColorPickerPopupRuntime.kt$ColorPickerPopupManager$( ownerScope: OverlayOwnerScope = OverlayOwnerScope.Application, anchorRect: Rect, title: String, state: ColorPickerState, style: ColorPickerStyle = ColorPickerStyle(), width: Int = 320, draggable: Boolean = true, closeOnOutsideClick: Boolean = false, onPreview: ((RgbaColor) -> Unit)? = null, onChange: ((RgbaColor) -> Unit)? = null, onCommit: ((RgbaColor) -> Unit)? = null, onClose: (() -> Unit)? = null, ) + LongParameterList:ColorPickerPopupEngine.kt$ColorPickerPopupManager$( ownerScope: OverlayOwnerScope = OverlayOwnerScope.Application, anchorRect: Rect, title: String, state: ColorPickerState, style: ColorPickerStyle = ColorPickerStyle(), width: Int = 320, draggable: Boolean = true, closeOnOutsideClick: Boolean = false, onPreview: ((RgbaColor) -> Unit)? = null, onChange: ((RgbaColor) -> Unit)? = null, onCommit: ((RgbaColor) -> Unit)? = null, onClose: (() -> Unit)? = null, ) LongParameterList:ComponentProps.kt$ComponentProps$( var style: StyleScope.() -> Unit = {}, var key: Any? = null, var id: String? = null, var className: String = "", var classes: Set<String> = emptySet(), var disabled: Boolean = false, var draggable: Boolean = false, var droppable: Boolean = false, var dragPreviewMode: DragPreviewMode = DragPreviewMode.GHOST, var hideSourceWhileDragging: Boolean = false, var dragPreview: (DragPreviewScope.() -> Unit)? = null, var dragPlaceholder: (PlaceholderScope.() -> Unit)? = null, var ref: RefTarget<ElementHandle>? = null, var onMouseEnter: ((MouseEnterEvent) -> Unit)? = null, var onMouseLeave: ((MouseLeaveEvent) -> Unit)? = null, var onMouseOver: ((MouseOverEvent) -> Unit)? = null, var onMouseMove: ((MouseMoveEvent) -> Unit)? = null, var onMouseDown: ((MouseDownEvent) -> Unit)? = null, var onMouseUp: ((MouseUpEvent) -> Unit)? = null, var onMouseClick: ((MouseClickEvent) -> Unit)? = null, var onMouseDrag: ((MouseDragEvent) -> Unit)? = null, var onMouseWheel: ((MouseWheelEvent) -> Unit)? = null, var onKeyDown: ((KeyboardKeyDownEvent) -> Unit)? = null, var onKeyUp: ((KeyboardKeyUpEvent) -> Unit)? = null, var onKeyPressed: ((KeyboardKeyDownEvent) -> Unit)? = null, var onKeyReleased: ((KeyboardKeyUpEvent) -> Unit)? = null, var onFocusGain: ((FocusGainEvent) -> Unit)? = null, var onFocusLose: ((FocusLoseEvent) -> Unit)? = null, var onInput: ((InputEvent) -> Unit)? = null, var onValueChange: ((ValueChangedEvent) -> Unit)? = null, var onDragStart: ((DragStartEvent) -> Unit)? = null, var onDrag: ((DragEvent) -> Unit)? = null, var onDragEnd: ((DragEndEvent) -> Unit)? = null, var onDragEnter: ((DragEnterEvent) -> Unit)? = null, var onDragOver: ((DragOverEvent) -> Unit)? = null, var onDragLeave: ((DragLeaveEvent) -> Unit)? = null, var onDrop: ((DropEvent) -> Unit)? = null, ) LongParameterList:ContainerNode.kt$ContainerNode$( ctx: UiMeasureContext, child: DOMNode, parentContentX: Int, parentContentY: Int, parentContentWidth: Int, parentContentHeight: Int, desiredX: Int, desiredY: Int, desiredWidth: Int, desiredHeight: Int, ) LongParameterList:DndHooks.kt$( id: String, nodeKey: Any = id, type: String = "default", data: Any? = null, previewMode: DragPreviewMode = DragPreviewMode.GHOST, hideSourceWhileDragging: Boolean = false, renderPreview: (DragPreviewScope.() -> Unit)? = null, renderPlaceholder: (PlaceholderScope.() -> Unit)? = null, onDragStart: ((DragStartEvent) -> Unit)? = null, onDrag: ((DragEvent) -> Unit)? = null, onDragEnd: ((DragEndEvent) -> Unit)? = null, ) LongParameterList:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$( x: Int, y: Int, width: Int, options: List<String>, property: StyleProperty, unitSelect: Boolean, pointerProjectionScrollY: Int, rowHeightPx: Int, viewportWidth: Int, viewportHeight: Int, mouseX: Int, mouseY: Int, currentScrollIndex: Int, ) LongParameterList:InspectorStyleEditorSnapshotBuilderTests.kt$InspectorStyleEditorSnapshotBuilderTests$( selected: ContainerNode, inspection: org.dreamfinity.dsgl.core.style.StyleInspection, panelRect: Rect = Rect(20, 20, 360, 260), editableProperties: List<StyleProperty>, pointerProjectionScrollY: Int = 0, mouseX: Int = 180, mouseY: Int = 120, openValueSelectProperty: StyleProperty? = null, openUnitSelectProperty: StyleProperty? = null, openValueSelectScrollIndex: Int = 0, openUnitSelectScrollIndex: Int = 0, ) LongParameterList:SelectNode.kt$SelectNode$( model: SelectModel, controlled: Boolean = false, value: String? = null, defaultValue: String? = null, closeOnSelect: Boolean = true, ownerScope: OverlayOwnerScope = OverlayOwnerScope.Application, key: Any? = null, ) - LongParameterList:SystemColorPickerPanelManager.kt$InspectorColorPickerHost$( anchorRect: Rect, title: String, state: ColorPickerState, style: ColorPickerStyle = ColorPickerStyle(), width: Int = 320, draggable: Boolean = true, closeOnOutsideClick: Boolean = false, onPreview: ((RgbaColor) -> Unit)? = null, onChange: ((RgbaColor) -> Unit)? = null, onCommit: ((RgbaColor) -> Unit)? = null, onClose: (() -> Unit)? = null, ) + LongParameterList:SystemColorPickerPanelManager.kt$SystemColorPickerPortalService$( anchorRect: Rect, title: String, state: ColorPickerState, style: ColorPickerStyle = ColorPickerStyle(), width: Int = 320, draggable: Boolean = true, closeOnOutsideClick: Boolean = false, onPreview: ((RgbaColor) -> Unit)? = null, onChange: ((RgbaColor) -> Unit)? = null, onCommit: ((RgbaColor) -> Unit)? = null, onClose: (() -> Unit)? = null, ) MagicNumber:AnimationModel.kt$ColorAnimatable$0xFF MagicNumber:AnimationModel.kt$ColorAnimatable$24 MagicNumber:AnimationModel.kt$TransformAnimatable$180f @@ -163,9 +163,9 @@ MagicNumber:ColorPickerPopupPaneNode.kt$ColorPickerPopupPaneNode$120 MagicNumber:ColorPickerPopupPaneNode.kt$ColorPickerPopupPaneNode$24 MagicNumber:ColorPickerPopupPaneNode.kt$ColorPickerPopupPaneNode$42 - MagicNumber:ColorPickerPopupRuntime.kt$ColorPickerPopupEngine$18 - MagicNumber:ColorPickerPopupRuntime.kt$ColorPickerPopupEngine$20 - MagicNumber:ColorPickerPopupRuntime.kt$ColorPickerPopupEngine$220 + MagicNumber:ColorPickerPopupEngine.kt$ColorPickerPopupEngine$18 + MagicNumber:ColorPickerPopupEngine.kt$ColorPickerPopupEngine$20 + MagicNumber:ColorPickerPopupEngine.kt$ColorPickerPopupEngine$220 MagicNumber:ColorTextCodec.kt$ColorTextCodec$0.5f MagicNumber:ColorTextCodec.kt$ColorTextCodec$0xFF MagicNumber:ColorTextCodec.kt$ColorTextCodec$1000f @@ -394,7 +394,7 @@ ReturnCount:ColorPickerController.kt$ColorPickerController$fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean ReturnCount:ColorPickerController.kt$ColorPickerController$fun handleMouseDown( globalX: Int, globalY: Int, button: MouseButton, layout: ColorPickerLayout, ): Boolean ReturnCount:ColorPickerController.kt$ColorPickerController$private fun applyInputDraftValue(key: String, rawValue: String): Boolean - ReturnCount:ColorPickerPopupRuntime.kt$ColorPickerPopupEngine$fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean + ReturnCount:ColorPickerPopupEngine.kt$ColorPickerPopupEngine$fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean ReturnCount:ColorTextCodec.kt$ColorTextCodec$private fun parseHsbLike(raw: String): RgbaColor? ReturnCount:ColorTextCodec.kt$ColorTextCodec$private fun parseHslLike(raw: String): RgbaColor? ReturnCount:ColorTextCodec.kt$ColorTextCodec$private fun parseRgbLike(raw: String): ParsedColorText? @@ -419,12 +419,12 @@ TooManyFunctions:ColorPickerController.kt$ColorPickerController TooManyFunctions:ColorPickerInlineNode.kt$ColorPickerInlineNode : DOMNode TooManyFunctions:ColorPickerPopupPaneNode.kt$ColorPickerPopupPaneNode : DOMNode - TooManyFunctions:ColorPickerPopupRuntime.kt$ColorPickerPopupEngine : ColorPickerPopupHost + TooManyFunctions:ColorPickerPopupEngine.kt$ColorPickerPopupEngine : ColorPickerPopupPortalService TooManyFunctions:ColorTextCodec.kt$ColorTextCodec TooManyFunctions:ComponentHookRuntime.kt$ComponentHookRuntime TooManyFunctions:ContainerNode.kt$ContainerNode : DOMNode TooManyFunctions:ContextMenuDsl.kt$ContextMenuSubmenuBuilder - TooManyFunctions:ContextMenuEngine.kt$ContextMenuEngine : ContextMenuHost + TooManyFunctions:ContextMenuEngine.kt$ContextMenuEngine : ContextMenuPortalService TooManyFunctions:DOMNode.kt$DOMNode TooManyFunctions:DefaultDndEngine.kt$DefaultDndEngine : DndEngine TooManyFunctions:DomTree.kt$DomTree @@ -443,7 +443,7 @@ TooManyFunctions:RadioGroupNode.kt$RadioGroupNode : DOMNode TooManyFunctions:RangeInputNode.kt$RangeInputNode : DOMNode TooManyFunctions:ScrollPerformanceCounters.kt$ScrollPerformanceCounters - TooManyFunctions:SelectEngine.kt$SelectEngine : SelectHost + TooManyFunctions:SelectEngine.kt$SelectEngine TooManyFunctions:SelectNode.kt$SelectNode : DOMNode TooManyFunctions:SingleLineInputNode.kt$SingleLineInputNode : DOMNode TooManyFunctions:StyleAnimationEngine.kt$StyleAnimationEngine @@ -454,7 +454,7 @@ TooManyFunctions:SystemColorPickerPopupBodyNode.kt$SystemColorPickerPopupBodyNode : DOMNode TooManyFunctions:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode : DOMNode TooManyFunctions:SystemOverlayHost.kt$SystemOverlayHost : OverlayLayerHost - TooManyFunctions:SystemOverlayHost.kt$SystemOverlayHost$ColorPickerOverlayEntry : SystemOverlayEntryInspectorColorPickerHost + TooManyFunctions:SystemOverlayHost.kt$SystemOverlayHost$ColorPickerOverlayEntry : SystemOverlayEntrySystemColorPickerPortalService TooManyFunctions:TextAreaNode.kt$TextAreaNode : DOMNode TooManyFunctions:ToggleNode.kt$ToggleNode : DOMNode diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngine.kt similarity index 98% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngine.kt index 360932b..0a04a4e 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupRuntime.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngine.kt @@ -4,11 +4,12 @@ import org.dreamfinity.dsgl.core.colorpicker.internal.ColorPickerDebugCounters import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.overlay.DomainPortalServices import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.popup.FloatingPaneDragModel import org.dreamfinity.dsgl.core.render.RenderCommand -interface ColorPickerPopupHost { +interface ColorPickerPopupPortalService { fun open(request: ColorPickerPopupRequest) fun close(owner: Any) @@ -36,7 +37,7 @@ data class ColorPickerPopupRequest( val onClose: (() -> Unit)? = null, ) -class ColorPickerPopupEngine : ColorPickerPopupHost { +class ColorPickerPopupEngine : ColorPickerPopupPortalService { private data class LayoutDirtyKey( val bodyRect: Rect, val mode: ColorFormatMode, @@ -646,7 +647,7 @@ class ColorPickerPopupEngine : ColorPickerPopupHost { } class ColorPickerPopupManager( - private val host: ColorPickerPopupHost = ColorPickerPortalServices.engine, + private val portalService: ColorPickerPopupPortalService = DomainPortalServices.applicationColorPickerEngine, private val ownerToken: Any = Any(), ) { fun open( @@ -663,7 +664,7 @@ class ColorPickerPopupManager( onCommit: ((RgbaColor) -> Unit)? = null, onClose: (() -> Unit)? = null, ) { - host.open( + portalService.open( ColorPickerPopupRequest( owner = ownerToken, ownerScope = ownerScope, @@ -683,12 +684,8 @@ class ColorPickerPopupManager( } fun close() { - host.close(ownerToken) + portalService.close(ownerToken) } - fun isOpen(): Boolean = host.isOpenFor(ownerToken) -} - -object ColorPickerPortalServices { - val engine: ColorPickerPopupEngine = ColorPickerPopupEngine() + fun isOpen(): Boolean = portalService.isOpenFor(ownerToken) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPanelManager.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPanelManager.kt index 5edc5e5..49f12be 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPanelManager.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPanelManager.kt @@ -7,7 +7,7 @@ import org.dreamfinity.dsgl.core.colorpicker.RgbaColor import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope -interface InspectorColorPickerHost { +interface SystemColorPickerPortalService { fun open( anchorRect: Rect, title: String, @@ -29,7 +29,7 @@ interface InspectorColorPickerHost { internal class SystemColorPickerPanelManager( private val delegate: ColorPickerPopupManager = ColorPickerPopupManager(), -) : InspectorColorPickerHost { +) : SystemColorPickerPortalService { override fun open( anchorRect: Rect, title: String, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngine.kt index af1cc3e..a1e6691 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngine.kt @@ -8,7 +8,7 @@ import org.dreamfinity.dsgl.core.style.* class ContextMenuEngine( private val clock: ContextMenuClock = SystemContextMenuClock, private val measurementCache: ContextMenuMeasurementCache = ContextMenuMeasurementCache(), -) : ContextMenuHost { +) : ContextMenuPortalService { private data class OpenLevel( val token: Long, val entries: List, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuPortalRequest.kt similarity index 81% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuHost.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuPortalRequest.kt index 29865e1..c12f453 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuPortalRequest.kt @@ -2,7 +2,7 @@ package org.dreamfinity.dsgl.core.contextmenu import org.dreamfinity.dsgl.core.dom.layout.Rect -interface ContextMenuHost { +interface ContextMenuPortalService { fun openAtCursor(model: ContextMenuModel, x: Int, y: Int) fun openAnchored(model: ContextMenuModel, anchorRect: Rect) @@ -26,18 +26,18 @@ data class ContextMenuTriggerScope( val anchorRect: Rect?, private val inheritedFontId: String?, private val inheritedFontSize: Int?, - private val host: ContextMenuHost, + private val portalService: ContextMenuPortalService, ) { fun openMenu(model: ContextMenuModel) { - host.openAtCursor(resolveModel(model), mouseX, mouseY) + portalService.openAtCursor(resolveModel(model), mouseX, mouseY) } fun openMenuAnchored(model: ContextMenuModel, anchor: Rect = anchorRect ?: Rect(mouseX, mouseY, 0, 0)) { - host.openAnchored(resolveModel(model), anchor) + portalService.openAnchored(resolveModel(model), anchor) } fun closeMenus() { - host.closeAll() + portalService.closeAll() } private fun resolveModel(model: ContextMenuModel): ContextMenuModel { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuPortalServices.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuPortalServices.kt deleted file mode 100644 index 65e044b..0000000 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuPortalServices.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.dreamfinity.dsgl.core.contextmenu - -object ContextMenuPortalServices { - val engine: ContextMenuEngine = ContextMenuEngine() -} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEvents.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEvents.kt index e11b2d1..c9b8e19 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEvents.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEvents.kt @@ -1,13 +1,13 @@ package org.dreamfinity.dsgl.core.dom -import org.dreamfinity.dsgl.core.contextmenu.ContextMenuHost import org.dreamfinity.dsgl.core.contextmenu.ContextMenuModel -import org.dreamfinity.dsgl.core.contextmenu.ContextMenuPortalServices +import org.dreamfinity.dsgl.core.contextmenu.ContextMenuPortalService import org.dreamfinity.dsgl.core.contextmenu.ContextMenuTriggerScope import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.overlay.DomainPortalServices fun DOMNode.onContextMenu( - host: ContextMenuHost = ContextMenuPortalServices.engine, + portalService: ContextMenuPortalService = DomainPortalServices.applicationContextMenuEngine, handler: ContextMenuTriggerScope.() -> Unit, ) { val previous = onMouseDown @@ -24,7 +24,7 @@ fun DOMNode.onContextMenu( anchorRect = anchor, inheritedFontId = sourceStyle?.fontId ?: sourceNode.fontId, inheritedFontSize = sourceStyle?.fontSize ?: sourceNode.fontSize, - host = host, + portalService = portalService, ), ) event.cancelled = true @@ -33,8 +33,8 @@ fun DOMNode.onContextMenu( } fun DOMNode.onContextMenuModel( - host: ContextMenuHost = ContextMenuPortalServices.engine, + portalService: ContextMenuPortalService = DomainPortalServices.applicationContextMenuEngine, modelProvider: () -> ContextMenuModel, ) { - onContextMenu(host = host, handler = { openMenu(modelProvider()) }) + onContextMenu(portalService = portalService, handler = { openMenu(modelProvider()) }) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerPopupPaneNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerPopupPaneNode.kt index 6d12db5..319deb7 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerPopupPaneNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerPopupPaneNode.kt @@ -7,6 +7,7 @@ import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.Size import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.* +import org.dreamfinity.dsgl.core.overlay.DomainPortalServices import org.dreamfinity.dsgl.core.render.RenderCommand class ColorPickerPopupPaneNode( @@ -58,8 +59,8 @@ class ColorPickerPopupPaneNode( return@addEventListener } FocusManager.requestFocus(this@ColorPickerPopupPaneNode) - if (ColorPickerPortalServices.engine.isOpenFor(ownerToken)) { - ColorPickerPortalServices.engine.close(ownerToken) + if (DomainPortalServices.applicationColorPickerEngine.isOpenFor(ownerToken)) { + DomainPortalServices.applicationColorPickerEngine.close(ownerToken) } else { openPopup() } @@ -125,7 +126,7 @@ class ColorPickerPopupPaneNode( color = textColor, ) out += - if (ColorPickerPortalServices.engine.isOpenFor(ownerToken)) { + if (DomainPortalServices.applicationColorPickerEngine.isOpenFor(ownerToken)) { drawTextCommand( ctx, text = "^", @@ -183,13 +184,13 @@ class ColorPickerPopupPaneNode( } private fun syncPopupIfOpen() { - if (!ColorPickerPortalServices.engine.isOpenFor(ownerToken)) return - ColorPickerPortalServices.engine.sync(openRequest()) + if (!DomainPortalServices.applicationColorPickerEngine.isOpenFor(ownerToken)) return + DomainPortalServices.applicationColorPickerEngine.sync(openRequest()) setOpenState(true) } private fun openPopup() { - ColorPickerPortalServices.engine.open(openRequest()) + DomainPortalServices.applicationColorPickerEngine.open(openRequest()) setOpenState(true) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt index 775a018..d3d8152 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt @@ -7,12 +7,12 @@ import org.dreamfinity.dsgl.core.dom.layout.Insets import org.dreamfinity.dsgl.core.dom.layout.Size import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.* +import org.dreamfinity.dsgl.core.overlay.DomainPortalServices import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.select.SelectEntry import org.dreamfinity.dsgl.core.select.SelectModel import org.dreamfinity.dsgl.core.select.SelectOpenRequest -import org.dreamfinity.dsgl.core.select.SelectPortalServices class SelectNode( model: SelectModel, @@ -65,7 +65,7 @@ class SelectNode( var disabledTextColor: Int = 0xFF8E8E8E.toInt() var minContentWidth: Int = 92 var arrowGlyph: String = - SelectPortalServices.engine + DomainPortalServices.applicationSelectEngine .currentStyle() .arrowGlyph var arrowSpacing: Int = 8 @@ -81,8 +81,8 @@ class SelectNode( if (event.mouseButton != MouseButton.LEFT) return@addEventListener if (!this@SelectNode.containsGlobalPoint(event.mouseX, event.mouseY)) return@addEventListener FocusManager.requestFocus(this@SelectNode) - if (SelectPortalServices.isOpenFor(ownerToken)) { - SelectPortalServices.close(ownerToken) + if (DomainPortalServices.isSelectOpenFor(ownerToken)) { + DomainPortalServices.closeSelect(ownerToken) } else { openPopup() } @@ -91,7 +91,7 @@ class SelectNode( this@SelectNode.addEventListener(Events.KEYDOWN) { event: KeyboardKeyDownEvent -> if (this@SelectNode.styleDisabled) return@addEventListener if (!FocusManager.isFocused(this@SelectNode)) return@addEventListener - if (SelectPortalServices.isOpenFor(ownerToken)) return@addEventListener + if (DomainPortalServices.isSelectOpenFor(ownerToken)) return@addEventListener when (event.keyCode) { KeyCodes.ENTER, KeyCodes.SPACE -> { openPopup() @@ -100,20 +100,20 @@ class SelectNode( KeyCodes.DOWN -> { openPopup() - SelectPortalServices.engineFor(ownerScope).moveHighlight(ownerToken, 1) + DomainPortalServices.selectEngineFor(ownerScope).moveHighlight(ownerToken, 1) event.cancelled = true } KeyCodes.UP -> { openPopup() - SelectPortalServices.engineFor(ownerScope).moveHighlight(ownerToken, -1) + DomainPortalServices.selectEngineFor(ownerScope).moveHighlight(ownerToken, -1) event.cancelled = true } } } this@SelectNode.addEventListener(Events.BLUR) { _: FocusLoseEvent -> - if (SelectPortalServices.isOpenFor(ownerToken)) { - SelectPortalServices.close(ownerToken) + if (DomainPortalServices.isSelectOpenFor(ownerToken)) { + DomainPortalServices.closeSelect(ownerToken) } } } @@ -150,8 +150,8 @@ class SelectNode( } override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) { - if (styleDisabled && SelectPortalServices.isOpenFor(ownerToken)) { - SelectPortalServices.close(ownerToken) + if (styleDisabled && DomainPortalServices.isSelectOpenFor(ownerToken)) { + DomainPortalServices.closeSelect(ownerToken) } syncPopup() val isFocused = FocusManager.isFocused(this) && !styleDisabled @@ -206,7 +206,7 @@ class SelectNode( var hash = 1L hash = 31L * hash + selectedLabelOrPlaceholder().hashCode() hash = 31L * hash + (selectedOptionId()?.hashCode() ?: 0) - hash = 31L * hash + if (SelectPortalServices.isOpenFor(ownerToken)) 1L else 0L + hash = 31L * hash + if (DomainPortalServices.isSelectOpenFor(ownerToken)) 1L else 0L return hash } @@ -246,15 +246,15 @@ class SelectNode( private fun openPopup() { if (!hasEnabledOption()) return - SelectPortalServices.open(openRequest()) + DomainPortalServices.openSelect(openRequest()) setOpenState(true) } private fun syncPopup() { - val open = SelectPortalServices.isOpenFor(ownerToken) + val open = DomainPortalServices.isSelectOpenFor(ownerToken) setOpenState(open) if (open) { - SelectPortalServices.engineFor(ownerScope).sync(openRequest()) + DomainPortalServices.selectEngineFor(ownerScope).sync(openRequest()) } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt index 25e713b..d5c4d91 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt @@ -4,8 +4,8 @@ import org.dreamfinity.dsgl.core.colorpicker.ColorFormatMode import org.dreamfinity.dsgl.core.colorpicker.ColorPickerState import org.dreamfinity.dsgl.core.colorpicker.ColorTextCodec import org.dreamfinity.dsgl.core.colorpicker.RgbaColor -import org.dreamfinity.dsgl.core.colorpicker.internal.InspectorColorPickerHost import org.dreamfinity.dsgl.core.colorpicker.internal.SystemColorPickerPanelManager +import org.dreamfinity.dsgl.core.colorpicker.internal.SystemColorPickerPortalService import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.elements.TextEditState import org.dreamfinity.dsgl.core.dom.elements.support.TextEditOps @@ -30,9 +30,9 @@ enum class InspectorPanelState { } class InspectorController( - colorPickerManager: InspectorColorPickerHost = SystemColorPickerPanelManager(), + colorPickerManager: SystemColorPickerPortalService = SystemColorPickerPanelManager(), ) { - private var colorPickerManager: InspectorColorPickerHost = colorPickerManager + private var colorPickerManager: SystemColorPickerPortalService = colorPickerManager private enum class EditOperation { Decrement, @@ -251,10 +251,10 @@ class InspectorController( deactivateInternal() } - fun installColorPickerHost(host: InspectorColorPickerHost) { - if (colorPickerManager === host) return + fun installColorPickerPortalService(portalService: SystemColorPickerPortalService) { + if (colorPickerManager === portalService) return colorPickerManager.close() - colorPickerManager = host + colorPickerManager = portalService } fun toggleMode() { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt index c9ab077..3ce0516 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt @@ -11,11 +11,11 @@ import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.* import org.dreamfinity.dsgl.core.inspector.* +import org.dreamfinity.dsgl.core.overlay.DomainPortalServices import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanel import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelDragSession import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelState -import org.dreamfinity.dsgl.core.select.SelectPortalServices import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.Overflow import org.dreamfinity.dsgl.core.style.TextWrap @@ -1034,7 +1034,7 @@ internal class SystemInspectorOverlayNode( hovered: Boolean, onSelected: (String) -> Unit, ): DOMNode { - val open = SelectPortalServices.isOpenFor(key) + val open = DomainPortalServices.isSelectOpenFor(key) val selectNode = scope.select( props = { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt index 1c7cedb..aa8a7e7 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt @@ -3,10 +3,8 @@ package org.dreamfinity.dsgl.core.overlay import org.dreamfinity.dsgl.core.DomTree import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupEngine import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPortalController -import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPortalServices import org.dreamfinity.dsgl.core.components.modal.internal.ModalPortalController import org.dreamfinity.dsgl.core.contextmenu.ContextMenuEngine -import org.dreamfinity.dsgl.core.contextmenu.ContextMenuPortalServices import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext @@ -15,13 +13,12 @@ import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.select.SelectEngine import org.dreamfinity.dsgl.core.select.SelectPortalController -import org.dreamfinity.dsgl.core.select.SelectPortalServices import org.dreamfinity.dsgl.core.style.StyleApplicationScope class ApplicationOverlayHost( - contextMenuEngine: ContextMenuEngine = ContextMenuPortalServices.engine, - selectEngine: SelectEngine = SelectPortalServices.applicationEngine, - colorPickerEngine: ColorPickerPopupEngine = ColorPickerPortalServices.engine, + contextMenuEngine: ContextMenuEngine = DomainPortalServices.applicationContextMenuEngine, + selectEngine: SelectEngine = DomainPortalServices.applicationSelectEngine, + colorPickerEngine: ColorPickerPopupEngine = DomainPortalServices.applicationColorPickerEngine, ) : OverlayLayerHost { override val layerId: UiLayerId = UiLayerId.ApplicationOverlay diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/DomainPortalServices.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/DomainPortalServices.kt new file mode 100644 index 0000000..9a8074d --- /dev/null +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/DomainPortalServices.kt @@ -0,0 +1,41 @@ +package org.dreamfinity.dsgl.core.overlay + +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupEngine +import org.dreamfinity.dsgl.core.contextmenu.ContextMenuEngine +import org.dreamfinity.dsgl.core.select.SelectEngine +import org.dreamfinity.dsgl.core.select.SelectOpenRequest + +object DomainPortalServices { + val applicationContextMenuEngine: ContextMenuEngine = ContextMenuEngine() + val applicationSelectEngine: SelectEngine = SelectEngine() + val systemSelectEngine: SelectEngine = SelectEngine() + val applicationColorPickerEngine: ColorPickerPopupEngine = ColorPickerPopupEngine() + + fun selectEngineFor(ownerScope: OverlayOwnerScope): SelectEngine = + when (ownerScope) { + OverlayOwnerScope.Application -> applicationSelectEngine + OverlayOwnerScope.System -> systemSelectEngine + } + + fun openSelect(request: SelectOpenRequest) { + val target = selectEngineFor(request.ownerScope) + val other = if (target === applicationSelectEngine) systemSelectEngine else applicationSelectEngine + other.close(request.owner) + target.open(request) + } + + fun closeSelect(owner: Any) { + applicationSelectEngine.close(owner) + systemSelectEngine.close(owner) + } + + fun closeAllSelects() { + applicationSelectEngine.closeAll() + systemSelectEngine.closeAll() + } + + fun isSelectOpenFor(owner: Any): Boolean = + applicationSelectEngine.isOpenFor(owner) || systemSelectEngine.isOpenFor(owner) + + fun isAnySelectOpen(): Boolean = applicationSelectEngine.isOpen() || systemSelectEngine.isOpen() +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt index 5d3dbd0..37ac9fa 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt @@ -115,7 +115,7 @@ internal class SystemOverlayEntryRegistry( fun entry(id: SystemOverlayEntryId): SystemOverlayEntry? = byId[id] } -internal class SystemOverlayPortalEntryAdapter( +internal class SystemOverlayPortalEntry( private val entry: SystemOverlayEntry, ) : PortalEntry { override val state: PortalEntryState = diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt index 0a20665..8831ef4 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt @@ -4,7 +4,7 @@ import org.dreamfinity.dsgl.core.DomTree import org.dreamfinity.dsgl.core.colorpicker.* import org.dreamfinity.dsgl.core.colorpicker.internal.ColorPickerPopupMount import org.dreamfinity.dsgl.core.colorpicker.internal.ColorPickerPopupOverlayNode -import org.dreamfinity.dsgl.core.colorpicker.internal.InspectorColorPickerHost +import org.dreamfinity.dsgl.core.colorpicker.internal.SystemColorPickerPortalService import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.elements.SingleLineInputNode import org.dreamfinity.dsgl.core.dom.layout.Rect @@ -14,6 +14,7 @@ import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.inspector.InspectorPanelState import org.dreamfinity.dsgl.core.inspector.internal.SystemInspectorOverlayNode +import org.dreamfinity.dsgl.core.overlay.DomainPortalServices import org.dreamfinity.dsgl.core.overlay.OverlayLayerContracts import org.dreamfinity.dsgl.core.overlay.OverlayLayerHost import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope @@ -26,7 +27,6 @@ import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanel import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelStyle import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.select.SelectPortalController -import org.dreamfinity.dsgl.core.select.SelectPortalServices import org.dreamfinity.dsgl.core.style.StyleApplicationScope class SystemOverlayHost( @@ -45,13 +45,13 @@ class SystemOverlayHost( ) private val portalHost: PortalHost = PortalHost(OverlayLayerContracts.domainSurfaceForLayer(UiLayerId.SystemOverlay)) - private val portalEntries: List = - entryRegistry.allEntries().map(::SystemOverlayPortalEntryAdapter) + private val portalEntries: List = + entryRegistry.allEntries().map(::SystemOverlayPortalEntry) private val transientOwnershipRegistry: SystemOverlayTransientOwnershipRegistry = SystemOverlayTransientOwnershipRegistry() private val systemSelectPortal: SelectPortalController = SelectPortalController( - engine = SelectPortalServices.systemEngine, + engine = DomainPortalServices.systemSelectEngine, ownerScope = OverlayOwnerScope.System, entryId = "system.select", ) @@ -81,7 +81,7 @@ class SystemOverlayHost( portalEntries.forEach(portalHost::register) } - fun systemInspectorColorPickerPortalService(): InspectorColorPickerHost = colorPickerEntry + fun systemInspectorColorPickerService(): SystemColorPickerPortalService = colorPickerEntry fun isSystemColorPickerOpen(): Boolean = colorPickerEntry.isOpen() @@ -272,7 +272,7 @@ class SystemOverlayHost( private fun reconcileMountedEntries() { val activeEntries = portalHost.entriesInPaintOrder().mapNotNull { - (it as? SystemOverlayPortalEntryAdapter)?.systemEntry + (it as? SystemOverlayPortalEntry)?.systemEntry } val panelNodes = activeEntries @@ -291,7 +291,7 @@ class SystemOverlayHost( private fun activeEntriesTopFirst(): List = portalHost .entriesInInputOrder() - .mapNotNull { (it as? SystemOverlayPortalEntryAdapter)?.systemEntry } + .mapNotNull { (it as? SystemOverlayPortalEntry)?.systemEntry } private inline fun dispatchManualInput(handler: (SystemOverlayEntry) -> Boolean): Boolean = activeEntriesTopFirst() @@ -385,7 +385,7 @@ class SystemOverlayHost( private class ColorPickerOverlayEntry : SystemOverlayEntry, - InspectorColorPickerHost { + SystemColorPickerPortalService { override val state: SystemOverlayEntryState = SystemOverlayEntryState( id = SystemOverlayEntryId.ColorPickerPopup, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt index e7a151e..67daf82 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt @@ -13,7 +13,7 @@ import org.dreamfinity.dsgl.core.render.RenderCommand class SelectEngine( private val clock: SelectClock = SystemSelectClock, private val measurementCache: SelectMeasurementCache = SelectMeasurementCache(), -) : SelectHost { +) { private data class PopupState( val owner: Any, var modelToken: Long, @@ -76,7 +76,7 @@ class SelectEngine( fun measurementComputeCount(): Long = measurementCache.computeCount - override fun open(request: SelectOpenRequest) { + fun open(request: SelectOpenRequest) { if (request.entries.isEmpty()) { close(request.owner) return @@ -126,13 +126,13 @@ class SelectEngine( } } - override fun close(owner: Any) { + fun close(owner: Any) { val current = popup ?: return if (current.owner != owner) return startVisibilityTransition(0f, style.closeDurationMs) } - override fun closeAll() { + fun closeAll() { val current = popup ?: return val onClose = current.onClose popup = null @@ -144,12 +144,12 @@ class SelectEngine( onClose?.invoke() } - override fun isOpenFor(owner: Any): Boolean { + fun isOpenFor(owner: Any): Boolean { val current = popup ?: return false return current.owner == owner && visibilityState != VisibilityState.Hidden } - override fun isOpen(): Boolean = popup != null && visibilityState != VisibilityState.Hidden + fun isOpen(): Boolean = popup != null && visibilityState != VisibilityState.Hidden fun snapshot(): Snapshot { val current = popup diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalRequest.kt similarity index 80% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectHost.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalRequest.kt index 6f363ec..94e3cb6 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalRequest.kt @@ -3,18 +3,6 @@ package org.dreamfinity.dsgl.core.select import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope -interface SelectHost { - fun open(request: SelectOpenRequest) - - fun close(owner: Any) - - fun closeAll() - - fun isOpenFor(owner: Any): Boolean - - fun isOpen(): Boolean -} - data class SelectOpenRequest( val owner: Any, val modelToken: Long, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalServices.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalServices.kt deleted file mode 100644 index b7a247d..0000000 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalServices.kt +++ /dev/null @@ -1,36 +0,0 @@ -package org.dreamfinity.dsgl.core.select - -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope - -object SelectPortalServices { - val applicationEngine: SelectEngine = SelectEngine() - val systemEngine: SelectEngine = SelectEngine() - val engine: SelectEngine = applicationEngine - - fun engineFor(ownerScope: OverlayOwnerScope): SelectEngine = - when (ownerScope) { - OverlayOwnerScope.Application -> applicationEngine - OverlayOwnerScope.System -> systemEngine - } - - fun open(request: SelectOpenRequest) { - val target = engineFor(request.ownerScope) - val other = if (target === applicationEngine) systemEngine else applicationEngine - other.close(request.owner) - target.open(request) - } - - fun close(owner: Any) { - applicationEngine.close(owner) - systemEngine.close(owner) - } - - fun closeAll() { - applicationEngine.closeAll() - systemEngine.closeAll() - } - - fun isOpenFor(owner: Any): Boolean = applicationEngine.isOpenFor(owner) || systemEngine.isOpenFor(owner) - - fun isOpen(): Boolean = applicationEngine.isOpen() || systemEngine.isOpen() -} diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngineTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngineTests.kt index 4e0560b..ae6f1fa 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngineTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngineTests.kt @@ -692,8 +692,8 @@ class ColorPickerPopupEngineTests { @Test fun `manager reuses same owner token`() { - val fakeHost = FakeColorPickerHost() - val manager = ColorPickerPopupManager(host = fakeHost) + val fakeService = FakeColorPickerPortalService() + val manager = ColorPickerPopupManager(portalService = fakeService) manager.open( anchorRect = Rect(10, 10, 10, 10), title = "A", @@ -705,19 +705,19 @@ class ColorPickerPopupEngineTests { state = ColorPickerState(RgbaColor(0f, 0f, 0f, 1f)), ) - assertEquals(2, fakeHost.opened.size) - val first = fakeHost.opened[0] - val second = fakeHost.opened[1] + assertEquals(2, fakeService.opened.size) + val first = fakeService.opened[0] + val second = fakeService.opened[1] assertTrue(first.owner === second.owner) manager.close() - assertNotNull(fakeHost.lastClosedOwner) - assertTrue(fakeHost.lastClosedOwner === first.owner) + assertNotNull(fakeService.lastClosedOwner) + assertTrue(fakeService.lastClosedOwner === first.owner) } @Test fun `manager popup owner scope defaults to application and supports explicit system owner scope`() { - val fakeHost = FakeColorPickerHost() - val manager = ColorPickerPopupManager(host = fakeHost) + val fakeService = FakeColorPickerPortalService() + val manager = ColorPickerPopupManager(portalService = fakeService) manager.open( anchorRect = Rect(10, 10, 10, 10), title = "App", @@ -730,12 +730,12 @@ class ColorPickerPopupEngineTests { state = ColorPickerState(RgbaColor.WHITE), ) - assertEquals(2, fakeHost.opened.size) - assertEquals(OverlayOwnerScope.Application, fakeHost.opened[0].ownerScope) - assertEquals(OverlayOwnerScope.System, fakeHost.opened[1].ownerScope) + assertEquals(2, fakeService.opened.size) + assertEquals(OverlayOwnerScope.Application, fakeService.opened[0].ownerScope) + assertEquals(OverlayOwnerScope.System, fakeService.opened[1].ownerScope) } - private class FakeColorPickerHost : ColorPickerPopupHost { + private class FakeColorPickerPortalService : ColorPickerPopupPortalService { val opened: MutableList = ArrayList() var lastClosedOwner: Any? = null diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEventsTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEventsTests.kt index 169ef65..4cb53c6 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEventsTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEventsTests.kt @@ -1,7 +1,7 @@ package org.dreamfinity.dsgl.core.dom -import org.dreamfinity.dsgl.core.contextmenu.ContextMenuHost import org.dreamfinity.dsgl.core.contextmenu.ContextMenuModel +import org.dreamfinity.dsgl.core.contextmenu.ContextMenuPortalService import org.dreamfinity.dsgl.core.contextmenu.contextMenu import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Rect @@ -13,7 +13,7 @@ import kotlin.test.assertNotNull import kotlin.test.assertTrue class ContextMenuEventsTests { - private class RecordingHost : ContextMenuHost { + private class RecordingPortalService : ContextMenuPortalService { var openedModel: ContextMenuModel? = null var openedAtCursor: Pair? = null var openedAnchored: Rect? = null @@ -35,14 +35,14 @@ class ContextMenuEventsTests { @Test fun `openMenu in context handler uses mouse root coordinates`() { - val host = RecordingHost() + val host = RecordingPortalService() val node = ContainerNode() node.bounds = Rect(10, 15, 120, 30) val model = contextMenu(id = "events.cursor") { item("Open") } - node.onContextMenu(host = host) { + node.onContextMenu(portalService = host) { openMenu(model) } @@ -56,14 +56,14 @@ class ContextMenuEventsTests { @Test fun `openMenuAnchored in context handler uses target bounds`() { - val host = RecordingHost() + val host = RecordingPortalService() val node = ContainerNode() node.bounds = Rect(22, 41, 86, 19) val model = contextMenu(id = "events.anchor") { item("Open") } - node.onContextMenu(host = host) { + node.onContextMenu(portalService = host) { openMenuAnchored(model) } @@ -79,7 +79,7 @@ class ContextMenuEventsTests { @Test fun `context menu inherits font settings from triggering node when model omits them`() { - val host = RecordingHost() + val host = RecordingPortalService() val node = ContainerNode() node.bounds = Rect(22, 41, 86, 19) node.fontId = "test-font" @@ -88,7 +88,7 @@ class ContextMenuEventsTests { contextMenu(id = "events.font") { item("Open") } - node.onContextMenu(host = host) { + node.onContextMenu(portalService = host) { openMenu(model) } @@ -104,7 +104,7 @@ class ContextMenuEventsTests { @Test fun `context menu inherits font from clicked target on repeated opens`() { - val host = RecordingHost() + val host = RecordingPortalService() val parent = ContainerNode() val child = ContainerNode() child.parent = parent @@ -117,7 +117,7 @@ class ContextMenuEventsTests { contextMenu(id = "events.target.font") { item("Open") } - parent.onContextMenu(host = host) { + parent.onContextMenu(portalService = host) { openMenu(model) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerScopeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerScopeTests.kt index ebf3ffc..6fb2a59 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerScopeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerScopeTests.kt @@ -6,10 +6,10 @@ import org.dreamfinity.dsgl.core.dom.elements.SelectNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.overlay.DomainPortalServices import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand -import org.dreamfinity.dsgl.core.select.SelectPortalServices import org.dreamfinity.dsgl.core.select.selectModel import kotlin.test.AfterTest import kotlin.test.Test @@ -28,7 +28,7 @@ class SelectNodeOwnerScopeTests { @AfterTest fun cleanup() { - SelectPortalServices.closeAll() + DomainPortalServices.closeAllSelects() } @Test @@ -62,7 +62,7 @@ class SelectNodeOwnerScopeTests { assertTrue(router.handleMouseDown(clickX, clickY, MouseButton.LEFT)) assertTrue(router.handleMouseUp(clickX, clickY, MouseButton.LEFT)) - assertFalse(SelectPortalServices.applicationEngine.isOpenFor(ownerKey)) - assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) + assertFalse(DomainPortalServices.applicationSelectEngine.isOpenFor(ownerKey)) + assertTrue(DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey)) } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectPopupAnchoringStickyTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectPopupAnchoringStickyTests.kt index 9dd4163..b8d6eb5 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectPopupAnchoringStickyTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectPopupAnchoringStickyTests.kt @@ -7,9 +7,9 @@ import org.dreamfinity.dsgl.core.dom.layout.Insets import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.overlay.DomainPortalServices import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand -import org.dreamfinity.dsgl.core.select.SelectPortalServices import org.dreamfinity.dsgl.core.select.selectModel import org.dreamfinity.dsgl.core.style.Overflow import org.dreamfinity.dsgl.core.style.StyleDeclarations @@ -37,7 +37,7 @@ class SelectPopupAnchoringStickyTests { @AfterTest fun cleanup() { - SelectPortalServices.closeAll() + DomainPortalServices.closeAllSelects() StyleEngine.clearAllInspectorOverrides() StyleEngine.clearCache() } @@ -96,7 +96,7 @@ class SelectPopupAnchoringStickyTests { val y = visible.y + visible.height / 2 assertTrue(fixture.router.handleMouseDown(x, y, MouseButton.LEFT)) - assertTrue(SelectPortalServices.isOpenFor(fixture.ownerKey)) + assertTrue(DomainPortalServices.isSelectOpenFor(fixture.ownerKey)) assertTrue(fixture.router.handleMouseUp(x, y, MouseButton.LEFT)) } @@ -106,19 +106,19 @@ class SelectPopupAnchoringStickyTests { val y = visible.y + visible.height / 2 assertTrue(fixture.router.handleMouseDown(x, y, MouseButton.LEFT)) assertTrue(fixture.router.handleMouseUp(x, y, MouseButton.LEFT)) - assertTrue(SelectPortalServices.isOpenFor(fixture.ownerKey)) + assertTrue(DomainPortalServices.isSelectOpenFor(fixture.ownerKey)) - SelectPortalServices.engine.onFrame( + DomainPortalServices.applicationSelectEngine.onFrame( measureContext = ctx, viewportWidth = viewportWidth, viewportHeight = viewportHeight, viewportScale = 1f, ) val anchor = - SelectPortalServices.engine.debugAnchorRect(fixture.ownerKey) + DomainPortalServices.applicationSelectEngine.debugAnchorRect(fixture.ownerKey) ?: error("Expected select anchor rect for owner=${fixture.ownerKey}") val panel = - SelectPortalServices.engine.debugPanelRect(fixture.ownerKey) + DomainPortalServices.applicationSelectEngine.debugPanelRect(fixture.ownerKey) ?: error("Expected select panel rect for owner=${fixture.ownerKey}") return PopupGeometry(anchor = anchor, panel = panel) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorControllerTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorControllerTests.kt index 5ee7124..e9a1b32 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorControllerTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorControllerTests.kt @@ -3,7 +3,7 @@ package org.dreamfinity.dsgl.core.inspector import org.dreamfinity.dsgl.core.colorpicker.ColorPickerState import org.dreamfinity.dsgl.core.colorpicker.ColorPickerStyle import org.dreamfinity.dsgl.core.colorpicker.RgbaColor -import org.dreamfinity.dsgl.core.colorpicker.internal.InspectorColorPickerHost +import org.dreamfinity.dsgl.core.colorpicker.internal.SystemColorPickerPortalService import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Rect @@ -362,8 +362,8 @@ class InspectorControllerTests { @Test fun `inspector opens picker for color property and stays interactive after preview commit`() { - val pickerHost = RecordingInspectorColorPickerHost() - val controller = InspectorController(colorPickerManager = pickerHost) + val pickerService = RecordingSystemColorPickerPortalService() + val controller = InspectorController(colorPickerManager = pickerService) controller.toggle() val root = container("root", 0, 0, 1200, 800) @@ -383,9 +383,9 @@ class InspectorControllerTests { ), ) - val opened = pickerHost.lastOpen + val opened = pickerService.lastOpen assertNotNull(opened) - assertTrue(pickerHost.isOpen()) + assertTrue(pickerService.isOpen()) opened.onPreview?.invoke(RgbaColor(0.25f, 0.5f, 0.75f, 1f)) val previewLiteral = @@ -407,8 +407,8 @@ class InspectorControllerTests { )?.value assertEquals("#FFFF0000", committedLiteral) - pickerHost.close() - assertFalse(pickerHost.isOpen()) + pickerService.close() + assertFalse(pickerService.isOpen()) assertTrue(controller.handleMouseDown(38, 30, MouseButton.LEFT)) assertTrue(controller.isDraggingPanel) assertTrue(controller.handleMouseUp(38, 30, MouseButton.LEFT)) @@ -564,7 +564,7 @@ class InspectorControllerTests { bounds = Rect(x, y, width, height) } - private class RecordingInspectorColorPickerHost : InspectorColorPickerHost { + private class RecordingSystemColorPickerPortalService : SystemColorPickerPortalService { var lastOpen: OpenCall? = null private var open: Boolean = false diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt index 3493d8b..92e47a2 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt @@ -1,12 +1,10 @@ package org.dreamfinity.dsgl.core.overlay import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupRequest -import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPortalServices import org.dreamfinity.dsgl.core.colorpicker.ColorPickerState import org.dreamfinity.dsgl.core.colorpicker.RgbaColor import org.dreamfinity.dsgl.core.colorpicker.ScreenColorSampler import org.dreamfinity.dsgl.core.colorpicker.ScreenColorSamplerBridge -import org.dreamfinity.dsgl.core.contextmenu.ContextMenuPortalServices import org.dreamfinity.dsgl.core.contextmenu.contextMenu import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent @@ -17,13 +15,13 @@ import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController +import org.dreamfinity.dsgl.core.overlay.DomainPortalServices import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayEntryId import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayHost import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayPanelDemoNode import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.select.SelectEntry import org.dreamfinity.dsgl.core.select.SelectOpenRequest -import org.dreamfinity.dsgl.core.select.SelectPortalServices import org.dreamfinity.dsgl.core.select.selectModel import kotlin.test.AfterTest import kotlin.test.Test @@ -44,11 +42,11 @@ class LiveLayerInteractionPathTests { } @AfterTest - fun cleanupContextMenuPortalServices() { - ContextMenuPortalServices.engine.closeAll() - ColorPickerPortalServices.engine.closeAll() + fun cleanupDomainContextMenuPortalService() { + DomainPortalServices.applicationContextMenuEngine.closeAll() + DomainPortalServices.applicationColorPickerEngine.closeAll() ScreenColorSamplerBridge.install(null) - SelectPortalServices.closeAll() + DomainPortalServices.closeAllSelects() } @Test @@ -241,7 +239,7 @@ class LiveLayerInteractionPathTests { val applicationOverlayHost = ApplicationOverlayHost() applicationOverlayHost.onInputFrame(320, 180) var actionHits = 0 - ContextMenuPortalServices.engine.openAtCursor( + DomainPortalServices.applicationContextMenuEngine.openAtCursor( contextMenu(id = "portal.context") { item("Run") { onClick { actionHits += 1 } @@ -254,7 +252,7 @@ class LiveLayerInteractionPathTests { applicationOverlayHost.syncPortalFrame(ctx, 320, 180, 1f, 24, 24) val commands = ArrayList() applicationOverlayHost.appendPortalOverlayCommands(ctx, 320, 180, commands) - val firstEntryRect = ContextMenuPortalServices.engine.debugEntryRect(levelIndex = 0, entryIndex = 0) + val firstEntryRect = DomainPortalServices.applicationContextMenuEngine.debugEntryRect(levelIndex = 0, entryIndex = 0) assertNotNull(firstEntryRect) val consumedByMenu = @@ -276,7 +274,7 @@ class LiveLayerInteractionPathTests { fun `application context menu portal blocks app-root fallthrough on outside dismiss`() { val applicationOverlayHost = ApplicationOverlayHost() applicationOverlayHost.onInputFrame(320, 180) - ContextMenuPortalServices.engine.openAtCursor( + DomainPortalServices.applicationContextMenuEngine.openAtCursor( contextMenu(id = "portal.dismiss") { item("Run") item("Build") @@ -285,7 +283,7 @@ class LiveLayerInteractionPathTests { y = 24, ) applicationOverlayHost.syncPortalFrame(ctx, 320, 180, 1f, 24, 24) - val panel = ContextMenuPortalServices.engine.debugPanelRect(0) + val panel = DomainPortalServices.applicationContextMenuEngine.debugPanelRect(0) assertNotNull(panel) val outsideX = panel.x + panel.width + 24 val outsideY = panel.y + panel.height + 24 @@ -314,7 +312,7 @@ class LiveLayerInteractionPathTests { fun `application context menu portal consumes wheel and escape while open`() { val applicationOverlayHost = ApplicationOverlayHost() applicationOverlayHost.onInputFrame(320, 180) - ContextMenuPortalServices.engine.openAtCursor( + DomainPortalServices.applicationContextMenuEngine.openAtCursor( contextMenu(id = "portal.keyboard") { item("Run") item("Build") @@ -335,15 +333,15 @@ class LiveLayerInteractionPathTests { applicationOverlayHost.onInputFrame(320, 180) var selected: String? = null val owner = "application-select-portal" - SelectPortalServices.open(selectRequest(owner, OverlayOwnerScope.Application) { selected = it }) + DomainPortalServices.openSelect(selectRequest(owner, OverlayOwnerScope.Application) { selected = it }) applicationOverlayHost.syncPortalFrame(ctx, 320, 180, 1f, 0, 0) val commands = ArrayList() applicationOverlayHost.appendPortalOverlayCommands(ctx, 320, 180, commands) - val panel = SelectPortalServices.applicationEngine.debugPanelRect(owner) + val panel = DomainPortalServices.applicationSelectEngine.debugPanelRect(owner) assertNotNull(panel) - val style = SelectPortalServices.applicationEngine.currentStyle() + val style = DomainPortalServices.applicationSelectEngine.currentStyle() val consumed = applicationOverlayHost.handlePortalPointerAfterDom( mouseX = panel.x + style.panelPaddingX + 1, @@ -363,9 +361,9 @@ class LiveLayerInteractionPathTests { val applicationOverlayHost = ApplicationOverlayHost() applicationOverlayHost.onInputFrame(320, 180) val owner = "application-select-dismiss" - SelectPortalServices.open(selectRequest(owner, OverlayOwnerScope.Application)) + DomainPortalServices.openSelect(selectRequest(owner, OverlayOwnerScope.Application)) applicationOverlayHost.syncPortalFrame(ctx, 320, 180, 1f, 0, 0) - val panel = SelectPortalServices.applicationEngine.debugPanelRect(owner) + val panel = DomainPortalServices.applicationSelectEngine.debugPanelRect(owner) assertNotNull(panel) val outsideX = panel.x + panel.width + 24 val outsideY = panel.y + panel.height + 24 @@ -395,7 +393,7 @@ class LiveLayerInteractionPathTests { applicationOverlayHost.onInputFrame(320, 120) val owner = "application-select-keyboard" var selected: String? = null - SelectPortalServices.open( + DomainPortalServices.openSelect( selectRequest( owner = owner, ownerScope = OverlayOwnerScope.Application, @@ -412,7 +410,7 @@ class LiveLayerInteractionPathTests { ), ) applicationOverlayHost.syncPortalFrame(ctx, 320, 120, 1f, 0, 0) - val panel = SelectPortalServices.applicationEngine.debugPanelRect(owner) + val panel = DomainPortalServices.applicationSelectEngine.debugPanelRect(owner) assertNotNull(panel) assertTrue(applicationOverlayHost.handlePortalPointerAfterDom(panel.x + 2, panel.y + 2, -120, null, false)) @@ -420,7 +418,7 @@ class LiveLayerInteractionPathTests { assertTrue(applicationOverlayHost.handlePortalKeyDownAfterDom(KeyCodes.ENTER, Char.MIN_VALUE)) assertEquals("d", selected) - SelectPortalServices.open(selectRequest(owner, OverlayOwnerScope.Application)) + DomainPortalServices.openSelect(selectRequest(owner, OverlayOwnerScope.Application)) applicationOverlayHost.syncPortalFrame(ctx, 320, 120, 1f, 0, 0) assertTrue(applicationOverlayHost.handlePortalKeyDownAfterDom(KeyCodes.ESCAPE, Char.MIN_VALUE)) } @@ -430,12 +428,12 @@ class LiveLayerInteractionPathTests { val applicationOverlayHost = ApplicationOverlayHost() applicationOverlayHost.onInputFrame(360, 240) val owner = "application-color-picker-portal" - ColorPickerPortalServices.engine.open(colorPickerRequest(owner, OverlayOwnerScope.Application)) + DomainPortalServices.applicationColorPickerEngine.open(colorPickerRequest(owner, OverlayOwnerScope.Application)) applicationOverlayHost.syncPortalFrame(ctx, 360, 240, 1f, 42, 48) val commands = ArrayList() applicationOverlayHost.appendPortalOverlayCommands(ctx, 360, 240, commands) - val layout = ColorPickerPortalServices.engine.debugBodyLayout(owner) + val layout = DomainPortalServices.applicationColorPickerEngine.debugBodyLayout(owner) assertNotNull(layout) val harness = @@ -470,15 +468,15 @@ class LiveLayerInteractionPathTests { applicationOverlayHost.onInputFrame(480, 320) val owner = "application-color-picker-drag-eyedropper" var committed: RgbaColor? = null - ColorPickerPortalServices.engine.open( + DomainPortalServices.applicationColorPickerEngine.open( colorPickerRequest(owner, OverlayOwnerScope.Application) { committed = it }, ) applicationOverlayHost.syncPortalFrame(ctx, 480, 320, 1f, 120, 80) - val panelBefore = ColorPickerPortalServices.engine.debugPanelRect(owner) ?: error("panel missing") - val header = ColorPickerPortalServices.engine.debugHeaderRect(owner) ?: error("header missing") + val panelBefore = DomainPortalServices.applicationColorPickerEngine.debugPanelRect(owner) ?: error("panel missing") + val header = DomainPortalServices.applicationColorPickerEngine.debugHeaderRect(owner) ?: error("header missing") val dragStartX = header.x + 6 val dragStartY = header.y + 6 assertTrue(applicationOverlayHost.handlePortalPointerBeforeDom(dragStartX, dragStartY, 0, MouseButton.LEFT, true)) @@ -500,10 +498,10 @@ class LiveLayerInteractionPathTests { pressed = false, ), ) - val panelAfter = ColorPickerPortalServices.engine.debugPanelRect(owner) ?: error("panel missing") + val panelAfter = DomainPortalServices.applicationColorPickerEngine.debugPanelRect(owner) ?: error("panel missing") assertNotEquals(panelBefore.x, panelAfter.x) - val layout = ColorPickerPortalServices.engine.debugBodyLayout(owner) ?: error("layout missing") + val layout = DomainPortalServices.applicationColorPickerEngine.debugBodyLayout(owner) ?: error("layout missing") assertTrue( applicationOverlayHost.handlePortalPointerBeforeDom( mouseX = layout.pipetteRect.x + 2, @@ -521,7 +519,7 @@ class LiveLayerInteractionPathTests { val expected = RgbaColor.fromArgbInt((0xFF shl 24) or (25 shl 16) or (52 shl 8) or 0x44) assertEquals(expected.toArgbInt(), committed?.toArgbInt()) - val closeRect = ColorPickerPortalServices.engine.debugCloseRect(owner) ?: error("close missing") + val closeRect = DomainPortalServices.applicationColorPickerEngine.debugCloseRect(owner) ?: error("close missing") assertTrue( applicationOverlayHost.handlePortalPointerBeforeDom( mouseX = closeRect.x + 1, @@ -539,15 +537,15 @@ class LiveLayerInteractionPathTests { val applicationOverlayHost = ApplicationOverlayHost() applicationOverlayHost.onInputFrame(360, 240) val owner = "system-color-picker-owner" - ColorPickerPortalServices.engine.open(colorPickerRequest(owner, OverlayOwnerScope.System)) + DomainPortalServices.applicationColorPickerEngine.open(colorPickerRequest(owner, OverlayOwnerScope.System)) applicationOverlayHost.syncPortalFrame(ctx, 360, 240, 1f, 42, 48) val commands = ArrayList() applicationOverlayHost.appendPortalOverlayCommands(ctx, 360, 240, commands) - val panel = ColorPickerPortalServices.engine.debugPanelRect(owner) + val panel = DomainPortalServices.applicationColorPickerEngine.debugPanelRect(owner) assertNotNull(panel) - assertTrue(ColorPickerPortalServices.engine.isOpenFor(owner)) + assertTrue(DomainPortalServices.applicationColorPickerEngine.isOpenFor(owner)) assertFalse(applicationOverlayHost.hasOpenColorPickerPortal()) assertFalse(applicationOverlayHost.handlePortalPointerBeforeDom(panel.x + 2, panel.y + 2, 0, MouseButton.LEFT, true)) assertTrue(commands.isEmpty()) @@ -559,14 +557,14 @@ class LiveLayerInteractionPathTests { systemHost.onInputFrame(320, 180) val owner = "system-select-portal" var selected: String? = null - SelectPortalServices.open(selectRequest(owner, OverlayOwnerScope.System) { selected = it }) + DomainPortalServices.openSelect(selectRequest(owner, OverlayOwnerScope.System) { selected = it }) systemHost.syncPortalFrame(ctx, 320, 180, 1f) val commands = ArrayList() systemHost.appendPortalOverlayCommands(ctx, 320, 180, commands) - val panel = SelectPortalServices.systemEngine.debugPanelRect(owner) + val panel = DomainPortalServices.systemSelectEngine.debugPanelRect(owner) assertNotNull(panel) - val style = SelectPortalServices.systemEngine.currentStyle() + val style = DomainPortalServices.systemSelectEngine.currentStyle() val harness = LiveLayerInputHarness( @@ -591,20 +589,20 @@ class LiveLayerInteractionPathTests { assertEquals(UiLayerId.SystemOverlay, consumedBy) assertFalse(appRootReceived) assertEquals("a", selected) - assertFalse(SelectPortalServices.applicationEngine.isOpenFor(owner)) + assertFalse(DomainPortalServices.applicationSelectEngine.isOpenFor(owner)) } @Test fun `select owner migration preserves application system routing`() { val owner = "select-owner-migration" - SelectPortalServices.open(selectRequest(owner, OverlayOwnerScope.Application)) - assertTrue(SelectPortalServices.applicationEngine.isOpenFor(owner)) - assertFalse(SelectPortalServices.systemEngine.isOpenFor(owner)) + DomainPortalServices.openSelect(selectRequest(owner, OverlayOwnerScope.Application)) + assertTrue(DomainPortalServices.applicationSelectEngine.isOpenFor(owner)) + assertFalse(DomainPortalServices.systemSelectEngine.isOpenFor(owner)) - SelectPortalServices.open(selectRequest(owner, OverlayOwnerScope.System)) + DomainPortalServices.openSelect(selectRequest(owner, OverlayOwnerScope.System)) - assertFalse(SelectPortalServices.applicationEngine.isOpenFor(owner)) - assertTrue(SelectPortalServices.systemEngine.isOpenFor(owner)) + assertFalse(DomainPortalServices.applicationSelectEngine.isOpenFor(owner)) + assertTrue(DomainPortalServices.systemSelectEngine.isOpenFor(owner)) } @Test diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt index 0ef5380..f117597 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt @@ -1,6 +1,5 @@ package org.dreamfinity.dsgl.core.overlay.system -import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPortalServices import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode @@ -13,9 +12,9 @@ import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.inspector.InspectorEditorKind import org.dreamfinity.dsgl.core.inspector.InspectorStyleEditorRowSnapshot +import org.dreamfinity.dsgl.core.overlay.DomainPortalServices import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.render.RenderCommand -import org.dreamfinity.dsgl.core.select.SelectPortalServices import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleProperty import kotlin.test.* @@ -34,8 +33,8 @@ class InspectorDragScrollDomMigrationTests { fun cleanup() { FocusManager.clearFocus() KeyModifiers.sync(shift = false, control = false, meta = false) - ColorPickerPortalServices.engine.closeAll() - SelectPortalServices.closeAll() + DomainPortalServices.applicationColorPickerEngine.closeAll() + DomainPortalServices.closeAllSelects() StyleEngine.clearAllInspectorOverrides() StyleEngine.clearCache() } @@ -304,12 +303,12 @@ class InspectorDragScrollDomMigrationTests { fixture.host.handleMouseUp(clickX, clickY, MouseButton.LEFT) syncAndRender(fixture, clickX, clickY) - assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) + assertTrue(DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey)) fixture.host.handleMouseDown(clickX, clickY, MouseButton.LEFT) fixture.host.handleMouseUp(clickX, clickY, MouseButton.LEFT) syncAndRender(fixture, clickX, clickY) - assertFalse(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) + assertFalse(DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey)) } @Test @@ -328,7 +327,7 @@ class InspectorDragScrollDomMigrationTests { private fun openInspectorAndSelectTarget(withManyChildren: Boolean): Fixture { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot(withManyChildren) inspector.toggle() diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt index b69da26..f0d4a93 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt @@ -1,6 +1,5 @@ package org.dreamfinity.dsgl.core.overlay.system -import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPortalServices import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode @@ -12,9 +11,9 @@ import org.dreamfinity.dsgl.core.event.KeyModifiers import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.inspector.InspectorEditorKind +import org.dreamfinity.dsgl.core.overlay.DomainPortalServices import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.render.RenderCommand -import org.dreamfinity.dsgl.core.select.SelectPortalServices import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleProperty import kotlin.test.* @@ -33,8 +32,8 @@ class InspectorDropdownCorrectiveTests { fun cleanup() { FocusManager.clearFocus() KeyModifiers.sync(shift = false, control = false, meta = false) - ColorPickerPortalServices.engine.closeAll() - SelectPortalServices.closeAll() + DomainPortalServices.applicationColorPickerEngine.closeAll() + DomainPortalServices.closeAllSelects() StyleEngine.clearAllInspectorOverrides() StyleEngine.clearCache() } @@ -74,7 +73,7 @@ class InspectorDropdownCorrectiveTests { assertTrue(dispatchSystemMouseWheel(fixture, wheelX, wheelY, -120)) syncAndRender(fixture, wheelX, wheelY) - assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) + assertTrue(DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey)) assertEquals(beforePanelScroll, fixture.inspector.panelScrollOffsetY) } @@ -145,7 +144,7 @@ class InspectorDropdownCorrectiveTests { private fun openInspectorAndSelectTarget(withManyChildren: Boolean): Fixture { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot(withManyChildren) inspector.toggle() @@ -255,7 +254,7 @@ class InspectorDropdownCorrectiveTests { dispatchSystemMouseUp(fixture, clickX, clickY) syncAndRender(fixture, clickX, clickY) - assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) + assertTrue(DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey)) return triggerRect to ownerKey } @@ -302,7 +301,7 @@ class InspectorDropdownCorrectiveTests { dispatchSystemMouseUp(fixture, clickX, clickY) syncAndRender(fixture, clickX, clickY) - val opened = SelectPortalServices.systemEngine.isOpenFor(ownerKey) + val opened = DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey) if (opened) { val popup = selectPanelRect(ownerKey, fixture) if (!requireScrollable || popup.height > triggerRect.height + 24) { @@ -322,17 +321,17 @@ class InspectorDropdownCorrectiveTests { } private fun selectPanelRect(ownerKey: String, fixture: Fixture): Rect { - SelectPortalServices.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) - return SelectPortalServices.systemEngine.debugPanelRect(ownerKey) + DomainPortalServices.systemSelectEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) + return DomainPortalServices.systemSelectEngine.debugPanelRect(ownerKey) ?: error("expected system select popup for owner=$ownerKey") } private fun dispatchSystemMouseDown(fixture: Fixture, x: Int, y: Int): Boolean = - SelectPortalServices.systemEngine.handleMouseDown(x, y, MouseButton.LEFT) || + DomainPortalServices.systemSelectEngine.handleMouseDown(x, y, MouseButton.LEFT) || fixture.host.handleMouseDown(x, y, MouseButton.LEFT) private fun dispatchSystemMouseUp(fixture: Fixture, x: Int, y: Int): Boolean = - SelectPortalServices.systemEngine.handleMouseUp(x, y, MouseButton.LEFT) || + DomainPortalServices.systemSelectEngine.handleMouseUp(x, y, MouseButton.LEFT) || fixture.host.handleMouseUp(x, y, MouseButton.LEFT) private fun dispatchSystemMouseWheel( @@ -341,7 +340,7 @@ class InspectorDropdownCorrectiveTests { y: Int, delta: Int, ): Boolean = - SelectPortalServices.systemEngine.handleMouseWheel(x, y, delta) || + DomainPortalServices.systemSelectEngine.handleMouseWheel(x, y, delta) || fixture.host.handleMouseWheel(x, y, delta) private fun waitForSystemSelectClosed( @@ -351,12 +350,12 @@ class InspectorDropdownCorrectiveTests { cursorY: Int, ) { repeat(30) { - if (!SelectPortalServices.systemEngine.isOpenFor(ownerKey)) return + if (!DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey)) return Thread.sleep(5) syncAndRender(fixture, cursorX, cursorY) - SelectPortalServices.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) + DomainPortalServices.systemSelectEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) } - assertFalse(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) + assertFalse(DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey)) } private fun focusInputByClick(fixture: Fixture, input: TextInputNode): Pair { diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorInputPathBaselineTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorInputPathBaselineTests.kt index b1808b3..5647dc5 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorInputPathBaselineTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorInputPathBaselineTests.kt @@ -1,6 +1,5 @@ package org.dreamfinity.dsgl.core.overlay.system -import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPortalServices import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode @@ -13,9 +12,9 @@ import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.inspector.InspectorEditorKind import org.dreamfinity.dsgl.core.inspector.InspectorStyleEditorRowSnapshot +import org.dreamfinity.dsgl.core.overlay.DomainPortalServices import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.render.RenderCommand -import org.dreamfinity.dsgl.core.select.SelectPortalServices import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleProperty import kotlin.test.* @@ -34,8 +33,8 @@ class InspectorInputPathBaselineTests { fun cleanup() { FocusManager.clearFocus() KeyModifiers.sync(shift = false, control = false, meta = false) - ColorPickerPortalServices.engine.closeAll() - SelectPortalServices.closeAll() + DomainPortalServices.applicationColorPickerEngine.closeAll() + DomainPortalServices.closeAllSelects() StyleEngine.clearAllInspectorOverrides() StyleEngine.clearCache() } @@ -45,7 +44,7 @@ class InspectorInputPathBaselineTests { val fixture = openInspectorAndSelectTarget(withManyChildren = false) val (trigger, ownerKey) = openDropdownFromVisibleSelectRow(fixture) - assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) + assertTrue(DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey)) assertNotNull(selectPanelRect(ownerKey, fixture)) assertFalse(fixture.inspector.hasOpenStyleDropdown()) assertFalse(fixture.inspector.closeOpenStyleDropdowns()) @@ -74,7 +73,7 @@ class InspectorInputPathBaselineTests { fun `inspector dropdown opens and closes from dom interactions`() { val fixture = openInspectorAndSelectTarget(withManyChildren = false) val (trigger, ownerKey) = openDropdownFromVisibleSelectRow(fixture) - assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) + assertTrue(DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey)) dispatchSystemMouseDown(fixture, trigger.x + 2, trigger.y + 2) dispatchSystemMouseUp(fixture, trigger.x + 2, trigger.y + 2) @@ -91,9 +90,9 @@ class InspectorInputPathBaselineTests { val optionX = panel.x + 6 val optionY = panel.y + 10 - SelectPortalServices.systemEngine.handleMouseMove(optionX, optionY) - assertTrue(SelectPortalServices.systemEngine.handleMouseDown(optionX, optionY, MouseButton.LEFT)) - assertTrue(SelectPortalServices.systemEngine.handleMouseUp(optionX, optionY, MouseButton.LEFT)) + DomainPortalServices.systemSelectEngine.handleMouseMove(optionX, optionY) + assertTrue(DomainPortalServices.systemSelectEngine.handleMouseDown(optionX, optionY, MouseButton.LEFT)) + assertTrue(DomainPortalServices.systemSelectEngine.handleMouseUp(optionX, optionY, MouseButton.LEFT)) syncAndRender(fixture, optionX, optionY) waitForSystemSelectClosed(fixture, ownerKey, optionX, optionY) @@ -107,7 +106,7 @@ class InspectorInputPathBaselineTests { val panel = selectPanelRect(ownerKey, fixture) syncAndRender(fixture, panel.x + 2, panel.y + 2) - assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) + assertTrue(DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey)) assertFalse(fixture.inspector.hasOpenStyleDropdown()) } @@ -116,7 +115,7 @@ class InspectorInputPathBaselineTests { val fixture = openInspectorAndSelectTarget(withManyChildren = false) val (_, ownerKey) = openDropdownFromVisibleSelectRow(fixture) - assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) + assertTrue(DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey)) assertFalse(fixture.inspector.hasOpenStyleDropdown()) assertFalse(fixture.inspector.handleOpenStyleDropdownWheel(-120)) @@ -128,7 +127,7 @@ class InspectorInputPathBaselineTests { assertTrue(dispatchSystemMouseWheel(fixture, wheelX, wheelY, -120)) syncAndRender(fixture, wheelX, wheelY) - assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) + assertTrue(DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey)) assertEquals(beforePanelScroll, fixture.inspector.panelScrollOffsetY) assertFalse(fixture.inspector.hasOpenStyleDropdown()) } @@ -194,7 +193,7 @@ class InspectorInputPathBaselineTests { val wheelY = contentRect.y + 10 assertTrue(dispatchSystemMouseWheel(fixture, wheelX, wheelY, -120)) syncAndRender(fixture, wheelX, wheelY) - assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) + assertTrue(DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey)) val panelRect = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") val outsideX = (panelRect.x - 12).coerceAtLeast(1) @@ -211,7 +210,7 @@ class InspectorInputPathBaselineTests { private fun openInspectorAndSelectTarget(withManyChildren: Boolean): Fixture { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot(withManyChildren) inspector.toggle() @@ -331,22 +330,22 @@ class InspectorInputPathBaselineTests { dispatchSystemMouseUp(fixture, clickX, clickY) syncAndRender(fixture, clickX, clickY) - assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) + assertTrue(DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey)) return triggerRect to ownerKey } private fun selectPanelRect(ownerKey: String, fixture: Fixture): Rect { - SelectPortalServices.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) - return SelectPortalServices.systemEngine.debugPanelRect(ownerKey) + DomainPortalServices.systemSelectEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) + return DomainPortalServices.systemSelectEngine.debugPanelRect(ownerKey) ?: error("expected system select popup for owner=$ownerKey") } private fun dispatchSystemMouseDown(fixture: Fixture, x: Int, y: Int): Boolean = - SelectPortalServices.systemEngine.handleMouseDown(x, y, MouseButton.LEFT) || + DomainPortalServices.systemSelectEngine.handleMouseDown(x, y, MouseButton.LEFT) || fixture.host.handleMouseDown(x, y, MouseButton.LEFT) private fun dispatchSystemMouseUp(fixture: Fixture, x: Int, y: Int): Boolean = - SelectPortalServices.systemEngine.handleMouseUp(x, y, MouseButton.LEFT) || + DomainPortalServices.systemSelectEngine.handleMouseUp(x, y, MouseButton.LEFT) || fixture.host.handleMouseUp(x, y, MouseButton.LEFT) private fun dispatchSystemMouseWheel( @@ -355,7 +354,7 @@ class InspectorInputPathBaselineTests { y: Int, delta: Int, ): Boolean = - SelectPortalServices.systemEngine.handleMouseWheel(x, y, delta) || + DomainPortalServices.systemSelectEngine.handleMouseWheel(x, y, delta) || fixture.host.handleMouseWheel(x, y, delta) private fun waitForSystemSelectClosed( @@ -365,12 +364,12 @@ class InspectorInputPathBaselineTests { cursorY: Int, ) { repeat(30) { - if (!SelectPortalServices.systemEngine.isOpenFor(ownerKey)) return + if (!DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey)) return Thread.sleep(5) syncAndRender(fixture, cursorX, cursorY) - SelectPortalServices.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) + DomainPortalServices.systemSelectEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) } - assertFalse(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) + assertFalse(DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey)) } private fun focusInputByClick(fixture: Fixture, input: TextInputNode): Pair { diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt index d433072..4f05214 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt @@ -1,6 +1,5 @@ package org.dreamfinity.dsgl.core.overlay.system -import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPortalServices import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Rect @@ -11,8 +10,8 @@ import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.inspector.InspectorEditorKind import org.dreamfinity.dsgl.core.inspector.InspectorStyleEditorRowSnapshot +import org.dreamfinity.dsgl.core.overlay.DomainPortalServices import org.dreamfinity.dsgl.core.render.RenderCommand -import org.dreamfinity.dsgl.core.select.SelectPortalServices import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleProperty import kotlin.test.AfterTest @@ -34,8 +33,8 @@ class InspectorPointerAlignmentTests { fun cleanup() { FocusManager.clearFocus() KeyModifiers.sync(shift = false, control = false, meta = false) - ColorPickerPortalServices.engine.closeAll() - SelectPortalServices.closeAll() + DomainPortalServices.applicationColorPickerEngine.closeAll() + DomainPortalServices.closeAllSelects() StyleEngine.clearAllInspectorOverrides() StyleEngine.clearCache() } @@ -122,7 +121,7 @@ class InspectorPointerAlignmentTests { val ownerKey = "dsgl-system-inspector-editor-select-$rowIndex" val property = row.property val triggerRect = openDropdownFromVisibleSelectRow(fixture, row) - assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) + assertTrue(DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey)) fixture.host.handleMouseDown( triggerRect.x + 2, @@ -167,7 +166,7 @@ class InspectorPointerAlignmentTests { assertTrue(dispatchSystemMouseWheel(fixture, wheelX, wheelY, -120)) syncAndRender(fixture, wheelX, wheelY) - assertTrue(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) + assertTrue(DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey)) val panelRect = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") val outsideX = (panelRect.x - 12).coerceAtLeast(1) @@ -183,7 +182,7 @@ class InspectorPointerAlignmentTests { private fun openInspectorAndSelectTarget(withManyChildren: Boolean): Fixture { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot(withManyChildren) inspector.toggle() @@ -303,17 +302,17 @@ class InspectorPointerAlignmentTests { } private fun selectPanelRect(ownerKey: String, fixture: Fixture): Rect { - SelectPortalServices.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) - return SelectPortalServices.systemEngine.debugPanelRect(ownerKey) + DomainPortalServices.systemSelectEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) + return DomainPortalServices.systemSelectEngine.debugPanelRect(ownerKey) ?: error("expected system select popup for owner=$ownerKey") } private fun dispatchSystemMouseDown(fixture: Fixture, x: Int, y: Int): Boolean = - SelectPortalServices.systemEngine.handleMouseDown(x, y, MouseButton.LEFT) || + DomainPortalServices.systemSelectEngine.handleMouseDown(x, y, MouseButton.LEFT) || fixture.host.handleMouseDown(x, y, MouseButton.LEFT) private fun dispatchSystemMouseUp(fixture: Fixture, x: Int, y: Int): Boolean = - SelectPortalServices.systemEngine.handleMouseUp(x, y, MouseButton.LEFT) || + DomainPortalServices.systemSelectEngine.handleMouseUp(x, y, MouseButton.LEFT) || fixture.host.handleMouseUp(x, y, MouseButton.LEFT) private fun dispatchSystemMouseWheel( @@ -322,7 +321,7 @@ class InspectorPointerAlignmentTests { y: Int, delta: Int, ): Boolean = - SelectPortalServices.systemEngine.handleMouseWheel(x, y, delta) || + DomainPortalServices.systemSelectEngine.handleMouseWheel(x, y, delta) || fixture.host.handleMouseWheel(x, y, delta) private fun waitForSystemSelectClosed( @@ -332,12 +331,12 @@ class InspectorPointerAlignmentTests { cursorY: Int, ) { repeat(30) { - if (!SelectPortalServices.systemEngine.isOpenFor(ownerKey)) return + if (!DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey)) return Thread.sleep(5) syncAndRender(fixture, cursorX, cursorY) - SelectPortalServices.systemEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) + DomainPortalServices.systemSelectEngine.onFrame(ctx, fixture.viewportWidth, fixture.viewportHeight, 1f) } - assertFalse(SelectPortalServices.systemEngine.isOpenFor(ownerKey)) + assertFalse(DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey)) } private fun findRowByProperty(fixture: Fixture, property: StyleProperty): InspectorStyleEditorRowSnapshot = diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorTextEditingDomMigrationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorTextEditingDomMigrationTests.kt index c35e83f..bc3b108 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorTextEditingDomMigrationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorTextEditingDomMigrationTests.kt @@ -1,6 +1,5 @@ package org.dreamfinity.dsgl.core.overlay.system -import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPortalServices import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode @@ -14,6 +13,7 @@ import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.input.ClipboardAccess import org.dreamfinity.dsgl.core.input.ClipboardBridge import org.dreamfinity.dsgl.core.inspector.InspectorController +import org.dreamfinity.dsgl.core.overlay.DomainPortalServices import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.StyleEngine @@ -42,7 +42,7 @@ class InspectorTextEditingDomMigrationTests { FocusManager.clearFocus() KeyModifiers.sync(shift = false, control = false, meta = false) ClipboardBridge.install(null) - ColorPickerPortalServices.engine.closeAll() + DomainPortalServices.applicationColorPickerEngine.closeAll() StyleEngine.clearAllInspectorOverrides() StyleEngine.clearCache() } @@ -212,7 +212,7 @@ class InspectorTextEditingDomMigrationTests { private fun openInspectorAndSelectTarget(): Fixture { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val (root, target) = inspectedRoot() inspector.toggle() diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt index a666862..573563d 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt @@ -2,7 +2,6 @@ package org.dreamfinity.dsgl.core.overlay.system import org.dreamfinity.dsgl.core.colorpicker.ColorFormatMode import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupRequest -import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPortalServices import org.dreamfinity.dsgl.core.colorpicker.ColorPickerState import org.dreamfinity.dsgl.core.colorpicker.ColorPickerStyle import org.dreamfinity.dsgl.core.colorpicker.RgbChannelOrder @@ -16,6 +15,7 @@ import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController +import org.dreamfinity.dsgl.core.overlay.DomainPortalServices import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.render.RenderCommand import kotlin.test.Test @@ -39,13 +39,13 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker popup lifecycle is entry owned and stable`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPortalService() + val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() assertFalse(host.isSystemColorPickerOpen()) - assertFalse(ColorPickerPortalServices.engine.isOpen()) + assertFalse(DomainPortalServices.applicationColorPickerEngine.isOpen()) - pickerHost.open(anchorRect = Rect(40, 42, 20, 18), title = "Popup", state = popupState()) + pickerService.open(anchorRect = Rect(40, 42, 20, 18), title = "Popup", state = popupState()) host.onInputFrame(960, 720) host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 44, cursorY = 48, inspectorPointerCaptured = false) val firstNode = host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry node missing") @@ -53,7 +53,7 @@ class SystemOverlayColorPickerEntryTests { assertEquals(OverlayOwnerScope.System, host.debugSystemColorPickerPopupOwnerScope()) assertTrue(firstState.active) assertNotNull(firstState.panelState.currentRectOrNull()) - assertFalse(ColorPickerPortalServices.engine.isOpen()) + assertFalse(DomainPortalServices.applicationColorPickerEngine.isOpen()) host.onInputFrame(960, 720) host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 50, cursorY = 56, inspectorPointerCaptured = false) @@ -62,12 +62,12 @@ class SystemOverlayColorPickerEntryTests { assertSame(firstNode, secondNode) assertSame(firstState, secondState) - pickerHost.close() + pickerService.close() host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = 50, cursorY = 56, inspectorPointerCaptured = false) assertFalse(host.isSystemColorPickerOpen()) assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerPopup)) - pickerHost.open(anchorRect = Rect(40, 42, 20, 18), title = "Popup", state = popupState()) + pickerService.open(anchorRect = Rect(40, 42, 20, 18), title = "Popup", state = popupState()) host.onInputFrame(960, 720) host.syncFrame(root, inspectedLayoutRevision = 4L, cursorX = 52, cursorY = 58, inspectorPointerCaptured = false) val reopenedNode = host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry node missing") @@ -80,12 +80,12 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker entry path stays independent from application runtime popup path`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPortalService() + val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() val appOwner = Any() try { - ColorPickerPortalServices.engine.open( + DomainPortalServices.applicationColorPickerEngine.open( ColorPickerPopupRequest( owner = appOwner, ownerScope = OverlayOwnerScope.Application, @@ -94,9 +94,9 @@ class SystemOverlayColorPickerEntryTests { state = popupState(), ), ) - assertTrue(ColorPickerPortalServices.engine.isOpenFor(appOwner)) + assertTrue(DomainPortalServices.applicationColorPickerEngine.isOpenFor(appOwner)) - pickerHost.open(anchorRect = Rect(40, 42, 20, 18), title = "Popup", state = popupState()) + pickerService.open(anchorRect = Rect(40, 42, 20, 18), title = "Popup", state = popupState()) host.onInputFrame(960, 720) host.syncFrame( root, @@ -113,9 +113,9 @@ class SystemOverlayColorPickerEntryTests { assertFalse(styleTypes.contains("dsgl-system-raw-render-command")) assertEquals(OverlayOwnerScope.System, host.debugSystemColorPickerPopupOwnerScope()) assertTrue(host.isSystemColorPickerOpen()) - assertTrue(ColorPickerPortalServices.engine.isOpenFor(appOwner)) + assertTrue(DomainPortalServices.applicationColorPickerEngine.isOpenFor(appOwner)) - pickerHost.close() + pickerService.close() host.syncFrame( root, inspectedLayoutRevision = 2L, @@ -124,20 +124,20 @@ class SystemOverlayColorPickerEntryTests { inspectorPointerCaptured = false, ) assertFalse(host.isSystemColorPickerOpen()) - assertTrue(ColorPickerPortalServices.engine.isOpenFor(appOwner)) + assertTrue(DomainPortalServices.applicationColorPickerEngine.isOpenFor(appOwner)) } finally { - pickerHost.close() - ColorPickerPortalServices.engine.close(appOwner) + pickerService.close() + DomainPortalServices.applicationColorPickerEngine.close(appOwner) } } @Test fun `system picker popup drag uses persistent entry drag session and keeps node stable`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPortalService() + val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() - pickerHost.open(anchorRect = Rect(80, 90, 20, 18), title = "Popup", state = popupState()) + pickerService.open(anchorRect = Rect(80, 90, 20, 18), title = "Popup", state = popupState()) host.onInputFrame(1200, 800) host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 88, cursorY = 98, inspectorPointerCaptured = false) @@ -196,10 +196,10 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker popup survives routine sync updates without remount during drag`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPortalService() + val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() - pickerHost.open(anchorRect = Rect(120, 100, 20, 18), title = "Popup", state = popupState()) + pickerService.open(anchorRect = Rect(120, 100, 20, 18), title = "Popup", state = popupState()) host.onInputFrame(1200, 800) host.syncFrame( root, @@ -236,10 +236,10 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker popup close button closes entry through panel panel`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPortalService() + val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() - pickerHost.open(anchorRect = Rect(80, 86, 20, 18), title = "Popup", state = popupState()) + pickerService.open(anchorRect = Rect(80, 86, 20, 18), title = "Popup", state = popupState()) host.onInputFrame(1200, 800) host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 84, cursorY = 92, inspectorPointerCaptured = false) val closeRect = host.debugSystemColorPickerCloseRect() ?: error("close rect missing") @@ -258,11 +258,11 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker keyboard-open path uses valid viewport after input frame sync`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPortalService() + val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() val anchor = Rect(360, 220, 1, 1) - pickerHost.open(anchorRect = anchor, title = "Popup", state = popupState()) + pickerService.open(anchorRect = anchor, title = "Popup", state = popupState()) host.onInputFrame(1200, 800) host.syncFrame( root, @@ -283,10 +283,10 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker entry mounts native body subtree without command bridge`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPortalService() + val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() - pickerHost.open(anchorRect = Rect(60, 70, 20, 18), title = "Popup", state = popupState()) + pickerService.open(anchorRect = Rect(60, 70, 20, 18), title = "Popup", state = popupState()) host.onInputFrame(1200, 800) host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 64, cursorY = 74, inspectorPointerCaptured = false) @@ -301,11 +301,11 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker color field drag updates color continuously`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPortalService() + val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() val previews = ArrayList() - pickerHost.open( + pickerService.open( anchorRect = Rect(80, 90, 20, 18), title = "Popup", state = popupState(), @@ -350,11 +350,11 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker current swatch click commits once without double apply`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPortalService() + val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() var commits = 0 - pickerHost.open( + pickerService.open( anchorRect = Rect(80, 90, 20, 18), title = "Popup", state = @@ -379,12 +379,12 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker recent swatch click previews once without double apply`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPortalService() + val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() val initial = popupState() val previews = ArrayList() - pickerHost.open( + pickerService.open( anchorRect = Rect(80, 90, 20, 18), title = "Popup", state = initial, @@ -444,7 +444,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker sync state updates current swatch without drag nudge`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPortalService() + val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() val initial = popupState() val updated = @@ -453,7 +453,7 @@ class SystemOverlayColorPickerEntryTests { previous = initial.color, ) - pickerHost.open(anchorRect = Rect(80, 90, 20, 18), title = "Popup", state = initial) + pickerService.open(anchorRect = Rect(80, 90, 20, 18), title = "Popup", state = initial) host.onInputFrame(1200, 800) host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 88, cursorY = 98, inspectorPointerCaptured = false) @@ -462,7 +462,7 @@ class SystemOverlayColorPickerEntryTests { val beforeColor = resolveRectFillColor(host.paint(ctx), swatchRect) ?: error("before swatch fill missing") assertEquals(initial.color.toArgbInt(), beforeColor) - pickerHost.open(anchorRect = Rect(80, 90, 20, 18), title = "Popup", state = updated) + pickerService.open(anchorRect = Rect(80, 90, 20, 18), title = "Popup", state = updated) host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 88, cursorY = 98, inspectorPointerCaptured = false) host.render(ctx, 1200, 800) @@ -474,13 +474,13 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker sync state updates rgb order button selected visuals without drag nudge`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPortalService() + val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() val style = ColorPickerStyle() val initial = popupState().copy(mode = ColorFormatMode.RGB, rgbOrder = RgbChannelOrder.RGBA) val updated = initial.copy(rgbOrder = RgbChannelOrder.ARGB) - pickerHost.open(anchorRect = Rect(80, 90, 20, 18), title = "Popup", state = initial) + pickerService.open(anchorRect = Rect(80, 90, 20, 18), title = "Popup", state = initial) host.onInputFrame(1200, 800) host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 2, cursorY = 2, inspectorPointerCaptured = false) @@ -493,7 +493,7 @@ class SystemOverlayColorPickerEntryTests { assertEquals(style.buttonActiveColor, rgbaBefore) assertEquals(style.buttonBackgroundColor, argbBefore) - pickerHost.open(anchorRect = Rect(80, 90, 20, 18), title = "Popup", state = updated) + pickerService.open(anchorRect = Rect(80, 90, 20, 18), title = "Popup", state = updated) host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 2, cursorY = 2, inspectorPointerCaptured = false) host.render(ctx, 1200, 800) @@ -508,10 +508,10 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker hue and alpha drag update state`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPortalService() + val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() - pickerHost.open(anchorRect = Rect(80, 90, 20, 18), title = "Popup", state = popupState()) + pickerService.open(anchorRect = Rect(80, 90, 20, 18), title = "Popup", state = popupState()) host.onInputFrame(1200, 800) host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 88, cursorY = 98, inspectorPointerCaptured = false) @@ -556,10 +556,10 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker text input and mode controls stay synchronized`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPortalService() + val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() - pickerHost.open(anchorRect = Rect(120, 120, 20, 18), title = "Popup", state = popupState()) + pickerService.open(anchorRect = Rect(120, 120, 20, 18), title = "Popup", state = popupState()) host.onInputFrame(1200, 800) host.syncFrame( root, @@ -616,10 +616,10 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker input focus retargets by semantic key across rgb order switch`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPortalService() + val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() - pickerHost.open(anchorRect = Rect(120, 120, 20, 18), title = "Popup", state = popupState()) + pickerService.open(anchorRect = Rect(120, 120, 20, 18), title = "Popup", state = popupState()) host.onInputFrame(1200, 800) host.syncFrame( root, @@ -660,12 +660,12 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker rgb order buttons use dom semantic actions without double apply`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPortalService() + val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() var previews = 0 var commits = 0 - pickerHost.open( + pickerService.open( anchorRect = Rect(120, 120, 20, 18), title = "Popup", state = popupState().copy(mode = ColorFormatMode.RGB, rgbOrder = RgbChannelOrder.RGBA), @@ -708,10 +708,10 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker mode trigger toggles dropdown through dom path without double apply`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPortalService() + val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() - pickerHost.open(anchorRect = Rect(120, 120, 20, 18), title = "Popup", state = popupState()) + pickerService.open(anchorRect = Rect(120, 120, 20, 18), title = "Popup", state = popupState()) host.onInputFrame(1200, 800) host.syncFrame( root, @@ -755,12 +755,12 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker mode option click changes mode and closes dropdown via dom path`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPortalService() + val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() var previews = 0 var commits = 0 - pickerHost.open( + pickerService.open( anchorRect = Rect(120, 120, 20, 18), title = "Popup", state = popupState(), @@ -810,10 +810,10 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker mode dropdown is mounted in transient lane and stays interactive`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPortalService() + val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() - pickerHost.open(anchorRect = Rect(120, 120, 20, 18), title = "Popup", state = popupState()) + pickerService.open(anchorRect = Rect(120, 120, 20, 18), title = "Popup", state = popupState()) host.onInputFrame(1200, 800) host.syncFrame( root, @@ -862,10 +862,10 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker pipette keeps system overlay visible and uses transient lane`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPortalService() + val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() - pickerHost.open(anchorRect = Rect(140, 140, 20, 18), title = "Popup", state = popupState()) + pickerService.open(anchorRect = Rect(140, 140, 20, 18), title = "Popup", state = popupState()) host.onInputFrame(1200, 800) host.syncFrame( root, @@ -909,13 +909,13 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker pipette transient entry emits visible tooltip commands`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPortalService() + val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() val gridColor = 0x7F4C93FF val checkerLight = 0x7F0AA0A0 val checkerDark = 0x7F104040 - pickerHost.open( + pickerService.open( anchorRect = Rect(140, 140, 20, 18), title = "Popup", state = popupState(), diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntryInfrastructureTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntryInfrastructureTests.kt index 669b650..3043b4f 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntryInfrastructureTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntryInfrastructureTests.kt @@ -74,9 +74,9 @@ class SystemOverlayEntryInfrastructureTests { @Test fun `color picker entry keeps panel state and identity stable across routine updates`() { val host = SystemOverlayHost(InspectorController()) - val pickerHost = host.systemInspectorColorPickerPortalService() + val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() - pickerHost.open(anchorRect = Rect(36, 44, 20, 18), title = "Popup", state = popupState()) + pickerService.open(anchorRect = Rect(36, 44, 20, 18), title = "Popup", state = popupState()) try { host.syncFrame( root, @@ -109,7 +109,7 @@ class SystemOverlayEntryInfrastructureTests { assertTrue(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerPopup)) assertTrue(host.debugActivePortalEntryIds().contains("system.ColorPickerPopup")) } finally { - pickerHost.close() + pickerService.close() } host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = 60, cursorY = 65, inspectorPointerCaptured = false) @@ -121,10 +121,10 @@ class SystemOverlayEntryInfrastructureTests { fun `entry ordering stays explicit when both system entries are active`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - val pickerHost = host.systemInspectorColorPickerPortalService() + val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() inspector.toggle() - pickerHost.open(anchorRect = Rect(36, 44, 20, 18), title = "Popup", state = popupState()) + pickerService.open(anchorRect = Rect(36, 44, 20, 18), title = "Popup", state = popupState()) try { host.syncFrame( root, @@ -143,7 +143,7 @@ class SystemOverlayEntryInfrastructureTests { ) } finally { inspector.deactivate() - pickerHost.close() + pickerService.close() } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt index 5851297..b1372b1 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt @@ -14,6 +14,7 @@ import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.inspector.InspectorMode import org.dreamfinity.dsgl.core.inspector.InspectorPanelState import org.dreamfinity.dsgl.core.inspector.internal.SystemInspectorOverlayNode +import org.dreamfinity.dsgl.core.overlay.DomainPortalServices import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.Display @@ -92,7 +93,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `live inspector path is native system-overlay entry and anti-legacy guarded`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot() inspector.toggle() @@ -118,7 +119,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `expanded inspector paints occluder above full highlight geometry`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val initialRoot = inspectedRoot() inspector.toggle() @@ -168,7 +169,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector runtime interaction path supports selection controls and system-owned color edit`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot() inspector.toggle() @@ -246,7 +247,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector minimize restore and close reopen remain stable`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot() inspector.toggle() @@ -362,7 +363,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector native path preserves scroll and scrollbar drag behavior`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -412,7 +413,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `scrollbar drag release over control ends capture and does not trigger control click`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -465,7 +466,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `scrollbar drag release outside inspector consumes mouse up and stops capture`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -529,11 +530,11 @@ class SystemOverlayInspectorNativeEntryTests { val appOwner = Any() val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot() try { - ColorPickerPortalServices.engine.open( + DomainPortalServices.applicationColorPickerEngine.open( ColorPickerPopupRequest( owner = appOwner, ownerScope = OverlayOwnerScope.Application, @@ -542,7 +543,7 @@ class SystemOverlayInspectorNativeEntryTests { state = popupState(), ), ) - assertTrue(ColorPickerPortalServices.engine.isOpenFor(appOwner)) + assertTrue(DomainPortalServices.applicationColorPickerEngine.isOpenFor(appOwner)) inspector.toggle() host.onInputFrame(1280, 720) @@ -586,9 +587,9 @@ class SystemOverlayInspectorNativeEntryTests { assertTrue(host.isSystemColorPickerOpen()) assertEquals(OverlayOwnerScope.System, host.debugSystemColorPickerPopupOwnerScope()) - assertTrue(ColorPickerPortalServices.engine.isOpenFor(appOwner)) + assertTrue(DomainPortalServices.applicationColorPickerEngine.isOpenFor(appOwner)) - host.systemInspectorColorPickerPortalService().close() + host.systemInspectorColorPickerService().close() host.syncFrame( root, inspectedLayoutRevision = 4L, @@ -597,10 +598,10 @@ class SystemOverlayInspectorNativeEntryTests { inspectorPointerCaptured = false, ) assertFalse(host.isSystemColorPickerOpen()) - assertTrue(ColorPickerPortalServices.engine.isOpenFor(appOwner)) + assertTrue(DomainPortalServices.applicationColorPickerEngine.isOpenFor(appOwner)) } finally { - host.systemInspectorColorPickerPortalService().close() - ColorPickerPortalServices.engine.close(appOwner) + host.systemInspectorColorPickerService().close() + DomainPortalServices.applicationColorPickerEngine.close(appOwner) } } @@ -608,7 +609,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector-opened system color picker top controls expose hover feedback`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot() fun sync(revision: Long, cursorX: Int, cursorY: Int) { @@ -678,7 +679,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector-opened system color picker mode dropdown options hover and click reliably`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot() fun sync(revision: Long, cursorX: Int, cursorY: Int) { @@ -765,7 +766,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector native body content remains clipped in narrow viewport`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -853,7 +854,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector expanded body renders baseline info text`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot() inspector.toggle() @@ -886,7 +887,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector clipped body blocks hidden row input and accepts visible portion`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -992,7 +993,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector body consumes generic scroll viewport and content state`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -1038,7 +1039,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector wheel scrolling works when hovering interactive input`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -1102,7 +1103,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector shift wheel does not consume vertical wheel path`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -1145,7 +1146,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector wheel scrolling remains symmetric across rebuilds`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -1235,7 +1236,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector thumb drag remains active across rebuild without controller pointer capture`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -1308,7 +1309,7 @@ class SystemOverlayInspectorNativeEntryTests { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot() inspector.toggle() @@ -1440,7 +1441,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector consumer scroll reacts on frame update without viewport resize`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -1484,7 +1485,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector consumer thumb drag remains smooth and stable on release`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() inspector.toggle() @@ -1563,7 +1564,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector consumer fast thumb drag to boundary stays stable`() { val inspector = InspectorController() val host = SystemOverlayHost(inspector) - inspector.installColorPickerHost(host.systemInspectorColorPickerPortalService()) + inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() inspector.toggle() diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/DomainPortalServicesOwnershipTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/DomainPortalServicesOwnershipTests.kt new file mode 100644 index 0000000..9fdcc34 --- /dev/null +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/DomainPortalServicesOwnershipTests.kt @@ -0,0 +1,59 @@ +package org.dreamfinity.dsgl.core.select + +import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.overlay.DomainPortalServices +import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class DomainPortalServicesOwnershipTests { + @AfterTest + fun cleanup() { + DomainPortalServices.closeAllSelects() + } + + @Test + fun `application-scoped request opens application engine only`() { + val owner = Any() + DomainPortalServices.openSelect(request(owner, OverlayOwnerScope.Application)) + + assertTrue(DomainPortalServices.applicationSelectEngine.isOpenFor(owner)) + assertFalse(DomainPortalServices.systemSelectEngine.isOpenFor(owner)) + assertTrue(DomainPortalServices.isSelectOpenFor(owner)) + } + + @Test + fun `system-scoped request opens system engine only`() { + val owner = Any() + DomainPortalServices.openSelect(request(owner, OverlayOwnerScope.System)) + + assertFalse(DomainPortalServices.applicationSelectEngine.isOpenFor(owner)) + assertTrue(DomainPortalServices.systemSelectEngine.isOpenFor(owner)) + assertTrue(DomainPortalServices.isSelectOpenFor(owner)) + } + + @Test + fun `opening same owner in another scope switches engine ownership`() { + val owner = Any() + DomainPortalServices.openSelect(request(owner, OverlayOwnerScope.Application)) + assertTrue(DomainPortalServices.applicationSelectEngine.isOpenFor(owner)) + assertFalse(DomainPortalServices.systemSelectEngine.isOpenFor(owner)) + + DomainPortalServices.openSelect(request(owner, OverlayOwnerScope.System)) + assertFalse(DomainPortalServices.applicationSelectEngine.isOpenFor(owner)) + assertTrue(DomainPortalServices.systemSelectEngine.isOpenFor(owner)) + } + + private fun request(owner: Any, scope: OverlayOwnerScope): SelectOpenRequest = + SelectOpenRequest( + owner = owner, + modelToken = 1L, + entries = listOf(SelectEntry.Option("a", labelProvider = { "Alpha" })), + selectedId = "a", + anchorRect = Rect(10, 10, 100, 20), + closeOnSelect = true, + ownerScope = scope, + ) +} diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalServicesOwnershipTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalServicesOwnershipTests.kt deleted file mode 100644 index 8ca1d95..0000000 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalServicesOwnershipTests.kt +++ /dev/null @@ -1,58 +0,0 @@ -package org.dreamfinity.dsgl.core.select - -import org.dreamfinity.dsgl.core.dom.layout.Rect -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class SelectPortalServicesOwnershipTests { - @AfterTest - fun cleanup() { - SelectPortalServices.closeAll() - } - - @Test - fun `application-scoped request opens application engine only`() { - val owner = Any() - SelectPortalServices.open(request(owner, OverlayOwnerScope.Application)) - - assertTrue(SelectPortalServices.applicationEngine.isOpenFor(owner)) - assertFalse(SelectPortalServices.systemEngine.isOpenFor(owner)) - assertTrue(SelectPortalServices.isOpenFor(owner)) - } - - @Test - fun `system-scoped request opens system engine only`() { - val owner = Any() - SelectPortalServices.open(request(owner, OverlayOwnerScope.System)) - - assertFalse(SelectPortalServices.applicationEngine.isOpenFor(owner)) - assertTrue(SelectPortalServices.systemEngine.isOpenFor(owner)) - assertTrue(SelectPortalServices.isOpenFor(owner)) - } - - @Test - fun `opening same owner in another scope switches engine ownership`() { - val owner = Any() - SelectPortalServices.open(request(owner, OverlayOwnerScope.Application)) - assertTrue(SelectPortalServices.applicationEngine.isOpenFor(owner)) - assertFalse(SelectPortalServices.systemEngine.isOpenFor(owner)) - - SelectPortalServices.open(request(owner, OverlayOwnerScope.System)) - assertFalse(SelectPortalServices.applicationEngine.isOpenFor(owner)) - assertTrue(SelectPortalServices.systemEngine.isOpenFor(owner)) - } - - private fun request(owner: Any, scope: OverlayOwnerScope): SelectOpenRequest = - SelectOpenRequest( - owner = owner, - modelToken = 1L, - entries = listOf(SelectEntry.Option("a", labelProvider = { "Alpha" })), - selectedId = "a", - anchorRect = Rect(10, 10, 100, 20), - closeOnSelect = true, - ownerScope = scope, - ) -} From 86d8c4ee39263ae11258d9d944e47bf6c9be9a31 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Wed, 27 May 2026 13:48:40 +0300 Subject: [PATCH 64/78] migrating overlay layers and hosts to ScreenDomainSurfaces; removing outdated classes and tests --- .../dsgl/mcForge1710/DsglScreenHost.kt | 92 +++---- .../ScreenDomainSurfaceOrchestrator.kt | 56 ++-- .../DsglScreenHostDomainOrchestrationTests.kt | 63 +++-- .../ColorPickerPortalController.kt | 6 +- .../modal/internal/ModalPortalController.kt | 7 +- .../core/debug/OverlayDebugControlHost.kt | 9 +- .../dsgl/core/debug/OverlayLayerDebugState.kt | 36 ++- .../core/overlay/ApplicationOverlayHost.kt | 8 +- .../overlay/ApplicationOverlayRootNode.kt | 2 +- .../ColorPickerPopupOverlayOwnership.kt | 4 +- ...erlayLayerHost.kt => DomainSurfaceHost.kt} | 4 +- .../core/overlay/OverlayLayerContracts.kt | 124 --------- .../dsgl/core/overlay/PortalHostContracts.kt | 6 +- .../core/overlay/ScreenDomainContracts.kt | 136 ++++++++++ .../overlay/system/SystemOverlayEntries.kt | 5 +- .../core/overlay/system/SystemOverlayHost.kt | 13 +- .../overlay/system/SystemOverlayRootNode.kt | 4 +- .../core/select/SelectPortalController.kt | 6 +- .../ColorPickerPopupEngineTests.kt | 11 +- .../debug/OverlayDebugControlHostTests.kt | 10 +- .../overlay/LiveLayerInteractionPathTests.kt | 127 ++++----- .../overlay/OverlayLayerContractsTests.kt | 248 ----------------- .../core/overlay/PortalHostContractsTests.kt | 10 +- .../overlay/ScreenDomainContractsTests.kt | 249 ++++++++++++++++++ 24 files changed, 632 insertions(+), 604 deletions(-) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/{OverlayLayerHost.kt => DomainSurfaceHost.kt} (92%) delete mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContracts.kt create mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ScreenDomainContracts.kt delete mode 100644 core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContractsTests.kt create mode 100644 core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/ScreenDomainContractsTests.kt diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index de7db14..ac89ea2 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -7,16 +7,13 @@ import net.minecraft.client.gui.GuiScreen import org.dreamfinity.dsgl.core.DomTree import org.dreamfinity.dsgl.core.DsglWindow import org.dreamfinity.dsgl.core.HotReloadBridge -import org.dreamfinity.dsgl.core.animation.StyleAnimationEngine +import org.dreamfinity.dsgl.core.animation.* import org.dreamfinity.dsgl.core.colorpicker.* import org.dreamfinity.dsgl.core.debug.OverlayDebugControlHost import org.dreamfinity.dsgl.core.debug.OverlayLayerDebugState -import org.dreamfinity.dsgl.core.dnd.DndRuntime +import org.dreamfinity.dsgl.core.dnd.* import org.dreamfinity.dsgl.core.dom.DOMNode -import org.dreamfinity.dsgl.core.dom.elements.ColorPickerInlineNode -import org.dreamfinity.dsgl.core.dom.elements.RangeInputNode -import org.dreamfinity.dsgl.core.dom.elements.SingleLineInputNode -import org.dreamfinity.dsgl.core.dom.elements.TextAreaNode +import org.dreamfinity.dsgl.core.dom.elements.* import org.dreamfinity.dsgl.core.event.* import org.dreamfinity.dsgl.core.hooks.HookHotReloadRemountException import org.dreamfinity.dsgl.core.hooks.HookRenderSessionMode @@ -26,13 +23,12 @@ import org.dreamfinity.dsgl.core.host.rawMouseToDsglX import org.dreamfinity.dsgl.core.host.rawMouseToDsglY import org.dreamfinity.dsgl.core.input.ClipboardAccess import org.dreamfinity.dsgl.core.input.ClipboardBridge -import org.dreamfinity.dsgl.core.inspector.InspectorController -import org.dreamfinity.dsgl.core.inspector.InspectorMode +import org.dreamfinity.dsgl.core.inspector.* import org.dreamfinity.dsgl.core.overlay.ApplicationOverlayHost -import org.dreamfinity.dsgl.core.overlay.OverlayLayerContracts -import org.dreamfinity.dsgl.core.overlay.OverlayLayerHost +import org.dreamfinity.dsgl.core.overlay.DomainSurfaceHost import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope -import org.dreamfinity.dsgl.core.overlay.UiLayerId +import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurface +import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces import org.dreamfinity.dsgl.core.overlay.appendPortalOverlayCommands import org.dreamfinity.dsgl.core.overlay.captureColorPickerEyedropperSample import org.dreamfinity.dsgl.core.overlay.closeFloatingPortals @@ -47,17 +43,14 @@ import org.dreamfinity.dsgl.core.overlay.hasOpenSelectPortal import org.dreamfinity.dsgl.core.overlay.syncPortalFrame import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayHost import org.dreamfinity.dsgl.core.render.RenderCommand -import org.dreamfinity.dsgl.core.style.Display -import org.dreamfinity.dsgl.core.style.StyleEngine +import org.dreamfinity.dsgl.core.style.* import org.lwjgl.input.Keyboard import org.lwjgl.input.Mouse import java.io.File import java.time.Instant import java.time.ZoneId -import java.util.ArrayList import java.util.Collections import java.util.IdentityHashMap -import java.util.LinkedHashMap /** * Minecraft 1.7.10 host that owns UI lifecycle and boilerplate. @@ -348,10 +341,10 @@ abstract class DsglScreenHost( if (inspectorPointerCaptured) { inspector.onCapturedPointerMove(dsglMouseX, dsglMouseY, lastWidth, lastHeight) } - val appOverlayRenderEnabled = OverlayLayerDebugState.isRenderEnabled(UiLayerId.ApplicationOverlay) - val systemOverlayRenderEnabled = OverlayLayerDebugState.isRenderEnabled(UiLayerId.SystemOverlay) - val appOverlayInputEnabled = OverlayLayerDebugState.isInputEnabled(UiLayerId.ApplicationOverlay) - val systemOverlayInputEnabled = OverlayLayerDebugState.isInputEnabled(UiLayerId.SystemOverlay) + val appOverlayRenderEnabled = OverlayLayerDebugState.isRenderEnabled(ScreenDomainSurfaces.ApplicationPortal) + val systemOverlayRenderEnabled = OverlayLayerDebugState.isRenderEnabled(ScreenDomainSurfaces.SystemPortal) + val appOverlayInputEnabled = OverlayLayerDebugState.isInputEnabled(ScreenDomainSurfaces.ApplicationPortal) + val systemOverlayInputEnabled = OverlayLayerDebugState.isInputEnabled(ScreenDomainSurfaces.SystemPortal) val inspectorBlocks = systemOverlayInputEnabled && ( @@ -575,7 +568,7 @@ abstract class DsglScreenHost( systemPortal = systemOverlayCommandsBuffer, debugRoot = debugOverlayCommands, out = stagingCommandsBuffer, - shouldRenderLayer = OverlayLayerDebugState::isRenderEnabled, + shouldRenderSurface = OverlayLayerDebugState::isRenderEnabled, ) val keepPrevious = shouldKeepPreviousFrameCommands( @@ -951,7 +944,7 @@ abstract class DsglScreenHost( refreshActiveColorSamplerOwner(tree.root) } - private fun runOverlayInputFrame(host: OverlayLayerHost) { + private fun runOverlayInputFrame(host: DomainSurfaceHost) { host.onInputFrame(lastWidth, lastHeight) } @@ -1079,10 +1072,12 @@ abstract class DsglScreenHost( ): Boolean { val consumedBy = domainOrchestrator.firstInputConsumer( - canConsume = { layer -> - when (layer) { - UiLayerId.Debug -> debugOverlayHost.handleKeyDown(keyCode, keyChar) - UiLayerId.SystemOverlay -> + canConsume = { surface -> + when (surface) { + ScreenDomainSurfaces.DebugPortal -> false + ScreenDomainSurfaces.DebugRoot -> debugOverlayHost.handleKeyDown(keyCode, keyChar) + + ScreenDomainSurfaces.SystemPortal -> consumeSystemOverlayKeyDown( keyCode = keyCode, keyChar = keyChar, @@ -1090,11 +1085,11 @@ abstract class DsglScreenHost( inspectorMouseY = inspectorMouseY, ) - UiLayerId.ApplicationOverlay -> consumeApplicationOverlayKeyDown(keyCode, keyChar) - UiLayerId.ApplicationRoot -> false + ScreenDomainSurfaces.ApplicationPortal -> consumeApplicationOverlayKeyDown(keyCode, keyChar) + else -> false } }, - isLayerInputEnabled = OverlayLayerDebugState::isInputEnabled, + isSurfaceInputEnabled = OverlayLayerDebugState::isInputEnabled, ) return consumedBy != null } @@ -1147,9 +1142,10 @@ abstract class DsglScreenHost( val buttonPressed = Mouse.getEventButtonState() val consumedBy = domainOrchestrator.firstInputConsumer( - canConsume = { layer -> - when (layer) { - UiLayerId.Debug -> + canConsume = { surface -> + when (surface) { + ScreenDomainSurfaces.DebugPortal -> false + ScreenDomainSurfaces.DebugRoot -> consumeDebugPointerEvent( mouseX = mouseX, mouseY = mouseY, @@ -1159,7 +1155,7 @@ abstract class DsglScreenHost( buttonPressed = buttonPressed, ) - UiLayerId.SystemOverlay -> + ScreenDomainSurfaces.SystemPortal -> consumeSystemOverlayPointerEvent( mouseX = mouseX, mouseY = mouseY, @@ -1169,7 +1165,7 @@ abstract class DsglScreenHost( buttonPressed = buttonPressed, ) - UiLayerId.ApplicationOverlay -> + ScreenDomainSurfaces.ApplicationPortal -> consumeApplicationOverlayPointerEvent( mouseX = mouseX, mouseY = mouseY, @@ -1179,10 +1175,10 @@ abstract class DsglScreenHost( buttonPressed = buttonPressed, ) - UiLayerId.ApplicationRoot -> false + else -> false } }, - isLayerInputEnabled = OverlayLayerDebugState::isInputEnabled, + isSurfaceInputEnabled = OverlayLayerDebugState::isInputEnabled, ) return consumedBy != null } @@ -1371,8 +1367,8 @@ abstract class DsglScreenHost( } private fun appendInlineColorPickerOverlayCommands(out: MutableList) { - val layer = OverlayLayerContracts.resolveTransientLayer(OverlayOwnerScope.Application) - if (layer != UiLayerId.ApplicationOverlay) return + val surface = ScreenDomainSurfaces.portalSurfaceForOwner(OverlayOwnerScope.Application) + if (surface != ScreenDomainSurfaces.ApplicationPortal) return if (activeColorSamplerOwner is ActiveColorSamplerOwner.Inline) { val inline = activeInlineColorSamplerNode ?: return if (!inline.wantsGlobalPointerInput()) return @@ -1386,11 +1382,11 @@ abstract class DsglScreenHost( private fun captureColorPickerEyedropperSamples() { refreshActiveColorSamplerOwner(domTree?.root) - if (OverlayLayerContracts.resolveTransientLayer(OverlayOwnerScope.System) == UiLayerId.SystemOverlay) { + if (ScreenDomainSurfaces.portalSurfaceForOwner(OverlayOwnerScope.System) == ScreenDomainSurfaces.SystemPortal) { systemOverlayHost.captureSystemColorPickerEyedropperSample() } - if (OverlayLayerContracts.resolveTransientLayer(OverlayOwnerScope.Application) != - UiLayerId.ApplicationOverlay + if (ScreenDomainSurfaces.portalSurfaceForOwner(OverlayOwnerScope.Application) != + ScreenDomainSurfaces.ApplicationPortal ) { return } @@ -1446,29 +1442,33 @@ abstract class DsglScreenHost( internal fun debugComposeDomainPaintCommandsForTests( applicationRoot: List, applicationPortal: List, + systemRoot: List = emptyList(), systemPortal: List, debugRoot: List, - shouldRenderLayer: (UiLayerId) -> Boolean = { true }, + debugPortal: List = emptyList(), + shouldRenderSurface: (ScreenDomainSurface) -> Boolean = { true }, ): List { val out = ArrayList() domainOrchestrator.composePaintCommands( applicationRoot = applicationRoot, applicationPortal = applicationPortal, + systemRoot = systemRoot, systemPortal = systemPortal, debugRoot = debugRoot, + debugPortal = debugPortal, out = out, - shouldRenderLayer = shouldRenderLayer, + shouldRenderSurface = shouldRenderSurface, ) return out } internal fun debugFirstDomainInputConsumerForTests( - canConsume: (UiLayerId) -> Boolean, - isLayerInputEnabled: (UiLayerId) -> Boolean = { true }, - ): UiLayerId? = + canConsume: (ScreenDomainSurface) -> Boolean, + isSurfaceInputEnabled: (ScreenDomainSurface) -> Boolean = { true }, + ): ScreenDomainSurface? = domainOrchestrator.firstInputConsumer( canConsume = canConsume, - isLayerInputEnabled = isLayerInputEnabled, + isSurfaceInputEnabled = isSurfaceInputEnabled, ) private fun setDragCapture(target: DOMNode) { diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/ScreenDomainSurfaceOrchestrator.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/ScreenDomainSurfaceOrchestrator.kt index 814e6cf..cb27871 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/ScreenDomainSurfaceOrchestrator.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/ScreenDomainSurfaceOrchestrator.kt @@ -1,63 +1,45 @@ package org.dreamfinity.dsgl.mcForge1710 -import org.dreamfinity.dsgl.core.overlay.OverlayLayerContracts -import org.dreamfinity.dsgl.core.overlay.UiLayerId +import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurface +import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces import org.dreamfinity.dsgl.core.render.RenderCommand internal class ScreenDomainSurfaceOrchestrator { - private val paintSurfaces: List = - OverlayLayerContracts.paintOrder.map(RuntimeDomainSurface::fromLayer) - private val inputSurfaces: List = - OverlayLayerContracts.inputPriority.map(RuntimeDomainSurface::fromLayer) + private val paintSurfaces: List = ScreenDomainSurfaces.paintOrder + private val inputSurfaces: List = ScreenDomainSurfaces.inputPriority fun composePaintCommands( applicationRoot: List, applicationPortal: List, + systemRoot: List = emptyList(), systemPortal: List, debugRoot: List, + debugPortal: List = emptyList(), out: MutableList, - shouldRenderLayer: (UiLayerId) -> Boolean = { true }, + shouldRenderSurface: (ScreenDomainSurface) -> Boolean = { true }, ) { out.clear() paintSurfaces.forEach { surface -> - if (!shouldRenderLayer(surface.layer)) return@forEach + if (!shouldRenderSurface(surface)) return@forEach when (surface) { - RuntimeDomainSurface.ApplicationRoot -> out.addAll(applicationRoot) - RuntimeDomainSurface.ApplicationPortal -> out.addAll(applicationPortal) - RuntimeDomainSurface.SystemPortal -> out.addAll(systemPortal) - RuntimeDomainSurface.DebugRoot -> out.addAll(debugRoot) + ScreenDomainSurfaces.ApplicationRoot -> out.addAll(applicationRoot) + ScreenDomainSurfaces.ApplicationPortal -> out.addAll(applicationPortal) + ScreenDomainSurfaces.SystemRoot -> out.addAll(systemRoot) + ScreenDomainSurfaces.SystemPortal -> out.addAll(systemPortal) + ScreenDomainSurfaces.DebugRoot -> out.addAll(debugRoot) + ScreenDomainSurfaces.DebugPortal -> out.addAll(debugPortal) } } } fun firstInputConsumer( - canConsume: (UiLayerId) -> Boolean, - isLayerInputEnabled: (UiLayerId) -> Boolean = { true }, - ): UiLayerId? { + canConsume: (ScreenDomainSurface) -> Boolean, + isSurfaceInputEnabled: (ScreenDomainSurface) -> Boolean = { true }, + ): ScreenDomainSurface? { inputSurfaces.forEach { surface -> - if (!isLayerInputEnabled(surface.layer)) return@forEach - if (canConsume(surface.layer)) return surface.layer + if (!isSurfaceInputEnabled(surface)) return@forEach + if (canConsume(surface)) return surface } return null } } - -private enum class RuntimeDomainSurface( - val layer: UiLayerId, -) { - ApplicationRoot(UiLayerId.ApplicationRoot), - ApplicationPortal(UiLayerId.ApplicationOverlay), - SystemPortal(UiLayerId.SystemOverlay), - DebugRoot(UiLayerId.Debug), - ; - - companion object { - fun fromLayer(layer: UiLayerId): RuntimeDomainSurface = - when (layer) { - UiLayerId.ApplicationRoot -> ApplicationRoot - UiLayerId.ApplicationOverlay -> ApplicationPortal - UiLayerId.SystemOverlay -> SystemPortal - UiLayerId.Debug -> DebugRoot - } - } -} diff --git a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt index 415156c..f6068f2 100644 --- a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt +++ b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt @@ -3,7 +3,8 @@ package org.dreamfinity.dsgl.mcForge1710 import org.dreamfinity.dsgl.core.DomTree import org.dreamfinity.dsgl.core.DsglWindow import org.dreamfinity.dsgl.core.dom.elements.ContainerNode -import org.dreamfinity.dsgl.core.overlay.UiLayerId +import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurface +import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces import org.dreamfinity.dsgl.core.render.RenderCommand import org.junit.Assert.assertEquals import org.junit.Assert.assertNull @@ -11,18 +12,20 @@ import org.junit.Test class DsglScreenHostDomainOrchestrationTests { @Test - fun `host domain paint orchestration preserves current render order`() { + fun `host domain paint orchestration uses six surface render order`() { val host = createHost() val commands = host.debugComposeDomainPaintCommandsForTests( applicationRoot = listOf(command(1)), applicationPortal = listOf(command(2)), - systemPortal = listOf(command(3)), - debugRoot = listOf(command(4)), + systemRoot = listOf(command(3)), + systemPortal = listOf(command(4)), + debugRoot = listOf(command(5)), + debugPortal = listOf(command(6)), ) - assertEquals(listOf(1, 2, 3, 4), commandColors(commands)) + assertEquals(listOf(1, 2, 3, 4, 5, 6), commandColors(commands)) } @Test @@ -35,7 +38,7 @@ class DsglScreenHostDomainOrchestrationTests { applicationPortal = listOf(command(2)), systemPortal = listOf(command(3)), debugRoot = listOf(command(4)), - shouldRenderLayer = { layer -> layer != UiLayerId.ApplicationOverlay }, + shouldRenderSurface = { surface -> surface != ScreenDomainSurfaces.ApplicationPortal }, ) assertEquals(listOf(1, 3, 4), commandColors(commands)) @@ -44,19 +47,26 @@ class DsglScreenHostDomainOrchestrationTests { @Test fun `host domain input orchestration preserves current priority`() { val host = createHost() - val visited = ArrayList() + val visited = ArrayList() val consumed = host.debugFirstDomainInputConsumerForTests( canConsume = { layer -> visited += layer - layer == UiLayerId.ApplicationRoot + layer == ScreenDomainSurfaces.ApplicationRoot }, ) - assertEquals(UiLayerId.ApplicationRoot, consumed) + assertEquals(ScreenDomainSurfaces.ApplicationRoot, consumed) assertEquals( - listOf(UiLayerId.Debug, UiLayerId.SystemOverlay, UiLayerId.ApplicationOverlay, UiLayerId.ApplicationRoot), + listOf( + ScreenDomainSurfaces.DebugPortal, + ScreenDomainSurfaces.DebugRoot, + ScreenDomainSurfaces.SystemPortal, + ScreenDomainSurfaces.SystemRoot, + ScreenDomainSurfaces.ApplicationPortal, + ScreenDomainSurfaces.ApplicationRoot, + ), visited, ) } @@ -64,36 +74,51 @@ class DsglScreenHostDomainOrchestrationTests { @Test fun `host domain input orchestration blocks lower domains after consumption`() { val host = createHost() - val visited = ArrayList() + val visited = ArrayList() val consumed = host.debugFirstDomainInputConsumerForTests( canConsume = { layer -> visited += layer - layer == UiLayerId.SystemOverlay + layer == ScreenDomainSurfaces.SystemPortal }, ) - assertEquals(UiLayerId.SystemOverlay, consumed) - assertEquals(listOf(UiLayerId.Debug, UiLayerId.SystemOverlay), visited) + assertEquals(ScreenDomainSurfaces.SystemPortal, consumed) + assertEquals( + listOf( + ScreenDomainSurfaces.DebugPortal, + ScreenDomainSurfaces.DebugRoot, + ScreenDomainSurfaces.SystemPortal, + ), + visited, + ) } @Test fun `host domain input orchestration preserves debug input disables`() { val host = createHost() - val visited = ArrayList() + val visited = ArrayList() val consumed = host.debugFirstDomainInputConsumerForTests( canConsume = { layer -> visited += layer - layer == UiLayerId.Debug || layer == UiLayerId.ApplicationOverlay + layer == ScreenDomainSurfaces.DebugRoot || layer == ScreenDomainSurfaces.ApplicationPortal }, - isLayerInputEnabled = { layer -> layer != UiLayerId.Debug }, + isSurfaceInputEnabled = { surface -> surface != ScreenDomainSurfaces.DebugRoot }, ) - assertEquals(UiLayerId.ApplicationOverlay, consumed) - assertEquals(listOf(UiLayerId.SystemOverlay, UiLayerId.ApplicationOverlay), visited) + assertEquals(ScreenDomainSurfaces.ApplicationPortal, consumed) + assertEquals( + listOf( + ScreenDomainSurfaces.DebugPortal, + ScreenDomainSurfaces.SystemPortal, + ScreenDomainSurfaces.SystemRoot, + ScreenDomainSurfaces.ApplicationPortal, + ), + visited, + ) } @Test diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPortalController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPortalController.kt index 8bf9c51..ea641d3 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPortalController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPortalController.kt @@ -4,7 +4,6 @@ import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.OverlayLayerContracts import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.overlay.PortalDismissPolicy import org.dreamfinity.dsgl.core.overlay.PortalEntry @@ -17,13 +16,14 @@ import org.dreamfinity.dsgl.core.overlay.PortalFocusPolicy import org.dreamfinity.dsgl.core.overlay.PortalHost import org.dreamfinity.dsgl.core.overlay.PortalInputPolicy import org.dreamfinity.dsgl.core.overlay.PortalPointerDispatch +import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces import org.dreamfinity.dsgl.core.render.RenderCommand internal class ColorPickerPortalController( private val engine: ColorPickerPopupEngine, ) : PortalPointerDispatch { private val portalHost: PortalHost = - PortalHost(OverlayLayerContracts.portalSurfaceForOwner(OverlayOwnerScope.Application)) + PortalHost(ScreenDomainSurfaces.ApplicationPortal) private val entry: ColorPickerPortalEntry = ColorPickerPortalEntry(engine) init { @@ -89,7 +89,7 @@ private class ColorPickerPortalEntry( PortalEntryState( id = PortalEntryId("application.color-picker"), ownerToken = engine, - surface = OverlayLayerContracts.portalSurfaceForOwner(OverlayOwnerScope.Application), + surface = ScreenDomainSurfaces.ApplicationPortal, order = PortalEntryOrder(zIndex = 0), dismissPolicy = PortalDismissPolicy.EscapeOrOutsidePointerDown, inputPolicy = PortalInputPolicy.ManualOnly, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt index 6c63c38..a07526a 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt @@ -5,8 +5,6 @@ import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.event.EventBus import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.OverlayLayerContracts -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.overlay.PortalDismissPolicy import org.dreamfinity.dsgl.core.overlay.PortalEntry import org.dreamfinity.dsgl.core.overlay.PortalEntryBounds @@ -17,10 +15,11 @@ import org.dreamfinity.dsgl.core.overlay.PortalEntryState import org.dreamfinity.dsgl.core.overlay.PortalFocusPolicy import org.dreamfinity.dsgl.core.overlay.PortalHost import org.dreamfinity.dsgl.core.overlay.PortalInputPolicy +import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces internal class ModalPortalController { private val portalHost: PortalHost = - PortalHost(OverlayLayerContracts.portalSurfaceForOwner(OverlayOwnerScope.Application)) + PortalHost(ScreenDomainSurfaces.ApplicationPortal) private val entriesByPortalKey: LinkedHashMap = LinkedHashMap() fun sync(rootNode: DOMNode, viewportWidth: Int, viewportHeight: Int) { @@ -119,7 +118,7 @@ private class ModalPortalEntry( PortalEntryState( id = PortalEntryId("application.modal.$portalKey"), ownerToken = portalKey, - surface = OverlayLayerContracts.portalSurfaceForOwner(OverlayOwnerScope.Application), + surface = ScreenDomainSurfaces.ApplicationPortal, order = PortalEntryOrder(zIndex = -100), dismissPolicy = PortalDismissPolicy.None, inputPolicy = PortalInputPolicy.DomOnly, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt index 90c5258..ac56a9e 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt @@ -15,8 +15,9 @@ import org.dreamfinity.dsgl.core.dsl.button import org.dreamfinity.dsgl.core.dsl.div import org.dreamfinity.dsgl.core.dsl.text import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.OverlayLayerHost -import org.dreamfinity.dsgl.core.overlay.UiLayerId +import org.dreamfinity.dsgl.core.overlay.DomainSurfaceHost +import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurface +import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.StyleApplicationScope @@ -45,8 +46,8 @@ private data class OverlayDebugToggleSnapshot( class OverlayDebugControlHost( private val state: OverlayLayerDebugState = OverlayLayerDebugState, -) : OverlayLayerHost { - override val layerId: UiLayerId = UiLayerId.Debug +) : DomainSurfaceHost { + override val surface: ScreenDomainSurface = ScreenDomainSurfaces.DebugRoot private var viewportWidth: Int = 1 private var viewportHeight: Int = 1 diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayLayerDebugState.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayLayerDebugState.kt index 4b007dd..9ffeb8f 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayLayerDebugState.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayLayerDebugState.kt @@ -1,6 +1,7 @@ package org.dreamfinity.dsgl.core.debug -import org.dreamfinity.dsgl.core.overlay.UiLayerId +import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurface +import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces data class OverlayLayerDebugSnapshot( val applicationOverlayRenderEnabled: Boolean, @@ -58,28 +59,25 @@ object OverlayLayerDebugState { .getBoolean("dsgl.overlay.controls") } - fun isRenderEnabled(layer: UiLayerId): Boolean = - when (layer) { - UiLayerId.ApplicationOverlay -> applicationOverlayRenderEnabled - UiLayerId.SystemOverlay -> systemOverlayRenderEnabled - UiLayerId.Debug -> true - UiLayerId.ApplicationRoot -> true + fun isRenderEnabled(surface: ScreenDomainSurface): Boolean = + when { + surface == ScreenDomainSurfaces.ApplicationPortal -> applicationOverlayRenderEnabled + surface == ScreenDomainSurfaces.SystemPortal -> systemOverlayRenderEnabled + else -> true } - fun isTintEnabled(layer: UiLayerId): Boolean = - when (layer) { - UiLayerId.ApplicationOverlay -> applicationOverlayTintEnabled - UiLayerId.SystemOverlay -> systemOverlayTintEnabled - UiLayerId.Debug -> true - UiLayerId.ApplicationRoot -> true + fun isTintEnabled(surface: ScreenDomainSurface): Boolean = + when { + surface == ScreenDomainSurfaces.ApplicationPortal -> applicationOverlayTintEnabled + surface == ScreenDomainSurfaces.SystemPortal -> systemOverlayTintEnabled + else -> true } - fun isInputEnabled(layer: UiLayerId): Boolean = - when (layer) { - UiLayerId.ApplicationOverlay -> applicationOverlayInputEnabled - UiLayerId.SystemOverlay -> systemOverlayInputEnabled - UiLayerId.Debug -> true - UiLayerId.ApplicationRoot -> true + fun isInputEnabled(surface: ScreenDomainSurface): Boolean = + when { + surface == ScreenDomainSurfaces.ApplicationPortal -> applicationOverlayInputEnabled + surface == ScreenDomainSurfaces.SystemPortal -> systemOverlayInputEnabled + else -> true } fun resetAll() { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt index aa8a7e7..9246f63 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt @@ -19,8 +19,8 @@ class ApplicationOverlayHost( contextMenuEngine: ContextMenuEngine = DomainPortalServices.applicationContextMenuEngine, selectEngine: SelectEngine = DomainPortalServices.applicationSelectEngine, colorPickerEngine: ColorPickerPopupEngine = DomainPortalServices.applicationColorPickerEngine, -) : OverlayLayerHost { - override val layerId: UiLayerId = UiLayerId.ApplicationOverlay +) : DomainSurfaceHost { + override val surface: ScreenDomainSurface = ScreenDomainSurfaces.ApplicationPortal internal val rootNode: ApplicationOverlayRootNode = ApplicationOverlayRootNode() private val tree: DomTree = @@ -196,7 +196,7 @@ internal class ContextMenuPortalController( private val engine: ContextMenuEngine, ) : PortalPointerDispatch { private val portalHost: PortalHost = - PortalHost(OverlayLayerContracts.portalSurfaceForOwner(OverlayOwnerScope.Application)) + PortalHost(ScreenDomainSurfaces.ApplicationPortal) private val entry: ContextMenuPortalEntry = ContextMenuPortalEntry(engine) init { @@ -253,7 +253,7 @@ private class ContextMenuPortalEntry( PortalEntryState( id = PortalEntryId("application.context-menu"), ownerToken = engine, - surface = OverlayLayerContracts.portalSurfaceForOwner(OverlayOwnerScope.Application), + surface = ScreenDomainSurfaces.ApplicationPortal, order = PortalEntryOrder(zIndex = 0), dismissPolicy = PortalDismissPolicy.EscapeOrOutsidePointerDown, inputPolicy = PortalInputPolicy.ManualOnly, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayRootNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayRootNode.kt index 26f4ea3..728d737 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayRootNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayRootNode.kt @@ -52,7 +52,7 @@ class ApplicationOverlayRootNode( ) { setViewportBounds(width, height) bounds = Rect(0, 0, viewportWidth, viewportHeight) - val tintEnabled = OverlayDebugVisualization.enabled && isTintEnabled(UiLayerId.ApplicationOverlay) + val tintEnabled = OverlayDebugVisualization.enabled && isTintEnabled(ScreenDomainSurfaces.ApplicationPortal) if (tintEnabled) { debugTintNode.display = Display.Block debugTintNode.backgroundColor = OverlayDebugVisualization.applicationOverlayFillColor diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ColorPickerPopupOverlayOwnership.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ColorPickerPopupOverlayOwnership.kt index 905ab52..a4c37de 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ColorPickerPopupOverlayOwnership.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ColorPickerPopupOverlayOwnership.kt @@ -3,6 +3,6 @@ package org.dreamfinity.dsgl.core.overlay import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupRequest object ColorPickerPopupOverlayOwnership { - fun resolveLayer(request: ColorPickerPopupRequest): UiLayerId = - OverlayLayerContracts.resolveTransientLayer(request.ownerScope) + fun resolveSurface(request: ColorPickerPopupRequest): ScreenDomainSurface = + ScreenDomainSurfaces.portalSurfaceForOwner(request.ownerScope) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/DomainSurfaceHost.kt similarity index 92% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerHost.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/DomainSurfaceHost.kt index e57aa98..921a239 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/DomainSurfaceHost.kt @@ -4,8 +4,8 @@ import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.render.RenderCommand -interface OverlayLayerHost { - val layerId: UiLayerId +interface DomainSurfaceHost { + val surface: ScreenDomainSurface fun onInputFrame(viewportWidth: Int, viewportHeight: Int) {} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContracts.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContracts.kt deleted file mode 100644 index 1f0d04f..0000000 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContracts.kt +++ /dev/null @@ -1,124 +0,0 @@ -package org.dreamfinity.dsgl.core.overlay - -import org.dreamfinity.dsgl.core.render.RenderCommand - -enum class UiLayerId { - Debug, - ApplicationRoot, - ApplicationOverlay, - SystemOverlay, -} - -enum class OverlayOwnerScope { - Application, - System, -} - -internal enum class ScreenDomainId { - Application, - System, - Debug, -} - -internal enum class ScreenDomainSurfaceRole { - Root, - Portal, -} - -internal data class ScreenDomainSurface( - val domain: ScreenDomainId, - val role: ScreenDomainSurfaceRole, -) - -object OverlayLayerContracts { - val paintOrder: List = - listOf( - UiLayerId.ApplicationRoot, - UiLayerId.ApplicationOverlay, - UiLayerId.SystemOverlay, - UiLayerId.Debug, - ) - - val inputPriority: List = - listOf( - UiLayerId.Debug, - UiLayerId.SystemOverlay, - UiLayerId.ApplicationOverlay, - UiLayerId.ApplicationRoot, - ) - - internal val paintSurfaces: List = paintOrder.map(::domainSurfaceForLayer) - - internal val inputSurfaces: List = inputPriority.map(::domainSurfaceForLayer) - - fun resolveTransientLayer(ownerScope: OverlayOwnerScope): UiLayerId = - when (ownerScope) { - OverlayOwnerScope.Application -> UiLayerId.ApplicationOverlay - OverlayOwnerScope.System -> UiLayerId.SystemOverlay - } - - internal fun domainSurfaceForLayer(layer: UiLayerId): ScreenDomainSurface = - when (layer) { - UiLayerId.ApplicationRoot -> - ScreenDomainSurface( - domain = ScreenDomainId.Application, - role = ScreenDomainSurfaceRole.Root, - ) - - UiLayerId.ApplicationOverlay -> - ScreenDomainSurface( - domain = ScreenDomainId.Application, - role = ScreenDomainSurfaceRole.Portal, - ) - - UiLayerId.SystemOverlay -> - ScreenDomainSurface( - domain = ScreenDomainId.System, - role = ScreenDomainSurfaceRole.Portal, - ) - - UiLayerId.Debug -> - ScreenDomainSurface( - domain = ScreenDomainId.Debug, - role = ScreenDomainSurfaceRole.Root, - ) - } - - internal fun portalSurfaceForOwner(ownerScope: OverlayOwnerScope): ScreenDomainSurface = - domainSurfaceForLayer(resolveTransientLayer(ownerScope)) - - @Suppress("UnusedParameter") - fun resolveTransientLayer(ownerScope: OverlayOwnerScope, cursorX: Int, cursorY: Int): UiLayerId = - resolveTransientLayer(ownerScope) - - fun firstInputConsumer( - canConsume: (UiLayerId) -> Boolean, - isLayerInputEnabled: (UiLayerId) -> Boolean = { true }, - ): UiLayerId? { - inputPriority.forEach { layer -> - if (!isLayerInputEnabled(layer)) return@forEach - if (canConsume(layer)) return layer - } - return null - } - - fun composePaintCommands( - applicationRoot: List, - applicationOverlay: List, - systemOverlay: List, - debug: List, - out: MutableList, - shouldRenderLayer: (UiLayerId) -> Boolean = { true }, - ) { - out.clear() - paintOrder.forEach { layer -> - if (!shouldRenderLayer(layer)) return@forEach - when (layer) { - UiLayerId.ApplicationRoot -> out.addAll(applicationRoot) - UiLayerId.ApplicationOverlay -> out.addAll(applicationOverlay) - UiLayerId.SystemOverlay -> out.addAll(systemOverlay) - UiLayerId.Debug -> out.addAll(debug) - } - } - } -} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContracts.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContracts.kt index b40b016..a4df4a1 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContracts.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContracts.kt @@ -171,8 +171,8 @@ internal class PortalHost( fun dispatchInput(handler: (PortalEntry) -> Boolean): Boolean = entriesInInputOrder().any(handler) } -internal data class OverlayLayerPortalHostAdapter( - val layerHost: OverlayLayerHost, +internal data class DomainSurfacePortalHostAdapter( + val surfaceHost: DomainSurfaceHost, ) { - val surface: ScreenDomainSurface = OverlayLayerContracts.domainSurfaceForLayer(layerHost.layerId) + val surface: ScreenDomainSurface = surfaceHost.surface } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ScreenDomainContracts.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ScreenDomainContracts.kt new file mode 100644 index 0000000..728dfe3 --- /dev/null +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ScreenDomainContracts.kt @@ -0,0 +1,136 @@ +package org.dreamfinity.dsgl.core.overlay + +import org.dreamfinity.dsgl.core.render.RenderCommand + +enum class OverlayOwnerScope { + Application, + System, +} + +enum class ScreenDomainId { + Application, + System, + Debug, +} + +enum class ScreenDomainSurfaceRole { + Root, + Portal, +} + +data class ScreenDomainSurface( + val domain: ScreenDomainId, + val role: ScreenDomainSurfaceRole, +) + +object ScreenDomainSurfaces { + val ApplicationRoot: ScreenDomainSurface = + ScreenDomainSurface(ScreenDomainId.Application, ScreenDomainSurfaceRole.Root) + val ApplicationPortal: ScreenDomainSurface = + ScreenDomainSurface(ScreenDomainId.Application, ScreenDomainSurfaceRole.Portal) + val SystemRoot: ScreenDomainSurface = + ScreenDomainSurface(ScreenDomainId.System, ScreenDomainSurfaceRole.Root) + val SystemPortal: ScreenDomainSurface = + ScreenDomainSurface(ScreenDomainId.System, ScreenDomainSurfaceRole.Portal) + val DebugRoot: ScreenDomainSurface = + ScreenDomainSurface(ScreenDomainId.Debug, ScreenDomainSurfaceRole.Root) + val DebugPortal: ScreenDomainSurface = + ScreenDomainSurface(ScreenDomainId.Debug, ScreenDomainSurfaceRole.Portal) + + val allDomains: List = + listOf( + ScreenDomainId.Application, + ScreenDomainId.System, + ScreenDomainId.Debug, + ) + + val allSurfaces: List = + allDomains.flatMap { domain -> + listOf( + rootSurface(domain), + portalSurface(domain), + ) + } + + val paintOrder: List = + listOf( + ApplicationRoot, + ApplicationPortal, + SystemRoot, + SystemPortal, + DebugRoot, + DebugPortal, + ) + + val inputPriority: List = + listOf( + DebugPortal, + DebugRoot, + SystemPortal, + SystemRoot, + ApplicationPortal, + ApplicationRoot, + ) + + fun rootSurface(domain: ScreenDomainId): ScreenDomainSurface = + when (domain) { + ScreenDomainId.Application -> ApplicationRoot + ScreenDomainId.System -> SystemRoot + ScreenDomainId.Debug -> DebugRoot + } + + fun portalSurface(domain: ScreenDomainId): ScreenDomainSurface = + when (domain) { + ScreenDomainId.Application -> ApplicationPortal + ScreenDomainId.System -> SystemPortal + ScreenDomainId.Debug -> DebugPortal + } + + fun portalSurfaceForOwner(ownerScope: OverlayOwnerScope): ScreenDomainSurface = portalSurface(ownerScope.domain) + + @Suppress("UnusedParameter") + fun portalSurfaceForOwner(ownerScope: OverlayOwnerScope, cursorX: Int, cursorY: Int): ScreenDomainSurface = + portalSurfaceForOwner(ownerScope) + + fun firstInputConsumer( + canConsume: (ScreenDomainSurface) -> Boolean, + isSurfaceInputEnabled: (ScreenDomainSurface) -> Boolean = { true }, + ): ScreenDomainSurface? { + inputPriority.forEach { surface -> + if (!isSurfaceInputEnabled(surface)) return@forEach + if (canConsume(surface)) return surface + } + return null + } + + fun composePaintCommands( + applicationRoot: List, + applicationPortal: List, + systemRoot: List = emptyList(), + systemPortal: List, + debugRoot: List, + debugPortal: List = emptyList(), + out: MutableList, + shouldRenderSurface: (ScreenDomainSurface) -> Boolean = { true }, + ) { + out.clear() + paintOrder.forEach { surface -> + if (!shouldRenderSurface(surface)) return@forEach + when (surface) { + ApplicationRoot -> out.addAll(applicationRoot) + ApplicationPortal -> out.addAll(applicationPortal) + SystemRoot -> out.addAll(systemRoot) + SystemPortal -> out.addAll(systemPortal) + DebugRoot -> out.addAll(debugRoot) + DebugPortal -> out.addAll(debugPortal) + } + } + } +} + +val OverlayOwnerScope.domain: ScreenDomainId + get() = + when (this) { + OverlayOwnerScope.Application -> ScreenDomainId.Application + OverlayOwnerScope.System -> ScreenDomainId.System + } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt index 37ac9fa..c16984d 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt @@ -3,7 +3,6 @@ package org.dreamfinity.dsgl.core.overlay.system import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.OverlayLayerContracts import org.dreamfinity.dsgl.core.overlay.PortalDismissPolicy import org.dreamfinity.dsgl.core.overlay.PortalEntry import org.dreamfinity.dsgl.core.overlay.PortalEntryBounds @@ -13,7 +12,7 @@ import org.dreamfinity.dsgl.core.overlay.PortalEntryPlacement import org.dreamfinity.dsgl.core.overlay.PortalEntryState import org.dreamfinity.dsgl.core.overlay.PortalFocusPolicy import org.dreamfinity.dsgl.core.overlay.PortalInputPolicy -import org.dreamfinity.dsgl.core.overlay.UiLayerId +import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelDragSession import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelState import java.util.IdentityHashMap @@ -122,7 +121,7 @@ internal class SystemOverlayPortalEntry( PortalEntryState( id = PortalEntryId("system.${entry.state.id.name}"), ownerToken = entry.state, - surface = OverlayLayerContracts.domainSurfaceForLayer(UiLayerId.SystemOverlay), + surface = ScreenDomainSurfaces.SystemPortal, order = PortalEntryOrder( zIndex = entry.state.lane.zOrder, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt index 8831ef4..61458da 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt @@ -15,12 +15,12 @@ import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.inspector.InspectorPanelState import org.dreamfinity.dsgl.core.inspector.internal.SystemInspectorOverlayNode import org.dreamfinity.dsgl.core.overlay.DomainPortalServices -import org.dreamfinity.dsgl.core.overlay.OverlayLayerContracts -import org.dreamfinity.dsgl.core.overlay.OverlayLayerHost +import org.dreamfinity.dsgl.core.overlay.DomainSurfaceHost import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.overlay.PortalFrameContext import org.dreamfinity.dsgl.core.overlay.PortalHost -import org.dreamfinity.dsgl.core.overlay.UiLayerId +import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurface +import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter import org.dreamfinity.dsgl.core.overlay.input.dispatchManualThenDomFallback import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanel @@ -29,10 +29,11 @@ import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.select.SelectPortalController import org.dreamfinity.dsgl.core.style.StyleApplicationScope +@Suppress("TooManyFunctions") class SystemOverlayHost( private val inspectorController: InspectorController, -) : OverlayLayerHost { - override val layerId: UiLayerId = UiLayerId.SystemOverlay +) : DomainSurfaceHost { + override val surface: ScreenDomainSurface = ScreenDomainSurfaces.SystemPortal private val rootNode: SystemOverlayRootNode = SystemOverlayRootNode() private val inspectorEntry: SystemOverlayEntry = InspectorOverlayEntry(inspectorController) @@ -44,7 +45,7 @@ class SystemOverlayHost( listOf(inspectorEntry, colorPickerEntry, colorPickerTransientEntry, overlayPanelDemoEntry), ) private val portalHost: PortalHost = - PortalHost(OverlayLayerContracts.domainSurfaceForLayer(UiLayerId.SystemOverlay)) + PortalHost(ScreenDomainSurfaces.SystemPortal) private val portalEntries: List = entryRegistry.allEntries().map(::SystemOverlayPortalEntry) private val transientOwnershipRegistry: SystemOverlayTransientOwnershipRegistry = diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayRootNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayRootNode.kt index 240ac79..d10adf3 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayRootNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayRootNode.kt @@ -12,7 +12,7 @@ import org.dreamfinity.dsgl.core.dsl.UiScope import org.dreamfinity.dsgl.core.dsl.div import org.dreamfinity.dsgl.core.font.FontRegistry import org.dreamfinity.dsgl.core.overlay.OverlayDebugVisualization -import org.dreamfinity.dsgl.core.overlay.UiLayerId +import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.StyleEngine @@ -85,7 +85,7 @@ internal class SystemOverlayRootNode( ) { setViewportBounds(width, height) bounds = Rect(0, 0, viewportWidth, viewportHeight) - val tintEnabled = OverlayDebugVisualization.enabled && isTintEnabled(UiLayerId.SystemOverlay) + val tintEnabled = OverlayDebugVisualization.enabled && isTintEnabled(ScreenDomainSurfaces.SystemPortal) if (tintEnabled) { debugTintNode.display = Display.Block debugTintNode.backgroundColor = OverlayDebugVisualization.systemOverlayFillColor diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt index fff2abc..5e6672a 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt @@ -4,7 +4,6 @@ import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.OverlayLayerContracts import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.overlay.PortalDismissPolicy import org.dreamfinity.dsgl.core.overlay.PortalEntry @@ -17,6 +16,7 @@ import org.dreamfinity.dsgl.core.overlay.PortalFocusPolicy import org.dreamfinity.dsgl.core.overlay.PortalHost import org.dreamfinity.dsgl.core.overlay.PortalInputPolicy import org.dreamfinity.dsgl.core.overlay.PortalPointerDispatch +import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces import org.dreamfinity.dsgl.core.render.RenderCommand internal class SelectPortalController( @@ -25,7 +25,7 @@ internal class SelectPortalController( entryId: String, ) : PortalPointerDispatch { private val portalHost: PortalHost = - PortalHost(OverlayLayerContracts.portalSurfaceForOwner(ownerScope)) + PortalHost(ScreenDomainSurfaces.portalSurfaceForOwner(ownerScope)) private val entry: SelectPortalEntry = SelectPortalEntry( engine = engine, @@ -88,7 +88,7 @@ private class SelectPortalEntry( PortalEntryState( id = PortalEntryId(entryId), ownerToken = engine, - surface = OverlayLayerContracts.portalSurfaceForOwner(ownerScope), + surface = ScreenDomainSurfaces.portalSurfaceForOwner(ownerScope), order = PortalEntryOrder(zIndex = 0), dismissPolicy = PortalDismissPolicy.EscapeOrOutsidePointerDown, inputPolicy = PortalInputPolicy.ManualOnly, diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngineTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngineTests.kt index ae6f1fa..b3b415c 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngineTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngineTests.kt @@ -3,9 +3,8 @@ package org.dreamfinity.dsgl.core.colorpicker import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.OverlayLayerContracts import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope -import org.dreamfinity.dsgl.core.overlay.UiLayerId +import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces import org.dreamfinity.dsgl.core.render.RenderCommand import kotlin.test.Test import kotlin.test.assertEquals @@ -467,8 +466,8 @@ class ColorPickerPopupEngineTests { assertTrue(overlay.isNotEmpty()) assertEquals(OverlayOwnerScope.Application, engine.debugActiveOwnerScope()) assertEquals( - UiLayerId.ApplicationOverlay, - OverlayLayerContracts.resolveTransientLayer(engine.debugActiveOwnerScope()!!), + ScreenDomainSurfaces.ApplicationPortal, + ScreenDomainSurfaces.portalSurfaceForOwner(engine.debugActiveOwnerScope()!!), ) } @@ -495,8 +494,8 @@ class ColorPickerPopupEngineTests { assertTrue(overlay.isNotEmpty()) assertEquals(OverlayOwnerScope.System, engine.debugActiveOwnerScope()) assertEquals( - UiLayerId.SystemOverlay, - OverlayLayerContracts.resolveTransientLayer(engine.debugActiveOwnerScope()!!), + ScreenDomainSurfaces.SystemPortal, + ScreenDomainSurfaces.portalSurfaceForOwner(engine.debugActiveOwnerScope()!!), ) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHostTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHostTests.kt index f168379..d4ffa12 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHostTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHostTests.kt @@ -2,7 +2,7 @@ package org.dreamfinity.dsgl.core.debug import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.UiLayerId +import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.StyleApplicationScope import java.util.Locale @@ -196,7 +196,7 @@ class OverlayDebugControlHostTests { } @Test - fun `debug layer remains enabled in state even when app and system layers are disabled`() { + fun `debug domain surfaces remain enabled in state even when app and system portals are disabled`() { OverlayLayerDebugState.applicationOverlayTintEnabled = false OverlayLayerDebugState.applicationOverlayRenderEnabled = false OverlayLayerDebugState.applicationOverlayInputEnabled = false @@ -204,8 +204,10 @@ class OverlayDebugControlHostTests { OverlayLayerDebugState.systemOverlayTintEnabled = false OverlayLayerDebugState.systemOverlayInputEnabled = false - assertTrue(OverlayLayerDebugState.isRenderEnabled(UiLayerId.Debug)) - assertTrue(OverlayLayerDebugState.isInputEnabled(UiLayerId.Debug)) + assertTrue(OverlayLayerDebugState.isRenderEnabled(ScreenDomainSurfaces.DebugRoot)) + assertTrue(OverlayLayerDebugState.isInputEnabled(ScreenDomainSurfaces.DebugRoot)) + assertTrue(OverlayLayerDebugState.isRenderEnabled(ScreenDomainSurfaces.DebugPortal)) + assertTrue(OverlayLayerDebugState.isInputEnabled(ScreenDomainSurfaces.DebugPortal)) assertEquals( OverlayLayerDebugSnapshot( applicationOverlayRenderEnabled = false, diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt index 92e47a2..16d0546 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt @@ -31,6 +31,7 @@ import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue +@Suppress("LargeClass") class LiveLayerInteractionPathTests { private val ctx = object : UiMeasureContext { @@ -51,31 +52,36 @@ class LiveLayerInteractionPathTests { @Test fun `runtime layer path resolves in debug system app-overlay app-root order`() { - val callOrder = ArrayList(4) - val harness = - LiveLayerInputHarness( + val callOrder = ArrayList(4) + val fixture = + LiveLayerInputFixture( debugHandler = { _, _, _ -> - callOrder += UiLayerId.Debug + callOrder += ScreenDomainSurfaces.DebugRoot false }, systemOverlayHandler = { _, _, _ -> - callOrder += UiLayerId.SystemOverlay + callOrder += ScreenDomainSurfaces.SystemPortal false }, applicationOverlayHandler = { _, _, _ -> - callOrder += UiLayerId.ApplicationOverlay + callOrder += ScreenDomainSurfaces.ApplicationPortal false }, ) val consumedBy = - harness.dispatchMouseDown(10, 10, MouseButton.LEFT) { - callOrder += UiLayerId.ApplicationRoot + fixture.dispatchMouseDown(10, 10, MouseButton.LEFT) { + callOrder += ScreenDomainSurfaces.ApplicationRoot true } - assertEquals(UiLayerId.ApplicationRoot, consumedBy) + assertEquals(ScreenDomainSurfaces.ApplicationRoot, consumedBy) assertEquals( - listOf(UiLayerId.Debug, UiLayerId.SystemOverlay, UiLayerId.ApplicationOverlay, UiLayerId.ApplicationRoot), + listOf( + ScreenDomainSurfaces.DebugRoot, + ScreenDomainSurfaces.SystemPortal, + ScreenDomainSurfaces.ApplicationPortal, + ScreenDomainSurfaces.ApplicationRoot, + ), callOrder, ) } @@ -85,8 +91,8 @@ class LiveLayerInteractionPathTests { var systemReceived = false var appOverlayReceived = false var appRootReceived = false - val harness = - LiveLayerInputHarness( + val fixture = + LiveLayerInputFixture( debugHandler = { _, _, _ -> true }, systemOverlayHandler = { _, _, _ -> systemReceived = true @@ -98,12 +104,12 @@ class LiveLayerInteractionPathTests { }, ) val consumedBy = - harness.dispatchMouseDown(12, 14, MouseButton.LEFT) { + fixture.dispatchMouseDown(12, 14, MouseButton.LEFT) { appRootReceived = true true } - assertEquals(UiLayerId.Debug, consumedBy) + assertEquals(ScreenDomainSurfaces.DebugRoot, consumedBy) assertFalse(systemReceived) assertFalse(appOverlayReceived) assertFalse(appRootReceived) @@ -125,20 +131,20 @@ class LiveLayerInteractionPathTests { val entryState = systemHost.debugEntryState(SystemOverlayEntryId.PanelDemo) ?: error("panel demo state missing") val panelRect = entryState.panelState.currentRectOrNull() ?: error("panel demo rect missing") - val harness = - LiveLayerInputHarness( + val fixture = + LiveLayerInputFixture( debugHandler = { _, _, _ -> false }, systemOverlayHandler = { x, y, button -> systemHost.handleMouseDown(x, y, button) }, applicationOverlayHandler = { _, _, _ -> false }, ) var appRootReceived = false val consumedBy = - harness.dispatchMouseDown(panelRect.x + 20, panelRect.y + 70, MouseButton.LEFT) { + fixture.dispatchMouseDown(panelRect.x + 20, panelRect.y + 70, MouseButton.LEFT) { appRootReceived = true true } - assertEquals(UiLayerId.SystemOverlay, consumedBy) + assertEquals(ScreenDomainSurfaces.SystemPortal, consumedBy) assertFalse(appRootReceived) } @@ -164,8 +170,8 @@ class LiveLayerInteractionPathTests { val outsideY = (panelRect.y + panelRect.height / 2).coerceIn(1, 719) assertFalse(panelRect.contains(outsideX, outsideY)) - val harness = - LiveLayerInputHarness( + val fixture = + LiveLayerInputFixture( debugHandler = { _, _, _ -> false }, systemOverlayHandler = { x, y, button -> systemHost.handleMouseDown(x, y, button) }, applicationOverlayHandler = { _, _, _ -> false }, @@ -173,30 +179,30 @@ class LiveLayerInteractionPathTests { var appRootReceivedOutside = false val consumedOutside = - harness.dispatchMouseDown(outsideX, outsideY, MouseButton.LEFT) { + fixture.dispatchMouseDown(outsideX, outsideY, MouseButton.LEFT) { appRootReceivedOutside = true true } - assertEquals(UiLayerId.ApplicationRoot, consumedOutside) + assertEquals(ScreenDomainSurfaces.ApplicationRoot, consumedOutside) assertTrue(appRootReceivedOutside) } @Test fun `application overlay consumption prevents app-root fallthrough`() { - val harness = - LiveLayerInputHarness( + val fixture = + LiveLayerInputFixture( debugHandler = { _, _, _ -> false }, systemOverlayHandler = { _, _, _ -> false }, applicationOverlayHandler = { _, _, _ -> true }, ) var appRootReceived = false val consumedBy = - harness.dispatchMouseDown(24, 30, MouseButton.LEFT) { + fixture.dispatchMouseDown(24, 30, MouseButton.LEFT) { appRootReceived = true true } - assertEquals(UiLayerId.ApplicationOverlay, consumedBy) + assertEquals(ScreenDomainSurfaces.ApplicationPortal, consumedBy) assertFalse(appRootReceived) } @@ -215,8 +221,8 @@ class LiveLayerInteractionPathTests { assertTrue(applicationOverlayHost.handleMouseUp(50, 50, MouseButton.LEFT)) assertEquals(1, clicks) - val harness = - LiveLayerInputHarness( + val fixture = + LiveLayerInputFixture( debugHandler = { _, _, _ -> false }, systemOverlayHandler = { _, _, _ -> false }, applicationOverlayHandler = { x, y, button -> @@ -225,12 +231,12 @@ class LiveLayerInteractionPathTests { ) var appRootReceived = false val consumedBy = - harness.dispatchMouseDown(50, 50, MouseButton.LEFT) { + fixture.dispatchMouseDown(50, 50, MouseButton.LEFT) { appRootReceived = true true } - assertEquals(UiLayerId.ApplicationOverlay, consumedBy) + assertEquals(ScreenDomainSurfaces.ApplicationPortal, consumedBy) assertFalse(appRootReceived) } @@ -288,8 +294,8 @@ class LiveLayerInteractionPathTests { val outsideX = panel.x + panel.width + 24 val outsideY = panel.y + panel.height + 24 - val harness = - LiveLayerInputHarness( + val fixture = + LiveLayerInputFixture( debugHandler = { _, _, _ -> false }, systemOverlayHandler = { _, _, _ -> false }, applicationOverlayHandler = { x, y, button -> @@ -298,12 +304,12 @@ class LiveLayerInteractionPathTests { ) var appRootReceived = false val consumedBy = - harness.dispatchMouseDown(outsideX, outsideY, MouseButton.LEFT) { + fixture.dispatchMouseDown(outsideX, outsideY, MouseButton.LEFT) { appRootReceived = true true } - assertEquals(UiLayerId.ApplicationOverlay, consumedBy) + assertEquals(ScreenDomainSurfaces.ApplicationPortal, consumedBy) assertFalse(appRootReceived) assertFalse(applicationOverlayHost.hasOpenContextMenuPortal()) } @@ -367,8 +373,8 @@ class LiveLayerInteractionPathTests { assertNotNull(panel) val outsideX = panel.x + panel.width + 24 val outsideY = panel.y + panel.height + 24 - val harness = - LiveLayerInputHarness( + val fixture = + LiveLayerInputFixture( debugHandler = { _, _, _ -> false }, systemOverlayHandler = { _, _, _ -> false }, applicationOverlayHandler = { x, y, button -> @@ -378,12 +384,12 @@ class LiveLayerInteractionPathTests { var appRootReceived = false val consumedBy = - harness.dispatchMouseDown(outsideX, outsideY, MouseButton.LEFT) { + fixture.dispatchMouseDown(outsideX, outsideY, MouseButton.LEFT) { appRootReceived = true true } - assertEquals(UiLayerId.ApplicationOverlay, consumedBy) + assertEquals(ScreenDomainSurfaces.ApplicationPortal, consumedBy) assertFalse(appRootReceived) } @@ -436,8 +442,8 @@ class LiveLayerInteractionPathTests { val layout = DomainPortalServices.applicationColorPickerEngine.debugBodyLayout(owner) assertNotNull(layout) - val harness = - LiveLayerInputHarness( + val fixture = + LiveLayerInputFixture( debugHandler = { _, _, _ -> false }, systemOverlayHandler = { _, _, _ -> false }, applicationOverlayHandler = { x, y, button -> @@ -446,7 +452,7 @@ class LiveLayerInteractionPathTests { ) var appRootReceived = false val consumedBy = - harness.dispatchMouseDown( + fixture.dispatchMouseDown( layout.colorFieldRect.x + 4, layout.colorFieldRect.y + 4, MouseButton.LEFT, @@ -456,7 +462,7 @@ class LiveLayerInteractionPathTests { } assertTrue(commands.isNotEmpty()) - assertEquals(UiLayerId.ApplicationOverlay, consumedBy) + assertEquals(ScreenDomainSurfaces.ApplicationPortal, consumedBy) assertFalse(appRootReceived) assertTrue(applicationOverlayHost.hasOpenColorPickerPortal()) } @@ -566,8 +572,8 @@ class LiveLayerInteractionPathTests { assertNotNull(panel) val style = DomainPortalServices.systemSelectEngine.currentStyle() - val harness = - LiveLayerInputHarness( + val fixture = + LiveLayerInputFixture( debugHandler = { _, _, _ -> false }, systemOverlayHandler = { x, y, button -> systemHost.handlePortalMouseDown(x, y, button) @@ -576,7 +582,7 @@ class LiveLayerInteractionPathTests { ) var appRootReceived = false val consumedBy = - harness.dispatchMouseDown( + fixture.dispatchMouseDown( panel.x + style.panelPaddingX + 1, panel.y + style.panelPaddingY + 1, MouseButton.LEFT, @@ -586,7 +592,7 @@ class LiveLayerInteractionPathTests { } assertTrue(commands.isNotEmpty()) - assertEquals(UiLayerId.SystemOverlay, consumedBy) + assertEquals(ScreenDomainSurfaces.SystemPortal, consumedBy) assertFalse(appRootReceived) assertEquals("a", selected) assertFalse(DomainPortalServices.applicationSelectEngine.isOpenFor(owner)) @@ -625,20 +631,20 @@ class LiveLayerInteractionPathTests { ?: error("panel demo node missing") val buttonRect = demoNode.buttonRect() assertNotNull(buttonRect) - val harness = - LiveLayerInputHarness( + val fixture = + LiveLayerInputFixture( debugHandler = { _, _, _ -> false }, systemOverlayHandler = { x, y, button -> systemHost.handleMouseDown(x, y, button) }, applicationOverlayHandler = { _, _, _ -> false }, ) var appRootReceived = false val consumedBy = - harness.dispatchMouseDown(buttonRect.x + 1, buttonRect.y + 1, MouseButton.LEFT) { + fixture.dispatchMouseDown(buttonRect.x + 1, buttonRect.y + 1, MouseButton.LEFT) { appRootReceived = true true } - assertEquals(UiLayerId.SystemOverlay, consumedBy) + assertEquals(ScreenDomainSurfaces.SystemPortal, consumedBy) assertFalse(appRootReceived) } @@ -693,7 +699,7 @@ class LiveLayerInteractionPathTests { onCommit = onCommit, ) - private class LiveLayerInputHarness( + private class LiveLayerInputFixture( private val debugHandler: (Int, Int, MouseButton) -> Boolean, private val systemOverlayHandler: (Int, Int, MouseButton) -> Boolean, private val applicationOverlayHandler: (Int, Int, MouseButton) -> Boolean, @@ -703,14 +709,17 @@ class LiveLayerInteractionPathTests { mouseY: Int, button: MouseButton, applicationRootHandler: () -> Boolean, - ): UiLayerId? = - OverlayLayerContracts.firstInputConsumer( - canConsume = { layer -> - when (layer) { - UiLayerId.Debug -> debugHandler(mouseX, mouseY, button) - UiLayerId.SystemOverlay -> systemOverlayHandler(mouseX, mouseY, button) - UiLayerId.ApplicationOverlay -> applicationOverlayHandler(mouseX, mouseY, button) - UiLayerId.ApplicationRoot -> applicationRootHandler() + ): ScreenDomainSurface? = + ScreenDomainSurfaces.firstInputConsumer( + canConsume = { surface -> + when (surface) { + ScreenDomainSurfaces.DebugPortal -> false + ScreenDomainSurfaces.DebugRoot -> debugHandler(mouseX, mouseY, button) + ScreenDomainSurfaces.SystemPortal -> systemOverlayHandler(mouseX, mouseY, button) + ScreenDomainSurfaces.SystemRoot -> false + ScreenDomainSurfaces.ApplicationPortal -> applicationOverlayHandler(mouseX, mouseY, button) + ScreenDomainSurfaces.ApplicationRoot -> applicationRootHandler() + else -> false } }, ) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContractsTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContractsTests.kt deleted file mode 100644 index be05741..0000000 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayLayerContractsTests.kt +++ /dev/null @@ -1,248 +0,0 @@ -package org.dreamfinity.dsgl.core.overlay - -import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupRequest -import org.dreamfinity.dsgl.core.colorpicker.ColorPickerState -import org.dreamfinity.dsgl.core.colorpicker.RgbaColor -import org.dreamfinity.dsgl.core.dom.layout.Rect -import org.dreamfinity.dsgl.core.render.RenderCommand -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNull - -class OverlayLayerContractsTests { - @Test - fun `paint order is app root then app overlay then system overlay then debug`() { - assertEquals( - listOf(UiLayerId.ApplicationRoot, UiLayerId.ApplicationOverlay, UiLayerId.SystemOverlay, UiLayerId.Debug), - OverlayLayerContracts.paintOrder, - ) - } - - @Test - fun `input priority is debug then system overlay then app overlay then app root`() { - assertEquals( - listOf(UiLayerId.Debug, UiLayerId.SystemOverlay, UiLayerId.ApplicationOverlay, UiLayerId.ApplicationRoot), - OverlayLayerContracts.inputPriority, - ) - } - - @Test - fun `paint surfaces map current layer order to domain surfaces`() { - assertEquals( - listOf( - ScreenDomainSurface(ScreenDomainId.Application, ScreenDomainSurfaceRole.Root), - ScreenDomainSurface(ScreenDomainId.Application, ScreenDomainSurfaceRole.Portal), - ScreenDomainSurface(ScreenDomainId.System, ScreenDomainSurfaceRole.Portal), - ScreenDomainSurface(ScreenDomainId.Debug, ScreenDomainSurfaceRole.Root), - ), - OverlayLayerContracts.paintSurfaces, - ) - } - - @Test - fun `input surfaces map current input priority to domain surfaces`() { - assertEquals( - listOf( - ScreenDomainSurface(ScreenDomainId.Debug, ScreenDomainSurfaceRole.Root), - ScreenDomainSurface(ScreenDomainId.System, ScreenDomainSurfaceRole.Portal), - ScreenDomainSurface(ScreenDomainId.Application, ScreenDomainSurfaceRole.Portal), - ScreenDomainSurface(ScreenDomainId.Application, ScreenDomainSurfaceRole.Root), - ), - OverlayLayerContracts.inputSurfaces, - ) - } - - @Test - fun `owner scope resolves to compatible portal domain surfaces`() { - assertEquals( - ScreenDomainSurface(ScreenDomainId.Application, ScreenDomainSurfaceRole.Portal), - OverlayLayerContracts.portalSurfaceForOwner(OverlayOwnerScope.Application), - ) - assertEquals( - ScreenDomainSurface(ScreenDomainId.System, ScreenDomainSurfaceRole.Portal), - OverlayLayerContracts.portalSurfaceForOwner(OverlayOwnerScope.System), - ) - } - - @Test - fun `debug layer maps to debug domain root without changing layer behavior`() { - assertEquals( - ScreenDomainSurface(ScreenDomainId.Debug, ScreenDomainSurfaceRole.Root), - OverlayLayerContracts.domainSurfaceForLayer(UiLayerId.Debug), - ) - assertEquals(UiLayerId.Debug, OverlayLayerContracts.paintOrder.last()) - assertEquals(UiLayerId.Debug, OverlayLayerContracts.inputPriority.first()) - } - - @Test - fun `firstInputConsumer respects configured input priority`() { - val consumed = - OverlayLayerContracts.firstInputConsumer( - canConsume = { layer -> - layer == UiLayerId.Debug || - layer == UiLayerId.ApplicationOverlay || - layer == UiLayerId.ApplicationRoot - }, - ) - assertEquals(UiLayerId.Debug, consumed) - } - - @Test - fun `firstInputConsumer returns null when no layer consumes`() { - val consumed = OverlayLayerContracts.firstInputConsumer(canConsume = { false }) - assertNull(consumed) - } - - @Test - fun `transient ownership uses owner scope and not cursor position`() { - val appAtA = - OverlayLayerContracts.resolveTransientLayer( - ownerScope = OverlayOwnerScope.Application, - cursorX = 10, - cursorY = 20, - ) - val appAtB = - OverlayLayerContracts.resolveTransientLayer( - ownerScope = OverlayOwnerScope.Application, - cursorX = 800, - cursorY = 640, - ) - val systemAtA = - OverlayLayerContracts.resolveTransientLayer( - ownerScope = OverlayOwnerScope.System, - cursorX = 10, - cursorY = 20, - ) - val systemAtB = - OverlayLayerContracts.resolveTransientLayer( - ownerScope = OverlayOwnerScope.System, - cursorX = 800, - cursorY = 640, - ) - assertEquals(UiLayerId.ApplicationOverlay, appAtA) - assertEquals(UiLayerId.ApplicationOverlay, appAtB) - assertEquals(UiLayerId.SystemOverlay, systemAtA) - assertEquals(UiLayerId.SystemOverlay, systemAtB) - } - - @Test - fun `composePaintCommands follows configured layer order`() { - val root = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000001.toInt())) - val appOverlay = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000002.toInt())) - val system = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000003.toInt())) - val debug = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000004.toInt())) - val out = ArrayList() - - OverlayLayerContracts.composePaintCommands(root, appOverlay, system, debug, out) - - assertEquals(4, out.size) - assertEquals(0xFF000001.toInt(), (out[0] as RenderCommand.DrawRect).color) - assertEquals(0xFF000002.toInt(), (out[1] as RenderCommand.DrawRect).color) - assertEquals(0xFF000003.toInt(), (out[2] as RenderCommand.DrawRect).color) - assertEquals(0xFF000004.toInt(), (out[3] as RenderCommand.DrawRect).color) - } - - @Test - fun `composePaintCommands skips app overlay render when disabled`() { - val root = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000001.toInt())) - val appOverlay = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000002.toInt())) - val system = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000003.toInt())) - val debug = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000004.toInt())) - val out = ArrayList() - - OverlayLayerContracts.composePaintCommands( - applicationRoot = root, - applicationOverlay = appOverlay, - systemOverlay = system, - debug = debug, - out = out, - shouldRenderLayer = { layer -> layer != UiLayerId.ApplicationOverlay }, - ) - - assertEquals( - listOf(0xFF000001.toInt(), 0xFF000003.toInt(), 0xFF000004.toInt()), - out.map { - (it as RenderCommand.DrawRect).color - }, - ) - } - - @Test - fun `composePaintCommands skips system overlay render when disabled`() { - val root = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000001.toInt())) - val appOverlay = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000002.toInt())) - val system = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000003.toInt())) - val debug = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000004.toInt())) - val out = ArrayList() - - OverlayLayerContracts.composePaintCommands( - applicationRoot = root, - applicationOverlay = appOverlay, - systemOverlay = system, - debug = debug, - out = out, - shouldRenderLayer = { layer -> layer != UiLayerId.SystemOverlay }, - ) - - assertEquals( - listOf(0xFF000001.toInt(), 0xFF000002.toInt(), 0xFF000004.toInt()), - out.map { - (it as RenderCommand.DrawRect).color - }, - ) - } - - @Test - fun `firstInputConsumer skips app overlay input when disabled`() { - val order = ArrayList() - val consumed = - OverlayLayerContracts.firstInputConsumer( - canConsume = { layer -> - order += layer - layer == UiLayerId.ApplicationOverlay || layer == UiLayerId.ApplicationRoot - }, - isLayerInputEnabled = { layer -> layer != UiLayerId.ApplicationOverlay }, - ) - assertEquals(UiLayerId.ApplicationRoot, consumed) - assertEquals(listOf(UiLayerId.Debug, UiLayerId.SystemOverlay, UiLayerId.ApplicationRoot), order) - } - - @Test - fun `firstInputConsumer skips system overlay input when disabled`() { - val order = ArrayList() - val consumed = - OverlayLayerContracts.firstInputConsumer( - canConsume = { layer -> - order += layer - layer == UiLayerId.SystemOverlay || layer == UiLayerId.ApplicationRoot - }, - isLayerInputEnabled = { layer -> layer != UiLayerId.SystemOverlay }, - ) - assertEquals(UiLayerId.ApplicationRoot, consumed) - assertEquals(listOf(UiLayerId.Debug, UiLayerId.ApplicationOverlay, UiLayerId.ApplicationRoot), order) - } - - @Test - fun `color picker popup defaults to application overlay ownership`() { - val request = - ColorPickerPopupRequest( - owner = "owner", - anchorRect = Rect(10, 12, 20, 18), - state = ColorPickerState(color = RgbaColor.WHITE), - ) - assertEquals(OverlayOwnerScope.Application, request.ownerScope) - assertEquals(UiLayerId.ApplicationOverlay, ColorPickerPopupOverlayOwnership.resolveLayer(request)) - } - - @Test - fun `system-owned color picker popup resolves to system overlay`() { - val request = - ColorPickerPopupRequest( - owner = "owner", - ownerScope = OverlayOwnerScope.System, - anchorRect = Rect(10, 12, 20, 18), - state = ColorPickerState(color = RgbaColor.WHITE), - ) - assertEquals(UiLayerId.SystemOverlay, ColorPickerPopupOverlayOwnership.resolveLayer(request)) - } -} diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContractsTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContractsTests.kt index 0346f69..ae449f6 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContractsTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContractsTests.kt @@ -167,14 +167,14 @@ class PortalHostContractsTests { } @Test - fun `overlay layer adapter maps current application and system hosts to portal surfaces`() { + fun `domain surface adapter maps current application and system hosts to portal surfaces`() { assertEquals( - ScreenDomainSurface(ScreenDomainId.Application, ScreenDomainSurfaceRole.Portal), - OverlayLayerPortalHostAdapter(ApplicationOverlayHost()).surface, + ScreenDomainSurfaces.ApplicationPortal, + DomainSurfacePortalHostAdapter(ApplicationOverlayHost()).surface, ) assertEquals( - ScreenDomainSurface(ScreenDomainId.System, ScreenDomainSurfaceRole.Portal), - OverlayLayerPortalHostAdapter(SystemOverlayHost(InspectorController())).surface, + ScreenDomainSurfaces.SystemPortal, + DomainSurfacePortalHostAdapter(SystemOverlayHost(InspectorController())).surface, ) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/ScreenDomainContractsTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/ScreenDomainContractsTests.kt new file mode 100644 index 0000000..5e3a7f6 --- /dev/null +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/ScreenDomainContractsTests.kt @@ -0,0 +1,249 @@ +package org.dreamfinity.dsgl.core.overlay + +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupRequest +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerState +import org.dreamfinity.dsgl.core.colorpicker.RgbaColor +import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.render.RenderCommand +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class ScreenDomainContractsTests { + @Test + fun `all domains expose root and portal surfaces`() { + assertEquals( + listOf( + ScreenDomainSurfaces.ApplicationRoot, + ScreenDomainSurfaces.ApplicationPortal, + ScreenDomainSurfaces.SystemRoot, + ScreenDomainSurfaces.SystemPortal, + ScreenDomainSurfaces.DebugRoot, + ScreenDomainSurfaces.DebugPortal, + ), + ScreenDomainSurfaces.allSurfaces, + ) + } + + @Test + fun `paint order is application root portal then system root portal then debug root portal`() { + assertEquals( + listOf( + ScreenDomainSurfaces.ApplicationRoot, + ScreenDomainSurfaces.ApplicationPortal, + ScreenDomainSurfaces.SystemRoot, + ScreenDomainSurfaces.SystemPortal, + ScreenDomainSurfaces.DebugRoot, + ScreenDomainSurfaces.DebugPortal, + ), + ScreenDomainSurfaces.paintOrder, + ) + } + + @Test + fun `input priority is reverse domain surface order`() { + assertEquals( + listOf( + ScreenDomainSurfaces.DebugPortal, + ScreenDomainSurfaces.DebugRoot, + ScreenDomainSurfaces.SystemPortal, + ScreenDomainSurfaces.SystemRoot, + ScreenDomainSurfaces.ApplicationPortal, + ScreenDomainSurfaces.ApplicationRoot, + ), + ScreenDomainSurfaces.inputPriority, + ) + } + + @Test + fun `owner scope resolves to compatible portal domain surfaces`() { + assertEquals( + ScreenDomainSurfaces.ApplicationPortal, + ScreenDomainSurfaces.portalSurfaceForOwner(OverlayOwnerScope.Application), + ) + assertEquals( + ScreenDomainSurfaces.SystemPortal, + ScreenDomainSurfaces.portalSurfaceForOwner(OverlayOwnerScope.System), + ) + } + + @Test + fun `transient ownership uses owner scope and not cursor position`() { + val appAtA = + ScreenDomainSurfaces.portalSurfaceForOwner( + ownerScope = OverlayOwnerScope.Application, + cursorX = 10, + cursorY = 20, + ) + val appAtB = + ScreenDomainSurfaces.portalSurfaceForOwner( + ownerScope = OverlayOwnerScope.Application, + cursorX = 800, + cursorY = 640, + ) + val systemAtA = + ScreenDomainSurfaces.portalSurfaceForOwner( + ownerScope = OverlayOwnerScope.System, + cursorX = 10, + cursorY = 20, + ) + val systemAtB = + ScreenDomainSurfaces.portalSurfaceForOwner( + ownerScope = OverlayOwnerScope.System, + cursorX = 800, + cursorY = 640, + ) + + assertEquals(ScreenDomainSurfaces.ApplicationPortal, appAtA) + assertEquals(ScreenDomainSurfaces.ApplicationPortal, appAtB) + assertEquals(ScreenDomainSurfaces.SystemPortal, systemAtA) + assertEquals(ScreenDomainSurfaces.SystemPortal, systemAtB) + } + + @Test + fun `firstInputConsumer respects configured input priority`() { + val consumed = + ScreenDomainSurfaces.firstInputConsumer( + canConsume = { surface -> + surface == ScreenDomainSurfaces.DebugRoot || + surface == ScreenDomainSurfaces.ApplicationPortal || + surface == ScreenDomainSurfaces.ApplicationRoot + }, + ) + assertEquals(ScreenDomainSurfaces.DebugRoot, consumed) + } + + @Test + fun `firstInputConsumer returns null when no surface consumes`() { + val consumed = ScreenDomainSurfaces.firstInputConsumer(canConsume = { false }) + assertNull(consumed) + } + + @Test + fun `composePaintCommands follows configured domain surface order`() { + val root = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000001.toInt())) + val appPortal = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000002.toInt())) + val systemRoot = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000003.toInt())) + val systemPortal = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000004.toInt())) + val debugRoot = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000005.toInt())) + val debugPortal = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000006.toInt())) + val out = ArrayList() + + ScreenDomainSurfaces.composePaintCommands( + applicationRoot = root, + applicationPortal = appPortal, + systemRoot = systemRoot, + systemPortal = systemPortal, + debugRoot = debugRoot, + debugPortal = debugPortal, + out = out, + ) + + assertEquals( + listOf( + 0xFF000001.toInt(), + 0xFF000002.toInt(), + 0xFF000003.toInt(), + 0xFF000004.toInt(), + 0xFF000005.toInt(), + 0xFF000006.toInt(), + ), + out.map { (it as RenderCommand.DrawRect).color }, + ) + } + + @Test + fun `composePaintCommands accepts empty root and portal surfaces`() { + val root = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000001.toInt())) + val systemPortal = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000004.toInt())) + val out = ArrayList() + + ScreenDomainSurfaces.composePaintCommands( + applicationRoot = root, + applicationPortal = emptyList(), + systemRoot = emptyList(), + systemPortal = systemPortal, + debugRoot = emptyList(), + debugPortal = emptyList(), + out = out, + ) + + assertEquals( + listOf(0xFF000001.toInt(), 0xFF000004.toInt()), + out.map { (it as RenderCommand.DrawRect).color }, + ) + } + + @Test + fun `composePaintCommands skips application portal render when disabled`() { + val root = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000001.toInt())) + val appPortal = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000002.toInt())) + val systemPortal = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000003.toInt())) + val debugRoot = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF000004.toInt())) + val out = ArrayList() + + ScreenDomainSurfaces.composePaintCommands( + applicationRoot = root, + applicationPortal = appPortal, + systemPortal = systemPortal, + debugRoot = debugRoot, + out = out, + shouldRenderSurface = { surface -> surface != ScreenDomainSurfaces.ApplicationPortal }, + ) + + assertEquals( + listOf(0xFF000001.toInt(), 0xFF000003.toInt(), 0xFF000004.toInt()), + out.map { (it as RenderCommand.DrawRect).color }, + ) + } + + @Test + fun `firstInputConsumer skips configured surface input`() { + val order = ArrayList() + val consumed = + ScreenDomainSurfaces.firstInputConsumer( + canConsume = { surface -> + order += surface + surface == ScreenDomainSurfaces.ApplicationPortal || + surface == ScreenDomainSurfaces.ApplicationRoot + }, + isSurfaceInputEnabled = { surface -> surface != ScreenDomainSurfaces.ApplicationPortal }, + ) + + assertEquals(ScreenDomainSurfaces.ApplicationRoot, consumed) + assertEquals( + listOf( + ScreenDomainSurfaces.DebugPortal, + ScreenDomainSurfaces.DebugRoot, + ScreenDomainSurfaces.SystemPortal, + ScreenDomainSurfaces.SystemRoot, + ScreenDomainSurfaces.ApplicationRoot, + ), + order, + ) + } + + @Test + fun `color picker popup defaults to application portal ownership`() { + val request = + ColorPickerPopupRequest( + owner = "owner", + anchorRect = Rect(10, 12, 20, 18), + state = ColorPickerState(color = RgbaColor.WHITE), + ) + assertEquals(OverlayOwnerScope.Application, request.ownerScope) + assertEquals(ScreenDomainSurfaces.ApplicationPortal, ColorPickerPopupOverlayOwnership.resolveSurface(request)) + } + + @Test + fun `system-owned color picker popup resolves to system portal`() { + val request = + ColorPickerPopupRequest( + owner = "owner", + ownerScope = OverlayOwnerScope.System, + anchorRect = Rect(10, 12, 20, 18), + state = ColorPickerState(color = RgbaColor.WHITE), + ) + assertEquals(ScreenDomainSurfaces.SystemPortal, ColorPickerPopupOverlayOwnership.resolveSurface(request)) + } +} From 8984c590f62fa3419f55f0a3f8b63f3ffcd19097 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Wed, 27 May 2026 15:59:29 +0300 Subject: [PATCH 65/78] adding new domain surface handlers and updating tests for fallthrough and consumption logic; --- .../dsgl/mcForge1710/DsglScreenHost.kt | 257 +++++++++++------- .../overlay/LiveLayerInteractionPathTests.kt | 82 +++++- 2 files changed, 241 insertions(+), 98 deletions(-) diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index ac89ea2..3b366ca 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -821,33 +821,27 @@ abstract class DsglScreenHost( mc.dispatchKeypresses() return true } - if (consumeOverlayKeyDown( + when ( + dispatchDomainKeyDown( keyCode = keyCode, keyChar = keyChar, inspectorMouseX = inspectorMouseX, inspectorMouseY = inspectorMouseY, ) ) { - mc.dispatchKeypresses() - return true - } - if (keyCode == Keyboard.KEY_F6) { - StyleEngine.forceReloadStylesheets() - requestRebuild("style reload") - } - if (pressedKeys.add(keyCode)) { - val downEvent = KeyboardKeyDownEvent(keyChar, keyCode) - EventBus.post(downEvent) - if (downEvent.cancelled) { - pressedKeys.remove(keyCode) - } else { - window.onKeyTyped(keyChar, keyCode) - if (keyCode == Keyboard.KEY_ESCAPE) { - mc.displayGuiScreen(null) - } + DomainKeyDispatchResult.HigherSurfaceConsumed -> { + mc.dispatchKeypresses() + return true } + + DomainKeyDispatchResult.ApplicationRootHandled, DomainKeyDispatchResult.None -> return false } - return false + } + + private enum class DomainKeyDispatchResult { + None, + HigherSurfaceConsumed, + ApplicationRootHandled, } private fun handleKeyboardKeyUp( @@ -880,12 +874,17 @@ abstract class DsglScreenHost( val tree = prepareMouseInputTree() ?: return val inputEvent = readMouseInputEvent() syncMouseInputFrame(tree, inputEvent) - if (consumeOverlayPointerPhase(inputEvent)) return - refreshHoverTarget(inputEvent.mouseX, inputEvent.mouseY) - if (inputEvent.mouseButton > 2) return - dispatchApplicationRootPointerPhase(tree, inputEvent) - dispatchApplicationRootWheelPhase(inputEvent) - finishMouseInputEvent(inputEvent) + when (dispatchDomainPointerPhase(tree, inputEvent)) { + DomainPointerDispatchResult.HigherSurfaceConsumed -> return + DomainPointerDispatchResult.ApplicationRootHandled, DomainPointerDispatchResult.None -> + finishMouseInputEvent(inputEvent) + } + } + + private enum class DomainPointerDispatchResult { + None, + HigherSurfaceConsumed, + ApplicationRootHandled, } private data class MouseInputEvent( @@ -895,6 +894,13 @@ abstract class DsglScreenHost( val mouseButton: Int, ) + private data class DomainPointerDispatchContext( + val inputEvent: MouseInputEvent, + val mappedButton: MouseButton?, + val buttonPressed: Boolean, + val applicationRootPressMove: Boolean, + ) + private fun prepareMouseInputTree(): DomTree? { val tree = domTree ?: return null if (needsLayout) { @@ -948,30 +954,115 @@ abstract class DsglScreenHost( host.onInputFrame(lastWidth, lastHeight) } - private fun consumeOverlayPointerPhase(inputEvent: MouseInputEvent): Boolean { - val appPressMove = inputEvent.mouseButton == -1 && eventButton != -1 - if (!appPressMove && - consumeOverlayPointerEvent( - mouseX = inputEvent.mouseX, - mouseY = inputEvent.mouseY, - dWheel = inputEvent.dWheel, - mouseButton = inputEvent.mouseButton, + private fun dispatchDomainPointerPhase(tree: DomTree, inputEvent: MouseInputEvent): DomainPointerDispatchResult { + val context = + DomainPointerDispatchContext( + inputEvent = inputEvent, + mappedButton = mapButton(inputEvent.mouseButton), + buttonPressed = Mouse.getEventButtonState(), + applicationRootPressMove = inputEvent.mouseButton == -1 && eventButton != -1, ) - ) { - consumeOverlayPointerState(inputEvent.mouseX, inputEvent.mouseY) - return true + val consumedBy = + domainOrchestrator.firstInputConsumer( + canConsume = { surface -> + consumeDomainPointerSurface(surface = surface, tree = tree, context = context) + }, + isSurfaceInputEnabled = OverlayLayerDebugState::isInputEnabled, + ) + return when (consumedBy) { + null -> DomainPointerDispatchResult.None + ScreenDomainSurfaces.ApplicationRoot -> DomainPointerDispatchResult.ApplicationRootHandled + else -> { + consumeOverlayPointerState(inputEvent.mouseX, inputEvent.mouseY) + DomainPointerDispatchResult.HigherSurfaceConsumed + } } - return false } - private fun dispatchApplicationRootPointerPhase(tree: DomTree, inputEvent: MouseInputEvent) { + private fun consumeDomainPointerSurface( + surface: ScreenDomainSurface, + tree: DomTree, + context: DomainPointerDispatchContext, + ): Boolean = + when (surface) { + ScreenDomainSurfaces.DebugPortal -> false + ScreenDomainSurfaces.DebugRoot -> consumeDebugRootPointerSurface(context) + ScreenDomainSurfaces.SystemPortal -> consumeSystemPortalPointerSurface(context) + ScreenDomainSurfaces.SystemRoot -> false + ScreenDomainSurfaces.ApplicationPortal -> consumeApplicationPortalPointerSurface(context) + ScreenDomainSurfaces.ApplicationRoot -> + dispatchApplicationRootPointerSurface( + tree = tree, + inputEvent = context.inputEvent, + ) + + else -> false + } + + private fun consumeDebugRootPointerSurface(context: DomainPointerDispatchContext): Boolean { + if (context.applicationRootPressMove) { + return false + } + return consumeDebugPointerEvent( + mouseX = context.inputEvent.mouseX, + mouseY = context.inputEvent.mouseY, + dWheel = context.inputEvent.dWheel, + mappedButton = context.mappedButton, + mouseButton = context.inputEvent.mouseButton, + buttonPressed = context.buttonPressed, + ) + } + + private fun consumeSystemPortalPointerSurface(context: DomainPointerDispatchContext): Boolean { + if (context.applicationRootPressMove) { + return false + } + return consumeSystemOverlayPointerEvent( + mouseX = context.inputEvent.mouseX, + mouseY = context.inputEvent.mouseY, + dWheel = context.inputEvent.dWheel, + mouseButton = context.inputEvent.mouseButton, + mappedButton = context.mappedButton, + buttonPressed = context.buttonPressed, + ) + } + + private fun consumeApplicationPortalPointerSurface(context: DomainPointerDispatchContext): Boolean { + if (context.applicationRootPressMove) { + return false + } + return consumeApplicationOverlayPointerEvent( + mouseX = context.inputEvent.mouseX, + mouseY = context.inputEvent.mouseY, + dWheel = context.inputEvent.dWheel, + mouseButton = context.inputEvent.mouseButton, + mappedButton = context.mappedButton, + buttonPressed = context.buttonPressed, + ) + } + + private fun dispatchApplicationRootPointerSurface(tree: DomTree, inputEvent: MouseInputEvent): Boolean { + refreshHoverTarget(inputEvent.mouseX, inputEvent.mouseY) + if (inputEvent.mouseButton > 2) { + return false + } + val pointerHandled = dispatchApplicationRootPointerPhase(tree, inputEvent) + val wheelHandled = dispatchApplicationRootWheelPhase(inputEvent) + return pointerHandled || wheelHandled + } + + private fun dispatchApplicationRootPointerPhase(tree: DomTree, inputEvent: MouseInputEvent): Boolean { if (Mouse.getEventButtonState()) { dispatchApplicationRootPointerDown(tree, inputEvent) + return true } else if (inputEvent.mouseButton != -1 && eventButton == inputEvent.mouseButton) { dispatchApplicationRootPointerUp(tree, inputEvent) + return true } else if (eventButton != -1 && lastMouseEvent > 0L) { dispatchApplicationRootPointerDrag(tree, inputEvent) + return true } + return false } private fun dispatchApplicationRootPointerDown(tree: DomTree, inputEvent: MouseInputEvent) { @@ -1045,7 +1136,7 @@ abstract class DsglScreenHost( } } - private fun dispatchApplicationRootWheelPhase(inputEvent: MouseInputEvent) { + private fun dispatchApplicationRootWheelPhase(inputEvent: MouseInputEvent): Boolean { if (inputEvent.dWheel != 0) { val wheelTarget = resolveWheelTarget() if (wheelTarget != null) { @@ -1055,8 +1146,10 @@ abstract class DsglScreenHost( if (!wheelEvent.cancelled) { bubbleGenericWheel(wheelTarget, inputEvent.mouseX, inputEvent.mouseY, inputEvent.dWheel) } + return true } } + return false } private fun finishMouseInputEvent(inputEvent: MouseInputEvent) { @@ -1064,12 +1157,12 @@ abstract class DsglScreenHost( lastMouseY = inputEvent.mouseY } - private fun consumeOverlayKeyDown( + private fun dispatchDomainKeyDown( keyCode: Int, keyChar: Char, inspectorMouseX: Int, inspectorMouseY: Int, - ): Boolean { + ): DomainKeyDispatchResult { val consumedBy = domainOrchestrator.firstInputConsumer( canConsume = { surface -> @@ -1086,12 +1179,41 @@ abstract class DsglScreenHost( ) ScreenDomainSurfaces.ApplicationPortal -> consumeApplicationOverlayKeyDown(keyCode, keyChar) + ScreenDomainSurfaces.ApplicationRoot -> { + dispatchApplicationRootKeyDown(keyCode, keyChar) + true + } + ScreenDomainSurfaces.SystemRoot -> false else -> false } }, isSurfaceInputEnabled = OverlayLayerDebugState::isInputEnabled, ) - return consumedBy != null + return when (consumedBy) { + null -> DomainKeyDispatchResult.None + ScreenDomainSurfaces.ApplicationRoot -> DomainKeyDispatchResult.ApplicationRootHandled + else -> DomainKeyDispatchResult.HigherSurfaceConsumed + } + } + + private fun dispatchApplicationRootKeyDown(keyCode: Int, keyChar: Char) { + // TODO(Veritaris): remove this handling from production build + if (keyCode == Keyboard.KEY_F6) { + StyleEngine.forceReloadStylesheets() + requestRebuild("style reload") + } + if (pressedKeys.add(keyCode)) { + val downEvent = KeyboardKeyDownEvent(keyChar, keyCode) + EventBus.post(downEvent) + if (downEvent.cancelled) { + pressedKeys.remove(keyCode) + } else { + window.onKeyTyped(keyChar, keyCode) + if (keyCode == Keyboard.KEY_ESCAPE) { + mc.displayGuiScreen(null) + } + } + } } private fun consumeSystemOverlayKeyDown( @@ -1132,57 +1254,6 @@ abstract class DsglScreenHost( return false } - private fun consumeOverlayPointerEvent( - mouseX: Int, - mouseY: Int, - dWheel: Int, - mouseButton: Int, - ): Boolean { - val mappedButton = mapButton(mouseButton) - val buttonPressed = Mouse.getEventButtonState() - val consumedBy = - domainOrchestrator.firstInputConsumer( - canConsume = { surface -> - when (surface) { - ScreenDomainSurfaces.DebugPortal -> false - ScreenDomainSurfaces.DebugRoot -> - consumeDebugPointerEvent( - mouseX = mouseX, - mouseY = mouseY, - dWheel = dWheel, - mappedButton = mappedButton, - mouseButton = mouseButton, - buttonPressed = buttonPressed, - ) - - ScreenDomainSurfaces.SystemPortal -> - consumeSystemOverlayPointerEvent( - mouseX = mouseX, - mouseY = mouseY, - dWheel = dWheel, - mouseButton = mouseButton, - mappedButton = mappedButton, - buttonPressed = buttonPressed, - ) - - ScreenDomainSurfaces.ApplicationPortal -> - consumeApplicationOverlayPointerEvent( - mouseX = mouseX, - mouseY = mouseY, - dWheel = dWheel, - mouseButton = mouseButton, - mappedButton = mappedButton, - buttonPressed = buttonPressed, - ) - - else -> false - } - }, - isSurfaceInputEnabled = OverlayLayerDebugState::isInputEnabled, - ) - return consumedBy != null - } - private fun consumeDebugPointerEvent( mouseX: Int, mouseY: Int, diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt index 16d0546..621d17b 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt @@ -51,10 +51,14 @@ class LiveLayerInteractionPathTests { } @Test - fun `runtime layer path resolves in debug system app-overlay app-root order`() { - val callOrder = ArrayList(4) + fun `runtime input path resolves in full domain surface order`() { + val callOrder = ArrayList(6) val fixture = LiveLayerInputFixture( + debugPortalHandler = { _, _, _ -> + callOrder += ScreenDomainSurfaces.DebugPortal + false + }, debugHandler = { _, _, _ -> callOrder += ScreenDomainSurfaces.DebugRoot false @@ -63,6 +67,10 @@ class LiveLayerInteractionPathTests { callOrder += ScreenDomainSurfaces.SystemPortal false }, + systemRootHandler = { _, _, _ -> + callOrder += ScreenDomainSurfaces.SystemRoot + false + }, applicationOverlayHandler = { _, _, _ -> callOrder += ScreenDomainSurfaces.ApplicationPortal false @@ -77,8 +85,10 @@ class LiveLayerInteractionPathTests { assertEquals(ScreenDomainSurfaces.ApplicationRoot, consumedBy) assertEquals( listOf( + ScreenDomainSurfaces.DebugPortal, ScreenDomainSurfaces.DebugRoot, ScreenDomainSurfaces.SystemPortal, + ScreenDomainSurfaces.SystemRoot, ScreenDomainSurfaces.ApplicationPortal, ScreenDomainSurfaces.ApplicationRoot, ), @@ -87,7 +97,42 @@ class LiveLayerInteractionPathTests { } @Test - fun `debug layer consumption prevents lower-layer fallthrough`() { + fun `debug portal consumption prevents lower-domain fallthrough`() { + var debugRootReceived = false + var systemReceived = false + var appOverlayReceived = false + var appRootReceived = false + val fixture = + LiveLayerInputFixture( + debugPortalHandler = { _, _, _ -> true }, + debugHandler = { _, _, _ -> + debugRootReceived = true + false + }, + systemOverlayHandler = { _, _, _ -> + systemReceived = true + false + }, + applicationOverlayHandler = { _, _, _ -> + appOverlayReceived = true + false + }, + ) + val consumedBy = + fixture.dispatchMouseDown(12, 14, MouseButton.LEFT) { + appRootReceived = true + true + } + + assertEquals(ScreenDomainSurfaces.DebugPortal, consumedBy) + assertFalse(debugRootReceived) + assertFalse(systemReceived) + assertFalse(appOverlayReceived) + assertFalse(appRootReceived) + } + + @Test + fun `debug root consumption prevents lower-domain fallthrough`() { var systemReceived = false var appOverlayReceived = false var appRootReceived = false @@ -115,6 +160,31 @@ class LiveLayerInteractionPathTests { assertFalse(appRootReceived) } + @Test + fun `system root consumption prevents lower-domain fallthrough`() { + var appOverlayReceived = false + var appRootReceived = false + val fixture = + LiveLayerInputFixture( + debugHandler = { _, _, _ -> false }, + systemOverlayHandler = { _, _, _ -> false }, + systemRootHandler = { _, _, _ -> true }, + applicationOverlayHandler = { _, _, _ -> + appOverlayReceived = true + false + }, + ) + val consumedBy = + fixture.dispatchMouseDown(12, 14, MouseButton.LEFT) { + appRootReceived = true + true + } + + assertEquals(ScreenDomainSurfaces.SystemRoot, consumedBy) + assertFalse(appOverlayReceived) + assertFalse(appRootReceived) + } + @Test fun `system overlay consumption prevents lower-layer fallthrough`() { val systemHost = SystemOverlayHost(InspectorController()) @@ -703,6 +773,8 @@ class LiveLayerInteractionPathTests { private val debugHandler: (Int, Int, MouseButton) -> Boolean, private val systemOverlayHandler: (Int, Int, MouseButton) -> Boolean, private val applicationOverlayHandler: (Int, Int, MouseButton) -> Boolean, + private val debugPortalHandler: (Int, Int, MouseButton) -> Boolean = { _, _, _ -> false }, + private val systemRootHandler: (Int, Int, MouseButton) -> Boolean = { _, _, _ -> false }, ) { fun dispatchMouseDown( mouseX: Int, @@ -713,10 +785,10 @@ class LiveLayerInteractionPathTests { ScreenDomainSurfaces.firstInputConsumer( canConsume = { surface -> when (surface) { - ScreenDomainSurfaces.DebugPortal -> false + ScreenDomainSurfaces.DebugPortal -> debugPortalHandler(mouseX, mouseY, button) ScreenDomainSurfaces.DebugRoot -> debugHandler(mouseX, mouseY, button) ScreenDomainSurfaces.SystemPortal -> systemOverlayHandler(mouseX, mouseY, button) - ScreenDomainSurfaces.SystemRoot -> false + ScreenDomainSurfaces.SystemRoot -> systemRootHandler(mouseX, mouseY, button) ScreenDomainSurfaces.ApplicationPortal -> applicationOverlayHandler(mouseX, mouseY, button) ScreenDomainSurfaces.ApplicationRoot -> applicationRootHandler() else -> false From aa495eb880e0deae09c3e30574be77e883ed3410 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Wed, 27 May 2026 18:43:52 +0300 Subject: [PATCH 66/78] adding portal lifecycle, dismiss, and backdrop policies; --- .../dsgl/core/overlay/PortalHostContracts.kt | 80 +++++- .../core/overlay/PortalHostContractsTests.kt | 239 +++++++++++++++++- 2 files changed, 315 insertions(+), 4 deletions(-) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContracts.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContracts.kt index a4df4a1..39edabe 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContracts.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContracts.kt @@ -47,6 +47,11 @@ internal enum class PortalDismissPolicy { EscapeOrOutsidePointerDown, } +internal enum class PortalBackdropPolicy { + None, + ConsumeOutsidePointerDown, +} + internal enum class PortalInputPolicy { None, DomOnly, @@ -60,6 +65,23 @@ internal enum class PortalFocusPolicy { TrapFocus, } +internal enum class PortalLifecyclePolicy { + Manual, + CloseOnUnmount, +} + +internal enum class PortalPointerRegion { + InsideEntry, + OutsideEntry, +} + +internal data class PortalPointerPolicyResult( + val entry: PortalEntry, + val region: PortalPointerRegion, + val shouldClose: Boolean, + val consumed: Boolean, +) + internal data class PortalEntryState( val id: PortalEntryId, val ownerToken: Any, @@ -68,11 +90,14 @@ internal data class PortalEntryState( val dismissPolicy: PortalDismissPolicy = PortalDismissPolicy.None, val inputPolicy: PortalInputPolicy = PortalInputPolicy.DomOnly, val focusPolicy: PortalFocusPolicy = PortalFocusPolicy.Preserve, + val backdropPolicy: PortalBackdropPolicy = PortalBackdropPolicy.None, + val lifecyclePolicy: PortalLifecyclePolicy = PortalLifecyclePolicy.CloseOnUnmount, ) { var active: Boolean = false internal set var placement: PortalEntryPlacement? = null internal set + private var protectedBounds: List = emptyList() fun activate(placement: PortalEntryPlacement) { this.placement = placement @@ -82,6 +107,22 @@ internal data class PortalEntryState( fun deactivate() { active = false placement = null + protectedBounds = emptyList() + } + + fun updateProtectedBounds(bounds: List) { + protectedBounds = bounds.filter { it.width > 0 && it.height > 0 } + } + + fun containsPointer(mouseX: Int, mouseY: Int, node: DOMNode?): Boolean { + if (node?.containsGlobalPoint(mouseX, mouseY) == true) return true + val activePlacement = placement ?: return false + if (activePlacement.bounds.entryBounds + .contains(mouseX, mouseY) + ) { + return true + } + return protectedBounds.any { it.contains(mouseX, mouseY) } } } @@ -163,7 +204,11 @@ internal class PortalHost( fun clearRefs() { entriesById.values.forEach { entry -> entry.clearRefs() - entry.close() + if (entry.state.lifecyclePolicy == PortalLifecyclePolicy.CloseOnUnmount) { + entry.close() + } else { + entry.state.deactivate() + } } entriesById.clear() } @@ -171,6 +216,39 @@ internal class PortalHost( fun dispatchInput(handler: (PortalEntry) -> Boolean): Boolean = entriesInInputOrder().any(handler) } +internal fun PortalHost.handleOutsidePointerDownPolicy(mouseX: Int, mouseY: Int): Boolean { + val result = evaluateOutsidePointerDown(mouseX, mouseY) ?: return false + if (result.shouldClose) { + result.entry.close() + } + return result.consumed +} + +internal fun PortalHost.evaluateOutsidePointerDown(mouseX: Int, mouseY: Int): PortalPointerPolicyResult? = + entriesInInputOrder().firstNotNullOfOrNull { entry -> + if (entry.state.containsPointer(mouseX, mouseY, entry.node)) { + return@firstNotNullOfOrNull PortalPointerPolicyResult( + entry = entry, + region = PortalPointerRegion.InsideEntry, + shouldClose = false, + consumed = false, + ) + } + val shouldClose = + entry.state.dismissPolicy == PortalDismissPolicy.OutsidePointerDown || + entry.state.dismissPolicy == PortalDismissPolicy.EscapeOrOutsidePointerDown + val consumed = shouldClose || entry.state.backdropPolicy == PortalBackdropPolicy.ConsumeOutsidePointerDown + if (!consumed) { + return@firstNotNullOfOrNull null + } + PortalPointerPolicyResult( + entry = entry, + region = PortalPointerRegion.OutsideEntry, + shouldClose = shouldClose, + consumed = true, + ) + } + internal data class DomainSurfacePortalHostAdapter( val surfaceHost: DomainSurfaceHost, ) { diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContractsTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContractsTests.kt index ae449f6..3808726 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContractsTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContractsTests.kt @@ -1,11 +1,17 @@ package org.dreamfinity.dsgl.core.overlay import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.dom.applyParent +import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.event.EventBus +import org.dreamfinity.dsgl.core.event.Events import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.event.MouseDownEvent import org.dreamfinity.dsgl.core.inspector.InspectorController +import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayHost import org.dreamfinity.dsgl.core.render.RenderCommand import kotlin.test.AfterTest @@ -234,6 +240,226 @@ class PortalHostContractsTests { ) } + @Test + fun `outside pointer policy evaluates topmost portal entry first`() { + val host = PortalHost(applicationSurface()) + val low = + FakePortalEntry( + id = "low", + config = + FakePortalEntryConfig( + order = PortalEntryOrder(zIndex = 1), + dismissPolicy = PortalDismissPolicy.OutsidePointerDown, + ), + ) + val high = + FakePortalEntry( + id = "high", + config = + FakePortalEntryConfig( + order = PortalEntryOrder(zIndex = 2), + dismissPolicy = PortalDismissPolicy.EscapeOrOutsidePointerDown, + ), + ) + host.register(low) + host.register(high) + low.activate(entryBounds = Rect(20, 20, 40, 40)) + high.activate(entryBounds = Rect(100, 20, 40, 40)) + + val result = host.evaluateOutsidePointerDown(mouseX = 180, mouseY = 180) ?: error("outside policy missing") + + assertEquals(high, result.entry) + assertEquals(PortalPointerRegion.OutsideEntry, result.region) + assertTrue(result.shouldClose) + assertTrue(result.consumed) + } + + @Test + fun `inside topmost portal entry blocks lower outside dismiss policy`() { + val host = PortalHost(applicationSurface()) + val low = + FakePortalEntry( + id = "low", + config = + FakePortalEntryConfig( + order = PortalEntryOrder(zIndex = 1), + dismissPolicy = PortalDismissPolicy.OutsidePointerDown, + ), + ) + val high = + FakePortalEntry( + id = "high", + config = + FakePortalEntryConfig( + order = PortalEntryOrder(zIndex = 2), + dismissPolicy = PortalDismissPolicy.None, + ), + ) + host.register(low) + host.register(high) + low.activate(entryBounds = Rect(20, 20, 40, 40)) + high.activate(entryBounds = Rect(100, 20, 40, 40)) + + val result = host.evaluateOutsidePointerDown(mouseX = 110, mouseY = 30) ?: error("inside policy missing") + + assertEquals(high, result.entry) + assertEquals(PortalPointerRegion.InsideEntry, result.region) + assertFalse(result.shouldClose) + assertFalse(result.consumed) + assertTrue(low.state.active) + } + + @Test + fun `outside pointer policy closes dismissible entry and consumes click-through`() { + val host = PortalHost(applicationSurface()) + val entry = + FakePortalEntry( + id = "dismissible", + config = + FakePortalEntryConfig( + dismissPolicy = PortalDismissPolicy.OutsidePointerDown, + ), + ) + host.register(entry) + entry.activate(entryBounds = Rect(20, 20, 40, 40)) + + assertTrue(host.handleOutsidePointerDownPolicy(mouseX = 200, mouseY = 200)) + + assertEquals(1, entry.closeCalls) + assertFalse(entry.state.active) + } + + @Test + fun `backdrop policy can consume outside pointer without closing entry`() { + val host = PortalHost(applicationSurface()) + val entry = + FakePortalEntry( + id = "backdrop", + config = + FakePortalEntryConfig( + backdropPolicy = PortalBackdropPolicy.ConsumeOutsidePointerDown, + ), + ) + host.register(entry) + entry.activate(entryBounds = Rect(20, 20, 40, 40)) + + assertTrue(host.handleOutsidePointerDownPolicy(mouseX = 200, mouseY = 200)) + + assertEquals(0, entry.closeCalls) + assertTrue(entry.state.active) + } + + @Test + fun `protected bounds count as inside portal interaction for outside dismiss`() { + val host = PortalHost(applicationSurface()) + val entry = + FakePortalEntry( + id = "protected", + config = + FakePortalEntryConfig( + dismissPolicy = PortalDismissPolicy.OutsidePointerDown, + ), + ) + host.register(entry) + entry.activate(entryBounds = Rect(20, 20, 40, 40)) + entry.state.updateProtectedBounds(listOf(Rect(120, 120, 40, 40))) + + val result = host.evaluateOutsidePointerDown(mouseX = 130, mouseY = 130) ?: error("protected policy missing") + + assertEquals(entry, result.entry) + assertEquals(PortalPointerRegion.InsideEntry, result.region) + assertFalse(result.shouldClose) + assertFalse(result.consumed) + assertTrue(entry.state.active) + } + + @Test + fun `manual lifecycle entry deactivates on host clear without component close`() { + val host = PortalHost(applicationSurface()) + val entry = + FakePortalEntry( + id = "manual", + config = + FakePortalEntryConfig( + lifecyclePolicy = PortalLifecyclePolicy.Manual, + ), + ) + host.register(entry) + entry.activate() + + host.clearRefs() + + assertEquals(1, entry.clearRefsCalls) + assertEquals(0, entry.closeCalls) + assertFalse(entry.state.active) + assertTrue(host.entriesInPaintOrder().isEmpty()) + } + + @Test + fun `portal dom event bubbles inside portal tree only`() { + val calls = ArrayList() + val applicationRoot = + ContainerNode(key = "application-root").apply { + bounds = Rect(0, 0, 320, 240) + } + val portalRoot = + ContainerNode(key = "portal-root").apply { + bounds = Rect(20, 20, 200, 120) + } + val portalParent = + ContainerNode(key = "portal-parent") + .apply { + bounds = Rect(30, 30, 120, 80) + }.applyParent(portalRoot) + val portalChild = + ContainerNode(key = "portal-child") + .apply { + bounds = Rect(40, 40, 40, 30) + }.applyParent(portalParent) + EventBus.run { + applicationRoot.addEventListener(Events.MOUSEDOWN) { calls += "application-root" } + portalRoot.addEventListener(Events.MOUSEDOWN) { calls += "portal-root" } + portalParent.addEventListener(Events.MOUSEDOWN) { calls += "portal-parent" } + portalChild.addEventListener(Events.MOUSEDOWN) { calls += "portal-child" } + } + + EventBus.post(MouseDownEvent(45, 45, MouseButton.LEFT).apply { target = portalChild }) + + assertEquals(listOf("portal-child", "portal-parent", "portal-root"), calls) + } + + @Test + fun `portal input router does not bubble portal subtree events into domain root`() { + val calls = ArrayList() + val applicationRoot = + ContainerNode(key = "application-root").apply { + bounds = Rect(0, 0, 320, 240) + } + val portalRoot = + ContainerNode(key = "portal-root").apply { + bounds = Rect(20, 20, 200, 120) + } + val portalParent = + ContainerNode(key = "portal-parent") + .apply { + bounds = Rect(30, 30, 120, 80) + }.applyParent(portalRoot) + ContainerNode(key = "portal-child") + .apply { + bounds = Rect(40, 40, 40, 30) + }.applyParent(portalParent) + EventBus.run { + applicationRoot.addEventListener(Events.MOUSEDOWN) { calls += "application-root" } + portalRoot.addEventListener(Events.MOUSEDOWN) { calls += "portal-root" } + portalParent.addEventListener(Events.MOUSEDOWN) { calls += "portal-parent" } + } + val portalRouter = LayerDomInputRouter { portalRoot } + + assertTrue(portalRouter.handleMouseDown(45, 45, MouseButton.LEFT)) + + assertEquals(listOf("portal-parent", "portal-root"), calls) + } + private fun applicationSurface(): ScreenDomainSurface = ScreenDomainSurface(ScreenDomainId.Application, ScreenDomainSurfaceRole.Portal) private class FakePortalEntry( @@ -246,9 +472,12 @@ class PortalHostContractsTests { ownerToken = config.ownerToken, surface = config.surface, order = config.order, + dismissPolicy = config.dismissPolicy, focusPolicy = config.focusPolicy, + backdropPolicy = config.backdropPolicy, + lifecyclePolicy = config.lifecyclePolicy, ) - override val node: DOMNode? = null + override val node: DOMNode? = config.node var clearRefsCalls: Int = 0 private set var closeCalls: Int = 0 @@ -258,14 +487,14 @@ class PortalHostContractsTests { var renderCalls: Int = 0 private set - fun activate() { + fun activate(entryBounds: Rect = Rect(12, 12, 100, 80)) { state.activate( PortalEntryPlacement( anchorBounds = Rect(10, 10, 20, 20), bounds = PortalEntryBounds( viewportBounds = Rect(0, 0, 320, 240), - entryBounds = Rect(12, 12, 100, 80), + entryBounds = entryBounds, ), ), ) @@ -297,7 +526,11 @@ class PortalHostContractsTests { val ownerToken: Any = Any(), val surface: ScreenDomainSurface = ScreenDomainSurface(ScreenDomainId.Application, ScreenDomainSurfaceRole.Portal), val order: PortalEntryOrder = PortalEntryOrder(zIndex = 0), + val dismissPolicy: PortalDismissPolicy = PortalDismissPolicy.None, val focusPolicy: PortalFocusPolicy = PortalFocusPolicy.Preserve, + val backdropPolicy: PortalBackdropPolicy = PortalBackdropPolicy.None, + val lifecyclePolicy: PortalLifecyclePolicy = PortalLifecyclePolicy.CloseOnUnmount, + val node: DOMNode? = null, val consumeMouseDown: Boolean = false, val paintColor: Int = 0xFFFFFFFF.toInt(), val renderOrder: MutableList? = null, From e9b812415ee66cf98783cb13e0bdfd39ec266a3a Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Wed, 27 May 2026 21:49:11 +0300 Subject: [PATCH 67/78] updating modal portal policies; adding pointer containment, backdrop, and dismiss handlers; --- .../dsgl/mcForge1710/DsglScreenHost.kt | 82 ++++++++- .../DsglScreenHostDomainOrchestrationTests.kt | 38 ++++ .../dsgl/core/components/modal/ModalDsl.kt | 12 +- .../modal/internal/ModalPortalController.kt | 111 ++++++++++-- .../modal/internal/ModalPortalSessionStore.kt | 2 + .../core/overlay/ApplicationOverlayHost.kt | 14 +- .../dsgl/core/overlay/PortalHostContracts.kt | 29 ++- .../ModalPortalKeyboardRegressionTests.kt | 169 ++++++++++++++++++ .../core/overlay/PortalHostContractsTests.kt | 32 ++++ 9 files changed, 452 insertions(+), 37 deletions(-) diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index 3b366ca..26485c4 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -81,6 +81,7 @@ abstract class DsglScreenHost( private var needsLayout: Boolean = true private var lastMouseEvent: Long = 0 private var eventButton: Int = -1 + private var higherSurfacePointerButton: Int = -1 private var lastMouseX: Int = 0 private var lastMouseY: Int = 0 private var lastMoveX: Int = Int.MIN_VALUE @@ -157,6 +158,7 @@ abstract class DsglScreenHost( ) inspector.deactivate() inspectorPointerCaptured = false + higherSurfacePointerButton = -1 colorSamplerOwnershipRouter.reset() activeColorSamplerOwner = ActiveColorSamplerOwner.None activeInlineColorSamplerNode = null @@ -970,9 +972,17 @@ abstract class DsglScreenHost( isSurfaceInputEnabled = OverlayLayerDebugState::isInputEnabled, ) return when (consumedBy) { - null -> DomainPointerDispatchResult.None + null -> + if (isHigherSurfaceOwnedPointerRelease(context)) { + higherSurfacePointerButton = -1 + consumeOverlayPointerState(inputEvent.mouseX, inputEvent.mouseY) + DomainPointerDispatchResult.HigherSurfaceConsumed + } else { + DomainPointerDispatchResult.None + } ScreenDomainSurfaces.ApplicationRoot -> DomainPointerDispatchResult.ApplicationRootHandled else -> { + updateHigherSurfacePointerOwnership(context) consumeOverlayPointerState(inputEvent.mouseX, inputEvent.mouseY) DomainPointerDispatchResult.HigherSurfaceConsumed } @@ -991,14 +1001,32 @@ abstract class DsglScreenHost( ScreenDomainSurfaces.SystemRoot -> false ScreenDomainSurfaces.ApplicationPortal -> consumeApplicationPortalPointerSurface(context) ScreenDomainSurfaces.ApplicationRoot -> - dispatchApplicationRootPointerSurface( - tree = tree, - inputEvent = context.inputEvent, - ) + if (isHigherSurfaceOwnedPointerRelease(context)) { + false + } else { + dispatchApplicationRootPointerSurface( + tree = tree, + inputEvent = context.inputEvent, + ) + } else -> false } + private fun isHigherSurfaceOwnedPointerRelease(context: DomainPointerDispatchContext): Boolean = + context.inputEvent.mouseButton != -1 && + !context.buttonPressed && + context.inputEvent.mouseButton == higherSurfacePointerButton + + private fun updateHigherSurfacePointerOwnership(context: DomainPointerDispatchContext) { + if (context.inputEvent.mouseButton == -1 || context.mappedButton == null) return + if (context.buttonPressed) { + higherSurfacePointerButton = context.inputEvent.mouseButton + } else if (higherSurfacePointerButton == context.inputEvent.mouseButton) { + higherSurfacePointerButton = -1 + } + } + private fun consumeDebugRootPointerSurface(context: DomainPointerDispatchContext): Boolean { if (context.applicationRootPressMove) { return false @@ -1542,6 +1570,50 @@ abstract class DsglScreenHost( isSurfaceInputEnabled = isSurfaceInputEnabled, ) + internal fun debugDispatchApplicationPortalThenRootPointerForTests( + mouseButton: Int, + buttonPressed: Boolean, + applicationPortalConsumes: () -> Boolean, + applicationRootConsumes: () -> Boolean, + ): ScreenDomainSurface? { + val context = + DomainPointerDispatchContext( + inputEvent = + MouseInputEvent( + mouseX = 0, + mouseY = 0, + dWheel = 0, + mouseButton = mouseButton, + ), + mappedButton = mapButton(mouseButton), + buttonPressed = buttonPressed, + applicationRootPressMove = false, + ) + val consumedBy = + domainOrchestrator.firstInputConsumer( + canConsume = { surface -> + when (surface) { + ScreenDomainSurfaces.ApplicationPortal -> applicationPortalConsumes() + ScreenDomainSurfaces.ApplicationRoot -> + if (isHigherSurfaceOwnedPointerRelease(context)) { + false + } else { + applicationRootConsumes() + } + + else -> false + } + }, + ) + if (consumedBy != null && consumedBy != ScreenDomainSurfaces.ApplicationRoot) { + updateHigherSurfacePointerOwnership(context) + } else if (consumedBy == null && isHigherSurfaceOwnedPointerRelease(context)) { + higherSurfacePointerButton = -1 + return ScreenDomainSurfaces.ApplicationPortal + } + return consumedBy + } + private fun setDragCapture(target: DOMNode) { dragCaptureTarget = target dragCaptureKey = target.key diff --git a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt index f6068f2..ca04daa 100644 --- a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt +++ b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt @@ -7,6 +7,7 @@ import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurface import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces import org.dreamfinity.dsgl.core.render.RenderCommand import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Test @@ -130,6 +131,43 @@ class DsglScreenHostDomainOrchestrationTests { assertNull(consumed) } + @Test + fun `host suppresses application root release after portal-owned pointer down`() { + val host = createHost() + var rootReceivedRelease = false + + val consumedDown = + host.debugDispatchApplicationPortalThenRootPointerForTests( + mouseButton = 0, + buttonPressed = true, + applicationPortalConsumes = { true }, + applicationRootConsumes = { true }, + ) + + val consumedUp = + host.debugDispatchApplicationPortalThenRootPointerForTests( + mouseButton = 0, + buttonPressed = false, + applicationPortalConsumes = { false }, + applicationRootConsumes = { + rootReceivedRelease = true + true + }, + ) + val consumedNextRelease = + host.debugDispatchApplicationPortalThenRootPointerForTests( + mouseButton = 0, + buttonPressed = false, + applicationPortalConsumes = { false }, + applicationRootConsumes = { true }, + ) + + assertEquals(ScreenDomainSurfaces.ApplicationPortal, consumedDown) + assertEquals(ScreenDomainSurfaces.ApplicationPortal, consumedUp) + assertEquals(ScreenDomainSurfaces.ApplicationRoot, consumedNextRelease) + assertFalse(rootReceivedRelease) + } + private fun createHost(): DsglScreenHost = object : DsglScreenHost( object : DsglWindow() { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt index fa9ab11..09776ff 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt @@ -91,17 +91,7 @@ private fun UiScope.modalLayer(spec: ModalSpec, modalKey: String, isTopMost: Boo } } onMouseClick = { event -> - if (!isTopMost) { - event.cancelled = true - } else { - val insideDialog = isEventInsideDialog(event.target, dialogKey, event.mouseX, event.mouseY) - if (!insideDialog) { - if (spec.backdrop == BackdropMode.True) { - spec.onHide?.invoke() - } - event.cancelled = true - } - } + event.cancelled = true } onMouseWheel = { event -> val insideDialog = isEventInsideDialog(event.target, dialogKey, event.mouseX, event.mouseY) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt index a07526a..d171bda 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt @@ -1,10 +1,13 @@ package org.dreamfinity.dsgl.core.components.modal.internal import org.dreamfinity.dsgl.core.DomTree +import org.dreamfinity.dsgl.core.components.modal.BackdropMode +import org.dreamfinity.dsgl.core.components.modal.ModalSpec import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.event.EventBus import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.overlay.PortalBackdropPolicy import org.dreamfinity.dsgl.core.overlay.PortalDismissPolicy import org.dreamfinity.dsgl.core.overlay.PortalEntry import org.dreamfinity.dsgl.core.overlay.PortalEntryBounds @@ -15,12 +18,16 @@ import org.dreamfinity.dsgl.core.overlay.PortalEntryState import org.dreamfinity.dsgl.core.overlay.PortalFocusPolicy import org.dreamfinity.dsgl.core.overlay.PortalHost import org.dreamfinity.dsgl.core.overlay.PortalInputPolicy +import org.dreamfinity.dsgl.core.overlay.PortalInsidePointerPolicy +import org.dreamfinity.dsgl.core.overlay.PortalPointerContainmentPolicy import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces +import org.dreamfinity.dsgl.core.overlay.evaluateOutsidePointerDown internal class ModalPortalController { private val portalHost: PortalHost = PortalHost(ScreenDomainSurfaces.ApplicationPortal) private val entriesByPortalKey: LinkedHashMap = LinkedHashMap() + private var pendingPolicyPointerSequence: PendingPolicyPointerSequence? = null fun sync(rootNode: DOMNode, viewportWidth: Int, viewportHeight: Int) { val snapshots = ModalPortalSessionStore.portalSnapshots() @@ -31,6 +38,7 @@ internal class ModalPortalController { ModalPortalEntry(snapshot.portalKey, snapshot.root).also(portalHost::register) } entry.reconcile(snapshot.root) + entry.syncTopMost(snapshot.topMostModal) entry.syncActive(viewportWidth, viewportHeight) } entriesByPortalKey @@ -50,19 +58,50 @@ internal class ModalPortalController { entry.detach() } entriesByPortalKey.clear() + pendingPolicyPointerSequence = null } fun commitActivePortals() { portalHost .entriesInPaintOrder() - .mapNotNull { it as? ModalPortalEntry } - .forEach { entry -> ModalPortalSessionStore.commitPortal(entry.portalKey, entry.root) } + .filterIsInstance() + .forEach { entry -> + entry.syncProtectedDialogBounds() + ModalPortalSessionStore.commitPortal(entry.portalKey, entry.root) + } } fun hasActivePortal(): Boolean = entriesByPortalKey.values.any { entry -> entry.state.active } + fun handlePointerPolicy( + mouseX: Int, + mouseY: Int, + button: MouseButton, + pressed: Boolean, + ): Boolean { + if (!pressed) { + val pending = pendingPolicyPointerSequence ?: return false + if (pending.button != button) return false + pending.dismissEntry?.let { entry -> entry.state.dismiss(entry) } + pendingPolicyPointerSequence = null + return true + } + val result = portalHost.evaluateOutsidePointerDown(mouseX, mouseY) ?: return false + if (result.consumed) { + pendingPolicyPointerSequence = + PendingPolicyPointerSequence( + button = button, + dismissEntry = result.entry.takeIf { result.shouldClose }, + ) + } + return result.consumed + } + internal fun debugActivePortalEntryIds(): List = portalHost.entriesInPaintOrder().map { it.state.id.value } + internal fun debugEvaluatePointerDownPolicy(mouseX: Int, mouseY: Int) = + portalHost.evaluateOutsidePointerDown(mouseX, mouseY) + internal fun debugFindNodeByKey(key: Any?): DOMNode? = debugFindNode { node -> node.key == key } internal fun debugFindNode(predicate: (DOMNode) -> Boolean): DOMNode? = @@ -92,24 +131,19 @@ internal class ModalPortalController { rootNode.children.removeAll(activeRoots.toSet()) rootNode.children.addAll(activeRoots) } - - private fun findNode(root: DOMNode, predicate: (DOMNode) -> Boolean): DOMNode? { - if (predicate(root)) return root - root.children.forEach { child -> - val found = findNode(child, predicate) - if (found != null) { - return found - } - } - return null - } } +private data class PendingPolicyPointerSequence( + val button: MouseButton, + val dismissEntry: PortalEntry?, +) + private class ModalPortalEntry( val portalKey: String, templateRoot: ModalPortalRootNode, ) : PortalEntry { private var tree: DomTree = DomTree(templateRoot) + private var topMostModal: ModalSpec? = null val root: ModalPortalRootNode get() = tree.root as ModalPortalRootNode @@ -123,6 +157,9 @@ private class ModalPortalEntry( dismissPolicy = PortalDismissPolicy.None, inputPolicy = PortalInputPolicy.DomOnly, focusPolicy = PortalFocusPolicy.TrapFocus, + backdropPolicy = PortalBackdropPolicy.ConsumeOutsidePointerDown, + insidePointerPolicy = PortalInsidePointerPolicy.ConsumePointerDown, + pointerContainmentPolicy = PortalPointerContainmentPolicy.ProtectedBoundsOnly, ) override val node: DOMNode get() = root @@ -144,6 +181,28 @@ private class ModalPortalEntry( root.parent = parent } + fun syncTopMost(spec: ModalSpec?) { + topMostModal = spec + state.dismissAction = { + val topMost = topMostModal + if (topMost?.backdrop == BackdropMode.True) { + topMost.onHide?.invoke() + } + } + state.dismissPolicy = + if (spec?.backdrop == BackdropMode.True) { + PortalDismissPolicy.OutsidePointerDown + } else { + PortalDismissPolicy.None + } + state.backdropPolicy = + if (spec != null) { + PortalBackdropPolicy.ConsumeOutsidePointerDown + } else { + PortalBackdropPolicy.None + } + } + fun syncActive(viewportWidth: Int, viewportHeight: Int) { if (!ModalPortalSessionStore.shouldKeepPortalActive(portalKey)) { state.deactivate() @@ -161,6 +220,16 @@ private class ModalPortalEntry( ) } + fun syncProtectedDialogBounds() { + val topMost = + topMostModal ?: run { + state.updateProtectedBounds(emptyList()) + return + } + val dialog = findNode(root) { node -> node.key == ModalPortalSessionStore.dialogKey(portalKey, topMost.key) } + state.updateProtectedBounds(listOfNotNull(dialog?.bounds)) + } + fun detach() { root.parent ?.children @@ -168,6 +237,11 @@ private class ModalPortalEntry( root.parent = null } + override fun clearRefs() { + tree.clearRefs() + EventBus.run { root.clearListenersDeep() } + } + override fun close() { ModalPortalSessionStore.forgetPortal(portalKey) state.deactivate() @@ -179,3 +253,14 @@ private class ModalPortalEntry( override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = false } + +private fun findNode(root: DOMNode, predicate: (DOMNode) -> Boolean): DOMNode? { + if (predicate(root)) return root + root.children.forEach { child -> + val found = findNode(child, predicate) + if (found != null) { + return found + } + } + return null +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalSessionStore.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalSessionStore.kt index aa4822a..b1e7287 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalSessionStore.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalSessionStore.kt @@ -27,6 +27,7 @@ internal object ModalPortalSessionStore { data class PortalSnapshot( val portalKey: String, val root: ModalPortalRootNode, + val topMostModal: ModalSpec?, ) private val states: MutableMap = ConcurrentHashMap() @@ -120,6 +121,7 @@ internal object ModalPortalSessionStore { PortalSnapshot( portalKey = portalKey, root = root, + topMostModal = states[portalKey]?.currentModals?.lastOrNull(), ) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt index 9246f63..2a11a84 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt @@ -64,11 +64,17 @@ class ApplicationOverlayHost( override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = domInputRouter.handleMouseMove(mouseX, mouseY) - override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = - domInputRouter.handleMouseDown(mouseX, mouseY, button) + override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { + val isConsumedByDOM = domInputRouter.handleMouseDown(mouseX, mouseY, button) + val isConsumedByPolicy = modalPortal.handlePointerPolicy(mouseX, mouseY, button, pressed = true) + return isConsumedByDOM || isConsumedByPolicy + } - override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = - domInputRouter.handleMouseUp(mouseX, mouseY, button) + override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { + val isConsumedByDOM = domInputRouter.handleMouseUp(mouseX, mouseY, button) + val isConsumedByPolicy = modalPortal.handlePointerPolicy(mouseX, mouseY, button, pressed = false) + return isConsumedByDOM || isConsumedByPolicy + } override fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = domInputRouter.handleMouseWheel(mouseX, mouseY, delta) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContracts.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContracts.kt index 39edabe..16ef585 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContracts.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContracts.kt @@ -52,6 +52,16 @@ internal enum class PortalBackdropPolicy { ConsumeOutsidePointerDown, } +internal enum class PortalInsidePointerPolicy { + PassThrough, + ConsumePointerDown, +} + +internal enum class PortalPointerContainmentPolicy { + DomOrEntryBounds, + ProtectedBoundsOnly, +} + internal enum class PortalInputPolicy { None, DomOnly, @@ -87,16 +97,19 @@ internal data class PortalEntryState( val ownerToken: Any, val surface: ScreenDomainSurface, val order: PortalEntryOrder, - val dismissPolicy: PortalDismissPolicy = PortalDismissPolicy.None, + var dismissPolicy: PortalDismissPolicy = PortalDismissPolicy.None, val inputPolicy: PortalInputPolicy = PortalInputPolicy.DomOnly, val focusPolicy: PortalFocusPolicy = PortalFocusPolicy.Preserve, - val backdropPolicy: PortalBackdropPolicy = PortalBackdropPolicy.None, + var backdropPolicy: PortalBackdropPolicy = PortalBackdropPolicy.None, + val insidePointerPolicy: PortalInsidePointerPolicy = PortalInsidePointerPolicy.PassThrough, + val pointerContainmentPolicy: PortalPointerContainmentPolicy = PortalPointerContainmentPolicy.DomOrEntryBounds, val lifecyclePolicy: PortalLifecyclePolicy = PortalLifecyclePolicy.CloseOnUnmount, ) { var active: Boolean = false internal set var placement: PortalEntryPlacement? = null internal set + var dismissAction: (() -> Unit)? = null private var protectedBounds: List = emptyList() fun activate(placement: PortalEntryPlacement) { @@ -115,6 +128,9 @@ internal data class PortalEntryState( } fun containsPointer(mouseX: Int, mouseY: Int, node: DOMNode?): Boolean { + if (pointerContainmentPolicy == PortalPointerContainmentPolicy.ProtectedBoundsOnly) { + return protectedBounds.any { it.contains(mouseX, mouseY) } + } if (node?.containsGlobalPoint(mouseX, mouseY) == true) return true val activePlacement = placement ?: return false if (activePlacement.bounds.entryBounds @@ -124,6 +140,10 @@ internal data class PortalEntryState( } return protectedBounds.any { it.contains(mouseX, mouseY) } } + + fun dismiss(entry: PortalEntry) { + dismissAction?.invoke() ?: entry.close() + } } internal data class PortalFrameContext( @@ -219,7 +239,8 @@ internal class PortalHost( internal fun PortalHost.handleOutsidePointerDownPolicy(mouseX: Int, mouseY: Int): Boolean { val result = evaluateOutsidePointerDown(mouseX, mouseY) ?: return false if (result.shouldClose) { - result.entry.close() + result.entry.state + .dismiss(result.entry) } return result.consumed } @@ -231,7 +252,7 @@ internal fun PortalHost.evaluateOutsidePointerDown(mouseX: Int, mouseY: Int): Po entry = entry, region = PortalPointerRegion.InsideEntry, shouldClose = false, - consumed = false, + consumed = entry.state.insidePointerPolicy == PortalInsidePointerPolicy.ConsumePointerDown, ) } val shouldClose = diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalKeyboardRegressionTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalKeyboardRegressionTests.kt index e84e506..c76bef8 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalKeyboardRegressionTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalKeyboardRegressionTests.kt @@ -22,6 +22,8 @@ import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.core.host.DsglWindowHost import org.dreamfinity.dsgl.core.host.Viewport import org.dreamfinity.dsgl.core.overlay.ApplicationOverlayHost +import org.dreamfinity.dsgl.core.overlay.PortalPointerRegion +import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces import org.dreamfinity.dsgl.core.render.RenderCommand import kotlin.test.AfterTest import kotlin.test.Test @@ -140,6 +142,151 @@ class ModalPortalKeyboardRegressionTests { assertTrue(overlay.handleMouseDown(4, 4, MouseButton.LEFT)) } + @Test + fun `modal portal generic policy classifies dialog as inside and backdrop as outside`() { + val hostKey = "tests.modal.portal.portal.policy" + val tree = buildTree(hostKey, listOf(dismissibleBodyModal {})) + trees += tree + tree.render(measureContext, 320, 180) + + val overlay = ApplicationOverlayHost() + overlays += overlay + overlay.render(measureContext, 320, 180) + + val dialog = overlay.modalPortal.debugFindNodeByKey(ModalPortalSessionStore.dialogKey(hostKey, "modal.dismissible")) + assertNotNull(dialog) + val inside = + overlay.modalPortal.debugEvaluatePointerDownPolicy( + mouseX = dialog.bounds.x + dialog.bounds.width / 2, + mouseY = dialog.bounds.y + dialog.bounds.height / 2, + ) + val outside = overlay.modalPortal.debugEvaluatePointerDownPolicy(mouseX = 2, mouseY = 2) + + assertNotNull(inside) + assertEquals(PortalPointerRegion.InsideEntry, inside.region) + assertTrue(inside.consumed) + assertFalse(inside.shouldClose) + assertNotNull(outside) + assertEquals(PortalPointerRegion.OutsideEntry, outside.region) + assertTrue(outside.consumed) + assertTrue(outside.shouldClose) + } + + @Test + fun `modal portal policy blocks application root fallthrough for non interactive dialog body`() { + val hostKey = "tests.modal.portal.portal.policy.inside" + val tree = buildTree(hostKey, listOf(dismissibleBodyModal {})) + trees += tree + tree.render(measureContext, 320, 180) + + val overlay = ApplicationOverlayHost() + overlays += overlay + overlay.render(measureContext, 320, 180) + + val dialog = overlay.modalPortal.debugFindNodeByKey(ModalPortalSessionStore.dialogKey(hostKey, "modal.dismissible")) + assertNotNull(dialog) + var applicationRootReceived = false + val consumedBy = + dispatchApplicationPortalPointer( + overlay = overlay, + mouseX = dialog.bounds.x + dialog.bounds.width / 2, + mouseY = dialog.bounds.y + dialog.bounds.height / 2, + pressed = true, + ) { + applicationRootReceived = true + true + } + + assertEquals(ScreenDomainSurfaces.ApplicationPortal, consumedBy) + assertFalse(applicationRootReceived) + } + + @Test + fun `static modal backdrop consumes without dismissing or falling through`() { + val hostKey = "tests.modal.portal.portal.policy.static" + var hideCount = 0 + val tree = + buildTree( + hostKey, + listOf( + ModalSpec( + key = "modal.static.backdrop", + backdrop = BackdropMode.Static, + onHide = { hideCount += 1 }, + ) { _ -> }, + ), + ) + trees += tree + tree.render(measureContext, 320, 180) + + val overlay = ApplicationOverlayHost() + overlays += overlay + overlay.render(measureContext, 320, 180) + + var applicationRootReceived = false + val consumedBy = + dispatchApplicationPortalPointer( + overlay = overlay, + mouseX = 2, + mouseY = 2, + pressed = true, + ) { + applicationRootReceived = true + true + } + + assertEquals(ScreenDomainSurfaces.ApplicationPortal, consumedBy) + assertFalse(applicationRootReceived) + assertEquals(0, hideCount) + } + + @Test + fun `modal backdrop dismiss consumes full pointer sequence and does not click through`() { + val hostKey = "tests.modal.portal.portal.policy.full.click" + var modals: List = emptyList() + modals = listOf(dismissibleBodyModal { modals = emptyList() }) + var tree = buildTree(hostKey, modals) + trees += tree + val overlay = ApplicationOverlayHost() + overlays += overlay + renderTreeAndOverlay(tree, overlay) + + var applicationRootReceivedDown = false + val consumedDown = + dispatchApplicationPortalPointer( + overlay = overlay, + mouseX = 2, + mouseY = 2, + pressed = true, + ) { + applicationRootReceivedDown = true + true + } + + assertEquals(listOf("modal.dismissible"), modals.map { it.key }) + assertEquals(ScreenDomainSurfaces.ApplicationPortal, consumedDown) + assertFalse(applicationRootReceivedDown) + + var applicationRootReceivedUp = false + val consumedUp = + dispatchApplicationPortalPointer( + overlay = overlay, + mouseX = 2, + mouseY = 2, + pressed = false, + ) { + applicationRootReceivedUp = true + true + } + + assertEquals(ScreenDomainSurfaces.ApplicationPortal, consumedUp) + assertFalse(applicationRootReceivedUp) + assertEquals(emptyList(), modals) + + tree = reconcileTree(tree, buildTree(hostKey, modals)) + renderTreeAndOverlay(tree, overlay) + } + @Test fun `modal portal does not dismiss non static modal when clicking inside dialog body`() { val hostKey = "tests.modal.portal.portal.inside.dismiss" @@ -544,6 +691,28 @@ class ModalPortalKeyboardRegressionTests { ) } + private fun dispatchApplicationPortalPointer( + overlay: ApplicationOverlayHost, + mouseX: Int, + mouseY: Int, + pressed: Boolean, + applicationRootHandler: () -> Boolean, + ) = ScreenDomainSurfaces.firstInputConsumer( + canConsume = { surface -> + when (surface) { + ScreenDomainSurfaces.ApplicationPortal -> + if (pressed) { + overlay.handleMouseDown(mouseX, mouseY, MouseButton.LEFT) + } else { + overlay.handleMouseUp(mouseX, mouseY, MouseButton.LEFT) + } + + ScreenDomainSurfaces.ApplicationRoot -> applicationRootHandler() + else -> false + } + }, + ) + private fun requireNodeByKey(root: DOMNode, key: Any): DOMNode { if (root.key == key) return root root.children.forEach { child -> diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContractsTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContractsTests.kt index 3808726..bb7fcf5 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContractsTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContractsTests.kt @@ -373,6 +373,34 @@ class PortalHostContractsTests { assertTrue(entry.state.active) } + @Test + fun `protected-only containment treats full viewport portal backdrop as outside`() { + val host = PortalHost(applicationSurface()) + val entry = + FakePortalEntry( + id = "modal", + config = + FakePortalEntryConfig( + dismissPolicy = PortalDismissPolicy.OutsidePointerDown, + insidePointerPolicy = PortalInsidePointerPolicy.ConsumePointerDown, + pointerContainmentPolicy = PortalPointerContainmentPolicy.ProtectedBoundsOnly, + ), + ) + host.register(entry) + entry.activate(entryBounds = Rect(0, 0, 320, 240)) + entry.state.updateProtectedBounds(listOf(Rect(80, 60, 120, 80))) + + val inside = host.evaluateOutsidePointerDown(mouseX = 100, mouseY = 80) ?: error("inside policy missing") + val outside = host.evaluateOutsidePointerDown(mouseX = 20, mouseY = 20) ?: error("outside policy missing") + + assertEquals(PortalPointerRegion.InsideEntry, inside.region) + assertTrue(inside.consumed) + assertFalse(inside.shouldClose) + assertEquals(PortalPointerRegion.OutsideEntry, outside.region) + assertTrue(outside.consumed) + assertTrue(outside.shouldClose) + } + @Test fun `manual lifecycle entry deactivates on host clear without component close`() { val host = PortalHost(applicationSurface()) @@ -475,6 +503,8 @@ class PortalHostContractsTests { dismissPolicy = config.dismissPolicy, focusPolicy = config.focusPolicy, backdropPolicy = config.backdropPolicy, + insidePointerPolicy = config.insidePointerPolicy, + pointerContainmentPolicy = config.pointerContainmentPolicy, lifecyclePolicy = config.lifecyclePolicy, ) override val node: DOMNode? = config.node @@ -529,6 +559,8 @@ class PortalHostContractsTests { val dismissPolicy: PortalDismissPolicy = PortalDismissPolicy.None, val focusPolicy: PortalFocusPolicy = PortalFocusPolicy.Preserve, val backdropPolicy: PortalBackdropPolicy = PortalBackdropPolicy.None, + val insidePointerPolicy: PortalInsidePointerPolicy = PortalInsidePointerPolicy.PassThrough, + val pointerContainmentPolicy: PortalPointerContainmentPolicy = PortalPointerContainmentPolicy.DomOrEntryBounds, val lifecyclePolicy: PortalLifecyclePolicy = PortalLifecyclePolicy.CloseOnUnmount, val node: DOMNode? = null, val consumeMouseDown: Boolean = false, From 232a7522eef331349cc0ed3e2780ce53a4987d1f Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Thu, 28 May 2026 12:12:33 +0300 Subject: [PATCH 68/78] updating focus behaviour, interaction handling, and portal dismissal logic; --- .../dsgl/mcForge1710/DsglScreenHost.kt | 20 +- .../core/colorpicker/ColorPickerController.kt | 16 +- .../colorpicker/ColorPickerPopupEngine.kt | 4 +- .../internal/SystemColorPickerOverlayNode.kt | 4 + .../SystemColorPickerPopupBodyNode.kt | 4 + .../dom/elements/ColorPickerInlineNode.kt | 5 +- .../dsgl/core/dom/elements/SelectNode.kt | 5 +- .../dsgl/core/event/FocusManager.kt | 11 + .../dsgl/core/overlay/DomainPortalServices.kt | 3 + .../core/overlay/system/SystemOverlayHost.kt | 5 +- .../dsgl/core/select/SelectEngine.kt | 89 +++++- .../core/select/SelectPortalController.kt | 202 +++++++++++-- .../colorpicker/ColorPickerInlineNodeTests.kt | 24 +- .../core/dom/SelectNodeOwnerScopeTests.kt | 117 ++++++++ .../overlay/LiveLayerInteractionPathTests.kt | 36 ++- .../SystemOverlayColorPickerEntryTests.kt | 97 ++++++ .../dsgl/core/select/SelectEngineTests.kt | 44 +++ .../select/SelectPortalControllerTests.kt | 283 ++++++++++++++++++ 18 files changed, 907 insertions(+), 62 deletions(-) create mode 100644 core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalControllerTests.kt diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index 26485c4..e4fde8a 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -1377,6 +1377,18 @@ abstract class DsglScreenHost( } } + if ( + applicationOverlayHost.handlePortalPointerAfterDom( + mouseX = mouseX, + mouseY = mouseY, + dWheel = dWheel, + button = mappedButton, + pressed = buttonPressed, + ) + ) { + return true + } + if (dWheel != 0 && applicationOverlayHost.handleMouseWheel(mouseX, mouseY, dWheel)) { return true } @@ -1394,13 +1406,7 @@ abstract class DsglScreenHost( return true } - return applicationOverlayHost.handlePortalPointerAfterDom( - mouseX = mouseX, - mouseY = mouseY, - dWheel = dWheel, - button = mappedButton, - pressed = buttonPressed, - ) + return false } private fun consumeOverlayPointerState(mouseX: Int, mouseY: Int) { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt index 9ce2547..85ce5fc 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt @@ -1041,9 +1041,13 @@ class ColorPickerController( } fun sampleEyedropperAtHover() { - if (!eyedropperActive) return - if (hoverX == Int.MIN_VALUE || hoverY == Int.MIN_VALUE) return - sampleEyedropper(hoverX, hoverY, commit = false) + sampleEyedropperAtHoverAndReportChange() + } + + internal fun sampleEyedropperAtHoverAndReportChange(): Boolean { + if (!eyedropperActive) return false + if (hoverX == Int.MIN_VALUE || hoverY == Int.MIN_VALUE) return false + return sampleEyedropper(hoverX, hoverY, commit = false) } private fun commitInputEdit(key: String) { @@ -1287,8 +1291,8 @@ class ColorPickerController( applyColor(state.color.copy(a = progress), notifyPreview = true, commit = commit) } - private fun sampleEyedropper(x: Int, y: Int, commit: Boolean) { - val argb = screenSampler?.sampleColorAt(x, y) ?: return + private fun sampleEyedropper(x: Int, y: Int, commit: Boolean): Boolean { + val argb = screenSampler?.sampleColorAt(x, y) ?: return false val sampled = RgbaColor.fromArgbInt(argb) val color = if (state.alphaEnabled) { @@ -1296,7 +1300,9 @@ class ColorPickerController( } else { sampled.copy(a = 1f) } + val before = state.color.toArgbInt() applyColor(color, notifyPreview = true, commit = commit) + return state.color.toArgbInt() != before } private fun normalizedEyedropperGridSize(): Int { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngine.kt index 0a04a4e..6331193 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngine.kt @@ -289,9 +289,7 @@ class ColorPickerPopupEngine : ColorPickerPopupPortalService { current.controller.handleMouseMove(mouseX, mouseY, current.layout) } - fun captureEyedropperSample() { - popup?.controller?.sampleEyedropperAtHover() - } + fun captureEyedropperSample(): Boolean = popup?.controller?.sampleEyedropperAtHoverAndReportChange() == true fun hasActiveEyedropper(): Boolean = popup?.controller?.isEyedropperActive() == true diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerOverlayNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerOverlayNode.kt index 4aef90b..36238af 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerOverlayNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerOverlayNode.kt @@ -41,6 +41,10 @@ internal class ColorPickerPopupOverlayNode( domInputRoutingReady = false } + fun invalidateColorState() { + markRenderCommandsDirty() + } + override fun measure(ctx: UiMeasureContext): Size = Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt index 56c254a..84c88c9 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt @@ -815,6 +815,10 @@ internal class SystemColorPickerTransientOverlayNode( modeDropdownOverlayNode.render(ctx, x, y, width, height) eyedropperOverlayNode.render(ctx, x, y, width, height) } + + fun invalidateColorState() { + markRenderCommandsDirty() + } } internal class SystemColorPickerModeDropdownOverlayNode( diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerInlineNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerInlineNode.kt index 83f7dae..a452c98 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerInlineNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerInlineNode.kt @@ -136,7 +136,10 @@ class ColorPickerInlineNode( } fun captureEyedropperSample() { - controller.sampleEyedropperAtHover() + if (controller.sampleEyedropperAtHoverAndReportChange()) { + refreshLayout() + markRenderCommandsDirty() + } } internal override fun measureForLayout(ctx: UiMeasureContext, availableOuterWidth: Int?): Size = diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt index d3d8152..dcb8e70 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt @@ -81,7 +81,10 @@ class SelectNode( if (event.mouseButton != MouseButton.LEFT) return@addEventListener if (!this@SelectNode.containsGlobalPoint(event.mouseX, event.mouseY)) return@addEventListener FocusManager.requestFocus(this@SelectNode) - if (DomainPortalServices.isSelectOpenFor(ownerToken)) { + if ( + DomainPortalServices.isSelectOpenFor(ownerToken) && + !DomainPortalServices.isSelectClosingFor(ownerToken) + ) { DomainPortalServices.closeSelect(ownerToken) } else { openPopup() diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/FocusManager.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/FocusManager.kt index 10d0471..2c15ddc 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/FocusManager.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/FocusManager.kt @@ -11,6 +11,7 @@ object FocusManager { private var focusedKey: Any? = null private var focusedPath: IntArray? = null private var lastRoot: DOMNode? = null + private var preservePointerFocusDepth: Int = 0 /** Current focused node, if any. */ fun focusedNode(): DOMNode? = focused @@ -60,6 +61,7 @@ object FocusManager { /** Updates focus from an event target. */ fun updateFocusFromTarget(target: DOMNode?) { + if (preservePointerFocusDepth > 0) return val focusable = resolveFocusable(target) if (focusable != null) { requestFocus(focusable) @@ -68,6 +70,15 @@ object FocusManager { } } + internal inline fun preservePointerFocus(block: () -> T): T { + preservePointerFocusDepth += 1 + return try { + block() + } finally { + preservePointerFocusDepth -= 1 + } + } + /** * Retains focus across rebuilds using node key or path. */ diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/DomainPortalServices.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/DomainPortalServices.kt index 9a8074d..46ec512 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/DomainPortalServices.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/DomainPortalServices.kt @@ -34,6 +34,9 @@ object DomainPortalServices { systemSelectEngine.closeAll() } + fun isSelectClosingFor(owner: Any): Boolean = + applicationSelectEngine.isClosingFor(owner) || systemSelectEngine.isClosingFor(owner) + fun isSelectOpenFor(owner: Any): Boolean = applicationSelectEngine.isOpenFor(owner) || systemSelectEngine.isOpenFor(owner) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt index 61458da..7288c53 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt @@ -601,7 +601,10 @@ class SystemOverlayHost( ?.snapshot() fun captureEyedropperSample() { - popupMount.popupEngine.captureEyedropperSample() + if (popupMount.popupEngine.captureEyedropperSample()) { + popupMount.node.invalidateColorState() + popupMount.transientNode.invalidateColorState() + } } fun debugOwnerScope(): OverlayOwnerScope? = popupMount.popupEngine.debugOwnerScope(popupMount.ownerToken) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt index 67daf82..159f00b 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt @@ -32,6 +32,7 @@ class SelectEngine( var scrollOffset: Int = 0, var scrollbarDragging: Boolean = false, var scrollbarDragThumbOffsetY: Int = 0, + var pressedOptionIndex: Int = -1, ) data class Snapshot( @@ -149,6 +150,11 @@ class SelectEngine( return current.owner == owner && visibilityState != VisibilityState.Hidden } + internal fun isClosingFor(owner: Any): Boolean { + val current = popup ?: return false + return current.owner == owner && visibilityState == VisibilityState.Closing + } + fun isOpen(): Boolean = popup != null && visibilityState != VisibilityState.Hidden fun snapshot(): Snapshot { @@ -175,6 +181,29 @@ class SelectEngine( return current.anchorRect } + internal fun shouldCaptureScrollbarDrag(mouseX: Int, mouseY: Int): Boolean { + val current = popup ?: return false + if (visibilityState == VisibilityState.Hidden) return false + ensureLayout() + return scrollbarTrackRect(current)?.contains(mouseX, mouseY) == true + } + + internal fun isScrollbarDragging(): Boolean = popup?.scrollbarDragging == true + + internal fun cancelScrollbarDrag() { + popup?.let { + it.scrollbarDragging = false + it.pressedOptionIndex = -1 + } + } + + internal fun debugScrollbarTrackRect(owner: Any): Rect? { + val current = popup ?: return null + if (current.owner != owner) return null + ensureLayout() + return scrollbarTrackRect(current) + } + fun onFrame( measureContext: UiMeasureContext, viewportWidth: Int, @@ -391,35 +420,63 @@ class SelectEngine( return true } - fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { + fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + handleMouseDown(mouseX, mouseY, button, closeOutside = true) + + internal fun handlePortalMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + handleMouseDown(mouseX, mouseY, button, closeOutside = false) + + private fun handleMouseDown( + mouseX: Int, + mouseY: Int, + button: MouseButton, + closeOutside: Boolean, + ): Boolean { val current = popup ?: return false if (visibilityState == VisibilityState.Hidden) return false ensureLayout() - if (!current.panelRect.contains(mouseX, mouseY)) { - startVisibilityTransition(0f, style.closeDurationMs) - return true - } - if (button != MouseButton.LEFT && button != MouseButton.RIGHT) { - return true - } - if (button == MouseButton.LEFT && startScrollbarDragIfNeeded(current, mouseX, mouseY)) { - return true + current.pressedOptionIndex = -1 + return when { + !current.panelRect.contains(mouseX, mouseY) -> { + if (closeOutside) { + startVisibilityTransition(0f, style.closeDurationMs) + } + true + } + + button != MouseButton.LEFT && button != MouseButton.RIGHT -> true + button == MouseButton.LEFT && startScrollbarDragIfNeeded(current, mouseX, mouseY) -> true + else -> { + val entryIndex = entryAt(current, mouseX, mouseY) + current.pressedOptionIndex = entryIndex + true + } } - val entryIndex = entryAt(current, mouseX, mouseY) - if (entryIndex < 0) return true - activate(current, entryIndex) - return true } - fun handleMouseUp(_mouseX: Int, _mouseY: Int, button: MouseButton): Boolean { + fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { val current = popup ?: return false if (visibilityState == VisibilityState.Hidden) return false if (button == MouseButton.LEFT && current.scrollbarDragging) { current.scrollbarDragging = false + current.pressedOptionIndex = -1 return true } return when (button) { - MouseButton.LEFT, MouseButton.RIGHT, MouseButton.MIDDLE -> true + MouseButton.LEFT, MouseButton.RIGHT -> { + val pressedOptionIndex = current.pressedOptionIndex + val releaseIndex = entryAt(current, mouseX, mouseY) + current.pressedOptionIndex = -1 + if (pressedOptionIndex >= 0 && pressedOptionIndex == releaseIndex) { + activate(current, pressedOptionIndex) + } + true + } + + MouseButton.MIDDLE -> { + current.pressedOptionIndex = -1 + true + } } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt index 5e6672a..43e4c3f 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt @@ -2,8 +2,14 @@ package org.dreamfinity.dsgl.core.select import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.dom.layout.Size import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.event.MouseDownEvent +import org.dreamfinity.dsgl.core.event.MouseMoveEvent +import org.dreamfinity.dsgl.core.event.MouseUpEvent +import org.dreamfinity.dsgl.core.event.MouseWheelEvent import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.overlay.PortalDismissPolicy import org.dreamfinity.dsgl.core.overlay.PortalEntry @@ -15,8 +21,13 @@ import org.dreamfinity.dsgl.core.overlay.PortalEntryState import org.dreamfinity.dsgl.core.overlay.PortalFocusPolicy import org.dreamfinity.dsgl.core.overlay.PortalHost import org.dreamfinity.dsgl.core.overlay.PortalInputPolicy +import org.dreamfinity.dsgl.core.overlay.PortalInsidePointerPolicy import org.dreamfinity.dsgl.core.overlay.PortalPointerDispatch +import org.dreamfinity.dsgl.core.overlay.PortalPointerPolicyResult import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces +import org.dreamfinity.dsgl.core.overlay.evaluateOutsidePointerDown +import org.dreamfinity.dsgl.core.overlay.handleOutsidePointerDownPolicy +import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand internal class SelectPortalController( @@ -44,6 +55,7 @@ internal class SelectPortalController( viewportScale: Float, ) { entry.onFrame(measureContext, viewportWidth, viewportHeight, viewportScale) + portalHost.render(measureContext, viewportWidth, viewportHeight) } fun appendCommands( @@ -52,8 +64,8 @@ internal class SelectPortalController( viewportHeight: Int, out: MutableList, ) { - entry.updatePaintContext(measureContext, viewportWidth, viewportHeight) - entry.syncActivePlacement() + entry.onFrame(measureContext, viewportWidth, viewportHeight, viewportScale = 1f) + portalHost.render(measureContext, viewportWidth, viewportHeight) out += portalHost.paint(measureContext) } @@ -67,7 +79,8 @@ internal class SelectPortalController( portalHost.dispatchInput { it.handleMouseMove(mouseX, mouseY) } override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = - portalHost.dispatchInput { it.handleMouseDown(mouseX, mouseY, button) } + portalHost.dispatchInput { it.handleMouseDown(mouseX, mouseY, button) } || + portalHost.handleOutsidePointerDownPolicy(mouseX, mouseY) override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = portalHost.dispatchInput { it.handleMouseUp(mouseX, mouseY, button) } @@ -77,8 +90,21 @@ internal class SelectPortalController( fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = portalHost.dispatchInput { it.handleKeyDown(keyCode, keyChar) } + + internal fun debugPortalState(mouseX: Int, mouseY: Int): SelectPortalDebugState = + SelectPortalDebugState( + node = entry.node, + state = entry.state, + outsidePointerPolicy = portalHost.evaluateOutsidePointerDown(mouseX, mouseY), + ) } +internal data class SelectPortalDebugState( + val node: DOMNode, + val state: PortalEntryState, + val outsidePointerPolicy: PortalPointerPolicyResult?, +) + private class SelectPortalEntry( private val engine: SelectEngine, ownerScope: OverlayOwnerScope, @@ -91,13 +117,35 @@ private class SelectPortalEntry( surface = ScreenDomainSurfaces.portalSurfaceForOwner(ownerScope), order = PortalEntryOrder(zIndex = 0), dismissPolicy = PortalDismissPolicy.EscapeOrOutsidePointerDown, - inputPolicy = PortalInputPolicy.ManualOnly, + inputPolicy = PortalInputPolicy.DomOnly, focusPolicy = PortalFocusPolicy.Preserve, + insidePointerPolicy = PortalInsidePointerPolicy.ConsumePointerDown, + ).apply { + dismissAction = { + val owner = engine.snapshot().owner + if (owner != null) { + engine.close(owner) + } else { + engine.closeAll() + } + syncActivePlacement() + } + } + private val popupNode: SelectPortalNode = + SelectPortalNode( + engine = engine, + onInputHandled = { + if (!clearingDomInputRouter) { + syncActivePlacement() + } + }, ) - override val node: DOMNode? = null + override val node: DOMNode = popupNode + private val domInputRouter: LayerDomInputRouter = LayerDomInputRouter { node } private var viewportWidth: Int = 1 private var viewportHeight: Int = 1 private var measureContext: UiMeasureContext? = null + private var clearingDomInputRouter: Boolean = false fun onFrame( measureContext: UiMeasureContext, @@ -105,7 +153,11 @@ private class SelectPortalEntry( viewportHeight: Int, viewportScale: Float, ) { - updatePaintContext(measureContext, viewportWidth, viewportHeight) + this.measureContext = measureContext + this.viewportWidth = viewportWidth.coerceAtLeast(1) + this.viewportHeight = viewportHeight.coerceAtLeast(1) + popupNode.viewportWidth = this.viewportWidth + popupNode.viewportHeight = this.viewportHeight engine.onFrame( measureContext = measureContext, viewportWidth = this.viewportWidth, @@ -115,56 +167,66 @@ private class SelectPortalEntry( syncActivePlacement() } - fun updatePaintContext(measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int) { - this.measureContext = measureContext - this.viewportWidth = viewportWidth.coerceAtLeast(1) - this.viewportHeight = viewportHeight.coerceAtLeast(1) - } - override fun paint(ctx: UiMeasureContext): List { if (!engine.isOpen()) { state.deactivate() return emptyList() } val commands = ArrayList() - engine.appendOverlayCommands( - measureContext = measureContext ?: ctx, - viewportWidth = viewportWidth, - viewportHeight = viewportHeight, - out = commands, - ) + node.appendRenderCommands(measureContext ?: ctx, commands) syncActivePlacement() return commands } override fun close() { + clearingDomInputRouter = true + try { + domInputRouter.clear() + } finally { + clearingDomInputRouter = false + } engine.closeAll() state.deactivate() } override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = - engine.handleMouseMove(mouseX, mouseY).also { syncActivePlacement() } + dispatchWithSyncedPlacement { + domInputRouter.handleMouseMove(mouseX, mouseY) + } override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = - engine.handleMouseDown(mouseX, mouseY, button).also { syncActivePlacement() } + dispatchWithSyncedPlacement { + domInputRouter.handleMouseDown(mouseX, mouseY, button) + } override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = - engine.handleMouseUp(mouseX, mouseY, button).also { syncActivePlacement() } + dispatchWithSyncedPlacement { + domInputRouter.handleMouseUp(mouseX, mouseY, button) + } override fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = - engine.handleMouseWheel(mouseX, mouseY, delta).also { syncActivePlacement() } + dispatchWithSyncedPlacement { + domInputRouter.handleMouseWheel(mouseX, mouseY, delta) + } override fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = engine.handleKeyDown(keyCode, keyChar).also { syncActivePlacement() } fun syncActivePlacement() { if (!engine.isOpen()) { + clearingDomInputRouter = true + try { + domInputRouter.clear() + } finally { + clearingDomInputRouter = false + } state.deactivate() return } val owner = engine.snapshot().owner ?: return val panelRect = engine.debugPanelRect(owner) ?: return val anchorRect = engine.debugAnchorRect(owner) + popupNode.bounds = panelRect state.activate( PortalEntryPlacement( anchorBounds = anchorRect, @@ -176,4 +238,100 @@ private class SelectPortalEntry( ), ) } + + private fun dispatchWithSyncedPlacement(dispatch: () -> Boolean): Boolean { + syncActivePlacement() + val handled = + FocusManager.preservePointerFocus { + dispatch() + } + syncActivePlacement() + return handled + } +} + +private class SelectPortalNode( + private val engine: SelectEngine, + private val onInputHandled: () -> Unit, +) : DOMNode(key = "dsgl-select-portal-popup") { + override val styleType: String = "select-portal" + var viewportWidth: Int = 1 + var viewportHeight: Int = 1 + + init { + onMouseMove = ::handleMove + onMouseDown = ::handleDown + onMouseUp = ::handleUp + onMouseWheel = ::handleWheel + } + + override fun measure(ctx: UiMeasureContext): Size { + val owner = engine.snapshot().owner + val panel = owner?.let(engine::debugPanelRect) + return Size(panel?.width ?: 0, panel?.height ?: 0) + } + + override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) { + if (!engine.isOpen()) return + engine.appendOverlayCommands( + measureContext = ctx, + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + out = out, + ) + } + + override fun shouldCapturePointerDrag(mouseX: Int, mouseY: Int): Boolean = + engine.isScrollbarDragging() || engine.shouldCaptureScrollbarDrag(mouseX, mouseY) + + override fun continuePointerCapture( + mouseX: Int, + mouseY: Int, + mouseDX: Int, + mouseDY: Int, + button: MouseButton, + ) { + if (engine.handleMouseMove(mouseX, mouseY)) { + onInputHandled() + } + } + + override fun endPointerCapture(mouseX: Int, mouseY: Int, button: MouseButton) { + if (engine.handleMouseUp(mouseX, mouseY, button)) { + onInputHandled() + } + } + + override fun cancelPointerCapture() { + engine.cancelScrollbarDrag() + onInputHandled() + } + + private fun handleMove(event: MouseMoveEvent) { + if (engine.handleMouseMove(event.mouseX, event.mouseY)) { + event.cancelled = true + onInputHandled() + } + } + + private fun handleDown(event: MouseDownEvent) { + if (engine.handlePortalMouseDown(event.mouseX, event.mouseY, event.mouseButton)) { + event.cancelled = true + onInputHandled() + } + } + + private fun handleUp(event: MouseUpEvent) { + if (engine.handleMouseUp(event.mouseX, event.mouseY, event.mouseButton)) { + event.cancelled = true + onInputHandled() + } + } + + private fun handleWheel(event: MouseWheelEvent) { + if (engine.handleMouseWheel(event.mouseX, event.mouseY, event.dWheel)) { + event.cancelled = true + onInputHandled() + } + } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInlineNodeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInlineNodeTests.kt index 3fe7ce0..67e2b37 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInlineNodeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInlineNodeTests.kt @@ -12,6 +12,7 @@ import org.dreamfinity.dsgl.core.event.MouseUpEvent import org.dreamfinity.dsgl.core.render.RenderCommand import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNotEquals import kotlin.test.assertTrue class ColorPickerInlineNodeTests { @@ -151,8 +152,11 @@ class ColorPickerInlineNodeTests { @Test fun `inline eyedropper samples color on mouse move even outside picker bounds`() { - val sampledArgb = 0xFF25AEEF.toInt() - ScreenColorSamplerBridge.install(ScreenColorSampler { _, _ -> sampledArgb }) + ScreenColorSamplerBridge.install( + ScreenColorSampler { x, y -> + (0xFF shl 24) or ((x and 0xFF) shl 16) or ((y and 0xFF) shl 8) or 0x44 + }, + ) try { var current = RgbaColor.WHITE val picker = @@ -188,7 +192,21 @@ class ColorPickerInlineNodeTests { ) picker.captureEyedropperSample() - assertEquals(sampledArgb, current.toArgbInt()) + val first = current.toArgbInt() + assertEquals((0xFF shl 24) or (0xB0 shl 16) or (0x84 shl 8) or 0x44, first) + + EventBus.post( + MouseMoveEvent( + mouseX = 1234, + mouseY = 912, + prevX = 1200, + prevY = 900, + ).also { it.target = picker }, + ) + picker.captureEyedropperSample() + + assertEquals((0xFF shl 24) or (0xD2 shl 16) or (0x90 shl 8) or 0x44, current.toArgbInt()) + assertNotEquals(first, current.toArgbInt()) } finally { ScreenColorSamplerBridge.install(null) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerScopeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerScopeTests.kt index 6fb2a59..6667120 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerScopeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerScopeTests.kt @@ -5,11 +5,16 @@ import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.elements.SelectNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.overlay.ApplicationOverlayHost import org.dreamfinity.dsgl.core.overlay.DomainPortalServices import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope +import org.dreamfinity.dsgl.core.overlay.handlePortalPointerAfterDom import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter +import org.dreamfinity.dsgl.core.overlay.syncPortalFrame import org.dreamfinity.dsgl.core.render.RenderCommand +import org.dreamfinity.dsgl.core.select.SelectStyle import org.dreamfinity.dsgl.core.select.selectModel import kotlin.test.AfterTest import kotlin.test.Test @@ -29,6 +34,9 @@ class SelectNodeOwnerScopeTests { @AfterTest fun cleanup() { DomainPortalServices.closeAllSelects() + DomainPortalServices.applicationSelectEngine.setStyle(SelectStyle()) + DomainPortalServices.systemSelectEngine.setStyle(SelectStyle()) + FocusManager.clearFocus() } @Test @@ -65,4 +73,113 @@ class SelectNodeOwnerScopeTests { assertFalse(DomainPortalServices.applicationSelectEngine.isOpenFor(ownerKey)) assertTrue(DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey)) } + + @Test + fun `anchor press reopens select while previous close animation is still active`() { + val root = ContainerNode(key = "root") + root.bounds = Rect(0, 0, 300, 200) + val ownerKey = "application-select-reopen-owner" + DomainPortalServices.applicationSelectEngine.setStyle( + SelectStyle( + openDurationMs = 1L, + closeDurationMs = 200L, + ), + ) + val select = + SelectNode( + model = + selectModel(id = "application.select.reopen.model") { + option("a", "Alpha") + option("b", "Beta") + }, + ownerScope = OverlayOwnerScope.Application, + key = ownerKey, + ).apply { + width = 120 + height = 20 + bounds = Rect(20, 20, 120, 20) + } + select.applyParent(root) + + val tree = DomTree(root) + tree.render(ctx, 300, 200) + tree.paint(ctx) + val router = LayerDomInputRouter { root } + val clickX = select.bounds.x + (select.bounds.width / 2).coerceAtLeast(1) + val clickY = select.bounds.y + (select.bounds.height / 2).coerceAtLeast(1) + + assertTrue(router.handleMouseDown(clickX, clickY, MouseButton.LEFT)) + assertTrue(DomainPortalServices.applicationSelectEngine.isOpenFor(ownerKey)) + Thread.sleep(3L) + DomainPortalServices.applicationSelectEngine.onFrame(ctx, 300, 200, 1f) + DomainPortalServices.closeSelect(ownerKey) + assertTrue(DomainPortalServices.isSelectClosingFor(ownerKey)) + + assertTrue(router.handleMouseDown(clickX, clickY, MouseButton.LEFT)) + + assertTrue(DomainPortalServices.applicationSelectEngine.isOpenFor(ownerKey)) + assertFalse(DomainPortalServices.isSelectClosingFor(ownerKey)) + } + + @Test + fun `application select portal pointer press preserves focused anchor`() { + val root = ContainerNode(key = "root") + root.bounds = Rect(0, 0, 300, 160) + val ownerKey = "application-select-focus-preserve-owner" + DomainPortalServices.applicationSelectEngine.setStyle( + SelectStyle( + openDurationMs = 1L, + closeDurationMs = 1L, + maxPanelHeightPadding = 10, + ), + ) + val select = + SelectNode( + model = + selectModel(id = "application.select.focus.preserve.model") { + repeat(48) { index -> + option("id-$index", "Option $index") + } + }, + ownerScope = OverlayOwnerScope.Application, + key = ownerKey, + ).apply { + width = 120 + height = 20 + bounds = Rect(20, 20, 120, 20) + } + select.applyParent(root) + + val tree = DomTree(root) + tree.render(ctx, 300, 160) + tree.paint(ctx) + val router = LayerDomInputRouter { root } + val applicationOverlayHost = ApplicationOverlayHost() + applicationOverlayHost.onInputFrame(300, 160) + val clickX = select.bounds.x + (select.bounds.width / 2).coerceAtLeast(1) + val clickY = select.bounds.y + (select.bounds.height / 2).coerceAtLeast(1) + + assertTrue(router.handleMouseDown(clickX, clickY, MouseButton.LEFT)) + assertTrue(FocusManager.isFocused(select)) + assertTrue(DomainPortalServices.applicationSelectEngine.isOpenFor(ownerKey)) + + applicationOverlayHost.syncPortalFrame(ctx, 300, 160, 1f, 0, 0) + val track = requireNotNull(DomainPortalServices.applicationSelectEngine.debugScrollbarTrackRect(ownerKey)) + val downX = track.x + track.width / 2 + val downY = track.y + 2 + + assertTrue( + applicationOverlayHost.handlePortalPointerAfterDom( + mouseX = downX, + mouseY = downY, + dWheel = 0, + button = MouseButton.LEFT, + pressed = true, + ), + ) + + assertTrue(FocusManager.isFocused(select)) + assertTrue(DomainPortalServices.applicationSelectEngine.isOpenFor(ownerKey)) + assertTrue(DomainPortalServices.applicationSelectEngine.isScrollbarDragging()) + } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt index 621d17b..da90c94 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt @@ -429,6 +429,16 @@ class LiveLayerInteractionPathTests { assertTrue(commands.isNotEmpty()) assertTrue(consumed) + assertEquals(null, selected) + assertTrue( + applicationOverlayHost.handlePortalPointerAfterDom( + mouseX = panel.x + style.panelPaddingX + 1, + mouseY = panel.y + style.panelPaddingY + 1, + dWheel = 0, + button = MouseButton.LEFT, + pressed = false, + ), + ) assertEquals("a", selected) } @@ -590,9 +600,21 @@ class LiveLayerInteractionPathTests { assertTrue(applicationOverlayHost.hasActiveColorPickerEyedropper()) assertTrue(applicationOverlayHost.handlePortalPointerBeforeDom(25, 52, 0, null, false)) applicationOverlayHost.captureColorPickerEyedropperSample() - assertTrue(applicationOverlayHost.handlePortalPointerBeforeDom(25, 52, 0, MouseButton.LEFT, true)) - assertTrue(applicationOverlayHost.handlePortalPointerBeforeDom(25, 52, 0, MouseButton.LEFT, false)) - val expected = RgbaColor.fromArgbInt((0xFF shl 24) or (25 shl 16) or (52 shl 8) or 0x44) + val firstExpected = RgbaColor.fromArgbInt((0xFF shl 24) or (25 shl 16) or (52 shl 8) or 0x44) + assertEquals( + firstExpected.toArgbInt(), + DomainPortalServices.applicationColorPickerEngine + .debugController(owner) + ?.snapshot() + ?.color + ?.toArgbInt(), + ) + + assertTrue(applicationOverlayHost.handlePortalPointerBeforeDom(31, 64, 0, null, false)) + applicationOverlayHost.captureColorPickerEyedropperSample() + assertTrue(applicationOverlayHost.handlePortalPointerBeforeDom(31, 64, 0, MouseButton.LEFT, true)) + assertTrue(applicationOverlayHost.handlePortalPointerBeforeDom(31, 64, 0, MouseButton.LEFT, false)) + val expected = RgbaColor.fromArgbInt((0xFF shl 24) or (31 shl 16) or (64 shl 8) or 0x44) assertEquals(expected.toArgbInt(), committed?.toArgbInt()) val closeRect = DomainPortalServices.applicationColorPickerEngine.debugCloseRect(owner) ?: error("close missing") @@ -664,6 +686,14 @@ class LiveLayerInteractionPathTests { assertTrue(commands.isNotEmpty()) assertEquals(ScreenDomainSurfaces.SystemPortal, consumedBy) assertFalse(appRootReceived) + assertEquals(null, selected) + assertTrue( + systemHost.handlePortalMouseUp( + panel.x + style.panelPaddingX + 1, + panel.y + style.panelPaddingY + 1, + MouseButton.LEFT, + ), + ) assertEquals("a", selected) assertFalse(DomainPortalServices.applicationSelectEngine.isOpenFor(owner)) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt index 573563d..095e8e5 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt @@ -4,8 +4,11 @@ import org.dreamfinity.dsgl.core.colorpicker.ColorFormatMode import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupRequest import org.dreamfinity.dsgl.core.colorpicker.ColorPickerState import org.dreamfinity.dsgl.core.colorpicker.ColorPickerStyle +import org.dreamfinity.dsgl.core.colorpicker.ColorTextCodec import org.dreamfinity.dsgl.core.colorpicker.RgbChannelOrder import org.dreamfinity.dsgl.core.colorpicker.RgbaColor +import org.dreamfinity.dsgl.core.colorpicker.ScreenColorSampler +import org.dreamfinity.dsgl.core.colorpicker.ScreenColorSamplerBridge import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode @@ -983,6 +986,87 @@ class SystemOverlayColorPickerEntryTests { ) } + @Test + fun `system picker pipette capture invalidates rendered sampled value`() { + ScreenColorSamplerBridge.install( + ScreenColorSampler { x, y -> + (0xFF shl 24) or ((x and 0xFF) shl 16) or ((y and 0xFF) shl 8) or 0x44 + }, + ) + try { + val host = SystemOverlayHost(InspectorController()) + val pickerService = host.systemInspectorColorPickerService() + val root = inspectedRoot() + + pickerService.open(anchorRect = Rect(140, 140, 20, 18), title = "Popup", state = popupState()) + host.onInputFrame(1200, 800) + host.syncFrame( + root, + inspectedLayoutRevision = 1L, + cursorX = 146, + cursorY = 146, + inspectorPointerCaptured = false, + ) + + val layout = host.debugSystemColorPickerBodyLayout() ?: error("layout missing") + val pipette = layout.pipetteRect + assertTrue(host.handleMouseDown(pipette.x + 2, pipette.y + 2, MouseButton.LEFT)) + + val firstX = pipette.x + 32 + val firstY = pipette.y + 28 + host.handleMouseMove(firstX, firstY) + host.syncFrame( + root, + inspectedLayoutRevision = 2L, + cursorX = firstX, + cursorY = firstY, + inspectorPointerCaptured = false, + ) + host.render(ctx, 1200, 800) + host.paint(ctx) + host.captureSystemColorPickerEyedropperSample() + host.render(ctx, 1200, 800) + val firstCommands = host.paint(ctx) + val firstColor = sampledColor(firstX, firstY) + assertEquals( + firstColor.toArgbInt(), + host + .debugSystemColorPickerState() + ?.color + ?.toArgbInt(), + ) + assertTrue(firstCommands.containsRenderedColorText(firstColor)) + + val secondX = pipette.x + 54 + val secondY = pipette.y + 43 + host.handleMouseMove(secondX, secondY) + host.syncFrame( + root, + inspectedLayoutRevision = 3L, + cursorX = secondX, + cursorY = secondY, + inspectorPointerCaptured = false, + ) + host.render(ctx, 1200, 800) + host.paint(ctx) + host.captureSystemColorPickerEyedropperSample() + host.render(ctx, 1200, 800) + val secondCommands = host.paint(ctx) + val secondColor = sampledColor(secondX, secondY) + assertEquals( + secondColor.toArgbInt(), + host + .debugSystemColorPickerState() + ?.color + ?.toArgbInt(), + ) + assertTrue(secondCommands.containsRenderedColorText(secondColor)) + assertNotEquals(firstColor.toArgbInt(), secondColor.toArgbInt()) + } finally { + ScreenColorSamplerBridge.install(null) + } + } + private fun collectStyleTypes(root: DOMNode): Set { val out = LinkedHashSet() @@ -1003,6 +1087,19 @@ class SystemOverlayColorPickerEntryTests { closeOnSelect = false, ) + private fun sampledColor(x: Int, y: Int): RgbaColor = RgbaColor.fromArgbInt((0xFF shl 24) or ((x and 0xFF) shl 16) or ((y and 0xFF) shl 8) or 0x44) + + private fun List.containsRenderedColorText(color: RgbaColor): Boolean { + val expected = + ColorTextCodec.format( + color, + ColorFormatMode.RGB, + includeAlpha = true, + rgbOrder = RgbChannelOrder.RGBA, + ) + return filterIsInstance().any { command -> command.text == expected } + } + private fun inspectedRoot(): ContainerNode { val root = ContainerNode(key = "root") root.bounds = Rect(0, 0, 1200, 800) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectEngineTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectEngineTests.kt index da9dc66..3677ce8 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectEngineTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectEngineTests.kt @@ -124,6 +124,50 @@ class SelectEngineTests { assertFalse(engine.isOpen()) } + @Test + fun `opening same owner while closing reverses close transition`() { + val clock = FakeClock() + val owner = "select.reopen" + val engine = SelectEngine(clock = clock) + engine.setStyle(SelectStyle(openDurationMs = 20L, closeDurationMs = 200L)) + val model = + selectModel { + option("a", "A") + option("b", "B") + } + val request = + SelectOpenRequest( + owner = owner, + modelToken = model.token, + entries = model.entries, + selectedId = "a", + anchorRect = Rect(30, 30, 90, 18), + closeOnSelect = true, + ) + + engine.open(request) + clock.advance(25L) + engine.onFrame(ctx, 320, 180, 1f) + val panel = requireNotNull(engine.debugPanelRect(owner)) + val style = engine.currentStyle() + val optionX = panel.x + style.panelPaddingX + style.rowPaddingX + 1 + val optionY = panel.y + style.panelPaddingY + 1 + + assertTrue(engine.handleMouseDown(optionX, optionY, MouseButton.LEFT)) + assertFalse(engine.isClosingFor(owner)) + assertTrue(engine.handleMouseUp(optionX, optionY, MouseButton.LEFT)) + assertTrue(engine.isOpen()) + assertTrue(engine.isClosingFor(owner)) + + engine.open(request) + assertTrue(engine.isOpen()) + assertFalse(engine.isClosingFor(owner)) + clock.advance(25L) + engine.onFrame(ctx, 320, 180, 1f) + + assertTrue(engine.isOpenFor(owner)) + } + @Test fun `keyboard navigation skips disabled options and enter selects`() { val clock = FakeClock() diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalControllerTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalControllerTests.kt new file mode 100644 index 0000000..a398a36 --- /dev/null +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalControllerTests.kt @@ -0,0 +1,283 @@ +package org.dreamfinity.dsgl.core.select + +import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope +import org.dreamfinity.dsgl.core.overlay.PortalDismissPolicy +import org.dreamfinity.dsgl.core.overlay.PortalInputPolicy +import org.dreamfinity.dsgl.core.overlay.PortalPointerRegion +import org.dreamfinity.dsgl.core.render.RenderCommand +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class SelectPortalControllerTests { + private class FakeClock( + var now: Long = 0L, + ) : SelectClock { + override fun nowMs(): Long = now + + fun advance(ms: Long) { + now += ms + } + } + + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * 6 + + override fun fontHeight(fontId: String?, fontSize: Int?): Int = 9 + + override fun paint(commands: List) = Unit + } + + @Test + fun `select portal entry exposes a DOM node with generic portal policies`() { + val fixture = openSelect() + val commands = ArrayList() + + fixture.controller.appendCommands(ctx, VIEWPORT_WIDTH, VIEWPORT_HEIGHT, commands) + + val panel = fixture.engine.debugPanelRect(fixture.owner) + val debug = fixture.controller.debugPortalState(mouseX = 2, mouseY = 2) + assertNotNull(panel) + assertEquals( + panel, + debug.node.bounds, + ) + assertEquals( + PortalInputPolicy.DomOnly, + debug.state.inputPolicy, + ) + assertEquals( + PortalDismissPolicy.EscapeOrOutsidePointerDown, + debug.state.dismissPolicy, + ) + assertTrue(debug.state.active) + assertTrue(commands.isNotEmpty()) + } + + @Test + fun `inside option release is handled through the portal DOM node`() { + val fixture = openSelect() + val panel = requireNotNull(fixture.engine.debugPanelRect(fixture.owner)) + val style = fixture.engine.currentStyle() + val optionX = panel.x + style.panelPaddingX + style.rowPaddingX + 1 + val optionY = panel.y + style.panelPaddingY + 1 + + assertTrue(fixture.controller.handleMouseDown(optionX, optionY, MouseButton.LEFT)) + assertEquals(null, fixture.selected) + assertTrue(fixture.controller.handleMouseUp(optionX, optionY, MouseButton.LEFT)) + + assertEquals("a", fixture.selected) + } + + @Test + fun `option press released outside does not select`() { + val fixture = openSelect() + val panel = requireNotNull(fixture.engine.debugPanelRect(fixture.owner)) + val style = fixture.engine.currentStyle() + val optionX = panel.x + style.panelPaddingX + style.rowPaddingX + 1 + val optionY = panel.y + style.panelPaddingY + 1 + val outsideY = panel.y + panel.height + 8 + + assertTrue(fixture.controller.handleMouseDown(optionX, optionY, MouseButton.LEFT)) + fixture.controller.handleMouseMove(optionX, outsideY) + assertTrue(fixture.controller.handleMouseUp(optionX, outsideY, MouseButton.LEFT)) + + assertEquals(null, fixture.selected) + assertTrue(fixture.engine.isOpen()) + } + + @Test + fun `outside press uses generic portal policy and closes without manual outside routing`() { + val fixture = openSelect() + val outsideX = 2 + val outsideY = 2 + + val policy = + fixture.controller + .debugPortalState(outsideX, outsideY) + .outsidePointerPolicy + assertNotNull(policy) + assertEquals(PortalPointerRegion.OutsideEntry, policy.region) + assertTrue(policy.shouldClose) + assertTrue(policy.consumed) + assertTrue(fixture.controller.handleMouseDown(outsideX, outsideY, MouseButton.LEFT)) + + fixture.clock.advance(CLOSE_DURATION_MS + 1) + fixture.controller.onFrame(ctx, VIEWPORT_WIDTH, VIEWPORT_HEIGHT, 1f) + + assertFalse(fixture.engine.isOpen()) + } + + @Test + fun `wheel inside select portal DOM updates scroll offset`() { + val fixture = openSelect(optionCount = 48) + val panel = requireNotNull(fixture.engine.debugPanelRect(fixture.owner)) + val style = fixture.engine.currentStyle() + val wheelX = panel.x + style.panelPaddingX + style.rowPaddingX + 1 + val wheelY = panel.y + style.panelPaddingY + 1 + val before = + fixture.engine + .snapshot() + .scrollOffset + + assertTrue(fixture.controller.handleMouseWheel(wheelX, wheelY, delta = -120)) + + assertTrue( + fixture.engine + .snapshot() + .scrollOffset > before, + ) + } + + @Test + fun `scrollbar drag is handled inside select portal DOM without outside dismiss`() { + val fixture = openSelect(optionCount = 48) + val track = requireNotNull(fixture.engine.debugScrollbarTrackRect(fixture.owner)) + val downX = track.x + track.width / 2 + val downY = track.y + track.height / 2 + val before = + fixture.engine + .snapshot() + .scrollOffset + + val policy = + fixture.controller + .debugPortalState(downX, downY) + .outsidePointerPolicy + assertNotNull(policy) + assertEquals(PortalPointerRegion.InsideEntry, policy.region) + assertFalse(policy.shouldClose) + assertTrue(policy.consumed) + assertTrue(fixture.controller.handleMouseDown(downX, downY, MouseButton.LEFT)) + assertTrue(fixture.engine.isOpen()) + + assertTrue(fixture.controller.handleMouseMove(downX, downY + 24)) + assertTrue( + fixture.engine + .snapshot() + .scrollOffset > before, + ) + assertTrue(fixture.controller.handleMouseUp(downX, downY + 24, MouseButton.LEFT)) + assertTrue(fixture.engine.isOpen()) + } + + @Test + fun `scrollbar drag remains captured when pointer leaves select portal bounds`() { + val fixture = openSelect(optionCount = 48) + val panel = requireNotNull(fixture.engine.debugPanelRect(fixture.owner)) + val track = requireNotNull(fixture.engine.debugScrollbarTrackRect(fixture.owner)) + val downX = track.x + track.width / 2 + val downY = track.y + 2 + val outsideY = panel.y + panel.height + 24 + val before = + fixture.engine + .snapshot() + .scrollOffset + + assertTrue(fixture.controller.handleMouseDown(downX, downY, MouseButton.LEFT)) + assertTrue(fixture.engine.isOpen()) + assertTrue(fixture.engine.isScrollbarDragging()) + + assertTrue(fixture.controller.handleMouseMove(downX, outsideY)) + assertTrue( + fixture.engine + .snapshot() + .scrollOffset > before, + ) + assertTrue(fixture.engine.isOpen()) + + assertTrue(fixture.controller.handleMouseUp(downX, outsideY, MouseButton.LEFT)) + assertTrue(fixture.engine.isOpen()) + assertFalse(fixture.engine.isScrollbarDragging()) + } + + @Test + fun `deactivating while scrollbar drag is captured does not recurse`() { + val fixture = openSelect(optionCount = 48) + val track = requireNotNull(fixture.engine.debugScrollbarTrackRect(fixture.owner)) + val downX = track.x + track.width / 2 + val downY = track.y + track.height / 2 + + assertTrue(fixture.controller.handleMouseDown(downX, downY, MouseButton.LEFT)) + assertTrue(fixture.engine.isScrollbarDragging()) + + fixture.engine.closeAll() + fixture.controller.onFrame(ctx, VIEWPORT_WIDTH, VIEWPORT_HEIGHT, 1f) + + assertFalse(fixture.engine.isOpen()) + assertFalse( + fixture.controller + .debugPortalState(downX, downY) + .state + .active, + ) + } + + private fun openSelect(optionCount: Int = 2): SelectPortalFixture { + val clock = FakeClock() + val engine = SelectEngine(clock = clock) + engine.setStyle( + SelectStyle( + openDurationMs = OPEN_DURATION_MS, + closeDurationMs = CLOSE_DURATION_MS, + maxPanelHeightPadding = 10, + ), + ) + val controller = + SelectPortalController( + engine = engine, + ownerScope = OverlayOwnerScope.Application, + entryId = "test.select", + ) + val model = + selectModel { + repeat(optionCount) { index -> + option(('a'.code + index).toChar().toString(), "Option $index") + } + } + var selected: String? = null + val owner = "select.portal.test" + engine.open( + SelectOpenRequest( + owner = owner, + modelToken = model.token, + entries = model.entries, + selectedId = null, + anchorRect = Rect(30, 30, 90, 18), + closeOnSelect = true, + onSelect = { selected = it }, + ), + ) + clock.advance(OPEN_DURATION_MS + 1) + controller.onFrame(ctx, VIEWPORT_WIDTH, VIEWPORT_HEIGHT, 1f) + return SelectPortalFixture(clock, engine, controller, owner) { selected } + } + + private data class SelectPortalFixture( + val clock: FakeClock, + val engine: SelectEngine, + val controller: SelectPortalController, + val owner: Any, + val selectedProvider: () -> String?, + ) { + val selected: String? + get() = selectedProvider() + } + + private companion object { + const val VIEWPORT_WIDTH = 320 + const val VIEWPORT_HEIGHT = 180 + const val OPEN_DURATION_MS = 1L + const val CLOSE_DURATION_MS = 1L + } +} From 1843938b470ffe2819db1ff472d75e1ddd782fd4 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Thu, 28 May 2026 14:46:34 +0300 Subject: [PATCH 69/78] removing OverlayDebugControlHost, migrating functionality to DebugDomainRootHost for streamlined domain management; --- .../dsgl/mcForge1710/DsglScreenHost.kt | 80 ++- .../dsgl/core/debug/DebugDomainHosts.kt | 521 ++++++++++++++++++ .../core/debug/OverlayDebugControlHost.kt | 436 --------------- ...lHostTests.kt => DebugDomainHostsTests.kt} | 112 +++- 4 files changed, 680 insertions(+), 469 deletions(-) create mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/DebugDomainHosts.kt delete mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt rename core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/{OverlayDebugControlHostTests.kt => DebugDomainHostsTests.kt} (69%) diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index e4fde8a..81cdf25 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -9,7 +9,8 @@ import org.dreamfinity.dsgl.core.DsglWindow import org.dreamfinity.dsgl.core.HotReloadBridge import org.dreamfinity.dsgl.core.animation.* import org.dreamfinity.dsgl.core.colorpicker.* -import org.dreamfinity.dsgl.core.debug.OverlayDebugControlHost +import org.dreamfinity.dsgl.core.debug.DebugDomainPortalHost +import org.dreamfinity.dsgl.core.debug.DebugDomainRootHost import org.dreamfinity.dsgl.core.debug.OverlayLayerDebugState import org.dreamfinity.dsgl.core.dnd.* import org.dreamfinity.dsgl.core.dom.DOMNode @@ -106,7 +107,8 @@ abstract class DsglScreenHost( private val inspector: InspectorController = InspectorController() private val applicationOverlayHost: ApplicationOverlayHost = ApplicationOverlayHost() private val systemOverlayHost: SystemOverlayHost = SystemOverlayHost(inspector) - private val debugOverlayHost: OverlayDebugControlHost = OverlayDebugControlHost() + private val debugDomainRootHost: DebugDomainRootHost = DebugDomainRootHost() + private val debugDomainPortalHost: DebugDomainPortalHost = DebugDomainPortalHost() private val domainOrchestrator: ScreenDomainSurfaceOrchestrator = ScreenDomainSurfaceOrchestrator() private val colorSamplerOwnershipRouter: ActiveColorSamplerOwnershipRouter = ActiveColorSamplerOwnershipRouter() private var activeColorSamplerOwner: ActiveColorSamplerOwner = ActiveColorSamplerOwner.None @@ -225,7 +227,7 @@ abstract class DsglScreenHost( systemOverlayCommands = systemOverlayCommands, systemOverlayRenderEnabled = overlayState.systemOverlayRenderEnabled, ) - val debugOverlayCommands = collectDebugOverlayCommands() + val debugDomainCommands = collectDebugDomainCommands() updateFrameInteractionState( tree = tree, dtSeconds = dtSeconds, @@ -243,7 +245,7 @@ abstract class DsglScreenHost( composeAndPresentFrame( tree = tree, commands = commands, - debugOverlayCommands = debugOverlayCommands, + debugDomainCommands = debugDomainCommands, rebuiltThisFrame = rebuiltThisFrame, layoutCommittedThisFrame = layoutPhase.layoutCommittedThisFrame, ) @@ -273,6 +275,11 @@ abstract class DsglScreenHost( val inspectorBlocks: Boolean, ) + private data class DebugDomainCommands( + val root: List, + val portal: List, + ) + private fun prepareFrameCursor(): FrameCursorPosition { updateSize(force = false) val dsglMouseX = lastViewport.rawMouseToDsglX(Mouse.getX()) @@ -471,13 +478,23 @@ abstract class DsglScreenHost( } } - private fun collectDebugOverlayCommands(): List = - runCatching { - debugOverlayHost.render(adapter, lastWidth, lastHeight) - debugOverlayHost.paint(adapter) - }.getOrElse { - emptyList() - } + private fun collectDebugDomainCommands(): DebugDomainCommands { + val root = + runCatching { + debugDomainRootHost.render(adapter, lastWidth, lastHeight) + debugDomainRootHost.paint(adapter) + }.getOrElse { + emptyList() + } + val portal = + runCatching { + debugDomainPortalHost.render(adapter, lastWidth, lastHeight) + debugDomainPortalHost.paint(adapter) + }.getOrElse { + emptyList() + } + return DebugDomainCommands(root = root, portal = portal) + } private fun updateFrameInteractionState( tree: DomTree, @@ -560,7 +577,7 @@ abstract class DsglScreenHost( private fun composeAndPresentFrame( tree: DomTree, commands: List, - debugOverlayCommands: List, + debugDomainCommands: DebugDomainCommands, rebuiltThisFrame: Boolean, layoutCommittedThisFrame: Boolean, ) { @@ -568,7 +585,8 @@ abstract class DsglScreenHost( applicationRoot = commands, applicationPortal = applicationOverlayCommandsBuffer, systemPortal = systemOverlayCommandsBuffer, - debugRoot = debugOverlayCommands, + debugRoot = debugDomainCommands.root, + debugPortal = debugDomainCommands.portal, out = stagingCommandsBuffer, shouldRenderSurface = OverlayLayerDebugState::isRenderEnabled, ) @@ -628,7 +646,8 @@ abstract class DsglScreenHost( domTree?.clearRefs() applicationOverlayHost.clearRefs() systemOverlayHost.clearRefs() - debugOverlayHost.clearRefs() + debugDomainRootHost.clearRefs() + debugDomainPortalHost.clearRefs() domTree?.root?.let { root -> EventBus.run { root.clearListenersDeep() } } @@ -941,6 +960,8 @@ abstract class DsglScreenHost( ) runOverlayInputFrame(applicationOverlayHost) runOverlayInputFrame(systemOverlayHost) + runOverlayInputFrame(debugDomainRootHost) + runOverlayInputFrame(debugDomainPortalHost) inspectorPointerCaptured = inspector.isPointerCaptured systemOverlayHost.syncFrame( inspectedRoot = tree.root, @@ -995,7 +1016,7 @@ abstract class DsglScreenHost( context: DomainPointerDispatchContext, ): Boolean = when (surface) { - ScreenDomainSurfaces.DebugPortal -> false + ScreenDomainSurfaces.DebugPortal -> consumeDebugPortalPointerSurface(context) ScreenDomainSurfaces.DebugRoot -> consumeDebugRootPointerSurface(context) ScreenDomainSurfaces.SystemPortal -> consumeSystemPortalPointerSurface(context) ScreenDomainSurfaces.SystemRoot -> false @@ -1032,6 +1053,22 @@ abstract class DsglScreenHost( return false } return consumeDebugPointerEvent( + host = debugDomainRootHost, + mouseX = context.inputEvent.mouseX, + mouseY = context.inputEvent.mouseY, + dWheel = context.inputEvent.dWheel, + mappedButton = context.mappedButton, + mouseButton = context.inputEvent.mouseButton, + buttonPressed = context.buttonPressed, + ) + } + + private fun consumeDebugPortalPointerSurface(context: DomainPointerDispatchContext): Boolean { + if (context.applicationRootPressMove) { + return false + } + return consumeDebugPointerEvent( + host = debugDomainPortalHost, mouseX = context.inputEvent.mouseX, mouseY = context.inputEvent.mouseY, dWheel = context.inputEvent.dWheel, @@ -1195,8 +1232,8 @@ abstract class DsglScreenHost( domainOrchestrator.firstInputConsumer( canConsume = { surface -> when (surface) { - ScreenDomainSurfaces.DebugPortal -> false - ScreenDomainSurfaces.DebugRoot -> debugOverlayHost.handleKeyDown(keyCode, keyChar) + ScreenDomainSurfaces.DebugPortal -> debugDomainPortalHost.handleKeyDown(keyCode, keyChar) + ScreenDomainSurfaces.DebugRoot -> debugDomainRootHost.handleKeyDown(keyCode, keyChar) ScreenDomainSurfaces.SystemPortal -> consumeSystemOverlayKeyDown( @@ -1283,6 +1320,7 @@ abstract class DsglScreenHost( } private fun consumeDebugPointerEvent( + host: DomainSurfaceHost, mouseX: Int, mouseY: Int, dWheel: Int, @@ -1290,17 +1328,17 @@ abstract class DsglScreenHost( mouseButton: Int, buttonPressed: Boolean, ): Boolean { - if (dWheel != 0 && debugOverlayHost.handleMouseWheel(mouseX, mouseY, dWheel)) { + if (dWheel != 0 && host.handleMouseWheel(mouseX, mouseY, dWheel)) { return true } if (mouseButton != -1 && mappedButton != null) { return if (buttonPressed) { - debugOverlayHost.handleMouseDown(mouseX, mouseY, mappedButton) + host.handleMouseDown(mouseX, mouseY, mappedButton) } else { - debugOverlayHost.handleMouseUp(mouseX, mouseY, mappedButton) + host.handleMouseUp(mouseX, mouseY, mappedButton) } } - if (mouseButton == -1 && debugOverlayHost.handleMouseMove(mouseX, mouseY)) { + if (mouseButton == -1 && host.handleMouseMove(mouseX, mouseY)) { return true } return false diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/DebugDomainHosts.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/DebugDomainHosts.kt new file mode 100644 index 0000000..84f8d26 --- /dev/null +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/DebugDomainHosts.kt @@ -0,0 +1,521 @@ +package org.dreamfinity.dsgl.core.debug + +import org.dreamfinity.dsgl.core.DomTree +import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.dom.elements.ButtonNode +import org.dreamfinity.dsgl.core.dom.elements.ContainerNode +import org.dreamfinity.dsgl.core.dom.elements.TextNode +import org.dreamfinity.dsgl.core.dom.elements.TextSource +import org.dreamfinity.dsgl.core.dom.layout.Border +import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.dom.layout.Size +import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.dsl.UiScope +import org.dreamfinity.dsgl.core.dsl.button +import org.dreamfinity.dsgl.core.dsl.div +import org.dreamfinity.dsgl.core.dsl.text +import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.overlay.DomainSurfaceHost +import org.dreamfinity.dsgl.core.overlay.PortalEntry +import org.dreamfinity.dsgl.core.overlay.PortalFrameContext +import org.dreamfinity.dsgl.core.overlay.PortalHost +import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurface +import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces +import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter +import org.dreamfinity.dsgl.core.render.RenderCommand +import org.dreamfinity.dsgl.core.style.Display +import org.dreamfinity.dsgl.core.style.StyleApplicationScope +import org.dreamfinity.dsgl.core.style.TextWrap +import java.util.Locale + +private const val MIN_VIEWPORT_SIZE = 1 +private const val PANEL_WIDTH = 300 +private const val PANEL_HEIGHT = 232 +private const val PANEL_MARGIN = 8 +private const val PANEL_MIN_WIDTH = 120 +private const val PANEL_MIN_HEIGHT = 96 +private const val PANEL_HORIZONTAL_PADDING = 10 +private const val TOGGLE_WIDTH = 56 +private const val TOGGLE_HEIGHT = 18 +private const val FIRST_ROW_OFFSET_Y = 34 +private const val ROW_STEP = 24 +private const val RESET_BOTTOM_OFFSET = 40 +private const val RESET_HEIGHT = 20 +private const val SHADOW_OFFSET = 2 +private const val TITLE_OFFSET_Y = 8 +private const val TITLE_HEIGHT = 18 +private const val STATUS_BOTTOM_OFFSET = 14 +private const val STATUS_HEIGHT = 14 +private const val LABEL_LEFT_PADDING_EXTRA = 6 +private const val BUTTON_FONT_SIZE = 14 +private const val TITLE_FONT_SIZE = 16 + +private const val COLOR_SHADOW = 0x5F000000 +private const val COLOR_PANEL = 0xEE1A2230.toInt() +private const val COLOR_PANEL_BORDER = 0xFF5A6B80.toInt() +private const val COLOR_TEXT_PRIMARY = 0xFFFFFFFF.toInt() +private const val COLOR_TEXT_MUTED = 0xFFBAC7D6.toInt() +private const val COLOR_LABEL = 0xFFE0E9F2.toInt() +private const val COLOR_RESET_BACKGROUND = 0xFF2E3A49.toInt() +private const val COLOR_RESET_BORDER = 0xFF6886A5.toInt() +private const val COLOR_TOGGLE_ON = 0xFF2F7D4E.toInt() +private const val COLOR_TOGGLE_OFF = 0xFF7A2E3A.toInt() +private const val COLOR_TOGGLE_BORDER = 0xFF9AB3C9.toInt() + +internal data class DebugDomainControlLayout( + val panelRect: Rect, + val appOverlayRenderRect: Rect, + val appOverlayTintRect: Rect, + val appOverlayInputRect: Rect, + val systemOverlayTintRect: Rect, + val systemOverlayRenderRect: Rect, + val systemOverlayInputRect: Rect, + val resetRect: Rect, +) + +private data class DebugDomainToggleSnapshot( + val applicationOverlayRenderEnabled: Boolean, + val applicationOverlayTintEnabled: Boolean, + val applicationOverlayInputEnabled: Boolean, + val systemOverlayRenderEnabled: Boolean, + val systemOverlayTintEnabled: Boolean, + val systemOverlayInputEnabled: Boolean, +) + +class DebugDomainRootHost( + private val state: OverlayLayerDebugState = OverlayLayerDebugState, +) : DomainSurfaceHost { + override val surface: ScreenDomainSurface = ScreenDomainSurfaces.DebugRoot + + private var viewportWidth: Int = MIN_VIEWPORT_SIZE + private var viewportHeight: Int = MIN_VIEWPORT_SIZE + private var layout: DebugDomainControlLayout? = null + private val rootNode: DebugDomainRootNode = DebugDomainRootNode(state) + private val tree: DomTree = + DomTree( + root = rootNode, + styleScope = StyleApplicationScope.Debug, + ) + private val domInputRouter: LayerDomInputRouter = LayerDomInputRouter { rootNode } + private var lastToggleSnapshot: DebugDomainToggleSnapshot? = null + + @Suppress("UnusedParameter") + override fun render(ctx: UiMeasureContext, width: Int, height: Int) { + viewportWidth = width.coerceAtLeast(MIN_VIEWPORT_SIZE) + viewportHeight = height.coerceAtLeast(MIN_VIEWPORT_SIZE) + if (!state.controlsEnabled) { + layout = null + lastToggleSnapshot = null + return + } + layout = buildLayout(viewportWidth, viewportHeight) + } + + override fun paint(ctx: UiMeasureContext): List { + val currentLayout = layout ?: return emptyList() + val snapshot = state.snapshot() + val toggleSnapshot = snapshot.toDebugToggleSnapshot() + if (lastToggleSnapshot != toggleSnapshot) { + tree.invalidateRenderCommandChunks() + lastToggleSnapshot = toggleSnapshot + } + rootNode.bind(currentLayout, snapshot) + tree.render(ctx, viewportWidth, viewportHeight) + return tree.paint(ctx, applyStyles = true) + } + + override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = domInputRouter.handleMouseMove(mouseX, mouseY) + + override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + domInputRouter.handleMouseDown(mouseX, mouseY, button) + + override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + domInputRouter.handleMouseUp(mouseX, mouseY, button) + + override fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean { + if (delta == 0) return false + return domInputRouter.handleMouseWheel(mouseX, mouseY, delta) + } + + override fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = domInputRouter.handleKeyDown(keyCode, keyChar) + + override fun clearRefs() { + layout = null + lastToggleSnapshot = null + domInputRouter.clear() + tree.clearRefs() + } + + internal fun debugLayout(): DebugDomainControlLayout? = layout + + internal val debugStyleScope: StyleApplicationScope + get() = StyleApplicationScope.Debug + + private fun buildLayout(viewportWidth: Int, viewportHeight: Int): DebugDomainControlLayout { + val panelX = PANEL_MARGIN + val panelY = (viewportHeight - PANEL_HEIGHT - PANEL_MARGIN).coerceAtLeast(PANEL_MARGIN) + val panelRect = + Rect( + x = panelX, + y = panelY, + width = PANEL_WIDTH.coerceAtMost((viewportWidth - PANEL_MARGIN * 2).coerceAtLeast(PANEL_MIN_WIDTH)), + height = PANEL_HEIGHT.coerceAtMost((viewportHeight - PANEL_MARGIN * 2).coerceAtLeast(PANEL_MIN_HEIGHT)), + ) + val toggleX = panelRect.x + panelRect.width - TOGGLE_WIDTH - PANEL_HORIZONTAL_PADDING + val firstY = panelRect.y + FIRST_ROW_OFFSET_Y + var row = 0 + return DebugDomainControlLayout( + panelRect = panelRect, + appOverlayRenderRect = Rect(toggleX, firstY + ROW_STEP * row++, TOGGLE_WIDTH, TOGGLE_HEIGHT), + appOverlayTintRect = Rect(toggleX, firstY + ROW_STEP * row++, TOGGLE_WIDTH, TOGGLE_HEIGHT), + appOverlayInputRect = Rect(toggleX, firstY + ROW_STEP * row++, TOGGLE_WIDTH, TOGGLE_HEIGHT), + systemOverlayRenderRect = Rect(toggleX, firstY + ROW_STEP * row++, TOGGLE_WIDTH, TOGGLE_HEIGHT), + systemOverlayTintRect = Rect(toggleX, firstY + ROW_STEP * row++, TOGGLE_WIDTH, TOGGLE_HEIGHT), + systemOverlayInputRect = Rect(toggleX, firstY + ROW_STEP * row++, TOGGLE_WIDTH, TOGGLE_HEIGHT), + resetRect = + Rect( + x = panelRect.x + PANEL_HORIZONTAL_PADDING, + y = panelRect.y + panelRect.height - RESET_BOTTOM_OFFSET, + width = panelRect.width - PANEL_HORIZONTAL_PADDING * 2, + height = RESET_HEIGHT, + ), + ) + } +} + +class DebugDomainPortalHost : DomainSurfaceHost { + override val surface: ScreenDomainSurface = ScreenDomainSurfaces.DebugPortal + + private val portalHost: PortalHost = PortalHost(ScreenDomainSurfaces.DebugPortal) + private var viewportWidth: Int = MIN_VIEWPORT_SIZE + private var viewportHeight: Int = MIN_VIEWPORT_SIZE + + override fun onInputFrame(viewportWidth: Int, viewportHeight: Int) { + this.viewportWidth = viewportWidth.coerceAtLeast(MIN_VIEWPORT_SIZE) + this.viewportHeight = viewportHeight.coerceAtLeast(MIN_VIEWPORT_SIZE) + portalHost.onInputFrame(PortalFrameContext(Rect(0, 0, this.viewportWidth, this.viewportHeight))) + } + + override fun render(ctx: UiMeasureContext, width: Int, height: Int) { + viewportWidth = width.coerceAtLeast(MIN_VIEWPORT_SIZE) + viewportHeight = height.coerceAtLeast(MIN_VIEWPORT_SIZE) + portalHost.render(ctx, viewportWidth, viewportHeight) + } + + override fun paint(ctx: UiMeasureContext): List = portalHost.paint(ctx) + + override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = + portalHost.dispatchInput { it.handleMouseMove(mouseX, mouseY) } + + override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + portalHost.dispatchInput { it.handleMouseDown(mouseX, mouseY, button) } + + override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + portalHost.dispatchInput { it.handleMouseUp(mouseX, mouseY, button) } + + override fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = + portalHost.dispatchInput { it.handleMouseWheel(mouseX, mouseY, delta) } + + override fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = + portalHost.dispatchInput { it.handleKeyDown(keyCode, keyChar) } + + override fun clearRefs() { + portalHost.clearRefs() + } + + internal fun debugRegisterPortalEntryForTests(entry: PortalEntry) { + portalHost.register(entry) + } + + internal val debugActivePortalEntryIds: List + get() = portalHost.entriesInPaintOrder().map { it.state.id.value } +} + +private fun OverlayLayerDebugSnapshot.toDebugToggleSnapshot(): DebugDomainToggleSnapshot = + DebugDomainToggleSnapshot( + applicationOverlayRenderEnabled = applicationOverlayRenderEnabled, + applicationOverlayTintEnabled = applicationOverlayTintEnabled, + applicationOverlayInputEnabled = applicationOverlayInputEnabled, + systemOverlayRenderEnabled = systemOverlayRenderEnabled, + systemOverlayTintEnabled = systemOverlayTintEnabled, + systemOverlayInputEnabled = systemOverlayInputEnabled, + ) + +private class DebugDomainRootNode( + private val state: OverlayLayerDebugState, + key: Any? = "dsgl-debug-domain-root", +) : DOMNode(key) { + override val styleType: String = "dsgl-debug-domain-root" + + private val scope = UiScope(this) + private val shadowNode: ContainerNode = + scope.div({ + this.key = "dsgl-debug-domain-shadow" + }) + private val panelNode: ContainerNode = + scope.div({ + this.key = "dsgl-debug-domain-panel" + onMouseDown = { event -> + event.cancelled = true + } + onMouseWheel = { event -> + event.cancelled = true + } + }) + private val titleNode: TextNode = + scope.text(props = { + this.key = "dsgl-debug-domain-title" + source = TextSource.Static("Debug Domain") + style = { + textWrap = TextWrap.NoWrap + } + }) + + private val appRenderLabelNode: TextNode = labelNode("App Portal Render", "dsgl-debug-domain-label-app-render") + private val appTintLabelNode: TextNode = labelNode("App Portal Tint", "dsgl-debug-domain-label-app-tint") + private val appInputLabelNode: TextNode = labelNode("App Portal Input", "dsgl-debug-domain-label-app-input") + private val systemRenderLabelNode: TextNode = + labelNode("System Portal Render", "dsgl-debug-domain-label-system-render") + private val systemTintLabelNode: TextNode = labelNode("System Portal Tint", "dsgl-debug-domain-label-system-tint") + private val systemInputLabelNode: TextNode = + labelNode("System Portal Input", "dsgl-debug-domain-label-system-input") + + private val appRenderToggleNode: ButtonNode = + toggleNode("dsgl-debug-domain-toggle-app-render") { + state.applicationOverlayRenderEnabled = !state.applicationOverlayRenderEnabled + } + private val appTintToggleNode: ButtonNode = + toggleNode("dsgl-debug-domain-toggle-app-tint") { + state.applicationOverlayTintEnabled = !state.applicationOverlayTintEnabled + } + private val appInputToggleNode: ButtonNode = + toggleNode("dsgl-debug-domain-toggle-app-input") { + state.applicationOverlayInputEnabled = !state.applicationOverlayInputEnabled + } + private val systemRenderToggleNode: ButtonNode = + toggleNode("dsgl-debug-domain-toggle-system-render") { + state.systemOverlayRenderEnabled = !state.systemOverlayRenderEnabled + } + private val systemTintToggleNode: ButtonNode = + toggleNode("dsgl-debug-domain-toggle-system-tint") { + state.systemOverlayTintEnabled = !state.systemOverlayTintEnabled + } + private val systemInputToggleNode: ButtonNode = + toggleNode("dsgl-debug-domain-toggle-system-input") { + state.systemOverlayInputEnabled = !state.systemOverlayInputEnabled + } + + private val resetButtonNode: ButtonNode = + scope.button("Reset All", { + this.key = "dsgl-debug-domain-reset" + onMouseDown = { event -> + state.resetAll() + event.cancelled = true + } + style = { + textWrap = TextWrap.NoWrap + } + }) + + private val statusNode: TextNode = + scope.text(props = { + this.key = "dsgl-debug-domain-status" + source = TextSource.Static("") + style = { + textWrap = TextWrap.NoWrap + } + }) + + private var layout: DebugDomainControlLayout? = null + private var snapshot: OverlayLayerDebugSnapshot = + OverlayLayerDebugSnapshot( + applicationOverlayRenderEnabled = true, + applicationOverlayTintEnabled = false, + applicationOverlayInputEnabled = true, + systemOverlayRenderEnabled = true, + systemOverlayTintEnabled = false, + systemOverlayInputEnabled = true, + frameFps = 0, + frameTimeMs = 0f, + frameFpsWindow = 0, + frameTimeWindowMs = 0f, + ) + + fun bind(layout: DebugDomainControlLayout, snapshot: OverlayLayerDebugSnapshot) { + this.layout = layout + this.snapshot = snapshot + } + + override fun measure(ctx: UiMeasureContext): Size = + Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) + + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { + bounds = Rect(x, y, width, height) + val localLayout = + layout ?: run { + hideAll(ctx) + return + } + + val panelRect = localLayout.panelRect + val shadowRect = + Rect(panelRect.x + SHADOW_OFFSET, panelRect.y + SHADOW_OFFSET, panelRect.width, panelRect.height) + + shadowNode.backgroundColor = COLOR_SHADOW + shadowNode.border = Border.NONE + + panelNode.backgroundColor = COLOR_PANEL + panelNode.border = Border.all(1, COLOR_PANEL_BORDER) + + titleNode.color = COLOR_TEXT_PRIMARY + titleNode.fontSize = TITLE_FONT_SIZE + + applyLabelStyle(appRenderLabelNode) + applyLabelStyle(appTintLabelNode) + applyLabelStyle(appInputLabelNode) + applyLabelStyle(systemRenderLabelNode) + applyLabelStyle(systemTintLabelNode) + applyLabelStyle(systemInputLabelNode) + + configureToggle(appRenderToggleNode, snapshot.applicationOverlayRenderEnabled) + configureToggle(appTintToggleNode, snapshot.applicationOverlayTintEnabled) + configureToggle(appInputToggleNode, snapshot.applicationOverlayInputEnabled) + configureToggle(systemRenderToggleNode, snapshot.systemOverlayRenderEnabled) + configureToggle(systemTintToggleNode, snapshot.systemOverlayTintEnabled) + configureToggle(systemInputToggleNode, snapshot.systemOverlayInputEnabled) + + resetButtonNode.backgroundColor = COLOR_RESET_BACKGROUND + resetButtonNode.border = Border.all(1, COLOR_RESET_BORDER) + resetButtonNode.textColor = COLOR_TEXT_PRIMARY + resetButtonNode.fontSize = BUTTON_FONT_SIZE + + val rApp = if (snapshot.applicationOverlayRenderEnabled) "A1" else "A0" + val rSys = if (snapshot.systemOverlayRenderEnabled) "S1" else "S0" + val iApp = if (snapshot.applicationOverlayInputEnabled) "A1" else "A0" + val iSys = if (snapshot.systemOverlayInputEnabled) "S1" else "S0" + val statusTextValue = + "R:$rApp/$rSys I:$iApp/$iSys " + + "FPS:${snapshot.frameFps} (${String.format(Locale.US, "%.1f", snapshot.frameTimeMs)}ms) " + + "AvgFPS:${snapshot.frameFpsWindow} (${String.format(Locale.US, "%.1f", snapshot.frameTimeWindowMs)}ms)" + statusNode.setText(statusTextValue) + statusNode.color = COLOR_TEXT_MUTED + statusNode.fontSize = BUTTON_FONT_SIZE + + renderNode(ctx, shadowNode, shadowRect) + renderNode(ctx, panelNode, panelRect) + renderNode( + ctx, + titleNode, + Rect( + panelRect.x + PANEL_HORIZONTAL_PADDING, + panelRect.y + TITLE_OFFSET_Y, + (panelRect.width - PANEL_HORIZONTAL_PADDING * 2).coerceAtLeast(MIN_VIEWPORT_SIZE), + TITLE_HEIGHT, + ), + ) + + renderToggleRow(ctx, panelRect, localLayout.appOverlayRenderRect, appRenderLabelNode, appRenderToggleNode) + renderToggleRow(ctx, panelRect, localLayout.appOverlayTintRect, appTintLabelNode, appTintToggleNode) + renderToggleRow(ctx, panelRect, localLayout.appOverlayInputRect, appInputLabelNode, appInputToggleNode) + renderToggleRow( + ctx, + panelRect, + localLayout.systemOverlayRenderRect, + systemRenderLabelNode, + systemRenderToggleNode, + ) + renderToggleRow(ctx, panelRect, localLayout.systemOverlayTintRect, systemTintLabelNode, systemTintToggleNode) + renderToggleRow(ctx, panelRect, localLayout.systemOverlayInputRect, systemInputLabelNode, systemInputToggleNode) + + renderNode(ctx, resetButtonNode, localLayout.resetRect) + renderNode( + ctx, + statusNode, + Rect( + x = panelRect.x + PANEL_HORIZONTAL_PADDING, + y = panelRect.y + panelRect.height - STATUS_BOTTOM_OFFSET, + width = (panelRect.width - PANEL_HORIZONTAL_PADDING * 2).coerceAtLeast(MIN_VIEWPORT_SIZE), + height = STATUS_HEIGHT, + ), + ) + } + + private fun labelNode(text: String, key: Any): TextNode = + scope.text(props = { + this.key = key + source = TextSource.Static(text) + style = { + textWrap = TextWrap.NoWrap + } + }) + + private fun toggleNode(key: Any, onToggle: () -> Unit): ButtonNode = + scope.button("ON", { + this.key = key + onMouseDown = { event -> + onToggle() + event.cancelled = true + } + style = { + textWrap = TextWrap.NoWrap + } + }) + + private fun applyLabelStyle(node: TextNode) { + node.color = COLOR_LABEL + node.fontSize = BUTTON_FONT_SIZE + } + + private fun configureToggle(node: ButtonNode, enabled: Boolean) { + node.text = if (enabled) "ON" else "OFF" + node.backgroundColor = if (enabled) COLOR_TOGGLE_ON else COLOR_TOGGLE_OFF + node.border = Border.all(1, COLOR_TOGGLE_BORDER) + node.textColor = COLOR_TEXT_PRIMARY + node.fontSize = BUTTON_FONT_SIZE + } + + private fun renderToggleRow( + ctx: UiMeasureContext, + panelRect: Rect, + toggleRect: Rect, + labelNode: TextNode, + toggleNode: ButtonNode, + ) { + val labelRect = + Rect( + x = panelRect.x + PANEL_HORIZONTAL_PADDING, + y = toggleRect.y + SHADOW_OFFSET, + width = + ( + toggleRect.x - + (panelRect.x + PANEL_HORIZONTAL_PADDING + LABEL_LEFT_PADDING_EXTRA) + ).coerceAtLeast(MIN_VIEWPORT_SIZE), + height = (toggleRect.height - SHADOW_OFFSET).coerceAtLeast(MIN_VIEWPORT_SIZE), + ) + renderNode(ctx, labelNode, labelRect) + renderNode(ctx, toggleNode, toggleRect) + } + + private fun renderNode(ctx: UiMeasureContext, node: DOMNode, rect: Rect?) { + if (rect == null || rect.width <= 0 || rect.height <= 0) { + node.display = Display.None + node.render(ctx, 0, 0, 0, 0) + return + } + node.display = Display.Block + node.render(ctx, rect.x, rect.y, rect.width, rect.height) + } + + private fun hideAll(ctx: UiMeasureContext) { + children.forEach { child -> + child.display = Display.None + child.render(ctx, 0, 0, 0, 0) + } + } +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt deleted file mode 100644 index ac56a9e..0000000 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHost.kt +++ /dev/null @@ -1,436 +0,0 @@ -package org.dreamfinity.dsgl.core.debug - -import org.dreamfinity.dsgl.core.DomTree -import org.dreamfinity.dsgl.core.dom.DOMNode -import org.dreamfinity.dsgl.core.dom.elements.ButtonNode -import org.dreamfinity.dsgl.core.dom.elements.ContainerNode -import org.dreamfinity.dsgl.core.dom.elements.TextNode -import org.dreamfinity.dsgl.core.dom.elements.TextSource -import org.dreamfinity.dsgl.core.dom.layout.Border -import org.dreamfinity.dsgl.core.dom.layout.Rect -import org.dreamfinity.dsgl.core.dom.layout.Size -import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext -import org.dreamfinity.dsgl.core.dsl.UiScope -import org.dreamfinity.dsgl.core.dsl.button -import org.dreamfinity.dsgl.core.dsl.div -import org.dreamfinity.dsgl.core.dsl.text -import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.DomainSurfaceHost -import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurface -import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces -import org.dreamfinity.dsgl.core.render.RenderCommand -import org.dreamfinity.dsgl.core.style.Display -import org.dreamfinity.dsgl.core.style.StyleApplicationScope -import org.dreamfinity.dsgl.core.style.TextWrap -import java.util.Locale - -internal data class OverlayDebugControlLayout( - val panelRect: Rect, - val appOverlayRenderRect: Rect, - val appOverlayTintRect: Rect, - val appOverlayInputRect: Rect, - val systemOverlayTintRect: Rect, - val systemOverlayRenderRect: Rect, - val systemOverlayInputRect: Rect, - val resetRect: Rect, -) - -private data class OverlayDebugToggleSnapshot( - val applicationOverlayRenderEnabled: Boolean, - val applicationOverlayTintEnabled: Boolean, - val applicationOverlayInputEnabled: Boolean, - val systemOverlayRenderEnabled: Boolean, - val systemOverlayTintEnabled: Boolean, - val systemOverlayInputEnabled: Boolean, -) - -class OverlayDebugControlHost( - private val state: OverlayLayerDebugState = OverlayLayerDebugState, -) : DomainSurfaceHost { - override val surface: ScreenDomainSurface = ScreenDomainSurfaces.DebugRoot - - private var viewportWidth: Int = 1 - private var viewportHeight: Int = 1 - private var layout: OverlayDebugControlLayout? = null - private val rootNode: OverlayDebugControlRootNode = OverlayDebugControlRootNode() - private val tree: DomTree = - DomTree( - root = rootNode, - styleScope = StyleApplicationScope.Debug, - ) - private var lastToggleSnapshot: OverlayDebugToggleSnapshot? = null - - @Suppress("UnusedParameter") - override fun render(ctx: UiMeasureContext, width: Int, height: Int) { - viewportWidth = width.coerceAtLeast(1) - viewportHeight = height.coerceAtLeast(1) - if (!state.controlsEnabled) { - layout = null - lastToggleSnapshot = null - return - } - layout = buildLayout(viewportWidth, viewportHeight) - } - - override fun paint(ctx: UiMeasureContext): List { - val currentLayout = layout ?: return emptyList() - val snapshot = state.snapshot() - val toggleSnapshot = snapshot.toDebugToggleSnapshot() - if (lastToggleSnapshot != toggleSnapshot) { - tree.invalidateRenderCommandChunks() - lastToggleSnapshot = toggleSnapshot - } - rootNode.bind(currentLayout, snapshot) - tree.render(ctx, viewportWidth, viewportHeight) - return tree.paint(ctx, applyStyles = true) - } - - override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean { - val currentLayout = layout ?: return false - return currentLayout.panelRect.contains(mouseX, mouseY) - } - - override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { - val currentLayout = layout ?: return false - if (!currentLayout.panelRect.contains(mouseX, mouseY)) { - return false - } - if (button != MouseButton.LEFT) { - return true - } - when { - currentLayout.appOverlayRenderRect.contains(mouseX, mouseY) -> { - state.applicationOverlayRenderEnabled = !state.applicationOverlayRenderEnabled - } - - currentLayout.appOverlayTintRect.contains(mouseX, mouseY) -> { - state.applicationOverlayTintEnabled = !state.applicationOverlayTintEnabled - } - - currentLayout.appOverlayInputRect.contains(mouseX, mouseY) -> { - state.applicationOverlayInputEnabled = !state.applicationOverlayInputEnabled - } - - currentLayout.systemOverlayRenderRect.contains(mouseX, mouseY) -> { - state.systemOverlayRenderEnabled = !state.systemOverlayRenderEnabled - } - - currentLayout.systemOverlayTintRect.contains(mouseX, mouseY) -> { - state.systemOverlayTintEnabled = !state.systemOverlayTintEnabled - } - - currentLayout.systemOverlayInputRect.contains(mouseX, mouseY) -> { - state.systemOverlayInputEnabled = !state.systemOverlayInputEnabled - } - - currentLayout.resetRect.contains(mouseX, mouseY) -> { - state.resetAll() - } - } - return true - } - - override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { - val currentLayout = layout ?: return false - if (button != MouseButton.LEFT) return false - return currentLayout.panelRect.contains(mouseX, mouseY) - } - - override fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean { - val currentLayout = layout ?: return false - if (delta == 0) return false - return currentLayout.panelRect.contains(mouseX, mouseY) - } - - @Suppress("FunctionOnlyReturningConstant", "UnusedParameter") - override fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = false - - override fun clearRefs() { - layout = null - lastToggleSnapshot = null - tree.clearRefs() - } - - internal fun debugLayout(): OverlayDebugControlLayout? = layout - - internal val debugStyleScope: StyleApplicationScope - get() = StyleApplicationScope.Debug - - private fun buildLayout(viewportWidth: Int, viewportHeight: Int): OverlayDebugControlLayout { - val panelWidth = 300 - val panelHeight = 176 + 56 - val panelX = 8 - val panelY = (viewportHeight - panelHeight - 8).coerceAtLeast(8) - val panelRect = - Rect( - x = panelX, - y = panelY, - width = panelWidth.coerceAtMost((viewportWidth - 16).coerceAtLeast(120)), - height = panelHeight.coerceAtMost((viewportHeight - 16).coerceAtLeast(96)), - ) - val toggleWidth = 56 - val toggleHeight = 18 - val toggleX = panelRect.x + panelRect.width - toggleWidth - 10 - val firstY = panelRect.y + 34 - val rowStep = 24 - var row = 0 - return OverlayDebugControlLayout( - panelRect = panelRect, - appOverlayRenderRect = Rect(toggleX, firstY + rowStep * row++, toggleWidth, toggleHeight), - appOverlayTintRect = Rect(toggleX, firstY + rowStep * row++, toggleWidth, toggleHeight), - appOverlayInputRect = Rect(toggleX, firstY + rowStep * row++, toggleWidth, toggleHeight), - systemOverlayRenderRect = Rect(toggleX, firstY + rowStep * row++, toggleWidth, toggleHeight), - systemOverlayTintRect = Rect(toggleX, firstY + rowStep * row++, toggleWidth, toggleHeight), - systemOverlayInputRect = Rect(toggleX, firstY + rowStep * row++, toggleWidth, toggleHeight), - resetRect = - Rect( - x = panelRect.x + 10, - y = panelRect.y + panelRect.height - 40, - width = panelRect.width - 20, - height = 20, - ), - ) - } -} - -private fun OverlayLayerDebugSnapshot.toDebugToggleSnapshot(): OverlayDebugToggleSnapshot = - OverlayDebugToggleSnapshot( - applicationOverlayRenderEnabled = applicationOverlayRenderEnabled, - applicationOverlayTintEnabled = applicationOverlayTintEnabled, - applicationOverlayInputEnabled = applicationOverlayInputEnabled, - systemOverlayRenderEnabled = systemOverlayRenderEnabled, - systemOverlayTintEnabled = systemOverlayTintEnabled, - systemOverlayInputEnabled = systemOverlayInputEnabled, - ) - -private class OverlayDebugControlRootNode( - key: Any? = "dsgl-overlay-debug-root", -) : DOMNode(key) { - override val styleType: String = "dsgl-overlay-debug-root" - - private val scope = UiScope(this) - private val shadowNode: ContainerNode = - scope.div({ - this.key = "dsgl-overlay-debug-shadow" - }) - private val panelNode: ContainerNode = - scope.div({ - this.key = "dsgl-overlay-debug-panel" - }) - private val titleNode: TextNode = - scope.text(props = { - this.key = "dsgl-overlay-debug-title" - source = TextSource.Static("Overlay Debug") - style = { - textWrap = TextWrap.NoWrap - } - }) - - private val appRenderLabelNode: TextNode = labelNode("App Overlay Render", "dsgl-overlay-debug-label-app-render") - private val appTintLabelNode: TextNode = labelNode("App Overlay Tint", "dsgl-overlay-debug-label-app-tint") - private val appInputLabelNode: TextNode = labelNode("App Overlay Input", "dsgl-overlay-debug-label-app-input") - private val systemRenderLabelNode: TextNode = - labelNode("System Overlay Render", "dsgl-overlay-debug-label-system-render") - private val systemTintLabelNode: TextNode = labelNode("System Overlay Tint", "dsgl-overlay-debug-label-system-tint") - private val systemInputLabelNode: TextNode = - labelNode("System Overlay Input", "dsgl-overlay-debug-label-system-input") - - private val appRenderToggleNode: ButtonNode = toggleNode("dsgl-overlay-debug-toggle-app-render") - private val appTintToggleNode: ButtonNode = toggleNode("dsgl-overlay-debug-toggle-app-tint") - private val appInputToggleNode: ButtonNode = toggleNode("dsgl-overlay-debug-toggle-app-input") - private val systemRenderToggleNode: ButtonNode = toggleNode("dsgl-overlay-debug-toggle-system-render") - private val systemTintToggleNode: ButtonNode = toggleNode("dsgl-overlay-debug-toggle-system-tint") - private val systemInputToggleNode: ButtonNode = toggleNode("dsgl-overlay-debug-toggle-system-input") - - private val resetButtonNode: ButtonNode = - scope.button("Reset All", { - this.key = "dsgl-overlay-debug-reset" - style = { - textWrap = TextWrap.NoWrap - } - }) - - private val statusNode: TextNode = - scope.text(props = { - this.key = "dsgl-overlay-debug-status" - source = TextSource.Static("") - style = { - textWrap = TextWrap.NoWrap - } - }) - - private var layout: OverlayDebugControlLayout? = null - private var snapshot: OverlayLayerDebugSnapshot = - OverlayLayerDebugSnapshot( - applicationOverlayRenderEnabled = true, - applicationOverlayTintEnabled = false, - applicationOverlayInputEnabled = true, - systemOverlayRenderEnabled = true, - systemOverlayTintEnabled = false, - systemOverlayInputEnabled = true, - frameFps = 0, - frameTimeMs = 0f, - frameFpsWindow = 0, - frameTimeWindowMs = 0f, - ) - - fun bind(layout: OverlayDebugControlLayout, snapshot: OverlayLayerDebugSnapshot) { - this.layout = layout - this.snapshot = snapshot - } - - override fun measure(ctx: UiMeasureContext): Size = - Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - - override fun render( - ctx: UiMeasureContext, - x: Int, - y: Int, - width: Int, - height: Int, - ) { - bounds = Rect(x, y, width, height) - val localLayout = - layout ?: run { - hideAll(ctx) - return - } - - val panelRect = localLayout.panelRect - val shadowRect = Rect(panelRect.x + 2, panelRect.y + 2, panelRect.width, panelRect.height) - - shadowNode.backgroundColor = 0x5F000000 - shadowNode.border = Border.NONE - - panelNode.backgroundColor = 0xEE1A2230.toInt() - panelNode.border = Border.all(1, 0xFF5A6B80.toInt()) - - titleNode.color = 0xFFFFFFFF.toInt() - titleNode.fontSize = 16 - - applyLabelStyle(appRenderLabelNode) - applyLabelStyle(appTintLabelNode) - applyLabelStyle(appInputLabelNode) - applyLabelStyle(systemRenderLabelNode) - applyLabelStyle(systemTintLabelNode) - applyLabelStyle(systemInputLabelNode) - - configureToggle(appRenderToggleNode, snapshot.applicationOverlayRenderEnabled) - configureToggle(appTintToggleNode, snapshot.applicationOverlayTintEnabled) - configureToggle(appInputToggleNode, snapshot.applicationOverlayInputEnabled) - configureToggle(systemRenderToggleNode, snapshot.systemOverlayRenderEnabled) - configureToggle(systemTintToggleNode, snapshot.systemOverlayTintEnabled) - configureToggle(systemInputToggleNode, snapshot.systemOverlayInputEnabled) - - resetButtonNode.backgroundColor = 0xFF2E3A49.toInt() - resetButtonNode.border = Border.all(1, 0xFF6886A5.toInt()) - resetButtonNode.textColor = 0xFFFFFFFF.toInt() - resetButtonNode.fontSize = 14 - - val rApp = if (snapshot.applicationOverlayRenderEnabled) "A1" else "A0" - val rSys = if (snapshot.systemOverlayRenderEnabled) "S1" else "S0" - val iApp = if (snapshot.applicationOverlayInputEnabled) "A1" else "A0" - val iSys = if (snapshot.systemOverlayInputEnabled) "S1" else "S0" - val statusTextValue = - "R:$rApp/$rSys I:$iApp/$iSys " + - "FPS:${snapshot.frameFps} (${String.format(Locale.US, "%.1f", snapshot.frameTimeMs)}ms) " + - "AvgFPS:${snapshot.frameFpsWindow} (${String.format(Locale.US, "%.1f", snapshot.frameTimeWindowMs)}ms)" - statusNode.setText(statusTextValue) - statusNode.color = 0xFFBAC7D6.toInt() - statusNode.fontSize = 14 - - renderNode(ctx, shadowNode, shadowRect) - renderNode(ctx, panelNode, panelRect) - renderNode(ctx, titleNode, Rect(panelRect.x + 10, panelRect.y + 8, (panelRect.width - 20).coerceAtLeast(1), 18)) - - renderToggleRow(ctx, panelRect, localLayout.appOverlayRenderRect, appRenderLabelNode, appRenderToggleNode) - renderToggleRow(ctx, panelRect, localLayout.appOverlayTintRect, appTintLabelNode, appTintToggleNode) - renderToggleRow(ctx, panelRect, localLayout.appOverlayInputRect, appInputLabelNode, appInputToggleNode) - renderToggleRow( - ctx, - panelRect, - localLayout.systemOverlayRenderRect, - systemRenderLabelNode, - systemRenderToggleNode, - ) - renderToggleRow(ctx, panelRect, localLayout.systemOverlayTintRect, systemTintLabelNode, systemTintToggleNode) - renderToggleRow(ctx, panelRect, localLayout.systemOverlayInputRect, systemInputLabelNode, systemInputToggleNode) - - renderNode(ctx, resetButtonNode, localLayout.resetRect) - renderNode( - ctx, - statusNode, - Rect( - x = panelRect.x + 10, - y = panelRect.y + panelRect.height - 14, - width = (panelRect.width - 20).coerceAtLeast(1), - height = 14, - ), - ) - } - - private fun labelNode(text: String, key: Any): TextNode = - scope.text(props = { - this.key = key - source = TextSource.Static(text) - style = { - textWrap = TextWrap.NoWrap - } - }) - - private fun toggleNode(key: Any): ButtonNode = - scope.button("ON", { - this.key = key - style = { - textWrap = TextWrap.NoWrap - } - }) - - private fun applyLabelStyle(node: TextNode) { - node.color = 0xFFE0E9F2.toInt() - node.fontSize = 14 - } - - private fun configureToggle(node: ButtonNode, enabled: Boolean) { - node.text = if (enabled) "ON" else "OFF" - node.backgroundColor = if (enabled) 0xFF2F7D4E.toInt() else 0xFF7A2E3A.toInt() - node.border = Border.all(1, 0xFF9AB3C9.toInt()) - node.textColor = 0xFFFFFFFF.toInt() - node.fontSize = 14 - } - - private fun renderToggleRow( - ctx: UiMeasureContext, - panelRect: Rect, - toggleRect: Rect, - labelNode: TextNode, - toggleNode: ButtonNode, - ) { - val labelRect = - Rect( - x = panelRect.x + 10, - y = toggleRect.y + 2, - width = (toggleRect.x - (panelRect.x + 16)).coerceAtLeast(1), - height = (toggleRect.height - 2).coerceAtLeast(1), - ) - renderNode(ctx, labelNode, labelRect) - renderNode(ctx, toggleNode, toggleRect) - } - - private fun renderNode(ctx: UiMeasureContext, node: DOMNode, rect: Rect?) { - if (rect == null || rect.width <= 0 || rect.height <= 0) { - node.display = Display.None - node.render(ctx, 0, 0, 0, 0) - return - } - node.display = Display.Block - node.render(ctx, rect.x, rect.y, rect.width, rect.height) - } - - private fun hideAll(ctx: UiMeasureContext) { - children.forEach { child -> - child.display = Display.None - child.render(ctx, 0, 0, 0, 0) - } - } -} diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHostTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/DebugDomainHostsTests.kt similarity index 69% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHostTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/DebugDomainHostsTests.kt index d4ffa12..a04b5c5 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/OverlayDebugControlHostTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/DebugDomainHostsTests.kt @@ -1,7 +1,15 @@ package org.dreamfinity.dsgl.core.debug +import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.overlay.PortalEntry +import org.dreamfinity.dsgl.core.overlay.PortalEntryBounds +import org.dreamfinity.dsgl.core.overlay.PortalEntryId +import org.dreamfinity.dsgl.core.overlay.PortalEntryOrder +import org.dreamfinity.dsgl.core.overlay.PortalEntryPlacement +import org.dreamfinity.dsgl.core.overlay.PortalEntryState +import org.dreamfinity.dsgl.core.overlay.PortalFrameContext import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.StyleApplicationScope @@ -13,7 +21,7 @@ import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertTrue -class OverlayDebugControlHostTests { +class DebugDomainHostsTests { private val ctx = object : UiMeasureContext { override val fontHeight: Int = 9 @@ -36,21 +44,21 @@ class OverlayDebugControlHostTests { OverlayLayerDebugState.applicationOverlayInputEnabled = false OverlayLayerDebugState.systemOverlayRenderEnabled = false OverlayLayerDebugState.systemOverlayInputEnabled = false - val host = OverlayDebugControlHost() + val host = DebugDomainRootHost() host.render(ctx, 960, 540) val layout = host.debugLayout() val commands = host.paint(ctx) assertNotNull(layout) - assertTrue(commands.any { it is RenderCommand.DrawText && it.text == "Overlay Debug" }) + assertTrue(commands.any { it is RenderCommand.DrawText && it.text == "Debug Domain" }) assertTrue(host.handleMouseDown(layout.panelRect.x + 3, layout.panelRect.y + 3, MouseButton.LEFT)) assertTrue(host.handleMouseUp(layout.panelRect.x + 3, layout.panelRect.y + 3, MouseButton.LEFT)) } @Test fun `debug control host uses explicit debug style scope`() { - val host = OverlayDebugControlHost() + val host = DebugDomainRootHost() assertEquals(StyleApplicationScope.Debug, host.debugStyleScope) } @@ -62,10 +70,11 @@ class OverlayDebugControlHostTests { OverlayLayerDebugState.applicationOverlayInputEnabled = false OverlayLayerDebugState.systemOverlayRenderEnabled = false OverlayLayerDebugState.systemOverlayInputEnabled = false - val host = OverlayDebugControlHost() + val host = DebugDomainRootHost() host.render(ctx, 960, 540) val layout = host.debugLayout() ?: error("layout missing") + host.paint(ctx) assertTrue(host.handleMouseDown(layout.resetRect.x + 2, layout.resetRect.y + 2, MouseButton.LEFT)) assertTrue(OverlayLayerDebugState.applicationOverlayRenderEnabled) @@ -78,10 +87,11 @@ class OverlayDebugControlHostTests { fun `debug panel toggles mutate independent app and system overlay state`() { OverlayLayerDebugState.setControlsEnabledTestOverride(true) OverlayLayerDebugState.resetAll() - val host = OverlayDebugControlHost() + val host = DebugDomainRootHost() host.render(ctx, 960, 540) val layout = host.debugLayout() ?: error("layout missing") + host.paint(ctx) assertTrue( host.handleMouseDown( @@ -110,7 +120,7 @@ class OverlayDebugControlHostTests { fun `debug panel status shows fps and frame time`() { OverlayLayerDebugState.setControlsEnabledTestOverride(true) OverlayLayerDebugState.updateFrameTiming(0.025) - val host = OverlayDebugControlHost() + val host = DebugDomainRootHost() host.render(ctx, 960, 540) host.paint(ctx) @@ -120,7 +130,7 @@ class OverlayDebugControlHostTests { .filterIsInstance() val statusTexts = drawTexts - .filter { it.sourceKey == "dsgl-overlay-debug-status" } + .filter { it.sourceKey == "dsgl-debug-domain-status" } .map { it.text } val statusTextValue = assertNotNull( @@ -141,7 +151,7 @@ class OverlayDebugControlHostTests { fun `toggle button label updates immediately after state change`() { OverlayLayerDebugState.setControlsEnabledTestOverride(true) OverlayLayerDebugState.resetAll() - val host = OverlayDebugControlHost() + val host = DebugDomainRootHost() host.render(ctx, 960, 540) val layout = host.debugLayout() ?: error("layout missing") @@ -149,7 +159,7 @@ class OverlayDebugControlHostTests { host .paint(ctx) .filterIsInstance() - .lastOrNull { it.sourceKey == "dsgl-overlay-debug-toggle-app-render" } + .lastOrNull { it.sourceKey == "dsgl-debug-domain-toggle-app-render" } ?.text assertEquals("ON", initialText) @@ -166,7 +176,7 @@ class OverlayDebugControlHostTests { host .paint(ctx) .filterIsInstance() - .lastOrNull { it.sourceKey == "dsgl-overlay-debug-toggle-app-render" } + .lastOrNull { it.sourceKey == "dsgl-debug-domain-toggle-app-render" } ?.text assertEquals("OFF", updatedText) } @@ -186,7 +196,7 @@ class OverlayDebugControlHostTests { @Test fun `controls visibility obeys debug-only toggle`() { OverlayLayerDebugState.setControlsEnabledTestOverride(false) - val host = OverlayDebugControlHost() + val host = DebugDomainRootHost() host.render(ctx, 960, 540) assertTrue(host.paint(ctx).isEmpty()) @@ -195,6 +205,39 @@ class OverlayDebugControlHostTests { assertTrue(host.paint(ctx).isNotEmpty()) } + @Test + fun `debug portal host starts empty but participates as debug portal surface`() { + val host = DebugDomainPortalHost() + + host.onInputFrame(960, 540) + host.render(ctx, 960, 540) + + assertEquals(ScreenDomainSurfaces.DebugPortal, host.surface) + assertTrue(host.paint(ctx).isEmpty()) + assertFalse(host.handleMouseDown(12, 12, MouseButton.LEFT)) + assertTrue(host.debugActivePortalEntryIds.isEmpty()) + } + + @Test + fun `debug portal host dispatches registered portal entries`() { + val host = DebugDomainPortalHost() + val entry = FakeDebugPortalEntry() + host.debugRegisterPortalEntryForTests(entry) + entry.activate() + + host.onInputFrame(960, 540) + host.render(ctx, 960, 540) + val consumed = host.handleMouseDown(14, 14, MouseButton.LEFT) + val commands = host.paint(ctx) + + assertTrue(consumed) + assertEquals(1, entry.inputFrameCalls) + assertEquals(1, entry.renderCalls) + assertEquals(1, entry.mouseDownCalls) + assertEquals(listOf("debug.test"), host.debugActivePortalEntryIds) + assertTrue(commands.any { it is RenderCommand.DrawRect && it.color == 0xFF336699.toInt() }) + } + @Test fun `debug domain surfaces remain enabled in state even when app and system portals are disabled`() { OverlayLayerDebugState.applicationOverlayTintEnabled = false @@ -224,4 +267,49 @@ class OverlayDebugControlHostTests { OverlayLayerDebugState.snapshot(), ) } + + private class FakeDebugPortalEntry : PortalEntry { + override val state: PortalEntryState = + PortalEntryState( + id = PortalEntryId("debug.test"), + ownerToken = this, + surface = ScreenDomainSurfaces.DebugPortal, + order = PortalEntryOrder(zIndex = 0), + ) + override val node = null + var inputFrameCalls: Int = 0 + private set + var renderCalls: Int = 0 + private set + var mouseDownCalls: Int = 0 + private set + + fun activate() { + state.activate( + PortalEntryPlacement( + anchorBounds = Rect(10, 10, 20, 20), + bounds = + PortalEntryBounds( + viewportBounds = Rect(0, 0, 960, 540), + entryBounds = Rect(12, 12, 100, 80), + ), + ), + ) + } + + override fun onInputFrame(context: PortalFrameContext) { + inputFrameCalls += 1 + } + + override fun render(ctx: UiMeasureContext, width: Int, height: Int) { + renderCalls += 1 + } + + override fun paint(ctx: UiMeasureContext): List = listOf(RenderCommand.DrawRect(0, 0, 1, 1, 0xFF336699.toInt())) + + override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { + mouseDownCalls += 1 + return true + } + } } From b13ebde2f747be23ecbd58fea4ba42ed22e03d04 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Thu, 28 May 2026 23:17:50 +0300 Subject: [PATCH 70/78] removing SystemOverlayPanelDemoNode and associated tests; migrating functionality to floating window APIs for cleaner portal and domain management; --- .../demo/sections/OverviewSection.kt | 2 +- .../dsgl/mcForge1710/DsglScreenHost.kt | 79 ++- .../DsglScreenHostDomainOrchestrationTests.kt | 179 ++++++- core/detekt-baseline.xml | 7 - .../core/colorpicker/ColorPickerController.kt | 2 + .../ColorPickerInteractionSession.kt | 10 +- .../dom/elements/ColorPickerInlineNode.kt | 16 + ...plicationFloatingWindowPortalController.kt | 468 ++++++++++++++++++ .../core/overlay/ApplicationOverlayHost.kt | 18 +- .../core/overlay/input/LayerDomInputRouter.kt | 14 + .../overlay/system/SystemOverlayEntries.kt | 1 - .../core/overlay/system/SystemOverlayHost.kt | 146 +----- .../system/SystemOverlayPanelDemoNode.kt | 143 ------ .../colorpicker/ColorPickerInlineNodeTests.kt | 87 ++++ .../ApplicationFloatingWindowPortalTests.kt | 189 +++++++ .../overlay/LiveLayerInteractionPathTests.kt | 69 ++- .../SystemOverlayEntryInfrastructureTests.kt | 2 - .../SystemOverlayPanelDemoEntryTests.kt | 324 ------------ 18 files changed, 1086 insertions(+), 670 deletions(-) create mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationFloatingWindowPortalController.kt delete mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayPanelDemoNode.kt create mode 100644 core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationFloatingWindowPortalTests.kt delete mode 100644 core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayPanelDemoEntryTests.kt diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/OverviewSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/OverviewSection.kt index 7e5e96b..f73d918 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/OverviewSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/OverviewSection.kt @@ -40,7 +40,7 @@ fun UiScope.overviewSection( text("Press F6 to force stylesheet reload and rebuild after file edits.", { style = { color = DEMO_MUTED } }) - text("Press F10 to toggle the draggable overlay panel panel demo (text + button + image).", { + text("Press F10 to toggle the draggable Application portal window demo (text + button + image).", { style = { color = DEMO_MUTED } }) diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index 81cdf25..ec08588 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -38,11 +38,13 @@ import org.dreamfinity.dsgl.core.overlay.handlePortalKeyDownBeforeDom import org.dreamfinity.dsgl.core.overlay.handlePortalPointerAfterDom import org.dreamfinity.dsgl.core.overlay.handlePortalPointerBeforeDom import org.dreamfinity.dsgl.core.overlay.hasActiveColorPickerEyedropper +import org.dreamfinity.dsgl.core.overlay.hasDomPointerTargetAt import org.dreamfinity.dsgl.core.overlay.hasOpenColorPickerPortal import org.dreamfinity.dsgl.core.overlay.hasOpenContextMenuPortal import org.dreamfinity.dsgl.core.overlay.hasOpenSelectPortal import org.dreamfinity.dsgl.core.overlay.syncPortalFrame import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayHost +import org.dreamfinity.dsgl.core.overlay.toggleFloatingWindowDemo import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.* import org.lwjgl.input.Keyboard @@ -202,14 +204,6 @@ abstract class DsglScreenHost( dsglMouseX = frameCursor.mouseX, dsglMouseY = frameCursor.mouseY, ) - val commands = - paintApplicationRootOrFallback( - tree = tree, - stylesAlreadyApplied = layoutPhase.stylesAlreadyApplied, - mouseX = mouseX, - mouseY = mouseY, - partialTicks = partialTicks, - ) ?: return syncFeatureRuntimeFrame( tree = tree, dsglMouseX = frameCursor.mouseX, @@ -237,6 +231,14 @@ abstract class DsglScreenHost( systemOverlayInputEnabled = overlayState.systemOverlayInputEnabled, inspectorBlocks = overlayState.inspectorBlocks, ) + val commands = + paintApplicationRootOrFallback( + tree = tree, + stylesAlreadyApplied = layoutPhase.stylesAlreadyApplied, + mouseX = mouseX, + mouseY = mouseY, + partialTicks = partialTicks, + ) ?: return stageApplicationOverlayCommands( tree = tree, applicationOverlayCommands = applicationOverlayCommands, @@ -509,6 +511,10 @@ abstract class DsglScreenHost( appOverlayInputEnabled && !inspectorBlocks && applicationOverlayHost.hasOpenContextMenuPortal() val selectBlocks = appOverlayInputEnabled && !inspectorBlocks && applicationOverlayHost.hasOpenSelectPortal() + val applicationPortalDomBlocks = + appOverlayInputEnabled && + !inspectorBlocks && + applicationOverlayHost.hasDomPointerTargetAt(dsglMouseX, dsglMouseY) val systemSelectBlocks = systemOverlayInputEnabled && systemOverlayHost.hasOpenPortal() val inlineSamplerOwnsSession = activeColorSamplerOwner is ActiveColorSamplerOwner.Inline val colorPickerBlocks = @@ -521,7 +527,13 @@ abstract class DsglScreenHost( !inlineSamplerOwnsSession ) ) - if (!inspectorBlocks && !contextMenuBlocks && !selectBlocks && !systemSelectBlocks && !colorPickerBlocks) { + var applicationRootFrameBlocked = inspectorBlocks + applicationRootFrameBlocked = applicationRootFrameBlocked || contextMenuBlocks + applicationRootFrameBlocked = applicationRootFrameBlocked || selectBlocks + applicationRootFrameBlocked = applicationRootFrameBlocked || applicationPortalDomBlocks + applicationRootFrameBlocked = applicationRootFrameBlocked || systemSelectBlocks + applicationRootFrameBlocked = applicationRootFrameBlocked || colorPickerBlocks + if (!applicationRootFrameBlocked) { DndRuntime.engine.onMouseMove(tree.root, dsglMouseX, dsglMouseY) } DndRuntime.engine.onFrame(tree.root, dtSeconds) @@ -529,8 +541,8 @@ abstract class DsglScreenHost( val prevY = if (lastMoveY == Int.MIN_VALUE) dsglMouseY else lastMoveY val dx = dsglMouseX - prevX val dy = dsglMouseY - prevY - if (inspectorBlocks || contextMenuBlocks || selectBlocks || systemSelectBlocks || colorPickerBlocks) { - clearHoverChainStates() + if (applicationRootFrameBlocked) { + clearHoverChainStates(postLeaveEvents = true, mouseX = dsglMouseX, mouseY = dsglMouseY) hoverTarget = null } else { updateHover(tree.root, hoverChain, dsglMouseX, dsglMouseY, dx, dy) @@ -833,7 +845,7 @@ abstract class DsglScreenHost( if (keyCode == Keyboard.KEY_F10) { val demoAnchorX = if (lastMoveX == Int.MIN_VALUE) inspectorMouseX else lastMoveX val demoAnchorY = if (lastMoveY == Int.MIN_VALUE) inspectorMouseY else lastMoveY - systemOverlayHost.togglePanelDemo(demoAnchorX, demoAnchorY) + applicationOverlayHost.toggleFloatingWindowDemo(demoAnchorX, demoAnchorY) mc.dispatchKeypresses() return true } @@ -1451,6 +1463,8 @@ abstract class DsglScreenHost( eventButton = -1 clearActiveTarget() releaseDragCapture() + clearHoverChainStates(postLeaveEvents = true, mouseX = mouseX, mouseY = mouseY) + hoverTarget = null lastMouseX = mouseX lastMouseY = mouseY } @@ -1617,6 +1631,8 @@ abstract class DsglScreenHost( internal fun debugDispatchApplicationPortalThenRootPointerForTests( mouseButton: Int, buttonPressed: Boolean, + mouseX: Int = 0, + mouseY: Int = 0, applicationPortalConsumes: () -> Boolean, applicationRootConsumes: () -> Boolean, ): ScreenDomainSurface? { @@ -1624,8 +1640,8 @@ abstract class DsglScreenHost( DomainPointerDispatchContext( inputEvent = MouseInputEvent( - mouseX = 0, - mouseY = 0, + mouseX = mouseX, + mouseY = mouseY, dWheel = 0, mouseButton = mouseButton, ), @@ -1651,13 +1667,36 @@ abstract class DsglScreenHost( ) if (consumedBy != null && consumedBy != ScreenDomainSurfaces.ApplicationRoot) { updateHigherSurfacePointerOwnership(context) + consumeOverlayPointerState(mouseX, mouseY) } else if (consumedBy == null && isHigherSurfaceOwnedPointerRelease(context)) { higherSurfacePointerButton = -1 + consumeOverlayPointerState(mouseX, mouseY) return ScreenDomainSurfaces.ApplicationPortal } return consumedBy } + internal fun debugApplicationOverlayHostForTests(): ApplicationOverlayHost = applicationOverlayHost + + internal fun debugUpdateFrameInteractionStateForTests( + tree: DomTree, + mouseX: Int, + mouseY: Int, + appOverlayInputEnabled: Boolean = true, + systemOverlayInputEnabled: Boolean = true, + inspectorBlocks: Boolean = false, + ) { + updateFrameInteractionState( + tree = tree, + dtSeconds = 1.0 / 60.0, + dsglMouseX = mouseX, + dsglMouseY = mouseY, + appOverlayInputEnabled = appOverlayInputEnabled, + systemOverlayInputEnabled = systemOverlayInputEnabled, + inspectorBlocks = inspectorBlocks, + ) + } + private fun setDragCapture(target: DOMNode) { dragCaptureTarget = target dragCaptureKey = target.key @@ -1807,9 +1846,19 @@ abstract class DsglScreenHost( return false } - private fun clearHoverChainStates() { + private fun clearHoverChainStates( + postLeaveEvents: Boolean = false, + mouseX: Int = lastMoveX, + mouseY: Int = lastMoveY, + ) { hoverChain.forEach { node -> node.setHoveredState(false) + if (postLeaveEvents) { + val leave = MouseLeaveEvent(mouseX, mouseY) + leave.target = node + EventBus.post(leave) + node.onmouseleave?.invoke(leave) + } } hoverChain.clear() } diff --git a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt index ca04daa..c4b56be 100644 --- a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt +++ b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt @@ -2,16 +2,42 @@ package org.dreamfinity.dsgl.mcForge1710 import org.dreamfinity.dsgl.core.DomTree import org.dreamfinity.dsgl.core.DsglWindow +import org.dreamfinity.dsgl.core.colorpicker.ColorFormatMode +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerController +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerLayout +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerState +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerStyle +import org.dreamfinity.dsgl.core.colorpicker.RgbaColor +import org.dreamfinity.dsgl.core.dom.applyParent +import org.dreamfinity.dsgl.core.dom.elements.ButtonNode +import org.dreamfinity.dsgl.core.dom.elements.ColorPickerInlineNode import org.dreamfinity.dsgl.core.dom.elements.ContainerNode +import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.event.EventBus +import org.dreamfinity.dsgl.core.event.Events +import org.dreamfinity.dsgl.core.event.MouseLeaveEvent +import org.dreamfinity.dsgl.core.event.MouseMoveEvent import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurface import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces +import org.dreamfinity.dsgl.core.overlay.toggleFloatingWindowDemo import org.dreamfinity.dsgl.core.render.RenderCommand import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull +import org.junit.Assert.assertSame import org.junit.Test class DsglScreenHostDomainOrchestrationTests { + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } + @Test fun `host domain paint orchestration uses six surface render order`() { val host = createHost() @@ -168,12 +194,139 @@ class DsglScreenHostDomainOrchestrationTests { assertFalse(rootReceivedRelease) } - private fun createHost(): DsglScreenHost = + @Test + fun `application portal dom target blocks application root frame hover`() { + val root = ContainerNode(key = "root").apply { bounds = Rect(0, 0, 1280, 720) } + val rootButton = + ButtonNode("Root", key = "root-button") + .apply { bounds = Rect(240, 180, 520, 300) } + .applyParent(root) + var leaveCount = 0 + EventBus.run { + rootButton.addEventListener(Events.MOUSELEAVE) { _: MouseLeaveEvent -> + leaveCount++ + } + } + val tree = DomTree(root) + val host = createHost(tree) + val applicationOverlay = host.debugApplicationOverlayHostForTests() + + host.debugUpdateFrameInteractionStateForTests(tree, mouseX = 300, mouseY = 230) + assertSame(rootButton, host.debugHoverTargetForTests()) + assertEquals(0, leaveCount) + + applicationOverlay.onInputFrame(1280, 720) + applicationOverlay.toggleFloatingWindowDemo(anchorX = 260, anchorY = 200) + applicationOverlay.render(ctx, 1280, 720) + val portalBodyX = 540 + val portalBodyY = 370 + + host.debugUpdateFrameInteractionStateForTests( + tree = tree, + mouseX = portalBodyX, + mouseY = portalBodyY, + ) + assertNull(host.debugHoverTargetForTests()) + assertEquals(1, leaveCount) + } + + @Test + fun `application portal live pointer consumption clears application root hover immediately`() { + val root = ContainerNode(key = "root").apply { bounds = Rect(0, 0, 1280, 720) } + val rootButton = + ButtonNode("Root", key = "root-button") + .apply { bounds = Rect(240, 180, 520, 300) } + .applyParent(root) + var leaveCount = 0 + EventBus.run { + rootButton.addEventListener(Events.MOUSELEAVE) { _: MouseLeaveEvent -> + leaveCount++ + } + } + val tree = DomTree(root) + val host = createHost(tree) + + host.debugUpdateFrameInteractionStateForTests(tree, mouseX = 300, mouseY = 230) + assertSame(rootButton, host.debugHoverTargetForTests()) + + val consumedBy = + host.debugDispatchApplicationPortalThenRootPointerForTests( + mouseButton = -1, + buttonPressed = false, + mouseX = 540, + mouseY = 370, + applicationPortalConsumes = { true }, + applicationRootConsumes = { true }, + ) + + assertEquals(ScreenDomainSurfaces.ApplicationPortal, consumedBy) + assertNull(host.debugHoverTargetForTests()) + assertEquals(1, leaveCount) + } + + @Test + fun `application root frame hover still updates outside application portal dom target`() { + val root = ContainerNode(key = "root").apply { bounds = Rect(0, 0, 1280, 720) } + val rootButton = + ButtonNode("Root", key = "root-button") + .apply { bounds = Rect(620, 500, 120, 32) } + .applyParent(root) + val tree = DomTree(root) + val host = createHost(tree) + val applicationOverlay = host.debugApplicationOverlayHostForTests() + + applicationOverlay.onInputFrame(1280, 720) + applicationOverlay.toggleFloatingWindowDemo(anchorX = 260, anchorY = 200) + applicationOverlay.render(ctx, 1280, 720) + + host.debugUpdateFrameInteractionStateForTests(tree, mouseX = 630, mouseY = 510) + + assertSame(rootButton, host.debugHoverTargetForTests()) + } + + @Test + fun `application portal dom target clears inline color picker hover before root paint`() { + val root = ContainerNode(key = "root").apply { bounds = Rect(0, 0, 1280, 720) } + val picker = + ColorPickerInlineNode( + controlled = true, + value = RgbaColor.WHITE, + mode = ColorFormatMode.RGB, + alphaEnabled = true, + key = "picker", + ).applyParent(root) + val tree = DomTree(root) + val host = createHost(tree) + val applicationOverlay = host.debugApplicationOverlayHostForTests() + val probeLayout = colorPickerLayoutProbe() + val style = ColorPickerStyle() + val hoverX = probeLayout.copyRect.x + 2 + val hoverY = probeLayout.copyRect.y + 2 + + tree.render(ctx, 1280, 720) + host.debugUpdateFrameInteractionStateForTests(tree, mouseX = hoverX, mouseY = hoverY) + EventBus.post(MouseMoveEvent(hoverX, hoverY, hoverX - 1, hoverY - 1).also { it.target = picker }) + assertRenderColorPresent(tree.paint(ctx), style.buttonHoverColor) + + applicationOverlay.onInputFrame(1280, 720) + applicationOverlay.toggleFloatingWindowDemo(anchorX = hoverX, anchorY = hoverY) + applicationOverlay.render(ctx, 1280, 720) + host.debugUpdateFrameInteractionStateForTests(tree, mouseX = hoverX, mouseY = hoverY) + + assertNull(host.debugHoverTargetForTests()) + assertRenderColorAbsent(tree.paint(ctx), style.buttonHoverColor) + } + + private fun createHost(): DsglScreenHost = createHost(DomTree(ContainerNode(key = "root"))) + + private fun createHost(tree: DomTree): DsglScreenHost = object : DsglScreenHost( object : DsglWindow() { - override fun render(): DomTree = DomTree(ContainerNode(key = "root")) + override fun render(): DomTree = tree }, - ) {} + ) {}.also { host -> + host.debugBindTreeForTests(tree, needsLayout = false) + } private fun command(color: Int): RenderCommand = RenderCommand.DrawRect(0, 0, 1, 1, color) @@ -181,4 +334,24 @@ class DsglScreenHostDomainOrchestrationTests { commands.map { command -> (command as RenderCommand.DrawRect).color } + + private fun assertRenderColorPresent(commands: List, color: Int) { + assertEquals(true, commands.any { command -> command is RenderCommand.DrawRect && command.color == color }) + } + + private fun assertRenderColorAbsent(commands: List, color: Int) { + assertEquals(false, commands.any { command -> command is RenderCommand.DrawRect && command.color == color }) + } + + private fun colorPickerLayoutProbe(): ColorPickerLayout = + ColorPickerController( + initial = + ColorPickerState( + color = RgbaColor.WHITE, + previous = RgbaColor.WHITE, + mode = ColorFormatMode.RGB, + alphaEnabled = true, + closeOnSelect = false, + ), + ).buildLayout(Rect(0, 0, 1280, 392)) } diff --git a/core/detekt-baseline.xml b/core/detekt-baseline.xml index a2c2ef9..84eb9db 100644 --- a/core/detekt-baseline.xml +++ b/core/detekt-baseline.xml @@ -127,7 +127,6 @@ LongMethod:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$private fun renderHighlights(scope: UiScope, ctx: UiMeasureContext) LongMethod:SystemOverlayInspectorNativeEntryTests.kt$SystemOverlayInspectorNativeEntryTests$@Test fun `inspector clipped body blocks hidden row input and accepts visible portion`() LongMethod:SystemOverlayInspectorNativeEntryTests.kt$SystemOverlayInspectorNativeEntryTests$@Test fun `inspector wheel scrolling remains symmetric across rebuilds`() - LongMethod:SystemOverlayPanelDemoEntryTests.kt$SystemOverlayPanelDemoEntryTests$@Test fun `panel panel demo remains stable across open drag body click drag close reopen sequence`() LongMethod:TextPerformanceHotPathCharacterizationTests.kt$TextPerformanceHotPathCharacterizationTests$@Test fun `cache boundaries are explicit for wrapped layout paths`() LongParameterList:ColorPickerInlineNode.kt$ColorPickerInlineNode$( controlled: Boolean = false, value: RgbaColor? = null, defaultValue: RgbaColor = RgbaColor.WHITE, previousValue: RgbaColor? = null, mode: ColorFormatMode = ColorFormatMode.HEX, alphaEnabled: Boolean = true, key: Any? = null, ) LongParameterList:ColorPickerPopupPaneNode.kt$ColorPickerPopupPaneNode$( controlled: Boolean = false, value: RgbaColor? = null, defaultValue: RgbaColor = RgbaColor.WHITE, previousValue: RgbaColor? = null, mode: ColorFormatMode = ColorFormatMode.HEX, alphaEnabled: Boolean = true, key: Any? = null, ) @@ -365,12 +364,6 @@ MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$64 MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$86 MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$96 - MagicNumber:SystemOverlayHost.kt$SystemOverlayHost.OverlayPanelDemoOverlayEntry$190 - MagicNumber:SystemOverlayHost.kt$SystemOverlayHost.OverlayPanelDemoOverlayEntry$300 - MagicNumber:SystemOverlayPanelDemoNode.kt$SystemOverlayPanelDemoNode.DemoBodyNode$120 - MagicNumber:SystemOverlayPanelDemoNode.kt$SystemOverlayPanelDemoNode.DemoBodyNode$24 - MagicNumber:SystemOverlayPanelDemoNode.kt$SystemOverlayPanelDemoNode.DemoBodyNode$44 - MagicNumber:SystemOverlayPanelDemoNode.kt$SystemOverlayPanelDemoNode.DemoBodyNode$6 MagicNumber:TextAreaNode.kt$TextAreaNode$17L MagicNumber:TextAreaNode.kt$TextAreaNode$31L MagicNumber:TextAreaNode.kt$TextAreaNode$6 diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt index 85ce5fc..c0a1e2e 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt @@ -141,6 +141,8 @@ class ColorPickerController( internal fun viewHoverPosition(): Pair = hoverX to hoverY + internal fun clearHover(): Boolean = interaction.clearHover() + internal fun viewModeDropdownOpen(): Boolean = modeDropdownOpen internal fun viewActiveInputKey(): String? = activeInputKey diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInteractionSession.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInteractionSession.kt index 58d85a3..7288308 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInteractionSession.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInteractionSession.kt @@ -48,6 +48,13 @@ internal class ColorPickerInteractionSession { hoverY = y } + fun clearHover(): Boolean { + if (hoverX == Int.MIN_VALUE && hoverY == Int.MIN_VALUE) return false + hoverX = Int.MIN_VALUE + hoverY = Int.MIN_VALUE + return true + } + fun clearDragTarget() { dragTarget = ColorPickerDragTarget.None } @@ -58,7 +65,6 @@ internal class ColorPickerInteractionSession { clearDragTarget() modeDropdownOpen = false textInput.clear() - hoverX = Int.MIN_VALUE - hoverY = Int.MIN_VALUE + clearHover() } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerInlineNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerInlineNode.kt index a452c98..1497203 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerInlineNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerInlineNode.kt @@ -87,6 +87,7 @@ class ColorPickerInlineNode( } } this@ColorPickerInlineNode.addEventListener(Events.MOUSEMOVE) { event: MouseMoveEvent -> + if (!shouldAcceptPointerHoverEvent(event.target)) return@addEventListener val currentLayout = layout ?: return@addEventListener val moved = controller.handleMouseMove(event.mouseX, event.mouseY, currentLayout) if (moved) { @@ -106,11 +107,17 @@ class ColorPickerInlineNode( } } this@ColorPickerInlineNode.addEventListener(Events.MOUSEOVER) { event: MouseOverEvent -> + if (!shouldAcceptPointerHoverEvent(event.target)) return@addEventListener val currentLayout = layout ?: return@addEventListener if (controller.handleMouseMove(event.mouseX, event.mouseY, currentLayout)) { markRenderCommandsDirty() } } + this@ColorPickerInlineNode.addEventListener(Events.MOUSELEAVE) { _: MouseLeaveEvent -> + if (controller.clearHover()) { + markRenderCommandsDirty() + } + } this@ColorPickerInlineNode.addEventListener(Events.KEYDOWN) { event: KeyboardKeyDownEvent -> if (!FocusManager.isFocused(this@ColorPickerInlineNode)) return@addEventListener if (controller.handleKeyDown(event.keyCode, event.keyChar)) { @@ -125,6 +132,15 @@ class ColorPickerInlineNode( override fun shouldCapturePointerDrag(mouseX: Int, mouseY: Int): Boolean = dragCaptured || containsGlobalPoint(mouseX, mouseY) + private fun shouldAcceptPointerHoverEvent(target: DOMNode?): Boolean { + if (dragCaptured || controller.isEyedropperActive()) return true + var current = target ?: return false + while (true) { + if (current === this) return true + current = current.parent ?: return false + } + } + fun wantsGlobalPointerInput(): Boolean = controller.isEyedropperActive() fun appendEyedropperOverlayCommands(viewportWidth: Int, viewportHeight: Int, out: MutableList) { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationFloatingWindowPortalController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationFloatingWindowPortalController.kt new file mode 100644 index 0000000..48c4bf2 --- /dev/null +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationFloatingWindowPortalController.kt @@ -0,0 +1,468 @@ +package org.dreamfinity.dsgl.core.overlay + +import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.dom.applyParent +import org.dreamfinity.dsgl.core.dom.elements.ButtonNode +import org.dreamfinity.dsgl.core.dom.elements.ImageNode +import org.dreamfinity.dsgl.core.dom.elements.TextNode +import org.dreamfinity.dsgl.core.dom.elements.TextSource +import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.dom.layout.Size +import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanel +import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelDragSession +import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelState +import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelStyle + +internal class ApplicationFloatingWindowPortalController { + private val portalHost: PortalHost = PortalHost(ScreenDomainSurfaces.ApplicationPortal) + private val entry: ApplicationFloatingWindowPortalEntry = ApplicationFloatingWindowPortalEntry() + private var viewportWidth: Int = 1 + private var viewportHeight: Int = 1 + + init { + portalHost.register(entry) + } + + fun toggle(anchorX: Int, anchorY: Int) { + entry.toggle(anchorX, anchorY, viewportWidth, viewportHeight) + } + + val open: Boolean + get() = entry.isOpen() + + fun onInputFrame(viewportWidth: Int, viewportHeight: Int) { + this.viewportWidth = viewportWidth.coerceAtLeast(1) + this.viewportHeight = viewportHeight.coerceAtLeast(1) + portalHost.onInputFrame(PortalFrameContext(Rect(0, 0, this.viewportWidth, this.viewportHeight))) + } + + fun onFrameCursor( + viewportWidth: Int, + viewportHeight: Int, + mouseX: Int, + mouseY: Int, + ) { + this.viewportWidth = viewportWidth.coerceAtLeast(1) + this.viewportHeight = viewportHeight.coerceAtLeast(1) + entry.updateActiveDrag( + mouseX = mouseX, + mouseY = mouseY, + viewportWidth = this.viewportWidth, + viewportHeight = this.viewportHeight, + ) + } + + fun sync(rootNode: DOMNode, viewportWidth: Int, viewportHeight: Int) { + this.viewportWidth = viewportWidth.coerceAtLeast(1) + this.viewportHeight = viewportHeight.coerceAtLeast(1) + entry.sync(this.viewportWidth, this.viewportHeight) + reconcileMountedNode(rootNode) + } + + fun close() { + entry.close() + } + + fun clearRefs() { + entry.close() + detachEntry() + } + + internal fun debugNode(): ApplicationFloatingWindowNode = entry.node + + internal fun debugState(): PortalEntryState = entry.state + + private fun reconcileMountedNode(rootNode: DOMNode) { + val activeNodes = portalHost.entriesInPaintOrder().mapNotNull { it.node } + if (entry.node !in activeNodes) { + detachEntry() + } + activeNodes.forEach { node -> + if (node.parent !== rootNode) { + node.parent + ?.children + ?.remove(node) + node.parent = rootNode + } + } + rootNode.children.removeAll(activeNodes.toSet()) + rootNode.children.addAll(activeNodes) + } + + private fun detachEntry() { + entry.node.parent + ?.children + ?.remove(entry.node) + entry.node.parent = null + } +} + +private class ApplicationFloatingWindowPortalEntry : PortalEntry { + private val panelState: OverlayPanelState = OverlayPanelState() + private val dragSession: OverlayPanelDragSession = OverlayPanelDragSession() + private val overlayPanel: OverlayPanel = + OverlayPanel( + ownerId = "application.f10-floating-window", + panelState = panelState, + dragSession = dragSession, + ) + override val node: ApplicationFloatingWindowNode = + ApplicationFloatingWindowNode( + overlayPanel = overlayPanel, + onPositionChanged = panelState::updateFromRect, + onCaptureCancelled = dragSession::end, + ) + override val state: PortalEntryState = + PortalEntryState( + id = PortalEntryId("application.f10-floating-window"), + ownerToken = this, + surface = ScreenDomainSurfaces.ApplicationPortal, + order = PortalEntryOrder(zIndex = 100), + dismissPolicy = PortalDismissPolicy.None, + inputPolicy = PortalInputPolicy.DomOnly, + focusPolicy = PortalFocusPolicy.Preserve, + insidePointerPolicy = PortalInsidePointerPolicy.ConsumePointerDown, + ) + private var opened: Boolean = false + + fun toggle( + anchorX: Int, + anchorY: Int, + viewportWidth: Int, + viewportHeight: Int, + ) { + if (opened) { + close() + return + } + val width = PANEL_WIDTH + val height = PANEL_HEIGHT + val maxX = (viewportWidth - width - PANEL_MARGIN).coerceAtLeast(PANEL_MARGIN) + val maxY = (viewportHeight - height - PANEL_MARGIN).coerceAtLeast(PANEL_MARGIN) + val x = anchorX.coerceIn(PANEL_MARGIN, maxX) + val y = anchorY.coerceIn(PANEL_MARGIN, maxY) + panelState.updateFromRect(Rect(x, y, width, height)) + opened = true + activate(viewportWidth, viewportHeight) + } + + fun isOpen(): Boolean = opened + + fun sync(viewportWidth: Int, viewportHeight: Int) { + if (!opened) { + panelState.hide() + dragSession.end() + state.deactivate() + return + } + overlayPanel.configure( + title = "Application Portal", + draggable = true, + style = OverlayPanelStyle(fontSize = 16), + onClose = ::close, + ) + overlayPanel.syncPanelRect(panelState.currentRectOrNull()) + activate(viewportWidth, viewportHeight) + } + + fun updateActiveDrag( + mouseX: Int, + mouseY: Int, + viewportWidth: Int, + viewportHeight: Int, + ) { + if (!opened) return + if (!overlayPanel.isDragging()) return + node.updateActiveDrag( + mouseX = mouseX, + mouseY = mouseY, + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + ) + activate(viewportWidth, viewportHeight) + } + + override fun close() { + opened = false + panelState.hide() + dragSession.end() + state.deactivate() + } + + private fun activate(viewportWidth: Int, viewportHeight: Int) { + val panelRect = panelState.currentRectOrNull() ?: return + state.activate( + PortalEntryPlacement( + anchorBounds = null, + bounds = + PortalEntryBounds( + viewportBounds = Rect(0, 0, viewportWidth.coerceAtLeast(1), viewportHeight.coerceAtLeast(1)), + entryBounds = panelRect, + ), + ), + ) + } + + private companion object { + const val PANEL_WIDTH: Int = 300 + const val PANEL_HEIGHT: Int = 190 + const val PANEL_MARGIN: Int = 2 + } +} + +internal class ApplicationFloatingWindowNode( + private val overlayPanel: OverlayPanel, + private val onPositionChanged: (Rect) -> Unit, + private val onCaptureCancelled: () -> Unit, + key: Any? = "dsgl-application-f10-floating-window", +) : DOMNode(key) { + override val styleType: String = "dsgl-application-f10-floating-window" + + private val hitTargetNode: DOMNode = + FloatingWindowPanelHitNode( + overlayPanel = overlayPanel, + viewportBoundsProvider = { viewportBounds }, + onPositionChanged = onPositionChanged, + onCaptureCancelled = onCaptureCancelled, + onDragUpdated = ::invalidatePanelRenderCommands, + ).applyParent(this) + private val panelNode: DOMNode = overlayPanel.node().applyParent(this) + private val bodyNode: FloatingWindowBodyNode = + FloatingWindowBodyNode().also(overlayPanel::setBodyContent) + private var viewportBounds: Rect = Rect(0, 0, 1, 1) + + fun currentButtonClicks(): Int = bodyNode.currentButtonClicks() + + fun buttonRect(): Rect? = bodyNode.buttonRect() + + fun panelRect(): Rect? = overlayPanel.panelRect() + + fun bodyRect(): Rect? = overlayPanel.bodyRect() + + override fun measure(ctx: UiMeasureContext): Size = + Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) + + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { + viewportBounds = Rect(x, y, width.coerceAtLeast(1), height.coerceAtLeast(1)) + bounds = viewportBounds + val panelRect = overlayPanel.panelRect() + if (panelRect == null) { + hitTargetNode.render(ctx, 0, 0, 0, 0) + } else { + hitTargetNode.render(ctx, panelRect.x, panelRect.y, panelRect.width, panelRect.height) + } + panelNode.render(ctx, x, y, width, height) + } + + fun updateActiveDrag( + mouseX: Int, + mouseY: Int, + viewportWidth: Int, + viewportHeight: Int, + ) { + if ( + overlayPanel.handleMouseMove( + mouseX = mouseX, + mouseY = mouseY, + viewportWidth = viewportWidth.coerceAtLeast(1), + viewportHeight = viewportHeight.coerceAtLeast(1), + onDragRectChanged = onPositionChanged, + ) + ) { + invalidatePanelRenderCommands() + } + } + + private fun invalidatePanelRenderCommands() { + requestRenderCommandsInvalidation() + hitTargetNode.requestRenderCommandsInvalidation() + panelNode.requestRenderCommandsInvalidation() + } +} + +private class FloatingWindowPanelHitNode( + private val overlayPanel: OverlayPanel, + private val viewportBoundsProvider: () -> Rect, + private val onPositionChanged: (Rect) -> Unit, + private val onCaptureCancelled: () -> Unit, + private val onDragUpdated: () -> Unit, + key: Any? = "dsgl-application-f10-floating-window-hit-target", +) : DOMNode(key) { + override val styleType: String = "dsgl-application-f10-floating-window-hit-target" + + init { + onMouseMove = { it.cancelled = true } + onMouseDown = { it.cancelled = true } + onMouseUp = { it.cancelled = true } + onMouseClick = { it.cancelled = true } + onMouseWheel = { it.cancelled = true } + } + + override fun measure(ctx: UiMeasureContext): Size = + Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) + + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { + bounds = Rect(x, y, width, height) + } + + override fun shouldCapturePointerDrag(mouseX: Int, mouseY: Int): Boolean { + val header = overlayPanel.headerRect() ?: return false + val close = overlayPanel.closeRect() + return header.contains(mouseX, mouseY) && close?.contains(mouseX, mouseY) != true + } + + override fun beginPointerCapture(mouseX: Int, mouseY: Int, button: MouseButton) { + if (button != MouseButton.LEFT) return + overlayPanel.beginHeaderDrag(mouseX, mouseY) + } + + override fun continuePointerCapture( + mouseX: Int, + mouseY: Int, + mouseDX: Int, + mouseDY: Int, + button: MouseButton, + ) { + if (button != MouseButton.LEFT) return + updateActiveDrag(mouseX, mouseY) + } + + override fun endPointerCapture(mouseX: Int, mouseY: Int, button: MouseButton) { + if (button != MouseButton.LEFT) return + val viewport = viewportBoundsProvider() + if ( + overlayPanel.handleMouseUp( + mouseX = mouseX, + mouseY = mouseY, + button = button, + viewportWidth = viewport.width.coerceAtLeast(1), + viewportHeight = viewport.height.coerceAtLeast(1), + onDragRectChanged = onPositionChanged, + ) + ) { + onDragUpdated() + } + } + + override fun cancelPointerCapture() { + onCaptureCancelled() + } + + private fun updateActiveDrag(mouseX: Int, mouseY: Int) { + val viewport = viewportBoundsProvider() + if ( + overlayPanel.handleMouseMove( + mouseX = mouseX, + mouseY = mouseY, + viewportWidth = viewport.width.coerceAtLeast(1), + viewportHeight = viewport.height.coerceAtLeast(1), + onDragRectChanged = onPositionChanged, + ) + ) { + onDragUpdated() + } + } +} + +private class FloatingWindowBodyNode( + key: Any? = "dsgl-application-f10-floating-window-body", +) : DOMNode(key) { + override val styleType: String = "dsgl-application-f10-floating-window-body" + + private val titleNode: TextNode = + TextNode(TextSource.Static("Reusable panel demo"), key = "application-f10-title").applyParent(this) + private val counterNode: TextNode = + TextNode(TextSource.Static("Button clicks: 0"), key = "application-f10-counter").applyParent(this) + private val actionButton: ButtonNode = + ButtonNode("Click me", key = "application-f10-button") + .apply { + onClick { + buttonClicks += 1 + syncCounter() + it.cancelled = true + } + }.applyParent(this) + private val imageNode: ImageNode = + ImageNode( + url = "minecraft:textures/gui/options_background.png", + imageWidth = IMAGE_SIZE, + imageHeight = IMAGE_SIZE, + key = "application-f10-image", + ).applyParent(this) + private val hintNode: TextNode = + TextNode(TextSource.Static("Drag the title bar to move."), key = "application-f10-hint").applyParent(this) + private var buttonClicks: Int = 0 + + fun currentButtonClicks(): Int = buttonClicks + + fun buttonRect(): Rect? = actionButton.bounds.takeIf { it.width > 0 && it.height > 0 } + + override fun measure(ctx: UiMeasureContext): Size = + Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) + + override fun render( + ctx: UiMeasureContext, + x: Int, + y: Int, + width: Int, + height: Int, + ) { + bounds = Rect(x, y, width, height) + titleNode.render( + ctx, + bounds.x + CONTENT_PADDING, + bounds.y + TITLE_TOP, + bounds.width - IMAGE_COLUMN_WIDTH, + TEXT_HEIGHT, + ) + counterNode.render( + ctx, + bounds.x + CONTENT_PADDING, + bounds.y + COUNTER_TOP, + bounds.width - IMAGE_COLUMN_WIDTH, + TEXT_HEIGHT, + ) + actionButton.render(ctx, bounds.x + CONTENT_PADDING, bounds.y + BUTTON_TOP, BUTTON_WIDTH, BUTTON_HEIGHT) + imageNode.render(ctx, bounds.x + bounds.width - IMAGE_OFFSET, bounds.y + IMAGE_TOP, IMAGE_SIZE, IMAGE_SIZE) + hintNode.render( + ctx, + bounds.x + CONTENT_PADDING, + bounds.y + HINT_TOP, + bounds.width - CONTENT_PADDING * 2, + TEXT_HEIGHT, + ) + } + + private fun syncCounter() { + counterNode.setText("Button clicks: $buttonClicks") + } + + private companion object { + const val CONTENT_PADDING: Int = 6 + const val TITLE_TOP: Int = 4 + const val COUNTER_TOP: Int = 22 + const val TEXT_HEIGHT: Int = 18 + const val BUTTON_TOP: Int = 44 + const val BUTTON_WIDTH: Int = 120 + const val BUTTON_HEIGHT: Int = 24 + const val IMAGE_SIZE: Int = 44 + const val IMAGE_TOP: Int = 6 + const val IMAGE_OFFSET: Int = 52 + const val IMAGE_COLUMN_WIDTH: Int = 64 + const val HINT_TOP: Int = 78 + } +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt index 2a11a84..1e3a0c6 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt @@ -28,7 +28,7 @@ class ApplicationOverlayHost( root = rootNode, styleScope = StyleApplicationScope.Application, ) - private val domInputRouter: LayerDomInputRouter = + internal val domInputRouter: LayerDomInputRouter = LayerDomInputRouter( rootProvider = { rootNode }, ) @@ -43,6 +43,8 @@ class ApplicationOverlayHost( internal val applicationColorPickerPortal: ColorPickerPortalController = ColorPickerPortalController(colorPickerEngine) internal val modalPortal: ModalPortalController = ModalPortalController() + internal val floatingWindowPortal: ApplicationFloatingWindowPortalController = + ApplicationFloatingWindowPortalController() private var modalPortalWasActive: Boolean = false override fun onInputFrame(viewportWidth: Int, viewportHeight: Int) { @@ -50,10 +52,12 @@ class ApplicationOverlayHost( width = viewportWidth.coerceAtLeast(1), height = viewportHeight.coerceAtLeast(1), ) + floatingWindowPortal.onInputFrame(viewportWidth, viewportHeight) } override fun render(ctx: UiMeasureContext, width: Int, height: Int) { rootNode.setViewportBounds(width, height) + floatingWindowPortal.sync(rootNode, width, height) modalPortal.sync(rootNode, width, height) closeStaleFloatingPortalsAfterModalOpen() tree.render(ctx, width, height) @@ -88,6 +92,7 @@ class ApplicationOverlayHost( applicationSelectPortal.close() applicationColorPickerPortal.close() modalPortal.close() + floatingWindowPortal.clearRefs() modalPortalWasActive = false } @@ -113,6 +118,7 @@ fun ApplicationOverlayHost.syncPortalFrame( contextMenuPortal.onFrame(measureContext, viewportWidth, viewportHeight, viewportScale) applicationSelectPortal.onFrame(measureContext, viewportWidth, viewportHeight, viewportScale) applicationColorPickerPortal.onFrame(viewportWidth, viewportHeight, mouseX, mouseY) + floatingWindowPortal.onFrameCursor(viewportWidth, viewportHeight, mouseX, mouseY) } fun ApplicationOverlayHost.appendPortalOverlayCommands( @@ -130,6 +136,7 @@ fun ApplicationOverlayHost.closeFloatingPortals() { contextMenuPortal.close() applicationSelectPortal.close() applicationColorPickerPortal.close() + floatingWindowPortal.close() } fun ApplicationOverlayHost.hasOpenContextMenuPortal(): Boolean = contextMenuPortal.isOpen() @@ -144,6 +151,15 @@ fun ApplicationOverlayHost.captureColorPickerEyedropperSample() { applicationColorPickerPortal.captureEyedropperSample() } +fun ApplicationOverlayHost.toggleFloatingWindowDemo(anchorX: Int, anchorY: Int) { + floatingWindowPortal.toggle(anchorX, anchorY) +} + +fun ApplicationOverlayHost.isFloatingWindowDemoOpen(): Boolean = floatingWindowPortal.open + +fun ApplicationOverlayHost.hasDomPointerTargetAt(mouseX: Int, mouseY: Int): Boolean = + domInputRouter.hasPointerTargetAt(mouseX, mouseY) + fun ApplicationOverlayHost.handlePortalKeyDownBeforeDom(keyCode: Int, keyChar: Char): Boolean = applicationColorPickerPortal.handleKeyDown(keyCode, keyChar) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouter.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouter.kt index cc70244..e8cce97 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouter.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouter.kt @@ -176,6 +176,20 @@ class LayerDomInputRouter( return true } + fun hasPointerTargetAt(mouseX: Int, mouseY: Int): Boolean { + val root = rootProvider() ?: return false + if (dragCaptureTarget != null) return true + val chain = ArrayList(hoverChain.size + 4) + return collectHoverChainLocal( + root = root, + mouseX = mouseX, + mouseY = mouseY, + parentTransform = AffineTransform2D.IDENTITY, + parentInputClipRect = null, + out = chain, + ) + } + fun clear() { clearHoverChainStates() hoverTarget = null diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt index c16984d..ffffad7 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt @@ -21,7 +21,6 @@ enum class SystemOverlayEntryId { Inspector, ColorPickerPopup, ColorPickerTransient, - PanelDemo, TransientSession, } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt index 7288c53..33cf624 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt @@ -39,10 +39,9 @@ class SystemOverlayHost( private val inspectorEntry: SystemOverlayEntry = InspectorOverlayEntry(inspectorController) private val colorPickerEntry: ColorPickerOverlayEntry = ColorPickerOverlayEntry() private val colorPickerTransientEntry: SystemOverlayEntry = ColorPickerTransientOverlayEntry(colorPickerEntry) - private val overlayPanelDemoEntry: OverlayPanelDemoOverlayEntry = OverlayPanelDemoOverlayEntry() private val entryRegistry: SystemOverlayEntryRegistry = SystemOverlayEntryRegistry( - listOf(inspectorEntry, colorPickerEntry, colorPickerTransientEntry, overlayPanelDemoEntry), + listOf(inspectorEntry, colorPickerEntry, colorPickerTransientEntry), ) private val portalHost: PortalHost = PortalHost(ScreenDomainSurfaces.SystemPortal) @@ -90,12 +89,6 @@ class SystemOverlayHost( colorPickerEntry.captureEyedropperSample() } - fun togglePanelDemo(anchorX: Int, anchorY: Int) { - overlayPanelDemoEntry.toggle(anchorX, anchorY, knownViewportWidth, knownViewportHeight) - } - - fun isOverlayPanelDemoOpen(): Boolean = overlayPanelDemoEntry.isOpen() - fun syncPortalFrame( measureContext: UiMeasureContext, viewportWidth: Int, @@ -217,7 +210,6 @@ class SystemOverlayHost( tree.clearRefs() transientOwnershipRegistry.clear() colorPickerEntry.close() - overlayPanelDemoEntry.close() systemSelectPortal.close() portalEntries.forEach { it.syncPlacement(knownViewportWidth, knownViewportHeight) } domInputRouter.clear() @@ -626,142 +618,6 @@ class SystemOverlayHost( } } - private class OverlayPanelDemoOverlayEntry : SystemOverlayEntry { - override val state: SystemOverlayEntryState = - SystemOverlayEntryState( - id = SystemOverlayEntryId.PanelDemo, - order = 300, - lane = SystemOverlayLane.PanelContent, - ) - private val overlayPanel: OverlayPanel = - OverlayPanel( - ownerId = state.id, - panelState = state.panelState, - dragSession = state.dragSession, - ) - private val demoNode: SystemOverlayPanelDemoNode = SystemOverlayPanelDemoNode(overlayPanel) - override val node: DOMNode = demoNode - private var opened: Boolean = false - private var viewportWidth: Int = 1 - private var viewportHeight: Int = 1 - private var buttonClicks: Int = 0 - - fun toggle( - anchorX: Int, - anchorY: Int, - viewportWidth: Int, - viewportHeight: Int, - ) { - if (opened) { - close() - return - } - this.viewportWidth = viewportWidth.coerceAtLeast(1) - this.viewportHeight = viewportHeight.coerceAtLeast(1) - val width = 300 - val height = 190 - val maxX = (this.viewportWidth - width - 2).coerceAtLeast(2) - val maxY = (this.viewportHeight - height - 2).coerceAtLeast(2) - val x = anchorX.coerceIn(2, maxX) - val y = anchorY.coerceIn(2, maxY) - state.panelState.updateFromRect(Rect(x, y, width, height)) - opened = true - state.active = true - } - - fun close() { - opened = false - state.active = false - state.dragSession.end() - state.panelState.hide() - } - - fun isOpen(): Boolean = opened - - override fun sync(frame: SystemOverlayFrameContext) { - state.active = opened - if (!state.active) { - state.panelState.hide() - state.dragSession.end() - return - } - overlayPanel.configure( - title = "Overlay PanelF", - draggable = true, - style = OverlayPanelStyle(fontSize = 16), - onClose = ::close, - ) - overlayPanel.syncPanelRect(state.panelState.currentRectOrNull()) - demoNode.setButtonClicks(buttonClicks) - overlayPanel.handleMouseMove( - mouseX = frame.cursorX, - mouseY = frame.cursorY, - viewportWidth = viewportWidth, - viewportHeight = viewportHeight, - ) { rect -> - state.panelState.updateFromRect(rect) - } - } - - override fun onInputFrame(viewportWidth: Int, viewportHeight: Int) { - this.viewportWidth = viewportWidth - this.viewportHeight = viewportHeight - } - - override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean { - if (!state.active) return false - if (overlayPanel.handleMouseMove( - mouseX = mouseX, - mouseY = mouseY, - viewportWidth = viewportWidth, - viewportHeight = viewportHeight, - ) { rect -> - state.panelState.updateFromRect(rect) - } - ) { - return true - } - val panelRect = state.panelState.currentRectOrNull() ?: return false - return panelRect.contains(mouseX, mouseY) - } - - override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { - if (!state.active) return false - if (overlayPanel.handleMouseDown(mouseX, mouseY, button)) { - return true - } - val panelRect = state.panelState.currentRectOrNull() ?: return false - if (!panelRect.contains(mouseX, mouseY)) { - return false - } - val buttonRect = demoNode.buttonRect() - if (button == MouseButton.LEFT && buttonRect != null && buttonRect.contains(mouseX, mouseY)) { - buttonClicks += 1 - demoNode.setButtonClicks(buttonClicks) - return true - } - return true - } - - override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { - if (!state.active) return false - if (overlayPanel.handleMouseUp( - mouseX = mouseX, - mouseY = mouseY, - button = button, - viewportWidth = viewportWidth, - viewportHeight = viewportHeight, - ) { rect -> - state.panelState.updateFromRect(rect) - } - ) { - return true - } - val panelRect = state.panelState.currentRectOrNull() ?: return false - return panelRect.contains(mouseX, mouseY) - } - } - private companion object { private fun inspectorPanelStyle(): OverlayPanelStyle = OverlayPanelStyle( diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayPanelDemoNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayPanelDemoNode.kt deleted file mode 100644 index 3a01398..0000000 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayPanelDemoNode.kt +++ /dev/null @@ -1,143 +0,0 @@ -package org.dreamfinity.dsgl.core.overlay.system - -import org.dreamfinity.dsgl.core.dom.DOMNode -import org.dreamfinity.dsgl.core.dom.applyParent -import org.dreamfinity.dsgl.core.dom.layout.Rect -import org.dreamfinity.dsgl.core.dom.layout.Size -import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext -import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanel -import org.dreamfinity.dsgl.core.render.RenderCommand - -internal class SystemOverlayPanelDemoNode( - private val overlayPanel: OverlayPanel, - key: Any? = "dsgl-system-panel-panel-demo", -) : DOMNode(key) { - override val styleType: String = "dsgl-system-panel-panel-demo" - - private val panelNode: DOMNode = overlayPanel.node().applyParent(this) - private val bodyNode: DemoBodyNode = DemoBodyNode().also(overlayPanel::setBodyContent) - - fun setButtonClicks(value: Int) { - bodyNode.setButtonClicks(value) - } - - fun currentButtonClicks(): Int = bodyNode.currentButtonClicks() - - fun buttonRect(): Rect? = bodyNode.buttonRect() - - override fun measure(ctx: UiMeasureContext): Size = - Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - - override fun render( - ctx: UiMeasureContext, - x: Int, - y: Int, - width: Int, - height: Int, - ) { - bounds = Rect(x, y, width, height) - panelNode.render(ctx, x, y, width, height) - } - - private class DemoBodyNode( - key: Any? = "dsgl-system-panel-demo-body", - ) : DOMNode(key) { - override val styleType: String = "dsgl-system-panel-demo-body" - - private val commandBuffer: MutableList = ArrayList(64) - private var renderCommandsRevision: Long = 0L - private var buttonClicks: Int = 0 - private var actionButtonRect: Rect? = null - - fun setButtonClicks(value: Int) { - buttonClicks = value - } - - fun currentButtonClicks(): Int = buttonClicks - - fun buttonRect(): Rect? = actionButtonRect - - override fun measure(ctx: UiMeasureContext): Size = - Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) - - override fun render( - ctx: UiMeasureContext, - x: Int, - y: Int, - width: Int, - height: Int, - ) { - bounds = Rect(x, y, width, height) - commandBuffer.clear() - actionButtonRect = null - commandBuffer += - RenderCommand.DrawText( - text = "Reusable panel demo", - x = bounds.x + 6, - y = bounds.y + 4, - color = 0xFFFFFFFF.toInt(), - fontSize = 16, - ) - commandBuffer += - RenderCommand.DrawText( - text = "Button clicks: $buttonClicks", - x = bounds.x + 6, - y = bounds.y + 22, - color = 0xFFB8C9DA.toInt(), - fontSize = 16, - ) - val buttonRect = Rect(bounds.x + 6, bounds.y + 44, 120, 24) - actionButtonRect = buttonRect - commandBuffer += - RenderCommand.DrawRect( - buttonRect.x, - buttonRect.y, - buttonRect.width, - buttonRect.height, - 0xFF314154.toInt(), - ) - drawBorder(commandBuffer, buttonRect, 0xFF6F879E.toInt()) - commandBuffer += - RenderCommand.DrawText( - text = "Click me", - x = buttonRect.x + 8, - y = buttonRect.y + 4, - color = 0xFFFFFFFF.toInt(), - fontSize = 16, - ) - commandBuffer += - RenderCommand.DrawImage( - resource = "minecraft:textures/gui/options_background.png", - x = bounds.x + bounds.width - 52, - y = bounds.y + 6, - width = 44, - height = 44, - ) - commandBuffer += - RenderCommand.DrawText( - text = "Drag the title bar to move.", - x = bounds.x + 6, - y = bounds.y + 78, - color = 0xFFB8C9DA.toInt(), - fontSize = 16, - ) - if (SystemOverlayCommandDslRenderer.rebuildInto(this, commandBuffer, "system-panel-panel-demo-body")) { - renderCommandsRevision += 1L - markRenderCommandsDirty() - } - children.forEach { child -> - child.render(ctx, bounds.x, bounds.y, bounds.width, bounds.height) - } - } - - override fun volatileRenderCommandsSignature(nowMs: Long): Long = renderCommandsRevision - - private fun drawBorder(out: MutableList, rect: Rect, color: Int) { - if (rect.width <= 0 || rect.height <= 0) return - out += RenderCommand.DrawRect(rect.x, rect.y, rect.width, 1, color) - out += RenderCommand.DrawRect(rect.x, rect.y + rect.height - 1, rect.width, 1, color) - out += RenderCommand.DrawRect(rect.x, rect.y, 1, rect.height, color) - out += RenderCommand.DrawRect(rect.x + rect.width - 1, rect.y, 1, rect.height, color) - } - } -} diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInlineNodeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInlineNodeTests.kt index 67e2b37..6001b9a 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInlineNodeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInlineNodeTests.kt @@ -7,7 +7,9 @@ import org.dreamfinity.dsgl.core.event.EventBus import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.event.MouseDownEvent import org.dreamfinity.dsgl.core.event.MouseDragEvent +import org.dreamfinity.dsgl.core.event.MouseLeaveEvent import org.dreamfinity.dsgl.core.event.MouseMoveEvent +import org.dreamfinity.dsgl.core.event.MouseOverEvent import org.dreamfinity.dsgl.core.event.MouseUpEvent import org.dreamfinity.dsgl.core.render.RenderCommand import kotlin.test.Test @@ -120,6 +122,71 @@ class ColorPickerInlineNodeTests { assertTrue(modeTexts.size >= 5) } + @Test + fun `inline picker clears custom hover state on mouse leave`() { + val picker = + ColorPickerInlineNode( + controlled = true, + value = RgbaColor.WHITE, + mode = ColorFormatMode.RGB, + alphaEnabled = true, + key = "picker", + ).apply { + closeOnSelect = false + } + picker.render(ctx, 0, 0, 350, 392) + val probeLayout = layoutProbe(mode = ColorFormatMode.RGB, alphaEnabled = true) + val style = ColorPickerStyle() + val copyX = probeLayout.copyRect.x + 2 + val copyY = probeLayout.copyRect.y + 2 + + EventBus.post(MouseMoveEvent(copyX, copyY, copyX - 1, copyY - 1).also { it.target = picker }) + + assertEquals(style.buttonHoverColor, fillForRect(buildCommands(picker), probeLayout.copyRect)) + + EventBus.post(MouseLeaveEvent(copyX, copyY).also { it.target = picker }) + + assertEquals(style.buttonBackgroundColor, fillForRect(buildCommands(picker), probeLayout.copyRect)) + } + + @Test + fun `inline picker ignores targetless hover events`() { + val picker = + ColorPickerInlineNode( + controlled = true, + value = RgbaColor.WHITE, + mode = ColorFormatMode.RGB, + alphaEnabled = true, + key = "picker", + ).apply { + closeOnSelect = false + } + picker.render(ctx, 0, 0, 350, 392) + val probeLayout = layoutProbe(mode = ColorFormatMode.RGB, alphaEnabled = true) + val style = ColorPickerStyle() + val inputX = + probeLayout.inputSlots + .first() + .inputRect.x + 2 + val inputY = + probeLayout.inputSlots + .first() + .inputRect.y + 2 + + EventBus.post(MouseMoveEvent(inputX, inputY, inputX - 1, inputY - 1)) + EventBus.post(MouseOverEvent(inputX, inputY)) + + assertEquals( + style.inputBorderColor, + borderColorForRect( + buildCommands(picker), + probeLayout.inputSlots + .first() + .inputRect, + ), + ) + } + @Test fun `inline picker draws eyedropper overlay after clicking pipette`() { val picker = @@ -331,6 +398,26 @@ class ColorPickerInlineNodeTests { return out } + private fun fillForRect(commands: List, rect: Rect): Int = + commands + .filterIsInstance() + .first { command -> + command.x == rect.x && + command.y == rect.y && + command.width == rect.width && + command.height == rect.height + }.color + + private fun borderColorForRect(commands: List, rect: Rect): Int = + commands + .filterIsInstance() + .first { command -> + command.x == rect.x && + command.y == rect.y && + command.width == rect.width && + command.height == 1 + }.color + private fun layoutProbe(mode: ColorFormatMode, alphaEnabled: Boolean): ColorPickerLayout = ColorPickerController( initial = diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationFloatingWindowPortalTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationFloatingWindowPortalTests.kt new file mode 100644 index 0000000..dc02ab5 --- /dev/null +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationFloatingWindowPortalTests.kt @@ -0,0 +1,189 @@ +package org.dreamfinity.dsgl.core.overlay + +import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.render.RenderCommand +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertSame +import kotlin.test.assertTrue + +class ApplicationFloatingWindowPortalTests { + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } + + @Test + fun `F10 floating window toggles through application portal and keeps stable identity while open`() { + val host = ApplicationOverlayHost() + + host.onInputFrame(1280, 720) + host.toggleFloatingWindowDemo(anchorX = 160, anchorY = 120) + host.render(ctx, 1280, 720) + val firstNode = host.floatingWindowPortal.debugNode() + val firstState = host.floatingWindowPortal.debugState() + + assertTrue(host.isFloatingWindowDemoOpen()) + assertTrue(firstState.active) + assertEquals("application.f10-floating-window", firstState.id.value) + assertTrue(firstNode.parent === host.rootNode) + + host.render(ctx, 1280, 720) + assertSame(firstNode, host.floatingWindowPortal.debugNode()) + assertSame(firstState, host.floatingWindowPortal.debugState()) + + host.toggleFloatingWindowDemo(anchorX = 160, anchorY = 120) + host.render(ctx, 1280, 720) + assertFalse(host.isFloatingWindowDemoOpen()) + assertFalse(firstState.active) + assertTrue(firstNode.parent == null) + } + + @Test + fun `F10 floating window supports DOM button click and pointer-captured drag`() { + val host = ApplicationOverlayHost() + + host.onInputFrame(1280, 720) + host.toggleFloatingWindowDemo(anchorX = 220, anchorY = 160) + host.render(ctx, 1280, 720) + val node = host.floatingWindowPortal.debugNode() + val before = node.panelRect() ?: error("panel missing") + val buttonRect = node.buttonRect() ?: error("button missing") + + assertTrue(host.handleMouseDown(buttonRect.x + 1, buttonRect.y + 1, MouseButton.LEFT)) + assertTrue(host.handleMouseUp(buttonRect.x + 1, buttonRect.y + 1, MouseButton.LEFT)) + host.render(ctx, 1280, 720) + assertEquals(1, node.currentButtonClicks()) + assertNotNull(node.buttonRect()) + + val headerStartX = before.x + 10 + val headerStartY = before.y + 10 + assertTrue(host.handleMouseDown(headerStartX, headerStartY, MouseButton.LEFT)) + assertTrue(host.handleMouseMove(headerStartX + 60, headerStartY + 30)) + assertTrue(host.handleMouseMove(headerStartX + 180, headerStartY + 70)) + assertTrue(host.handleMouseUp(headerStartX + 180, headerStartY + 70, MouseButton.LEFT)) + host.render(ctx, 1280, 720) + + val moved = node.panelRect() ?: error("panel missing") + assertTrue(moved.x > before.x) + assertTrue(moved.y > before.y) + assertSame(node, host.floatingWindowPortal.debugNode()) + } + + @Test + fun `F10 floating window consumes body hover and pointer input without blocking child controls`() { + val host = ApplicationOverlayHost() + + host.onInputFrame(1280, 720) + host.toggleFloatingWindowDemo(anchorX = 220, anchorY = 160) + host.render(ctx, 1280, 720) + val node = host.floatingWindowPortal.debugNode() + val bodyRect = node.bodyRect() ?: error("body missing") + val bodyX = bodyRect.x + bodyRect.width - 8 + val bodyY = bodyRect.y + bodyRect.height - 8 + + assertTrue(host.handleMouseMove(bodyX, bodyY)) + assertTrue(host.handleMouseDown(bodyX, bodyY, MouseButton.LEFT)) + assertTrue(host.handleMouseUp(bodyX, bodyY, MouseButton.LEFT)) + assertEquals(0, node.currentButtonClicks()) + + val buttonRect = node.buttonRect() ?: error("button missing") + assertTrue(host.handleMouseDown(buttonRect.x + 1, buttonRect.y + 1, MouseButton.LEFT)) + assertTrue(host.handleMouseUp(buttonRect.x + 1, buttonRect.y + 1, MouseButton.LEFT)) + assertEquals(1, node.currentButtonClicks()) + } + + @Test + fun `F10 floating window does not consume outside panel input`() { + val host = ApplicationOverlayHost() + + host.onInputFrame(1280, 720) + host.toggleFloatingWindowDemo(anchorX = 220, anchorY = 160) + host.render(ctx, 1280, 720) + val panelRect = + host.floatingWindowPortal + .debugNode() + .panelRect() ?: error("panel missing") + val outsideX = panelRect.x + panelRect.width + 12 + val outsideY = panelRect.y + panelRect.height + 12 + + assertFalse(host.handleMouseMove(outsideX, outsideY)) + assertFalse(host.handleMouseDown(outsideX, outsideY, MouseButton.LEFT)) + } + + @Test + fun `F10 floating window follows frame cursor during active drag`() { + val host = ApplicationOverlayHost() + + host.onInputFrame(1280, 720) + host.toggleFloatingWindowDemo(anchorX = 220, anchorY = 160) + host.render(ctx, 1280, 720) + val node = host.floatingWindowPortal.debugNode() + val before = node.panelRect() ?: error("panel missing") + val headerStartX = before.x + 10 + val headerStartY = before.y + 10 + + assertTrue(host.handleMouseDown(headerStartX, headerStartY, MouseButton.LEFT)) + host.syncPortalFrame( + measureContext = ctx, + viewportWidth = 1280, + viewportHeight = 720, + viewportScale = 1f, + mouseX = headerStartX + 90, + mouseY = headerStartY + 40, + ) + host.render(ctx, 1280, 720) + + val moved = node.panelRect() ?: error("panel missing") + assertTrue(moved.x > before.x) + assertTrue(moved.y > before.y) + } + + @Test + fun `F10 floating window close button closes and reopen restores interactions`() { + val host = ApplicationOverlayHost() + + host.onInputFrame(1280, 720) + host.toggleFloatingWindowDemo(anchorX = 280, anchorY = 180) + host.render(ctx, 1280, 720) + val node = host.floatingWindowPortal.debugNode() + val rect = node.panelRect() ?: error("panel missing") + val closeX = rect.x + rect.width - 4 - 16 + 1 + val closeY = rect.y + 4 + 1 + + assertTrue(host.handleMouseDown(closeX, closeY, MouseButton.LEFT)) + assertTrue(host.handleMouseUp(closeX, closeY, MouseButton.LEFT)) + host.render(ctx, 1280, 720) + assertFalse(host.isFloatingWindowDemoOpen()) + assertTrue(node.parent == null) + + host.toggleFloatingWindowDemo(anchorX = 280, anchorY = 180) + host.render(ctx, 1280, 720) + assertTrue(host.isFloatingWindowDemoOpen()) + assertSame(node, host.floatingWindowPortal.debugNode()) + assertTrue(node.parent === host.rootNode) + } + + @Test + fun `F10 floating window uses render viewport before first mouse input`() { + val host = ApplicationOverlayHost() + + host.render(ctx, 1280, 720) + host.toggleFloatingWindowDemo(anchorX = 460, anchorY = 320) + host.render(ctx, 1280, 720) + + val rect = + host.floatingWindowPortal + .debugNode() + .panelRect() ?: error("panel missing") + assertEquals(460, rect.x) + assertEquals(320, rect.y) + } +} diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt index da90c94..95c119a 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt @@ -15,10 +15,8 @@ import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController -import org.dreamfinity.dsgl.core.overlay.DomainPortalServices import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayEntryId import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayHost -import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayPanelDemoNode import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.select.SelectEntry import org.dreamfinity.dsgl.core.select.SelectOpenRequest @@ -186,11 +184,12 @@ class LiveLayerInteractionPathTests { } @Test - fun `system overlay consumption prevents lower-layer fallthrough`() { - val systemHost = SystemOverlayHost(InspectorController()) + fun `system portal consumption prevents lower-domain fallthrough`() { + val inspector = InspectorController() + val systemHost = SystemOverlayHost(inspector) val root = inspectedRoot() systemHost.onInputFrame(1280, 720) - systemHost.togglePanelDemo(anchorX = 240, anchorY = 180) + inspector.toggle() systemHost.syncFrame( root, inspectedLayoutRevision = 1L, @@ -198,9 +197,10 @@ class LiveLayerInteractionPathTests { cursorY = 186, inspectorPointerCaptured = false, ) + systemHost.render(ctx, 1280, 720) - val entryState = systemHost.debugEntryState(SystemOverlayEntryId.PanelDemo) ?: error("panel demo state missing") - val panelRect = entryState.panelState.currentRectOrNull() ?: error("panel demo rect missing") + val entryState = systemHost.debugEntryState(SystemOverlayEntryId.Inspector) ?: error("inspector state missing") + val panelRect = entryState.panelState.currentRectOrNull() ?: error("inspector panel rect missing") val fixture = LiveLayerInputFixture( debugHandler = { _, _, _ -> false }, @@ -712,30 +712,20 @@ class LiveLayerInteractionPathTests { } @Test - fun `rendered system overlay content is reachable through same live interaction path`() { - val systemHost = SystemOverlayHost(InspectorController()) - val root = inspectedRoot() - systemHost.onInputFrame(1280, 720) - systemHost.togglePanelDemo(anchorX = 260, anchorY = 200) - systemHost.syncFrame( - root, - inspectedLayoutRevision = 1L, - cursorX = 260, - cursorY = 200, - inspectorPointerCaptured = false, - ) - systemHost.render(ctx, 1280, 720) + fun `F10 application portal content is reachable through same live interaction path`() { + val applicationOverlayHost = ApplicationOverlayHost() + applicationOverlayHost.onInputFrame(1280, 720) + applicationOverlayHost.toggleFloatingWindowDemo(anchorX = 260, anchorY = 200) + applicationOverlayHost.render(ctx, 1280, 720) - val demoNode = - systemHost.debugEntryNode(SystemOverlayEntryId.PanelDemo) as? SystemOverlayPanelDemoNode - ?: error("panel demo node missing") + val demoNode = applicationOverlayHost.floatingWindowPortal.debugNode() val buttonRect = demoNode.buttonRect() assertNotNull(buttonRect) val fixture = LiveLayerInputFixture( debugHandler = { _, _, _ -> false }, - systemOverlayHandler = { x, y, button -> systemHost.handleMouseDown(x, y, button) }, - applicationOverlayHandler = { _, _, _ -> false }, + systemOverlayHandler = { _, _, _ -> false }, + applicationOverlayHandler = { x, y, button -> applicationOverlayHost.handleMouseDown(x, y, button) }, ) var appRootReceived = false val consumedBy = @@ -744,7 +734,34 @@ class LiveLayerInteractionPathTests { true } - assertEquals(ScreenDomainSurfaces.SystemPortal, consumedBy) + assertEquals(ScreenDomainSurfaces.ApplicationPortal, consumedBy) + assertFalse(appRootReceived) + } + + @Test + fun `F10 application portal body blocks app-root fallthrough`() { + val applicationOverlayHost = ApplicationOverlayHost() + applicationOverlayHost.onInputFrame(1280, 720) + applicationOverlayHost.toggleFloatingWindowDemo(anchorX = 260, anchorY = 200) + applicationOverlayHost.render(ctx, 1280, 720) + + val demoNode = applicationOverlayHost.floatingWindowPortal.debugNode() + val bodyRect = demoNode.bodyRect() + assertNotNull(bodyRect) + val fixture = + LiveLayerInputFixture( + debugHandler = { _, _, _ -> false }, + systemOverlayHandler = { _, _, _ -> false }, + applicationOverlayHandler = { x, y, button -> applicationOverlayHost.handleMouseDown(x, y, button) }, + ) + var appRootReceived = false + val consumedBy = + fixture.dispatchMouseDown(bodyRect.x + bodyRect.width - 8, bodyRect.y + bodyRect.height - 8, MouseButton.LEFT) { + appRootReceived = true + true + } + + assertEquals(ScreenDomainSurfaces.ApplicationPortal, consumedBy) assertFalse(appRootReceived) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntryInfrastructureTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntryInfrastructureTests.kt index 3043b4f..21083f7 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntryInfrastructureTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntryInfrastructureTests.kt @@ -27,7 +27,6 @@ class SystemOverlayEntryInfrastructureTests { SystemOverlayEntryId.Inspector, SystemOverlayEntryId.ColorPickerPopup, SystemOverlayEntryId.ColorPickerTransient, - SystemOverlayEntryId.PanelDemo, ), host.debugRegisteredEntryIds(), ) @@ -36,7 +35,6 @@ class SystemOverlayEntryInfrastructureTests { "system.Inspector", "system.ColorPickerPopup", "system.ColorPickerTransient", - "system.PanelDemo", ), host.debugRegisteredPortalEntryIds(), ) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayPanelDemoEntryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayPanelDemoEntryTests.kt deleted file mode 100644 index d6ab3db..0000000 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayPanelDemoEntryTests.kt +++ /dev/null @@ -1,324 +0,0 @@ -package org.dreamfinity.dsgl.core.overlay.system - -import org.dreamfinity.dsgl.core.dom.applyParent -import org.dreamfinity.dsgl.core.dom.elements.ContainerNode -import org.dreamfinity.dsgl.core.dom.layout.Rect -import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext -import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.inspector.InspectorController -import org.dreamfinity.dsgl.core.render.RenderCommand -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertSame -import kotlin.test.assertTrue - -class SystemOverlayPanelDemoEntryTests { - private val ctx = - object : UiMeasureContext { - override val fontHeight: Int = 9 - - override fun measureText(text: String): Int = text.length * 6 - - override fun paint(commands: List) = Unit - } - - @Test - fun `panel panel demo entry toggles mounts and keeps stable identity while open`() { - val host = SystemOverlayHost(InspectorController()) - val root = inspectedRoot() - - host.onInputFrame(1280, 720) - host.togglePanelDemo(anchorX = 160, anchorY = 120) - host.syncFrame( - root, - inspectedLayoutRevision = 1L, - cursorX = 162, - cursorY = 122, - inspectorPointerCaptured = false, - ) - val firstNode = host.debugEntryNode(SystemOverlayEntryId.PanelDemo) ?: error("node missing") - val firstState = host.debugEntryState(SystemOverlayEntryId.PanelDemo) ?: error("state missing") - assertTrue(firstState.active) - assertTrue(host.debugMountedEntryIds().contains(SystemOverlayEntryId.PanelDemo)) - - host.syncFrame( - root, - inspectedLayoutRevision = 2L, - cursorX = 170, - cursorY = 134, - inspectorPointerCaptured = false, - ) - val secondNode = host.debugEntryNode(SystemOverlayEntryId.PanelDemo) ?: error("node missing") - val secondState = host.debugEntryState(SystemOverlayEntryId.PanelDemo) ?: error("state missing") - assertSame(firstNode, secondNode) - assertSame(firstState, secondState) - - host.togglePanelDemo(anchorX = 160, anchorY = 120) - host.syncFrame( - root, - inspectedLayoutRevision = 3L, - cursorX = 170, - cursorY = 134, - inspectorPointerCaptured = false, - ) - assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.PanelDemo)) - } - - @Test - fun `panel panel demo supports drag and body button click`() { - val host = SystemOverlayHost(InspectorController()) - val root = inspectedRoot() - - host.onInputFrame(1280, 720) - host.togglePanelDemo(anchorX = 220, anchorY = 160) - host.syncFrame( - root, - inspectedLayoutRevision = 1L, - cursorX = 224, - cursorY = 166, - inspectorPointerCaptured = false, - ) - host.render(ctx, 1280, 720) - val state = host.debugEntryState(SystemOverlayEntryId.PanelDemo) ?: error("state missing") - val before = state.panelState.currentRectOrNull() ?: error("panel missing") - val node = - host.debugEntryNode(SystemOverlayEntryId.PanelDemo) as? SystemOverlayPanelDemoNode - ?: error("demo node missing") - val buttonRect = node.buttonRect() ?: error("button rect missing") - - val headerStartX = before.x + 10 - val headerStartY = before.y + 10 - assertTrue(host.handleMouseDown(headerStartX, headerStartY, MouseButton.LEFT)) - assertTrue(host.handleMouseMove(headerStartX + 60, headerStartY + 30)) - host.syncFrame( - root, - inspectedLayoutRevision = 2L, - cursorX = headerStartX + 60, - cursorY = headerStartY + 30, - inspectorPointerCaptured = false, - ) - val moved = state.panelState.currentRectOrNull() ?: error("panel missing") - assertTrue(moved.x > before.x) - assertTrue(host.handleMouseUp(headerStartX + 60, headerStartY + 30, MouseButton.LEFT)) - - host.syncFrame( - root, - inspectedLayoutRevision = 3L, - cursorX = moved.x + 8, - cursorY = moved.y + 8, - inspectorPointerCaptured = false, - ) - host.render(ctx, 1280, 720) - val updatedNode = - host.debugEntryNode(SystemOverlayEntryId.PanelDemo) as? SystemOverlayPanelDemoNode - ?: error("demo node missing") - val movedButtonRect = updatedNode.buttonRect() ?: error("button rect missing") - assertTrue(host.handleMouseDown(movedButtonRect.x + 1, movedButtonRect.y + 1, MouseButton.LEFT)) - host.syncFrame( - root, - inspectedLayoutRevision = 4L, - cursorX = movedButtonRect.x + 1, - cursorY = movedButtonRect.y + 1, - inspectorPointerCaptured = false, - ) - host.render(ctx, 1280, 720) - assertEquals(1, updatedNode.currentButtonClicks()) - assertNotNull(updatedNode.buttonRect()) - } - - @Test - fun `panel panel demo close button closes and reopen restores interactions`() { - val host = SystemOverlayHost(InspectorController()) - val root = inspectedRoot() - - host.onInputFrame(1280, 720) - host.togglePanelDemo(anchorX = 280, anchorY = 180) - host.syncFrame( - root, - inspectedLayoutRevision = 1L, - cursorX = 282, - cursorY = 182, - inspectorPointerCaptured = false, - ) - val state = host.debugEntryState(SystemOverlayEntryId.PanelDemo) ?: error("state missing") - val rect = state.panelState.currentRectOrNull() ?: error("panel missing") - val closeX = rect.x + rect.width - 4 - 16 + 1 - val closeY = rect.y + 4 + 1 - - assertTrue(host.handleMouseDown(closeX, closeY, MouseButton.LEFT)) - host.syncFrame( - root, - inspectedLayoutRevision = 2L, - cursorX = closeX, - cursorY = closeY, - inspectorPointerCaptured = false, - ) - assertFalse(host.isOverlayPanelDemoOpen()) - assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.PanelDemo)) - - host.togglePanelDemo(anchorX = 280, anchorY = 180) - host.syncFrame( - root, - inspectedLayoutRevision = 3L, - cursorX = 284, - cursorY = 184, - inspectorPointerCaptured = false, - ) - assertTrue(host.isOverlayPanelDemoOpen()) - assertTrue(host.debugMountedEntryIds().contains(SystemOverlayEntryId.PanelDemo)) - } - - @Test - fun `panel panel demo remains stable across open drag body click drag close reopen sequence`() { - val host = SystemOverlayHost(InspectorController()) - val root = inspectedRoot() - - host.onInputFrame(1280, 720) - host.togglePanelDemo(anchorX = 260, anchorY = 170) - host.syncFrame( - root, - inspectedLayoutRevision = 1L, - cursorX = 260, - cursorY = 170, - inspectorPointerCaptured = false, - ) - host.render(ctx, 1280, 720) - - val initialNode = - host.debugEntryNode(SystemOverlayEntryId.PanelDemo) as? SystemOverlayPanelDemoNode - ?: error("demo node missing") - val state = host.debugEntryState(SystemOverlayEntryId.PanelDemo) ?: error("state missing") - val initialRect = state.panelState.currentRectOrNull() ?: error("panel missing") - - val firstDragStartX = initialRect.x + 10 - val firstDragStartY = initialRect.y + 10 - assertTrue(host.handleMouseDown(firstDragStartX, firstDragStartY, MouseButton.LEFT)) - assertTrue(host.handleMouseMove(firstDragStartX + 40, firstDragStartY + 20)) - assertTrue(host.handleMouseUp(firstDragStartX + 40, firstDragStartY + 20, MouseButton.LEFT)) - host.syncFrame( - root, - inspectedLayoutRevision = 2L, - cursorX = firstDragStartX + 40, - cursorY = firstDragStartY + 20, - inspectorPointerCaptured = false, - ) - host.render(ctx, 1280, 720) - - val movedRect = state.panelState.currentRectOrNull() ?: error("panel missing") - assertTrue(movedRect.x > initialRect.x) - val movedNode = - host.debugEntryNode(SystemOverlayEntryId.PanelDemo) as? SystemOverlayPanelDemoNode - ?: error("demo node missing") - assertSame(initialNode, movedNode) - - val buttonRect = movedNode.buttonRect() ?: error("button missing") - assertTrue(host.handleMouseDown(buttonRect.x + 1, buttonRect.y + 1, MouseButton.LEFT)) - host.syncFrame( - root, - inspectedLayoutRevision = 3L, - cursorX = buttonRect.x + 1, - cursorY = buttonRect.y + 1, - inspectorPointerCaptured = false, - ) - host.render(ctx, 1280, 720) - assertEquals(1, movedNode.currentButtonClicks()) - - val secondDragStartX = movedRect.x + 10 - val secondDragStartY = movedRect.y + 10 - assertTrue(host.handleMouseDown(secondDragStartX, secondDragStartY, MouseButton.LEFT)) - assertTrue(host.handleMouseMove(secondDragStartX + 30, secondDragStartY + 10)) - assertTrue(host.handleMouseUp(secondDragStartX + 30, secondDragStartY + 10, MouseButton.LEFT)) - host.syncFrame( - root, - inspectedLayoutRevision = 4L, - cursorX = secondDragStartX + 30, - cursorY = secondDragStartY + 10, - inspectorPointerCaptured = false, - ) - - val secondMovedRect = state.panelState.currentRectOrNull() ?: error("panel missing") - assertTrue(secondMovedRect.x > movedRect.x) - - val closeX = secondMovedRect.x + secondMovedRect.width - 4 - 16 + 1 - val closeY = secondMovedRect.y + 4 + 1 - assertTrue(host.handleMouseDown(closeX, closeY, MouseButton.LEFT)) - host.syncFrame( - root, - inspectedLayoutRevision = 5L, - cursorX = closeX, - cursorY = closeY, - inspectorPointerCaptured = false, - ) - assertFalse(host.isOverlayPanelDemoOpen()) - - host.togglePanelDemo(anchorX = 260, anchorY = 170) - host.syncFrame( - root, - inspectedLayoutRevision = 6L, - cursorX = 262, - cursorY = 172, - inspectorPointerCaptured = false, - ) - val reopenedNode = - host.debugEntryNode(SystemOverlayEntryId.PanelDemo) as? SystemOverlayPanelDemoNode - ?: error("demo node missing") - assertSame(initialNode, reopenedNode) - assertTrue(host.isOverlayPanelDemoOpen()) - } - - @Test - fun `panel panel demo uses render viewport before first mouse input`() { - val host = SystemOverlayHost(InspectorController()) - val root = inspectedRoot() - - host.render(ctx, 1280, 720) - host.togglePanelDemo(anchorX = 460, anchorY = 320) - host.syncFrame( - root, - inspectedLayoutRevision = 1L, - cursorX = 460, - cursorY = 320, - inspectorPointerCaptured = false, - ) - - val state = host.debugEntryState(SystemOverlayEntryId.PanelDemo) ?: error("state missing") - val rect = state.panelState.currentRectOrNull() ?: error("panel missing") - assertEquals(460, rect.x) - assertEquals(320, rect.y) - } - - @Test - fun `panel panel demo uses native overlay panel node in live path`() { - val host = SystemOverlayHost(InspectorController()) - val root = inspectedRoot() - - host.onInputFrame(1280, 720) - host.togglePanelDemo(anchorX = 180, anchorY = 140) - host.syncFrame( - root, - inspectedLayoutRevision = 1L, - cursorX = 182, - cursorY = 142, - inspectorPointerCaptured = false, - ) - host.render(ctx, 1280, 720) - - val node = - host.debugEntryNode(SystemOverlayEntryId.PanelDemo) as? SystemOverlayPanelDemoNode - ?: error("demo node missing") - assertTrue(node.children.any { it.styleType == "dsgl-overlay-panel" }) - assertTrue(node.children.none { it.styleType == "dsgl-system-raw-render-command" }) - } - - private fun inspectedRoot(): ContainerNode { - val root = ContainerNode(key = "root") - root.bounds = Rect(0, 0, 1280, 720) - ContainerNode(key = "child") - .apply { - bounds = Rect(20, 20, 120, 32) - }.applyParent(root) - return root - } -} From a730a7a4ee058fae6df80b3427cbeab0b100e602 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Mon, 1 Jun 2026 22:46:51 +0300 Subject: [PATCH 71/78] intermediate commit to track changes; --- .../dsgl/mcForge1710/DsglScreenHost.kt | 458 ++++++++++++++---- .../DsglScreenHostDomainOrchestrationTests.kt | 309 ++++++++++++ .../ColorPickerPortalController.kt | 3 + .../dsgl/core/components/modal/ModalDsl.kt | 59 --- .../modal/internal/ModalPortalController.kt | 163 +++++++ .../dsgl/core/debug/DebugDomainHosts.kt | 7 + .../core/dnd/internal/DefaultDndEngine.kt | 4 + .../dsgl/core/dom/elements/RangeInputNode.kt | 96 ++-- .../core/dom/elements/SingleLineInputNode.kt | 39 ++ .../dsgl/core/dom/elements/TextAreaNode.kt | 60 +++ .../dsgl/core/event/FocusManager.kt | 22 + .../dsgl/core/hooks/ref/ElementHandle.kt | 4 +- .../core/overlay/ApplicationOverlayHost.kt | 45 +- .../dsgl/core/overlay/DomainSurfaceHost.kt | 2 + .../dsgl/core/overlay/PortalHostContracts.kt | 3 + .../core/overlay/input/LayerDomInputRouter.kt | 23 +- .../overlay/system/SystemOverlayEntries.kt | 4 + .../core/overlay/system/SystemOverlayHost.kt | 8 + .../dsgl/core/select/SelectEngine.kt | 7 +- .../core/select/SelectPortalController.kt | 17 +- .../dsgl/core/style/StyleEngine.kt | 20 +- .../ModalPortalKeyboardRegressionTests.kt | 43 ++ .../ModalPortalPointerRegressionTests.kt | 259 ++++++++++ .../dnd/internal/DefaultDndEngineTests.kt | 115 +++++ .../DomTreeFontSizeInvalidationTests.kt | 66 +++ .../overlay/LiveLayerInteractionPathTests.kt | 6 +- .../overlay/input/LayerDomInputRouterTests.kt | 88 ++++ .../select/SelectPortalControllerTests.kt | 4 +- 28 files changed, 1717 insertions(+), 217 deletions(-) create mode 100644 core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalPointerRegressionTests.kt create mode 100644 core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/DomTreeFontSizeInvalidationTests.kt diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index ec08588..e40cd1b 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -15,6 +15,7 @@ import org.dreamfinity.dsgl.core.debug.OverlayLayerDebugState import org.dreamfinity.dsgl.core.dnd.* import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.elements.* +import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.* import org.dreamfinity.dsgl.core.hooks.HookHotReloadRemountException import org.dreamfinity.dsgl.core.hooks.HookRenderSessionMode @@ -35,9 +36,12 @@ import org.dreamfinity.dsgl.core.overlay.captureColorPickerEyedropperSample import org.dreamfinity.dsgl.core.overlay.closeFloatingPortals import org.dreamfinity.dsgl.core.overlay.handlePortalKeyDownAfterDom import org.dreamfinity.dsgl.core.overlay.handlePortalKeyDownBeforeDom +import org.dreamfinity.dsgl.core.overlay.handlePortalKeyUpAfterDom +import org.dreamfinity.dsgl.core.overlay.handlePortalKeyUpBeforeDom import org.dreamfinity.dsgl.core.overlay.handlePortalPointerAfterDom import org.dreamfinity.dsgl.core.overlay.handlePortalPointerBeforeDom import org.dreamfinity.dsgl.core.overlay.hasActiveColorPickerEyedropper +import org.dreamfinity.dsgl.core.overlay.hasActiveModalPortal import org.dreamfinity.dsgl.core.overlay.hasDomPointerTargetAt import org.dreamfinity.dsgl.core.overlay.hasOpenColorPickerPortal import org.dreamfinity.dsgl.core.overlay.hasOpenContextMenuPortal @@ -209,6 +213,7 @@ abstract class DsglScreenHost( dsglMouseX = frameCursor.mouseX, dsglMouseY = frameCursor.mouseY, ) + cancelApplicationRootDndBehindModal() val applicationOverlayCommands = collectApplicationOverlayCommands(overlayState.appOverlayRenderEnabled) val systemOverlayCommands = syncSystemOverlayAndCollectCommands( @@ -416,6 +421,12 @@ abstract class DsglScreenHost( refreshActiveColorSamplerOwner(tree.root) } + private fun cancelApplicationRootDndBehindModal() { + if (applicationOverlayHost.hasActiveModalPortal()) { + DndRuntime.engine.cancelActiveDrag() + } + } + private fun collectApplicationOverlayCommands(appOverlayRenderEnabled: Boolean): List { if (!appOverlayRenderEnabled) { return emptyList() @@ -507,77 +518,134 @@ abstract class DsglScreenHost( systemOverlayInputEnabled: Boolean, inspectorBlocks: Boolean, ) { - val contextMenuBlocks = - appOverlayInputEnabled && !inspectorBlocks && applicationOverlayHost.hasOpenContextMenuPortal() - val selectBlocks = - appOverlayInputEnabled && !inspectorBlocks && applicationOverlayHost.hasOpenSelectPortal() - val applicationPortalDomBlocks = - appOverlayInputEnabled && - !inspectorBlocks && - applicationOverlayHost.hasDomPointerTargetAt(dsglMouseX, dsglMouseY) - val systemSelectBlocks = systemOverlayInputEnabled && systemOverlayHost.hasOpenPortal() - val inlineSamplerOwnsSession = activeColorSamplerOwner is ActiveColorSamplerOwner.Inline - val colorPickerBlocks = - !inspectorBlocks && - ( - (systemOverlayInputEnabled && systemOverlayHost.isSystemColorPickerOpen()) || - ( - appOverlayInputEnabled && - applicationOverlayHost.hasOpenColorPickerPortal() && - !inlineSamplerOwnsSession - ) - ) - var applicationRootFrameBlocked = inspectorBlocks - applicationRootFrameBlocked = applicationRootFrameBlocked || contextMenuBlocks - applicationRootFrameBlocked = applicationRootFrameBlocked || selectBlocks - applicationRootFrameBlocked = applicationRootFrameBlocked || applicationPortalDomBlocks - applicationRootFrameBlocked = applicationRootFrameBlocked || systemSelectBlocks - applicationRootFrameBlocked = applicationRootFrameBlocked || colorPickerBlocks - if (!applicationRootFrameBlocked) { - DndRuntime.engine.onMouseMove(tree.root, dsglMouseX, dsglMouseY) - } - DndRuntime.engine.onFrame(tree.root, dtSeconds) + val applicationRootFrameBlocked = + isApplicationRootFrameBlocked( + dsglMouseX = dsglMouseX, + dsglMouseY = dsglMouseY, + appOverlayInputEnabled = appOverlayInputEnabled, + systemOverlayInputEnabled = systemOverlayInputEnabled, + inspectorBlocks = inspectorBlocks, + ) val prevX = if (lastMoveX == Int.MIN_VALUE) dsglMouseX else lastMoveX val prevY = if (lastMoveY == Int.MIN_VALUE) dsglMouseY else lastMoveY val dx = dsglMouseX - prevX val dy = dsglMouseY - prevY + val applicationModalBlocks = applicationOverlayHost.hasActiveModalPortal() if (applicationRootFrameBlocked) { + if (applicationModalBlocks) { + DndRuntime.engine.cancelActiveDrag() + } clearHoverChainStates(postLeaveEvents = true, mouseX = dsglMouseX, mouseY = dsglMouseY) hoverTarget = null } else { - updateHover(tree.root, hoverChain, dsglMouseX, dsglMouseY, dx, dy) - hoverTarget = hoverChain.lastOrNull() - if (dragCaptureTarget != null && hasFocusChangedSinceCapture()) { - releaseDragCapture() - } - if (dx != 0 || dy != 0) { + updateFrameApplicationRootInteraction(tree, dsglMouseX, dsglMouseY, prevX, prevY, dx, dy) + } + if (!applicationModalBlocks) { + DndRuntime.engine.onFrame(tree.root, dtSeconds) + } + lastMoveX = dsglMouseX + lastMoveY = dsglMouseY + } + + private fun isApplicationRootFrameBlocked( + dsglMouseX: Int, + dsglMouseY: Int, + appOverlayInputEnabled: Boolean, + systemOverlayInputEnabled: Boolean, + inspectorBlocks: Boolean, + ): Boolean { + if (appOverlayInputEnabled && applicationOverlayHost.hasActiveModalPortal()) return true + if (isApplicationRootPointerDragActive() && dragCaptureTarget != null) return false + if (inspectorBlocks || higherSurfacePointerButton != -1) return true + if (isApplicationPortalFrameBlocking(dsglMouseX, dsglMouseY, appOverlayInputEnabled)) return true + if (systemOverlayInputEnabled && systemOverlayHost.hasOpenPortal()) return true + return isColorPickerFrameBlocking( + appOverlayInputEnabled = appOverlayInputEnabled, + systemOverlayInputEnabled = systemOverlayInputEnabled, + ) + } + + private fun isApplicationPortalFrameBlocking( + dsglMouseX: Int, + dsglMouseY: Int, + appOverlayInputEnabled: Boolean, + ): Boolean { + if (!appOverlayInputEnabled) return false + return applicationOverlayHost.hasOpenContextMenuPortal() || + applicationOverlayHost.hasOpenSelectPortal() || + applicationOverlayHost.hasDomPointerTargetAt(dsglMouseX, dsglMouseY) + } + + private fun isColorPickerFrameBlocking( + appOverlayInputEnabled: Boolean, + systemOverlayInputEnabled: Boolean, + ): Boolean { + val inlineSamplerOwnsSession = activeColorSamplerOwner is ActiveColorSamplerOwner.Inline + val systemPickerBlocks = systemOverlayInputEnabled && systemOverlayHost.isSystemColorPickerOpen() + val applicationPickerBlocks = + appOverlayInputEnabled && + applicationOverlayHost.hasOpenColorPickerPortal() && + !inlineSamplerOwnsSession + return systemPickerBlocks || applicationPickerBlocks + } + + private fun updateFrameApplicationRootInteraction( + tree: DomTree, + dsglMouseX: Int, + dsglMouseY: Int, + prevX: Int, + prevY: Int, + dx: Int, + dy: Int, + ) { + updateHover(tree.root, hoverChain, dsglMouseX, dsglMouseY, dx, dy) + hoverTarget = hoverChain.lastOrNull() + if (dragCaptureTarget != null && hasFocusChangedSinceCapture()) { + releaseDragCapture() + } + if (dx != 0 || dy != 0) { + if (isApplicationRootPointerDragActive()) { + dispatchApplicationRootPointerDragDelta( + tree = tree, + mouseX = dsglMouseX, + mouseY = dsglMouseY, + previousMouseX = prevX, + previousMouseY = prevY, + dx = dx, + dy = dy, + ) + lastMouseX = dsglMouseX + lastMouseY = dsglMouseY + } else { + DndRuntime.engine.onMouseMove(tree.root, dsglMouseX, dsglMouseY) val moveEvent = MouseMoveEvent(dsglMouseX, dsglMouseY, prevX, prevY) moveEvent.target = resolveForcedPointerTarget() ?: dragCaptureTarget ?: hoverTarget EventBus.post(moveEvent) } } - lastMoveX = dsglMouseX - lastMoveY = dsglMouseY } private fun stageApplicationOverlayCommands( tree: DomTree, applicationOverlayCommands: List, appOverlayRenderEnabled: Boolean, + measureContext: UiMeasureContext = adapter, ) { applicationOverlayCommandsBuffer.clear() if (appOverlayRenderEnabled) { applicationOverlayCommandsBuffer.addAll(applicationOverlayCommands) - DndRuntime.engine.appendPlaceholderCommands(applicationOverlayCommandsBuffer) - DndRuntime.engine.appendOverlayCommands( - tree.root, - adapter, - lastWidth, - lastHeight, - applicationOverlayCommandsBuffer, - ) + if (!applicationOverlayHost.hasActiveModalPortal()) { + DndRuntime.engine.appendPlaceholderCommands(applicationOverlayCommandsBuffer) + DndRuntime.engine.appendOverlayCommands( + tree.root, + measureContext, + lastWidth, + lastHeight, + applicationOverlayCommandsBuffer, + ) + } applicationOverlayHost.appendPortalOverlayCommands( - measureContext = adapter, + measureContext = measureContext, viewportWidth = lastWidth, viewportHeight = lastHeight, out = applicationOverlayCommandsBuffer, @@ -895,10 +963,21 @@ abstract class DsglScreenHost( mc.dispatchKeypresses() return true } - if (pressedKeys.remove(keyCode)) { - EventBus.post(KeyboardKeyUpEvent(keyChar, keyCode)) + when ( + dispatchDomainKeyUp( + keyCode = keyCode, + keyChar = keyChar, + inspectorMouseX = inspectorMouseX, + inspectorMouseY = inspectorMouseY, + ) + ) { + DomainKeyDispatchResult.HigherSurfaceConsumed -> { + mc.dispatchKeypresses() + return true + } + + DomainKeyDispatchResult.ApplicationRootHandled, DomainKeyDispatchResult.None -> return false } - return false } override fun handleMouseInput() { @@ -1008,7 +1087,11 @@ abstract class DsglScreenHost( null -> if (isHigherSurfaceOwnedPointerRelease(context)) { higherSurfacePointerButton = -1 - consumeOverlayPointerState(inputEvent.mouseX, inputEvent.mouseY) + consumeOverlayPointerState( + mouseX = inputEvent.mouseX, + mouseY = inputEvent.mouseY, + cancelRootDnd = context.inputEvent.mouseButton != -1, + ) DomainPointerDispatchResult.HigherSurfaceConsumed } else { DomainPointerDispatchResult.None @@ -1016,7 +1099,11 @@ abstract class DsglScreenHost( ScreenDomainSurfaces.ApplicationRoot -> DomainPointerDispatchResult.ApplicationRootHandled else -> { updateHigherSurfacePointerOwnership(context) - consumeOverlayPointerState(inputEvent.mouseX, inputEvent.mouseY) + consumeOverlayPointerState( + mouseX = inputEvent.mouseX, + mouseY = inputEvent.mouseY, + cancelRootDnd = context.inputEvent.mouseButton != -1, + ) DomainPointerDispatchResult.HigherSurfaceConsumed } } @@ -1129,8 +1216,8 @@ abstract class DsglScreenHost( } private fun dispatchApplicationRootPointerPhase(tree: DomTree, inputEvent: MouseInputEvent): Boolean { - if (Mouse.getEventButtonState()) { - dispatchApplicationRootPointerDown(tree, inputEvent) + if (inputEvent.mouseButton != -1 && Mouse.getEventButtonState()) { + dispatchApplicationRootPointerDown(tree, inputEvent, Minecraft.getSystemTime()) return true } else if (inputEvent.mouseButton != -1 && eventButton == inputEvent.mouseButton) { dispatchApplicationRootPointerUp(tree, inputEvent) @@ -1142,14 +1229,13 @@ abstract class DsglScreenHost( return false } - private fun dispatchApplicationRootPointerDown(tree: DomTree, inputEvent: MouseInputEvent) { + private fun dispatchApplicationRootPointerDown(tree: DomTree, inputEvent: MouseInputEvent, eventTimeMs: Long) { eventButton = inputEvent.mouseButton - lastMouseEvent = Minecraft.getSystemTime() + lastMouseEvent = eventTimeMs mapButton(inputEvent.mouseButton)?.let { mappedButton -> val event = MouseDownEvent(inputEvent.mouseX, inputEvent.mouseY, mappedButton) event.target = resolvePointerDownTarget() EventBus.post(event) - DndRuntime.engine.onMouseDown(tree.root, event.target ?: hoverTarget, event) if (mappedButton == MouseButton.LEFT) { setActiveTarget(event.target ?: hoverTarget) val captureTarget = @@ -1160,6 +1246,11 @@ abstract class DsglScreenHost( } else if (dragCaptureTarget != null) { releaseDragCapture() } + if (captureTarget == null && !event.cancelled) { + DndRuntime.engine.onMouseDown(tree.root, event.target ?: hoverTarget, event) + } + } else if (!event.cancelled) { + DndRuntime.engine.onMouseDown(tree.root, event.target ?: hoverTarget, event) } } } @@ -1189,30 +1280,56 @@ abstract class DsglScreenHost( val dx = inputEvent.mouseX - lastMouseX val dy = inputEvent.mouseY - lastMouseY if (dx != 0 || dy != 0) { - DndRuntime.engine.onMouseMove(tree.root, inputEvent.mouseX, inputEvent.mouseY) - val dragEvent = - MouseDragEvent( - lastMouseX, - lastMouseY, - dx, - dy, - mappedButton, - ) - if (!DndRuntime.engine.isDragging) { - dragEvent.target = dragCaptureTarget ?: hoverTarget - EventBus.post(dragEvent) - } - dragCaptureTarget?.continuePointerCapture( + dispatchApplicationRootPointerDragDelta( + tree = tree, mouseX = inputEvent.mouseX, mouseY = inputEvent.mouseY, - mouseDX = dx, - mouseDY = dy, - button = mappedButton, + previousMouseX = lastMouseX, + previousMouseY = lastMouseY, + dx = dx, + dy = dy, + mappedButton = mappedButton, ) } } } + private fun dispatchApplicationRootPointerDragDelta( + tree: DomTree, + mouseX: Int, + mouseY: Int, + previousMouseX: Int, + previousMouseY: Int, + dx: Int, + dy: Int, + mappedButton: MouseButton? = mapButton(eventButton), + ) { + val button = mappedButton ?: return + val captured = dragCaptureTarget + if (captured == null) { + DndRuntime.engine.onMouseMove(tree.root, mouseX, mouseY) + } + val dragEvent = + MouseDragEvent( + previousMouseX, + previousMouseY, + dx, + dy, + button, + ) + if (captured != null || !DndRuntime.engine.isDragging) { + dragEvent.target = captured ?: hoverTarget + EventBus.post(dragEvent) + } + captured?.continuePointerCapture( + mouseX = mouseX, + mouseY = mouseY, + mouseDX = dx, + mouseDY = dy, + button = button, + ) + } + private fun dispatchApplicationRootWheelPhase(inputEvent: MouseInputEvent): Boolean { if (inputEvent.dWheel != 0) { val wheelTarget = resolveWheelTarget() @@ -1273,6 +1390,42 @@ abstract class DsglScreenHost( } } + private fun dispatchDomainKeyUp( + keyCode: Int, + keyChar: Char, + inspectorMouseX: Int, + inspectorMouseY: Int, + ): DomainKeyDispatchResult { + val consumedBy = + domainOrchestrator.firstInputConsumer( + canConsume = { surface -> + when (surface) { + ScreenDomainSurfaces.DebugPortal -> debugDomainPortalHost.handleKeyUp(keyCode, keyChar) + ScreenDomainSurfaces.DebugRoot -> debugDomainRootHost.handleKeyUp(keyCode, keyChar) + + ScreenDomainSurfaces.SystemPortal -> + consumeSystemOverlayKeyUp( + keyCode = keyCode, + keyChar = keyChar, + inspectorMouseX = inspectorMouseX, + inspectorMouseY = inspectorMouseY, + ) + + ScreenDomainSurfaces.ApplicationPortal -> consumeApplicationOverlayKeyUp(keyCode, keyChar) + ScreenDomainSurfaces.ApplicationRoot -> dispatchApplicationRootKeyUp(keyCode, keyChar) + ScreenDomainSurfaces.SystemRoot -> false + else -> false + } + }, + isSurfaceInputEnabled = OverlayLayerDebugState::isInputEnabled, + ) + return when (consumedBy) { + null -> DomainKeyDispatchResult.None + ScreenDomainSurfaces.ApplicationRoot -> DomainKeyDispatchResult.ApplicationRootHandled + else -> DomainKeyDispatchResult.HigherSurfaceConsumed + } + } + private fun dispatchApplicationRootKeyDown(keyCode: Int, keyChar: Char) { // TODO(Veritaris): remove this handling from production build if (keyCode == Keyboard.KEY_F6) { @@ -1280,8 +1433,11 @@ abstract class DsglScreenHost( requestRebuild("style reload") } if (pressedKeys.add(keyCode)) { - val downEvent = KeyboardKeyDownEvent(keyChar, keyCode) - EventBus.post(downEvent) + val downEvent = dispatchFocusedApplicationRootKeyDown(keyCode, keyChar) + if (!downEvent.cancelled && keyCode == Keyboard.KEY_ESCAPE && downEvent.target != null) { + FocusManager.clearFocus() + downEvent.cancelled = true + } if (downEvent.cancelled) { pressedKeys.remove(keyCode) } else { @@ -1293,6 +1449,32 @@ abstract class DsglScreenHost( } } + private fun dispatchApplicationRootKeyUp(keyCode: Int, keyChar: Char): Boolean { + if (!pressedKeys.remove(keyCode)) return false + dispatchFocusedApplicationRootKeyUp(keyCode, keyChar) + return true + } + + private fun dispatchFocusedApplicationRootKeyDown(keyCode: Int, keyChar: Char): KeyboardKeyDownEvent { + val downEvent = KeyboardKeyDownEvent(keyChar, keyCode) + val root = domTree?.root + if (root != null) { + downEvent.target = FocusManager.focusedNodeWithin(root) + if (downEvent.target != null) { + EventBus.post(downEvent) + } + } + return downEvent + } + + private fun dispatchFocusedApplicationRootKeyUp(keyCode: Int, keyChar: Char) { + val root = domTree?.root ?: return + val focused = FocusManager.focusedNodeWithin(root) ?: return + val upEvent = KeyboardKeyUpEvent(keyChar, keyCode) + upEvent.target = focused + EventBus.post(upEvent) + } + private fun consumeSystemOverlayKeyDown( keyCode: Int, keyChar: Char, @@ -1318,6 +1500,31 @@ abstract class DsglScreenHost( return false } + private fun consumeSystemOverlayKeyUp( + keyCode: Int, + keyChar: Char, + inspectorMouseX: Int, + inspectorMouseY: Int, + ): Boolean { + if (systemOverlayHost.handlePortalKeyUp(keyCode, keyChar)) { + return true + } + if (systemOverlayHost.handleKeyUp(keyCode, keyChar)) { + return true + } + val keyboardBlocked = + inspector.active && + ( + inspector.shouldConsumeKeyboard(inspectorMouseX, inspectorMouseY) || + inspector.mode == InspectorMode.Locked + ) + if (keyboardBlocked) { + logInspectorInput("keyboard up consumed keyCode=$keyCode") + return true + } + return false + } + private fun consumeApplicationOverlayKeyDown(keyCode: Int, keyChar: Char): Boolean { if (applicationOverlayHost.handlePortalKeyDownBeforeDom(keyCode, keyChar)) { return true @@ -1331,6 +1538,19 @@ abstract class DsglScreenHost( return false } + private fun consumeApplicationOverlayKeyUp(keyCode: Int, keyChar: Char): Boolean { + if (applicationOverlayHost.handlePortalKeyUpBeforeDom(keyCode, keyChar)) { + return true + } + if (applicationOverlayHost.handleKeyUp(keyCode, keyChar)) { + return true + } + if (applicationOverlayHost.handlePortalKeyUpAfterDom(keyCode, keyChar)) { + return true + } + return false + } + private fun consumeDebugPointerEvent( host: DomainSurfaceHost, mouseX: Int, @@ -1427,18 +1647,6 @@ abstract class DsglScreenHost( } } - if ( - applicationOverlayHost.handlePortalPointerAfterDom( - mouseX = mouseX, - mouseY = mouseY, - dWheel = dWheel, - button = mappedButton, - pressed = buttonPressed, - ) - ) { - return true - } - if (dWheel != 0 && applicationOverlayHost.handleMouseWheel(mouseX, mouseY, dWheel)) { return true } @@ -1456,10 +1664,19 @@ abstract class DsglScreenHost( return true } - return false + return applicationOverlayHost.handlePortalPointerAfterDom( + mouseX = mouseX, + mouseY = mouseY, + dWheel = dWheel, + button = mappedButton, + pressed = buttonPressed, + ) } - private fun consumeOverlayPointerState(mouseX: Int, mouseY: Int) { + private fun consumeOverlayPointerState(mouseX: Int, mouseY: Int, cancelRootDnd: Boolean = false) { + if (cancelRootDnd) { + DndRuntime.engine.cancelActiveDrag() + } eventButton = -1 clearActiveTarget() releaseDragCapture() @@ -1477,6 +1694,8 @@ abstract class DsglScreenHost( else -> null } + private fun isApplicationRootPointerDragActive(): Boolean = eventButton != -1 && lastMouseEvent > 0L + init { inspector.installColorPickerPortalService(systemOverlayHost.systemInspectorColorPickerService()) } @@ -1619,6 +1838,21 @@ abstract class DsglScreenHost( return out } + internal fun debugStageApplicationOverlayCommandsForTests( + tree: DomTree, + applicationOverlayCommands: List, + appOverlayRenderEnabled: Boolean = true, + measureContext: UiMeasureContext, + ): List { + stageApplicationOverlayCommands( + tree = tree, + applicationOverlayCommands = applicationOverlayCommands, + appOverlayRenderEnabled = appOverlayRenderEnabled, + measureContext = measureContext, + ) + return applicationOverlayCommandsBuffer.toList() + } + internal fun debugFirstDomainInputConsumerForTests( canConsume: (ScreenDomainSurface) -> Boolean, isSurfaceInputEnabled: (ScreenDomainSurface) -> Boolean = { true }, @@ -1667,10 +1901,18 @@ abstract class DsglScreenHost( ) if (consumedBy != null && consumedBy != ScreenDomainSurfaces.ApplicationRoot) { updateHigherSurfacePointerOwnership(context) - consumeOverlayPointerState(mouseX, mouseY) + consumeOverlayPointerState( + mouseX = mouseX, + mouseY = mouseY, + cancelRootDnd = context.inputEvent.mouseButton != -1, + ) } else if (consumedBy == null && isHigherSurfaceOwnedPointerRelease(context)) { higherSurfacePointerButton = -1 - consumeOverlayPointerState(mouseX, mouseY) + consumeOverlayPointerState( + mouseX = mouseX, + mouseY = mouseY, + cancelRootDnd = context.inputEvent.mouseButton != -1, + ) return ScreenDomainSurfaces.ApplicationPortal } return consumedBy @@ -1697,6 +1939,30 @@ abstract class DsglScreenHost( ) } + internal fun debugCancelApplicationRootDndBehindModalForTests() { + cancelApplicationRootDndBehindModal() + } + + internal fun debugDispatchApplicationRootPointerDownForTests( + tree: DomTree, + mouseX: Int, + mouseY: Int, + mouseButton: Int = 0, + ) { + val inputEvent = + MouseInputEvent( + mouseX = mouseX, + mouseY = mouseY, + dWheel = 0, + mouseButton = mouseButton, + ) + refreshHoverTarget(mouseX, mouseY) + dispatchApplicationRootPointerDown(tree, inputEvent, eventTimeMs = 1L) + finishMouseInputEvent(inputEvent) + lastMoveX = mouseX + lastMoveY = mouseY + } + private fun setDragCapture(target: DOMNode) { dragCaptureTarget = target dragCaptureKey = target.key @@ -1706,9 +1972,6 @@ abstract class DsglScreenHost( private fun releaseDragCapture() { dragCaptureTarget?.cancelPointerCapture() - RangeInputNode.clearActiveDrag() - SingleLineInputNode.clearActiveDrag() - TextAreaNode.clearActiveDrag() dragCaptureTarget = null dragCaptureKey = null dragCaptureClass = null @@ -1787,7 +2050,10 @@ abstract class DsglScreenHost( private fun hasFocusChangedSinceCapture(): Boolean { if (dragCaptureFocusKey == null) return false - val currentFocusKey = FocusManager.focusedNode()?.key + val captured = dragCaptureTarget + val currentFocus = FocusManager.focusedNode() + if (captured != null && isSameOrAncestor(captured, currentFocus)) return false + val currentFocusKey = currentFocus?.key return currentFocusKey != dragCaptureFocusKey } diff --git a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt index c4b56be..6b466f4 100644 --- a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt +++ b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt @@ -8,24 +8,36 @@ import org.dreamfinity.dsgl.core.colorpicker.ColorPickerLayout import org.dreamfinity.dsgl.core.colorpicker.ColorPickerState import org.dreamfinity.dsgl.core.colorpicker.ColorPickerStyle import org.dreamfinity.dsgl.core.colorpicker.RgbaColor +import org.dreamfinity.dsgl.core.components.modal.ModalSpec +import org.dreamfinity.dsgl.core.components.modal.modalPortal +import org.dreamfinity.dsgl.core.dnd.DndRuntime import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ButtonNode import org.dreamfinity.dsgl.core.dom.elements.ColorPickerInlineNode import org.dreamfinity.dsgl.core.dom.elements.ContainerNode +import org.dreamfinity.dsgl.core.dom.elements.RangeInputNode +import org.dreamfinity.dsgl.core.dom.elements.SingleLineInputNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.dsl.text +import org.dreamfinity.dsgl.core.dsl.ui import org.dreamfinity.dsgl.core.event.EventBus import org.dreamfinity.dsgl.core.event.Events +import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.event.MouseDownEvent import org.dreamfinity.dsgl.core.event.MouseLeaveEvent import org.dreamfinity.dsgl.core.event.MouseMoveEvent +import org.dreamfinity.dsgl.core.overlay.ApplicationOverlayHost import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurface import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces +import org.dreamfinity.dsgl.core.overlay.hasActiveModalPortal import org.dreamfinity.dsgl.core.overlay.toggleFloatingWindowDemo import org.dreamfinity.dsgl.core.render.RenderCommand import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assert.assertSame +import org.junit.Assert.assertTrue import org.junit.Test class DsglScreenHostDomainOrchestrationTests { @@ -317,6 +329,267 @@ class DsglScreenHostDomainOrchestrationTests { assertRenderColorAbsent(tree.paint(ctx), style.buttonHoverColor) } + @Test + fun `application root frame movement continues captured range drag`() { + val root = ContainerNode(key = "root").apply { bounds = Rect(0, 0, 300, 120) } + val range = + RangeInputNode(value = 0L, min = 0L, max = 100L, key = "range") + .apply { + bounds = Rect(20, 40, 100, 12) + }.applyParent(root) + val tree = DomTree(root) + val host = createHost(tree) + + host.debugDispatchApplicationRootPointerDownForTests(tree, mouseX = 20, mouseY = 46) + host.debugUpdateFrameInteractionStateForTests(tree, mouseX = 120, mouseY = 46) + + assertEquals(100L, range.value) + } + + @Test + fun `application root frame movement continues captured text selection drag`() { + val root = ContainerNode(key = "root").apply { bounds = Rect(0, 0, 300, 120) } + val input = + SingleLineInputNode(text = "abcdef", key = "text-input") + .apply { + bounds = Rect(20, 40, 120, 12) + }.applyParent(root) + val tree = DomTree(root) + val host = createHost(tree) + + host.debugDispatchApplicationRootPointerDownForTests(tree, mouseX = 20, mouseY = 46) + host.debugUpdateFrameInteractionStateForTests(tree, mouseX = 56, mouseY = 46) + + assertRenderColorPresent(input.buildCommandsForTest(), input.selectionColor) + } + + @Test + fun `application portal pointer down clears pending root dnd before frame movement`() { + val root = ContainerNode(key = "root").apply { bounds = Rect(0, 0, 300, 120) } + val draggable = + ContainerNode(key = "draggable") + .apply { + draggable = true + bounds = Rect(20, 40, 80, 20) + }.applyParent(root) + val tree = DomTree(root) + val host = createHost(tree) + val down = + MouseDownEvent(24, 44, MouseButton.LEFT) + .also { event -> + event.target = draggable + } + + DndRuntime.engine.cancelActiveDrag() + try { + DndRuntime.engine.onMouseDown(root, draggable, down) + assertTrue(DndRuntime.engine.isPointerCaptured) + + host.debugDispatchApplicationPortalThenRootPointerForTests( + mouseButton = 0, + buttonPressed = true, + mouseX = 60, + mouseY = 60, + applicationPortalConsumes = { true }, + applicationRootConsumes = { true }, + ) + DndRuntime.engine.onMouseMove(root, 120, 60) + + assertFalse(DndRuntime.engine.isPointerCaptured) + assertFalse(DndRuntime.engine.isDragging) + } finally { + DndRuntime.engine.cancelActiveDrag() + } + } + + @Test + fun `active application modal cancels root dnd before ghost can render`() { + val root = ContainerNode(key = "root").apply { bounds = Rect(0, 0, 300, 120) } + val draggable = + ContainerNode(key = "draggable") + .apply { + draggable = true + bounds = Rect(20, 40, 80, 20) + }.applyParent(root) + val tree = DomTree(root) + val host = createHost(tree) + val overlay = host.debugApplicationOverlayHostForTests() + val modalKey = "tests.host.modal.cancel.dnd" + val down = + MouseDownEvent(24, 44, MouseButton.LEFT) + .also { event -> + event.target = draggable + } + + DndRuntime.engine.cancelActiveDrag() + try { + DndRuntime.engine.onMouseDown(root, draggable, down) + DndRuntime.engine.onMouseMove(root, 120, 60) + assertTrue(DndRuntime.engine.isDragging) + + val modalTree = + ui { + modalPortal( + modals = + listOf( + ModalSpec(key = "static-modal") { + text("Static") + }, + ), + key = modalKey, + ) { + text("content") + } + } + modalTree.render(ctx, 300, 120) + overlay.render(ctx, 300, 120) + assertTrue(overlay.hasActiveModalPortal()) + + host.debugCancelApplicationRootDndBehindModalForTests() + + assertFalse(DndRuntime.engine.isPointerCaptured) + assertFalse(DndRuntime.engine.isDragging) + } finally { + val emptyModalTree = + ui { + modalPortal(modals = emptyList(), key = modalKey) { + text("content") + } + } + emptyModalTree.render(ctx, 300, 120) + overlay.render(ctx, 300, 120) + DndRuntime.engine.cancelActiveDrag() + } + } + + @Test + fun `active application modal frame blocks and cancels root dnd`() { + val root = ContainerNode(key = "root").apply { bounds = Rect(0, 0, 300, 120) } + val draggable = + ContainerNode(key = "draggable") + .apply { + draggable = true + bounds = Rect(20, 40, 80, 20) + }.applyParent(root) + val tree = DomTree(root) + val host = createHost(tree) + val overlay = host.debugApplicationOverlayHostForTests() + val modalKey = "tests.host.modal.frame.blocks.dnd" + val down = + MouseDownEvent(24, 44, MouseButton.LEFT) + .also { event -> + event.target = draggable + } + + DndRuntime.engine.cancelActiveDrag() + try { + activateStaticModal(overlay, modalKey) + DndRuntime.engine.onMouseDown(root, draggable, down) + DndRuntime.engine.onMouseMove(root, 120, 60) + assertTrue(DndRuntime.engine.isDragging) + + host.debugUpdateFrameInteractionStateForTests(tree, mouseX = 120, mouseY = 60) + + assertFalse(DndRuntime.engine.isPointerCaptured) + assertFalse(DndRuntime.engine.isDragging) + } finally { + clearStaticModal(overlay, modalKey) + DndRuntime.engine.cancelActiveDrag() + } + } + + @Test + fun `active application modal suppresses root dnd ghost commands`() { + val root = ContainerNode(key = "root").apply { bounds = Rect(0, 0, 300, 120) } + val draggable = + ContainerNode(key = "draggable") + .apply { + draggable = true + bounds = Rect(20, 40, 80, 20) + }.applyParent(root) + val tree = DomTree(root) + val host = createHost(tree) + val overlay = host.debugApplicationOverlayHostForTests() + val modalKey = "tests.host.modal.suppresses.ghost.commands" + val down = + MouseDownEvent(24, 44, MouseButton.LEFT) + .also { event -> + event.target = draggable + } + + DndRuntime.engine.cancelActiveDrag() + try { + activateStaticModal(overlay, modalKey) + DndRuntime.engine.onMouseDown(root, draggable, down) + DndRuntime.engine.onMouseMove(root, 120, 60) + assertTrue(DndRuntime.engine.isDragging) + assertTrue(overlay.hasActiveModalPortal()) + + val staged = + host.debugStageApplicationOverlayCommandsForTests( + tree = tree, + applicationOverlayCommands = overlay.paint(ctx), + measureContext = ctx, + ) + + assertFalse( + staged.any { command -> + command is RenderCommand.DrawText && command.text == "drag" + }, + ) + } finally { + clearStaticModal(overlay, modalKey) + DndRuntime.engine.cancelActiveDrag() + } + } + + @Test + fun `application portal pointer release clears active root dnd`() { + val root = ContainerNode(key = "root").apply { bounds = Rect(0, 0, 300, 120) } + val draggable = + ContainerNode(key = "draggable") + .apply { + draggable = true + bounds = Rect(20, 40, 80, 20) + }.applyParent(root) + val tree = DomTree(root) + val host = createHost(tree) + val down = + MouseDownEvent(24, 44, MouseButton.LEFT) + .also { event -> + event.target = draggable + } + + DndRuntime.engine.cancelActiveDrag() + try { + host.debugDispatchApplicationPortalThenRootPointerForTests( + mouseButton = 0, + buttonPressed = true, + mouseX = 60, + mouseY = 60, + applicationPortalConsumes = { true }, + applicationRootConsumes = { true }, + ) + DndRuntime.engine.onMouseDown(root, draggable, down) + DndRuntime.engine.onMouseMove(root, 120, 60) + assertTrue(DndRuntime.engine.isDragging) + + host.debugDispatchApplicationPortalThenRootPointerForTests( + mouseButton = 0, + buttonPressed = false, + mouseX = 120, + mouseY = 60, + applicationPortalConsumes = { true }, + applicationRootConsumes = { true }, + ) + + assertFalse(DndRuntime.engine.isPointerCaptured) + assertFalse(DndRuntime.engine.isDragging) + } finally { + DndRuntime.engine.cancelActiveDrag() + } + } + private fun createHost(): DsglScreenHost = createHost(DomTree(ContainerNode(key = "root"))) private fun createHost(tree: DomTree): DsglScreenHost = @@ -343,6 +616,42 @@ class DsglScreenHostDomainOrchestrationTests { assertEquals(false, commands.any { command -> command is RenderCommand.DrawRect && command.color == color }) } + private fun SingleLineInputNode.buildCommandsForTest(): List = + ArrayList().also { out -> + buildRenderCommands(ctx, out) + } + + private fun activateStaticModal(overlay: ApplicationOverlayHost, modalKey: String) { + val modalTree = + ui { + modalPortal( + modals = + listOf( + ModalSpec(key = "static-modal") { + text("Static") + }, + ), + key = modalKey, + ) { + text("content") + } + } + modalTree.render(ctx, 300, 120) + overlay.render(ctx, 300, 120) + assertTrue(overlay.hasActiveModalPortal()) + } + + private fun clearStaticModal(overlay: ApplicationOverlayHost, modalKey: String) { + val emptyModalTree = + ui { + modalPortal(modals = emptyList(), key = modalKey) { + text("content") + } + } + emptyModalTree.render(ctx, 300, 120) + overlay.render(ctx, 300, 120) + } + private fun colorPickerLayoutProbe(): ColorPickerLayout = ColorPickerController( initial = diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPortalController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPortalController.kt index ea641d3..4582464 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPortalController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPortalController.kt @@ -80,6 +80,9 @@ internal class ColorPickerPortalController( fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = portalHost.dispatchInput { it.handleKeyDown(keyCode, keyChar) } + + fun handleKeyUp(keyCode: Int, keyChar: Char): Boolean = + portalHost.dispatchInput { it.handleKeyUp(keyCode, keyChar) } } private class ColorPickerPortalEntry( diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt index 09776ff..0334aba 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalDsl.kt @@ -4,7 +4,6 @@ import org.dreamfinity.dsgl.core.components.modal.internal.ModalPortalAnchorNode import org.dreamfinity.dsgl.core.components.modal.internal.ModalPortalRootNode import org.dreamfinity.dsgl.core.components.modal.internal.ModalPortalSessionStore import org.dreamfinity.dsgl.core.components.modal.internal.modalLifecycleKey -import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.elements.InputType import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.FocusManager @@ -79,26 +78,6 @@ private fun UiScope.modalLayer(spec: ModalSpec, modalKey: String, isTopMost: Boo val dialogKey = ModalPortalSessionStore.dialogKey(modalKey, spec.key) div({ key = "$modalKey.modal.${spec.key}.layer" - onMouseDown = { event -> - if (!isTopMost) { - event.cancelled = true - } else { - val insideDialog = isEventInsideDialog(event.target, dialogKey, event.mouseX, event.mouseY) - if (!insideDialog && spec.trapFocus) { - FocusManager.requestFocusFirstInSubtree(dialogKey) - } - event.cancelled = true - } - } - onMouseClick = { event -> - event.cancelled = true - } - onMouseWheel = { event -> - val insideDialog = isEventInsideDialog(event.target, dialogKey, event.mouseX, event.mouseY) - if (!insideDialog) { - event.cancelled = true - } - } style = { backgroundColor = spec.backdropColor() display = Display.Flex @@ -352,41 +331,3 @@ fun promptModal( }) } } - -private fun isTargetInsideDialog(target: DOMNode?, dialogKey: String): Boolean { - var node = target - while (node != null) { - if (node.key == dialogKey) return true - node = node.parent - } - return false -} - -private fun isEventInsideDialog( - target: DOMNode?, - dialogKey: String, - mouseX: Int, - mouseY: Int, -): Boolean { - if (isTargetInsideDialog(target, dialogKey)) return true - val root = target?.rootAncestor() ?: return false - val dialog = findNodeByKey(root, dialogKey) ?: return false - return dialog.bounds.contains(mouseX, mouseY) -} - -private fun DOMNode.rootAncestor(): DOMNode { - var current = this - while (current.parent != null) { - current = current.parent ?: return current - } - return current -} - -private fun findNodeByKey(root: DOMNode, key: Any?): DOMNode? { - if (root.key == key) return root - root.children.forEach { child -> - val found = findNodeByKey(child, key) - if (found != null) return found - } - return null -} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt index d171bda..76d6d20 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt @@ -6,6 +6,8 @@ import org.dreamfinity.dsgl.core.components.modal.ModalSpec import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.event.EventBus +import org.dreamfinity.dsgl.core.event.FocusManager +import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.overlay.PortalBackdropPolicy import org.dreamfinity.dsgl.core.overlay.PortalDismissPolicy @@ -20,14 +22,18 @@ import org.dreamfinity.dsgl.core.overlay.PortalHost import org.dreamfinity.dsgl.core.overlay.PortalInputPolicy import org.dreamfinity.dsgl.core.overlay.PortalInsidePointerPolicy import org.dreamfinity.dsgl.core.overlay.PortalPointerContainmentPolicy +import org.dreamfinity.dsgl.core.overlay.PortalPointerRegion import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces import org.dreamfinity.dsgl.core.overlay.evaluateOutsidePointerDown +import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter +@Suppress("TooManyFunctions") internal class ModalPortalController { private val portalHost: PortalHost = PortalHost(ScreenDomainSurfaces.ApplicationPortal) private val entriesByPortalKey: LinkedHashMap = LinkedHashMap() private var pendingPolicyPointerSequence: PendingPolicyPointerSequence? = null + private var pendingDomPointerSequence: PendingDomPointerSequence? = null fun sync(rootNode: DOMNode, viewportWidth: Int, viewportHeight: Int) { val snapshots = ModalPortalSessionStore.portalSnapshots() @@ -59,6 +65,7 @@ internal class ModalPortalController { } entriesByPortalKey.clear() pendingPolicyPointerSequence = null + pendingDomPointerSequence = null } fun commitActivePortals() { @@ -73,6 +80,95 @@ internal class ModalPortalController { fun hasActivePortal(): Boolean = entriesByPortalKey.values.any { entry -> entry.state.active } + fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean { + if (!hasActivePortal()) return false + val pendingEntry = pendingDomPointerSequence?.entry + if (pendingEntry != null) { + pendingEntry.handleDomMouseMove(mouseX, mouseY) + return true + } + val result = + portalHost.evaluateOutsidePointerDown(mouseX, mouseY) + ?: run { + clearDomPointerState() + return true + } + if (result.region == PortalPointerRegion.InsideEntry) { + val entry = result.entry as? ModalPortalEntry + clearDomPointerStateExcept(entry) + entry?.handleDomMouseMove(mouseX, mouseY) + } else { + clearDomPointerState() + } + return true + } + + fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { + val result = + portalHost.evaluateOutsidePointerDown(mouseX, mouseY) + ?: run { + clearDomPointerState() + return hasActivePortal() + } + if (result.region == PortalPointerRegion.InsideEntry) { + val entry = result.entry as? ModalPortalEntry ?: return true + clearDomPointerStateExcept(entry) + entry.handleDomMouseDown(mouseX, mouseY, button) + pendingDomPointerSequence = PendingDomPointerSequence(button = button, entry = entry) + pendingPolicyPointerSequence = null + return true + } + clearDomPointerState() + if (result.consumed) { + (result.entry as? ModalPortalEntry)?.requestFocusForOutsidePointer() + pendingPolicyPointerSequence = + PendingPolicyPointerSequence( + button = button, + dismissEntry = result.entry.takeIf { result.shouldClose }, + ) + pendingDomPointerSequence = null + } + return result.consumed + } + + fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { + val pendingDom = pendingDomPointerSequence + if (pendingDom != null && pendingDom.button == button) { + pendingDom.entry.handleDomMouseUp(mouseX, mouseY, button) + pendingDomPointerSequence = null + pendingPolicyPointerSequence = null + return true + } + val pendingPolicy = pendingPolicyPointerSequence + if (pendingPolicy != null && pendingPolicy.button == button) { + pendingPolicy.dismissEntry?.let { entry -> entry.state.dismiss(entry) } + clearDomPointerState() + pendingPolicyPointerSequence = null + pendingDomPointerSequence = null + return true + } + clearDomPointerState() + return hasActivePortal() + } + + fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean { + if (!hasActivePortal()) return false + val result = + portalHost.evaluateOutsidePointerDown(mouseX, mouseY) + ?: run { + clearDomPointerState() + return true + } + if (result.region == PortalPointerRegion.InsideEntry) { + val entry = result.entry as? ModalPortalEntry + clearDomPointerStateExcept(entry) + entry?.handleDomMouseWheel(mouseX, mouseY, delta) + } else { + clearDomPointerState() + } + return true + } + fun handlePointerPolicy( mouseX: Int, mouseY: Int, @@ -88,6 +184,9 @@ internal class ModalPortalController { } val result = portalHost.evaluateOutsidePointerDown(mouseX, mouseY) ?: return false if (result.consumed) { + if (result.region == PortalPointerRegion.OutsideEntry) { + (result.entry as? ModalPortalEntry)?.requestFocusForOutsidePointer() + } pendingPolicyPointerSequence = PendingPolicyPointerSequence( button = button, @@ -97,6 +196,16 @@ internal class ModalPortalController { return result.consumed } + fun handleWheelPolicy(mouseX: Int, mouseY: Int): Boolean { + val result = portalHost.evaluateOutsidePointerDown(mouseX, mouseY) ?: return false + return result.region == PortalPointerRegion.OutsideEntry && result.consumed + } + + fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = + portalHost.dispatchInput { + it.handleKeyDown(keyCode, keyChar) + } + internal fun debugActivePortalEntryIds(): List = portalHost.entriesInPaintOrder().map { it.state.id.value } internal fun debugEvaluatePointerDownPolicy(mouseX: Int, mouseY: Int) = @@ -131,6 +240,18 @@ internal class ModalPortalController { rootNode.children.removeAll(activeRoots.toSet()) rootNode.children.addAll(activeRoots) } + + private fun clearDomPointerState() { + entriesByPortalKey.values.forEach { entry -> entry.clearDomPointerState() } + } + + private fun clearDomPointerStateExcept(entryToKeep: ModalPortalEntry?) { + entriesByPortalKey.values.forEach { entry -> + if (entry !== entryToKeep) { + entry.clearDomPointerState() + } + } + } } private data class PendingPolicyPointerSequence( @@ -138,11 +259,21 @@ private data class PendingPolicyPointerSequence( val dismissEntry: PortalEntry?, ) +private data class PendingDomPointerSequence( + val button: MouseButton, + val entry: ModalPortalEntry, +) + +@Suppress("TooManyFunctions") private class ModalPortalEntry( val portalKey: String, templateRoot: ModalPortalRootNode, ) : PortalEntry { private var tree: DomTree = DomTree(templateRoot) + private val domInputRouter: LayerDomInputRouter = + LayerDomInputRouter( + rootProvider = { root }, + ) private var topMostModal: ModalSpec? = null val root: ModalPortalRootNode @@ -230,15 +361,38 @@ private class ModalPortalEntry( state.updateProtectedBounds(listOfNotNull(dialog?.bounds)) } + fun requestFocusForOutsidePointer() { + val topMost = topMostModal ?: return + if (!topMost.trapFocus) return + FocusManager.requestFocusFirstInSubtree(ModalPortalSessionStore.dialogKey(portalKey, topMost.key)) + } + fun detach() { root.parent ?.children ?.remove(root) root.parent = null + domInputRouter.clear() + } + + fun handleDomMouseMove(mouseX: Int, mouseY: Int): Boolean = domInputRouter.handleMouseMove(mouseX, mouseY) + + fun handleDomMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + domInputRouter.handleMouseDown(mouseX, mouseY, button) + + fun handleDomMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = + domInputRouter.handleMouseUp(mouseX, mouseY, button) + + fun handleDomMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = + domInputRouter.handleMouseWheel(mouseX, mouseY, delta) + + fun clearDomPointerState() { + domInputRouter.clear() } override fun clearRefs() { tree.clearRefs() + domInputRouter.clear() EventBus.run { root.clearListenersDeep() } } @@ -252,6 +406,15 @@ private class ModalPortalEntry( override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = false override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = false + + override fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean { + val topMost = topMostModal ?: return false + if (keyCode != KeyCodes.ESCAPE) return false + if (topMost.keyboard) { + topMost.onHide?.invoke() + } + return true + } } private fun findNode(root: DOMNode, predicate: (DOMNode) -> Boolean): DOMNode? { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/DebugDomainHosts.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/DebugDomainHosts.kt index 84f8d26..938f396 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/DebugDomainHosts.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/DebugDomainHosts.kt @@ -82,6 +82,7 @@ private data class DebugDomainToggleSnapshot( val systemOverlayInputEnabled: Boolean, ) +@Suppress("TooManyFunctions") class DebugDomainRootHost( private val state: OverlayLayerDebugState = OverlayLayerDebugState, ) : DomainSurfaceHost { @@ -139,6 +140,8 @@ class DebugDomainRootHost( override fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = domInputRouter.handleKeyDown(keyCode, keyChar) + override fun handleKeyUp(keyCode: Int, keyChar: Char): Boolean = domInputRouter.handleKeyUp(keyCode, keyChar) + override fun clearRefs() { layout = null lastToggleSnapshot = null @@ -183,6 +186,7 @@ class DebugDomainRootHost( } } +@Suppress("TooManyFunctions") class DebugDomainPortalHost : DomainSurfaceHost { override val surface: ScreenDomainSurface = ScreenDomainSurfaces.DebugPortal @@ -219,6 +223,9 @@ class DebugDomainPortalHost : DomainSurfaceHost { override fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = portalHost.dispatchInput { it.handleKeyDown(keyCode, keyChar) } + override fun handleKeyUp(keyCode: Int, keyChar: Char): Boolean = + portalHost.dispatchInput { it.handleKeyUp(keyCode, keyChar) } + override fun clearRefs() { portalHost.clearRefs() } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngine.kt index 42befc1..8d98bbf 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngine.kt @@ -122,6 +122,10 @@ object DefaultDndEngine : DndEngine { } override fun onMouseDown(_root: DOMNode, target: DOMNode?, event: MouseDownEvent) { + if (event.cancelled) { + pendingDrag = null + return + } if (event.mouseButton != MouseButton.LEFT) { pendingDrag = null return diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/RangeInputNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/RangeInputNode.kt index 29f49e5..59909f6 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/RangeInputNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/RangeInputNode.kt @@ -49,45 +49,22 @@ class RangeInputNode( if (this@RangeInputNode.styleDisabled) return@addEventListener if (event.mouseButton != MouseButton.LEFT) return@addEventListener if (!containsGlobalPoint(event.mouseX, event.mouseY)) return@addEventListener - activeDragIdentity = dragIdentity() - activeDragStartValue = this@RangeInputNode.value - val before = this@RangeInputNode.value - updateFromMouse(event.mouseX) - if (this@RangeInputNode.value != before) { - postInput( - this@RangeInputNode, - this@RangeInputNode.value.toString(), - this@RangeInputNode.value, - ) - } + beginActiveDrag(event.mouseX) + event.cancelled = true } this@RangeInputNode.addEventListener(Events.MOUSEUP) { event: MouseUpEvent -> if (this@RangeInputNode.styleDisabled) return@addEventListener if (event.mouseButton != MouseButton.LEFT) return@addEventListener if (!isActiveDragTarget()) return@addEventListener - val start = activeDragStartValue ?: this@RangeInputNode.value - if (this@RangeInputNode.value != start) { - postChange( - this@RangeInputNode, - this@RangeInputNode.value.toString(), - this@RangeInputNode.value, - ) - } - clearActiveDrag() + completeActiveDrag() + event.cancelled = true } this@RangeInputNode.addEventListener(Events.DRAG) { event: MouseDragEvent -> if (this@RangeInputNode.styleDisabled) return@addEventListener if (!isActiveDragTarget()) return@addEventListener val currentX = event.lastMouseX + event.dx - val before = this@RangeInputNode.value - updateFromMouse(currentX) - if (this@RangeInputNode.value != before) { - postInput( - this@RangeInputNode, - this@RangeInputNode.value.toString(), - this@RangeInputNode.value, - ) - } + updateActiveDrag(currentX) + event.cancelled = true } } } @@ -128,6 +105,37 @@ class RangeInputNode( bounds = Rect(x, y, width, height) } + override fun shouldCapturePointerDrag(mouseX: Int, mouseY: Int): Boolean = + !styleDisabled && containsGlobalPoint(mouseX, mouseY) + + override fun beginPointerCapture(mouseX: Int, mouseY: Int, button: MouseButton) { + if (button != MouseButton.LEFT || styleDisabled) return + if (isActiveDragTarget()) return + beginActiveDrag(mouseX) + } + + override fun continuePointerCapture( + mouseX: Int, + mouseY: Int, + mouseDX: Int, + mouseDY: Int, + button: MouseButton, + ) { + if (button != MouseButton.LEFT || styleDisabled || !isActiveDragTarget()) return + updateActiveDrag(mouseX) + } + + override fun endPointerCapture(mouseX: Int, mouseY: Int, button: MouseButton) { + if (button != MouseButton.LEFT || !isActiveDragTarget()) return + completeActiveDrag() + } + + override fun cancelPointerCapture() { + if (isActiveDragTarget()) { + clearActiveDrag() + } + } + override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) { val geometry = sliderGeometry() addBackgroundImageCommand(out) @@ -163,6 +171,36 @@ class RangeInputNode( setValue(next) } + private fun beginActiveDrag(mouseX: Int) { + activeDragIdentity = dragIdentity() + activeDragStartValue = this@RangeInputNode.value + updateActiveDrag(mouseX) + } + + private fun updateActiveDrag(mouseX: Int) { + val before = this@RangeInputNode.value + updateFromMouse(mouseX) + if (this@RangeInputNode.value != before) { + postInput( + this@RangeInputNode, + this@RangeInputNode.value.toString(), + this@RangeInputNode.value, + ) + } + } + + private fun completeActiveDrag() { + val start = activeDragStartValue ?: this@RangeInputNode.value + if (this@RangeInputNode.value != start) { + postChange( + this@RangeInputNode, + this@RangeInputNode.value.toString(), + this@RangeInputNode.value, + ) + } + clearActiveDrag() + } + private fun valueToX(geometry: SliderGeometry): Int { val trackRect = geometry.trackRect val knobSize = geometry.knobSize diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SingleLineInputNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SingleLineInputNode.kt index 1fb2d11..5ad1a60 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SingleLineInputNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SingleLineInputNode.kt @@ -75,12 +75,14 @@ open class SingleLineInputNode( if (event.mouseButton != MouseButton.LEFT) return@addEventListener if (!this@SingleLineInputNode.containsGlobalPoint(event.mouseX, event.mouseY)) return@addEventListener handlePointerDown(event.mouseX) + event.cancelled = true } this@SingleLineInputNode.addEventListener(Events.DRAG) { event: MouseDragEvent -> if (this@SingleLineInputNode.styleDisabled) return@addEventListener if (!isActiveSelectionDragTarget()) return@addEventListener val currentX = event.lastMouseX + event.dx updateSelectionFromPointerDrag(currentX) + event.cancelled = true } this@SingleLineInputNode.addEventListener(Events.MOUSEUP) { event: MouseUpEvent -> if (event.mouseButton != MouseButton.LEFT) return@addEventListener @@ -91,6 +93,7 @@ open class SingleLineInputNode( } editState.resetBlinkClock() persistState() + event.cancelled = true } this@SingleLineInputNode.addEventListener(Events.KEYDOWN) { event: KeyboardKeyDownEvent -> if (this@SingleLineInputNode.styleDisabled) return@addEventListener @@ -254,6 +257,42 @@ open class SingleLineInputNode( return containsGlobalPoint(mouseX, mouseY) } + override fun shouldCapturePointerDrag(mouseX: Int, mouseY: Int): Boolean = + shouldCaptureTextSelectionDrag(mouseX, mouseY) || super.shouldCapturePointerDrag(mouseX, mouseY) + + override fun beginPointerCapture(mouseX: Int, mouseY: Int, button: MouseButton) { + if (button != MouseButton.LEFT || styleDisabled) return + if (isActiveSelectionDragTarget()) return + handlePointerDown(mouseX) + } + + override fun continuePointerCapture( + mouseX: Int, + mouseY: Int, + mouseDX: Int, + mouseDY: Int, + button: MouseButton, + ) { + if (button != MouseButton.LEFT || styleDisabled || !isActiveSelectionDragTarget()) return + updateSelectionFromPointerDrag(mouseX) + } + + override fun endPointerCapture(mouseX: Int, mouseY: Int, button: MouseButton) { + if (button != MouseButton.LEFT || !isActiveSelectionDragTarget()) return + clearActiveDrag() + if (!editState.hasSelection()) { + editState.clearSelection() + } + editState.resetBlinkClock() + persistState() + } + + override fun cancelPointerCapture() { + if (isActiveSelectionDragTarget()) { + clearActiveDrag() + } + } + private fun handlePointerDown(mouseX: Int) { resetTypingUndoGroup() val target = caretIndexFromMouseX(mouseX) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextAreaNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextAreaNode.kt index e8d4340..65c658d 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextAreaNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/TextAreaNode.kt @@ -96,6 +96,7 @@ class TextAreaNode( } if (!this@TextAreaNode.containsGlobalPoint(event.mouseX, event.mouseY)) return@addEventListener handleTextPointerDown(event.mouseX, event.mouseY) + event.cancelled = true } this@TextAreaNode.addEventListener(Events.DRAG) { event: MouseDragEvent -> if (this@TextAreaNode.styleDisabled) return@addEventListener @@ -336,6 +337,65 @@ class TextAreaNode( fun shouldCaptureAnyDrag(mouseX: Int, mouseY: Int): Boolean = shouldCaptureScrollbarDrag(mouseX, mouseY) || shouldCaptureTextSelectionDrag(mouseX, mouseY) + override fun shouldCapturePointerDrag(mouseX: Int, mouseY: Int): Boolean = + shouldCaptureAnyDrag(mouseX, mouseY) || super.shouldCapturePointerDrag(mouseX, mouseY) + + override fun beginPointerCapture(mouseX: Int, mouseY: Int, button: MouseButton) { + if (button != MouseButton.LEFT || styleDisabled) return + if (isActiveScrollbarDragTarget() || isActiveSelectionDragTarget()) return + if (handleScrollbarMouseDown(mouseX, mouseY)) return + if (containsGlobalPoint(mouseX, mouseY)) { + handleTextPointerDown(mouseX, mouseY) + } + } + + override fun continuePointerCapture( + mouseX: Int, + mouseY: Int, + mouseDX: Int, + mouseDY: Int, + button: MouseButton, + ) { + if (button != MouseButton.LEFT || styleDisabled) return + when { + isActiveScrollbarDragTarget() -> { + updateScrollbarFromDrag(mouseY) + persistState() + } + + isActiveSelectionDragTarget() -> { + updateSelectionFromPointerDrag(mouseX, mouseY) + persistState() + } + } + } + + override fun endPointerCapture(mouseX: Int, mouseY: Int, button: MouseButton) { + if (button != MouseButton.LEFT) return + var handled = false + if (isActiveScrollbarDragTarget()) { + activeScrollbarDragIdentity = null + handled = true + } + if (isActiveSelectionDragTarget()) { + activeSelectionDragIdentity = null + if (!editState.hasSelection()) { + editState.clearSelection() + } + handled = true + } + if (handled) { + editState.resetBlinkClock() + persistState() + } + } + + override fun cancelPointerCapture() { + if (isActiveScrollbarDragTarget() || isActiveSelectionDragTarget()) { + clearActiveDrag() + } + } + override fun inspectorScrollOffset(): Pair? = 0 to editState.scrollY private fun handleKey(event: KeyboardKeyDownEvent) { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/FocusManager.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/FocusManager.kt index 2c15ddc..7fdf088 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/FocusManager.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/event/FocusManager.kt @@ -16,6 +16,12 @@ object FocusManager { /** Current focused node, if any. */ fun focusedNode(): DOMNode? = focused + /** Current focused node when it belongs to the given physical DOM root. */ + fun focusedNodeWithin(root: DOMNode): DOMNode? { + val currentFocus = focused ?: return null + return if (isSameOrAncestor(root, currentFocus)) currentFocus else null + } + /** Returns true if the given node is focused. */ fun isFocused(node: DOMNode): Boolean = focused === node @@ -44,6 +50,13 @@ object FocusManager { } } + /** Resolves and requests focus from a node reference. */ + fun requestFocusFrom(start: DOMNode?): Boolean { + val focusable = resolveFocusable(start) ?: return false + requestFocus(focusable) + return true + } + /** Clears current focus. */ fun clearFocus() { requestFocus(null) @@ -194,4 +207,13 @@ object FocusManager { } return null } + + private fun isSameOrAncestor(candidate: DOMNode, node: DOMNode?): Boolean { + var current = node + while (current != null) { + if (current === candidate) return true + current = current.parent + } + return false + } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ref/ElementHandle.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ref/ElementHandle.kt index 5e89efe..bbb9f28 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ref/ElementHandle.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ref/ElementHandle.kt @@ -25,9 +25,7 @@ internal class NodeElementHandle( get() = nodeRef?.bounds ?: Rect(0, 0, 0, 0) override fun requestFocus() { - val node = nodeRef ?: return - val focusable = FocusManager.resolveFocusable(node) ?: return - FocusManager.requestFocus(focusable) + FocusManager.requestFocusFrom(nodeRef) } @Suppress("ForbiddenComment") diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt index 1e3a0c6..ad99786 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt @@ -15,6 +15,7 @@ import org.dreamfinity.dsgl.core.select.SelectEngine import org.dreamfinity.dsgl.core.select.SelectPortalController import org.dreamfinity.dsgl.core.style.StyleApplicationScope +@Suppress("TooManyFunctions") class ApplicationOverlayHost( contextMenuEngine: ContextMenuEngine = DomainPortalServices.applicationContextMenuEngine, selectEngine: SelectEngine = DomainPortalServices.applicationSelectEngine, @@ -57,34 +58,54 @@ class ApplicationOverlayHost( override fun render(ctx: UiMeasureContext, width: Int, height: Int) { rootNode.setViewportBounds(width, height) - floatingWindowPortal.sync(rootNode, width, height) modalPortal.sync(rootNode, width, height) closeStaleFloatingPortalsAfterModalOpen() + floatingWindowPortal.sync(rootNode, width, height) tree.render(ctx, width, height) modalPortal.commitActivePortals() } override fun paint(ctx: UiMeasureContext): List = tree.paint(ctx, applyStyles = true) - override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = domInputRouter.handleMouseMove(mouseX, mouseY) + override fun handleMouseMove(mouseX: Int, mouseY: Int): Boolean = + if (modalPortal.hasActivePortal()) { + domInputRouter.clear() + modalPortal.handleMouseMove(mouseX, mouseY) + } else { + domInputRouter.handleMouseMove(mouseX, mouseY) + } override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { + if (modalPortal.hasActivePortal()) { + domInputRouter.clear() + return modalPortal.handleMouseDown(mouseX, mouseY, button) + } val isConsumedByDOM = domInputRouter.handleMouseDown(mouseX, mouseY, button) val isConsumedByPolicy = modalPortal.handlePointerPolicy(mouseX, mouseY, button, pressed = true) return isConsumedByDOM || isConsumedByPolicy } override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { + if (modalPortal.hasActivePortal()) { + domInputRouter.clear() + return modalPortal.handleMouseUp(mouseX, mouseY, button) + } val isConsumedByDOM = domInputRouter.handleMouseUp(mouseX, mouseY, button) val isConsumedByPolicy = modalPortal.handlePointerPolicy(mouseX, mouseY, button, pressed = false) return isConsumedByDOM || isConsumedByPolicy } override fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = - domInputRouter.handleMouseWheel(mouseX, mouseY, delta) + if (modalPortal.hasActivePortal()) { + modalPortal.handleMouseWheel(mouseX, mouseY, delta) + } else { + domInputRouter.handleMouseWheel(mouseX, mouseY, delta) + } override fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = domInputRouter.handleKeyDown(keyCode, keyChar) + override fun handleKeyUp(keyCode: Int, keyChar: Char): Boolean = domInputRouter.handleKeyUp(keyCode, keyChar) + override fun clearRefs() { tree.clearRefs() domInputRouter.clear() @@ -145,6 +166,8 @@ fun ApplicationOverlayHost.hasOpenSelectPortal(): Boolean = applicationSelectPor fun ApplicationOverlayHost.hasOpenColorPickerPortal(): Boolean = applicationColorPickerPortal.isOpen +fun ApplicationOverlayHost.hasActiveModalPortal(): Boolean = modalPortal.hasActivePortal() + fun ApplicationOverlayHost.hasActiveColorPickerEyedropper(): Boolean = applicationColorPickerPortal.hasActiveEyedropper fun ApplicationOverlayHost.captureColorPickerEyedropperSample() { @@ -152,6 +175,7 @@ fun ApplicationOverlayHost.captureColorPickerEyedropperSample() { } fun ApplicationOverlayHost.toggleFloatingWindowDemo(anchorX: Int, anchorY: Int) { + if (hasActiveModalPortal()) return floatingWindowPortal.toggle(anchorX, anchorY) } @@ -161,12 +185,20 @@ fun ApplicationOverlayHost.hasDomPointerTargetAt(mouseX: Int, mouseY: Int): Bool domInputRouter.hasPointerTargetAt(mouseX, mouseY) fun ApplicationOverlayHost.handlePortalKeyDownBeforeDom(keyCode: Int, keyChar: Char): Boolean = - applicationColorPickerPortal.handleKeyDown(keyCode, keyChar) + applicationColorPickerPortal.handleKeyDown(keyCode, keyChar) || + modalPortal.handleKeyDown(keyCode, keyChar) fun ApplicationOverlayHost.handlePortalKeyDownAfterDom(keyCode: Int, keyChar: Char): Boolean = applicationSelectPortal.handleKeyDown(keyCode, keyChar) || contextMenuPortal.handleKeyDown(keyCode) +fun ApplicationOverlayHost.handlePortalKeyUpBeforeDom(keyCode: Int, keyChar: Char): Boolean = + applicationColorPickerPortal.handleKeyUp(keyCode, keyChar) + +fun ApplicationOverlayHost.handlePortalKeyUpAfterDom(keyCode: Int, keyChar: Char): Boolean = + applicationSelectPortal.handleKeyUp(keyCode, keyChar) || + contextMenuPortal.handleKeyUp(keyCode, keyChar) + fun ApplicationOverlayHost.handlePortalPointerBeforeDom( mouseX: Int, mouseY: Int, @@ -266,6 +298,11 @@ internal class ContextMenuPortalController( portalHost.dispatchInput { it.handleKeyDown(keyCode, Char.MIN_VALUE) } + + fun handleKeyUp(keyCode: Int, keyChar: Char): Boolean = + portalHost.dispatchInput { + it.handleKeyUp(keyCode, keyChar) + } } private class ContextMenuPortalEntry( diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/DomainSurfaceHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/DomainSurfaceHost.kt index 921a239..8b4214c 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/DomainSurfaceHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/DomainSurfaceHost.kt @@ -24,4 +24,6 @@ interface DomainSurfaceHost { fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = false fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = false + + fun handleKeyUp(keyCode: Int, keyChar: Char): Boolean = false } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContracts.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContracts.kt index 16ef585..2e4b05c 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContracts.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContracts.kt @@ -156,6 +156,7 @@ internal data class PortalFrameContext( } } +@Suppress("TooManyFunctions") internal interface PortalEntry { val state: PortalEntryState val node: DOMNode? @@ -181,6 +182,8 @@ internal interface PortalEntry { fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = false fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = false + + fun handleKeyUp(keyCode: Int, keyChar: Char): Boolean = false } internal class PortalHost( diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouter.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouter.kt index e8cce97..081db73 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouter.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouter.kt @@ -9,6 +9,7 @@ import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.event.EventBus import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.event.KeyboardKeyDownEvent +import org.dreamfinity.dsgl.core.event.KeyboardKeyUpEvent import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.event.MouseClickEvent import org.dreamfinity.dsgl.core.event.MouseDownEvent @@ -168,14 +169,26 @@ class LayerDomInputRouter( clear() return false } - val focused = FocusManager.focusedNode() ?: return false - if (!isSameOrAncestor(root, focused)) return false + val focused = FocusManager.focusedNodeWithin(root) ?: return false val event = KeyboardKeyDownEvent(keyChar = keyChar, keyCode = keyCode) event.target = focused EventBus.post(event) return true } + fun handleKeyUp(keyCode: Int, keyChar: Char): Boolean { + val root = + rootProvider() ?: run { + clear() + return false + } + val focused = FocusManager.focusedNodeWithin(root) ?: return false + val event = KeyboardKeyUpEvent(keyChar = keyChar, keyCode = keyCode) + event.target = focused + EventBus.post(event) + return true + } + fun hasPointerTargetAt(mouseX: Int, mouseY: Int): Boolean { val root = rootProvider() ?: return false if (dragCaptureTarget != null) return true @@ -226,9 +239,6 @@ class LayerDomInputRouter( private fun releaseDragCapture() { dragCaptureTarget?.cancelPointerCapture() - RangeInputNode.clearActiveDrag() - SingleLineInputNode.clearActiveDrag() - TextAreaNode.clearActiveDrag() dragCaptureTarget = null dragCaptureKey = null dragCaptureClass = null @@ -275,6 +285,9 @@ class LayerDomInputRouter( private fun hasFocusChangedSinceCapture(): Boolean { if (dragCaptureFocusKey == null) return false + val captured = dragCaptureTarget + val currentFocus = FocusManager.focusedNode() + if (captured != null && isSameOrAncestor(captured, currentFocus)) return false val currentFocusKey = FocusManager.focusedNode()?.key return currentFocusKey != dragCaptureFocusKey } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt index ffffad7..c1ca9fc 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt @@ -71,6 +71,8 @@ internal interface SystemOverlayEntry { fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean = false fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = false + + fun handleKeyUp(keyCode: Int, keyChar: Char): Boolean = false } class SystemOverlayTransientSession( @@ -174,6 +176,8 @@ internal class SystemOverlayPortalEntry( override fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = entry.handleKeyDown(keyCode, keyChar) + override fun handleKeyUp(keyCode: Int, keyChar: Char): Boolean = entry.handleKeyUp(keyCode, keyChar) + private fun SystemOverlayEntry.inputPolicy(): PortalInputPolicy = when { participatesInDomInput() || enablesDomInputFallbackRouting() -> PortalInputPolicy.ManualThenDomFallback diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt index 33cf624..928cf61 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt @@ -121,6 +121,8 @@ class SystemOverlayHost( fun handlePortalKeyDown(keyCode: Int, keyChar: Char): Boolean = systemSelectPortal.handleKeyDown(keyCode, keyChar) + fun handlePortalKeyUp(keyCode: Int, keyChar: Char): Boolean = systemSelectPortal.handleKeyUp(keyCode, keyChar) + fun handlePortalMouseMove(mouseX: Int, mouseY: Int): Boolean = systemSelectPortal.handleMouseMove(mouseX, mouseY) fun handlePortalMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = @@ -206,6 +208,12 @@ class SystemOverlayHost( domFallbackDispatch = { domInputRouter.handleKeyDown(keyCode, keyChar) }, ) + override fun handleKeyUp(keyCode: Int, keyChar: Char): Boolean = + dispatchManualThenDomFallback( + manualDispatch = { dispatchManualInput { entry -> entry.handleKeyUp(keyCode, keyChar) } }, + domFallbackDispatch = { domInputRouter.handleKeyUp(keyCode, keyChar) }, + ) + override fun clearRefs() { tree.clearRefs() transientOwnershipRegistry.clear() diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt index 159f00b..af18c83 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt @@ -421,16 +421,17 @@ class SelectEngine( } fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = - handleMouseDown(mouseX, mouseY, button, closeOutside = true) + handleMouseDown(mouseX, mouseY, button, closeOutside = true, consumeOutside = true) internal fun handlePortalMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = - handleMouseDown(mouseX, mouseY, button, closeOutside = false) + handleMouseDown(mouseX, mouseY, button, closeOutside = true, consumeOutside = false) private fun handleMouseDown( mouseX: Int, mouseY: Int, button: MouseButton, closeOutside: Boolean, + consumeOutside: Boolean, ): Boolean { val current = popup ?: return false if (visibilityState == VisibilityState.Hidden) return false @@ -441,7 +442,7 @@ class SelectEngine( if (closeOutside) { startVisibilityTransition(0f, style.closeDurationMs) } - true + consumeOutside } button != MouseButton.LEFT && button != MouseButton.RIGHT -> true diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt index 43e4c3f..e4adf2a 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt @@ -26,10 +26,10 @@ import org.dreamfinity.dsgl.core.overlay.PortalPointerDispatch import org.dreamfinity.dsgl.core.overlay.PortalPointerPolicyResult import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces import org.dreamfinity.dsgl.core.overlay.evaluateOutsidePointerDown -import org.dreamfinity.dsgl.core.overlay.handleOutsidePointerDownPolicy import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand +@Suppress("TooManyFunctions") internal class SelectPortalController( private val engine: SelectEngine, ownerScope: OverlayOwnerScope, @@ -79,8 +79,16 @@ internal class SelectPortalController( portalHost.dispatchInput { it.handleMouseMove(mouseX, mouseY) } override fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = - portalHost.dispatchInput { it.handleMouseDown(mouseX, mouseY, button) } || - portalHost.handleOutsidePointerDownPolicy(mouseX, mouseY) + if (portalHost.dispatchInput { it.handleMouseDown(mouseX, mouseY, button) }) { + true + } else { + val outside = portalHost.evaluateOutsidePointerDown(mouseX, mouseY) + if (outside?.shouldClose == true) { + outside.entry.state + .dismiss(outside.entry) + } + false + } override fun handleMouseUp(mouseX: Int, mouseY: Int, button: MouseButton): Boolean = portalHost.dispatchInput { it.handleMouseUp(mouseX, mouseY, button) } @@ -91,6 +99,9 @@ internal class SelectPortalController( fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = portalHost.dispatchInput { it.handleKeyDown(keyCode, keyChar) } + fun handleKeyUp(keyCode: Int, keyChar: Char): Boolean = + portalHost.dispatchInput { it.handleKeyUp(keyCode, keyChar) } + internal fun debugPortalState(mouseX: Int, mouseY: Int): SelectPortalDebugState = SelectPortalDebugState( node = entry.node, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt index 06453ea..d583067 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt @@ -367,17 +367,17 @@ object StyleEngine { fun lastStyleApplyReport(): StyleApplyReport = lastApplyReport fun currentStyleRevision(scope: StyleApplicationScope = StyleApplicationScope.Application): Long { - val base = - when (scope) { - StyleApplicationScope.Application -> { - (StylesheetManager.snapshot().version shl 2) xor - (themeVersion shl 1) xor - inspectorOverridesVersion - } - - StyleApplicationScope.SystemOverlay -> 0L - StyleApplicationScope.Debug -> 0L + val snapshotVersion = snapshotForScope(scope).version + val inspectorVersion = + if (scope == StyleApplicationScope.Application) { + inspectorOverridesVersion + } else { + 0L } + val base = + (snapshotVersion shl 2) xor + (themeVersion shl 1) xor + inspectorVersion return base xor (pseudoStateVersion shl 3) xor (selectorStateVersion shl 4) xor diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalKeyboardRegressionTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalKeyboardRegressionTests.kt index c76bef8..b4bb324 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalKeyboardRegressionTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalKeyboardRegressionTests.kt @@ -24,6 +24,9 @@ import org.dreamfinity.dsgl.core.host.Viewport import org.dreamfinity.dsgl.core.overlay.ApplicationOverlayHost import org.dreamfinity.dsgl.core.overlay.PortalPointerRegion import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces +import org.dreamfinity.dsgl.core.overlay.handlePortalKeyDownBeforeDom +import org.dreamfinity.dsgl.core.overlay.isFloatingWindowDemoOpen +import org.dreamfinity.dsgl.core.overlay.toggleFloatingWindowDemo import org.dreamfinity.dsgl.core.render.RenderCommand import kotlin.test.AfterTest import kotlin.test.Test @@ -128,6 +131,28 @@ class ModalPortalKeyboardRegressionTests { assertEquals(listOf("application.modal.$hostKey"), overlay.modalPortal.debugActivePortalEntryIds()) } + @Test + fun `modal activation detaches stale application floating window before paint`() { + val hostKey = "tests.modal.portal.floating.stale" + val overlay = ApplicationOverlayHost() + overlays += overlay + + overlay.onInputFrame(320, 180) + overlay.toggleFloatingWindowDemo(anchorX = 24, anchorY = 24) + overlay.render(measureContext, 320, 180) + val floatingNode = overlay.floatingWindowPortal.debugNode() + assertTrue(floatingNode.parent === overlay.rootNode) + + val tree = buildTree(hostKey, listOf(basicModal())) + trees += tree + tree.render(measureContext, 320, 180) + overlay.render(measureContext, 320, 180) + + assertFalse(overlay.isFloatingWindowDemoOpen()) + assertTrue(floatingNode.parent == null) + assertEquals(listOf("application.modal.$hostKey"), overlay.modalPortal.debugActivePortalEntryIds()) + } + @Test fun `modal portal blocks application root click through`() { val hostKey = "tests.modal.portal.portal.input" @@ -142,6 +167,24 @@ class ModalPortalKeyboardRegressionTests { assertTrue(overlay.handleMouseDown(4, 4, MouseButton.LEFT)) } + @Test + fun `modal portal escape is handled before focused application root`() { + val hostKey = "tests.modal.portal.escape.pre.dom" + var hideCount = 0 + val tree = buildTreeWithContentInput(hostKey, listOf(dismissibleBodyModal { hideCount += 1 })) + trees += tree + tree.render(measureContext, 320, 180) + val focusedContentInput = requireNodeByKey(tree.root, "$hostKey.content.input") + FocusManager.requestFocus(focusedContentInput) + + val overlay = ApplicationOverlayHost() + overlays += overlay + overlay.render(measureContext, 320, 180) + + assertTrue(overlay.handlePortalKeyDownBeforeDom(KeyCodes.ESCAPE, 0.toChar())) + assertEquals(1, hideCount) + } + @Test fun `modal portal generic policy classifies dialog as inside and backdrop as outside`() { val hostKey = "tests.modal.portal.portal.policy" diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalPointerRegressionTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalPointerRegressionTests.kt new file mode 100644 index 0000000..e9cb370 --- /dev/null +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalPointerRegressionTests.kt @@ -0,0 +1,259 @@ +package org.dreamfinity.dsgl.core.components.modal + +import org.dreamfinity.dsgl.core.DomTree +import org.dreamfinity.dsgl.core.components.modal.internal.ModalPortalSessionStore +import org.dreamfinity.dsgl.core.dom.applyParent +import org.dreamfinity.dsgl.core.dom.elements.ButtonNode +import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.dsl.UiScope +import org.dreamfinity.dsgl.core.dsl.button +import org.dreamfinity.dsgl.core.dsl.div +import org.dreamfinity.dsgl.core.dsl.text +import org.dreamfinity.dsgl.core.dsl.ui +import org.dreamfinity.dsgl.core.event.EventBus +import org.dreamfinity.dsgl.core.event.FocusManager +import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.overlay.ApplicationOverlayHost +import org.dreamfinity.dsgl.core.overlay.hasActiveModalPortal +import org.dreamfinity.dsgl.core.overlay.isFloatingWindowDemoOpen +import org.dreamfinity.dsgl.core.overlay.toggleFloatingWindowDemo +import org.dreamfinity.dsgl.core.render.RenderCommand +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class ModalPortalPointerRegressionTests { + private val trees: MutableList = ArrayList() + private val overlays: MutableList = ArrayList() + private val hostKeys: MutableSet = LinkedHashSet() + private val measureContext = + object : UiMeasureContext { + override fun measureText(text: String): Int = text.length * 6 + + override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * 6 + + override val fontHeight: Int = 9 + + override fun fontHeight(fontId: String?, fontSize: Int?): Int = 9 + + override fun paint(commands: List) = Unit + } + + @AfterTest + fun cleanup() { + FocusManager.clearFocus() + EventBus.run { + trees.forEach { tree -> + tree.clearRefs() + tree.root.clearListenersDeep() + } + } + overlays.forEach { overlay -> overlay.clearRefs() } + hostKeys.forEach(ModalPortalSessionStore::forgetPortal) + trees.clear() + overlays.clear() + hostKeys.clear() + } + + @Test + fun `active modal prevents application floating window from opening`() { + val hostKey = "tests.modal.portal.floating.blocked" + val overlay = ApplicationOverlayHost() + overlays += overlay + val tree = buildTree(hostKey, listOf(staticModal())) + trees += tree + + tree.render(measureContext, 320, 180) + overlay.render(measureContext, 320, 180) + assertTrue(overlay.hasActiveModalPortal()) + + overlay.toggleFloatingWindowDemo(anchorX = 24, anchorY = 24) + overlay.render(measureContext, 320, 180) + + assertFalse(overlay.isFloatingWindowDemoOpen()) + assertTrue( + overlay.floatingWindowPortal + .debugNode() + .parent == null, + ) + } + + @Test + fun `static modal backdrop pointer press does not activate modal layer`() { + val hostKey = "tests.modal.portal.static.backdrop.active" + val overlay = renderStaticModalOverlay(hostKey) + val layer = overlay.modalPortal.debugFindNodeByKey("$hostKey.modal.modal.static.layer") + assertNotNull(layer) + + assertTrue(overlay.handleMouseDown(4, 4, MouseButton.LEFT)) + assertFalse(layer.styleActive) + assertTrue(overlay.handleMouseUp(4, 4, MouseButton.LEFT)) + assertFalse(layer.styleActive) + } + + @Test + fun `static modal dialog pointer press does not activate modal layer`() { + val hostKey = "tests.modal.portal.static.dialog.active" + val overlay = renderStaticModalOverlay(hostKey) + val layer = overlay.modalPortal.debugFindNodeByKey("$hostKey.modal.modal.static.layer") + val dialog = overlay.modalPortal.debugFindNodeByKey(ModalPortalSessionStore.dialogKey(hostKey, "modal.static")) + assertNotNull(layer) + assertNotNull(dialog) + val clickX = dialog.bounds.x + dialog.bounds.width / 2 + val clickY = dialog.bounds.y + dialog.bounds.height / 2 + + assertTrue(overlay.handleMouseDown(clickX, clickY, MouseButton.LEFT)) + assertFalse(layer.styleActive) + assertTrue(overlay.handleMouseUp(clickX, clickY, MouseButton.LEFT)) + assertFalse(layer.styleActive) + } + + @Test + fun `static modal dialog control receives pointer through modal owned routing`() { + val hostKey = "tests.modal.portal.static.dialog.button" + var clicks = 0 + val tree = + buildTree( + hostKey = hostKey, + modals = + listOf( + staticModal { + button("OK", { + key = "$hostKey.modal.button" + onMouseClick = { clicks += 1 } + }) + }, + ), + ) + trees += tree + tree.render(measureContext, 320, 180) + val overlay = + ApplicationOverlayHost().also { overlay -> + overlays += overlay + overlay.render(measureContext, 320, 180) + } + val button = overlay.modalPortal.debugFindNodeByKey("$hostKey.modal.button") + assertNotNull(button) + val clickX = button.bounds.x + button.bounds.width / 2 + val clickY = button.bounds.y + button.bounds.height / 2 + + assertTrue(overlay.handleMouseDown(clickX, clickY, MouseButton.LEFT)) + assertTrue(overlay.handleMouseUp(clickX, clickY, MouseButton.LEFT)) + + assertTrue(clicks == 1) + } + + @Test + fun `static modal clears dialog hover when pointer moves to backdrop`() { + val hostKey = "tests.modal.portal.static.dialog.hover.clear" + val overlay = renderStaticModalWithButton(hostKey) + val button = overlay.modalPortal.debugFindNodeByKey("$hostKey.modal.button") + assertNotNull(button) + val hoverX = button.bounds.x + button.bounds.width / 2 + val hoverY = button.bounds.y + button.bounds.height / 2 + + assertTrue(overlay.handleMouseMove(hoverX, hoverY)) + assertTrue(button.styleHovered) + + assertTrue(overlay.handleMouseMove(4, 4)) + assertFalse(button.styleHovered) + assertFalse(button.styleActive) + } + + @Test + fun `static modal backdrop move does not activate modal nodes`() { + val hostKey = "tests.modal.portal.static.backdrop.move" + val overlay = renderStaticModalWithButton(hostKey) + val layer = overlay.modalPortal.debugFindNodeByKey("$hostKey.modal.modal.static.layer") + val dialog = overlay.modalPortal.debugFindNodeByKey(ModalPortalSessionStore.dialogKey(hostKey, "modal.static")) + val button = overlay.modalPortal.debugFindNodeByKey("$hostKey.modal.button") + assertNotNull(layer) + assertNotNull(dialog) + assertNotNull(button) + + assertTrue(overlay.handleMouseMove(4, 4)) + + assertFalse(layer.styleActive) + assertFalse(dialog.styleActive) + assertFalse(button.styleActive) + assertFalse(button.styleHovered) + } + + @Test + fun `active modal mouse move clears stale application portal hover`() { + val hostKey = "tests.modal.portal.static.lower.hover.clear" + val overlay = ApplicationOverlayHost() + overlays += overlay + overlay.rootNode.setViewportBounds(320, 180) + val lowerButton = + ButtonNode("Lower", key = "$hostKey.lower.button").apply { + bounds = Rect(0, 0, 80, 24) + } + lowerButton.applyParent(overlay.rootNode) + + assertTrue(overlay.handleMouseMove(12, 12)) + assertTrue(lowerButton.styleHovered) + + val tree = buildTree(hostKey, listOf(staticModal())) + trees += tree + tree.render(measureContext, 320, 180) + overlay.render(measureContext, 320, 180) + + assertTrue(overlay.handleMouseMove(4, 4)) + assertFalse(lowerButton.styleHovered) + } + + private fun renderStaticModalOverlay(hostKey: String): ApplicationOverlayHost { + val tree = buildTree(hostKey, listOf(staticModal())) + trees += tree + tree.render(measureContext, 320, 180) + return ApplicationOverlayHost().also { overlay -> + overlays += overlay + overlay.render(measureContext, 320, 180) + } + } + + private fun renderStaticModalWithButton(hostKey: String): ApplicationOverlayHost { + val tree = + buildTree( + hostKey = hostKey, + modals = + listOf( + staticModal { + button("OK", { + key = "$hostKey.modal.button" + onMouseClick = {} + }) + }, + ), + ) + trees += tree + tree.render(measureContext, 320, 180) + return ApplicationOverlayHost().also { overlay -> + overlays += overlay + overlay.render(measureContext, 320, 180) + } + } + + private fun buildTree(hostKey: String, modals: List): DomTree { + hostKeys += hostKey + return ui { + modalPortal(modals = modals, key = hostKey) { + div({ key = "$hostKey.content" }) + } + } + } + + private fun staticModal(content: UiScope.() -> Unit = {}): ModalSpec = + ModalSpec( + key = "modal.static", + backdrop = BackdropMode.Static, + keyboard = false, + ) { _ -> + text("Static") + content() + } +} diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngineTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngineTests.kt index 5256ea6..665bee7 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngineTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngineTests.kt @@ -2,15 +2,29 @@ package org.dreamfinity.dsgl.core.dnd.internal import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode +import org.dreamfinity.dsgl.core.dom.elements.RangeInputNode +import org.dreamfinity.dsgl.core.dom.elements.TextInputNode import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.event.EventBus +import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.event.MouseDownEvent +import org.dreamfinity.dsgl.core.event.MouseDragEvent import org.dreamfinity.dsgl.core.event.collectHoverChain import org.dreamfinity.dsgl.core.render.RenderCommand +import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertSame +import kotlin.test.assertTrue class DefaultDndEngineTests { + @AfterTest + fun cleanup() { + DefaultDndEngine.cancelActiveDrag() + } + @Test fun `drop target selection prefers deepest candidate over previous ancestor`() { val list = ContainerNode(key = "list") @@ -80,6 +94,96 @@ class DefaultDndEngineTests { assertNull(selected) } + @Test + fun `cancelled mouse down does not arm dnd`() { + val root = ContainerNode(key = "dnd-cancel-root") + val draggable = + ContainerNode(key = "dnd-cancel-source") + .apply { + draggable = true + width = 80 + height = 20 + }.applyParent(root) + root.render(testMeasureContext(), 0, 0, 200, 120) + val down = + MouseDownEvent(10, 10, MouseButton.LEFT).also { event -> + event.target = draggable + event.cancelled = true + } + + DefaultDndEngine.onMouseDown(root, draggable, down) + DefaultDndEngine.onMouseMove(root, 80, 10) + + assertFalse(DefaultDndEngine.isDragging) + } + + @Test + fun `range input consumes pointer sequence before draggable ancestor can arm dnd`() { + val root = ContainerNode(key = "dnd-range-root") + val draggableParent = + ContainerNode(key = "dnd-range-parent") + .apply { + draggable = true + width = 160 + height = 40 + }.applyParent(root) + val range = + RangeInputNode(value = 0L, min = 0L, max = 100L, key = "dnd-range") + .applyParent(draggableParent) + root.render(testMeasureContext(), 0, 0, 200, 120) + range.render(testMeasureContext(), 10, 10, 120, 12) + val down = + MouseDownEvent(10, 16, MouseButton.LEFT).also { event -> + event.target = range + } + val drag = + MouseDragEvent(10, 16, 110, 0, MouseButton.LEFT).also { event -> + event.target = range + } + + EventBus.post(down) + DefaultDndEngine.onMouseDown(root, range, down) + EventBus.post(drag) + DefaultDndEngine.onMouseMove(root, 120, 16) + + assertTrue(range.value > 0L) + assertTrue(drag.cancelled) + assertFalse(DefaultDndEngine.isDragging) + } + + @Test + fun `text input selection drag consumes before draggable ancestor can arm dnd`() { + val root = ContainerNode(key = "dnd-text-root") + val draggableParent = + ContainerNode(key = "dnd-text-parent") + .apply { + draggable = true + width = 180 + height = 40 + }.applyParent(root) + val input = + TextInputNode(text = "abcdef", key = "dnd-text-input") + .applyParent(draggableParent) + root.render(testMeasureContext(), 0, 0, 240, 120) + input.render(testMeasureContext(), 10, 10, 120, 20) + val down = + MouseDownEvent(12, 16, MouseButton.LEFT).also { event -> + event.target = input + } + val drag = + MouseDragEvent(12, 16, 90, 0, MouseButton.LEFT).also { event -> + event.target = input + } + + EventBus.post(down) + DefaultDndEngine.onMouseDown(root, input, down) + EventBus.post(drag) + DefaultDndEngine.onMouseMove(root, 102, 16) + + assertFalse(DefaultDndEngine.isDragging) + assertTrue(drag.cancelled) + } + @Test fun `hover chain remains coherent for dnd candidate selection after core hover migration`() { val root = ContainerNode(key = "dnd-root") @@ -120,4 +224,15 @@ class DefaultDndEngineTests { assertSame(child, selected) assertEquals(listOf(root, parent, child), chain) } + + private fun testMeasureContext(): UiMeasureContext = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/DomTreeFontSizeInvalidationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/DomTreeFontSizeInvalidationTests.kt new file mode 100644 index 0000000..e30e98c --- /dev/null +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/elements/DomTreeFontSizeInvalidationTests.kt @@ -0,0 +1,66 @@ +package org.dreamfinity.dsgl.core.dom.elements + +import org.dreamfinity.dsgl.core.DomTree +import org.dreamfinity.dsgl.core.dom.applyParent +import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.render.RenderCommand +import org.dreamfinity.dsgl.core.style.Display +import org.dreamfinity.dsgl.core.style.StyleEngine +import org.dreamfinity.dsgl.core.style.StyleProperty +import kotlin.math.roundToInt +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class DomTreeFontSizeInvalidationTests { + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 10 + + override fun measureText(text: String): Int = text.length * 6 + + override fun measureText(text: String, fontId: String?, fontSize: Int?): Int { + val glyphWidth = ((fontSize ?: 16) * 0.5f).roundToInt().coerceAtLeast(1) + return text.length * glyphWidth + } + + override fun fontHeight(fontId: String?, fontSize: Int?): Int = (fontSize ?: fontHeight).coerceAtLeast(1) + + override fun paint(commands: List) = Unit + } + + @AfterTest + fun cleanup() { + StyleEngine.clearAllInspectorOverrides() + StyleEngine.clearCache() + } + + @Test + fun `inspector font-size override relayouts text before repaint`() { + val root = + ContainerNode(key = "inspector.font-size.root").apply { + display = Display.Block + width = 240 + } + val node = + TextNode(TextSource.Static("inspector"), key = "inspector.font-size.text") + .apply { + fontSize = 16 + }.applyParent(root) + val tree = DomTree(root) + + tree.render(ctx, 240, 80) + val baselineHeight = node.bounds.height + + StyleEngine + .setInspectorOverrideLiteral(node, StyleProperty.FONT_SIZE, "32px") + .getOrThrow() + val commands = tree.paint(ctx) + val draw = commands.filterIsInstance().single() + + assertEquals(32, node.appliedComputedStyleSnapshot()?.fontSize) + assertEquals(32, draw.fontSize) + assertTrue(node.bounds.height > baselineHeight) + } +} diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt index 95c119a..1136000 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt @@ -443,7 +443,7 @@ class LiveLayerInteractionPathTests { } @Test - fun `application select portal blocks app-root fallthrough on outside dismiss`() { + fun `application select portal closes and allows app-root fallthrough on outside dismiss`() { val applicationOverlayHost = ApplicationOverlayHost() applicationOverlayHost.onInputFrame(320, 180) val owner = "application-select-dismiss" @@ -469,8 +469,8 @@ class LiveLayerInteractionPathTests { true } - assertEquals(ScreenDomainSurfaces.ApplicationPortal, consumedBy) - assertFalse(appRootReceived) + assertEquals(ScreenDomainSurfaces.ApplicationRoot, consumedBy) + assertTrue(appRootReceived) } @Test diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouterTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouterTests.kt index ad736c8..3de801d 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouterTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouterTests.kt @@ -143,6 +143,37 @@ class LayerDomInputRouterTests { assertEquals("b", inputB.text) } + @Test + fun `keyboard key up follows focused node root membership`() { + val (rootA, routerA) = createLayerRouter("layer-a") + val (rootB, routerB) = createLayerRouter("layer-b") + val keyUps = mutableListOf() + + val parentA = + ContainerNode(key = "a-parent").apply { + onKeyUp = { keyUps += "parent:${it.keyChar}" } + } + parentA.applyParent(rootA) + val inputA = + TextInputNode(text = "a", key = "a-input").apply { + bounds = Rect(10, 10, 100, 20) + onKeyUp = { keyUps += "input:${it.keyChar}" } + } + inputA.applyParent(parentA) + + val inputB = + TextInputNode(text = "b", key = "b-input").apply { + bounds = Rect(10, 10, 100, 20) + onKeyUp = { keyUps += "wrong:${it.keyChar}" } + } + inputB.applyParent(rootB) + + FocusManager.requestFocus(inputA) + assertTrue(routerA.handleKeyUp(KeyCodes.Z, 'z')) + assertFalse(routerB.handleKeyUp(KeyCodes.X, 'x')) + assertEquals(listOf("input:z", "parent:z"), keyUps) + } + @Test fun `pointer drag capture is generic for header and thumb style controls`() { listOf("header-drag", "thumb-drag").forEach { key -> @@ -217,6 +248,63 @@ class LayerDomInputRouterTests { assertTrue(range.value > 0L) } + @Test + fun `range input drag survives focus changing to captured control`() { + val (root, router) = createLayerRouter("range-drag-focus") + val previousFocus = + TextInputNode(text = "before", key = "range-drag-focus-before").apply { + bounds = Rect(4, 4, 80, 20) + } + previousFocus.applyParent(root) + val range = RangeInputNode(value = 0L, min = 0L, max = 100L, key = "range-drag-focus-range") + range.applyParent(root) + range.render(ctx, 20, 40, 120, 12) + + FocusManager.requestFocus(previousFocus) + assertTrue(router.handleMouseDown(20, 46, MouseButton.LEFT)) + assertTrue(FocusManager.isFocused(range)) + assertTrue(router.handleMouseMove(220, 46)) + assertTrue(router.handleMouseUp(220, 46, MouseButton.LEFT)) + assertTrue(range.value > 0L) + } + + @Test + fun `range input drag is not cancelled by unrelated router clear`() { + val (rootA, routerA) = createLayerRouter("range-drag-owner") + val (_, routerB) = createLayerRouter("unrelated-router") + val range = RangeInputNode(value = 0L, min = 0L, max = 100L, key = "owned-range") + range.applyParent(rootA) + range.render(ctx, 20, 20, 120, 12) + + assertTrue(routerA.handleMouseDown(20, 26, MouseButton.LEFT)) + routerB.clear() + assertTrue(routerA.handleMouseMove(220, 26)) + assertTrue(routerA.handleMouseUp(220, 26, MouseButton.LEFT)) + + assertEquals(100L, range.value) + } + + @Test + fun `text selection drag is not cancelled by unrelated router clear`() { + ClipboardBridge.install(clipboard) + val (rootA, routerA) = createLayerRouter("text-drag-owner") + val (_, routerB) = createLayerRouter("unrelated-text-router") + val input = + TextInputNode(text = "abcdef", key = "owned-text").apply { + bounds = Rect(20, 20, 160, 20) + } + input.applyParent(rootA) + + assertTrue(routerA.handleMouseDown(20, 26, MouseButton.LEFT)) + routerB.clear() + assertTrue(routerA.handleMouseMove(80, 26)) + assertTrue(routerA.handleMouseUp(80, 26, MouseButton.LEFT)) + KeyModifiers.sync(shift = false, control = true, meta = false) + assertTrue(routerA.handleKeyDown(KeyCodes.C, 'c')) + + assertTrue(clipboard.value.isNotEmpty()) + } + @Test fun `range input drag survives unkeyed rerender replacement`() { val (root, router) = createLayerRouter("range-rerender") diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalControllerTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalControllerTests.kt index a398a36..bcc75a2 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalControllerTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalControllerTests.kt @@ -97,7 +97,7 @@ class SelectPortalControllerTests { } @Test - fun `outside press uses generic portal policy and closes without manual outside routing`() { + fun `outside press uses generic portal policy closes and passes through`() { val fixture = openSelect() val outsideX = 2 val outsideY = 2 @@ -110,7 +110,7 @@ class SelectPortalControllerTests { assertEquals(PortalPointerRegion.OutsideEntry, policy.region) assertTrue(policy.shouldClose) assertTrue(policy.consumed) - assertTrue(fixture.controller.handleMouseDown(outsideX, outsideY, MouseButton.LEFT)) + assertFalse(fixture.controller.handleMouseDown(outsideX, outsideY, MouseButton.LEFT)) fixture.clock.advance(CLOSE_DURATION_MS + 1) fixture.controller.onFrame(ctx, VIEWPORT_WIDTH, VIEWPORT_HEIGHT, 1f) From 0db3d7c20a9c8183840f87f14b44f00ab6c25f04 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Tue, 2 Jun 2026 22:47:34 +0300 Subject: [PATCH 72/78] fixing modals issue; --- .../dsgl/mcForge1710/DsglScreenHost.kt | 71 +++++- ...glScreenHostApplicationPortalFrameTests.kt | 207 ++++++++++++++++++ .../modal/internal/ModalPortalNode.kt | 1 + .../overlay/ApplicationOverlayRootNode.kt | 1 + .../modal/ModalPortalLayoutRegressionTests.kt | 118 ++++++++++ 5 files changed, 393 insertions(+), 5 deletions(-) create mode 100644 adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostApplicationPortalFrameTests.kt create mode 100644 core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalLayoutRegressionTests.kt diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index e40cd1b..c52ef57 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -213,8 +213,9 @@ abstract class DsglScreenHost( dsglMouseX = frameCursor.mouseX, dsglMouseY = frameCursor.mouseY, ) - cancelApplicationRootDndBehindModal() - val applicationOverlayCommands = collectApplicationOverlayCommands(overlayState.appOverlayRenderEnabled) + syncApplicationOverlaySurface( + appOverlayEnabled = overlayState.appOverlayRenderEnabled || overlayState.appOverlayInputEnabled, + ) val systemOverlayCommands = syncSystemOverlayAndCollectCommands( tree = tree, @@ -236,6 +237,7 @@ abstract class DsglScreenHost( systemOverlayInputEnabled = overlayState.systemOverlayInputEnabled, inspectorBlocks = overlayState.inspectorBlocks, ) + cancelApplicationRootDndBehindModal() val commands = paintApplicationRootOrFallback( tree = tree, @@ -244,10 +246,10 @@ abstract class DsglScreenHost( mouseY = mouseY, partialTicks = partialTicks, ) ?: return - stageApplicationOverlayCommands( + syncCollectAndStageApplicationOverlayAfterRootPaint( tree = tree, - applicationOverlayCommands = applicationOverlayCommands, appOverlayRenderEnabled = overlayState.appOverlayRenderEnabled, + appOverlayInputEnabled = overlayState.appOverlayInputEnabled, ) composeAndPresentFrame( tree = tree, @@ -427,12 +429,41 @@ abstract class DsglScreenHost( } } + private fun syncApplicationOverlaySurface(appOverlayEnabled: Boolean) { + if (!appOverlayEnabled) return + try { + applicationOverlayHost.render(adapter, lastWidth, lastHeight) + } catch ( + @Suppress("TooGenericExceptionCaught") error: Throwable, + ) { + logPipelineError( + key = "draw.applicationOverlay.sync", + message = "[DSGL] Application overlay sync failed; skipping app overlay sync frame: ${error.message}", + ) + } + } + + private fun syncCollectAndStageApplicationOverlayAfterRootPaint( + tree: DomTree, + appOverlayRenderEnabled: Boolean, + appOverlayInputEnabled: Boolean, + ) { + syncApplicationOverlaySurface( + appOverlayEnabled = appOverlayRenderEnabled || appOverlayInputEnabled, + ) + val applicationOverlayCommands = collectApplicationOverlayCommands(appOverlayRenderEnabled) + stageApplicationOverlayCommands( + tree = tree, + applicationOverlayCommands = applicationOverlayCommands, + appOverlayRenderEnabled = appOverlayRenderEnabled, + ) + } + private fun collectApplicationOverlayCommands(appOverlayRenderEnabled: Boolean): List { if (!appOverlayRenderEnabled) { return emptyList() } return try { - applicationOverlayHost.render(adapter, lastWidth, lastHeight) applicationOverlayHost.paint(adapter) } catch ( @Suppress("TooGenericExceptionCaught") error: Throwable, @@ -531,10 +562,19 @@ abstract class DsglScreenHost( val dx = dsglMouseX - prevX val dy = dsglMouseY - prevY val applicationModalBlocks = applicationOverlayHost.hasActiveModalPortal() + val applicationPortalBlocks = + isApplicationPortalFrameBlocking( + dsglMouseX = dsglMouseX, + dsglMouseY = dsglMouseY, + appOverlayInputEnabled = appOverlayInputEnabled, + ) if (applicationRootFrameBlocked) { if (applicationModalBlocks) { DndRuntime.engine.cancelActiveDrag() } + if (applicationModalBlocks || applicationPortalBlocks) { + applicationOverlayHost.handleMouseMove(dsglMouseX, dsglMouseY) + } clearHoverChainStates(postLeaveEvents = true, mouseX = dsglMouseX, mouseY = dsglMouseY) hoverTarget = null } else { @@ -1853,6 +1893,27 @@ abstract class DsglScreenHost( return applicationOverlayCommandsBuffer.toList() } + internal fun debugSyncApplicationOverlaySurfaceForTests( + measureContext: UiMeasureContext, + width: Int, + height: Int, + appOverlayEnabled: Boolean = true, + ) { + if (appOverlayEnabled) { + applicationOverlayHost.render(measureContext, width, height) + } + } + + internal fun debugCollectApplicationOverlayCommandsForTests( + measureContext: UiMeasureContext, + appOverlayRenderEnabled: Boolean = true, + ): List = + if (appOverlayRenderEnabled) { + applicationOverlayHost.paint(measureContext) + } else { + emptyList() + } + internal fun debugFirstDomainInputConsumerForTests( canConsume: (ScreenDomainSurface) -> Boolean, isSurfaceInputEnabled: (ScreenDomainSurface) -> Boolean = { true }, diff --git a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostApplicationPortalFrameTests.kt b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostApplicationPortalFrameTests.kt new file mode 100644 index 0000000..fe11800 --- /dev/null +++ b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostApplicationPortalFrameTests.kt @@ -0,0 +1,207 @@ +package org.dreamfinity.dsgl.mcForge1710 + +import org.dreamfinity.dsgl.core.DomTree +import org.dreamfinity.dsgl.core.DsglWindow +import org.dreamfinity.dsgl.core.components.modal.BackdropMode +import org.dreamfinity.dsgl.core.components.modal.ModalSpec +import org.dreamfinity.dsgl.core.components.modal.modalPortal +import org.dreamfinity.dsgl.core.dom.elements.ContainerNode +import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.dsl.button +import org.dreamfinity.dsgl.core.dsl.text +import org.dreamfinity.dsgl.core.dsl.ui +import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.overlay.hasActiveModalPortal +import org.dreamfinity.dsgl.core.render.RenderCommand +import org.dreamfinity.dsgl.core.style.StyleEngine +import org.junit.After +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import java.nio.file.Files + +class DsglScreenHostApplicationPortalFrameTests { + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } + + @After + fun cleanup() { + StyleEngine.setStylesDirectory(null) + StyleEngine.clearAllInspectorOverrides() + StyleEngine.clearCache() + } + + @Test + fun `application modal frame movement clears stale hovered portal commands before staging`() { + val hoverColor = 0xFF_12_34_56.toInt() + installStylesheet( + """ + button:hover { + background-color: #123456; + } + """.trimIndent(), + ) + val root = ContainerNode(key = "root").apply { bounds = Rect(0, 0, 300, 120) } + val tree = DomTree(root) + val host = createHost(tree) + val overlay = host.debugApplicationOverlayHostForTests() + val modalKey = "tests.host.modal.frame.hover.commands" + + try { + renderStaticModalWithButton(modalKey) + host.debugSyncApplicationOverlaySurfaceForTests(ctx, width = 300, height = 120) + + assertTrue(overlay.hasActiveModalPortal()) + assertTrue(overlay.handleMouseMove(76, 25)) + assertRenderColorPresent( + host.debugCollectApplicationOverlayCommandsForTests(ctx), + hoverColor, + ) + + host.debugUpdateFrameInteractionStateForTests(tree, mouseX = 4, mouseY = 4) + val settledCommands = host.debugCollectApplicationOverlayCommandsForTests(ctx) + val stagedCommands = + host.debugStageApplicationOverlayCommandsForTests( + tree = tree, + applicationOverlayCommands = settledCommands, + measureContext = ctx, + ) + + assertRenderColorAbsent(settledCommands, hoverColor) + assertRenderColorAbsent(stagedCommands, hoverColor) + } finally { + renderEmptyModal(modalKey) + host.debugSyncApplicationOverlaySurfaceForTests(ctx, width = 300, height = 120) + } + } + + @Test + fun `application modal button click updates portal commands after root repaint in same frame`() { + val modalKey = "tests.host.modal.frame.click.commands" + var clicked = false + var tree = renderClickStateModal(modalKey = modalKey, clicked = clicked, onClick = { clicked = true }) + val host = createHost(tree) + val overlay = host.debugApplicationOverlayHostForTests() + + try { + host.debugSyncApplicationOverlaySurfaceForTests(ctx, width = 300, height = 120) + assertTrue(overlay.hasActiveModalPortal()) + assertRenderTextPresent(host.debugCollectApplicationOverlayCommandsForTests(ctx), "Before") + + assertTrue(overlay.handleMouseDown(76, 25, MouseButton.LEFT)) + assertTrue(overlay.handleMouseUp(76, 25, MouseButton.LEFT)) + assertTrue(clicked) + + tree = renderClickStateModal(modalKey = modalKey, clicked = clicked, onClick = { clicked = true }) + host.debugBindTreeForTests(tree, needsLayout = false) + host.debugSyncApplicationOverlaySurfaceForTests(ctx, width = 300, height = 120) + + val finalPortalCommands = host.debugCollectApplicationOverlayCommandsForTests(ctx) + assertRenderTextAbsent(finalPortalCommands, "Before") + assertRenderTextPresent(finalPortalCommands, "After") + } finally { + renderEmptyModal(modalKey) + host.debugSyncApplicationOverlaySurfaceForTests(ctx, width = 300, height = 120) + } + } + + private fun renderStaticModalWithButton(modalKey: String) { + val modalTree = + ui { + modalPortal( + modals = + listOf( + ModalSpec( + key = "static-modal", + backdrop = BackdropMode.Static, + keyboard = false, + ) { + button("Hover", { + key = "$modalKey.button" + onMouseClick = {} + }) + }, + ), + key = modalKey, + ) { + text("content") + } + } + modalTree.render(ctx, 300, 120) + } + + private fun renderClickStateModal(modalKey: String, clicked: Boolean, onClick: () -> Unit): DomTree { + val modalTree = + ui { + modalPortal( + modals = + listOf( + ModalSpec( + key = "click-modal", + backdrop = BackdropMode.Static, + keyboard = false, + ) { + button(if (clicked) "After" else "Before", { + key = "$modalKey.button" + onMouseClick = { onClick() } + }) + }, + ), + key = modalKey, + ) { + text("content") + } + } + modalTree.render(ctx, 300, 120) + return modalTree + } + + private fun renderEmptyModal(modalKey: String) { + val modalTree = + ui { + modalPortal(modals = emptyList(), key = modalKey) { + text("content") + } + } + modalTree.render(ctx, 300, 120) + } + + private fun createHost(tree: DomTree): DsglScreenHost = + object : DsglScreenHost( + object : DsglWindow() { + override fun render(): DomTree = tree + }, + ) {}.also { host -> + host.debugBindTreeForTests(tree, needsLayout = false) + } + + private fun installStylesheet(contents: String) { + val dir = Files.createTempDirectory("dsgl-host-domain-test").toFile() + dir.resolve("test.dss").writeText(contents) + StyleEngine.setStylesDirectory(dir) + StyleEngine.forceReloadStylesheets() + } + + private fun assertRenderColorPresent(commands: List, color: Int) { + assertTrue(commands.any { command -> command is RenderCommand.DrawRect && command.color == color }) + } + + private fun assertRenderColorAbsent(commands: List, color: Int) { + assertFalse(commands.any { command -> command is RenderCommand.DrawRect && command.color == color }) + } + + private fun assertRenderTextPresent(commands: List, text: String) { + assertTrue(commands.any { command -> command is RenderCommand.DrawText && command.text == text }) + } + + private fun assertRenderTextAbsent(commands: List, text: String) { + assertFalse(commands.any { command -> command is RenderCommand.DrawText && command.text == text }) + } +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalNode.kt index 913df97..90a668b 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalNode.kt @@ -109,6 +109,7 @@ internal class ModalPortalRootNode( .coerceAtLeast(0), ) children.forEach { child -> + child.resolveLayoutStyleValues(ctx, bounds.width, bounds.height) child.render(ctx, bounds.x, bounds.y, bounds.width, bounds.height) } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayRootNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayRootNode.kt index 728d737..4d0bc7c 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayRootNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayRootNode.kt @@ -64,6 +64,7 @@ class ApplicationOverlayRootNode( } children.forEach { child -> if (child === debugTintNode) return@forEach + child.resolveLayoutStyleValues(ctx, bounds.width, bounds.height) child.render(ctx, bounds.x, bounds.y, bounds.width, bounds.height) } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalLayoutRegressionTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalLayoutRegressionTests.kt new file mode 100644 index 0000000..4f71cf7 --- /dev/null +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalLayoutRegressionTests.kt @@ -0,0 +1,118 @@ +package org.dreamfinity.dsgl.core.components.modal + +import org.dreamfinity.dsgl.core.DomTree +import org.dreamfinity.dsgl.core.components.modal.internal.ModalPortalSessionStore +import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.dsl.div +import org.dreamfinity.dsgl.core.dsl.ui +import org.dreamfinity.dsgl.core.event.EventBus +import org.dreamfinity.dsgl.core.overlay.ApplicationOverlayHost +import org.dreamfinity.dsgl.core.render.RenderCommand +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class ModalPortalLayoutRegressionTests { + private val trees: MutableList = ArrayList() + private val overlays: MutableList = ArrayList() + private val hostKeys: MutableSet = LinkedHashSet() + private val measureContext = + object : UiMeasureContext { + override fun measureText(text: String): Int = text.length * 6 + + override fun measureText(text: String, fontId: String?, fontSize: Int?): Int = text.length * 6 + + override val fontHeight: Int = 9 + + override fun fontHeight(fontId: String?, fontSize: Int?): Int = 9 + + override fun paint(commands: List) = Unit + } + + @AfterTest + fun cleanup() { + EventBus.run { + trees.forEach { tree -> + tree.clearRefs() + tree.root.clearListenersDeep() + } + } + overlays.forEach { overlay -> overlay.clearRefs() } + hostKeys.forEach(ModalPortalSessionStore::forgetPortal) + trees.clear() + overlays.clear() + hostKeys.clear() + } + + @Test + fun `modal portal resolves layer style before first overlay layout`() { + val hostKey = "tests.modal.portal.layout.first.frame" + val tree = buildTree(hostKey, listOf(basicModal())) + trees += tree + tree.render(measureContext, 320, 180) + + val overlay = ApplicationOverlayHost() + overlays += overlay + overlay.render(measureContext, 320, 180) + + val layer = overlay.modalPortal.debugFindNodeByKey("$hostKey.modal.modal.basic.layer") + val dialog = overlay.modalPortal.debugFindNodeByKey(ModalPortalSessionStore.dialogKey(hostKey, "modal.basic")) + assertNotNull(layer) + assertNotNull(dialog) + + assertEquals(10, layer.padding.top) + assertEquals(10, layer.padding.right) + assertEquals(10, layer.padding.bottom) + assertEquals(10, layer.padding.left) + assertEquals(16, dialog.bounds.y) + } + + @Test + fun `centered modal keeps stable bounds across passive overlay frames`() { + val hostKey = "tests.modal.portal.layout.centered.stable" + val tree = + buildTree( + hostKey, + listOf( + ModalSpec( + key = "modal.centered", + centered = true, + ) { _ -> }, + ), + ) + trees += tree + tree.render(measureContext, 320, 180) + + val overlay = ApplicationOverlayHost() + overlays += overlay + overlay.render(measureContext, 320, 180) + + val dialogKey = ModalPortalSessionStore.dialogKey(hostKey, "modal.centered") + val firstDialog = overlay.modalPortal.debugFindNodeByKey(dialogKey) + assertNotNull(firstDialog) + val firstBounds = firstDialog.bounds + + assertTrue(overlay.handleMouseMove(firstBounds.x + firstBounds.width / 2, firstBounds.y + firstBounds.height / 2)) + overlay.render(measureContext, 320, 180) + + val secondDialog = overlay.modalPortal.debugFindNodeByKey(dialogKey) + assertNotNull(secondDialog) + assertEquals(firstBounds, secondDialog.bounds) + } + + private fun buildTree(hostKey: String, modals: List): DomTree { + hostKeys += hostKey + return ui { + modalPortal(modals = modals, key = hostKey) { + div({ key = "$hostKey.content" }) + } + } + } + + private fun basicModal(): ModalSpec = + ModalSpec( + key = "modal.basic", + ) { _ -> } +} From 8e1861d9168cfb815b040ac85b8576ba43dc4793 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Tue, 2 Jun 2026 23:14:56 +0300 Subject: [PATCH 73/78] removing fix leftovers; --- .../dsgl/mcForge1710/DsglScreenHost.kt | 11 ---- .../DsglScreenHostDomainOrchestrationTests.kt | 60 ------------------- .../modal/internal/ModalPortalController.kt | 5 -- 3 files changed, 76 deletions(-) diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index c52ef57..d8f9f56 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -237,7 +237,6 @@ abstract class DsglScreenHost( systemOverlayInputEnabled = overlayState.systemOverlayInputEnabled, inspectorBlocks = overlayState.inspectorBlocks, ) - cancelApplicationRootDndBehindModal() val commands = paintApplicationRootOrFallback( tree = tree, @@ -423,12 +422,6 @@ abstract class DsglScreenHost( refreshActiveColorSamplerOwner(tree.root) } - private fun cancelApplicationRootDndBehindModal() { - if (applicationOverlayHost.hasActiveModalPortal()) { - DndRuntime.engine.cancelActiveDrag() - } - } - private fun syncApplicationOverlaySurface(appOverlayEnabled: Boolean) { if (!appOverlayEnabled) return try { @@ -2000,10 +1993,6 @@ abstract class DsglScreenHost( ) } - internal fun debugCancelApplicationRootDndBehindModalForTests() { - cancelApplicationRootDndBehindModal() - } - internal fun debugDispatchApplicationRootPointerDownForTests( tree: DomTree, mouseX: Int, diff --git a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt index 6b466f4..b955824 100644 --- a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt +++ b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt @@ -402,66 +402,6 @@ class DsglScreenHostDomainOrchestrationTests { } } - @Test - fun `active application modal cancels root dnd before ghost can render`() { - val root = ContainerNode(key = "root").apply { bounds = Rect(0, 0, 300, 120) } - val draggable = - ContainerNode(key = "draggable") - .apply { - draggable = true - bounds = Rect(20, 40, 80, 20) - }.applyParent(root) - val tree = DomTree(root) - val host = createHost(tree) - val overlay = host.debugApplicationOverlayHostForTests() - val modalKey = "tests.host.modal.cancel.dnd" - val down = - MouseDownEvent(24, 44, MouseButton.LEFT) - .also { event -> - event.target = draggable - } - - DndRuntime.engine.cancelActiveDrag() - try { - DndRuntime.engine.onMouseDown(root, draggable, down) - DndRuntime.engine.onMouseMove(root, 120, 60) - assertTrue(DndRuntime.engine.isDragging) - - val modalTree = - ui { - modalPortal( - modals = - listOf( - ModalSpec(key = "static-modal") { - text("Static") - }, - ), - key = modalKey, - ) { - text("content") - } - } - modalTree.render(ctx, 300, 120) - overlay.render(ctx, 300, 120) - assertTrue(overlay.hasActiveModalPortal()) - - host.debugCancelApplicationRootDndBehindModalForTests() - - assertFalse(DndRuntime.engine.isPointerCaptured) - assertFalse(DndRuntime.engine.isDragging) - } finally { - val emptyModalTree = - ui { - modalPortal(modals = emptyList(), key = modalKey) { - text("content") - } - } - emptyModalTree.render(ctx, 300, 120) - overlay.render(ctx, 300, 120) - DndRuntime.engine.cancelActiveDrag() - } - } - @Test fun `active application modal frame blocks and cancels root dnd`() { val root = ContainerNode(key = "root").apply { bounds = Rect(0, 0, 300, 120) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt index 76d6d20..3727616 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt @@ -196,11 +196,6 @@ internal class ModalPortalController { return result.consumed } - fun handleWheelPolicy(mouseX: Int, mouseY: Int): Boolean { - val result = portalHost.evaluateOutsidePointerDown(mouseX, mouseY) ?: return false - return result.region == PortalPointerRegion.OutsideEntry && result.consumed - } - fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean = portalHost.dispatchInput { it.handleKeyDown(keyCode, keyChar) From 756cec54686f3ba4e481ceb20cecb82fd04aef5f Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Wed, 3 Jun 2026 12:24:50 +0300 Subject: [PATCH 74/78] migrating drag handling logic to PointerCaptureSession for streamlined pointer management and cleanup; --- .gitignore | 2 + .../dsgl/mcForge1710/DsglScreenHost.kt | 104 +++------------ .../core/overlay/input/LayerDomInputRouter.kt | 108 ++-------------- .../overlay/input/PointerCaptureSession.kt | 122 ++++++++++++++++++ .../input/PointerCaptureSessionTests.kt | 102 +++++++++++++++ 5 files changed, 259 insertions(+), 179 deletions(-) create mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/PointerCaptureSession.kt create mode 100644 core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/input/PointerCaptureSessionTests.kt diff --git a/.gitignore b/.gitignore index e71afd1..df7b58d 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,5 @@ build # docs generated folder /site/ +# Added by code-review-graph +.code-review-graph/ diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index d8f9f56..4d8538c 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -46,6 +46,7 @@ import org.dreamfinity.dsgl.core.overlay.hasDomPointerTargetAt import org.dreamfinity.dsgl.core.overlay.hasOpenColorPickerPortal import org.dreamfinity.dsgl.core.overlay.hasOpenContextMenuPortal import org.dreamfinity.dsgl.core.overlay.hasOpenSelectPortal +import org.dreamfinity.dsgl.core.overlay.input.PointerCaptureSession import org.dreamfinity.dsgl.core.overlay.syncPortalFrame import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayHost import org.dreamfinity.dsgl.core.overlay.toggleFloatingWindowDemo @@ -96,10 +97,7 @@ abstract class DsglScreenHost( private val pressedKeys: MutableSet = HashSet() private val hoverChain: MutableList = mutableListOf() private var hoverTarget: DOMNode? = null - private var dragCaptureTarget: DOMNode? = null - private var dragCaptureKey: Any? = null - private var dragCaptureClass: Class? = null - private var dragCaptureFocusKey: Any? = null + private val pointerCapture: PointerCaptureSession = PointerCaptureSession() private var inspectorPointerCaptured: Boolean = false private var layoutRevision: Long = 0L private val pendingCleanupRoots: MutableSet = @@ -588,7 +586,7 @@ abstract class DsglScreenHost( inspectorBlocks: Boolean, ): Boolean { if (appOverlayInputEnabled && applicationOverlayHost.hasActiveModalPortal()) return true - if (isApplicationRootPointerDragActive() && dragCaptureTarget != null) return false + if (isApplicationRootPointerDragActive() && pointerCapture.target != null) return false if (inspectorBlocks || higherSurfacePointerButton != -1) return true if (isApplicationPortalFrameBlocking(dsglMouseX, dsglMouseY, appOverlayInputEnabled)) return true if (systemOverlayInputEnabled && systemOverlayHost.hasOpenPortal()) return true @@ -633,7 +631,7 @@ abstract class DsglScreenHost( ) { updateHover(tree.root, hoverChain, dsglMouseX, dsglMouseY, dx, dy) hoverTarget = hoverChain.lastOrNull() - if (dragCaptureTarget != null && hasFocusChangedSinceCapture()) { + if (pointerCapture.target != null && pointerCapture.hasFocusChanged()) { releaseDragCapture() } if (dx != 0 || dy != 0) { @@ -652,7 +650,7 @@ abstract class DsglScreenHost( } else { DndRuntime.engine.onMouseMove(tree.root, dsglMouseX, dsglMouseY) val moveEvent = MouseMoveEvent(dsglMouseX, dsglMouseY, prevX, prevY) - moveEvent.target = resolveForcedPointerTarget() ?: dragCaptureTarget ?: hoverTarget + moveEvent.target = resolveForcedPointerTarget() ?: pointerCapture.target ?: hoverTarget EventBus.post(moveEvent) } } @@ -1272,11 +1270,15 @@ abstract class DsglScreenHost( if (mappedButton == MouseButton.LEFT) { setActiveTarget(event.target ?: hoverTarget) val captureTarget = - resolveDragCaptureTarget(event.target ?: hoverTarget, inputEvent.mouseX, inputEvent.mouseY) + PointerCaptureSession.resolveCaptureTarget( + start = event.target ?: hoverTarget, + mouseX = inputEvent.mouseX, + mouseY = inputEvent.mouseY, + ) if (captureTarget != null) { setDragCapture(captureTarget) captureTarget.beginPointerCapture(inputEvent.mouseX, inputEvent.mouseY, mappedButton) - } else if (dragCaptureTarget != null) { + } else if (pointerCapture.target != null) { releaseDragCapture() } if (captureTarget == null && !event.cancelled) { @@ -1290,13 +1292,13 @@ abstract class DsglScreenHost( private fun dispatchApplicationRootPointerUp(tree: DomTree, inputEvent: MouseInputEvent) { val releaseTarget = resolvePointerUpTarget() - val hadDragCapture = dragCaptureTarget != null + val hadDragCapture = pointerCapture.target != null eventButton = -1 mapButton(inputEvent.mouseButton)?.let { mappedButton -> val upEvent = MouseUpEvent(inputEvent.mouseX, inputEvent.mouseY, mappedButton) upEvent.target = releaseTarget EventBus.post(upEvent) - dragCaptureTarget?.endPointerCapture(inputEvent.mouseX, inputEvent.mouseY, mappedButton) + pointerCapture.target?.endPointerCapture(inputEvent.mouseX, inputEvent.mouseY, mappedButton) val dndConsumed = DndRuntime.engine.onMouseUp(tree.root, upEvent) if (!hadDragCapture && !dndConsumed) { val clickEvent = MouseClickEvent(inputEvent.mouseX, inputEvent.mouseY, mappedButton) @@ -1338,7 +1340,7 @@ abstract class DsglScreenHost( mappedButton: MouseButton? = mapButton(eventButton), ) { val button = mappedButton ?: return - val captured = dragCaptureTarget + val captured = pointerCapture.target if (captured == null) { DndRuntime.engine.onMouseMove(tree.root, mouseX, mouseY) } @@ -2014,18 +2016,11 @@ abstract class DsglScreenHost( } private fun setDragCapture(target: DOMNode) { - dragCaptureTarget = target - dragCaptureKey = target.key - dragCaptureClass = target.javaClass - dragCaptureFocusKey = FocusManager.focusedNode()?.key + pointerCapture.capture(target) } private fun releaseDragCapture() { - dragCaptureTarget?.cancelPointerCapture() - dragCaptureTarget = null - dragCaptureKey = null - dragCaptureClass = null - dragCaptureFocusKey = null + pointerCapture.release() } private fun setActiveTarget(target: DOMNode?) { @@ -2041,70 +2036,8 @@ abstract class DsglScreenHost( activeTarget = null } - private fun resolveDragCaptureTarget(start: DOMNode?, mouseX: Int, mouseY: Int): DOMNode? { - var current = start - while (current != null) { - when (current) { - is RangeInputNode -> return current - is SingleLineInputNode -> if (current.shouldCaptureTextSelectionDrag(mouseX, mouseY)) return current - is TextAreaNode -> if (current.shouldCaptureAnyDrag(mouseX, mouseY)) return current - } - if (current.shouldCapturePointerDrag(mouseX, mouseY)) { - return current - } - current = current.parent - } - return null - } - private fun restoreDragCapture(root: DOMNode) { - if (dragCaptureTarget == null) return - val key = dragCaptureKey - val cls = dragCaptureClass - if (cls == null) { - releaseDragCapture() - return - } - if (key == null) { - val captured = dragCaptureTarget - if (captured != null && captured.javaClass == cls) { - if (eventButton != -1) { - return - } - if (isSameOrAncestor(root, captured)) { - return - } - } - releaseDragCapture() - return - } - - val restored = findByKeyAndClass(root, key, cls) - if (restored != null) { - dragCaptureTarget = restored - } else { - releaseDragCapture() - } - } - - private fun findByKeyAndClass(node: DOMNode, key: Any, cls: Class): DOMNode? { - if (node.key == key && node.javaClass == cls) return node - for (child in node.children) { - val found = findByKeyAndClass(child, key, cls) - if (found != null) { - return found - } - } - return null - } - - private fun hasFocusChangedSinceCapture(): Boolean { - if (dragCaptureFocusKey == null) return false - val captured = dragCaptureTarget - val currentFocus = FocusManager.focusedNode() - if (captured != null && isSameOrAncestor(captured, currentFocus)) return false - val currentFocusKey = currentFocus?.key - return currentFocusKey != dragCaptureFocusKey + pointerCapture.restore(root, pointerPressed = eventButton != -1) } private fun refreshHoverTarget(mouseX: Int, mouseY: Int) { @@ -2122,7 +2055,8 @@ abstract class DsglScreenHost( private fun resolvePointerDownTarget(): DOMNode? = resolveForcedPointerTarget() ?: hoverTarget - private fun resolvePointerUpTarget(): DOMNode? = dragCaptureTarget ?: resolveForcedPointerTarget() ?: hoverTarget + private fun resolvePointerUpTarget(): DOMNode? = + pointerCapture.target ?: resolveForcedPointerTarget() ?: hoverTarget private fun resolveClickTarget(): DOMNode? = hoverTarget diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouter.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouter.kt index 081db73..fcc159a 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouter.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouter.kt @@ -1,8 +1,6 @@ package org.dreamfinity.dsgl.core.overlay.input import org.dreamfinity.dsgl.core.dom.DOMNode -import org.dreamfinity.dsgl.core.dom.elements.RangeInputNode -import org.dreamfinity.dsgl.core.dom.elements.SingleLineInputNode import org.dreamfinity.dsgl.core.dom.elements.TextAreaNode import org.dreamfinity.dsgl.core.dom.layout.AffineTransform2D import org.dreamfinity.dsgl.core.dom.layout.Rect @@ -26,10 +24,7 @@ class LayerDomInputRouter( ) { private val hoverChain: MutableList = ArrayList() private var hoverTarget: DOMNode? = null - private var dragCaptureTarget: DOMNode? = null - private var dragCaptureKey: Any? = null - private var dragCaptureClass: Class? = null - private var dragCaptureFocusKey: Any? = null + private val pointerCapture: PointerCaptureSession = PointerCaptureSession() private var activeTarget: DOMNode? = null private var pressedButton: MouseButton? = null private var draggedSincePress: Boolean = false @@ -50,21 +45,21 @@ class LayerDomInputRouter( updateHoverLocal(root, hoverChain, mouseX, mouseY, dx, dy) hoverTarget = hoverChain.lastOrNull() - if (dragCaptureTarget != null && hasFocusChangedSinceCapture()) { + if (pointerCapture.hasCapture && pointerCapture.hasFocusChanged()) { releaseDragCapture() } if (dx != 0 || dy != 0) { val move = MouseMoveEvent(mouseX, mouseY, prevX, prevY) - move.target = dragCaptureTarget ?: hoverTarget + move.target = pointerCapture.target ?: hoverTarget EventBus.post(move) val button = pressedButton if (button != null) { draggedSincePress = true val drag = MouseDragEvent(prevX, prevY, dx, dy, button) - drag.target = dragCaptureTarget ?: hoverTarget + drag.target = pointerCapture.target ?: hoverTarget EventBus.post(drag) - dragCaptureTarget?.continuePointerCapture( + pointerCapture.target?.continuePointerCapture( mouseX = mouseX, mouseY = mouseY, mouseDX = dx, @@ -75,7 +70,7 @@ class LayerDomInputRouter( } lastMoveX = mouseX lastMoveY = mouseY - return dragCaptureTarget != null || hoverTarget != null + return pointerCapture.hasCapture || hoverTarget != null } fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { @@ -95,7 +90,7 @@ class LayerDomInputRouter( if (button == MouseButton.LEFT) { setActiveTarget(target) - val capture = resolveDragCaptureTarget(target, mouseX, mouseY) + val capture = PointerCaptureSession.resolveCaptureTarget(target, mouseX, mouseY) if (capture != null) { setDragCapture(capture) capture.beginPointerCapture(mouseX, mouseY, button) @@ -119,8 +114,8 @@ class LayerDomInputRouter( restoreDragCapture(root) updateHoverLocal(root, hoverChain, mouseX, mouseY, 0, 0) hoverTarget = hoverChain.lastOrNull() - val releaseTarget = dragCaptureTarget ?: hoverTarget ?: activeTarget - val hadCapture = dragCaptureTarget != null + val releaseTarget = pointerCapture.target ?: hoverTarget ?: activeTarget + val hadCapture = pointerCapture.hasCapture val pressed = pressedButton if (pressed != button && releaseTarget == null) { return false @@ -129,7 +124,7 @@ class LayerDomInputRouter( val up = MouseUpEvent(mouseX, mouseY, button) up.target = releaseTarget EventBus.post(up) - dragCaptureTarget?.endPointerCapture(mouseX, mouseY, button) + pointerCapture.target?.endPointerCapture(mouseX, mouseY, button) if (!hadCapture && !draggedSincePress && pressed == button) { val click = MouseClickEvent(mouseX, mouseY, button) click.target = hoverTarget @@ -191,7 +186,7 @@ class LayerDomInputRouter( fun hasPointerTargetAt(mouseX: Int, mouseY: Int): Boolean { val root = rootProvider() ?: return false - if (dragCaptureTarget != null) return true + if (pointerCapture.hasCapture) return true val chain = ArrayList(hoverChain.size + 4) return collectHoverChainLocal( root = root, @@ -214,82 +209,16 @@ class LayerDomInputRouter( lastMoveY = Int.MIN_VALUE } - private fun resolveDragCaptureTarget(start: DOMNode?, mouseX: Int, mouseY: Int): DOMNode? { - var current = start - while (current != null) { - when (current) { - is RangeInputNode -> return current - is SingleLineInputNode -> if (current.shouldCaptureTextSelectionDrag(mouseX, mouseY)) return current - is TextAreaNode -> if (current.shouldCaptureAnyDrag(mouseX, mouseY)) return current - } - if (current.shouldCapturePointerDrag(mouseX, mouseY)) { - return current - } - current = current.parent - } - return null - } - private fun setDragCapture(target: DOMNode) { - dragCaptureTarget = target - dragCaptureKey = target.key - dragCaptureClass = target.javaClass - dragCaptureFocusKey = FocusManager.focusedNode()?.key + pointerCapture.capture(target) } private fun releaseDragCapture() { - dragCaptureTarget?.cancelPointerCapture() - dragCaptureTarget = null - dragCaptureKey = null - dragCaptureClass = null - dragCaptureFocusKey = null + pointerCapture.release() } private fun restoreDragCapture(root: DOMNode) { - if (dragCaptureTarget == null) return - val key = dragCaptureKey - val cls = dragCaptureClass - if (cls == null) { - releaseDragCapture() - return - } - if (key == null) { - val captured = dragCaptureTarget - if (captured != null && captured.javaClass == cls) { - if (pressedButton != null) { - return - } - if (isSameOrAncestor(root, captured)) { - return - } - } - releaseDragCapture() - return - } - val restored = findByKeyAndClass(root, key, cls) - if (restored != null) { - dragCaptureTarget = restored - return - } - releaseDragCapture() - } - - private fun findByKeyAndClass(node: DOMNode, key: Any, cls: Class): DOMNode? { - if (node.key == key && node.javaClass == cls) return node - node.children.forEach { child -> - val found = findByKeyAndClass(child, key, cls) - if (found != null) return found - } - return null - } - - private fun hasFocusChangedSinceCapture(): Boolean { - if (dragCaptureFocusKey == null) return false - val captured = dragCaptureTarget - val currentFocus = FocusManager.focusedNode() - if (captured != null && isSameOrAncestor(captured, currentFocus)) return false - val currentFocusKey = FocusManager.focusedNode()?.key - return currentFocusKey != dragCaptureFocusKey + pointerCapture.restore(root, pointerPressed = pressedButton != null) } private fun setActiveTarget(target: DOMNode?) { @@ -328,15 +257,6 @@ class LayerDomInputRouter( return false } - private fun isSameOrAncestor(candidate: DOMNode, node: DOMNode?): Boolean { - var current = node - while (current != null) { - if (current === candidate) return true - current = current.parent - } - return false - } - private fun clearHoverChainStates() { hoverChain.forEach { node -> node.setHoveredState(false) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/PointerCaptureSession.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/PointerCaptureSession.kt new file mode 100644 index 0000000..8a86e62 --- /dev/null +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/PointerCaptureSession.kt @@ -0,0 +1,122 @@ +package org.dreamfinity.dsgl.core.overlay.input + +import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.dom.elements.RangeInputNode +import org.dreamfinity.dsgl.core.dom.elements.SingleLineInputNode +import org.dreamfinity.dsgl.core.dom.elements.TextAreaNode +import org.dreamfinity.dsgl.core.event.FocusManager + +/** + * Shared DOM-node pointer capture bookkeeping for domain root and portal dispatchers. + */ +class PointerCaptureSession { + var target: DOMNode? = null + private set + + private var targetKey: Any? = null + private var targetClass: Class? = null + private var focusKey: Any? = null + + val hasCapture: Boolean + get() = target != null + + fun capture(target: DOMNode) { + this.target = target + targetKey = target.key + targetClass = target.javaClass + focusKey = FocusManager.focusedNode()?.key + } + + fun release() { + target?.cancelPointerCapture() + reset() + } + + fun restore(root: DOMNode, pointerPressed: Boolean) { + if (target == null) return + val cls = targetClass + if (cls == null) { + release() + return + } + + val key = targetKey + if (key == null) { + val captured = target + if (shouldKeepUnkeyedCapture(captured, cls, root, pointerPressed)) { + return + } + release() + return + } + + val restored = findByKeyAndClass(root, key, cls) + if (restored != null) { + target = restored + } else { + release() + } + } + + fun hasFocusChanged(): Boolean { + if (focusKey == null) return false + val captured = target + val currentFocus = FocusManager.focusedNode() + if (captured != null && isSameOrAncestor(captured, currentFocus)) return false + return currentFocus?.key != focusKey + } + + private fun reset() { + target = null + targetKey = null + targetClass = null + focusKey = null + } + + private fun shouldKeepUnkeyedCapture( + captured: DOMNode?, + cls: Class, + root: DOMNode, + pointerPressed: Boolean, + ): Boolean { + if (captured == null) return false + if (captured.javaClass != cls) return false + return pointerPressed || isSameOrAncestor(root, captured) + } + + companion object { + fun resolveCaptureTarget(start: DOMNode?, mouseX: Int, mouseY: Int): DOMNode? { + var current = start + while (current != null) { + when (current) { + is RangeInputNode -> return current + is SingleLineInputNode -> if (current.shouldCaptureTextSelectionDrag(mouseX, mouseY)) return current + is TextAreaNode -> if (current.shouldCaptureAnyDrag(mouseX, mouseY)) return current + } + if (current.shouldCapturePointerDrag(mouseX, mouseY)) { + return current + } + current = current.parent + } + return null + } + + fun isSameOrAncestor(candidate: DOMNode, node: DOMNode?): Boolean { + var current = node + while (current != null) { + if (current === candidate) return true + current = current.parent + } + return false + } + + private fun findByKeyAndClass(node: DOMNode, key: Any, cls: Class): DOMNode? { + if (node.key == key && node.javaClass == cls) return node + node.children.forEach { child -> + val found = findByKeyAndClass(child, key, cls) + if (found != null) return found + } + return null + } + } +} diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/input/PointerCaptureSessionTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/input/PointerCaptureSessionTests.kt new file mode 100644 index 0000000..63e66d1 --- /dev/null +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/input/PointerCaptureSessionTests.kt @@ -0,0 +1,102 @@ +package org.dreamfinity.dsgl.core.overlay.input + +import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.dom.applyParent +import org.dreamfinity.dsgl.core.dom.elements.ContainerNode +import org.dreamfinity.dsgl.core.dom.elements.RangeInputNode +import org.dreamfinity.dsgl.core.dom.elements.TextInputNode +import org.dreamfinity.dsgl.core.dom.layout.Rect +import org.dreamfinity.dsgl.core.event.FocusManager +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertSame +import kotlin.test.assertTrue + +class PointerCaptureSessionTests { + @AfterTest + fun cleanup() { + FocusManager.clearFocus() + } + + @Test + fun `restores keyed capture target after root subtree replacement`() { + val firstRoot = ContainerNode(key = "root") + val firstTarget = CapturingNode(key = "target").applyParent(firstRoot) + val session = PointerCaptureSession() + session.capture(firstTarget) + + val secondRoot = ContainerNode(key = "root") + val secondTarget = CapturingNode(key = "target").applyParent(secondRoot) + + session.restore(secondRoot, pointerPressed = true) + + assertSame(secondTarget, session.target) + assertFalse(firstTarget.cancelled) + } + + @Test + fun `keeps unkeyed capture target while pointer is still pressed`() { + val root = ContainerNode(key = "root") + val target = CapturingNode().applyParent(root) + val session = PointerCaptureSession() + session.capture(target) + + session.restore(ContainerNode(key = "other-root"), pointerPressed = true) + + assertSame(target, session.target) + assertFalse(target.cancelled) + } + + @Test + fun `releases unkeyed capture target when detached after pointer is no longer pressed`() { + val root = ContainerNode(key = "root") + val target = CapturingNode().applyParent(root) + val session = PointerCaptureSession() + session.capture(target) + + session.restore(ContainerNode(key = "other-root"), pointerPressed = false) + + assertFalse(session.hasCapture) + assertTrue(target.cancelled) + } + + @Test + fun `focus moving into captured subtree does not cancel capture`() { + val root = ContainerNode(key = "root") + val target = CapturingNode(key = "target").applyParent(root) + val child = TextInputNode(text = "", key = "child").applyParent(target) + val previous = TextInputNode(text = "", key = "previous").applyParent(root) + val session = PointerCaptureSession() + + FocusManager.requestFocus(previous) + session.capture(target) + FocusManager.requestFocus(child) + + assertFalse(session.hasFocusChanged()) + } + + @Test + fun `capture target resolution includes control-owned drag nodes`() { + val root = ContainerNode(key = "root").apply { bounds = Rect(0, 0, 200, 80) } + val range = + RangeInputNode(value = 0L, min = 0L, max = 100L, key = "range") + .apply { + bounds = Rect(20, 20, 120, 12) + }.applyParent(root) + + val resolved = PointerCaptureSession.resolveCaptureTarget(range, 24, 24) + + assertSame(range, resolved) + } + + private class CapturingNode( + key: Any? = null, + ) : DOMNode(key) { + var cancelled: Boolean = false + + override fun cancelPointerCapture() { + cancelled = true + } + } +} From e950e95afffb686f084f984b89c359bae88e7a19 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Wed, 3 Jun 2026 15:01:03 +0300 Subject: [PATCH 75/78] adding DnD ghost portal logic and integration; --- .../dsgl/mcForge1710/DsglScreenHost.kt | 14 +- .../DsglScreenHostDomainOrchestrationTests.kt | 40 ++++++ .../core/overlay/ApplicationOverlayHost.kt | 126 ++++++++++++++++++ .../overlay/ApplicationDndGhostPortalTests.kt | 96 +++++++++++++ 4 files changed, 269 insertions(+), 7 deletions(-) create mode 100644 core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationDndGhostPortalTests.kt diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index 4d8538c..e11de1e 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -31,6 +31,7 @@ import org.dreamfinity.dsgl.core.overlay.DomainSurfaceHost import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurface import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces +import org.dreamfinity.dsgl.core.overlay.appendDndGhostPortalCommands import org.dreamfinity.dsgl.core.overlay.appendPortalOverlayCommands import org.dreamfinity.dsgl.core.overlay.captureColorPickerEyedropperSample import org.dreamfinity.dsgl.core.overlay.closeFloatingPortals @@ -666,13 +667,12 @@ abstract class DsglScreenHost( if (appOverlayRenderEnabled) { applicationOverlayCommandsBuffer.addAll(applicationOverlayCommands) if (!applicationOverlayHost.hasActiveModalPortal()) { - DndRuntime.engine.appendPlaceholderCommands(applicationOverlayCommandsBuffer) - DndRuntime.engine.appendOverlayCommands( - tree.root, - measureContext, - lastWidth, - lastHeight, - applicationOverlayCommandsBuffer, + applicationOverlayHost.appendDndGhostPortalCommands( + root = tree.root, + measureContext = measureContext, + viewportWidth = lastWidth, + viewportHeight = lastHeight, + out = applicationOverlayCommandsBuffer, ) } applicationOverlayHost.appendPortalOverlayCommands( diff --git a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt index b955824..85d5653 100644 --- a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt +++ b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt @@ -438,6 +438,46 @@ class DsglScreenHostDomainOrchestrationTests { } } + @Test + fun `application root dnd ghost commands are staged through application portal`() { + val root = ContainerNode(key = "root").apply { bounds = Rect(0, 0, 300, 120) } + val draggable = + ContainerNode(key = "draggable") + .apply { + draggable = true + bounds = Rect(20, 40, 80, 20) + }.applyParent(root) + val tree = DomTree(root) + val host = createHost(tree) + val down = + MouseDownEvent(24, 44, MouseButton.LEFT) + .also { event -> + event.target = draggable + } + + DndRuntime.engine.cancelActiveDrag() + try { + DndRuntime.engine.onMouseDown(root, draggable, down) + DndRuntime.engine.onMouseMove(root, 120, 60) + assertTrue(DndRuntime.engine.isDragging) + + val staged = + host.debugStageApplicationOverlayCommandsForTests( + tree = tree, + applicationOverlayCommands = emptyList(), + measureContext = ctx, + ) + + assertTrue( + staged.any { command -> + command is RenderCommand.DrawText && command.text == "drag" + }, + ) + } finally { + DndRuntime.engine.cancelActiveDrag() + } + } + @Test fun `active application modal suppresses root dnd ghost commands`() { val root = ContainerNode(key = "root").apply { bounds = Rect(0, 0, 300, 120) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt index ad99786..9e77c42 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt @@ -5,6 +5,8 @@ import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupEngine import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPortalController import org.dreamfinity.dsgl.core.components.modal.internal.ModalPortalController import org.dreamfinity.dsgl.core.contextmenu.ContextMenuEngine +import org.dreamfinity.dsgl.core.dnd.DndEngine +import org.dreamfinity.dsgl.core.dnd.DndRuntime import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext @@ -20,6 +22,7 @@ class ApplicationOverlayHost( contextMenuEngine: ContextMenuEngine = DomainPortalServices.applicationContextMenuEngine, selectEngine: SelectEngine = DomainPortalServices.applicationSelectEngine, colorPickerEngine: ColorPickerPopupEngine = DomainPortalServices.applicationColorPickerEngine, + dndEngine: DndEngine = DndRuntime.engine, ) : DomainSurfaceHost { override val surface: ScreenDomainSurface = ScreenDomainSurfaces.ApplicationPortal @@ -46,6 +49,8 @@ class ApplicationOverlayHost( internal val modalPortal: ModalPortalController = ModalPortalController() internal val floatingWindowPortal: ApplicationFloatingWindowPortalController = ApplicationFloatingWindowPortalController() + internal val dndGhostPortal: ApplicationDndGhostPortalController = + ApplicationDndGhostPortalController(dndEngine) private var modalPortalWasActive: Boolean = false override fun onInputFrame(viewportWidth: Int, viewportHeight: Int) { @@ -114,6 +119,7 @@ class ApplicationOverlayHost( applicationColorPickerPortal.close() modalPortal.close() floatingWindowPortal.clearRefs() + dndGhostPortal.clearRefs() modalPortalWasActive = false } @@ -153,6 +159,16 @@ fun ApplicationOverlayHost.appendPortalOverlayCommands( applicationColorPickerPortal.appendCommands(measureContext, viewportWidth, viewportHeight, out) } +fun ApplicationOverlayHost.appendDndGhostPortalCommands( + root: DOMNode, + measureContext: UiMeasureContext, + viewportWidth: Int, + viewportHeight: Int, + out: MutableList, +) { + dndGhostPortal.appendCommands(root, measureContext, viewportWidth, viewportHeight, out) +} + fun ApplicationOverlayHost.closeFloatingPortals() { contextMenuPortal.close() applicationSelectPortal.close() @@ -181,6 +197,8 @@ fun ApplicationOverlayHost.toggleFloatingWindowDemo(anchorX: Int, anchorY: Int) fun ApplicationOverlayHost.isFloatingWindowDemoOpen(): Boolean = floatingWindowPortal.open +internal fun ApplicationOverlayHost.debugDndGhostPortalState(): PortalEntryState = dndGhostPortal.debugState() + fun ApplicationOverlayHost.hasDomPointerTargetAt(mouseX: Int, mouseY: Int): Boolean = domInputRouter.hasPointerTargetAt(mouseX, mouseY) @@ -246,6 +264,114 @@ internal interface PortalPointerDispatch { fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean } +internal class ApplicationDndGhostPortalController( + private val engine: DndEngine, +) { + private val portalHost: PortalHost = PortalHost(ScreenDomainSurfaces.ApplicationPortal) + private val entry: ApplicationDndGhostPortalEntry = ApplicationDndGhostPortalEntry(engine) + + init { + portalHost.register(entry) + } + + fun appendCommands( + root: DOMNode, + measureContext: UiMeasureContext, + viewportWidth: Int, + viewportHeight: Int, + out: MutableList, + ) { + entry.updatePaintContext( + root = root, + measureContext = measureContext, + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + ) + out += portalHost.paint(measureContext) + } + + fun clearRefs() { + entry.clearRefs() + } + + internal fun debugState(): PortalEntryState = entry.state +} + +private class ApplicationDndGhostPortalEntry( + private val engine: DndEngine, +) : PortalEntry { + override val state: PortalEntryState = + PortalEntryState( + id = PortalEntryId("application.dnd-ghost"), + ownerToken = engine, + surface = ScreenDomainSurfaces.ApplicationPortal, + order = PortalEntryOrder(zIndex = 80), + dismissPolicy = PortalDismissPolicy.None, + inputPolicy = PortalInputPolicy.None, + focusPolicy = PortalFocusPolicy.Preserve, + ) + override val node: DOMNode? = null + private var root: DOMNode? = null + private var measureContext: UiMeasureContext? = null + private var viewportWidth: Int = 1 + private var viewportHeight: Int = 1 + + fun updatePaintContext( + root: DOMNode, + measureContext: UiMeasureContext, + viewportWidth: Int, + viewportHeight: Int, + ) { + this.root = root + this.measureContext = measureContext + this.viewportWidth = viewportWidth.coerceAtLeast(1) + this.viewportHeight = viewportHeight.coerceAtLeast(1) + syncActivePlacement() + } + + override fun paint(ctx: UiMeasureContext): List { + val activeRoot = root + if (activeRoot == null || !engine.isDragging) { + state.deactivate() + return emptyList() + } + syncActivePlacement() + val commands = ArrayList() + engine.appendPlaceholderCommands(commands) + engine.appendOverlayCommands( + root = activeRoot, + ctx = measureContext ?: ctx, + viewportWidth = viewportWidth, + viewportHeight = viewportHeight, + out = commands, + ) + return commands + } + + override fun clearRefs() { + root = null + measureContext = null + state.deactivate() + } + + private fun syncActivePlacement() { + if (!engine.isDragging) { + state.deactivate() + return + } + state.activate( + PortalEntryPlacement( + anchorBounds = null, + bounds = + PortalEntryBounds( + viewportBounds = Rect(0, 0, viewportWidth, viewportHeight), + entryBounds = Rect(0, 0, viewportWidth, viewportHeight), + ), + ), + ) + } +} + internal class ContextMenuPortalController( private val engine: ContextMenuEngine, ) : PortalPointerDispatch { diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationDndGhostPortalTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationDndGhostPortalTests.kt new file mode 100644 index 0000000..1fb38cb --- /dev/null +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationDndGhostPortalTests.kt @@ -0,0 +1,96 @@ +package org.dreamfinity.dsgl.core.overlay + +import org.dreamfinity.dsgl.core.dnd.DndRuntime +import org.dreamfinity.dsgl.core.dom.DOMNode +import org.dreamfinity.dsgl.core.dom.applyParent +import org.dreamfinity.dsgl.core.dom.elements.ContainerNode +import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.event.MouseButton +import org.dreamfinity.dsgl.core.event.MouseDownEvent +import org.dreamfinity.dsgl.core.render.RenderCommand +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ApplicationDndGhostPortalTests { + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } + + @AfterTest + fun cleanup() { + DndRuntime.engine.cancelActiveDrag() + } + + @Test + fun `application drag ghost paints through application portal entry`() { + val host = ApplicationOverlayHost() + val root = draggableRoot() + val draggable = root.children.first() + + startDrag(root, draggable) + + val commands = ArrayList() + host.appendDndGhostPortalCommands( + root = root, + measureContext = ctx, + viewportWidth = 300, + viewportHeight = 120, + out = commands, + ) + + val state = host.debugDndGhostPortalState() + assertTrue(state.active) + assertEquals(ScreenDomainSurfaces.ApplicationPortal, state.surface) + assertEquals("application.dnd-ghost", state.id.value) + assertTrue(commands.any { command -> command is RenderCommand.DrawText && command.text == "drag" }) + } + + @Test + fun `application drag ghost portal deactivates after drag cancel`() { + val host = ApplicationOverlayHost() + val root = draggableRoot() + val draggable = root.children.first() + + startDrag(root, draggable) + host.appendDndGhostPortalCommands(root, ctx, 300, 120, ArrayList()) + assertTrue(host.debugDndGhostPortalState().active) + + DndRuntime.engine.cancelActiveDrag() + val commands = ArrayList() + host.appendDndGhostPortalCommands(root, ctx, 300, 120, commands) + + assertFalse(host.debugDndGhostPortalState().active) + assertTrue(commands.isEmpty()) + } + + private fun draggableRoot(): ContainerNode { + val root = ContainerNode(key = "dnd-portal-root") + ContainerNode(key = "dnd-portal-source") + .apply { + draggable = true + width = 80 + height = 20 + }.applyParent(root) + root.render(ctx, 0, 0, 300, 120) + return root + } + + private fun startDrag(root: ContainerNode, draggable: DOMNode) { + DndRuntime.engine.cancelActiveDrag() + val down = + MouseDownEvent(10, 10, MouseButton.LEFT).also { event -> + event.target = draggable + } + DndRuntime.engine.onMouseDown(root, draggable, down) + DndRuntime.engine.onMouseMove(root, 80, 10) + assertTrue(DndRuntime.engine.isDragging) + } +} From 87d628abae3b4f22d8a1dde9a728820c8ec2836e Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Wed, 3 Jun 2026 20:33:47 +0300 Subject: [PATCH 76/78] removing old overlay mentions in the code; renaming overlay package to portal; --- .../mc-forge-1-7-10/demo/build.gradle.kts | 8 +- .../mc-forge-1-7-10/demo/gradle.properties | 4 +- .../demo/sections/ColorPickerSection.kt | 2 +- .../demo/sections/ContextMenuSection.kt | 2 +- .../demo/sections/DragDropSection.kt | 2 +- .../demo/sections/InputsGallerySection.kt | 4 +- .../demo/sections/InspectorSection.kt | 2 +- .../demo/sections/LayoutStyleSection.kt | 123 ++--- .../support/CapabilityChecklistCatalog.kt | 16 +- .../mcForge1710/demo/support/DemoSection.kt | 2 +- ...itionedLayoutStickyDemoIntegrationTests.kt | 4 +- adapters/mc-forge-1-7-10/detekt-baseline.xml | 10 +- .../dsgl/mcForge1710/DsglScreenHost.kt | 474 +++++++++--------- .../dsgl/mcForge1710/Mc1710UiAdapter.kt | 6 +- .../ScreenDomainSurfaceOrchestrator.kt | 4 +- ...glScreenHostApplicationPortalFrameTests.kt | 38 +- .../DsglScreenHostDomainOrchestrationTests.kt | 66 +-- core/detekt-baseline.xml | 172 +++---- .../core/colorpicker/ColorPickerController.kt | 68 +-- .../colorpicker/ColorPickerPopupEngine.kt | 42 +- .../ColorPickerPortalController.kt | 30 +- .../dsgl/core/colorpicker/ColorPickerStyle.kt | 8 +- .../internal/ColorPickerPopupMount.kt | 24 +- .../SystemColorPickerCustomSurfaceNodes.kt | 4 +- .../internal/SystemColorPickerPanelManager.kt | 4 +- .../SystemColorPickerPopupBodyNode.kt | 60 +-- ...Node.kt => SystemColorPickerPortalNode.kt} | 14 +- .../modal/internal/ModalPortalController.kt | 38 +- .../core/contextmenu/ContextMenuEngine.kt | 2 +- .../dsgl/core/debug/DebugDomainHosts.kt | 132 ++--- ...bugState.kt => DomainSurfaceDebugState.kt} | 76 +-- .../dsgl/core/dnd/DndInterfaces.kt | 6 +- .../core/dnd/internal/DefaultDndEngine.kt | 2 +- .../dsgl/core/dom/ContextMenuEvents.kt | 2 +- .../dom/elements/ColorPickerInlineNode.kt | 4 +- .../dom/elements/ColorPickerPopupPaneNode.kt | 2 +- .../dsgl/core/dom/elements/SelectNode.kt | 18 +- .../dsgl/core/dsl/ComponentProps.kt | 1 + .../dreamfinity/dsgl/core/dsl/ContainerDsl.kt | 20 +- .../org/dreamfinity/dsgl/core/dsl/InputDsl.kt | 6 +- .../core/inspector/InspectorController.kt | 72 +-- ...ayNode.kt => SystemInspectorPortalNode.kt} | 120 ++--- .../ColorPickerPopupOverlayOwnership.kt | 8 - .../core/overlay/OverlayDebugVisualization.kt | 19 - ...plicationFloatingWindowPortalController.kt | 56 +-- .../ApplicationPortalHost.kt} | 60 +-- .../ApplicationPortalRootNode.kt} | 19 +- .../portal/ColorPickerPopupPortalOwnership.kt | 8 + .../DomainPortalServices.kt | 13 +- .../portal/DomainSurfaceDebugVisualization.kt | 19 + .../{overlay => portal}/DomainSurfaceHost.kt | 2 +- .../PortalHostContracts.kt | 2 +- .../ScreenDomainContracts.kt | 20 +- .../input/PointerCaptureSession.kt | 2 +- .../input/SurfaceDomInputRouter.kt} | 4 +- .../input/SurfaceInputDispatch.kt} | 2 +- .../panel/FloatingPanel.kt} | 172 +++---- .../panel/FloatingPanelDragSession.kt} | 18 +- .../panel/FloatingPanelState.kt} | 4 +- .../system/SystemPortalCommandDslRenderer.kt} | 16 +- .../system/SystemPortalDebugCounters.kt} | 6 +- .../system/SystemPortalEntries.kt} | 90 ++-- .../system/SystemPortalHost.kt} | 218 ++++---- .../SystemPortalRawRenderCommandNode.kt} | 4 +- .../system/SystemPortalRootNode.kt} | 48 +- .../dsgl/core/render/RenderCommand.kt | 6 +- .../dsgl/core/select/SelectEngine.kt | 2 +- .../core/select/SelectPortalController.kt | 48 +- .../dsgl/core/select/SelectPortalRequest.kt | 4 +- .../dsgl/core/style/StyleApplicationScope.kt | 2 +- .../dsgl/core/style/StyleEngine.kt | 2 +- .../dsgl/core/ContainerDslTests.kt | 34 ++ .../dsgl/core/DomTreeCachingTests.kt | 2 +- .../colorpicker/ColorPickerControllerTests.kt | 20 +- .../colorpicker/ColorPickerInlineNodeTests.kt | 4 +- .../ColorPickerPopupEngineTests.kt | 42 +- .../ModalPortalKeyboardRegressionTests.kt | 232 ++++----- .../modal/ModalPortalLayoutRegressionTests.kt | 36 +- .../ModalPortalPointerRegressionTests.kt | 110 ++-- .../contextmenu/ContextMenuEngineTests.kt | 6 +- .../dsgl/core/debug/DebugDomainHostsTests.kt | 146 +++--- .../core/dom/OverflowInputClippingTests.kt | 18 +- .../PositionedLayoutStickyBehaviorTests.kt | 10 +- .../core/dom/RangeInputScrollFastPathTests.kt | 10 +- .../core/dom/ScrollContainerStateTests.kt | 4 +- .../dom/ScrollPerformanceCountersTests.kt | 6 +- .../core/dom/ScrollReactiveSmoothTests.kt | 6 +- .../dom/ScrollbarRenderingInteractionTests.kt | 12 +- ...Tests.kt => SelectNodeOwnerDomainTests.kt} | 34 +- .../dom/SelectPopupAnchoringStickyTests.kt | 8 +- ...dGeometryInspectorCharacterizationTests.kt | 8 +- .../inspector/InspectorControllerTests.kt | 16 +- ...stemInspectorPortalFocusIsolationTests.kt} | 52 +- ... SystemInspectorPortalInputBoundsTests.kt} | 6 +- .../overlay/OverlayDebugVisualizationTests.kt | 95 ---- .../ApplicationDndGhostPortalTests.kt | 6 +- .../ApplicationFloatingWindowPortalTests.kt | 16 +- .../DomainSurfaceDebugVisualizationTests.kt | 95 ++++ .../DomainSurfaceInteractionPathTests.kt} | 328 ++++++------ .../PortalGeometryIntegrationTests.kt} | 16 +- .../PortalHostContractsTests.kt | 12 +- .../ScreenDomainContractsTests.kt | 30 +- .../input/PointerCaptureSessionTests.kt | 2 +- .../input/SurfaceDomInputRouterTests.kt} | 56 +-- .../panel/FloatingPanelTests.kt} | 24 +- .../InspectorDragScrollDomMigrationTests.kt | 66 +-- .../InspectorDropdownCorrectiveTests.kt | 36 +- .../system/InspectorInputPathBaselineTests.kt | 36 +- .../system/InspectorPointerAlignmentTests.kt | 26 +- .../InspectorTextEditingDomMigrationTests.kt | 22 +- .../SystemPortalColorPickerEntryTests.kt} | 148 +++--- .../system/SystemPortalDomBridgeTests.kt} | 68 +-- .../SystemPortalEntryInfrastructureTests.kt} | 72 +-- .../SystemPortalInspectorNativeEntryTests.kt} | 202 ++++---- .../SystemPortalStyleIsolationTests.kt} | 26 +- .../DomainPortalServicesOwnershipTests.kt | 16 +- .../dsgl/core/select/SelectEngineTests.kt | 8 +- .../select/SelectPortalControllerTests.kt | 10 +- 118 files changed, 2409 insertions(+), 2397 deletions(-) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/{SystemColorPickerOverlayNode.kt => SystemColorPickerPortalNode.kt} (84%) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/{OverlayLayerDebugState.kt => DomainSurfaceDebugState.kt} (65%) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/{SystemInspectorOverlayNode.kt => SystemInspectorPortalNode.kt} (91%) delete mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ColorPickerPopupOverlayOwnership.kt delete mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayDebugVisualization.kt rename core/src/main/kotlin/org/dreamfinity/dsgl/core/{overlay => portal}/ApplicationFloatingWindowPortalController.kt (90%) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/{overlay/ApplicationOverlayHost.kt => portal/ApplicationPortalHost.kt} (89%) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/{overlay/ApplicationOverlayRootNode.kt => portal/ApplicationPortalRootNode.kt} (78%) create mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/ColorPickerPopupPortalOwnership.kt rename core/src/main/kotlin/org/dreamfinity/dsgl/core/{overlay => portal}/DomainPortalServices.kt (77%) create mode 100644 core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/DomainSurfaceDebugVisualization.kt rename core/src/main/kotlin/org/dreamfinity/dsgl/core/{overlay => portal}/DomainSurfaceHost.kt (95%) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/{overlay => portal}/PortalHostContracts.kt (99%) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/{overlay => portal}/ScreenDomainContracts.kt (86%) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/{overlay => portal}/input/PointerCaptureSession.kt (98%) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/{overlay/input/LayerDomInputRouter.kt => portal/input/SurfaceDomInputRouter.kt} (99%) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/{overlay/input/LayerInputDispatch.kt => portal/input/SurfaceInputDispatch.kt} (82%) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/{overlay/panel/OverlayPanel.kt => portal/panel/FloatingPanel.kt} (79%) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/{overlay/panel/OverlayPanelDragSession.kt => portal/panel/FloatingPanelDragSession.kt} (79%) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/{overlay/panel/OverlayPanelState.kt => portal/panel/FloatingPanelState.kt} (91%) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/{overlay/system/SystemOverlayCommandDslRenderer.kt => portal/system/SystemPortalCommandDslRenderer.kt} (71%) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/{overlay/system/SystemOverlayDebugCounters.kt => portal/system/SystemPortalDebugCounters.kt} (91%) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/{overlay/system/SystemOverlayEntries.kt => portal/system/SystemPortalEntries.kt} (63%) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/{overlay/system/SystemOverlayHost.kt => portal/system/SystemPortalHost.kt} (76%) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/{overlay/system/SystemOverlayRawRenderCommandNode.kt => portal/system/SystemPortalRawRenderCommandNode.kt} (92%) rename core/src/main/kotlin/org/dreamfinity/dsgl/core/{overlay/system/SystemOverlayRootNode.kt => portal/system/SystemPortalRootNode.kt} (72%) create mode 100644 core/src/test/kotlin/org/dreamfinity/dsgl/core/ContainerDslTests.kt rename core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/{SelectNodeOwnerScopeTests.kt => SelectNodeOwnerDomainTests.kt} (86%) rename core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/{SystemInspectorOverlayFocusIsolationTests.kt => SystemInspectorPortalFocusIsolationTests.kt} (80%) rename core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/{SystemInspectorOverlayInputBoundsTests.kt => SystemInspectorPortalInputBoundsTests.kt} (91%) delete mode 100644 core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayDebugVisualizationTests.kt rename core/src/test/kotlin/org/dreamfinity/dsgl/core/{overlay => portal}/ApplicationDndGhostPortalTests.kt (96%) rename core/src/test/kotlin/org/dreamfinity/dsgl/core/{overlay => portal}/ApplicationFloatingWindowPortalTests.kt (95%) create mode 100644 core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/DomainSurfaceDebugVisualizationTests.kt rename core/src/test/kotlin/org/dreamfinity/dsgl/core/{overlay/LiveLayerInteractionPathTests.kt => portal/DomainSurfaceInteractionPathTests.kt} (69%) rename core/src/test/kotlin/org/dreamfinity/dsgl/core/{overlay/OverlayGeometryIntegrationTests.kt => portal/PortalGeometryIntegrationTests.kt} (91%) rename core/src/test/kotlin/org/dreamfinity/dsgl/core/{overlay => portal}/PortalHostContractsTests.kt (98%) rename core/src/test/kotlin/org/dreamfinity/dsgl/core/{overlay => portal}/ScreenDomainContractsTests.kt (90%) rename core/src/test/kotlin/org/dreamfinity/dsgl/core/{overlay => portal}/input/PointerCaptureSessionTests.kt (98%) rename core/src/test/kotlin/org/dreamfinity/dsgl/core/{overlay/input/LayerDomInputRouterTests.kt => portal/input/SurfaceDomInputRouterTests.kt} (90%) rename core/src/test/kotlin/org/dreamfinity/dsgl/core/{overlay/panel/OverlayPanelTests.kt => portal/panel/FloatingPanelTests.kt} (89%) rename core/src/test/kotlin/org/dreamfinity/dsgl/core/{overlay => portal}/system/InspectorDragScrollDomMigrationTests.kt (88%) rename core/src/test/kotlin/org/dreamfinity/dsgl/core/{overlay => portal}/system/InspectorDropdownCorrectiveTests.kt (93%) rename core/src/test/kotlin/org/dreamfinity/dsgl/core/{overlay => portal}/system/InspectorInputPathBaselineTests.kt (94%) rename core/src/test/kotlin/org/dreamfinity/dsgl/core/{overlay => portal}/system/InspectorPointerAlignmentTests.kt (95%) rename core/src/test/kotlin/org/dreamfinity/dsgl/core/{overlay => portal}/system/InspectorTextEditingDomMigrationTests.kt (93%) rename core/src/test/kotlin/org/dreamfinity/dsgl/core/{overlay/system/SystemOverlayColorPickerEntryTests.kt => portal/system/SystemPortalColorPickerEntryTests.kt} (88%) rename core/src/test/kotlin/org/dreamfinity/dsgl/core/{overlay/system/SystemOverlayDomBridgeTests.kt => portal/system/SystemPortalDomBridgeTests.kt} (67%) rename core/src/test/kotlin/org/dreamfinity/dsgl/core/{overlay/system/SystemOverlayEntryInfrastructureTests.kt => portal/system/SystemPortalEntryInfrastructureTests.kt} (74%) rename core/src/test/kotlin/org/dreamfinity/dsgl/core/{overlay/system/SystemOverlayInspectorNativeEntryTests.kt => portal/system/SystemPortalInspectorNativeEntryTests.kt} (89%) rename core/src/test/kotlin/org/dreamfinity/dsgl/core/{overlay/system/SystemOverlayStyleIsolationTests.kt => portal/system/SystemPortalStyleIsolationTests.kt} (83%) diff --git a/adapters/mc-forge-1-7-10/demo/build.gradle.kts b/adapters/mc-forge-1-7-10/demo/build.gradle.kts index 0e9319d..5245f0d 100644 --- a/adapters/mc-forge-1-7-10/demo/build.gradle.kts +++ b/adapters/mc-forge-1-7-10/demo/build.gradle.kts @@ -21,8 +21,8 @@ val msdfDebugDecorations: String by project val msdfDebugPerformance: String by project val rebuildTrace: String by project val perfDebug: String by project -val dsglOverlayDebug: String by project -val dsglOverlayControls: String by project +val dsglDomainDebug: String by project +val dsglDomainControls: String by project val dsglColorPickerDebugCounters: String by project val hotReloadAgentLibraryName: String? by project @@ -110,8 +110,8 @@ tasks { "-Ddsgl.msdf.debug.performance=$msdfDebugPerformance", "-Ddsgl.rebuild.trace=$rebuildTrace", "-Ddsgl.perf.debug=$perfDebug", - "-Ddsgl.overlay.debug=$dsglOverlayDebug", - "-Ddsgl.overlay.controls=$dsglOverlayControls", + "-Ddsgl.domain.debug=$dsglDomainDebug", + "-Ddsgl.domain.controls=$dsglDomainControls", "-Ddsgl.colorPicker.debugCounters=$dsglColorPickerDebugCounters", ) diff --git a/adapters/mc-forge-1-7-10/demo/gradle.properties b/adapters/mc-forge-1-7-10/demo/gradle.properties index d0e0d33..8f2674f 100644 --- a/adapters/mc-forge-1-7-10/demo/gradle.properties +++ b/adapters/mc-forge-1-7-10/demo/gradle.properties @@ -26,8 +26,8 @@ msdfDebugDecorations=false msdfDebugPerformance=false rebuildTrace=false perfDebug=false -dsglOverlayDebug=true -dsglOverlayControls=true +dsglDomainDebug=true +dsglDomainControls=true dsglColorPickerDebugCounters=false startParameter.offline=true diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ColorPickerSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ColorPickerSection.kt index fb36beb..d6105e4 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ColorPickerSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ColorPickerSection.kt @@ -78,7 +78,7 @@ fun UiScope.colorPickerSection() { { style = { color = DEMO_MUTED } }, ) text( - "Inline picker follows app styling. Inspector picker (F12) is rendered in isolated system overlay styles.", + "Inline picker follows app styling. Inspector picker (F12) is rendered in isolated system portal styles.", { style = { color = DEMO_MUTED } }, ) diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ContextMenuSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ContextMenuSection.kt index 18252bc..1cb2078 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ContextMenuSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/ContextMenuSection.kt @@ -10,7 +10,7 @@ import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.hooks.useState -import org.dreamfinity.dsgl.core.overlay.DomainPortalServices +import org.dreamfinity.dsgl.core.portal.DomainPortalServices import org.dreamfinity.dsgl.core.style.AlignItems import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/DragDropSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/DragDropSection.kt index 478c264..2c61e01 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/DragDropSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/DragDropSection.kt @@ -529,7 +529,7 @@ fun UiScope.dragNDropSection( flexDirection = FlexDirection.Column } }) { - text("Drag preview modes: ORIGINAL (detached source) and GHOST (overlay preview).") + text("Drag preview modes: ORIGINAL (detached source) and GHOST (portal preview).") text( "active=${state.activeItem} mode=${monitor.mode?.name ?: "none"} " + "effect=${state.dropEffect} hover=${state.hoverZone}", diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt index 92057ae..ce748b1 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InputsGallerySection.kt @@ -5,7 +5,7 @@ import org.dreamfinity.dsgl.core.dom.elements.InputType import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.hooks.useState -import org.dreamfinity.dsgl.core.overlay.DomainPortalServices +import org.dreamfinity.dsgl.core.portal.DomainPortalServices import org.dreamfinity.dsgl.core.select.SelectStyle import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection @@ -289,7 +289,7 @@ fun UiScope.inputsGallerySection(clippingScrollDemoText: String, onClippingScrol } }) - text("Select (overlay popup + keyboard + disabled options)") + text("Select (portal popup + keyboard + disabled options)") text( "Use Enter/Space/ArrowDown when focused. Esc closes popup. Wheel scrolls long list.", { style = { color = DEMO_MUTED } }, diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InspectorSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InspectorSection.kt index c8648c5..5a9b3e4 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InspectorSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/InspectorSection.kt @@ -22,7 +22,7 @@ fun UiScope.inspectorSection(onInfo: (String) -> Unit) { } }) { text("In-game Inspector is global (works on every DSGL screen).") - text("F12: toggle inspector overlay", { style = { color = INSPECTOR_MUTED_TEXT } }) + text("F12: toggle inspector portal", { style = { color = INSPECTOR_MUTED_TEXT } }) text("F9: switch mode (Pick/Locked)", { style = { color = INSPECTOR_MUTED_TEXT } }) text("Expanded panel: click Min to collapse into floating chip.", { style = { color = INSPECTOR_MUTED_TEXT } }) text("Minimized chip: drag to move, click (no drag) to restore.", { style = { color = INSPECTOR_MUTED_TEXT } }) diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutStyleSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutStyleSection.kt index ece510b..242f357 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutStyleSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/LayoutStyleSection.kt @@ -19,23 +19,24 @@ fun UiScope.layoutStyleSection(onInfo: (String) -> Unit, onLogHook: (String, Eve var styleUseBorder by useState(true) var styleLargeGap by useState(false) var styleFixedSize by useState(false) - var stackOverlayEnabled by useState(true) - var layoutOverlayX by useState(8) - var layoutOverlayY by useState(92) - var layoutOverlayDragging by useState(false) - var overlayClicks by useState(0) + var stackLayerEnabled by useState(true) + var stackLayerX by useState(8) + var stackLayerY by useState(92) + var stackLayerDragging by useState(false) + var stackLayerClicks by useState(0) - val overlayDragAnchorXRef by useRef(0) - val overlayDragAnchorYRef by useRef(0) - val overlayDragMovedRef by useRef(false) + val stackLayerDragAnchorXRef by useRef(0) + val stackLayerDragAnchorYRef by useRef(0) + val stackLayerDragMovedRef by useRef(false) val demoGap = if (styleLargeGap) 10 else 3 val fixedSize = if (styleFixedSize) 24 else null - val overlayWidth = 148 - val overlayHeight = 26 + val stackLayerWidth = 148 + val stackLayerHeight = 26 - overlay({ + div({ key = "section.layoutStyle.stack" + overlapChildren = true style = { width = 100.percent gap = 0.px @@ -194,89 +195,89 @@ fun UiScope.layoutStyleSection(onInfo: (String) -> Unit, onLogHook: (String, Eve flexDirection = FlexDirection.Row } }) { - button(if (stackOverlayEnabled) "Stack Overlay ON" else "Stack Overlay OFF", { + button(if (stackLayerEnabled) "Stack Layer ON" else "Stack Layer OFF", { onMouseClick = { - stackOverlayEnabled = !stackOverlayEnabled - onInfo("Layout: stackOverlay=$stackOverlayEnabled") + stackLayerEnabled = !stackLayerEnabled + onInfo("Layout: stackLayer=$stackLayerEnabled") } }) - button("Reset Overlay", { + button("Reset Stack Layer", { onMouseClick = { - layoutOverlayX = 8 - layoutOverlayY = 92 - layoutOverlayDragging = false - overlayDragMovedRef.current = false - onInfo("Layout: overlay reset") + stackLayerX = 8 + stackLayerY = 92 + stackLayerDragging = false + stackLayerDragMovedRef.current = false + onInfo("Layout: stack layer reset") } }) text( - "Overlay: $layoutOverlayX,$layoutOverlayY clicks=$overlayClicks", + "Stack layer: $stackLayerX,$stackLayerY clicks=$stackLayerClicks", { style = { color = DEMO_MUTED } }, ) } } - if (stackOverlayEnabled) { + if (stackLayerEnabled) { div({ - key = "layout.stack.overlay" + key = "layout.stack.layer" onMouseDown = onMouseDown@{ event -> if (event.mouseButton != MouseButton.LEFT) return@onMouseDown - val overlayNode = findNodeInPath(event.target, "layout.stack.overlay") ?: return@onMouseDown - layoutOverlayDragging = true - overlayDragAnchorXRef.current = - (event.mouseX - overlayNode.bounds.x).coerceIn( + val stackLayerNode = findNodeInPath(event.target, "layout.stack.layer") ?: return@onMouseDown + stackLayerDragging = true + stackLayerDragAnchorXRef.current = + (event.mouseX - stackLayerNode.bounds.x).coerceIn( 0, - overlayNode.bounds.width + stackLayerNode.bounds.width .coerceAtLeast(1), ) - overlayDragAnchorYRef.current = - (event.mouseY - overlayNode.bounds.y).coerceIn( + stackLayerDragAnchorYRef.current = + (event.mouseY - stackLayerNode.bounds.y).coerceIn( 0, - overlayNode.bounds.height + stackLayerNode.bounds.height .coerceAtLeast(1), ) - overlayDragMovedRef.current = false + stackLayerDragMovedRef.current = false } onMouseDrag = { event -> - updateOverlayDrag( + updateStackLayerDrag( event = event, - overlayWidth = overlayWidth, - overlayHeight = overlayHeight, - isDragging = layoutOverlayDragging, - currentX = layoutOverlayX, - currentY = layoutOverlayY, - anchorX = overlayDragAnchorXRef.current ?: 0, - anchorY = overlayDragAnchorYRef.current ?: 0, + stackLayerWidth = stackLayerWidth, + stackLayerHeight = stackLayerHeight, + isDragging = stackLayerDragging, + currentX = stackLayerX, + currentY = stackLayerY, + anchorX = stackLayerDragAnchorXRef.current ?: 0, + anchorY = stackLayerDragAnchorYRef.current ?: 0, ) { nextX, nextY, moved -> if (moved) { - overlayDragMovedRef.current = true + stackLayerDragMovedRef.current = true } - if (nextX != layoutOverlayX) { - layoutOverlayX = nextX + if (nextX != stackLayerX) { + stackLayerX = nextX } - if (nextY != layoutOverlayY) { - layoutOverlayY = nextY + if (nextY != stackLayerY) { + stackLayerY = nextY } } } onMouseUp = onMouseUp@{ event -> - if (!layoutOverlayDragging) return@onMouseUp - if (event.mouseButton == MouseButton.LEFT && !(overlayDragMovedRef.current ?: false)) { - overlayClicks += 1 - onLogHook("overlay.onMouseClick", event, "overlayClicks=$overlayClicks") + if (!stackLayerDragging) return@onMouseUp + if (event.mouseButton == MouseButton.LEFT && !(stackLayerDragMovedRef.current ?: false)) { + stackLayerClicks += 1 + onLogHook("stackLayer.onMouseClick", event, "stackLayerClicks=$stackLayerClicks") } - layoutOverlayDragging = false - overlayDragMovedRef.current = false + stackLayerDragging = false + stackLayerDragMovedRef.current = false } style = { - width = overlayWidth.px - height = overlayHeight.px + width = stackLayerWidth.px + height = stackLayerHeight.px backgroundColor = 0xCC5A3131.toInt() margin { - top = layoutOverlayY.px + top = stackLayerY.px right = 0.px bottom = 0.px - left = layoutOverlayX.px + left = stackLayerX.px } padding { all(4.px) } border { @@ -286,7 +287,7 @@ fun UiScope.layoutStyleSection(onInfo: (String) -> Unit, onLogHook: (String, Eve } }) { text( - if (layoutOverlayDragging) "Overlay (dragging...)" else "Overlay (drag me)", + if (stackLayerDragging) "Stack layer (dragging...)" else "Stack layer (drag me)", { style = { color = 0xFFF5F7FA.toInt() } }, ) } @@ -294,10 +295,10 @@ fun UiScope.layoutStyleSection(onInfo: (String) -> Unit, onLogHook: (String, Eve } } -private fun updateOverlayDrag( +private fun updateStackLayerDrag( event: MouseDragEvent, - overlayWidth: Int, - overlayHeight: Int, + stackLayerWidth: Int, + stackLayerHeight: Int, isDragging: Boolean, currentX: Int, currentY: Int, @@ -309,8 +310,8 @@ private fun updateOverlayDrag( val stackNode = findNodeInPath(event.target, "section.layoutStyle.stack") ?: return val currentMouseX = event.lastMouseX + event.dx val currentMouseY = event.lastMouseY + event.dy - val maxX = (stackNode.bounds.width - overlayWidth - 2).coerceAtLeast(0) - val maxY = (stackNode.bounds.height - overlayHeight - 2).coerceAtLeast(0) + val maxX = (stackNode.bounds.width - stackLayerWidth - 2).coerceAtLeast(0) + val maxY = (stackNode.bounds.height - stackLayerHeight - 2).coerceAtLeast(0) val nextX = (currentMouseX - stackNode.bounds.x - anchorX).coerceIn(0, maxX) val nextY = (currentMouseY - stackNode.bounds.y - anchorY).coerceIn(0, maxY) val moved = abs(nextX - currentX) > 0 || abs(nextY - currentY) > 0 diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/CapabilityChecklistCatalog.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/CapabilityChecklistCatalog.kt index 0fe6157..a75b7c1 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/CapabilityChecklistCatalog.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/CapabilityChecklistCatalog.kt @@ -15,7 +15,7 @@ enum class CapabilityId( val group: CapabilityGroup, ) { BUILDER_DIV("builder: div", CapabilityGroup.DSL_BUILDERS), - BUILDER_OVERLAY("builder: overlay", CapabilityGroup.DSL_BUILDERS), + BUILDER_STACK_LAYOUT("builder: stack-layout DSL", CapabilityGroup.DSL_BUILDERS), BUILDER_TEXT("builder: text", CapabilityGroup.DSL_BUILDERS), BUILDER_TEXT_LAMBDA("builder: text(lambda)", CapabilityGroup.DSL_BUILDERS), BUILDER_BUTTON("builder: button", CapabilityGroup.DSL_BUILDERS), @@ -73,18 +73,18 @@ enum class CapabilityId( ANIMATION_TRANSFORM("Transform animation demo", CapabilityGroup.SHOWCASE_FEATURES), ANIMATION_OPACITY("Opacity animation demo", CapabilityGroup.SHOWCASE_FEATURES), ANIMATION_KEYFRAMES("Keyframes animation demo", CapabilityGroup.SHOWCASE_FEATURES), - MODAL_HOST("Modal host overlay", CapabilityGroup.SHOWCASE_FEATURES), + MODAL_HOST("Modal portal host", CapabilityGroup.SHOWCASE_FEATURES), MODAL_STACKING("Modal deterministic stacking", CapabilityGroup.SHOWCASE_FEATURES), MODAL_BACKDROP("Modal backdrop behaviors", CapabilityGroup.SHOWCASE_FEATURES), MODAL_ESCAPE("Modal ESC close behavior", CapabilityGroup.SHOWCASE_FEATURES), MODAL_FOCUS_TRAP("Modal focus trap + restore", CapabilityGroup.SHOWCASE_FEATURES), - CONTEXT_MENU_OVERLAY("Context menu overlay rendering", CapabilityGroup.SHOWCASE_FEATURES), + CONTEXT_MENU_PORTAL("Context menu portal rendering", CapabilityGroup.SHOWCASE_FEATURES), CONTEXT_MENU_NESTED("Context menu nested submenus", CapabilityGroup.SHOWCASE_FEATURES), CONTEXT_MENU_ANCHORED("Context menu anchored + cursor open", CapabilityGroup.SHOWCASE_FEATURES), CONTEXT_MENU_SCROLL("Context menu overflow + wheel scroll", CapabilityGroup.SHOWCASE_FEATURES), LAYOUT_GAP_FIXED("Gap + fixed-size demo", CapabilityGroup.SHOWCASE_FEATURES), STYLE_MARGIN_PADDING_BORDER("Style margin/padding/border toggles", CapabilityGroup.SHOWCASE_FEATURES), - OVERLAY_BEHAVIOR("Overlay behavior demo", CapabilityGroup.SHOWCASE_FEATURES), + PORTAL_BEHAVIOR("Portal behavior demo", CapabilityGroup.SHOWCASE_FEATURES), STYLESHEET_SELECTORS("Stylesheet selectors demo", CapabilityGroup.SHOWCASE_FEATURES), STYLESHEET_COMBINATORS("Stylesheet descendant/child/sibling combinators demo", CapabilityGroup.SHOWCASE_FEATURES), STYLESHEET_CASCADE("Stylesheet cascade/specificity/important/inheritance demo", CapabilityGroup.SHOWCASE_FEATURES), @@ -135,11 +135,11 @@ object CapabilityChecklistCatalog { DemoSection.LAYOUT_STYLE -> setOf( CapabilityId.BUILDER_DIV, - CapabilityId.BUILDER_OVERLAY, + CapabilityId.BUILDER_STACK_LAYOUT, CapabilityId.HOOK_MOUSE_CLICK, CapabilityId.LAYOUT_GAP_FIXED, CapabilityId.STYLE_MARGIN_PADDING_BORDER, - CapabilityId.OVERLAY_BEHAVIOR, + CapabilityId.PORTAL_BEHAVIOR, ) DemoSection.LAYOUT_DEBUG -> @@ -161,7 +161,7 @@ object CapabilityChecklistCatalog { CapabilityId.HOOK_MOUSE_LEAVE, CapabilityId.HOOK_MOUSE_CLICK, CapabilityId.HOOK_MOUSE_WHEEL, - CapabilityId.OVERLAY_BEHAVIOR, + CapabilityId.PORTAL_BEHAVIOR, ) DemoSection.OVERFLOW_SCROLL -> @@ -238,7 +238,7 @@ object CapabilityChecklistCatalog { CapabilityId.BUILDER_TEXT, CapabilityId.BUILDER_BUTTON, CapabilityId.HOOK_MOUSE_DOWN, - CapabilityId.CONTEXT_MENU_OVERLAY, + CapabilityId.CONTEXT_MENU_PORTAL, CapabilityId.CONTEXT_MENU_NESTED, CapabilityId.CONTEXT_MENU_ANCHORED, CapabilityId.CONTEXT_MENU_SCROLL, diff --git a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/DemoSection.kt b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/DemoSection.kt index 8256be0..e8c24ea 100644 --- a/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/DemoSection.kt +++ b/adapters/mc-forge-1-7-10/demo/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/support/DemoSection.kt @@ -23,7 +23,7 @@ enum class DemoSection( "Descendant/child/sibling selectors, specificity, source order, !important, inheritance", ), MODALS("Modals", "Declarative stacked modal portal (RB-inspired)"), - CONTEXT_MENU("Context Menu", "Right-click nested menus with overlay-first hit testing"), + CONTEXT_MENU("Context Menu", "Right-click nested menus with portal-first hit testing"), INPUTS("Inputs Gallery", "All input factory variants and textarea"), INPUT_EVENTS("Input Events", "HTML-like onFocus/onBlur/onInput/onChange"), COLOR_PICKER("Color Picker", "Reusable inline + popup pane color picker with eyedropper/history"), diff --git a/adapters/mc-forge-1-7-10/demo/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutStickyDemoIntegrationTests.kt b/adapters/mc-forge-1-7-10/demo/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutStickyDemoIntegrationTests.kt index 663b306..109a4f7 100644 --- a/adapters/mc-forge-1-7-10/demo/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutStickyDemoIntegrationTests.kt +++ b/adapters/mc-forge-1-7-10/demo/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/demo/sections/PositionedLayoutStickyDemoIntegrationTests.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.mcForge1710.demo.sections +package org.dreamfinity.dsgl.mcForge1710.demo.sections import org.dreamfinity.dsgl.core.DomTree import org.dreamfinity.dsgl.core.dom.DOMNode @@ -250,7 +250,7 @@ class PositionedLayoutStickyDemoIntegrationTests { } private fun inspectorHoveredBorderRect(inspector: InspectorController): Rect? { - val highlightMethod = findMethodByNameAndArity(inspector.javaClass, "overlayHoveredHighlight", 0) + val highlightMethod = findMethodByNameAndArity(inspector.javaClass, "portalHoveredHighlight", 0) highlightMethod.isAccessible = true val snapshot = highlightMethod.invoke(inspector) ?: return null val borderRectField = findField(snapshot.javaClass, "borderRect") diff --git a/adapters/mc-forge-1-7-10/detekt-baseline.xml b/adapters/mc-forge-1-7-10/detekt-baseline.xml index 3ae6052..0370594 100644 --- a/adapters/mc-forge-1-7-10/detekt-baseline.xml +++ b/adapters/mc-forge-1-7-10/detekt-baseline.xml @@ -7,10 +7,10 @@ ComplexCondition:DsglScreenHost.kt$DsglScreenHost$lastWidth > 0 && lastHeight > 0 && (rootBounds.width <= 0 || rootBounds.height <= 0) ComplexCondition:Mc1710UiAdapter.kt$Mc1710UiAdapter$x < 0 || y < 0 || x >= viewport.width || y >= viewport.height ComplexCondition:MsdfTextRenderer.kt$MsdfTextRenderer.SegmentBuffer$last >= 0 && kind[last] == segmentKind && color[last] == segmentColor && kotlin.math.abs(endX[last] - segmentStartX) <= 0.51f && kotlin.math.abs(y[last] - segmentY) <= 0.51f && kotlin.math.abs(thickness[last] - segmentThickness) <= 0.1f - CyclomaticComplexMethod:DsglScreenHost.kt$DsglScreenHost$private fun consumeApplicationOverlayPointerEvent( mouseX: Int, mouseY: Int, dWheel: Int, mouseButton: Int, mappedButton: MouseButton?, buttonPressed: Boolean, ): Boolean - CyclomaticComplexMethod:DsglScreenHost.kt$DsglScreenHost$private fun consumeSystemOverlayPointerEvent( mouseX: Int, mouseY: Int, dWheel: Int, mouseButton: Int, mappedButton: MouseButton?, buttonPressed: Boolean, ): Boolean + CyclomaticComplexMethod:DsglScreenHost.kt$DsglScreenHost$private fun consumeApplicationPortalPointerEvent( mouseX: Int, mouseY: Int, dWheel: Int, mouseButton: Int, mappedButton: MouseButton?, buttonPressed: Boolean, ): Boolean + CyclomaticComplexMethod:DsglScreenHost.kt$DsglScreenHost$private fun consumeSystemPortalPointerEvent( mouseX: Int, mouseY: Int, dWheel: Int, mouseButton: Int, mappedButton: MouseButton?, buttonPressed: Boolean, ): Boolean CyclomaticComplexMethod:DsglScreenHost.kt$DsglScreenHost$private fun handleKeyboardKeyDown( keyCode: Int, keyChar: Char, inspectorMouseX: Int, inspectorMouseY: Int, ): Boolean - CyclomaticComplexMethod:DsglScreenHost.kt$DsglScreenHost$private fun updateFrameInteractionState( tree: DomTree, dtSeconds: Double, dsglMouseX: Int, dsglMouseY: Int, appOverlayInputEnabled: Boolean, systemOverlayInputEnabled: Boolean, inspectorBlocks: Boolean, ) + CyclomaticComplexMethod:DsglScreenHost.kt$DsglScreenHost$private fun updateFrameInteractionState( tree: DomTree, dtSeconds: Double, dsglMouseX: Int, dsglMouseY: Int, applicationPortalInputEnabled: Boolean, systemPortalInputEnabled: Boolean, inspectorBlocks: Boolean, ) CyclomaticComplexMethod:Mc1710UiAdapter.kt$Mc1710UiAdapter$@Suppress("LoopWithTooManyJumpStatements") override fun paint(commands: List<RenderCommand>) CyclomaticComplexMethod:MsdfTextRenderer.kt$MsdfTextRenderer$fun draw(command: RenderCommand.DrawText, opacityMultiplier: Float) CyclomaticComplexMethod:MsdfTextRenderer.kt$MsdfTextRenderer$private fun drawDecorationSegments(segments: SegmentBuffer, includeDebug: Boolean) @@ -61,8 +61,8 @@ MagicNumber:MsdfTextRenderer.kt$MsdfTextRenderer.SegmentBuffer$0.51f NestedBlockDepth:Mc1710UiAdapter.kt$Mc1710UiAdapter$@Suppress("LoopWithTooManyJumpStatements") override fun paint(commands: List<RenderCommand>) NestedBlockDepth:MsdfTextRenderer.kt$MsdfTextRenderer$fun draw(command: RenderCommand.DrawText, opacityMultiplier: Float) - ReturnCount:DsglScreenHost.kt$DsglScreenHost$private fun consumeApplicationOverlayPointerEvent( mouseX: Int, mouseY: Int, dWheel: Int, mouseButton: Int, mappedButton: MouseButton?, buttonPressed: Boolean, ): Boolean - ReturnCount:DsglScreenHost.kt$DsglScreenHost$private fun consumeSystemOverlayPointerEvent( mouseX: Int, mouseY: Int, dWheel: Int, mouseButton: Int, mappedButton: MouseButton?, buttonPressed: Boolean, ): Boolean + ReturnCount:DsglScreenHost.kt$DsglScreenHost$private fun consumeApplicationPortalPointerEvent( mouseX: Int, mouseY: Int, dWheel: Int, mouseButton: Int, mappedButton: MouseButton?, buttonPressed: Boolean, ): Boolean + ReturnCount:DsglScreenHost.kt$DsglScreenHost$private fun consumeSystemPortalPointerEvent( mouseX: Int, mouseY: Int, dWheel: Int, mouseButton: Int, mappedButton: MouseButton?, buttonPressed: Boolean, ): Boolean TooManyFunctions:DsglScreenHost.kt$DsglScreenHost : GuiScreenDsglWindowHost TooManyFunctions:Mc1710UiAdapter.kt$Mc1710UiAdapter : UiMeasureContext TooManyFunctions:MsdfTextRenderer.kt$MsdfTextRenderer diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt index e11de1e..02bdf4a 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHost.kt @@ -11,7 +11,7 @@ import org.dreamfinity.dsgl.core.animation.* import org.dreamfinity.dsgl.core.colorpicker.* import org.dreamfinity.dsgl.core.debug.DebugDomainPortalHost import org.dreamfinity.dsgl.core.debug.DebugDomainRootHost -import org.dreamfinity.dsgl.core.debug.OverlayLayerDebugState +import org.dreamfinity.dsgl.core.debug.DomainSurfaceDebugState import org.dreamfinity.dsgl.core.dnd.* import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.elements.* @@ -26,31 +26,31 @@ import org.dreamfinity.dsgl.core.host.rawMouseToDsglY import org.dreamfinity.dsgl.core.input.ClipboardAccess import org.dreamfinity.dsgl.core.input.ClipboardBridge import org.dreamfinity.dsgl.core.inspector.* -import org.dreamfinity.dsgl.core.overlay.ApplicationOverlayHost -import org.dreamfinity.dsgl.core.overlay.DomainSurfaceHost -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope -import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurface -import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces -import org.dreamfinity.dsgl.core.overlay.appendDndGhostPortalCommands -import org.dreamfinity.dsgl.core.overlay.appendPortalOverlayCommands -import org.dreamfinity.dsgl.core.overlay.captureColorPickerEyedropperSample -import org.dreamfinity.dsgl.core.overlay.closeFloatingPortals -import org.dreamfinity.dsgl.core.overlay.handlePortalKeyDownAfterDom -import org.dreamfinity.dsgl.core.overlay.handlePortalKeyDownBeforeDom -import org.dreamfinity.dsgl.core.overlay.handlePortalKeyUpAfterDom -import org.dreamfinity.dsgl.core.overlay.handlePortalKeyUpBeforeDom -import org.dreamfinity.dsgl.core.overlay.handlePortalPointerAfterDom -import org.dreamfinity.dsgl.core.overlay.handlePortalPointerBeforeDom -import org.dreamfinity.dsgl.core.overlay.hasActiveColorPickerEyedropper -import org.dreamfinity.dsgl.core.overlay.hasActiveModalPortal -import org.dreamfinity.dsgl.core.overlay.hasDomPointerTargetAt -import org.dreamfinity.dsgl.core.overlay.hasOpenColorPickerPortal -import org.dreamfinity.dsgl.core.overlay.hasOpenContextMenuPortal -import org.dreamfinity.dsgl.core.overlay.hasOpenSelectPortal -import org.dreamfinity.dsgl.core.overlay.input.PointerCaptureSession -import org.dreamfinity.dsgl.core.overlay.syncPortalFrame -import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayHost -import org.dreamfinity.dsgl.core.overlay.toggleFloatingWindowDemo +import org.dreamfinity.dsgl.core.portal.ApplicationPortalHost +import org.dreamfinity.dsgl.core.portal.DomainSurfaceHost +import org.dreamfinity.dsgl.core.portal.ScreenDomainId +import org.dreamfinity.dsgl.core.portal.ScreenDomainSurface +import org.dreamfinity.dsgl.core.portal.ScreenDomainSurfaces +import org.dreamfinity.dsgl.core.portal.appendDndGhostPortalCommands +import org.dreamfinity.dsgl.core.portal.appendFloatingPortalCommands +import org.dreamfinity.dsgl.core.portal.captureColorPickerEyedropperSample +import org.dreamfinity.dsgl.core.portal.closeFloatingPortals +import org.dreamfinity.dsgl.core.portal.handlePortalKeyDownAfterDom +import org.dreamfinity.dsgl.core.portal.handlePortalKeyDownBeforeDom +import org.dreamfinity.dsgl.core.portal.handlePortalKeyUpAfterDom +import org.dreamfinity.dsgl.core.portal.handlePortalKeyUpBeforeDom +import org.dreamfinity.dsgl.core.portal.handlePortalPointerAfterDom +import org.dreamfinity.dsgl.core.portal.handlePortalPointerBeforeDom +import org.dreamfinity.dsgl.core.portal.hasActiveColorPickerEyedropper +import org.dreamfinity.dsgl.core.portal.hasActiveModalPortal +import org.dreamfinity.dsgl.core.portal.hasDomPointerTargetAt +import org.dreamfinity.dsgl.core.portal.hasOpenColorPickerPortal +import org.dreamfinity.dsgl.core.portal.hasOpenContextMenuPortal +import org.dreamfinity.dsgl.core.portal.hasOpenSelectPortal +import org.dreamfinity.dsgl.core.portal.input.PointerCaptureSession +import org.dreamfinity.dsgl.core.portal.syncPortalFrame +import org.dreamfinity.dsgl.core.portal.system.SystemPortalHost +import org.dreamfinity.dsgl.core.portal.toggleFloatingWindowDemo import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.* import org.lwjgl.input.Keyboard @@ -105,13 +105,13 @@ abstract class DsglScreenHost( Collections.newSetFromMap(IdentityHashMap()) private val composedCommandsBuffer: MutableList = ArrayList(512) private val stagingCommandsBuffer: MutableList = ArrayList(512) - private val applicationOverlayCommandsBuffer: MutableList = ArrayList(256) - private val systemOverlayCommandsBuffer: MutableList = ArrayList(256) + private val applicationPortalCommandsBuffer: MutableList = ArrayList(256) + private val systemPortalCommandsBuffer: MutableList = ArrayList(256) private var activeTarget: DOMNode? = null private var lastFrameNanos: Long = 0L private val inspector: InspectorController = InspectorController() - private val applicationOverlayHost: ApplicationOverlayHost = ApplicationOverlayHost() - private val systemOverlayHost: SystemOverlayHost = SystemOverlayHost(inspector) + private val applicationPortalHost: ApplicationPortalHost = ApplicationPortalHost() + private val systemPortalHost: SystemPortalHost = SystemPortalHost(inspector) private val debugDomainRootHost: DebugDomainRootHost = DebugDomainRootHost() private val debugDomainPortalHost: DebugDomainPortalHost = DebugDomainPortalHost() private val domainOrchestrator: ScreenDomainSurfaceOrchestrator = ScreenDomainSurfaceOrchestrator() @@ -201,8 +201,8 @@ abstract class DsglScreenHost( mouseY = mouseY, partialTicks = partialTicks, ) ?: return - val overlayState = - syncInspectorAndResolveOverlayState( + val domainDebugState = + syncInspectorAndResolveSurfaceState( tree = tree, dsglMouseX = frameCursor.mouseX, dsglMouseY = frameCursor.mouseY, @@ -212,19 +212,19 @@ abstract class DsglScreenHost( dsglMouseX = frameCursor.mouseX, dsglMouseY = frameCursor.mouseY, ) - syncApplicationOverlaySurface( - appOverlayEnabled = overlayState.appOverlayRenderEnabled || overlayState.appOverlayInputEnabled, + syncApplicationPortalSurface( + appPortalEnabled = domainDebugState.appPortalRenderEnabled || domainDebugState.appPortalInputEnabled, ) - val systemOverlayCommands = - syncSystemOverlayAndCollectCommands( + val systemPortalCommands = + syncSystemPortalAndCollectCommands( tree = tree, dsglMouseX = frameCursor.mouseX, dsglMouseY = frameCursor.mouseY, - systemOverlayRenderEnabled = overlayState.systemOverlayRenderEnabled, + systemPortalRenderEnabled = domainDebugState.systemPortalRenderEnabled, ) - stageSystemOverlayCommands( - systemOverlayCommands = systemOverlayCommands, - systemOverlayRenderEnabled = overlayState.systemOverlayRenderEnabled, + stageSystemPortalCommands( + systemPortalCommands = systemPortalCommands, + systemPortalRenderEnabled = domainDebugState.systemPortalRenderEnabled, ) val debugDomainCommands = collectDebugDomainCommands() updateFrameInteractionState( @@ -232,9 +232,9 @@ abstract class DsglScreenHost( dtSeconds = dtSeconds, dsglMouseX = frameCursor.mouseX, dsglMouseY = frameCursor.mouseY, - appOverlayInputEnabled = overlayState.appOverlayInputEnabled, - systemOverlayInputEnabled = overlayState.systemOverlayInputEnabled, - inspectorBlocks = overlayState.inspectorBlocks, + appPortalInputEnabled = domainDebugState.appPortalInputEnabled, + systemPortalInputEnabled = domainDebugState.systemPortalInputEnabled, + inspectorBlocks = domainDebugState.inspectorBlocks, ) val commands = paintApplicationRootOrFallback( @@ -244,10 +244,10 @@ abstract class DsglScreenHost( mouseY = mouseY, partialTicks = partialTicks, ) ?: return - syncCollectAndStageApplicationOverlayAfterRootPaint( + syncCollectAndStageApplicationPortalAfterRootPaint( tree = tree, - appOverlayRenderEnabled = overlayState.appOverlayRenderEnabled, - appOverlayInputEnabled = overlayState.appOverlayInputEnabled, + appPortalRenderEnabled = domainDebugState.appPortalRenderEnabled, + appPortalInputEnabled = domainDebugState.appPortalInputEnabled, ) composeAndPresentFrame( tree = tree, @@ -274,11 +274,11 @@ abstract class DsglScreenHost( val layoutCommittedThisFrame: Boolean, ) - private data class OverlayLayerFrameState( - val appOverlayRenderEnabled: Boolean, - val systemOverlayRenderEnabled: Boolean, - val appOverlayInputEnabled: Boolean, - val systemOverlayInputEnabled: Boolean, + private data class DomainSurfaceFrameState( + val appPortalRenderEnabled: Boolean, + val systemPortalRenderEnabled: Boolean, + val appPortalInputEnabled: Boolean, + val systemPortalInputEnabled: Boolean, val inspectorBlocks: Boolean, ) @@ -307,7 +307,7 @@ abstract class DsglScreenHost( ((nowNanos - lastFrameNanos).toDouble() / 1_000_000_000.0).coerceIn(0.0, 0.25) } lastFrameNanos = nowNanos - OverlayLayerDebugState.updateFrameTiming(dtSeconds) + DomainSurfaceDebugState.updateFrameTiming(dtSeconds) window.tick(dtSeconds.toFloat(), partialTicks) val animationVisualsChanged = StyleAnimationEngine.tickAndApply(tree.root, dtSeconds, partialTicks) if (animationVisualsChanged) { @@ -346,31 +346,31 @@ abstract class DsglScreenHost( ) } - private fun syncInspectorAndResolveOverlayState( + private fun syncInspectorAndResolveSurfaceState( tree: DomTree, dsglMouseX: Int, dsglMouseY: Int, - ): OverlayLayerFrameState { + ): DomainSurfaceFrameState { inspector.onLayoutCommitted(tree.root, layoutRevision) inspector.onCursorMoved(dsglMouseX, dsglMouseY) inspectorPointerCaptured = inspector.isPointerCaptured if (inspectorPointerCaptured) { inspector.onCapturedPointerMove(dsglMouseX, dsglMouseY, lastWidth, lastHeight) } - val appOverlayRenderEnabled = OverlayLayerDebugState.isRenderEnabled(ScreenDomainSurfaces.ApplicationPortal) - val systemOverlayRenderEnabled = OverlayLayerDebugState.isRenderEnabled(ScreenDomainSurfaces.SystemPortal) - val appOverlayInputEnabled = OverlayLayerDebugState.isInputEnabled(ScreenDomainSurfaces.ApplicationPortal) - val systemOverlayInputEnabled = OverlayLayerDebugState.isInputEnabled(ScreenDomainSurfaces.SystemPortal) + val appPortalRenderEnabled = DomainSurfaceDebugState.isRenderEnabled(ScreenDomainSurfaces.ApplicationPortal) + val systemPortalRenderEnabled = DomainSurfaceDebugState.isRenderEnabled(ScreenDomainSurfaces.SystemPortal) + val appPortalInputEnabled = DomainSurfaceDebugState.isInputEnabled(ScreenDomainSurfaces.ApplicationPortal) + val systemPortalInputEnabled = DomainSurfaceDebugState.isInputEnabled(ScreenDomainSurfaces.SystemPortal) val inspectorBlocks = - systemOverlayInputEnabled && + systemPortalInputEnabled && ( inspectorPointerCaptured || inspector.shouldConsumePointer(dsglMouseX, dsglMouseY) ) - return OverlayLayerFrameState( - appOverlayRenderEnabled = appOverlayRenderEnabled, - systemOverlayRenderEnabled = systemOverlayRenderEnabled, - appOverlayInputEnabled = appOverlayInputEnabled, - systemOverlayInputEnabled = systemOverlayInputEnabled, + return DomainSurfaceFrameState( + appPortalRenderEnabled = appPortalRenderEnabled, + systemPortalRenderEnabled = systemPortalRenderEnabled, + appPortalInputEnabled = appPortalInputEnabled, + systemPortalInputEnabled = systemPortalInputEnabled, inspectorBlocks = inspectorBlocks, ) } @@ -409,7 +409,7 @@ abstract class DsglScreenHost( } private fun syncFeatureRuntimeFrame(tree: DomTree, dsglMouseX: Int, dsglMouseY: Int) { - applicationOverlayHost.syncPortalFrame( + applicationPortalHost.syncPortalFrame( measureContext = adapter, viewportWidth = lastWidth, viewportHeight = lastHeight, @@ -417,99 +417,101 @@ abstract class DsglScreenHost( mouseX = dsglMouseX, mouseY = dsglMouseY, ) - systemOverlayHost.syncPortalFrame(adapter, lastWidth, lastHeight, 1f) + systemPortalHost.syncPortalFrame(adapter, lastWidth, lastHeight, 1f) refreshActiveColorSamplerOwner(tree.root) } - private fun syncApplicationOverlaySurface(appOverlayEnabled: Boolean) { - if (!appOverlayEnabled) return + private fun syncApplicationPortalSurface(appPortalEnabled: Boolean) { + if (!appPortalEnabled) return try { - applicationOverlayHost.render(adapter, lastWidth, lastHeight) + applicationPortalHost.render(adapter, lastWidth, lastHeight) } catch ( @Suppress("TooGenericExceptionCaught") error: Throwable, ) { logPipelineError( - key = "draw.applicationOverlay.sync", - message = "[DSGL] Application overlay sync failed; skipping app overlay sync frame: ${error.message}", + key = "draw.applicationPortal.sync", + message = + "[DSGL] Application portal sync failed; " + + "skipping application portal sync frame: ${error.message}", ) } } - private fun syncCollectAndStageApplicationOverlayAfterRootPaint( + private fun syncCollectAndStageApplicationPortalAfterRootPaint( tree: DomTree, - appOverlayRenderEnabled: Boolean, - appOverlayInputEnabled: Boolean, + appPortalRenderEnabled: Boolean, + appPortalInputEnabled: Boolean, ) { - syncApplicationOverlaySurface( - appOverlayEnabled = appOverlayRenderEnabled || appOverlayInputEnabled, + syncApplicationPortalSurface( + appPortalEnabled = appPortalRenderEnabled || appPortalInputEnabled, ) - val applicationOverlayCommands = collectApplicationOverlayCommands(appOverlayRenderEnabled) - stageApplicationOverlayCommands( + val applicationPortalCommands = collectApplicationPortalCommands(appPortalRenderEnabled) + stageApplicationPortalCommands( tree = tree, - applicationOverlayCommands = applicationOverlayCommands, - appOverlayRenderEnabled = appOverlayRenderEnabled, + applicationPortalCommands = applicationPortalCommands, + appPortalRenderEnabled = appPortalRenderEnabled, ) } - private fun collectApplicationOverlayCommands(appOverlayRenderEnabled: Boolean): List { - if (!appOverlayRenderEnabled) { + private fun collectApplicationPortalCommands(appPortalRenderEnabled: Boolean): List { + if (!appPortalRenderEnabled) { return emptyList() } return try { - applicationOverlayHost.paint(adapter) + applicationPortalHost.paint(adapter) } catch ( @Suppress("TooGenericExceptionCaught") error: Throwable, ) { logPipelineError( - key = "draw.applicationOverlay", - message = "[DSGL] Application overlay paint failed; skipping app overlay frame: ${error.message}", + key = "draw.applicationPortal", + message = "[DSGL] Application portal paint failed; skipping application portal frame: ${error.message}", ) emptyList() } } - private fun syncSystemOverlayAndCollectCommands( + private fun syncSystemPortalAndCollectCommands( tree: DomTree, dsglMouseX: Int, dsglMouseY: Int, - systemOverlayRenderEnabled: Boolean, + systemPortalRenderEnabled: Boolean, ): List { - systemOverlayHost.syncFrame( + systemPortalHost.syncFrame( inspectedRoot = tree.root, inspectedLayoutRevision = layoutRevision, cursorX = dsglMouseX, cursorY = dsglMouseY, inspectorPointerCaptured = inspectorPointerCaptured, ) - if (!systemOverlayRenderEnabled) { + if (!systemPortalRenderEnabled) { return emptyList() } return try { - systemOverlayHost.render(adapter, lastWidth, lastHeight) - systemOverlayHost.paint(adapter) + systemPortalHost.render(adapter, lastWidth, lastHeight) + systemPortalHost.paint(adapter) } catch ( @Suppress("TooGenericExceptionCaught") error: Throwable, ) { logPipelineError( - key = "draw.systemOverlay", - message = "[DSGL] System overlay paint failed; skipping system overlay frame: ${error.message}", + key = "draw.systemPortal", + message = "[DSGL] System portal paint failed; skipping system portal frame: ${error.message}", ) emptyList() } } - private fun stageSystemOverlayCommands( - systemOverlayCommands: List, - systemOverlayRenderEnabled: Boolean, + private fun stageSystemPortalCommands( + systemPortalCommands: List, + systemPortalRenderEnabled: Boolean, ) { - systemOverlayCommandsBuffer.clear() - systemOverlayCommandsBuffer.addAll(systemOverlayCommands) - if (systemOverlayRenderEnabled) { - systemOverlayHost.appendPortalOverlayCommands( + systemPortalCommandsBuffer.clear() + systemPortalCommandsBuffer.addAll(systemPortalCommands) + if (systemPortalRenderEnabled) { + systemPortalHost.appendFloatingPortalCommands( measureContext = adapter, viewportWidth = lastWidth, viewportHeight = lastHeight, - out = systemOverlayCommandsBuffer, + out = systemPortalCommandsBuffer, ) } } @@ -537,35 +539,35 @@ abstract class DsglScreenHost( dtSeconds: Double, dsglMouseX: Int, dsglMouseY: Int, - appOverlayInputEnabled: Boolean, - systemOverlayInputEnabled: Boolean, + appPortalInputEnabled: Boolean, + systemPortalInputEnabled: Boolean, inspectorBlocks: Boolean, ) { val applicationRootFrameBlocked = isApplicationRootFrameBlocked( dsglMouseX = dsglMouseX, dsglMouseY = dsglMouseY, - appOverlayInputEnabled = appOverlayInputEnabled, - systemOverlayInputEnabled = systemOverlayInputEnabled, + appPortalInputEnabled = appPortalInputEnabled, + systemPortalInputEnabled = systemPortalInputEnabled, inspectorBlocks = inspectorBlocks, ) val prevX = if (lastMoveX == Int.MIN_VALUE) dsglMouseX else lastMoveX val prevY = if (lastMoveY == Int.MIN_VALUE) dsglMouseY else lastMoveY val dx = dsglMouseX - prevX val dy = dsglMouseY - prevY - val applicationModalBlocks = applicationOverlayHost.hasActiveModalPortal() + val applicationModalBlocks = applicationPortalHost.hasActiveModalPortal() val applicationPortalBlocks = isApplicationPortalFrameBlocking( dsglMouseX = dsglMouseX, dsglMouseY = dsglMouseY, - appOverlayInputEnabled = appOverlayInputEnabled, + appPortalInputEnabled = appPortalInputEnabled, ) if (applicationRootFrameBlocked) { if (applicationModalBlocks) { DndRuntime.engine.cancelActiveDrag() } if (applicationModalBlocks || applicationPortalBlocks) { - applicationOverlayHost.handleMouseMove(dsglMouseX, dsglMouseY) + applicationPortalHost.handleMouseMove(dsglMouseX, dsglMouseY) } clearHoverChainStates(postLeaveEvents = true, mouseX = dsglMouseX, mouseY = dsglMouseY) hoverTarget = null @@ -582,41 +584,41 @@ abstract class DsglScreenHost( private fun isApplicationRootFrameBlocked( dsglMouseX: Int, dsglMouseY: Int, - appOverlayInputEnabled: Boolean, - systemOverlayInputEnabled: Boolean, + appPortalInputEnabled: Boolean, + systemPortalInputEnabled: Boolean, inspectorBlocks: Boolean, ): Boolean { - if (appOverlayInputEnabled && applicationOverlayHost.hasActiveModalPortal()) return true + if (appPortalInputEnabled && applicationPortalHost.hasActiveModalPortal()) return true if (isApplicationRootPointerDragActive() && pointerCapture.target != null) return false if (inspectorBlocks || higherSurfacePointerButton != -1) return true - if (isApplicationPortalFrameBlocking(dsglMouseX, dsglMouseY, appOverlayInputEnabled)) return true - if (systemOverlayInputEnabled && systemOverlayHost.hasOpenPortal()) return true + if (isApplicationPortalFrameBlocking(dsglMouseX, dsglMouseY, appPortalInputEnabled)) return true + if (systemPortalInputEnabled && systemPortalHost.hasOpenPortal()) return true return isColorPickerFrameBlocking( - appOverlayInputEnabled = appOverlayInputEnabled, - systemOverlayInputEnabled = systemOverlayInputEnabled, + appPortalInputEnabled = appPortalInputEnabled, + systemPortalInputEnabled = systemPortalInputEnabled, ) } private fun isApplicationPortalFrameBlocking( dsglMouseX: Int, dsglMouseY: Int, - appOverlayInputEnabled: Boolean, + appPortalInputEnabled: Boolean, ): Boolean { - if (!appOverlayInputEnabled) return false - return applicationOverlayHost.hasOpenContextMenuPortal() || - applicationOverlayHost.hasOpenSelectPortal() || - applicationOverlayHost.hasDomPointerTargetAt(dsglMouseX, dsglMouseY) + if (!appPortalInputEnabled) return false + return applicationPortalHost.hasOpenContextMenuPortal() || + applicationPortalHost.hasOpenSelectPortal() || + applicationPortalHost.hasDomPointerTargetAt(dsglMouseX, dsglMouseY) } private fun isColorPickerFrameBlocking( - appOverlayInputEnabled: Boolean, - systemOverlayInputEnabled: Boolean, + appPortalInputEnabled: Boolean, + systemPortalInputEnabled: Boolean, ): Boolean { val inlineSamplerOwnsSession = activeColorSamplerOwner is ActiveColorSamplerOwner.Inline - val systemPickerBlocks = systemOverlayInputEnabled && systemOverlayHost.isSystemColorPickerOpen() + val systemPickerBlocks = systemPortalInputEnabled && systemPortalHost.isSystemColorPickerOpen() val applicationPickerBlocks = - appOverlayInputEnabled && - applicationOverlayHost.hasOpenColorPickerPortal() && + appPortalInputEnabled && + applicationPortalHost.hasOpenColorPickerPortal() && !inlineSamplerOwnsSession return systemPickerBlocks || applicationPickerBlocks } @@ -657,31 +659,31 @@ abstract class DsglScreenHost( } } - private fun stageApplicationOverlayCommands( + private fun stageApplicationPortalCommands( tree: DomTree, - applicationOverlayCommands: List, - appOverlayRenderEnabled: Boolean, + applicationPortalCommands: List, + appPortalRenderEnabled: Boolean, measureContext: UiMeasureContext = adapter, ) { - applicationOverlayCommandsBuffer.clear() - if (appOverlayRenderEnabled) { - applicationOverlayCommandsBuffer.addAll(applicationOverlayCommands) - if (!applicationOverlayHost.hasActiveModalPortal()) { - applicationOverlayHost.appendDndGhostPortalCommands( + applicationPortalCommandsBuffer.clear() + if (appPortalRenderEnabled) { + applicationPortalCommandsBuffer.addAll(applicationPortalCommands) + if (!applicationPortalHost.hasActiveModalPortal()) { + applicationPortalHost.appendDndGhostPortalCommands( root = tree.root, measureContext = measureContext, viewportWidth = lastWidth, viewportHeight = lastHeight, - out = applicationOverlayCommandsBuffer, + out = applicationPortalCommandsBuffer, ) } - applicationOverlayHost.appendPortalOverlayCommands( + applicationPortalHost.appendFloatingPortalCommands( measureContext = measureContext, viewportWidth = lastWidth, viewportHeight = lastHeight, - out = applicationOverlayCommandsBuffer, + out = applicationPortalCommandsBuffer, ) - appendInlineColorPickerOverlayCommands(applicationOverlayCommandsBuffer) + appendInlineColorPickerPortalCommands(applicationPortalCommandsBuffer) } } @@ -694,12 +696,12 @@ abstract class DsglScreenHost( ) { domainOrchestrator.composePaintCommands( applicationRoot = commands, - applicationPortal = applicationOverlayCommandsBuffer, - systemPortal = systemOverlayCommandsBuffer, + applicationPortal = applicationPortalCommandsBuffer, + systemPortal = systemPortalCommandsBuffer, debugRoot = debugDomainCommands.root, debugPortal = debugDomainCommands.portal, out = stagingCommandsBuffer, - shouldRenderSurface = OverlayLayerDebugState::isRenderEnabled, + shouldRenderSurface = DomainSurfaceDebugState::isRenderEnabled, ) val keepPrevious = shouldKeepPreviousFrameCommands( @@ -741,8 +743,8 @@ abstract class DsglScreenHost( ScreenColorSamplerBridge.install(null) FocusManager.clearFocus() DndRuntime.engine.cancelActiveDrag() - applicationOverlayHost.closeFloatingPortals() - systemOverlayHost.clearRefs() + applicationPortalHost.closeFloatingPortals() + systemPortalHost.clearRefs() clearActiveTarget() flushPendingCleanup() clearHoverChainStates() @@ -755,8 +757,8 @@ abstract class DsglScreenHost( StyleEngine.clearAllInspectorOverrides() StyleAnimationEngine.clear() domTree?.clearRefs() - applicationOverlayHost.clearRefs() - systemOverlayHost.clearRefs() + applicationPortalHost.clearRefs() + systemPortalHost.clearRefs() debugDomainRootHost.clearRefs() debugDomainPortalHost.clearRefs() domTree?.root?.let { root -> @@ -789,7 +791,7 @@ abstract class DsglScreenHost( val height = viewport.height lastViewport = viewport if (force || width != lastWidth || height != lastHeight) { - applicationOverlayHost.closeFloatingPortals() + applicationPortalHost.closeFloatingPortals() lastWidth = width lastHeight = height needsLayout = true @@ -887,9 +889,9 @@ abstract class DsglScreenHost( control = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL), meta = Keyboard.isKeyDown(Keyboard.KEY_LMETA) || Keyboard.isKeyDown(Keyboard.KEY_RMETA), ) - runOverlayInputFrame(applicationOverlayHost) - runOverlayInputFrame(systemOverlayHost) - applicationOverlayHost.syncPortalFrame( + runSurfaceInputFrame(applicationPortalHost) + runSurfaceInputFrame(systemPortalHost) + applicationPortalHost.syncPortalFrame( measureContext = adapter, viewportWidth = lastWidth, viewportHeight = lastHeight, @@ -944,7 +946,7 @@ abstract class DsglScreenHost( if (keyCode == Keyboard.KEY_F10) { val demoAnchorX = if (lastMoveX == Int.MIN_VALUE) inspectorMouseX else lastMoveX val demoAnchorY = if (lastMoveY == Int.MIN_VALUE) inspectorMouseY else lastMoveY - applicationOverlayHost.toggleFloatingWindowDemo(demoAnchorX, demoAnchorY) + applicationPortalHost.toggleFloatingWindowDemo(demoAnchorX, demoAnchorY) mc.dispatchKeypresses() return true } @@ -1066,7 +1068,7 @@ abstract class DsglScreenHost( private fun syncMouseInputFrame(tree: DomTree, inputEvent: MouseInputEvent) { inspector.onCursorMoved(inputEvent.mouseX, inputEvent.mouseY) - applicationOverlayHost.syncPortalFrame( + applicationPortalHost.syncPortalFrame( measureContext = adapter, viewportWidth = lastWidth, viewportHeight = lastHeight, @@ -1074,18 +1076,18 @@ abstract class DsglScreenHost( mouseX = inputEvent.mouseX, mouseY = inputEvent.mouseY, ) - systemOverlayHost.syncPortalFrame( + systemPortalHost.syncPortalFrame( measureContext = adapter, viewportWidth = lastWidth, viewportHeight = lastHeight, viewportScale = 1f, ) - runOverlayInputFrame(applicationOverlayHost) - runOverlayInputFrame(systemOverlayHost) - runOverlayInputFrame(debugDomainRootHost) - runOverlayInputFrame(debugDomainPortalHost) + runSurfaceInputFrame(applicationPortalHost) + runSurfaceInputFrame(systemPortalHost) + runSurfaceInputFrame(debugDomainRootHost) + runSurfaceInputFrame(debugDomainPortalHost) inspectorPointerCaptured = inspector.isPointerCaptured - systemOverlayHost.syncFrame( + systemPortalHost.syncFrame( inspectedRoot = tree.root, inspectedLayoutRevision = layoutRevision, cursorX = inputEvent.mouseX, @@ -1095,7 +1097,7 @@ abstract class DsglScreenHost( refreshActiveColorSamplerOwner(tree.root) } - private fun runOverlayInputFrame(host: DomainSurfaceHost) { + private fun runSurfaceInputFrame(host: DomainSurfaceHost) { host.onInputFrame(lastWidth, lastHeight) } @@ -1112,13 +1114,13 @@ abstract class DsglScreenHost( canConsume = { surface -> consumeDomainPointerSurface(surface = surface, tree = tree, context = context) }, - isSurfaceInputEnabled = OverlayLayerDebugState::isInputEnabled, + isSurfaceInputEnabled = DomainSurfaceDebugState::isInputEnabled, ) return when (consumedBy) { null -> if (isHigherSurfaceOwnedPointerRelease(context)) { higherSurfacePointerButton = -1 - consumeOverlayPointerState( + consumePortalPointerState( mouseX = inputEvent.mouseX, mouseY = inputEvent.mouseY, cancelRootDnd = context.inputEvent.mouseButton != -1, @@ -1130,7 +1132,7 @@ abstract class DsglScreenHost( ScreenDomainSurfaces.ApplicationRoot -> DomainPointerDispatchResult.ApplicationRootHandled else -> { updateHigherSurfacePointerOwnership(context) - consumeOverlayPointerState( + consumePortalPointerState( mouseX = inputEvent.mouseX, mouseY = inputEvent.mouseY, cancelRootDnd = context.inputEvent.mouseButton != -1, @@ -1212,7 +1214,7 @@ abstract class DsglScreenHost( if (context.applicationRootPressMove) { return false } - return consumeSystemOverlayPointerEvent( + return consumeSystemPortalPointerEvent( mouseX = context.inputEvent.mouseX, mouseY = context.inputEvent.mouseY, dWheel = context.inputEvent.dWheel, @@ -1226,7 +1228,7 @@ abstract class DsglScreenHost( if (context.applicationRootPressMove) { return false } - return consumeApplicationOverlayPointerEvent( + return consumeApplicationPortalPointerEvent( mouseX = context.inputEvent.mouseX, mouseY = context.inputEvent.mouseY, dWheel = context.inputEvent.dWheel, @@ -1400,14 +1402,14 @@ abstract class DsglScreenHost( ScreenDomainSurfaces.DebugRoot -> debugDomainRootHost.handleKeyDown(keyCode, keyChar) ScreenDomainSurfaces.SystemPortal -> - consumeSystemOverlayKeyDown( + consumeSystemPortalKeyDown( keyCode = keyCode, keyChar = keyChar, inspectorMouseX = inspectorMouseX, inspectorMouseY = inspectorMouseY, ) - ScreenDomainSurfaces.ApplicationPortal -> consumeApplicationOverlayKeyDown(keyCode, keyChar) + ScreenDomainSurfaces.ApplicationPortal -> consumeApplicationPortalKeyDown(keyCode, keyChar) ScreenDomainSurfaces.ApplicationRoot -> { dispatchApplicationRootKeyDown(keyCode, keyChar) true @@ -1416,7 +1418,7 @@ abstract class DsglScreenHost( else -> false } }, - isSurfaceInputEnabled = OverlayLayerDebugState::isInputEnabled, + isSurfaceInputEnabled = DomainSurfaceDebugState::isInputEnabled, ) return when (consumedBy) { null -> DomainKeyDispatchResult.None @@ -1439,20 +1441,20 @@ abstract class DsglScreenHost( ScreenDomainSurfaces.DebugRoot -> debugDomainRootHost.handleKeyUp(keyCode, keyChar) ScreenDomainSurfaces.SystemPortal -> - consumeSystemOverlayKeyUp( + consumeSystemPortalKeyUp( keyCode = keyCode, keyChar = keyChar, inspectorMouseX = inspectorMouseX, inspectorMouseY = inspectorMouseY, ) - ScreenDomainSurfaces.ApplicationPortal -> consumeApplicationOverlayKeyUp(keyCode, keyChar) + ScreenDomainSurfaces.ApplicationPortal -> consumeApplicationPortalKeyUp(keyCode, keyChar) ScreenDomainSurfaces.ApplicationRoot -> dispatchApplicationRootKeyUp(keyCode, keyChar) ScreenDomainSurfaces.SystemRoot -> false else -> false } }, - isSurfaceInputEnabled = OverlayLayerDebugState::isInputEnabled, + isSurfaceInputEnabled = DomainSurfaceDebugState::isInputEnabled, ) return when (consumedBy) { null -> DomainKeyDispatchResult.None @@ -1510,16 +1512,16 @@ abstract class DsglScreenHost( EventBus.post(upEvent) } - private fun consumeSystemOverlayKeyDown( + private fun consumeSystemPortalKeyDown( keyCode: Int, keyChar: Char, inspectorMouseX: Int, inspectorMouseY: Int, ): Boolean { - if (systemOverlayHost.handlePortalKeyDown(keyCode, keyChar)) { + if (systemPortalHost.handlePortalKeyDown(keyCode, keyChar)) { return true } - if (systemOverlayHost.handleKeyDown(keyCode, keyChar)) { + if (systemPortalHost.handleKeyDown(keyCode, keyChar)) { return true } val keyboardBlocked = @@ -1535,16 +1537,16 @@ abstract class DsglScreenHost( return false } - private fun consumeSystemOverlayKeyUp( + private fun consumeSystemPortalKeyUp( keyCode: Int, keyChar: Char, inspectorMouseX: Int, inspectorMouseY: Int, ): Boolean { - if (systemOverlayHost.handlePortalKeyUp(keyCode, keyChar)) { + if (systemPortalHost.handlePortalKeyUp(keyCode, keyChar)) { return true } - if (systemOverlayHost.handleKeyUp(keyCode, keyChar)) { + if (systemPortalHost.handleKeyUp(keyCode, keyChar)) { return true } val keyboardBlocked = @@ -1560,27 +1562,27 @@ abstract class DsglScreenHost( return false } - private fun consumeApplicationOverlayKeyDown(keyCode: Int, keyChar: Char): Boolean { - if (applicationOverlayHost.handlePortalKeyDownBeforeDom(keyCode, keyChar)) { + private fun consumeApplicationPortalKeyDown(keyCode: Int, keyChar: Char): Boolean { + if (applicationPortalHost.handlePortalKeyDownBeforeDom(keyCode, keyChar)) { return true } - if (applicationOverlayHost.handleKeyDown(keyCode, keyChar)) { + if (applicationPortalHost.handleKeyDown(keyCode, keyChar)) { return true } - if (applicationOverlayHost.handlePortalKeyDownAfterDom(keyCode, keyChar)) { + if (applicationPortalHost.handlePortalKeyDownAfterDom(keyCode, keyChar)) { return true } return false } - private fun consumeApplicationOverlayKeyUp(keyCode: Int, keyChar: Char): Boolean { - if (applicationOverlayHost.handlePortalKeyUpBeforeDom(keyCode, keyChar)) { + private fun consumeApplicationPortalKeyUp(keyCode: Int, keyChar: Char): Boolean { + if (applicationPortalHost.handlePortalKeyUpBeforeDom(keyCode, keyChar)) { return true } - if (applicationOverlayHost.handleKeyUp(keyCode, keyChar)) { + if (applicationPortalHost.handleKeyUp(keyCode, keyChar)) { return true } - if (applicationOverlayHost.handlePortalKeyUpAfterDom(keyCode, keyChar)) { + if (applicationPortalHost.handlePortalKeyUpAfterDom(keyCode, keyChar)) { return true } return false @@ -1611,7 +1613,7 @@ abstract class DsglScreenHost( return false } - private fun consumeSystemOverlayPointerEvent( + private fun consumeSystemPortalPointerEvent( mouseX: Int, mouseY: Int, dWheel: Int, @@ -1619,34 +1621,34 @@ abstract class DsglScreenHost( mappedButton: MouseButton?, buttonPressed: Boolean, ): Boolean { - if (dWheel != 0 && systemOverlayHost.handlePortalMouseWheel(mouseX, mouseY, dWheel)) { + if (dWheel != 0 && systemPortalHost.handlePortalMouseWheel(mouseX, mouseY, dWheel)) { return true } - if (dWheel != 0 && systemOverlayHost.handleMouseWheel(mouseX, mouseY, dWheel)) { + if (dWheel != 0 && systemPortalHost.handleMouseWheel(mouseX, mouseY, dWheel)) { return true } if (mouseButton != -1 && mappedButton != null) { val consumedBySystemSelect = if (buttonPressed) { - systemOverlayHost.handlePortalMouseDown(mouseX, mouseY, mappedButton) + systemPortalHost.handlePortalMouseDown(mouseX, mouseY, mappedButton) } else { - systemOverlayHost.handlePortalMouseUp(mouseX, mouseY, mappedButton) + systemPortalHost.handlePortalMouseUp(mouseX, mouseY, mappedButton) } if (consumedBySystemSelect) { return true } - val consumedBySystemOverlay = + val consumedBySystemPortal = if (buttonPressed) { - systemOverlayHost.handleMouseDown(mouseX, mouseY, mappedButton) + systemPortalHost.handleMouseDown(mouseX, mouseY, mappedButton) } else { - systemOverlayHost.handleMouseUp(mouseX, mouseY, mappedButton) + systemPortalHost.handleMouseUp(mouseX, mouseY, mappedButton) } - if (consumedBySystemOverlay) { + if (consumedBySystemPortal) { return true } - } else if (mouseButton == -1 && systemOverlayHost.handlePortalMouseMove(mouseX, mouseY)) { + } else if (mouseButton == -1 && systemPortalHost.handlePortalMouseMove(mouseX, mouseY)) { return true - } else if (mouseButton == -1 && systemOverlayHost.handleMouseMove(mouseX, mouseY)) { + } else if (mouseButton == -1 && systemPortalHost.handleMouseMove(mouseX, mouseY)) { return true } @@ -1659,7 +1661,7 @@ abstract class DsglScreenHost( return true } - private fun consumeApplicationOverlayPointerEvent( + private fun consumeApplicationPortalPointerEvent( mouseX: Int, mouseY: Int, dWheel: Int, @@ -1670,7 +1672,7 @@ abstract class DsglScreenHost( val inlineSamplerOwnsSession = activeColorSamplerOwner is ActiveColorSamplerOwner.Inline if (!inlineSamplerOwnsSession) { if ( - applicationOverlayHost.handlePortalPointerBeforeDom( + applicationPortalHost.handlePortalPointerBeforeDom( mouseX = mouseX, mouseY = mouseY, dWheel = dWheel, @@ -1682,24 +1684,24 @@ abstract class DsglScreenHost( } } - if (dWheel != 0 && applicationOverlayHost.handleMouseWheel(mouseX, mouseY, dWheel)) { + if (dWheel != 0 && applicationPortalHost.handleMouseWheel(mouseX, mouseY, dWheel)) { return true } if (mouseButton != -1 && mappedButton != null) { - val consumedByAppOverlay = + val consumedByApplicationPortal = if (buttonPressed) { - applicationOverlayHost.handleMouseDown(mouseX, mouseY, mappedButton) + applicationPortalHost.handleMouseDown(mouseX, mouseY, mappedButton) } else { - applicationOverlayHost.handleMouseUp(mouseX, mouseY, mappedButton) + applicationPortalHost.handleMouseUp(mouseX, mouseY, mappedButton) } - if (consumedByAppOverlay) { + if (consumedByApplicationPortal) { return true } - } else if (mouseButton == -1 && applicationOverlayHost.handleMouseMove(mouseX, mouseY)) { + } else if (mouseButton == -1 && applicationPortalHost.handleMouseMove(mouseX, mouseY)) { return true } - return applicationOverlayHost.handlePortalPointerAfterDom( + return applicationPortalHost.handlePortalPointerAfterDom( mouseX = mouseX, mouseY = mouseY, dWheel = dWheel, @@ -1708,7 +1710,7 @@ abstract class DsglScreenHost( ) } - private fun consumeOverlayPointerState(mouseX: Int, mouseY: Int, cancelRootDnd: Boolean = false) { + private fun consumePortalPointerState(mouseX: Int, mouseY: Int, cancelRootDnd: Boolean = false) { if (cancelRootDnd) { DndRuntime.engine.cancelActiveDrag() } @@ -1732,7 +1734,7 @@ abstract class DsglScreenHost( private fun isApplicationRootPointerDragActive(): Boolean = eventButton != -1 && lastMouseEvent > 0L init { - inspector.installColorPickerPortalService(systemOverlayHost.systemInspectorColorPickerService()) + inspector.installColorPickerPortalService(systemPortalHost.systemInspectorColorPickerService()) } private fun refreshActiveColorSamplerOwner(root: DOMNode?) { @@ -1746,7 +1748,7 @@ abstract class DsglScreenHost( } activeColorSamplerOwner = colorSamplerOwnershipRouter.update( - popupEyedropperActive = applicationOverlayHost.hasActiveColorPickerEyedropper(), + popupEyedropperActive = applicationPortalHost.hasActiveColorPickerEyedropper(), inlineActiveTokens = inlineByToken.keys.toSet(), ) activeInlineColorSamplerNode = @@ -1777,13 +1779,13 @@ abstract class DsglScreenHost( return null } - private fun appendInlineColorPickerOverlayCommands(out: MutableList) { - val surface = ScreenDomainSurfaces.portalSurfaceForOwner(OverlayOwnerScope.Application) + private fun appendInlineColorPickerPortalCommands(out: MutableList) { + val surface = ScreenDomainSurfaces.portalSurfaceForDomain(ScreenDomainId.Application) if (surface != ScreenDomainSurfaces.ApplicationPortal) return if (activeColorSamplerOwner is ActiveColorSamplerOwner.Inline) { val inline = activeInlineColorSamplerNode ?: return if (!inline.wantsGlobalPointerInput()) return - inline.appendEyedropperOverlayCommands( + inline.appendEyedropperPortalCommands( viewportWidth = lastWidth.coerceAtLeast(1), viewportHeight = lastHeight.coerceAtLeast(1), out = out, @@ -1793,16 +1795,16 @@ abstract class DsglScreenHost( private fun captureColorPickerEyedropperSamples() { refreshActiveColorSamplerOwner(domTree?.root) - if (ScreenDomainSurfaces.portalSurfaceForOwner(OverlayOwnerScope.System) == ScreenDomainSurfaces.SystemPortal) { - systemOverlayHost.captureSystemColorPickerEyedropperSample() + if (ScreenDomainSurfaces.portalSurfaceForDomain(ScreenDomainId.System) == ScreenDomainSurfaces.SystemPortal) { + systemPortalHost.captureSystemColorPickerEyedropperSample() } - if (ScreenDomainSurfaces.portalSurfaceForOwner(OverlayOwnerScope.Application) != + if (ScreenDomainSurfaces.portalSurfaceForDomain(ScreenDomainId.Application) != ScreenDomainSurfaces.ApplicationPortal ) { return } when (activeColorSamplerOwner) { - ActiveColorSamplerOwner.Popup -> applicationOverlayHost.captureColorPickerEyedropperSample() + ActiveColorSamplerOwner.Popup -> applicationPortalHost.captureColorPickerEyedropperSample() is ActiveColorSamplerOwner.Inline -> { val inline = activeInlineColorSamplerNode if (inline != null && inline.wantsGlobalPointerInput()) { @@ -1811,8 +1813,8 @@ abstract class DsglScreenHost( } ActiveColorSamplerOwner.None -> { - if (applicationOverlayHost.hasActiveColorPickerEyedropper()) { - applicationOverlayHost.captureColorPickerEyedropperSample() + if (applicationPortalHost.hasActiveColorPickerEyedropper()) { + applicationPortalHost.captureColorPickerEyedropperSample() } } } @@ -1873,38 +1875,38 @@ abstract class DsglScreenHost( return out } - internal fun debugStageApplicationOverlayCommandsForTests( + internal fun debugStageApplicationPortalCommandsForTests( tree: DomTree, - applicationOverlayCommands: List, - appOverlayRenderEnabled: Boolean = true, + applicationPortalCommands: List, + appPortalRenderEnabled: Boolean = true, measureContext: UiMeasureContext, ): List { - stageApplicationOverlayCommands( + stageApplicationPortalCommands( tree = tree, - applicationOverlayCommands = applicationOverlayCommands, - appOverlayRenderEnabled = appOverlayRenderEnabled, + applicationPortalCommands = applicationPortalCommands, + appPortalRenderEnabled = appPortalRenderEnabled, measureContext = measureContext, ) - return applicationOverlayCommandsBuffer.toList() + return applicationPortalCommandsBuffer.toList() } - internal fun debugSyncApplicationOverlaySurfaceForTests( + internal fun debugSyncApplicationPortalSurfaceForTests( measureContext: UiMeasureContext, width: Int, height: Int, - appOverlayEnabled: Boolean = true, + appPortalEnabled: Boolean = true, ) { - if (appOverlayEnabled) { - applicationOverlayHost.render(measureContext, width, height) + if (appPortalEnabled) { + applicationPortalHost.render(measureContext, width, height) } } - internal fun debugCollectApplicationOverlayCommandsForTests( + internal fun debugCollectApplicationPortalCommandsForTests( measureContext: UiMeasureContext, - appOverlayRenderEnabled: Boolean = true, + appPortalRenderEnabled: Boolean = true, ): List = - if (appOverlayRenderEnabled) { - applicationOverlayHost.paint(measureContext) + if (appPortalRenderEnabled) { + applicationPortalHost.paint(measureContext) } else { emptyList() } @@ -1957,14 +1959,14 @@ abstract class DsglScreenHost( ) if (consumedBy != null && consumedBy != ScreenDomainSurfaces.ApplicationRoot) { updateHigherSurfacePointerOwnership(context) - consumeOverlayPointerState( + consumePortalPointerState( mouseX = mouseX, mouseY = mouseY, cancelRootDnd = context.inputEvent.mouseButton != -1, ) } else if (consumedBy == null && isHigherSurfaceOwnedPointerRelease(context)) { higherSurfacePointerButton = -1 - consumeOverlayPointerState( + consumePortalPointerState( mouseX = mouseX, mouseY = mouseY, cancelRootDnd = context.inputEvent.mouseButton != -1, @@ -1974,14 +1976,14 @@ abstract class DsglScreenHost( return consumedBy } - internal fun debugApplicationOverlayHostForTests(): ApplicationOverlayHost = applicationOverlayHost + internal fun debugApplicationPortalHostForTests(): ApplicationPortalHost = applicationPortalHost internal fun debugUpdateFrameInteractionStateForTests( tree: DomTree, mouseX: Int, mouseY: Int, - appOverlayInputEnabled: Boolean = true, - systemOverlayInputEnabled: Boolean = true, + appPortalInputEnabled: Boolean = true, + systemPortalInputEnabled: Boolean = true, inspectorBlocks: Boolean = false, ) { updateFrameInteractionState( @@ -1989,8 +1991,8 @@ abstract class DsglScreenHost( dtSeconds = 1.0 / 60.0, dsglMouseX = mouseX, dsglMouseY = mouseY, - appOverlayInputEnabled = appOverlayInputEnabled, - systemOverlayInputEnabled = systemOverlayInputEnabled, + appPortalInputEnabled = appPortalInputEnabled, + systemPortalInputEnabled = systemPortalInputEnabled, inspectorBlocks = inspectorBlocks, ) } diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/Mc1710UiAdapter.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/Mc1710UiAdapter.kt index 1c16434..a40f3d7 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/Mc1710UiAdapter.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/Mc1710UiAdapter.kt @@ -445,11 +445,11 @@ class Mc1710UiAdapter( GL11.glTexCoord2f(0f, 0f) GL11.glVertex2f(command.x.toFloat(), (command.y + command.height).toFloat()) GL11.glEnd() - drawCapturedRegionGridOverlay(command) + drawCapturedRegionGrid(command) } - private fun drawCapturedRegionGridOverlay(command: RenderCommand.DrawCapturedScreenRegion) { - val grid = command.gridOverlay ?: return + private fun drawCapturedRegionGrid(command: RenderCommand.DrawCapturedScreenRegion) { + val grid = command.grid ?: return val columns = grid.columns.coerceAtLeast(1) val rows = grid.rows.coerceAtLeast(1) val magnification = grid.magnification.coerceAtLeast(1) diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/ScreenDomainSurfaceOrchestrator.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/ScreenDomainSurfaceOrchestrator.kt index cb27871..d079665 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/ScreenDomainSurfaceOrchestrator.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/ScreenDomainSurfaceOrchestrator.kt @@ -1,7 +1,7 @@ package org.dreamfinity.dsgl.mcForge1710 -import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurface -import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces +import org.dreamfinity.dsgl.core.portal.ScreenDomainSurface +import org.dreamfinity.dsgl.core.portal.ScreenDomainSurfaces import org.dreamfinity.dsgl.core.render.RenderCommand internal class ScreenDomainSurfaceOrchestrator { diff --git a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostApplicationPortalFrameTests.kt b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostApplicationPortalFrameTests.kt index fe11800..4eed530 100644 --- a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostApplicationPortalFrameTests.kt +++ b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostApplicationPortalFrameTests.kt @@ -12,7 +12,7 @@ import org.dreamfinity.dsgl.core.dsl.button import org.dreamfinity.dsgl.core.dsl.text import org.dreamfinity.dsgl.core.dsl.ui import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.hasActiveModalPortal +import org.dreamfinity.dsgl.core.portal.hasActiveModalPortal import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.StyleEngine import org.junit.After @@ -51,26 +51,26 @@ class DsglScreenHostApplicationPortalFrameTests { val root = ContainerNode(key = "root").apply { bounds = Rect(0, 0, 300, 120) } val tree = DomTree(root) val host = createHost(tree) - val overlay = host.debugApplicationOverlayHostForTests() + val applicationPortal = host.debugApplicationPortalHostForTests() val modalKey = "tests.host.modal.frame.hover.commands" try { renderStaticModalWithButton(modalKey) - host.debugSyncApplicationOverlaySurfaceForTests(ctx, width = 300, height = 120) + host.debugSyncApplicationPortalSurfaceForTests(ctx, width = 300, height = 120) - assertTrue(overlay.hasActiveModalPortal()) - assertTrue(overlay.handleMouseMove(76, 25)) + assertTrue(applicationPortal.hasActiveModalPortal()) + assertTrue(applicationPortal.handleMouseMove(76, 25)) assertRenderColorPresent( - host.debugCollectApplicationOverlayCommandsForTests(ctx), + host.debugCollectApplicationPortalCommandsForTests(ctx), hoverColor, ) host.debugUpdateFrameInteractionStateForTests(tree, mouseX = 4, mouseY = 4) - val settledCommands = host.debugCollectApplicationOverlayCommandsForTests(ctx) + val settledCommands = host.debugCollectApplicationPortalCommandsForTests(ctx) val stagedCommands = - host.debugStageApplicationOverlayCommandsForTests( + host.debugStageApplicationPortalCommandsForTests( tree = tree, - applicationOverlayCommands = settledCommands, + applicationPortalCommands = settledCommands, measureContext = ctx, ) @@ -78,7 +78,7 @@ class DsglScreenHostApplicationPortalFrameTests { assertRenderColorAbsent(stagedCommands, hoverColor) } finally { renderEmptyModal(modalKey) - host.debugSyncApplicationOverlaySurfaceForTests(ctx, width = 300, height = 120) + host.debugSyncApplicationPortalSurfaceForTests(ctx, width = 300, height = 120) } } @@ -88,27 +88,27 @@ class DsglScreenHostApplicationPortalFrameTests { var clicked = false var tree = renderClickStateModal(modalKey = modalKey, clicked = clicked, onClick = { clicked = true }) val host = createHost(tree) - val overlay = host.debugApplicationOverlayHostForTests() + val applicationPortal = host.debugApplicationPortalHostForTests() try { - host.debugSyncApplicationOverlaySurfaceForTests(ctx, width = 300, height = 120) - assertTrue(overlay.hasActiveModalPortal()) - assertRenderTextPresent(host.debugCollectApplicationOverlayCommandsForTests(ctx), "Before") + host.debugSyncApplicationPortalSurfaceForTests(ctx, width = 300, height = 120) + assertTrue(applicationPortal.hasActiveModalPortal()) + assertRenderTextPresent(host.debugCollectApplicationPortalCommandsForTests(ctx), "Before") - assertTrue(overlay.handleMouseDown(76, 25, MouseButton.LEFT)) - assertTrue(overlay.handleMouseUp(76, 25, MouseButton.LEFT)) + assertTrue(applicationPortal.handleMouseDown(76, 25, MouseButton.LEFT)) + assertTrue(applicationPortal.handleMouseUp(76, 25, MouseButton.LEFT)) assertTrue(clicked) tree = renderClickStateModal(modalKey = modalKey, clicked = clicked, onClick = { clicked = true }) host.debugBindTreeForTests(tree, needsLayout = false) - host.debugSyncApplicationOverlaySurfaceForTests(ctx, width = 300, height = 120) + host.debugSyncApplicationPortalSurfaceForTests(ctx, width = 300, height = 120) - val finalPortalCommands = host.debugCollectApplicationOverlayCommandsForTests(ctx) + val finalPortalCommands = host.debugCollectApplicationPortalCommandsForTests(ctx) assertRenderTextAbsent(finalPortalCommands, "Before") assertRenderTextPresent(finalPortalCommands, "After") } finally { renderEmptyModal(modalKey) - host.debugSyncApplicationOverlaySurfaceForTests(ctx, width = 300, height = 120) + host.debugSyncApplicationPortalSurfaceForTests(ctx, width = 300, height = 120) } } diff --git a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt index 85d5653..849e703 100644 --- a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt +++ b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/DsglScreenHostDomainOrchestrationTests.kt @@ -27,11 +27,11 @@ import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.event.MouseDownEvent import org.dreamfinity.dsgl.core.event.MouseLeaveEvent import org.dreamfinity.dsgl.core.event.MouseMoveEvent -import org.dreamfinity.dsgl.core.overlay.ApplicationOverlayHost -import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurface -import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces -import org.dreamfinity.dsgl.core.overlay.hasActiveModalPortal -import org.dreamfinity.dsgl.core.overlay.toggleFloatingWindowDemo +import org.dreamfinity.dsgl.core.portal.ApplicationPortalHost +import org.dreamfinity.dsgl.core.portal.ScreenDomainSurface +import org.dreamfinity.dsgl.core.portal.ScreenDomainSurfaces +import org.dreamfinity.dsgl.core.portal.hasActiveModalPortal +import org.dreamfinity.dsgl.core.portal.toggleFloatingWindowDemo import org.dreamfinity.dsgl.core.render.RenderCommand import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -221,15 +221,15 @@ class DsglScreenHostDomainOrchestrationTests { } val tree = DomTree(root) val host = createHost(tree) - val applicationOverlay = host.debugApplicationOverlayHostForTests() + val applicationPortal = host.debugApplicationPortalHostForTests() host.debugUpdateFrameInteractionStateForTests(tree, mouseX = 300, mouseY = 230) assertSame(rootButton, host.debugHoverTargetForTests()) assertEquals(0, leaveCount) - applicationOverlay.onInputFrame(1280, 720) - applicationOverlay.toggleFloatingWindowDemo(anchorX = 260, anchorY = 200) - applicationOverlay.render(ctx, 1280, 720) + applicationPortal.onInputFrame(1280, 720) + applicationPortal.toggleFloatingWindowDemo(anchorX = 260, anchorY = 200) + applicationPortal.render(ctx, 1280, 720) val portalBodyX = 540 val portalBodyY = 370 @@ -285,11 +285,11 @@ class DsglScreenHostDomainOrchestrationTests { .applyParent(root) val tree = DomTree(root) val host = createHost(tree) - val applicationOverlay = host.debugApplicationOverlayHostForTests() + val applicationPortal = host.debugApplicationPortalHostForTests() - applicationOverlay.onInputFrame(1280, 720) - applicationOverlay.toggleFloatingWindowDemo(anchorX = 260, anchorY = 200) - applicationOverlay.render(ctx, 1280, 720) + applicationPortal.onInputFrame(1280, 720) + applicationPortal.toggleFloatingWindowDemo(anchorX = 260, anchorY = 200) + applicationPortal.render(ctx, 1280, 720) host.debugUpdateFrameInteractionStateForTests(tree, mouseX = 630, mouseY = 510) @@ -309,7 +309,7 @@ class DsglScreenHostDomainOrchestrationTests { ).applyParent(root) val tree = DomTree(root) val host = createHost(tree) - val applicationOverlay = host.debugApplicationOverlayHostForTests() + val applicationPortal = host.debugApplicationPortalHostForTests() val probeLayout = colorPickerLayoutProbe() val style = ColorPickerStyle() val hoverX = probeLayout.copyRect.x + 2 @@ -320,9 +320,9 @@ class DsglScreenHostDomainOrchestrationTests { EventBus.post(MouseMoveEvent(hoverX, hoverY, hoverX - 1, hoverY - 1).also { it.target = picker }) assertRenderColorPresent(tree.paint(ctx), style.buttonHoverColor) - applicationOverlay.onInputFrame(1280, 720) - applicationOverlay.toggleFloatingWindowDemo(anchorX = hoverX, anchorY = hoverY) - applicationOverlay.render(ctx, 1280, 720) + applicationPortal.onInputFrame(1280, 720) + applicationPortal.toggleFloatingWindowDemo(anchorX = hoverX, anchorY = hoverY) + applicationPortal.render(ctx, 1280, 720) host.debugUpdateFrameInteractionStateForTests(tree, mouseX = hoverX, mouseY = hoverY) assertNull(host.debugHoverTargetForTests()) @@ -413,7 +413,7 @@ class DsglScreenHostDomainOrchestrationTests { }.applyParent(root) val tree = DomTree(root) val host = createHost(tree) - val overlay = host.debugApplicationOverlayHostForTests() + val applicationPortal = host.debugApplicationPortalHostForTests() val modalKey = "tests.host.modal.frame.blocks.dnd" val down = MouseDownEvent(24, 44, MouseButton.LEFT) @@ -423,7 +423,7 @@ class DsglScreenHostDomainOrchestrationTests { DndRuntime.engine.cancelActiveDrag() try { - activateStaticModal(overlay, modalKey) + activateStaticModal(applicationPortal, modalKey) DndRuntime.engine.onMouseDown(root, draggable, down) DndRuntime.engine.onMouseMove(root, 120, 60) assertTrue(DndRuntime.engine.isDragging) @@ -433,7 +433,7 @@ class DsglScreenHostDomainOrchestrationTests { assertFalse(DndRuntime.engine.isPointerCaptured) assertFalse(DndRuntime.engine.isDragging) } finally { - clearStaticModal(overlay, modalKey) + clearStaticModal(applicationPortal, modalKey) DndRuntime.engine.cancelActiveDrag() } } @@ -462,9 +462,9 @@ class DsglScreenHostDomainOrchestrationTests { assertTrue(DndRuntime.engine.isDragging) val staged = - host.debugStageApplicationOverlayCommandsForTests( + host.debugStageApplicationPortalCommandsForTests( tree = tree, - applicationOverlayCommands = emptyList(), + applicationPortalCommands = emptyList(), measureContext = ctx, ) @@ -489,7 +489,7 @@ class DsglScreenHostDomainOrchestrationTests { }.applyParent(root) val tree = DomTree(root) val host = createHost(tree) - val overlay = host.debugApplicationOverlayHostForTests() + val applicationPortal = host.debugApplicationPortalHostForTests() val modalKey = "tests.host.modal.suppresses.ghost.commands" val down = MouseDownEvent(24, 44, MouseButton.LEFT) @@ -499,16 +499,16 @@ class DsglScreenHostDomainOrchestrationTests { DndRuntime.engine.cancelActiveDrag() try { - activateStaticModal(overlay, modalKey) + activateStaticModal(applicationPortal, modalKey) DndRuntime.engine.onMouseDown(root, draggable, down) DndRuntime.engine.onMouseMove(root, 120, 60) assertTrue(DndRuntime.engine.isDragging) - assertTrue(overlay.hasActiveModalPortal()) + assertTrue(applicationPortal.hasActiveModalPortal()) val staged = - host.debugStageApplicationOverlayCommandsForTests( + host.debugStageApplicationPortalCommandsForTests( tree = tree, - applicationOverlayCommands = overlay.paint(ctx), + applicationPortalCommands = applicationPortal.paint(ctx), measureContext = ctx, ) @@ -518,7 +518,7 @@ class DsglScreenHostDomainOrchestrationTests { }, ) } finally { - clearStaticModal(overlay, modalKey) + clearStaticModal(applicationPortal, modalKey) DndRuntime.engine.cancelActiveDrag() } } @@ -601,7 +601,7 @@ class DsglScreenHostDomainOrchestrationTests { buildRenderCommands(ctx, out) } - private fun activateStaticModal(overlay: ApplicationOverlayHost, modalKey: String) { + private fun activateStaticModal(applicationPortal: ApplicationPortalHost, modalKey: String) { val modalTree = ui { modalPortal( @@ -617,11 +617,11 @@ class DsglScreenHostDomainOrchestrationTests { } } modalTree.render(ctx, 300, 120) - overlay.render(ctx, 300, 120) - assertTrue(overlay.hasActiveModalPortal()) + applicationPortal.render(ctx, 300, 120) + assertTrue(applicationPortal.hasActiveModalPortal()) } - private fun clearStaticModal(overlay: ApplicationOverlayHost, modalKey: String) { + private fun clearStaticModal(applicationPortal: ApplicationPortalHost, modalKey: String) { val emptyModalTree = ui { modalPortal(modals = emptyList(), key = modalKey) { @@ -629,7 +629,7 @@ class DsglScreenHostDomainOrchestrationTests { } } emptyModalTree.render(ctx, 300, 120) - overlay.render(ctx, 300, 120) + applicationPortal.render(ctx, 300, 120) } private fun colorPickerLayoutProbe(): ColorPickerLayout = diff --git a/core/detekt-baseline.xml b/core/detekt-baseline.xml index 84eb9db..af3c86c 100644 --- a/core/detekt-baseline.xml +++ b/core/detekt-baseline.xml @@ -6,7 +6,7 @@ ComplexCondition:ColorPickerPopupEngine.kt$ColorPickerPopupEngine$previous.anchorRect != request.anchorRect || previous.width != request.width || previous.style != request.style || previous.state.alphaEnabled != request.state.alphaEnabled ComplexCondition:DOMNode.kt$DOMNode$border.top <= 0 && border.right <= 0 && border.bottom <= 0 && border.left <= 0 ComplexCondition:DOMNode.kt$DOMNode$relativeOffsetX == 0 && relativeOffsetY == 0 && stickyOffsetX == 0 && stickyOffsetY == 0 - ComplexCondition:DomTree.kt$DomTree$( !laidOut || styleReport.layoutDirty || scrollInvalidation.layoutDirty || requiresSystemOverlayScrollLayoutFallback ) && lastWidth > 0 && lastHeight > 0 + ComplexCondition:DomTree.kt$DomTree$( !laidOut || styleReport.layoutDirty || scrollInvalidation.layoutDirty || requiresIsolatedScopeScrollLayoutFallback ) && lastWidth > 0 && lastHeight > 0 ComplexCondition:FontRegistry.kt$FontRegistry$end > start && end < text.length && Character.isLowSurrogate(text[end]) && Character.isHighSurrogate(text[end - 1]) ComplexCondition:FontRegistry.kt$FontRegistry$start > 0 && start < text.length && Character.isLowSurrogate(text[start]) && Character.isHighSurrogate(text[start - 1]) ComplexCondition:InspectorController.kt$InspectorController$!dragMoved && ( kotlin.math.abs(resized.width - dragStartRect.width) >= 2 || kotlin.math.abs(resized.height - dragStartRect.height) >= 2 || kotlin.math.abs(resized.x - dragStartRect.x) >= 2 || kotlin.math.abs(resized.y - dragStartRect.y) >= 2 ) @@ -15,7 +15,7 @@ ComplexCondition:InspectorController.kt$InspectorController$cached != null && cached.key == key && cached.nodeClass == klass && cached.layoutVersion == layoutVersion ComplexCondition:InspectorController.kt$InspectorController$endedMode == DragMode.MinimizedMove && clickLike && panelState == InspectorPanelState.Minimized && minimizedBounds.contains(mouseX, mouseY) ComplexCondition:InspectorController.kt$InspectorController$numberText.isEmpty() || numberText == "-" || numberText == "." || numberText == "-." - ComplexCondition:LayerDomInputRouter.kt$LayerDomInputRouter$node.onMouseDown != null || node.onMouseUp != null || node.onMouseClick != null || node.onMouseDrag != null || node.onMouseWheel != null || node.onMouseMove != null || node.onKeyDown != null || node.onKeyUp != null + ComplexCondition:SurfaceDomInputRouter.kt$SurfaceDomInputRouter$node.onMouseDown != null || node.onMouseUp != null || node.onMouseClick != null || node.onMouseDrag != null || node.onMouseWheel != null || node.onMouseMove != null || node.onKeyDown != null || node.onKeyUp != null ComplexCondition:ModalPortalSessionStore.kt$ModalPortalSessionStore$(needsFocusOnTop || (topMost.trapFocus && focusOutsideTop)) && !FocusManager.requestFocusFirstInSubtree(topDialogKey) ComplexCondition:SelectNode.kt$SelectNode$!controlled && uncontrolledValue == null && value != null && optionExists(value) ComplexCondition:SingleLineInputNode.kt$SingleLineInputNode$!showPlaceholder && focused && !styleDisabled && editState.isCaretVisible(caretBlinkPeriodMs) @@ -37,7 +37,7 @@ CyclomaticComplexMethod:ContainerNode.kt$ContainerNode$private fun renderFlex(ctx: UiMeasureContext, children: List<DOMNode>) CyclomaticComplexMethod:ContainerNode.kt$ContainerNode$private fun renderGrid(ctx: UiMeasureContext, children: List<DOMNode>) CyclomaticComplexMethod:ContextMenuEngine.kt$ContextMenuEngine$@Suppress("LoopWithTooManyJumpStatements") private fun ensureLayout() - CyclomaticComplexMethod:ContextMenuEngine.kt$ContextMenuEngine$fun appendOverlayCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, out: MutableList<RenderCommand>, ) + CyclomaticComplexMethod:ContextMenuEngine.kt$ContextMenuEngine$fun appendPortalCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, out: MutableList<RenderCommand>, ) CyclomaticComplexMethod:ContextMenuEngine.kt$ContextMenuEngine$fun handleKeyDown(keyCode: Int): Boolean CyclomaticComplexMethod:ContextMenuMeasurementCache.kt$ContextMenuMeasurementCache$fun measure( menuToken: Long, entries: List<MenuEntry>, style: ContextMenuStyle, fontId: String?, fontSize: Int?, ctx: UiMeasureContext, dpiScale: Float, ): Measurement CyclomaticComplexMethod:CssLength.kt$fun parseCssLength(raw: String, allowUnitlessZero: Boolean = true): CssLength @@ -62,8 +62,8 @@ CyclomaticComplexMethod:MinecraftFormattingParser.kt$MinecraftFormattingParser$@Suppress("LoopWithTooManyJumpStatements") private fun parseMinecraft(text: String): ParsedText CyclomaticComplexMethod:ModalDsl.kt$fun UiScope.modalPortal(modals: List<ModalSpec>, modalKey: String = "modal.host", content: UiScope.() -> Unit) CyclomaticComplexMethod:MsdfFontMetaParser.kt$MsdfFontMetaParser$fun parse(rawJson: String): MsdfFontMeta - CyclomaticComplexMethod:OverlayPanel.kt$OverlayPanel$private fun buildResizedRect(viewportWidth: Int, viewportHeight: Int): Rect - CyclomaticComplexMethod:SelectEngine.kt$SelectEngine$fun appendOverlayCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, out: MutableList<RenderCommand>, ) + CyclomaticComplexMethod:FloatingPanel.kt$FloatingPanel$private fun buildResizedRect(viewportWidth: Int, viewportHeight: Int): Rect + CyclomaticComplexMethod:SelectEngine.kt$SelectEngine$fun appendPortalCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, out: MutableList<RenderCommand>, ) CyclomaticComplexMethod:SingleLineInputNode.kt$SingleLineInputNode$override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList<RenderCommand>) CyclomaticComplexMethod:StyleEngine.kt$StyleEngine$private fun applyProperty( current: ComputedStyle, parentComputed: ComputedStyle?, property: StyleProperty, expression: StyleExpression, variables: Map<String, String>, rootFontSizePx: Int, ): ComputedStyle CyclomaticComplexMethod:StyleEngine.kt$StyleEngine$private fun selectorLabel(selector: StyleSelector): String @@ -72,8 +72,8 @@ CyclomaticComplexMethod:StyleSelector.kt$StyleSelector.Companion$private fun parsePartToken(token: String, rawSelector: String): StyleSelectorPart CyclomaticComplexMethod:StyleValueParsing.kt$fun validateLiteralForProperty( property: StyleProperty, literal: String, warningReporter: StyleWarningReporter? = null, deprecatedLengthWarningKey: String = "deprecated.unitless-length", ) CyclomaticComplexMethod:StylesheetManager.kt$StylesheetManager$@Synchronized fun pollForChanges(force: Boolean = false) - CyclomaticComplexMethod:SystemOverlayInspectorNativeEntryTests.kt$SystemOverlayInspectorNativeEntryTests$@Test fun `inspector clipped body blocks hidden row input and accepts visible portion`() - CyclomaticComplexMethod:SystemOverlayInspectorNativeEntryTests.kt$SystemOverlayInspectorNativeEntryTests$@Test fun `inspector native body content remains clipped in narrow viewport`() + CyclomaticComplexMethod:SystemPortalInspectorNativeEntryTests.kt$SystemPortalInspectorNativeEntryTests$@Test fun `inspector clipped body blocks hidden row input and accepts visible portion`() + CyclomaticComplexMethod:SystemPortalInspectorNativeEntryTests.kt$SystemPortalInspectorNativeEntryTests$@Test fun `inspector native body content remains clipped in narrow viewport`() CyclomaticComplexMethod:TextAreaNode.kt$TextAreaNode$override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList<RenderCommand>) CyclomaticComplexMethod:TextAreaNode.kt$TextAreaNode$private fun handleKey(event: KeyboardKeyDownEvent) CyclomaticComplexMethod:TextEditShortcutDispatcher.kt$TextEditShortcutDispatcher$fun dispatch(event: KeyboardKeyDownEvent, callbacks: TextShortcutCallbacks): Boolean @@ -91,19 +91,19 @@ LargeClass:StyleEngine.kt$StyleEngine LargeClass:StyleScope.kt$StyleScope : CssLengthUnitsDsl LargeClass:SystemColorPickerPopupBodyNode.kt$SystemColorPickerPopupBodyNode : DOMNode - LargeClass:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode : DOMNode - LargeClass:SystemOverlayColorPickerEntryTests.kt$SystemOverlayColorPickerEntryTests - LargeClass:SystemOverlayInspectorNativeEntryTests.kt$SystemOverlayInspectorNativeEntryTests + LargeClass:SystemInspectorPortalNode.kt$SystemInspectorPortalNode : DOMNode + LargeClass:SystemPortalColorPickerEntryTests.kt$SystemPortalColorPickerEntryTests + LargeClass:SystemPortalInspectorNativeEntryTests.kt$SystemPortalInspectorNativeEntryTests LargeClass:TextAreaNode.kt$TextAreaNode : DOMNode LargeClass:TextPerformanceHotPathCharacterizationTests.kt$TextPerformanceHotPathCharacterizationTests LongMethod:ColorPickerController.kt$ColorPickerController$fun appendCommands( layout: ColorPickerLayout, out: MutableList<RenderCommand>, nowMs: Long = System.currentTimeMillis(), ) - LongMethod:ColorPickerController.kt$ColorPickerController$fun appendEyedropperOverlay(viewportWidth: Int, viewportHeight: Int, out: MutableList<RenderCommand>) + LongMethod:ColorPickerController.kt$ColorPickerController$fun appendEyedropperPreview(viewportWidth: Int, viewportHeight: Int, out: MutableList<RenderCommand>) LongMethod:ColorPickerController.kt$ColorPickerController$fun buildLayout(bounds: Rect): ColorPickerLayout LongMethod:ColorPickerController.kt$ColorPickerController$fun handleMouseDown( globalX: Int, globalY: Int, button: MouseButton, layout: ColorPickerLayout, ): Boolean LongMethod:ComponentHookRuntime.kt$ComponentHookRuntime$private fun applyEffectCommitBatch(batch: PendingEffectCommitBatch) LongMethod:ContainerNode.kt$ContainerNode$private fun renderFlex(ctx: UiMeasureContext, children: List<DOMNode>) LongMethod:ContainerNode.kt$ContainerNode$private fun renderGrid(ctx: UiMeasureContext, children: List<DOMNode>) - LongMethod:ContextMenuEngine.kt$ContextMenuEngine$fun appendOverlayCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, out: MutableList<RenderCommand>, ) + LongMethod:ContextMenuEngine.kt$ContextMenuEngine$fun appendPortalCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, out: MutableList<RenderCommand>, ) LongMethod:ContextMenuMeasurementCache.kt$ContextMenuMeasurementCache$fun measure( menuToken: Long, entries: List<MenuEntry>, style: ContextMenuStyle, fontId: String?, fontSize: Int?, ctx: UiMeasureContext, dpiScale: Float, ): Measurement LongMethod:DOMNode.kt$DOMNode$@Suppress("UnusedParameter") internal fun resolveLayoutStyleValues(ctx: UiMeasureContext, parentContentWidth: Int?, parentContentHeight: Int?) LongMethod:DOMNode.kt$DOMNode$fun scrollContainerState(): ScrollContainerState @@ -118,25 +118,25 @@ LongMethod:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$fun build(context: InspectorStyleEditorSnapshotBuildContext): InspectorStyleEditorSnapshotBuildResult LongMethod:ModalDsl.kt$fun UiScope.modalPortal(modals: List<ModalSpec>, modalKey: String = "modal.host", content: UiScope.() -> Unit) LongMethod:MsdfFontMetaParser.kt$MsdfFontMetaParser$fun parse(rawJson: String): MsdfFontMeta - LongMethod:OverlayPanel.kt$OverlayPanel$private fun buildResizedRect(viewportWidth: Int, viewportHeight: Int): Rect + LongMethod:FloatingPanel.kt$FloatingPanel$private fun buildResizedRect(viewportWidth: Int, viewportHeight: Int): Rect LongMethod:PositionedLayoutStickyBehaviorTests.kt$PositionedLayoutStickyBehaviorTests$@Test fun `non-sticky positioned modes remain unchanged with sticky enabled`() - LongMethod:SelectEngine.kt$SelectEngine$fun appendOverlayCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, out: MutableList<RenderCommand>, ) + LongMethod:SelectEngine.kt$SelectEngine$fun appendPortalCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, out: MutableList<RenderCommand>, ) LongMethod:StyleEngine.kt$StyleEngine$private fun applyProperty( current: ComputedStyle, parentComputed: ComputedStyle?, property: StyleProperty, expression: StyleExpression, variables: Map<String, String>, rootFontSizePx: Int, ): ComputedStyle LongMethod:StyleValueParsing.kt$fun validateLiteralForProperty( property: StyleProperty, literal: String, warningReporter: StyleWarningReporter? = null, deprecatedLengthWarningKey: String = "deprecated.unitless-length", ) - LongMethod:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$private fun renderExpanded(ctx: UiMeasureContext, snapshot: InspectorDomSnapshot, viewportRect: Rect) - LongMethod:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$private fun renderHighlights(scope: UiScope, ctx: UiMeasureContext) - LongMethod:SystemOverlayInspectorNativeEntryTests.kt$SystemOverlayInspectorNativeEntryTests$@Test fun `inspector clipped body blocks hidden row input and accepts visible portion`() - LongMethod:SystemOverlayInspectorNativeEntryTests.kt$SystemOverlayInspectorNativeEntryTests$@Test fun `inspector wheel scrolling remains symmetric across rebuilds`() + LongMethod:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$private fun renderExpanded(ctx: UiMeasureContext, snapshot: InspectorDomSnapshot, viewportRect: Rect) + LongMethod:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$private fun renderHighlights(scope: UiScope, ctx: UiMeasureContext) + LongMethod:SystemPortalInspectorNativeEntryTests.kt$SystemPortalInspectorNativeEntryTests$@Test fun `inspector clipped body blocks hidden row input and accepts visible portion`() + LongMethod:SystemPortalInspectorNativeEntryTests.kt$SystemPortalInspectorNativeEntryTests$@Test fun `inspector wheel scrolling remains symmetric across rebuilds`() LongMethod:TextPerformanceHotPathCharacterizationTests.kt$TextPerformanceHotPathCharacterizationTests$@Test fun `cache boundaries are explicit for wrapped layout paths`() LongParameterList:ColorPickerInlineNode.kt$ColorPickerInlineNode$( controlled: Boolean = false, value: RgbaColor? = null, defaultValue: RgbaColor = RgbaColor.WHITE, previousValue: RgbaColor? = null, mode: ColorFormatMode = ColorFormatMode.HEX, alphaEnabled: Boolean = true, key: Any? = null, ) LongParameterList:ColorPickerPopupPaneNode.kt$ColorPickerPopupPaneNode$( controlled: Boolean = false, value: RgbaColor? = null, defaultValue: RgbaColor = RgbaColor.WHITE, previousValue: RgbaColor? = null, mode: ColorFormatMode = ColorFormatMode.HEX, alphaEnabled: Boolean = true, key: Any? = null, ) - LongParameterList:ColorPickerPopupEngine.kt$ColorPickerPopupManager$( ownerScope: OverlayOwnerScope = OverlayOwnerScope.Application, anchorRect: Rect, title: String, state: ColorPickerState, style: ColorPickerStyle = ColorPickerStyle(), width: Int = 320, draggable: Boolean = true, closeOnOutsideClick: Boolean = false, onPreview: ((RgbaColor) -> Unit)? = null, onChange: ((RgbaColor) -> Unit)? = null, onCommit: ((RgbaColor) -> Unit)? = null, onClose: (() -> Unit)? = null, ) - LongParameterList:ComponentProps.kt$ComponentProps$( var style: StyleScope.() -> Unit = {}, var key: Any? = null, var id: String? = null, var className: String = "", var classes: Set<String> = emptySet(), var disabled: Boolean = false, var draggable: Boolean = false, var droppable: Boolean = false, var dragPreviewMode: DragPreviewMode = DragPreviewMode.GHOST, var hideSourceWhileDragging: Boolean = false, var dragPreview: (DragPreviewScope.() -> Unit)? = null, var dragPlaceholder: (PlaceholderScope.() -> Unit)? = null, var ref: RefTarget<ElementHandle>? = null, var onMouseEnter: ((MouseEnterEvent) -> Unit)? = null, var onMouseLeave: ((MouseLeaveEvent) -> Unit)? = null, var onMouseOver: ((MouseOverEvent) -> Unit)? = null, var onMouseMove: ((MouseMoveEvent) -> Unit)? = null, var onMouseDown: ((MouseDownEvent) -> Unit)? = null, var onMouseUp: ((MouseUpEvent) -> Unit)? = null, var onMouseClick: ((MouseClickEvent) -> Unit)? = null, var onMouseDrag: ((MouseDragEvent) -> Unit)? = null, var onMouseWheel: ((MouseWheelEvent) -> Unit)? = null, var onKeyDown: ((KeyboardKeyDownEvent) -> Unit)? = null, var onKeyUp: ((KeyboardKeyUpEvent) -> Unit)? = null, var onKeyPressed: ((KeyboardKeyDownEvent) -> Unit)? = null, var onKeyReleased: ((KeyboardKeyUpEvent) -> Unit)? = null, var onFocusGain: ((FocusGainEvent) -> Unit)? = null, var onFocusLose: ((FocusLoseEvent) -> Unit)? = null, var onInput: ((InputEvent) -> Unit)? = null, var onValueChange: ((ValueChangedEvent) -> Unit)? = null, var onDragStart: ((DragStartEvent) -> Unit)? = null, var onDrag: ((DragEvent) -> Unit)? = null, var onDragEnd: ((DragEndEvent) -> Unit)? = null, var onDragEnter: ((DragEnterEvent) -> Unit)? = null, var onDragOver: ((DragOverEvent) -> Unit)? = null, var onDragLeave: ((DragLeaveEvent) -> Unit)? = null, var onDrop: ((DropEvent) -> Unit)? = null, ) + LongParameterList:ColorPickerPopupEngine.kt$ColorPickerPopupManager$( ownerDomain: ScreenDomainId = ScreenDomainId.Application, anchorRect: Rect, title: String, state: ColorPickerState, style: ColorPickerStyle = ColorPickerStyle(), width: Int = 320, draggable: Boolean = true, closeOnOutsideClick: Boolean = false, onPreview: ((RgbaColor) -> Unit)? = null, onChange: ((RgbaColor) -> Unit)? = null, onCommit: ((RgbaColor) -> Unit)? = null, onClose: (() -> Unit)? = null, ) + LongParameterList:ComponentProps.kt$ComponentProps$( var style: StyleScope.() -> Unit = {}, var key: Any? = null, var id: String? = null, var className: String = "", var classes: Set<String> = emptySet(), var overlapChildren: Boolean = false, var disabled: Boolean = false, var draggable: Boolean = false, var droppable: Boolean = false, var dragPreviewMode: DragPreviewMode = DragPreviewMode.GHOST, var hideSourceWhileDragging: Boolean = false, var dragPreview: (DragPreviewScope.() -> Unit)? = null, var dragPlaceholder: (PlaceholderScope.() -> Unit)? = null, var ref: RefTarget<ElementHandle>? = null, var onMouseEnter: ((MouseEnterEvent) -> Unit)? = null, var onMouseLeave: ((MouseLeaveEvent) -> Unit)? = null, var onMouseOver: ((MouseOverEvent) -> Unit)? = null, var onMouseMove: ((MouseMoveEvent) -> Unit)? = null, var onMouseDown: ((MouseDownEvent) -> Unit)? = null, var onMouseUp: ((MouseUpEvent) -> Unit)? = null, var onMouseClick: ((MouseClickEvent) -> Unit)? = null, var onMouseDrag: ((MouseDragEvent) -> Unit)? = null, var onMouseWheel: ((MouseWheelEvent) -> Unit)? = null, var onKeyDown: ((KeyboardKeyDownEvent) -> Unit)? = null, var onKeyUp: ((KeyboardKeyUpEvent) -> Unit)? = null, var onKeyPressed: ((KeyboardKeyDownEvent) -> Unit)? = null, var onKeyReleased: ((KeyboardKeyUpEvent) -> Unit)? = null, var onFocusGain: ((FocusGainEvent) -> Unit)? = null, var onFocusLose: ((FocusLoseEvent) -> Unit)? = null, var onInput: ((InputEvent) -> Unit)? = null, var onValueChange: ((ValueChangedEvent) -> Unit)? = null, var onDragStart: ((DragStartEvent) -> Unit)? = null, var onDrag: ((DragEvent) -> Unit)? = null, var onDragEnd: ((DragEndEvent) -> Unit)? = null, var onDragEnter: ((DragEnterEvent) -> Unit)? = null, var onDragOver: ((DragOverEvent) -> Unit)? = null, var onDragLeave: ((DragLeaveEvent) -> Unit)? = null, var onDrop: ((DropEvent) -> Unit)? = null, ) LongParameterList:ContainerNode.kt$ContainerNode$( ctx: UiMeasureContext, child: DOMNode, parentContentX: Int, parentContentY: Int, parentContentWidth: Int, parentContentHeight: Int, desiredX: Int, desiredY: Int, desiredWidth: Int, desiredHeight: Int, ) LongParameterList:DndHooks.kt$( id: String, nodeKey: Any = id, type: String = "default", data: Any? = null, previewMode: DragPreviewMode = DragPreviewMode.GHOST, hideSourceWhileDragging: Boolean = false, renderPreview: (DragPreviewScope.() -> Unit)? = null, renderPlaceholder: (PlaceholderScope.() -> Unit)? = null, onDragStart: ((DragStartEvent) -> Unit)? = null, onDrag: ((DragEvent) -> Unit)? = null, onDragEnd: ((DragEndEvent) -> Unit)? = null, ) LongParameterList:InspectorStyleEditorSnapshotBuilder.kt$InspectorStyleEditorSnapshotBuilder$( x: Int, y: Int, width: Int, options: List<String>, property: StyleProperty, unitSelect: Boolean, pointerProjectionScrollY: Int, rowHeightPx: Int, viewportWidth: Int, viewportHeight: Int, mouseX: Int, mouseY: Int, currentScrollIndex: Int, ) LongParameterList:InspectorStyleEditorSnapshotBuilderTests.kt$InspectorStyleEditorSnapshotBuilderTests$( selected: ContainerNode, inspection: org.dreamfinity.dsgl.core.style.StyleInspection, panelRect: Rect = Rect(20, 20, 360, 260), editableProperties: List<StyleProperty>, pointerProjectionScrollY: Int = 0, mouseX: Int = 180, mouseY: Int = 120, openValueSelectProperty: StyleProperty? = null, openUnitSelectProperty: StyleProperty? = null, openValueSelectScrollIndex: Int = 0, openUnitSelectScrollIndex: Int = 0, ) - LongParameterList:SelectNode.kt$SelectNode$( model: SelectModel, controlled: Boolean = false, value: String? = null, defaultValue: String? = null, closeOnSelect: Boolean = true, ownerScope: OverlayOwnerScope = OverlayOwnerScope.Application, key: Any? = null, ) + LongParameterList:SelectNode.kt$SelectNode$( model: SelectModel, controlled: Boolean = false, value: String? = null, defaultValue: String? = null, closeOnSelect: Boolean = true, ownerDomain: ScreenDomainId = ScreenDomainId.Application, key: Any? = null, ) LongParameterList:SystemColorPickerPanelManager.kt$SystemColorPickerPortalService$( anchorRect: Rect, title: String, state: ColorPickerState, style: ColorPickerStyle = ColorPickerStyle(), width: Int = 320, draggable: Boolean = true, closeOnOutsideClick: Boolean = false, onPreview: ((RgbaColor) -> Unit)? = null, onChange: ((RgbaColor) -> Unit)? = null, onCommit: ((RgbaColor) -> Unit)? = null, onClose: (() -> Unit)? = null, ) MagicNumber:AnimationModel.kt$ColorAnimatable$0xFF MagicNumber:AnimationModel.kt$ColorAnimatable$24 @@ -283,20 +283,20 @@ MagicNumber:ObfuscationTextSelector.kt$ObfuscationTextSelector$0x7FFF_FFFFL MagicNumber:ObfuscationTextSelector.kt$ObfuscationTextSelector$17 MagicNumber:ObfuscationTextSelector.kt$ObfuscationTextSelector$31 - MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlHost$120 - MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlHost$176 - MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlHost$18 - MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlHost$24 - MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlHost$300 - MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlHost$34 - MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlHost$56 - MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlHost$96 - MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlRootNode$0x5F000000 - MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlRootNode$14 - MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlRootNode$18 - MagicNumber:OverlayDebugControlHost.kt$OverlayDebugControlRootNode$20 - MagicNumber:OverlayLayerDebugState.kt$OverlayLayerDebugState$1000.0 - MagicNumber:OverlayPanel.kt$OverlayPanel$6 + MagicNumber:DebugDomainHosts.kt$DebugDomainControlHost$120 + MagicNumber:DebugDomainHosts.kt$DebugDomainControlHost$176 + MagicNumber:DebugDomainHosts.kt$DebugDomainControlHost$18 + MagicNumber:DebugDomainHosts.kt$DebugDomainControlHost$24 + MagicNumber:DebugDomainHosts.kt$DebugDomainControlHost$300 + MagicNumber:DebugDomainHosts.kt$DebugDomainControlHost$34 + MagicNumber:DebugDomainHosts.kt$DebugDomainControlHost$56 + MagicNumber:DebugDomainHosts.kt$DebugDomainControlHost$96 + MagicNumber:DebugDomainHosts.kt$DebugDomainControlRootNode$0x5F000000 + MagicNumber:DebugDomainHosts.kt$DebugDomainControlRootNode$14 + MagicNumber:DebugDomainHosts.kt$DebugDomainControlRootNode$18 + MagicNumber:DebugDomainHosts.kt$DebugDomainControlRootNode$20 + MagicNumber:DomainSurfaceDebugState.kt$DomainSurfaceDebugState$1000.0 + MagicNumber:FloatingPanel.kt$FloatingPanel$6 MagicNumber:RadioGroupNode.kt$RadioGroupNode$6 MagicNumber:RangeInputNode.kt$RangeInputNode$12 MagicNumber:RangeInputNode.kt$RangeInputNode$120 @@ -325,45 +325,45 @@ MagicNumber:SystemColorPickerCustomSurfaceNodes.kt$ColorFieldSurfaceNode$7 MagicNumber:SystemColorPickerCustomSurfaceNodes.kt$ColorSwatchSurfaceNode$0x33222A34 MagicNumber:SystemColorPickerCustomSurfaceNodes.kt$HueSurfaceNode$360f - MagicNumber:SystemColorPickerPopupBodyNode.kt$SystemColorPickerEyedropperOverlayNode$6 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x18212C39 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x1B293746 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x1E263241 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x22313D4B - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x2A425164 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x2A465968 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x2A4E3F56 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x3346596E - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x334D5D70 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x3A47A0FF - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x4426A69A - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x444285F4 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x44F3B33D - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x55394654 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x553F4A57 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x663F4A57 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x66FF5252 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x775E738C - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x77607084 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$0x777A5C84 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$12 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$14 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$160 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$18 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$20 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$22 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$24 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$264 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$32 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$34 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$36 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$40 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$5 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$58 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$6 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$64 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$86 - MagicNumber:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode$96 + MagicNumber:SystemColorPickerPopupBodyNode.kt$SystemColorPickerEyedropperPreviewNode$6 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$0x18212C39 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$0x1B293746 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$0x1E263241 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$0x22313D4B + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$0x2A425164 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$0x2A465968 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$0x2A4E3F56 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$0x3346596E + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$0x334D5D70 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$0x3A47A0FF + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$0x4426A69A + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$0x444285F4 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$0x44F3B33D + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$0x55394654 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$0x553F4A57 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$0x663F4A57 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$0x66FF5252 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$0x775E738C + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$0x77607084 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$0x777A5C84 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$12 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$14 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$160 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$18 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$20 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$22 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$24 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$264 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$32 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$34 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$36 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$40 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$5 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$58 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$6 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$64 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$86 + MagicNumber:SystemInspectorPortalNode.kt$SystemInspectorPortalNode$96 MagicNumber:TextAreaNode.kt$TextAreaNode$17L MagicNumber:TextAreaNode.kt$TextAreaNode$31L MagicNumber:TextAreaNode.kt$TextAreaNode$6 @@ -378,11 +378,11 @@ MagicNumber:TextStyleMetrics.kt$TextStyleMetrics$0x00A0 NestedBlockDepth:ComponentHookRuntime.kt$ComponentHookRuntime$private fun applyEffectCommitBatch(batch: PendingEffectCommitBatch) NestedBlockDepth:ContainerNode.kt$ContainerNode$private fun computeGridPlacements(children: List<DOMNode>, columns: Int): List<GridPlacement> - NestedBlockDepth:ContextMenuEngine.kt$ContextMenuEngine$fun appendOverlayCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, out: MutableList<RenderCommand>, ) + NestedBlockDepth:ContextMenuEngine.kt$ContextMenuEngine$fun appendPortalCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, out: MutableList<RenderCommand>, ) NestedBlockDepth:DssParser.kt$DssParser$@Suppress("ThrowsCount") private fun parseDeclarations( sourceName: String, text: String, fromIndex: Int, declarations: StyleDeclarations, rootVars: MutableMap<String, String>, allowVariables: Boolean, warnings: ParseWarnings, ): Int NestedBlockDepth:EventBus.kt$EventBus$fun post(event: Event) NestedBlockDepth:FontRegistry.kt$AtlasPayload$private fun decodeDeflatedRgba(bytes: ByteArray): AtlasBitmap? - NestedBlockDepth:SelectEngine.kt$SelectEngine$fun appendOverlayCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, out: MutableList<RenderCommand>, ) + NestedBlockDepth:SelectEngine.kt$SelectEngine$fun appendPortalCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, out: MutableList<RenderCommand>, ) NestedBlockDepth:StylesheetManager.kt$StylesheetManager$@Synchronized fun pollForChanges(force: Boolean = false) ReturnCount:ColorPickerController.kt$ColorPickerController$fun handleKeyDown(keyCode: Int, keyChar: Char): Boolean ReturnCount:ColorPickerController.kt$ColorPickerController$fun handleMouseDown( globalX: Int, globalY: Int, button: MouseButton, layout: ColorPickerLayout, ): Boolean @@ -400,9 +400,9 @@ ReturnCount:InspectorController.kt$InspectorController$fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean ReturnCount:InspectorController.kt$InspectorController$fun handleMouseWheel(mouseX: Int, mouseY: Int, delta: Int): Boolean ReturnCount:InspectorController.kt$InspectorController$private fun resolveResizeDragMode(mouseX: Int, mouseY: Int): DragMode - ReturnCount:LayerDomInputRouter.kt$LayerDomInputRouter$private fun collectHoverChainLocal( root: DOMNode, mouseX: Int, mouseY: Int, parentTransform: AffineTransform2D, parentInputClipRect: Rect?, out: MutableList<DOMNode>, ): Boolean - ReturnCount:OverlayPanel.kt$OverlayPanel$fun handleMouseDown( mouseX: Int, mouseY: Int, button: MouseButton, includeCloseButton: Boolean = true, ): Boolean - ReturnCount:OverlayPanel.kt$OverlayPanel$private fun resolveResizeHandle(panelRect: Rect, mouseX: Int, mouseY: Int): OverlayPanelResizeHandle? + ReturnCount:SurfaceDomInputRouter.kt$SurfaceDomInputRouter$private fun collectHoverChainLocal( root: DOMNode, mouseX: Int, mouseY: Int, parentTransform: AffineTransform2D, parentInputClipRect: Rect?, out: MutableList<DOMNode>, ): Boolean + ReturnCount:FloatingPanel.kt$FloatingPanel$fun handleMouseDown( mouseX: Int, mouseY: Int, button: MouseButton, includeCloseButton: Boolean = true, ): Boolean + ReturnCount:FloatingPanel.kt$FloatingPanel$private fun resolveResizeHandle(panelRect: Rect, mouseX: Int, mouseY: Int): FloatingPanelResizeHandle? ReturnCount:SelectEngine.kt$SelectEngine$fun handleKeyDown(keyCode: Int, keyChar: Char = 0.toChar()): Boolean ReturnCount:SelectEngine.kt$SelectEngine$fun handleMouseDown(mouseX: Int, mouseY: Int, button: MouseButton): Boolean ReturnCount:SelectEngine.kt$SelectEngine$private fun entryAt(current: PopupState, mouseX: Int, mouseY: Int): Int @@ -427,11 +427,11 @@ TooManyFunctions:FontRegistry.kt$FontRegistry TooManyFunctions:InspectorController.kt$InspectorController TooManyFunctions:InspectorEditorRegistry.kt$InspectorEditorRegistry - TooManyFunctions:LayerDomInputRouter.kt$LayerDomInputRouter + TooManyFunctions:SurfaceDomInputRouter.kt$SurfaceDomInputRouter TooManyFunctions:LayoutValidator.kt$LayoutValidator TooManyFunctions:MsdfFontMeta.kt$MsdfFontMeta - TooManyFunctions:OverlayDebugControlHost.kt$OverlayDebugControlHost - TooManyFunctions:OverlayPanel.kt$OverlayPanel + TooManyFunctions:DebugDomainHosts.kt$DebugDomainControlHost + TooManyFunctions:FloatingPanel.kt$FloatingPanel TooManyFunctions:PositionedLayoutModel.kt$PositionedLayoutModel TooManyFunctions:RadioGroupNode.kt$RadioGroupNode : DOMNode TooManyFunctions:RangeInputNode.kt$RangeInputNode : DOMNode @@ -443,11 +443,11 @@ TooManyFunctions:StyleEngine.kt$StyleEngine TooManyFunctions:StyleScope.kt$StyleScope : CssLengthUnitsDsl TooManyFunctions:StyleValueParsing.kt$org.dreamfinity.dsgl.core.style.StyleValueParsing.kt - TooManyFunctions:SystemColorPickerPopupBodyNode.kt$SystemColorPickerEyedropperOverlayNode : DOMNode + TooManyFunctions:SystemColorPickerPopupBodyNode.kt$SystemColorPickerEyedropperPreviewNode : DOMNode TooManyFunctions:SystemColorPickerPopupBodyNode.kt$SystemColorPickerPopupBodyNode : DOMNode - TooManyFunctions:SystemInspectorOverlayNode.kt$SystemInspectorOverlayNode : DOMNode - TooManyFunctions:SystemOverlayHost.kt$SystemOverlayHost : OverlayLayerHost - TooManyFunctions:SystemOverlayHost.kt$SystemOverlayHost$ColorPickerOverlayEntry : SystemOverlayEntrySystemColorPickerPortalService + TooManyFunctions:SystemInspectorPortalNode.kt$SystemInspectorPortalNode : DOMNode + TooManyFunctions:SystemPortalHost.kt$SystemPortalHost : DomainSurfaceHost + TooManyFunctions:SystemPortalHost.kt$SystemPortalHost$ColorPickerPortalEntry : SystemPortalEntrySystemColorPickerPortalService TooManyFunctions:TextAreaNode.kt$TextAreaNode : DOMNode TooManyFunctions:ToggleNode.kt$ToggleNode : DOMNode diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt index c0a1e2e..60a7384 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerController.kt @@ -39,7 +39,7 @@ data class ColorPickerLayout( val recentRects: List, ) -data class ColorPickerEyedropperOverlayModel( +data class ColorPickerEyedropperPreviewModel( val panelRect: Rect, val magnifierRect: Rect, val captureSourceRect: Rect, @@ -94,8 +94,8 @@ class ColorPickerController( } private var eyedropperActive: Boolean = false private var eyedropperBaseColor: RgbaColor = state.color - private val eyedropperOverlayDrag: FloatingPaneDragModel = FloatingPaneDragModel() - private var eyedropperOverlayRect: Rect? = null + private val eyedropperPreviewDrag: FloatingPaneDragModel = FloatingPaneDragModel() + private var eyedropperPreviewRect: Rect? = null private var domFocusedInputKey: String? = null private var domInputFocusResyncRequested: Boolean = false private var domLastFocusedInputKey: String? = null @@ -118,8 +118,8 @@ class ColorPickerController( eyedropperActive = false interaction.clearDragTarget() modeDropdownOpen = false - eyedropperOverlayDrag.end() - eyedropperOverlayRect = null + eyedropperPreviewDrag.end() + eyedropperPreviewRect = null domFocusedInputKey = null domInputFocusResyncRequested = false domLastFocusedInputKey = null @@ -314,8 +314,8 @@ class ColorPickerController( eyedropperBaseColor = state.color } eyedropperActive = true - eyedropperOverlayDrag.end() - eyedropperOverlayRect = null + eyedropperPreviewDrag.end() + eyedropperPreviewRect = null modeDropdownOpen = false clearInputEdit() } @@ -323,8 +323,8 @@ class ColorPickerController( fun cancelEyedropper() { if (!eyedropperActive) return eyedropperActive = false - eyedropperOverlayDrag.end() - eyedropperOverlayRect = null + eyedropperPreviewDrag.end() + eyedropperPreviewRect = null applyColor(eyedropperBaseColor, notifyPreview = true, commit = false) } @@ -631,7 +631,7 @@ class ColorPickerController( drawModeOptions(layout, out) } - fun appendEyedropperOverlay(viewportWidth: Int, viewportHeight: Int, out: MutableList) { + fun appendEyedropperPreview(viewportWidth: Int, viewportHeight: Int, out: MutableList) { if (!eyedropperActive) return if (hoverX == Int.MIN_VALUE || hoverY == Int.MIN_VALUE) return @@ -648,25 +648,25 @@ class ColorPickerController( val preferredX = hoverX + style.eyedropperGapToCursor val preferredY = hoverY + style.eyedropperGapToCursor val desiredRect = - clampOverlayRect( + clampPreviewRect( rect = Rect(preferredX, preferredY, panelWidth, panelHeight), viewportWidth = viewportWidth, viewportHeight = viewportHeight, ) - val currentRect = eyedropperOverlayRect + val currentRect = eyedropperPreviewRect if (currentRect == null || currentRect.width != panelWidth || currentRect.height != panelHeight) { - eyedropperOverlayRect = desiredRect - eyedropperOverlayDrag.begin(mouseX = hoverX, mouseY = hoverY, rect = desiredRect) + eyedropperPreviewRect = desiredRect + eyedropperPreviewDrag.begin(mouseX = hoverX, mouseY = hoverY, rect = desiredRect) } val nextRect = - eyedropperOverlayDrag.update( + eyedropperPreviewDrag.update( mouseX = hoverX, mouseY = hoverY, viewportWidth = viewportWidth, viewportHeight = viewportHeight, - clamp = ::clampOverlayRect, + clamp = ::clampPreviewRect, ) - eyedropperOverlayRect = nextRect + eyedropperPreviewRect = nextRect val panelX = nextRect.x val panelY = nextRect.y @@ -681,8 +681,8 @@ class ColorPickerController( fallbackColor = state.color.toArgbInt(), ) out += RenderCommand.DrawRect(panelX + 2, panelY + 2, panelWidth, panelHeight, style.panelShadowColor) - out += RenderCommand.DrawRect(panelX, panelY, panelWidth, panelHeight, style.eyedropperOverlayBackgroundColor) - drawBorder(out, Rect(panelX, panelY, panelWidth, panelHeight), style.eyedropperOverlayBorderColor) + out += RenderCommand.DrawRect(panelX, panelY, panelWidth, panelHeight, style.eyedropperPreviewBackgroundColor) + drawBorder(out, Rect(panelX, panelY, panelWidth, panelHeight), style.eyedropperPreviewBorderColor) out += RenderCommand.DrawCapturedScreenRegion( x = magnifierX, @@ -690,14 +690,14 @@ class ColorPickerController( width = magnifierContentSize, height = magnifierContentSize, ) - if (style.eyedropperGridOverlayEnabled) { - drawEyedropperGridOverlay( + if (style.eyedropperGridEnabled) { + drawEyedropperGrid( out = out, rect = Rect(magnifierX, magnifierY, magnifierContentSize, magnifierContentSize), columns = gridSize, rows = gridSize, cellSize = cell, - color = style.eyedropperGridOverlayColor, + color = style.eyedropperGridColor, ) } drawBorder( @@ -746,10 +746,10 @@ class ColorPickerController( ) } - internal fun resolveEyedropperOverlayModel( + internal fun resolveEyedropperPreviewModel( viewportWidth: Int, viewportHeight: Int, - ): ColorPickerEyedropperOverlayModel? { + ): ColorPickerEyedropperPreviewModel? { if (!eyedropperActive) return null if (hoverX == Int.MIN_VALUE || hoverY == Int.MIN_VALUE) return null @@ -766,25 +766,25 @@ class ColorPickerController( val preferredX = hoverX + style.eyedropperGapToCursor val preferredY = hoverY + style.eyedropperGapToCursor val desiredRect = - clampOverlayRect( + clampPreviewRect( rect = Rect(preferredX, preferredY, panelWidth, panelHeight), viewportWidth = viewportWidth, viewportHeight = viewportHeight, ) - val currentRect = eyedropperOverlayRect + val currentRect = eyedropperPreviewRect if (currentRect == null || currentRect.width != panelWidth || currentRect.height != panelHeight) { - eyedropperOverlayRect = desiredRect - eyedropperOverlayDrag.begin(mouseX = hoverX, mouseY = hoverY, rect = desiredRect) + eyedropperPreviewRect = desiredRect + eyedropperPreviewDrag.begin(mouseX = hoverX, mouseY = hoverY, rect = desiredRect) } val nextRect = - eyedropperOverlayDrag.update( + eyedropperPreviewDrag.update( mouseX = hoverX, mouseY = hoverY, viewportWidth = viewportWidth, viewportHeight = viewportHeight, - clamp = ::clampOverlayRect, + clamp = ::clampPreviewRect, ) - eyedropperOverlayRect = nextRect + eyedropperPreviewRect = nextRect val magnifierRect = Rect( @@ -811,7 +811,7 @@ class ColorPickerController( "Mode: ${state.mode.name}" } val valueText = ColorTextCodec.format(state.color, state.mode, state.alphaEnabled, state.rgbOrder) - return ColorPickerEyedropperOverlayModel( + return ColorPickerEyedropperPreviewModel( panelRect = nextRect, magnifierRect = magnifierRect, captureSourceRect = @@ -1312,7 +1312,7 @@ class ColorPickerController( return if ((raw and 1) == 0) raw + 1 else raw } - private fun clampOverlayRect(rect: Rect, viewportWidth: Int, viewportHeight: Int): Rect { + private fun clampPreviewRect(rect: Rect, viewportWidth: Int, viewportHeight: Int): Rect { val safeViewportW = viewportWidth.coerceAtLeast(rect.width + 4) val safeViewportH = viewportHeight.coerceAtLeast(rect.height + 4) val minX = 2 @@ -1536,7 +1536,7 @@ class ColorPickerController( out += RenderCommand.DrawRect(x - 1, rect.y - 1, 3, rect.height + 2, style.thumbOutlineColor) } - private fun drawEyedropperGridOverlay( + private fun drawEyedropperGrid( out: MutableList, rect: Rect, columns: Int, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngine.kt index 6331193..1ee8a88 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngine.kt @@ -4,9 +4,9 @@ import org.dreamfinity.dsgl.core.colorpicker.internal.ColorPickerDebugCounters import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.DomainPortalServices -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope import org.dreamfinity.dsgl.core.popup.FloatingPaneDragModel +import org.dreamfinity.dsgl.core.portal.DomainPortalServices +import org.dreamfinity.dsgl.core.portal.ScreenDomainId import org.dreamfinity.dsgl.core.render.RenderCommand interface ColorPickerPopupPortalService { @@ -23,7 +23,7 @@ interface ColorPickerPopupPortalService { data class ColorPickerPopupRequest( val owner: Any, - val ownerScope: OverlayOwnerScope = OverlayOwnerScope.Application, + val ownerDomain: ScreenDomainId = ScreenDomainId.Application, val anchorRect: Rect, val title: String = "Color Picker", val state: ColorPickerState, @@ -95,7 +95,7 @@ class ColorPickerPopupEngine : ColorPickerPopupPortalService { val initialY = rememberedPanel?.y ?: request.anchorRect.y val initialRect = Rect(initialX, initialY, request.width.coerceAtLeast(220), 1) val initialBody = Rect(initialRect.x + panelPadding, initialRect.y + headerHeight + panelPadding, 1, 1) - ColorPickerDebugCounters.onBuildLayoutCall(request.ownerScope == OverlayOwnerScope.System) + ColorPickerDebugCounters.onBuildLayoutCall(request.ownerDomain == ScreenDomainId.System) val initialLayout = controller.buildLayout(initialBody) val state = PopupState( @@ -215,10 +215,10 @@ class ColorPickerPopupEngine : ColorPickerPopupPortalService { return current.request.style } - internal fun debugOwnerScope(owner: Any): OverlayOwnerScope? { + internal fun debugOwnerDomain(owner: Any): ScreenDomainId? { val current = popup ?: return null if (current.owner != owner) return null - return current.request.ownerScope + return current.request.ownerDomain } internal fun debugController(owner: Any): ColorPickerController? { @@ -235,7 +235,7 @@ class ColorPickerPopupEngine : ColorPickerPopupPortalService { internal fun debugActivePanelRect(): Rect? = popup?.panelRect - internal fun debugActiveOwnerScope(): OverlayOwnerScope? = popup?.request?.ownerScope + internal fun debugActiveOwnerDomain(): ScreenDomainId? = popup?.request?.ownerDomain internal fun debugIsDraggingPopup(): Boolean = popup?.dragModel?.dragging == true @@ -293,7 +293,7 @@ class ColorPickerPopupEngine : ColorPickerPopupPortalService { fun hasActiveEyedropper(): Boolean = popup?.controller?.isEyedropperActive() == true - fun appendOverlayCommands(out: MutableList) { + fun appendPortalCommands(out: MutableList) { val current = popup ?: return refreshLayout(current) val panel = current.panelRect @@ -358,9 +358,9 @@ class ColorPickerPopupEngine : ColorPickerPopupPortalService { current.bodyRect.width, current.bodyRect.height, ) - appendOverlayBodyCommands(out) + appendPortalBodyCommands(out) out += RenderCommand.PopClip - appendEyedropperOverlayCommands( + appendEyedropperPortalCommands( viewportWidth = viewportWidth.coerceAtLeast(1), viewportHeight = viewportHeight.coerceAtLeast(1), out = out, @@ -368,18 +368,18 @@ class ColorPickerPopupEngine : ColorPickerPopupPortalService { out += RenderCommand.PopClip } - internal fun appendOverlayBodyCommands(out: MutableList) { + internal fun appendPortalBodyCommands(out: MutableList) { val current = popup ?: return current.controller.appendCommands(current.layout, out) } - internal fun appendEyedropperOverlayCommands( + internal fun appendEyedropperPortalCommands( viewportWidth: Int = this.viewportWidth.coerceAtLeast(1), viewportHeight: Int = this.viewportHeight.coerceAtLeast(1), out: MutableList, ) { val current = popup ?: return - current.controller.appendEyedropperOverlay( + current.controller.appendEyedropperPreview( viewportWidth = viewportWidth.coerceAtLeast(1), viewportHeight = viewportHeight.coerceAtLeast(1), out = out, @@ -427,7 +427,7 @@ class ColorPickerPopupEngine : ColorPickerPopupPortalService { fun shouldRouteSystemInputSlotMouseDownToDom(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { val current = popup ?: return false - if (current.request.ownerScope != OverlayOwnerScope.System) return false + if (current.request.ownerDomain != ScreenDomainId.System) return false if (button != MouseButton.LEFT) return false if (current.controller.isEyedropperActive()) return false val hit = @@ -439,7 +439,7 @@ class ColorPickerPopupEngine : ColorPickerPopupPortalService { fun shouldRouteSystemBodyIntentMouseDownToDom(mouseX: Int, mouseY: Int, button: MouseButton): Boolean { val current = popup ?: return false - if (current.request.ownerScope != OverlayOwnerScope.System) return false + if (current.request.ownerDomain != ScreenDomainId.System) return false if (button != MouseButton.LEFT) return false if (current.controller.isEyedropperActive()) return false refreshLayout(current) @@ -470,7 +470,7 @@ class ColorPickerPopupEngine : ColorPickerPopupPortalService { fun focusSystemInputSlotForDomEditing(mouseX: Int, mouseY: Int, focusInputByIndex: (Int) -> Boolean): Boolean { val current = popup ?: return false - if (current.request.ownerScope != OverlayOwnerScope.System) return false + if (current.request.ownerDomain != ScreenDomainId.System) return false val slotIndex = current.layout.inputSlots .indexOfFirst { slot -> slot.inputRect.contains(mouseX, mouseY) } @@ -563,7 +563,7 @@ class ColorPickerPopupEngine : ColorPickerPopupPortalService { } private fun refreshLayout(state: PopupState) { - ColorPickerDebugCounters.onRefreshLayoutCall(state.request.ownerScope == OverlayOwnerScope.System) + ColorPickerDebugCounters.onRefreshLayoutCall(state.request.ownerDomain == ScreenDomainId.System) ensureLayoutUpToDate(state) } @@ -576,7 +576,7 @@ class ColorPickerPopupEngine : ColorPickerPopupPortalService { } private fun rebuildLayout(state: PopupState) { - ColorPickerDebugCounters.onBuildLayoutCall(state.request.ownerScope == OverlayOwnerScope.System) + ColorPickerDebugCounters.onBuildLayoutCall(state.request.ownerDomain == ScreenDomainId.System) state.layout = state.controller.buildLayout(state.bodyRect) state.layoutDirtyKey = resolveLayoutDirtyKey(state) } @@ -623,7 +623,7 @@ class ColorPickerPopupEngine : ColorPickerPopupPortalService { val removeMs = nanosToMsString(snapshot.recentSwatchRemoveNanos) println( "dsgl.colorPicker.debugCounters " + - "ownerScope=${current.request.ownerScope} " + + "ownerDomain=${current.request.ownerDomain} " + "recentComposeCalls=${snapshot.recentSwatchGridComposeCalls} " + "recentCreated=${snapshot.recentSwatchNodesCreated} " + "recentRemoved=${snapshot.recentSwatchNodesRemoved} " + @@ -649,7 +649,7 @@ class ColorPickerPopupManager( private val ownerToken: Any = Any(), ) { fun open( - ownerScope: OverlayOwnerScope = OverlayOwnerScope.Application, + ownerDomain: ScreenDomainId = ScreenDomainId.Application, anchorRect: Rect, title: String, state: ColorPickerState, @@ -665,7 +665,7 @@ class ColorPickerPopupManager( portalService.open( ColorPickerPopupRequest( owner = ownerToken, - ownerScope = ownerScope, + ownerDomain = ownerDomain, anchorRect = anchorRect, title = title, state = state, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPortalController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPortalController.kt index 4582464..7726dd3 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPortalController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPortalController.kt @@ -4,19 +4,19 @@ import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope -import org.dreamfinity.dsgl.core.overlay.PortalDismissPolicy -import org.dreamfinity.dsgl.core.overlay.PortalEntry -import org.dreamfinity.dsgl.core.overlay.PortalEntryBounds -import org.dreamfinity.dsgl.core.overlay.PortalEntryId -import org.dreamfinity.dsgl.core.overlay.PortalEntryOrder -import org.dreamfinity.dsgl.core.overlay.PortalEntryPlacement -import org.dreamfinity.dsgl.core.overlay.PortalEntryState -import org.dreamfinity.dsgl.core.overlay.PortalFocusPolicy -import org.dreamfinity.dsgl.core.overlay.PortalHost -import org.dreamfinity.dsgl.core.overlay.PortalInputPolicy -import org.dreamfinity.dsgl.core.overlay.PortalPointerDispatch -import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces +import org.dreamfinity.dsgl.core.portal.PortalDismissPolicy +import org.dreamfinity.dsgl.core.portal.PortalEntry +import org.dreamfinity.dsgl.core.portal.PortalEntryBounds +import org.dreamfinity.dsgl.core.portal.PortalEntryId +import org.dreamfinity.dsgl.core.portal.PortalEntryOrder +import org.dreamfinity.dsgl.core.portal.PortalEntryPlacement +import org.dreamfinity.dsgl.core.portal.PortalEntryState +import org.dreamfinity.dsgl.core.portal.PortalFocusPolicy +import org.dreamfinity.dsgl.core.portal.PortalHost +import org.dreamfinity.dsgl.core.portal.PortalInputPolicy +import org.dreamfinity.dsgl.core.portal.PortalPointerDispatch +import org.dreamfinity.dsgl.core.portal.ScreenDomainId +import org.dreamfinity.dsgl.core.portal.ScreenDomainSurfaces import org.dreamfinity.dsgl.core.render.RenderCommand internal class ColorPickerPortalController( @@ -129,7 +129,7 @@ private class ColorPickerPortalEntry( return emptyList() } val commands = ArrayList() - engine.appendOverlayCommands(commands) + engine.appendPortalCommands(commands) syncActivePlacement() return commands } @@ -195,5 +195,5 @@ private class ColorPickerPortalEntry( } val isApplicationPopupOpen: Boolean - get() = engine.isOpen() && engine.debugActiveOwnerScope() == OverlayOwnerScope.Application + get() = engine.isOpen() && engine.debugActiveOwnerDomain() == ScreenDomainId.Application } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerStyle.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerStyle.kt index 74da8ec..ab1f37e 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerStyle.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerStyle.kt @@ -40,13 +40,13 @@ data class ColorPickerStyle( val eyedropperGapToCursor: Int = 14, val eyedropperTooltipWidth: Int = 228, val eyedropperTooltipHeight: Int = 52, - val eyedropperOverlayBackgroundColor: Int = 0xF01A222C.toInt(), - val eyedropperOverlayBorderColor: Int = 0xFF7F96AD.toInt(), + val eyedropperPreviewBackgroundColor: Int = 0xF01A222C.toInt(), + val eyedropperPreviewBorderColor: Int = 0xFF7F96AD.toInt(), val eyedropperCenterBorderColor: Int = 0xFFFFFFFF.toInt(), val rowGap: Int = 6, val recentCellSize: Int = 14, val recentCellGap: Int = 2, val minWidth: Int = 280, - val eyedropperGridOverlayEnabled: Boolean = true, - val eyedropperGridOverlayColor: Int = 0x22FFFFff.toInt(), + val eyedropperGridEnabled: Boolean = true, + val eyedropperGridColor: Int = 0x22FFFFFF, ) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/ColorPickerPopupMount.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/ColorPickerPopupMount.kt index 01322b5..b5a6a67 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/ColorPickerPopupMount.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/ColorPickerPopupMount.kt @@ -1,31 +1,31 @@ package org.dreamfinity.dsgl.core.colorpicker.internal import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupEngine -import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanel -import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelDragSession -import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelState +import org.dreamfinity.dsgl.core.portal.panel.FloatingPanel +import org.dreamfinity.dsgl.core.portal.panel.FloatingPanelDragSession +import org.dreamfinity.dsgl.core.portal.panel.FloatingPanelState internal class ColorPickerPopupMount( ownerId: Any, - panelState: OverlayPanelState, - dragSession: OverlayPanelDragSession, + panelState: FloatingPanelState, + dragSession: FloatingPanelDragSession, initialOwnerToken: Any = Any(), ) { val ownerToken: Any = initialOwnerToken val popupEngine: ColorPickerPopupEngine = ColorPickerPopupEngine() - val overlayPanel: OverlayPanel = - OverlayPanel( + val floatingPanel: FloatingPanel = + FloatingPanel( ownerId = ownerId, panelState = panelState, dragSession = dragSession, ) - val node: ColorPickerPopupOverlayNode = - ColorPickerPopupOverlayNode( + val node: ColorPickerPopupPortalNode = + ColorPickerPopupPortalNode( popupEngine = popupEngine, - overlayPanel = overlayPanel, + floatingPanel = floatingPanel, ) - val transientNode: ColorPickerTransientOverlayNode = - ColorPickerTransientOverlayNode(popupEngine = popupEngine) + val transientNode: ColorPickerTransientPortalNode = + ColorPickerTransientPortalNode(popupEngine = popupEngine) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerCustomSurfaceNodes.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerCustomSurfaceNodes.kt index 7fd0512..8342439 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerCustomSurfaceNodes.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerCustomSurfaceNodes.kt @@ -111,9 +111,9 @@ internal class EyedropperMagnifierDrawNode( y = bounds.y, width = bounds.width, height = bounds.height, - gridOverlay = + grid = if (gridEnabled) { - RenderCommand.CapturedGridOverlay( + RenderCommand.CapturedGrid( columns = columns, rows = rows, magnification = magnification, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPanelManager.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPanelManager.kt index 49f12be..4138d75 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPanelManager.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPanelManager.kt @@ -5,7 +5,7 @@ import org.dreamfinity.dsgl.core.colorpicker.ColorPickerState import org.dreamfinity.dsgl.core.colorpicker.ColorPickerStyle import org.dreamfinity.dsgl.core.colorpicker.RgbaColor import org.dreamfinity.dsgl.core.dom.layout.Rect -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope +import org.dreamfinity.dsgl.core.portal.ScreenDomainId interface SystemColorPickerPortalService { fun open( @@ -44,7 +44,7 @@ internal class SystemColorPickerPanelManager( onClose: (() -> Unit)?, ) { delegate.open( - ownerScope = OverlayOwnerScope.System, + ownerDomain = ScreenDomainId.System, anchorRect = anchorRect, title = title, state = state, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt index 84c88c9..91f791e 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPopupBodyNode.kt @@ -790,16 +790,16 @@ internal class SystemColorPickerPopupBodyNode( } } -internal class SystemColorPickerTransientOverlayNode( +internal class SystemColorPickerTransientPortalNode( private val popupEngine: ColorPickerPopupEngine, key: Any? = "dsgl-system-color-picker-native-transient", ) : DOMNode(key) { override val styleType: String = "dsgl-system-color-picker-native-transient" - private val modeDropdownOverlayNode: SystemColorPickerModeDropdownOverlayNode = - SystemColorPickerModeDropdownOverlayNode(popupEngine).applyParent(this) - private val eyedropperOverlayNode: SystemColorPickerEyedropperOverlayNode = - SystemColorPickerEyedropperOverlayNode(popupEngine).applyParent(this) + private val modeDropdownPortalNode: SystemColorPickerModeDropdownPortalNode = + SystemColorPickerModeDropdownPortalNode(popupEngine).applyParent(this) + private val eyedropperPreviewNode: SystemColorPickerEyedropperPreviewNode = + SystemColorPickerEyedropperPreviewNode(popupEngine).applyParent(this) override fun measure(ctx: UiMeasureContext): Size = Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) @@ -812,8 +812,8 @@ internal class SystemColorPickerTransientOverlayNode( height: Int, ) { bounds = Rect(x, y, width, height) - modeDropdownOverlayNode.render(ctx, x, y, width, height) - eyedropperOverlayNode.render(ctx, x, y, width, height) + modeDropdownPortalNode.render(ctx, x, y, width, height) + eyedropperPreviewNode.render(ctx, x, y, width, height) } fun invalidateColorState() { @@ -821,11 +821,11 @@ internal class SystemColorPickerTransientOverlayNode( } } -internal class SystemColorPickerModeDropdownOverlayNode( +internal class SystemColorPickerModeDropdownPortalNode( private val popupEngine: ColorPickerPopupEngine, - key: Any? = "dsgl-system-color-picker-native-mode-dropdown-overlay", + key: Any? = "dsgl-system-color-picker-native-mode-dropdown-portal", ) : DOMNode(key) { - override val styleType: String = "dsgl-system-color-picker-native-mode-dropdown-overlay" + override val styleType: String = "dsgl-system-color-picker-native-mode-dropdown-portal" private val scope = UiScope(this) private val popupBackgroundNode: ContainerNode = @@ -1019,11 +1019,11 @@ internal class SystemColorPickerModeDropdownOverlayNode( } } -internal class SystemColorPickerEyedropperOverlayNode( +internal class SystemColorPickerEyedropperPreviewNode( private val popupEngine: ColorPickerPopupEngine, - key: Any? = "dsgl-system-color-picker-native-eyedropper-overlay", + key: Any? = "dsgl-system-color-picker-native-eyedropper-preview", ) : DOMNode(key) { - override val styleType: String = "dsgl-system-color-picker-native-eyedropper-overlay" + override val styleType: String = "dsgl-system-color-picker-native-eyedropper-preview" private val scope = UiScope(this) private val captureNode: EyedropperCaptureNode = @@ -1052,18 +1052,18 @@ internal class SystemColorPickerEyedropperOverlayNode( this.key = "dsgl-system-color-picker-eyedropper-swatch" }) private val modeTextNode: TextNode = - createOverlayTextNode( + createPreviewTextNode( key = "dsgl-system-color-picker-eyedropper-mode", text = "", ) private val valueTextNode: TextNode = - createOverlayTextNode( + createPreviewTextNode( key = "dsgl-system-color-picker-eyedropper-value", text = "", ) private data class EyedropperRenderState( - val model: ColorPickerEyedropperOverlayModel, + val model: ColorPickerEyedropperPreviewModel, val style: ColorPickerStyle, val color: RgbaColor, ) @@ -1092,13 +1092,13 @@ internal class SystemColorPickerEyedropperOverlayNode( syncVisuals(renderState) bindVisualNodes(renderState) - renderOverlayNodes(ctx, renderState) + renderPreviewNodes(ctx, renderState) } private fun resolveRenderState(): EyedropperRenderState? { val controller = popupEngine.debugActiveController() ?: return null val model = - controller.resolveEyedropperOverlayModel( + controller.resolveEyedropperPreviewModel( viewportWidth = bounds.width.coerceAtLeast(1), viewportHeight = bounds.height.coerceAtLeast(1), ) ?: return null @@ -1111,7 +1111,7 @@ internal class SystemColorPickerEyedropperOverlayNode( } private fun syncVisuals(state: EyedropperRenderState) { - syncOverlayText(state.model.modeText, state.model.valueText) + syncPreviewText(state.model.modeText, state.model.valueText) syncContainerVisual( node = shadowNode, backgroundColor = state.style.panelShadowColor, @@ -1119,16 +1119,16 @@ internal class SystemColorPickerEyedropperOverlayNode( ) syncContainerVisual( node = panelNode, - backgroundColor = state.style.eyedropperOverlayBackgroundColor, - border = Border.all(1, state.style.eyedropperOverlayBorderColor), + backgroundColor = state.style.eyedropperPreviewBackgroundColor, + border = Border.all(1, state.style.eyedropperPreviewBorderColor), ) syncContainerVisual( node = centerNode, backgroundColor = null, border = Border.all(1, state.style.eyedropperCenterBorderColor), ) - syncOverlayTextVisual(modeTextNode, state.style.mutedTextColor, state.style.fontSize) - syncOverlayTextVisual(valueTextNode, state.style.textColor, state.style.fontSize) + syncPreviewTextVisual(modeTextNode, state.style.mutedTextColor, state.style.fontSize) + syncPreviewTextVisual(valueTextNode, state.style.textColor, state.style.fontSize) } private fun bindVisualNodes(state: EyedropperRenderState) { @@ -1146,12 +1146,12 @@ internal class SystemColorPickerEyedropperOverlayNode( state.model.captureSourceRect.width .coerceAtLeast(1) ).coerceAtLeast(1), - gridEnabled = state.style.eyedropperGridOverlayEnabled, - gridColor = state.style.eyedropperGridOverlayColor, + gridEnabled = state.style.eyedropperGridEnabled, + gridColor = state.style.eyedropperGridColor, ) } - private fun renderOverlayNodes(ctx: UiMeasureContext, state: EyedropperRenderState) { + private fun renderPreviewNodes(ctx: UiMeasureContext, state: EyedropperRenderState) { val shadowRect = Rect( x = state.model.panelRect.x + 2, @@ -1197,7 +1197,7 @@ internal class SystemColorPickerEyedropperOverlayNode( ) } - private fun syncOverlayText(modeText: String, valueText: String) { + private fun syncPreviewText(modeText: String, valueText: String) { if (modeTextNode.text != modeText) { modeTextNode.setText(modeText) } @@ -1206,7 +1206,7 @@ internal class SystemColorPickerEyedropperOverlayNode( } } - private fun syncOverlayTextVisual(node: TextNode, color: Int, fontSize: Int) { + private fun syncPreviewTextVisual(node: TextNode, color: Int, fontSize: Int) { var changed = false if (node.color != color) { node.color = color @@ -1236,7 +1236,7 @@ internal class SystemColorPickerEyedropperOverlayNode( } } - private fun createOverlayTextNode(key: Any, text: String): TextNode = + private fun createPreviewTextNode(key: Any, text: String): TextNode = TextNode(TextSource.Static(text), key = key) .apply { textWrap = TextWrap.NoWrap @@ -1262,7 +1262,7 @@ internal class SystemColorPickerEyedropperOverlayNode( internal typealias ColorPickerPopupBodyNode = SystemColorPickerPopupBodyNode -internal typealias ColorPickerTransientOverlayNode = SystemColorPickerTransientOverlayNode +internal typealias ColorPickerTransientPortalNode = SystemColorPickerTransientPortalNode private fun requestRenderCommandsInvalidationTracked(node: DOMNode) { ColorPickerDebugCounters.onRenderInvalidationCall() diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerOverlayNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPortalNode.kt similarity index 84% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerOverlayNode.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPortalNode.kt index 36238af..fd6376c 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerOverlayNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/colorpicker/internal/SystemColorPickerPortalNode.kt @@ -6,12 +6,12 @@ import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.Size import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext -import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanel +import org.dreamfinity.dsgl.core.portal.panel.FloatingPanel import org.dreamfinity.dsgl.core.style.Display -internal class ColorPickerPopupOverlayNode( +internal class ColorPickerPopupPortalNode( private val popupEngine: ColorPickerPopupEngine, - private val overlayPanel: OverlayPanel, + private val floatingPanel: FloatingPanel, key: Any? = "dsgl-system-color-picker", ) : DOMNode(key) { override val styleType: String = "dsgl-system-color-picker" @@ -20,9 +20,9 @@ internal class ColorPickerPopupOverlayNode( private var cursorY: Int = 0 private var domInputRoutingReady: Boolean = false - private val panelNode: DOMNode = overlayPanel.node().applyParent(this) + private val panelNode: DOMNode = floatingPanel.node().applyParent(this) private val bodyNode: ColorPickerPopupBodyNode = - ColorPickerPopupBodyNode(popupEngine = popupEngine).also(overlayPanel::setBodyContent) + ColorPickerPopupBodyNode(popupEngine = popupEngine).also(floatingPanel::setBodyContent) fun updateCursor(mouseX: Int, mouseY: Int) { cursorX = mouseX @@ -59,11 +59,11 @@ internal class ColorPickerPopupOverlayNode( popupEngine.onFrame(width, height) popupEngine.onCursorPosition(cursorX, cursorY) - val panelRect = overlayPanel.panelRect() + val panelRect = floatingPanel.panelRect() bodyNode.display = if (panelRect == null) Display.None else Display.Block domInputRoutingReady = bodyNode.display == Display.Block panelNode.render(ctx, x, y, width, height) } } -internal typealias SystemColorPickerOverlayNode = ColorPickerPopupOverlayNode +internal typealias SystemColorPickerPortalNode = ColorPickerPopupPortalNode diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt index 3727616..f963eee 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/components/modal/internal/ModalPortalController.kt @@ -9,23 +9,23 @@ import org.dreamfinity.dsgl.core.event.EventBus import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.PortalBackdropPolicy -import org.dreamfinity.dsgl.core.overlay.PortalDismissPolicy -import org.dreamfinity.dsgl.core.overlay.PortalEntry -import org.dreamfinity.dsgl.core.overlay.PortalEntryBounds -import org.dreamfinity.dsgl.core.overlay.PortalEntryId -import org.dreamfinity.dsgl.core.overlay.PortalEntryOrder -import org.dreamfinity.dsgl.core.overlay.PortalEntryPlacement -import org.dreamfinity.dsgl.core.overlay.PortalEntryState -import org.dreamfinity.dsgl.core.overlay.PortalFocusPolicy -import org.dreamfinity.dsgl.core.overlay.PortalHost -import org.dreamfinity.dsgl.core.overlay.PortalInputPolicy -import org.dreamfinity.dsgl.core.overlay.PortalInsidePointerPolicy -import org.dreamfinity.dsgl.core.overlay.PortalPointerContainmentPolicy -import org.dreamfinity.dsgl.core.overlay.PortalPointerRegion -import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces -import org.dreamfinity.dsgl.core.overlay.evaluateOutsidePointerDown -import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter +import org.dreamfinity.dsgl.core.portal.PortalBackdropPolicy +import org.dreamfinity.dsgl.core.portal.PortalDismissPolicy +import org.dreamfinity.dsgl.core.portal.PortalEntry +import org.dreamfinity.dsgl.core.portal.PortalEntryBounds +import org.dreamfinity.dsgl.core.portal.PortalEntryId +import org.dreamfinity.dsgl.core.portal.PortalEntryOrder +import org.dreamfinity.dsgl.core.portal.PortalEntryPlacement +import org.dreamfinity.dsgl.core.portal.PortalEntryState +import org.dreamfinity.dsgl.core.portal.PortalFocusPolicy +import org.dreamfinity.dsgl.core.portal.PortalHost +import org.dreamfinity.dsgl.core.portal.PortalInputPolicy +import org.dreamfinity.dsgl.core.portal.PortalInsidePointerPolicy +import org.dreamfinity.dsgl.core.portal.PortalPointerContainmentPolicy +import org.dreamfinity.dsgl.core.portal.PortalPointerRegion +import org.dreamfinity.dsgl.core.portal.ScreenDomainSurfaces +import org.dreamfinity.dsgl.core.portal.evaluateOutsidePointerDown +import org.dreamfinity.dsgl.core.portal.input.SurfaceDomInputRouter @Suppress("TooManyFunctions") internal class ModalPortalController { @@ -265,8 +265,8 @@ private class ModalPortalEntry( templateRoot: ModalPortalRootNode, ) : PortalEntry { private var tree: DomTree = DomTree(templateRoot) - private val domInputRouter: LayerDomInputRouter = - LayerDomInputRouter( + private val domInputRouter: SurfaceDomInputRouter = + SurfaceDomInputRouter( rootProvider = { root }, ) private var topMostModal: ModalSpec? = null diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngine.kt index a1e6691..2bc8ac8 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngine.kt @@ -157,7 +157,7 @@ class ContextMenuEngine( ensureLayout() } - fun appendOverlayCommands( + fun appendPortalCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/DebugDomainHosts.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/DebugDomainHosts.kt index 938f396..9cf5860 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/DebugDomainHosts.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/DebugDomainHosts.kt @@ -15,13 +15,13 @@ import org.dreamfinity.dsgl.core.dsl.button import org.dreamfinity.dsgl.core.dsl.div import org.dreamfinity.dsgl.core.dsl.text import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.DomainSurfaceHost -import org.dreamfinity.dsgl.core.overlay.PortalEntry -import org.dreamfinity.dsgl.core.overlay.PortalFrameContext -import org.dreamfinity.dsgl.core.overlay.PortalHost -import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurface -import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces -import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter +import org.dreamfinity.dsgl.core.portal.DomainSurfaceHost +import org.dreamfinity.dsgl.core.portal.PortalEntry +import org.dreamfinity.dsgl.core.portal.PortalFrameContext +import org.dreamfinity.dsgl.core.portal.PortalHost +import org.dreamfinity.dsgl.core.portal.ScreenDomainSurface +import org.dreamfinity.dsgl.core.portal.ScreenDomainSurfaces +import org.dreamfinity.dsgl.core.portal.input.SurfaceDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.StyleApplicationScope @@ -64,27 +64,27 @@ private const val COLOR_TOGGLE_BORDER = 0xFF9AB3C9.toInt() internal data class DebugDomainControlLayout( val panelRect: Rect, - val appOverlayRenderRect: Rect, - val appOverlayTintRect: Rect, - val appOverlayInputRect: Rect, - val systemOverlayTintRect: Rect, - val systemOverlayRenderRect: Rect, - val systemOverlayInputRect: Rect, + val appPortalRenderRect: Rect, + val appPortalTintRect: Rect, + val appPortalInputRect: Rect, + val systemPortalTintRect: Rect, + val systemPortalRenderRect: Rect, + val systemPortalInputRect: Rect, val resetRect: Rect, ) private data class DebugDomainToggleSnapshot( - val applicationOverlayRenderEnabled: Boolean, - val applicationOverlayTintEnabled: Boolean, - val applicationOverlayInputEnabled: Boolean, - val systemOverlayRenderEnabled: Boolean, - val systemOverlayTintEnabled: Boolean, - val systemOverlayInputEnabled: Boolean, + val applicationPortalRenderEnabled: Boolean, + val applicationPortalTintEnabled: Boolean, + val applicationPortalInputEnabled: Boolean, + val systemPortalRenderEnabled: Boolean, + val systemPortalTintEnabled: Boolean, + val systemPortalInputEnabled: Boolean, ) @Suppress("TooManyFunctions") class DebugDomainRootHost( - private val state: OverlayLayerDebugState = OverlayLayerDebugState, + private val state: DomainSurfaceDebugState = DomainSurfaceDebugState, ) : DomainSurfaceHost { override val surface: ScreenDomainSurface = ScreenDomainSurfaces.DebugRoot @@ -97,7 +97,7 @@ class DebugDomainRootHost( root = rootNode, styleScope = StyleApplicationScope.Debug, ) - private val domInputRouter: LayerDomInputRouter = LayerDomInputRouter { rootNode } + private val domInputRouter: SurfaceDomInputRouter = SurfaceDomInputRouter { rootNode } private var lastToggleSnapshot: DebugDomainToggleSnapshot? = null @Suppress("UnusedParameter") @@ -169,12 +169,12 @@ class DebugDomainRootHost( var row = 0 return DebugDomainControlLayout( panelRect = panelRect, - appOverlayRenderRect = Rect(toggleX, firstY + ROW_STEP * row++, TOGGLE_WIDTH, TOGGLE_HEIGHT), - appOverlayTintRect = Rect(toggleX, firstY + ROW_STEP * row++, TOGGLE_WIDTH, TOGGLE_HEIGHT), - appOverlayInputRect = Rect(toggleX, firstY + ROW_STEP * row++, TOGGLE_WIDTH, TOGGLE_HEIGHT), - systemOverlayRenderRect = Rect(toggleX, firstY + ROW_STEP * row++, TOGGLE_WIDTH, TOGGLE_HEIGHT), - systemOverlayTintRect = Rect(toggleX, firstY + ROW_STEP * row++, TOGGLE_WIDTH, TOGGLE_HEIGHT), - systemOverlayInputRect = Rect(toggleX, firstY + ROW_STEP * row++, TOGGLE_WIDTH, TOGGLE_HEIGHT), + appPortalRenderRect = Rect(toggleX, firstY + ROW_STEP * row++, TOGGLE_WIDTH, TOGGLE_HEIGHT), + appPortalTintRect = Rect(toggleX, firstY + ROW_STEP * row++, TOGGLE_WIDTH, TOGGLE_HEIGHT), + appPortalInputRect = Rect(toggleX, firstY + ROW_STEP * row++, TOGGLE_WIDTH, TOGGLE_HEIGHT), + systemPortalRenderRect = Rect(toggleX, firstY + ROW_STEP * row++, TOGGLE_WIDTH, TOGGLE_HEIGHT), + systemPortalTintRect = Rect(toggleX, firstY + ROW_STEP * row++, TOGGLE_WIDTH, TOGGLE_HEIGHT), + systemPortalInputRect = Rect(toggleX, firstY + ROW_STEP * row++, TOGGLE_WIDTH, TOGGLE_HEIGHT), resetRect = Rect( x = panelRect.x + PANEL_HORIZONTAL_PADDING, @@ -238,18 +238,18 @@ class DebugDomainPortalHost : DomainSurfaceHost { get() = portalHost.entriesInPaintOrder().map { it.state.id.value } } -private fun OverlayLayerDebugSnapshot.toDebugToggleSnapshot(): DebugDomainToggleSnapshot = +private fun DomainSurfaceDebugSnapshot.toDebugToggleSnapshot(): DebugDomainToggleSnapshot = DebugDomainToggleSnapshot( - applicationOverlayRenderEnabled = applicationOverlayRenderEnabled, - applicationOverlayTintEnabled = applicationOverlayTintEnabled, - applicationOverlayInputEnabled = applicationOverlayInputEnabled, - systemOverlayRenderEnabled = systemOverlayRenderEnabled, - systemOverlayTintEnabled = systemOverlayTintEnabled, - systemOverlayInputEnabled = systemOverlayInputEnabled, + applicationPortalRenderEnabled = applicationPortalRenderEnabled, + applicationPortalTintEnabled = applicationPortalTintEnabled, + applicationPortalInputEnabled = applicationPortalInputEnabled, + systemPortalRenderEnabled = systemPortalRenderEnabled, + systemPortalTintEnabled = systemPortalTintEnabled, + systemPortalInputEnabled = systemPortalInputEnabled, ) private class DebugDomainRootNode( - private val state: OverlayLayerDebugState, + private val state: DomainSurfaceDebugState, key: Any? = "dsgl-debug-domain-root", ) : DOMNode(key) { override val styleType: String = "dsgl-debug-domain-root" @@ -289,27 +289,27 @@ private class DebugDomainRootNode( private val appRenderToggleNode: ButtonNode = toggleNode("dsgl-debug-domain-toggle-app-render") { - state.applicationOverlayRenderEnabled = !state.applicationOverlayRenderEnabled + state.applicationPortalRenderEnabled = !state.applicationPortalRenderEnabled } private val appTintToggleNode: ButtonNode = toggleNode("dsgl-debug-domain-toggle-app-tint") { - state.applicationOverlayTintEnabled = !state.applicationOverlayTintEnabled + state.applicationPortalTintEnabled = !state.applicationPortalTintEnabled } private val appInputToggleNode: ButtonNode = toggleNode("dsgl-debug-domain-toggle-app-input") { - state.applicationOverlayInputEnabled = !state.applicationOverlayInputEnabled + state.applicationPortalInputEnabled = !state.applicationPortalInputEnabled } private val systemRenderToggleNode: ButtonNode = toggleNode("dsgl-debug-domain-toggle-system-render") { - state.systemOverlayRenderEnabled = !state.systemOverlayRenderEnabled + state.systemPortalRenderEnabled = !state.systemPortalRenderEnabled } private val systemTintToggleNode: ButtonNode = toggleNode("dsgl-debug-domain-toggle-system-tint") { - state.systemOverlayTintEnabled = !state.systemOverlayTintEnabled + state.systemPortalTintEnabled = !state.systemPortalTintEnabled } private val systemInputToggleNode: ButtonNode = toggleNode("dsgl-debug-domain-toggle-system-input") { - state.systemOverlayInputEnabled = !state.systemOverlayInputEnabled + state.systemPortalInputEnabled = !state.systemPortalInputEnabled } private val resetButtonNode: ButtonNode = @@ -334,21 +334,21 @@ private class DebugDomainRootNode( }) private var layout: DebugDomainControlLayout? = null - private var snapshot: OverlayLayerDebugSnapshot = - OverlayLayerDebugSnapshot( - applicationOverlayRenderEnabled = true, - applicationOverlayTintEnabled = false, - applicationOverlayInputEnabled = true, - systemOverlayRenderEnabled = true, - systemOverlayTintEnabled = false, - systemOverlayInputEnabled = true, + private var snapshot: DomainSurfaceDebugSnapshot = + DomainSurfaceDebugSnapshot( + applicationPortalRenderEnabled = true, + applicationPortalTintEnabled = false, + applicationPortalInputEnabled = true, + systemPortalRenderEnabled = true, + systemPortalTintEnabled = false, + systemPortalInputEnabled = true, frameFps = 0, frameTimeMs = 0f, frameFpsWindow = 0, frameTimeWindowMs = 0f, ) - fun bind(layout: DebugDomainControlLayout, snapshot: OverlayLayerDebugSnapshot) { + fun bind(layout: DebugDomainControlLayout, snapshot: DomainSurfaceDebugSnapshot) { this.layout = layout this.snapshot = snapshot } @@ -390,22 +390,22 @@ private class DebugDomainRootNode( applyLabelStyle(systemTintLabelNode) applyLabelStyle(systemInputLabelNode) - configureToggle(appRenderToggleNode, snapshot.applicationOverlayRenderEnabled) - configureToggle(appTintToggleNode, snapshot.applicationOverlayTintEnabled) - configureToggle(appInputToggleNode, snapshot.applicationOverlayInputEnabled) - configureToggle(systemRenderToggleNode, snapshot.systemOverlayRenderEnabled) - configureToggle(systemTintToggleNode, snapshot.systemOverlayTintEnabled) - configureToggle(systemInputToggleNode, snapshot.systemOverlayInputEnabled) + configureToggle(appRenderToggleNode, snapshot.applicationPortalRenderEnabled) + configureToggle(appTintToggleNode, snapshot.applicationPortalTintEnabled) + configureToggle(appInputToggleNode, snapshot.applicationPortalInputEnabled) + configureToggle(systemRenderToggleNode, snapshot.systemPortalRenderEnabled) + configureToggle(systemTintToggleNode, snapshot.systemPortalTintEnabled) + configureToggle(systemInputToggleNode, snapshot.systemPortalInputEnabled) resetButtonNode.backgroundColor = COLOR_RESET_BACKGROUND resetButtonNode.border = Border.all(1, COLOR_RESET_BORDER) resetButtonNode.textColor = COLOR_TEXT_PRIMARY resetButtonNode.fontSize = BUTTON_FONT_SIZE - val rApp = if (snapshot.applicationOverlayRenderEnabled) "A1" else "A0" - val rSys = if (snapshot.systemOverlayRenderEnabled) "S1" else "S0" - val iApp = if (snapshot.applicationOverlayInputEnabled) "A1" else "A0" - val iSys = if (snapshot.systemOverlayInputEnabled) "S1" else "S0" + val rApp = if (snapshot.applicationPortalRenderEnabled) "A1" else "A0" + val rSys = if (snapshot.systemPortalRenderEnabled) "S1" else "S0" + val iApp = if (snapshot.applicationPortalInputEnabled) "A1" else "A0" + val iSys = if (snapshot.systemPortalInputEnabled) "S1" else "S0" val statusTextValue = "R:$rApp/$rSys I:$iApp/$iSys " + "FPS:${snapshot.frameFps} (${String.format(Locale.US, "%.1f", snapshot.frameTimeMs)}ms) " + @@ -427,18 +427,18 @@ private class DebugDomainRootNode( ), ) - renderToggleRow(ctx, panelRect, localLayout.appOverlayRenderRect, appRenderLabelNode, appRenderToggleNode) - renderToggleRow(ctx, panelRect, localLayout.appOverlayTintRect, appTintLabelNode, appTintToggleNode) - renderToggleRow(ctx, panelRect, localLayout.appOverlayInputRect, appInputLabelNode, appInputToggleNode) + renderToggleRow(ctx, panelRect, localLayout.appPortalRenderRect, appRenderLabelNode, appRenderToggleNode) + renderToggleRow(ctx, panelRect, localLayout.appPortalTintRect, appTintLabelNode, appTintToggleNode) + renderToggleRow(ctx, panelRect, localLayout.appPortalInputRect, appInputLabelNode, appInputToggleNode) renderToggleRow( ctx, panelRect, - localLayout.systemOverlayRenderRect, + localLayout.systemPortalRenderRect, systemRenderLabelNode, systemRenderToggleNode, ) - renderToggleRow(ctx, panelRect, localLayout.systemOverlayTintRect, systemTintLabelNode, systemTintToggleNode) - renderToggleRow(ctx, panelRect, localLayout.systemOverlayInputRect, systemInputLabelNode, systemInputToggleNode) + renderToggleRow(ctx, panelRect, localLayout.systemPortalTintRect, systemTintLabelNode, systemTintToggleNode) + renderToggleRow(ctx, panelRect, localLayout.systemPortalInputRect, systemInputLabelNode, systemInputToggleNode) renderNode(ctx, resetButtonNode, localLayout.resetRect) renderNode( diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayLayerDebugState.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/DomainSurfaceDebugState.kt similarity index 65% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayLayerDebugState.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/DomainSurfaceDebugState.kt index 9ffeb8f..b96edad 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/OverlayLayerDebugState.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/debug/DomainSurfaceDebugState.kt @@ -1,22 +1,22 @@ package org.dreamfinity.dsgl.core.debug -import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurface -import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces - -data class OverlayLayerDebugSnapshot( - val applicationOverlayRenderEnabled: Boolean, - val applicationOverlayTintEnabled: Boolean, - val applicationOverlayInputEnabled: Boolean, - val systemOverlayRenderEnabled: Boolean, - val systemOverlayTintEnabled: Boolean, - val systemOverlayInputEnabled: Boolean, +import org.dreamfinity.dsgl.core.portal.ScreenDomainSurface +import org.dreamfinity.dsgl.core.portal.ScreenDomainSurfaces + +data class DomainSurfaceDebugSnapshot( + val applicationPortalRenderEnabled: Boolean, + val applicationPortalTintEnabled: Boolean, + val applicationPortalInputEnabled: Boolean, + val systemPortalRenderEnabled: Boolean, + val systemPortalTintEnabled: Boolean, + val systemPortalInputEnabled: Boolean, val frameFps: Int, val frameTimeMs: Float, val frameFpsWindow: Int, val frameTimeWindowMs: Float, ) -object OverlayLayerDebugState { +object DomainSurfaceDebugState { private const val FRAME_TIMING_WINDOW_SIZE: Int = 60 private val frameTimeWindowSeconds: DoubleArray = DoubleArray(FRAME_TIMING_WINDOW_SIZE) private var frameTimeWindowWriteIndex: Int = 0 @@ -24,22 +24,22 @@ object OverlayLayerDebugState { private var frameTimeWindowSumSeconds: Double = 0.0 @Volatile - var applicationOverlayRenderEnabled: Boolean = true + var applicationPortalRenderEnabled: Boolean = true @Volatile - var applicationOverlayTintEnabled: Boolean = false + var applicationPortalTintEnabled: Boolean = false @Volatile - var applicationOverlayInputEnabled: Boolean = true + var applicationPortalInputEnabled: Boolean = true @Volatile - var systemOverlayRenderEnabled: Boolean = true + var systemPortalRenderEnabled: Boolean = true @Volatile - var systemOverlayTintEnabled: Boolean = false + var systemPortalTintEnabled: Boolean = false @Volatile - var systemOverlayInputEnabled: Boolean = true + var systemPortalInputEnabled: Boolean = true @Volatile var frameFps: Int = 0 @@ -56,37 +56,37 @@ object OverlayLayerDebugState { val controlsEnabled: Boolean get() { return controlsEnabledOverride ?: java.lang.Boolean - .getBoolean("dsgl.overlay.controls") + .getBoolean("dsgl.domain.controls") } fun isRenderEnabled(surface: ScreenDomainSurface): Boolean = when { - surface == ScreenDomainSurfaces.ApplicationPortal -> applicationOverlayRenderEnabled - surface == ScreenDomainSurfaces.SystemPortal -> systemOverlayRenderEnabled + surface == ScreenDomainSurfaces.ApplicationPortal -> applicationPortalRenderEnabled + surface == ScreenDomainSurfaces.SystemPortal -> systemPortalRenderEnabled else -> true } fun isTintEnabled(surface: ScreenDomainSurface): Boolean = when { - surface == ScreenDomainSurfaces.ApplicationPortal -> applicationOverlayTintEnabled - surface == ScreenDomainSurfaces.SystemPortal -> systemOverlayTintEnabled + surface == ScreenDomainSurfaces.ApplicationPortal -> applicationPortalTintEnabled + surface == ScreenDomainSurfaces.SystemPortal -> systemPortalTintEnabled else -> true } fun isInputEnabled(surface: ScreenDomainSurface): Boolean = when { - surface == ScreenDomainSurfaces.ApplicationPortal -> applicationOverlayInputEnabled - surface == ScreenDomainSurfaces.SystemPortal -> systemOverlayInputEnabled + surface == ScreenDomainSurfaces.ApplicationPortal -> applicationPortalInputEnabled + surface == ScreenDomainSurfaces.SystemPortal -> systemPortalInputEnabled else -> true } fun resetAll() { - applicationOverlayRenderEnabled = true - applicationOverlayTintEnabled = false - applicationOverlayInputEnabled = true - systemOverlayRenderEnabled = true - systemOverlayTintEnabled = false - systemOverlayInputEnabled = true + applicationPortalRenderEnabled = true + applicationPortalTintEnabled = false + applicationPortalInputEnabled = true + systemPortalRenderEnabled = true + systemPortalTintEnabled = false + systemPortalInputEnabled = true frameFps = 0 frameTimeMs = 0f frameFpsWindow = 0 @@ -131,14 +131,14 @@ object OverlayLayerDebugState { .coerceAtLeast(0) } - fun snapshot(): OverlayLayerDebugSnapshot = - OverlayLayerDebugSnapshot( - applicationOverlayRenderEnabled = applicationOverlayRenderEnabled, - applicationOverlayTintEnabled = applicationOverlayTintEnabled, - applicationOverlayInputEnabled = applicationOverlayInputEnabled, - systemOverlayRenderEnabled = systemOverlayRenderEnabled, - systemOverlayTintEnabled = systemOverlayTintEnabled, - systemOverlayInputEnabled = systemOverlayInputEnabled, + fun snapshot(): DomainSurfaceDebugSnapshot = + DomainSurfaceDebugSnapshot( + applicationPortalRenderEnabled = applicationPortalRenderEnabled, + applicationPortalTintEnabled = applicationPortalTintEnabled, + applicationPortalInputEnabled = applicationPortalInputEnabled, + systemPortalRenderEnabled = systemPortalRenderEnabled, + systemPortalTintEnabled = systemPortalTintEnabled, + systemPortalInputEnabled = systemPortalInputEnabled, frameFps = frameFps, frameTimeMs = frameTimeMs, frameFpsWindow = frameFpsWindow, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndInterfaces.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndInterfaces.kt index 8d2bc7e..072f026 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndInterfaces.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/DndInterfaces.kt @@ -25,10 +25,10 @@ interface DndMonitorRegistry { interface DndHitTester -interface DndOverlayRenderer { +interface DndPortalRenderer { fun appendPlaceholderCommands(out: MutableList) - fun appendOverlayCommands( + fun appendPortalCommands( root: DOMNode, ctx: UiMeasureContext, viewportWidth: Int, @@ -43,7 +43,7 @@ interface DndClock { interface DndEngine : DndMonitorRegistry, - DndOverlayRenderer, + DndPortalRenderer, DndClock { val isDragging: Boolean val isPointerCaptured: Boolean diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngine.kt index 8d98bbf..9352b11 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dnd/internal/DefaultDndEngine.kt @@ -242,7 +242,7 @@ object DefaultDndEngine : DndEngine { out.addAll(scope.buildCommands(bounds)) } - override fun appendOverlayCommands( + override fun appendPortalCommands( root: DOMNode, ctx: UiMeasureContext, viewportWidth: Int, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEvents.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEvents.kt index c9b8e19..c141241 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEvents.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/ContextMenuEvents.kt @@ -4,7 +4,7 @@ import org.dreamfinity.dsgl.core.contextmenu.ContextMenuModel import org.dreamfinity.dsgl.core.contextmenu.ContextMenuPortalService import org.dreamfinity.dsgl.core.contextmenu.ContextMenuTriggerScope import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.DomainPortalServices +import org.dreamfinity.dsgl.core.portal.DomainPortalServices fun DOMNode.onContextMenu( portalService: ContextMenuPortalService = DomainPortalServices.applicationContextMenuEngine, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerInlineNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerInlineNode.kt index 1497203..4a8f8b0 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerInlineNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerInlineNode.kt @@ -143,8 +143,8 @@ class ColorPickerInlineNode( fun wantsGlobalPointerInput(): Boolean = controller.isEyedropperActive() - fun appendEyedropperOverlayCommands(viewportWidth: Int, viewportHeight: Int, out: MutableList) { - controller.appendEyedropperOverlay( + fun appendEyedropperPortalCommands(viewportWidth: Int, viewportHeight: Int, out: MutableList) { + controller.appendEyedropperPreview( viewportWidth = viewportWidth.coerceAtLeast(1), viewportHeight = viewportHeight.coerceAtLeast(1), out = out, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerPopupPaneNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerPopupPaneNode.kt index 319deb7..1aa759d 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerPopupPaneNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/ColorPickerPopupPaneNode.kt @@ -7,7 +7,7 @@ import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.Size import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.* -import org.dreamfinity.dsgl.core.overlay.DomainPortalServices +import org.dreamfinity.dsgl.core.portal.DomainPortalServices import org.dreamfinity.dsgl.core.render.RenderCommand class ColorPickerPopupPaneNode( diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt index dcb8e70..4efa121 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/elements/SelectNode.kt @@ -7,8 +7,8 @@ import org.dreamfinity.dsgl.core.dom.layout.Insets import org.dreamfinity.dsgl.core.dom.layout.Size import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.* -import org.dreamfinity.dsgl.core.overlay.DomainPortalServices -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope +import org.dreamfinity.dsgl.core.portal.DomainPortalServices +import org.dreamfinity.dsgl.core.portal.ScreenDomainId import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.select.SelectEntry import org.dreamfinity.dsgl.core.select.SelectModel @@ -20,7 +20,7 @@ class SelectNode( value: String? = null, defaultValue: String? = null, closeOnSelect: Boolean = true, - ownerScope: OverlayOwnerScope = OverlayOwnerScope.Application, + ownerDomain: ScreenDomainId = ScreenDomainId.Application, key: Any? = null, ) : DOMNode(key) { override val styleType: String = "select" @@ -58,7 +58,7 @@ class SelectNode( markRenderCommandsDirty() } var closeOnSelect: Boolean = closeOnSelect - var ownerScope: OverlayOwnerScope = ownerScope + var ownerDomain: ScreenDomainId = ownerDomain var textColor: Int = DsglColors.TEXT var placeholderColor: Int = 0xFF8A8A8A.toInt() var backgroundColor: Int = 0xFF2E2E33.toInt() @@ -103,13 +103,13 @@ class SelectNode( KeyCodes.DOWN -> { openPopup() - DomainPortalServices.selectEngineFor(ownerScope).moveHighlight(ownerToken, 1) + DomainPortalServices.selectEngineFor(ownerDomain).moveHighlight(ownerToken, 1) event.cancelled = true } KeyCodes.UP -> { openPopup() - DomainPortalServices.selectEngineFor(ownerScope).moveHighlight(ownerToken, -1) + DomainPortalServices.selectEngineFor(ownerDomain).moveHighlight(ownerToken, -1) event.cancelled = true } } @@ -219,7 +219,7 @@ class SelectNode( controlledValue = template.controlledValue defaultValue = template.defaultValue closeOnSelect = template.closeOnSelect - ownerScope = template.ownerScope + ownerDomain = template.ownerDomain textColor = template.textColor placeholderColor = template.placeholderColor backgroundColor = template.backgroundColor @@ -257,7 +257,7 @@ class SelectNode( val open = DomainPortalServices.isSelectOpenFor(ownerToken) setOpenState(open) if (open) { - DomainPortalServices.selectEngineFor(ownerScope).sync(openRequest()) + DomainPortalServices.selectEngineFor(ownerDomain).sync(openRequest()) } } @@ -275,7 +275,7 @@ class SelectNode( onClose = { setOpenState(false) }, fontId = fontId, fontSize = fontSize, - ownerScope = ownerScope, + ownerDomain = ownerDomain, ) } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ComponentProps.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ComponentProps.kt index f32a200..2e91271 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ComponentProps.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ComponentProps.kt @@ -39,6 +39,7 @@ open class ComponentProps( var id: String? = null, var className: String = "", var classes: Set = emptySet(), + var overlapChildren: Boolean = false, var disabled: Boolean = false, var draggable: Boolean = false, var droppable: Boolean = false, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ContainerDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ContainerDsl.kt index e0f40d9..3604157 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ContainerDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/ContainerDsl.kt @@ -11,25 +11,7 @@ fun UiScope.div( block: UiScope.() -> Unit = {}, ) = withProps(ComponentProps().apply(props)) { props -> ContainerNode( - stackLayout = false, - key = props.key, - ).apply { - applyStyle(this, props.style) - applyHandlers(this, props) - applyRef(this, ref) - add(this) - childScope(this).block() - } -} - -/** Overlay layout container (children overlap). */ -fun UiScope.overlay( - props: ComponentProps.() -> Unit, - ref: RefTarget? = null, - block: UiScope.() -> Unit = {}, -) = withProps(ComponentProps().apply(props)) { props -> - ContainerNode( - stackLayout = true, + stackLayout = props.overlapChildren, key = props.key, ).apply { applyStyle(this, props.style) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/InputDsl.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/InputDsl.kt index 3a6185f..f6065f2 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/InputDsl.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dsl/InputDsl.kt @@ -3,7 +3,7 @@ package org.dreamfinity.dsgl.core.dsl import org.dreamfinity.dsgl.core.dom.elements.* import org.dreamfinity.dsgl.core.hooks.ref.ElementHandle import org.dreamfinity.dsgl.core.hooks.ref.RefTarget -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope +import org.dreamfinity.dsgl.core.portal.ScreenDomainId import org.dreamfinity.dsgl.core.select.SelectModelBuilder import org.dreamfinity.dsgl.core.select.selectModel import java.time.Instant @@ -18,7 +18,7 @@ open class TextAreaProps( open class SelectProps : ComponentProps() { var closeOnSelect: Boolean = true var defaultValue: String? = null - var ownerScope: OverlayOwnerScope = OverlayOwnerScope.Application + var ownerDomain: ScreenDomainId = ScreenDomainId.Application var value: String? get() = valueInternal @@ -170,7 +170,7 @@ fun UiScope.select( value = if (controlled) props.controlledValue() else null, defaultValue = props.defaultValue, closeOnSelect = props.closeOnSelect, - ownerScope = props.ownerScope, + ownerDomain = props.ownerDomain, key = props.key, ).apply { applyStyle(this, props.style) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt index d5c4d91..0caf97a 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorController.kt @@ -145,8 +145,8 @@ class InspectorController( private var dragStartOffsetX: Int = 0 private var dragStartOffsetY: Int = 0 private var dragMoved: Boolean = false - private var overlayPanelPointerCapture: Boolean = false - private var overlayPanelAuthorityEnabled: Boolean = false + private var floatingPanelPointerCapture: Boolean = false + private var floatingPanelAuthorityEnabled: Boolean = false private val paneMoveDrag: FloatingPaneDragModel = FloatingPaneDragModel() private var viewportW: Int = 0 private var viewportH: Int = 0 @@ -284,7 +284,7 @@ class InspectorController( minimizedPosY = current.y panelState = InspectorPanelState.Minimized dragMode = DragMode.None - overlayPanelPointerCapture = false + floatingPanelPointerCapture = false dragMoved = false } @@ -294,7 +294,7 @@ class InspectorController( expandedRect = clampExpandedRect(expandedRect, viewportW, viewportH) panelState = InspectorPanelState.Expanded dragMode = DragMode.None - overlayPanelPointerCapture = false + floatingPanelPointerCapture = false dragMoved = false } @@ -303,12 +303,12 @@ class InspectorController( ( mode == InspectorMode.Pick || dragMode != DragMode.None || - overlayPanelPointerCapture + floatingPanelPointerCapture ) fun shouldConsumePointer(mouseX: Int, mouseY: Int): Boolean { if (!active) return false - if (dragMode != DragMode.None || overlayPanelPointerCapture) return true + if (dragMode != DragMode.None || floatingPanelPointerCapture) return true if (editSession.textSelectionDragActive) return true if (mode == InspectorMode.Pick) return true return hitTestUi(mouseX, mouseY) @@ -316,14 +316,14 @@ class InspectorController( fun shouldConsumeWheel(mouseX: Int, mouseY: Int): Boolean { if (!active) return false - if (dragMode != DragMode.None || overlayPanelPointerCapture) return true + if (dragMode != DragMode.None || floatingPanelPointerCapture) return true if (mode == InspectorMode.Pick) return true return hitTestUi(mouseX, mouseY) } fun shouldConsumeKeyboard(mouseX: Int, mouseY: Int): Boolean { if (!active) return false - if (dragMode != DragMode.None || overlayPanelPointerCapture) return true + if (dragMode != DragMode.None || floatingPanelPointerCapture) return true if (mode == InspectorMode.Pick) return true return hitTestUi(mouseX, mouseY) } @@ -615,7 +615,7 @@ class InspectorController( } if (panelState == InspectorPanelState.Minimized) { if (minimizedBounds.contains(mouseX, mouseY)) { - if (overlayPanelAuthorityEnabled) { + if (floatingPanelAuthorityEnabled) { return true } startMinimizedMoveDrag(mouseX, mouseY) @@ -635,7 +635,7 @@ class InspectorController( if (shouldCommitActiveEdit(action)) { commitActiveTextEdit() } - if (!overlayPanelAuthorityEnabled && startScrollbarDrag(mouseX, mouseY)) { + if (!floatingPanelAuthorityEnabled && startScrollbarDrag(mouseX, mouseY)) { return true } if (action != null) { @@ -647,7 +647,7 @@ class InspectorController( return true } editSession.closeAllDropdowns() - if (!overlayPanelAuthorityEnabled) { + if (!floatingPanelAuthorityEnabled) { val resizeMode = resolveResizeDragMode(mouseX, mouseY) if (resizeMode != DragMode.None) { startExpandedDrag(resizeMode, mouseX, mouseY) @@ -1146,20 +1146,20 @@ class InspectorController( scrollbarThumbRect = Rect(track.x, thumbY, track.width, thumbHeight) } - internal fun overlayPickToggleBounds(): Rect? = panelActions.lastOrNull { it.kind == ActionKind.TogglePick }?.bounds + internal fun portalPickToggleBounds(): Rect? = panelActions.lastOrNull { it.kind == ActionKind.TogglePick }?.bounds - internal fun overlayMinimizeBounds(): Rect? = panelActions.lastOrNull { it.kind == ActionKind.Minimize }?.bounds + internal fun portalMinimizeBounds(): Rect? = panelActions.lastOrNull { it.kind == ActionKind.Minimize }?.bounds - internal fun overlayContentRect(): Rect = contentBounds + internal fun portalContentRect(): Rect = contentBounds - internal fun overlayScrollbarThumbRect(): Rect = + internal fun portalScrollbarThumbRect(): Rect = if (nativeDomBodyScrollStateActive) { nativeDomScrollbarThumbRectOverride ?: Rect(0, 0, 0, 0) } else { scrollbarThumbRect } - internal fun overlayScrollbarTrackRect(): Rect = + internal fun portalScrollbarTrackRect(): Rect = if (nativeDomBodyScrollStateActive) { nativeDomScrollbarTrackRectOverride ?: Rect(0, 0, 0, 0) } else { @@ -1181,16 +1181,16 @@ class InspectorController( minimizedBounds = Rect(0, 0, 0, 0) } - internal fun onOverlayPanelRectChanged(rect: Rect, viewportWidth: Int, viewportHeight: Int) { + internal fun onFloatingPanelRectChanged(rect: Rect, viewportWidth: Int, viewportHeight: Int) { onNativeDomExpandedPanelRect(rect, viewportWidth, viewportHeight) } - internal fun onOverlayPanelPointerCaptureChanged(captured: Boolean) { - overlayPanelPointerCapture = captured + internal fun onFloatingPanelPointerCaptureChanged(captured: Boolean) { + floatingPanelPointerCapture = captured } - internal fun setOverlayPanelAuthorityEnabled(enabled: Boolean) { - overlayPanelAuthorityEnabled = enabled + internal fun setFloatingPanelAuthorityEnabled(enabled: Boolean) { + floatingPanelAuthorityEnabled = enabled if (enabled && dragMode != DragMode.ScrollbarThumb) { dragMode = DragMode.None } @@ -1208,21 +1208,21 @@ class InspectorController( minimizedBounds = Rect(minimizedPosX, minimizedPosY, minimizedWidth(), minimizedHeight()) } - internal fun overlaySelectedHighlight(): InspectorHighlightSnapshot? = nativeSelectedHighlight + internal fun portalSelectedHighlight(): InspectorHighlightSnapshot? = nativeSelectedHighlight - internal fun overlayHoveredHighlight(): InspectorHighlightSnapshot? = nativeHoveredHighlight + internal fun portalHoveredHighlight(): InspectorHighlightSnapshot? = nativeHoveredHighlight - internal fun overlayCursorTooltip(): InspectorTooltipSnapshot? = nativeCursorTooltip + internal fun portalCursorTooltip(): InspectorTooltipSnapshot? = nativeCursorTooltip - internal fun overlayVariableTooltip(): InspectorTooltipSnapshot? = nativeVariableTooltip + internal fun portalVariableTooltip(): InspectorTooltipSnapshot? = nativeVariableTooltip - internal fun overlayStyleEditorRows(): List = nativeStyleEditorRows + internal fun portalStyleEditorRows(): List = nativeStyleEditorRows - internal fun overlayStyleEditorResetRect(): Rect = nativeStyleEditorResetRect + internal fun portalStyleEditorResetRect(): Rect = nativeStyleEditorResetRect - internal fun overlayStyleEditorClearRect(): Rect = nativeStyleEditorClearRect + internal fun portalStyleEditorClearRect(): Rect = nativeStyleEditorClearRect - internal fun overlayStyleEditorDropdowns(): List = nativeDropdowns + internal fun portalStyleEditorDropdowns(): List = nativeDropdowns internal fun onNativeDomDropdownSnapshots(dropdowns: List) { nativeDropdowns.clear() @@ -1463,7 +1463,7 @@ class InspectorController( return OpenStyleDropdown(unitSelect = false, optionCount = optionCount) } - internal fun overlayApplyLiteralOverride(property: StyleProperty, literal: String): Boolean { + internal fun portalApplyLiteralOverride(property: StyleProperty, literal: String): Boolean { val selected = selectedNode ?: return false val normalized = literal.trim() return runCatching { @@ -1477,7 +1477,7 @@ class InspectorController( } } - internal fun overlayApplyNumericOverride( + internal fun portalApplyNumericOverride( property: StyleProperty, numericLiteral: String, unitToken: String?, @@ -1553,8 +1553,8 @@ class InspectorController( hoverPickEnabled = true styleEditorError = null dragMode = DragMode.None - overlayPanelPointerCapture = false - overlayPanelAuthorityEnabled = false + floatingPanelPointerCapture = false + floatingPanelAuthorityEnabled = false dragMoved = false panelScrollY = 0 panelContentHeight = 0 @@ -1705,7 +1705,7 @@ class InspectorController( } } - internal fun overlayColorPickerActionBounds(property: StyleProperty): Rect? = + internal fun portalColorPickerActionBounds(property: StyleProperty): Rect? = panelActions .lastOrNull { it.kind == ActionKind.EditProperty && @@ -1719,12 +1719,12 @@ class InspectorController( return true } - internal fun overlayPanelRect(): Rect? { + internal fun floatingPanelRect(): Rect? { if (!active) return null return currentInspectorRect() } - internal fun overlayExpandedPanelRect(): Rect? { + internal fun floatingExpandedPanelRect(): Rect? { if (!active || panelState != InspectorPanelState.Expanded) return null return expandedRect } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorPortalNode.kt similarity index 91% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorPortalNode.kt index 3ce0516..8885c1f 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorPortalNode.kt @@ -11,18 +11,18 @@ import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.dsl.* import org.dreamfinity.dsgl.core.event.* import org.dreamfinity.dsgl.core.inspector.* -import org.dreamfinity.dsgl.core.overlay.DomainPortalServices -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope -import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanel -import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelDragSession -import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelState +import org.dreamfinity.dsgl.core.portal.DomainPortalServices +import org.dreamfinity.dsgl.core.portal.ScreenDomainId +import org.dreamfinity.dsgl.core.portal.panel.FloatingPanel +import org.dreamfinity.dsgl.core.portal.panel.FloatingPanelDragSession +import org.dreamfinity.dsgl.core.portal.panel.FloatingPanelState import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.Overflow import org.dreamfinity.dsgl.core.style.TextWrap -internal class SystemInspectorOverlayNode( +internal class SystemInspectorPortalNode( private val controller: InspectorController, - private val overlayPanel: OverlayPanel, + private val floatingPanel: FloatingPanel, key: Any? = "dsgl-system-inspector", ) : DOMNode(key) { override val styleType: String = "dsgl-system-inspector" @@ -33,11 +33,11 @@ internal class SystemInspectorOverlayNode( key: Any? = "dsgl-system-inspector", ) : this( controller = controller, - overlayPanel = - OverlayPanel( + floatingPanel = + FloatingPanel( ownerId = "standalone-system-inspector", - panelState = OverlayPanelState(), - dragSession = OverlayPanelDragSession(), + panelState = FloatingPanelState(), + dragSession = FloatingPanelDragSession(), ), key = key, ) @@ -49,8 +49,8 @@ internal class SystemInspectorOverlayNode( private var lastViewportWidth: Int = 1 private var lastViewportHeight: Int = 1 private var persistedBodyScrollSession: ScrollSessionSnapshot? = null - private var overlayPanelDragUpdatedByDomInput: Boolean = false - private val panelNode: DOMNode = overlayPanel.node().applyParent(this) + private var floatingPanelDragUpdatedByDomInput: Boolean = false + private val panelNode: DOMNode = floatingPanel.node().applyParent(this) private var minimizedChipDragSession: MinimizedChipDragSession? = null private data class MinimizedChipDragSession( @@ -64,8 +64,8 @@ internal class SystemInspectorOverlayNode( init { EventBus.run { - this@SystemInspectorOverlayNode.addEventListener(Events.MOUSEDOWN) { event: MouseDownEvent -> - if (handleOverlayPanelMouseDown(event)) { + this@SystemInspectorPortalNode.addEventListener(Events.MOUSEDOWN) { event: MouseDownEvent -> + if (handleFloatingPanelMouseDown(event)) { event.cancelled = true return@addEventListener } @@ -75,8 +75,8 @@ internal class SystemInspectorOverlayNode( event.cancelled = true } } - this@SystemInspectorOverlayNode.addEventListener(Events.MOUSEUP) { event: MouseUpEvent -> - if (handleOverlayPanelMouseUp(event)) { + this@SystemInspectorPortalNode.addEventListener(Events.MOUSEUP) { event: MouseUpEvent -> + if (handleFloatingPanelMouseUp(event)) { event.cancelled = true return@addEventListener } @@ -86,10 +86,10 @@ internal class SystemInspectorOverlayNode( event.cancelled = true } } - this@SystemInspectorOverlayNode.addEventListener(Events.DRAG) { event: MouseDragEvent -> + this@SystemInspectorPortalNode.addEventListener(Events.DRAG) { event: MouseDragEvent -> val nextMouseX = event.lastMouseX + event.dx val nextMouseY = event.lastMouseY + event.dy - if (handleOverlayPanelDrag(nextMouseX, nextMouseY)) { + if (handleFloatingPanelDrag(nextMouseX, nextMouseY)) { event.cancelled = true return@addEventListener } @@ -101,65 +101,65 @@ internal class SystemInspectorOverlayNode( } } - private fun handleOverlayPanelMouseDown(event: MouseDownEvent): Boolean { + private fun handleFloatingPanelMouseDown(event: MouseDownEvent): Boolean { if (event.mouseButton != MouseButton.LEFT) return false - val bodyRect = controller.overlayContentRect() + val bodyRect = controller.portalContentRect() val pointerInsideBody = bodyRect.width > 0 && bodyRect.height > 0 && bodyRect.contains(event.mouseX, event.mouseY) if (pointerInsideBody) return false val handled = - overlayPanel.handleMouseDown( + floatingPanel.handleMouseDown( mouseX = event.mouseX, mouseY = event.mouseY, button = event.mouseButton, includeCloseButton = false, ) if (handled) { - controller.onOverlayPanelPointerCaptureChanged(true) + controller.onFloatingPanelPointerCaptureChanged(true) } return handled } - private fun handleOverlayPanelDrag(mouseX: Int, mouseY: Int): Boolean { + private fun handleFloatingPanelDrag(mouseX: Int, mouseY: Int): Boolean { val handled = - overlayPanel.handleMouseMove( + floatingPanel.handleMouseMove( mouseX = mouseX, mouseY = mouseY, viewportWidth = lastViewportWidth, viewportHeight = lastViewportHeight, ) { rect -> - controller.onOverlayPanelRectChanged(rect, lastViewportWidth, lastViewportHeight) + controller.onFloatingPanelRectChanged(rect, lastViewportWidth, lastViewportHeight) } if (handled) { - overlayPanelDragUpdatedByDomInput = true - controller.onOverlayPanelPointerCaptureChanged(true) + floatingPanelDragUpdatedByDomInput = true + controller.onFloatingPanelPointerCaptureChanged(true) } return handled } - private fun handleOverlayPanelMouseUp(event: MouseUpEvent): Boolean { + private fun handleFloatingPanelMouseUp(event: MouseUpEvent): Boolean { val handled = - overlayPanel.handleMouseUp( + floatingPanel.handleMouseUp( mouseX = event.mouseX, mouseY = event.mouseY, button = event.mouseButton, viewportWidth = lastViewportWidth, viewportHeight = lastViewportHeight, ) { rect -> - controller.onOverlayPanelRectChanged(rect, lastViewportWidth, lastViewportHeight) + controller.onFloatingPanelRectChanged(rect, lastViewportWidth, lastViewportHeight) } if (handled) { - overlayPanelDragUpdatedByDomInput = false - controller.onOverlayPanelPointerCaptureChanged(false) + floatingPanelDragUpdatedByDomInput = false + controller.onFloatingPanelPointerCaptureChanged(false) } return handled } - internal fun consumeOverlayPanelDomDragUpdate(): Boolean { - val consumed = overlayPanelDragUpdatedByDomInput - overlayPanelDragUpdatedByDomInput = false + internal fun consumeFloatingPanelDomDragUpdate(): Boolean { + val consumed = floatingPanelDragUpdatedByDomInput + floatingPanelDragUpdatedByDomInput = false return consumed } @@ -177,7 +177,7 @@ internal class SystemInspectorOverlayNode( fun syncInputBounds(viewportWidth: Int, viewportHeight: Int) { val viewportRect = Rect(0, 0, viewportWidth.coerceAtLeast(0), viewportHeight.coerceAtLeast(0)) - bounds = resolveInputBounds(viewportRect, controller.overlayPanelRect()) + bounds = resolveInputBounds(viewportRect, controller.floatingPanelRect()) } override fun measure(ctx: UiMeasureContext): Size = @@ -191,7 +191,7 @@ internal class SystemInspectorOverlayNode( height: Int, ) { val viewportRect = Rect(x, y, width, height) - bounds = resolveInputBounds(viewportRect, controller.overlayPanelRect()) + bounds = resolveInputBounds(viewportRect, controller.floatingPanelRect()) inspectedRoot?.let { root -> controller.onLayoutCommitted(root, inspectedLayoutRevision) } @@ -211,7 +211,7 @@ internal class SystemInspectorOverlayNode( controller.onNativeDomDropdownSnapshots(emptyList()) clearMinimizedChipDragSession() controller.onNativeDomBodyScrollState(0, null, null) - controller.onOverlayPanelPointerCaptureChanged(false) + controller.onFloatingPanelPointerCaptureChanged(false) return } bounds = resolveInputBounds(viewportRect, snapshot.panelRect) @@ -228,7 +228,7 @@ internal class SystemInspectorOverlayNode( } private fun resolveInputBounds(viewportRect: Rect, panelRect: Rect?): Rect { - if (controller.blocksUnderlyingInput() || overlayPanel.isDragging() || minimizedChipDragSession != null) { + if (controller.blocksUnderlyingInput() || floatingPanel.isDragging() || minimizedChipDragSession != null) { return viewportRect } val base = panelRect ?: viewportRect @@ -240,7 +240,7 @@ internal class SystemInspectorOverlayNode( } private fun resolveRenderedDropdownInputBounds(): Rect? { - val dropdowns = controller.overlayStyleEditorDropdowns() + val dropdowns = controller.portalStyleEditorDropdowns() if (dropdowns.isNotEmpty()) { return dropdowns .map { it.popupRect } @@ -289,7 +289,7 @@ internal class SystemInspectorOverlayNode( val panelRect = snapshot.panelRect val bodyRect = snapshot.bodyRect - ?: overlayPanel.bodyRect() + ?: floatingPanel.bodyRect() ?: Rect( panelRect.x + 6, panelRect.y + 58, @@ -365,7 +365,7 @@ internal class SystemInspectorOverlayNode( lineHeightPx = lineHeightPx, ) - val styleRows = controller.overlayStyleEditorRows() + val styleRows = controller.portalStyleEditorRows() renderStyleEditorRows(bodyScope, body, ctx, bodyScrollY, styleRows) y += snapshot.styleEditorHeight @@ -393,7 +393,7 @@ internal class SystemInspectorOverlayNode( scope, ctx, "dsgl-system-inspector-variable-tooltip", - controller.overlayVariableTooltip(), + controller.portalVariableTooltip(), 0xEE141A22.toInt(), 0xCC60758F.toInt(), ) @@ -401,7 +401,7 @@ internal class SystemInspectorOverlayNode( scope, ctx, "dsgl-system-inspector-cursor-tooltip", - controller.overlayCursorTooltip(), + controller.portalCursorTooltip(), 0xDD11151A.toInt(), 0xCC3F4A57.toInt(), ) @@ -467,10 +467,10 @@ internal class SystemInspectorOverlayNode( private fun renderExpandedChrome(scope: UiScope, ctx: UiMeasureContext, panelRect: Rect) { val pickRect = - controller.overlayPickToggleBounds() + controller.portalPickToggleBounds() ?: Rect(panelRect.x + panelRect.width - 264, panelRect.y + 8, 160, 36) val minimizeRect = - controller.overlayMinimizeBounds() + controller.portalMinimizeBounds() ?: Rect(panelRect.x + panelRect.width - 96, panelRect.y + 8, 86, 36) renderPickToggleButton(scope, ctx, pickRect) renderMinimizeButton(scope, ctx, minimizeRect) @@ -671,7 +671,7 @@ internal class SystemInspectorOverlayNode( } private fun renderHighlights(scope: UiScope, ctx: UiMeasureContext) { - controller.overlaySelectedHighlight()?.let { highlight -> + controller.portalSelectedHighlight()?.let { highlight -> renderHighlightRect( scope, ctx, @@ -739,7 +739,7 @@ internal class SystemInspectorOverlayNode( ) } } - controller.overlayHoveredHighlight()?.let { highlight -> + controller.portalHoveredHighlight()?.let { highlight -> renderHighlightRect( scope, ctx, @@ -920,10 +920,10 @@ internal class SystemInspectorOverlayNode( input.placeholderColor = 0xAA9AAFC6.toInt() input.fontSize = 18 input.onInput = { - controller.overlayApplyLiteralOverride(row.property, it.value) + controller.portalApplyLiteralOverride(row.property, it.value) } input.onValueChange = { - controller.overlayApplyLiteralOverride(row.property, it.value) + controller.portalApplyLiteralOverride(row.property, it.value) } input.applyParent(parentNode) renderNode(ctx, input, translateRectY(row.controlRect, -bodyScrollY)) @@ -987,10 +987,10 @@ internal class SystemInspectorOverlayNode( input.placeholderColor = 0xAA9AAFC6.toInt() input.fontSize = 18 input.onInput = { - controller.overlayApplyNumericOverride(row.property, it.value, row.unitValue) + controller.portalApplyNumericOverride(row.property, it.value, row.unitValue) } input.onValueChange = { - controller.overlayApplyNumericOverride(row.property, it.value, row.unitValue) + controller.portalApplyNumericOverride(row.property, it.value, row.unitValue) } input.applyParent(parentNode) renderNode(ctx, input, translateRectY(rect, -bodyScrollY)) @@ -1039,7 +1039,7 @@ internal class SystemInspectorOverlayNode( scope.select( props = { this.key = key - ownerScope = OverlayOwnerScope.System + ownerDomain = ScreenDomainId.System value = selectedValue onInput = { onSelected(it.value) } }, @@ -1064,7 +1064,7 @@ internal class SystemInspectorOverlayNode( } private fun renderStyleEditorFooterActions(scope: UiScope, ctx: UiMeasureContext, bodyScrollY: Int) { - val resetRect = controller.overlayStyleEditorResetRect() + val resetRect = controller.portalStyleEditorResetRect() if (resetRect.width > 0 && resetRect.height > 0) { val resetButton = scope.button("Reset node", { @@ -1080,7 +1080,7 @@ internal class SystemInspectorOverlayNode( renderNode(ctx, resetButton, translateRectY(resetRect, -bodyScrollY)) } - val clearRect = controller.overlayStyleEditorClearRect() + val clearRect = controller.portalStyleEditorClearRect() if (clearRect.width > 0 && clearRect.height > 0) { val clearButton = scope.button("Clear all", { @@ -1107,7 +1107,7 @@ internal class SystemInspectorOverlayNode( currentPointerY = mouseY, moved = false, ) - controller.onOverlayPanelPointerCaptureChanged(true) + controller.onFloatingPanelPointerCaptureChanged(true) } private fun updateMinimizedChipDragPointer(mouseX: Int, mouseY: Int) { @@ -1143,12 +1143,12 @@ internal class SystemInspectorOverlayNode( controller.restore() } minimizedChipDragSession = null - controller.onOverlayPanelPointerCaptureChanged(false) + controller.onFloatingPanelPointerCaptureChanged(false) } private fun clearMinimizedChipDragSession() { minimizedChipDragSession = null - controller.onOverlayPanelPointerCaptureChanged(false) + controller.onFloatingPanelPointerCaptureChanged(false) } private fun renderTooltip( @@ -1203,7 +1203,7 @@ internal class SystemInspectorOverlayNode( if (nodeKey?.startsWith("dsgl-system-inspector-") == true) { return true } - if (nodeKey?.startsWith("dsgl-overlay-panel-") == true) { + if (nodeKey?.startsWith("dsgl-floating-panel-") == true) { return true } current = current.parent diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ColorPickerPopupOverlayOwnership.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ColorPickerPopupOverlayOwnership.kt deleted file mode 100644 index a4c37de..0000000 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ColorPickerPopupOverlayOwnership.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.dreamfinity.dsgl.core.overlay - -import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupRequest - -object ColorPickerPopupOverlayOwnership { - fun resolveSurface(request: ColorPickerPopupRequest): ScreenDomainSurface = - ScreenDomainSurfaces.portalSurfaceForOwner(request.ownerScope) -} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayDebugVisualization.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayDebugVisualization.kt deleted file mode 100644 index c9d802c..0000000 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayDebugVisualization.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.dreamfinity.dsgl.core.overlay - -object OverlayDebugVisualization { - var applicationOverlayFillColor: Int = 0x22307BC8 - var applicationOverlayBorderColor: Int = 0xAA4DA4FF.toInt() - var systemOverlayFillColor: Int = 0x22A84BD8 - var systemOverlayBorderColor: Int = 0xAAE18BFF.toInt() - val enabled: Boolean - get() { - return testOverride ?: java.lang.Boolean - .getBoolean("dsgl.overlay.debug") - } - - private var testOverride: Boolean? = null - - internal fun setTestOverride(value: Boolean?) { - testOverride = value - } -} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationFloatingWindowPortalController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/ApplicationFloatingWindowPortalController.kt similarity index 90% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationFloatingWindowPortalController.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/ApplicationFloatingWindowPortalController.kt index 48c4bf2..1f577e5 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationFloatingWindowPortalController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/ApplicationFloatingWindowPortalController.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay +package org.dreamfinity.dsgl.core.portal import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent @@ -10,10 +10,10 @@ import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.Size import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanel -import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelDragSession -import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelState -import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelStyle +import org.dreamfinity.dsgl.core.portal.panel.FloatingPanel +import org.dreamfinity.dsgl.core.portal.panel.FloatingPanelDragSession +import org.dreamfinity.dsgl.core.portal.panel.FloatingPanelState +import org.dreamfinity.dsgl.core.portal.panel.FloatingPanelStyle internal class ApplicationFloatingWindowPortalController { private val portalHost: PortalHost = PortalHost(ScreenDomainSurfaces.ApplicationPortal) @@ -100,17 +100,17 @@ internal class ApplicationFloatingWindowPortalController { } private class ApplicationFloatingWindowPortalEntry : PortalEntry { - private val panelState: OverlayPanelState = OverlayPanelState() - private val dragSession: OverlayPanelDragSession = OverlayPanelDragSession() - private val overlayPanel: OverlayPanel = - OverlayPanel( + private val panelState: FloatingPanelState = FloatingPanelState() + private val dragSession: FloatingPanelDragSession = FloatingPanelDragSession() + private val floatingPanel: FloatingPanel = + FloatingPanel( ownerId = "application.f10-floating-window", panelState = panelState, dragSession = dragSession, ) override val node: ApplicationFloatingWindowNode = ApplicationFloatingWindowNode( - overlayPanel = overlayPanel, + floatingPanel = floatingPanel, onPositionChanged = panelState::updateFromRect, onCaptureCancelled = dragSession::end, ) @@ -157,13 +157,13 @@ private class ApplicationFloatingWindowPortalEntry : PortalEntry { state.deactivate() return } - overlayPanel.configure( + floatingPanel.configure( title = "Application Portal", draggable = true, - style = OverlayPanelStyle(fontSize = 16), + style = FloatingPanelStyle(fontSize = 16), onClose = ::close, ) - overlayPanel.syncPanelRect(panelState.currentRectOrNull()) + floatingPanel.syncPanelRect(panelState.currentRectOrNull()) activate(viewportWidth, viewportHeight) } @@ -174,7 +174,7 @@ private class ApplicationFloatingWindowPortalEntry : PortalEntry { viewportHeight: Int, ) { if (!opened) return - if (!overlayPanel.isDragging()) return + if (!floatingPanel.isDragging()) return node.updateActiveDrag( mouseX = mouseX, mouseY = mouseY, @@ -213,7 +213,7 @@ private class ApplicationFloatingWindowPortalEntry : PortalEntry { } internal class ApplicationFloatingWindowNode( - private val overlayPanel: OverlayPanel, + private val floatingPanel: FloatingPanel, private val onPositionChanged: (Rect) -> Unit, private val onCaptureCancelled: () -> Unit, key: Any? = "dsgl-application-f10-floating-window", @@ -222,24 +222,24 @@ internal class ApplicationFloatingWindowNode( private val hitTargetNode: DOMNode = FloatingWindowPanelHitNode( - overlayPanel = overlayPanel, + floatingPanel = floatingPanel, viewportBoundsProvider = { viewportBounds }, onPositionChanged = onPositionChanged, onCaptureCancelled = onCaptureCancelled, onDragUpdated = ::invalidatePanelRenderCommands, ).applyParent(this) - private val panelNode: DOMNode = overlayPanel.node().applyParent(this) + private val panelNode: DOMNode = floatingPanel.node().applyParent(this) private val bodyNode: FloatingWindowBodyNode = - FloatingWindowBodyNode().also(overlayPanel::setBodyContent) + FloatingWindowBodyNode().also(floatingPanel::setBodyContent) private var viewportBounds: Rect = Rect(0, 0, 1, 1) fun currentButtonClicks(): Int = bodyNode.currentButtonClicks() fun buttonRect(): Rect? = bodyNode.buttonRect() - fun panelRect(): Rect? = overlayPanel.panelRect() + fun panelRect(): Rect? = floatingPanel.panelRect() - fun bodyRect(): Rect? = overlayPanel.bodyRect() + fun bodyRect(): Rect? = floatingPanel.bodyRect() override fun measure(ctx: UiMeasureContext): Size = Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) @@ -253,7 +253,7 @@ internal class ApplicationFloatingWindowNode( ) { viewportBounds = Rect(x, y, width.coerceAtLeast(1), height.coerceAtLeast(1)) bounds = viewportBounds - val panelRect = overlayPanel.panelRect() + val panelRect = floatingPanel.panelRect() if (panelRect == null) { hitTargetNode.render(ctx, 0, 0, 0, 0) } else { @@ -269,7 +269,7 @@ internal class ApplicationFloatingWindowNode( viewportHeight: Int, ) { if ( - overlayPanel.handleMouseMove( + floatingPanel.handleMouseMove( mouseX = mouseX, mouseY = mouseY, viewportWidth = viewportWidth.coerceAtLeast(1), @@ -289,7 +289,7 @@ internal class ApplicationFloatingWindowNode( } private class FloatingWindowPanelHitNode( - private val overlayPanel: OverlayPanel, + private val floatingPanel: FloatingPanel, private val viewportBoundsProvider: () -> Rect, private val onPositionChanged: (Rect) -> Unit, private val onCaptureCancelled: () -> Unit, @@ -320,14 +320,14 @@ private class FloatingWindowPanelHitNode( } override fun shouldCapturePointerDrag(mouseX: Int, mouseY: Int): Boolean { - val header = overlayPanel.headerRect() ?: return false - val close = overlayPanel.closeRect() + val header = floatingPanel.headerRect() ?: return false + val close = floatingPanel.closeRect() return header.contains(mouseX, mouseY) && close?.contains(mouseX, mouseY) != true } override fun beginPointerCapture(mouseX: Int, mouseY: Int, button: MouseButton) { if (button != MouseButton.LEFT) return - overlayPanel.beginHeaderDrag(mouseX, mouseY) + floatingPanel.beginHeaderDrag(mouseX, mouseY) } override fun continuePointerCapture( @@ -345,7 +345,7 @@ private class FloatingWindowPanelHitNode( if (button != MouseButton.LEFT) return val viewport = viewportBoundsProvider() if ( - overlayPanel.handleMouseUp( + floatingPanel.handleMouseUp( mouseX = mouseX, mouseY = mouseY, button = button, @@ -365,7 +365,7 @@ private class FloatingWindowPanelHitNode( private fun updateActiveDrag(mouseX: Int, mouseY: Int) { val viewport = viewportBoundsProvider() if ( - overlayPanel.handleMouseMove( + floatingPanel.handleMouseMove( mouseX = mouseX, mouseY = mouseY, viewportWidth = viewport.width.coerceAtLeast(1), diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/ApplicationPortalHost.kt similarity index 89% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/ApplicationPortalHost.kt index 9e77c42..c3fa05c 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/ApplicationPortalHost.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay +package org.dreamfinity.dsgl.core.portal import org.dreamfinity.dsgl.core.DomTree import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupEngine @@ -11,14 +11,14 @@ import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter +import org.dreamfinity.dsgl.core.portal.input.SurfaceDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.select.SelectEngine import org.dreamfinity.dsgl.core.select.SelectPortalController import org.dreamfinity.dsgl.core.style.StyleApplicationScope @Suppress("TooManyFunctions") -class ApplicationOverlayHost( +class ApplicationPortalHost( contextMenuEngine: ContextMenuEngine = DomainPortalServices.applicationContextMenuEngine, selectEngine: SelectEngine = DomainPortalServices.applicationSelectEngine, colorPickerEngine: ColorPickerPopupEngine = DomainPortalServices.applicationColorPickerEngine, @@ -26,14 +26,14 @@ class ApplicationOverlayHost( ) : DomainSurfaceHost { override val surface: ScreenDomainSurface = ScreenDomainSurfaces.ApplicationPortal - internal val rootNode: ApplicationOverlayRootNode = ApplicationOverlayRootNode() + internal val rootNode: ApplicationPortalRootNode = ApplicationPortalRootNode() private val tree: DomTree = DomTree( root = rootNode, styleScope = StyleApplicationScope.Application, ) - internal val domInputRouter: LayerDomInputRouter = - LayerDomInputRouter( + internal val domInputRouter: SurfaceDomInputRouter = + SurfaceDomInputRouter( rootProvider = { rootNode }, ) internal val contextMenuPortal: ContextMenuPortalController = @@ -41,7 +41,7 @@ class ApplicationOverlayHost( internal val applicationSelectPortal: SelectPortalController = SelectPortalController( engine = selectEngine, - ownerScope = OverlayOwnerScope.Application, + ownerDomain = ScreenDomainId.Application, entryId = "application.select", ) internal val applicationColorPickerPortal: ColorPickerPortalController = @@ -132,9 +132,9 @@ class ApplicationOverlayHost( } } -internal fun ApplicationOverlayHost.debugRootBounds(): Rect = rootNode.bounds +internal fun ApplicationPortalHost.debugRootBounds(): Rect = rootNode.bounds -fun ApplicationOverlayHost.syncPortalFrame( +fun ApplicationPortalHost.syncPortalFrame( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, @@ -148,7 +148,7 @@ fun ApplicationOverlayHost.syncPortalFrame( floatingWindowPortal.onFrameCursor(viewportWidth, viewportHeight, mouseX, mouseY) } -fun ApplicationOverlayHost.appendPortalOverlayCommands( +fun ApplicationPortalHost.appendFloatingPortalCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, @@ -159,7 +159,7 @@ fun ApplicationOverlayHost.appendPortalOverlayCommands( applicationColorPickerPortal.appendCommands(measureContext, viewportWidth, viewportHeight, out) } -fun ApplicationOverlayHost.appendDndGhostPortalCommands( +fun ApplicationPortalHost.appendDndGhostPortalCommands( root: DOMNode, measureContext: UiMeasureContext, viewportWidth: Int, @@ -169,55 +169,55 @@ fun ApplicationOverlayHost.appendDndGhostPortalCommands( dndGhostPortal.appendCommands(root, measureContext, viewportWidth, viewportHeight, out) } -fun ApplicationOverlayHost.closeFloatingPortals() { +fun ApplicationPortalHost.closeFloatingPortals() { contextMenuPortal.close() applicationSelectPortal.close() applicationColorPickerPortal.close() floatingWindowPortal.close() } -fun ApplicationOverlayHost.hasOpenContextMenuPortal(): Boolean = contextMenuPortal.isOpen() +fun ApplicationPortalHost.hasOpenContextMenuPortal(): Boolean = contextMenuPortal.isOpen() -fun ApplicationOverlayHost.hasOpenSelectPortal(): Boolean = applicationSelectPortal.isOpen() +fun ApplicationPortalHost.hasOpenSelectPortal(): Boolean = applicationSelectPortal.isOpen() -fun ApplicationOverlayHost.hasOpenColorPickerPortal(): Boolean = applicationColorPickerPortal.isOpen +fun ApplicationPortalHost.hasOpenColorPickerPortal(): Boolean = applicationColorPickerPortal.isOpen -fun ApplicationOverlayHost.hasActiveModalPortal(): Boolean = modalPortal.hasActivePortal() +fun ApplicationPortalHost.hasActiveModalPortal(): Boolean = modalPortal.hasActivePortal() -fun ApplicationOverlayHost.hasActiveColorPickerEyedropper(): Boolean = applicationColorPickerPortal.hasActiveEyedropper +fun ApplicationPortalHost.hasActiveColorPickerEyedropper(): Boolean = applicationColorPickerPortal.hasActiveEyedropper -fun ApplicationOverlayHost.captureColorPickerEyedropperSample() { +fun ApplicationPortalHost.captureColorPickerEyedropperSample() { applicationColorPickerPortal.captureEyedropperSample() } -fun ApplicationOverlayHost.toggleFloatingWindowDemo(anchorX: Int, anchorY: Int) { +fun ApplicationPortalHost.toggleFloatingWindowDemo(anchorX: Int, anchorY: Int) { if (hasActiveModalPortal()) return floatingWindowPortal.toggle(anchorX, anchorY) } -fun ApplicationOverlayHost.isFloatingWindowDemoOpen(): Boolean = floatingWindowPortal.open +fun ApplicationPortalHost.isFloatingWindowDemoOpen(): Boolean = floatingWindowPortal.open -internal fun ApplicationOverlayHost.debugDndGhostPortalState(): PortalEntryState = dndGhostPortal.debugState() +internal fun ApplicationPortalHost.debugDndGhostPortalState(): PortalEntryState = dndGhostPortal.debugState() -fun ApplicationOverlayHost.hasDomPointerTargetAt(mouseX: Int, mouseY: Int): Boolean = +fun ApplicationPortalHost.hasDomPointerTargetAt(mouseX: Int, mouseY: Int): Boolean = domInputRouter.hasPointerTargetAt(mouseX, mouseY) -fun ApplicationOverlayHost.handlePortalKeyDownBeforeDom(keyCode: Int, keyChar: Char): Boolean = +fun ApplicationPortalHost.handlePortalKeyDownBeforeDom(keyCode: Int, keyChar: Char): Boolean = applicationColorPickerPortal.handleKeyDown(keyCode, keyChar) || modalPortal.handleKeyDown(keyCode, keyChar) -fun ApplicationOverlayHost.handlePortalKeyDownAfterDom(keyCode: Int, keyChar: Char): Boolean = +fun ApplicationPortalHost.handlePortalKeyDownAfterDom(keyCode: Int, keyChar: Char): Boolean = applicationSelectPortal.handleKeyDown(keyCode, keyChar) || contextMenuPortal.handleKeyDown(keyCode) -fun ApplicationOverlayHost.handlePortalKeyUpBeforeDom(keyCode: Int, keyChar: Char): Boolean = +fun ApplicationPortalHost.handlePortalKeyUpBeforeDom(keyCode: Int, keyChar: Char): Boolean = applicationColorPickerPortal.handleKeyUp(keyCode, keyChar) -fun ApplicationOverlayHost.handlePortalKeyUpAfterDom(keyCode: Int, keyChar: Char): Boolean = +fun ApplicationPortalHost.handlePortalKeyUpAfterDom(keyCode: Int, keyChar: Char): Boolean = applicationSelectPortal.handleKeyUp(keyCode, keyChar) || contextMenuPortal.handleKeyUp(keyCode, keyChar) -fun ApplicationOverlayHost.handlePortalPointerBeforeDom( +fun ApplicationPortalHost.handlePortalPointerBeforeDom( mouseX: Int, mouseY: Int, dWheel: Int, @@ -225,7 +225,7 @@ fun ApplicationOverlayHost.handlePortalPointerBeforeDom( pressed: Boolean, ): Boolean = handlePortalPointer(applicationColorPickerPortal, mouseX, mouseY, dWheel, button, pressed) -fun ApplicationOverlayHost.handlePortalPointerAfterDom( +fun ApplicationPortalHost.handlePortalPointerAfterDom( mouseX: Int, mouseY: Int, dWheel: Int, @@ -338,7 +338,7 @@ private class ApplicationDndGhostPortalEntry( syncActivePlacement() val commands = ArrayList() engine.appendPlaceholderCommands(commands) - engine.appendOverlayCommands( + engine.appendPortalCommands( root = activeRoot, ctx = measureContext ?: ctx, viewportWidth = viewportWidth, @@ -477,7 +477,7 @@ private class ContextMenuPortalEntry( return emptyList() } val commands = ArrayList() - engine.appendOverlayCommands( + engine.appendPortalCommands( measureContext = measureContext ?: ctx, viewportWidth = viewportWidth, viewportHeight = viewportHeight, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayRootNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/ApplicationPortalRootNode.kt similarity index 78% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayRootNode.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/ApplicationPortalRootNode.kt index 4d0bc7c..cc131b3 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationOverlayRootNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/ApplicationPortalRootNode.kt @@ -1,7 +1,7 @@ -package org.dreamfinity.dsgl.core.overlay +package org.dreamfinity.dsgl.core.portal import org.dreamfinity.dsgl.core.DsglColors -import org.dreamfinity.dsgl.core.debug.OverlayLayerDebugState.isTintEnabled +import org.dreamfinity.dsgl.core.debug.DomainSurfaceDebugState.isTintEnabled import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Border @@ -14,15 +14,15 @@ import org.dreamfinity.dsgl.core.font.FontRegistry import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.StyleEngine -class ApplicationOverlayRootNode( - key: Any? = "dsgl-application-overlay-root", +class ApplicationPortalRootNode( + key: Any? = "dsgl-application-portal-root", ) : DOMNode(key) { - override val styleType: String = "dsgl-application-overlay-root" + override val styleType: String = "dsgl-application-portal-root" private var viewportWidth: Int = 0 private var viewportHeight: Int = 0 private val debugTintNode: ContainerNode = UiScope(this).div({ - this.key = "dsgl-application-overlay-debug-tint" + this.key = "dsgl-application-portal-debug-tint" style = { display = Display.None } @@ -52,11 +52,12 @@ class ApplicationOverlayRootNode( ) { setViewportBounds(width, height) bounds = Rect(0, 0, viewportWidth, viewportHeight) - val tintEnabled = OverlayDebugVisualization.enabled && isTintEnabled(ScreenDomainSurfaces.ApplicationPortal) + val tintEnabled = + DomainSurfaceDebugVisualization.enabled && isTintEnabled(ScreenDomainSurfaces.ApplicationPortal) if (tintEnabled) { debugTintNode.display = Display.Block - debugTintNode.backgroundColor = OverlayDebugVisualization.applicationOverlayFillColor - debugTintNode.border = Border.all(1, OverlayDebugVisualization.applicationOverlayBorderColor) + debugTintNode.backgroundColor = DomainSurfaceDebugVisualization.applicationPortalFillColor + debugTintNode.border = Border.all(1, DomainSurfaceDebugVisualization.applicationPortalBorderColor) debugTintNode.render(ctx, bounds.x, bounds.y, bounds.width, bounds.height) } else { debugTintNode.display = Display.None diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/ColorPickerPopupPortalOwnership.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/ColorPickerPopupPortalOwnership.kt new file mode 100644 index 0000000..943b025 --- /dev/null +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/ColorPickerPopupPortalOwnership.kt @@ -0,0 +1,8 @@ +package org.dreamfinity.dsgl.core.portal + +import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupRequest + +object ColorPickerPopupPortalOwnership { + fun resolveSurface(request: ColorPickerPopupRequest): ScreenDomainSurface = + ScreenDomainSurfaces.portalSurfaceForDomain(request.ownerDomain) +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/DomainPortalServices.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/DomainPortalServices.kt similarity index 77% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/DomainPortalServices.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/DomainPortalServices.kt index 46ec512..e3f9ab2 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/DomainPortalServices.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/DomainPortalServices.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay +package org.dreamfinity.dsgl.core.portal import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupEngine import org.dreamfinity.dsgl.core.contextmenu.ContextMenuEngine @@ -11,14 +11,15 @@ object DomainPortalServices { val systemSelectEngine: SelectEngine = SelectEngine() val applicationColorPickerEngine: ColorPickerPopupEngine = ColorPickerPopupEngine() - fun selectEngineFor(ownerScope: OverlayOwnerScope): SelectEngine = - when (ownerScope) { - OverlayOwnerScope.Application -> applicationSelectEngine - OverlayOwnerScope.System -> systemSelectEngine + fun selectEngineFor(ownerDomain: ScreenDomainId): SelectEngine = + when (ownerDomain) { + ScreenDomainId.Application -> applicationSelectEngine + ScreenDomainId.System -> systemSelectEngine + ScreenDomainId.Debug -> error("Debug domain select portals are not supported yet") } fun openSelect(request: SelectOpenRequest) { - val target = selectEngineFor(request.ownerScope) + val target = selectEngineFor(request.ownerDomain) val other = if (target === applicationSelectEngine) systemSelectEngine else applicationSelectEngine other.close(request.owner) target.open(request) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/DomainSurfaceDebugVisualization.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/DomainSurfaceDebugVisualization.kt new file mode 100644 index 0000000..12fecbf --- /dev/null +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/DomainSurfaceDebugVisualization.kt @@ -0,0 +1,19 @@ +package org.dreamfinity.dsgl.core.portal + +object DomainSurfaceDebugVisualization { + var applicationPortalFillColor: Int = 0x22307BC8 + var applicationPortalBorderColor: Int = 0xAA4DA4FF.toInt() + var systemPortalFillColor: Int = 0x22A84BD8 + var systemPortalBorderColor: Int = 0xAAE18BFF.toInt() + val enabled: Boolean + get() { + return testOverride ?: java.lang.Boolean + .getBoolean("dsgl.domain.debug") + } + + private var testOverride: Boolean? = null + + internal fun setTestOverride(value: Boolean?) { + testOverride = value + } +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/DomainSurfaceHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/DomainSurfaceHost.kt similarity index 95% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/DomainSurfaceHost.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/DomainSurfaceHost.kt index 8b4214c..47d3ca5 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/DomainSurfaceHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/DomainSurfaceHost.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay +package org.dreamfinity.dsgl.core.portal import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContracts.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/PortalHostContracts.kt similarity index 99% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContracts.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/PortalHostContracts.kt index 2e4b05c..f6c2ba2 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContracts.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/PortalHostContracts.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay +package org.dreamfinity.dsgl.core.portal import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.layout.Rect diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ScreenDomainContracts.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/ScreenDomainContracts.kt similarity index 86% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ScreenDomainContracts.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/ScreenDomainContracts.kt index 728dfe3..f240ed3 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/ScreenDomainContracts.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/ScreenDomainContracts.kt @@ -1,12 +1,7 @@ -package org.dreamfinity.dsgl.core.overlay +package org.dreamfinity.dsgl.core.portal import org.dreamfinity.dsgl.core.render.RenderCommand -enum class OverlayOwnerScope { - Application, - System, -} - enum class ScreenDomainId { Application, System, @@ -86,11 +81,11 @@ object ScreenDomainSurfaces { ScreenDomainId.Debug -> DebugPortal } - fun portalSurfaceForOwner(ownerScope: OverlayOwnerScope): ScreenDomainSurface = portalSurface(ownerScope.domain) + fun portalSurfaceForDomain(ownerDomain: ScreenDomainId): ScreenDomainSurface = portalSurface(ownerDomain) @Suppress("UnusedParameter") - fun portalSurfaceForOwner(ownerScope: OverlayOwnerScope, cursorX: Int, cursorY: Int): ScreenDomainSurface = - portalSurfaceForOwner(ownerScope) + fun portalSurfaceForDomain(ownerDomain: ScreenDomainId, cursorX: Int, cursorY: Int): ScreenDomainSurface = + portalSurfaceForDomain(ownerDomain) fun firstInputConsumer( canConsume: (ScreenDomainSurface) -> Boolean, @@ -127,10 +122,3 @@ object ScreenDomainSurfaces { } } } - -val OverlayOwnerScope.domain: ScreenDomainId - get() = - when (this) { - OverlayOwnerScope.Application -> ScreenDomainId.Application - OverlayOwnerScope.System -> ScreenDomainId.System - } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/PointerCaptureSession.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/input/PointerCaptureSession.kt similarity index 98% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/PointerCaptureSession.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/input/PointerCaptureSession.kt index 8a86e62..251c569 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/PointerCaptureSession.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/input/PointerCaptureSession.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay.input +package org.dreamfinity.dsgl.core.portal.input import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.elements.RangeInputNode diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouter.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/input/SurfaceDomInputRouter.kt similarity index 99% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouter.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/input/SurfaceDomInputRouter.kt index fcc159a..7a3ced2 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouter.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/input/SurfaceDomInputRouter.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay.input +package org.dreamfinity.dsgl.core.portal.input import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.elements.TextAreaNode @@ -19,7 +19,7 @@ import org.dreamfinity.dsgl.core.event.MouseOverEvent import org.dreamfinity.dsgl.core.event.MouseUpEvent import org.dreamfinity.dsgl.core.event.MouseWheelEvent -class LayerDomInputRouter( +class SurfaceDomInputRouter( private val rootProvider: () -> DOMNode?, ) { private val hoverChain: MutableList = ArrayList() diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerInputDispatch.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/input/SurfaceInputDispatch.kt similarity index 82% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerInputDispatch.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/input/SurfaceInputDispatch.kt index 78df55f..9166f2c 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerInputDispatch.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/input/SurfaceInputDispatch.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay.input +package org.dreamfinity.dsgl.core.portal.input internal inline fun dispatchManualThenDomFallback( manualDispatch: () -> Boolean, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanel.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/panel/FloatingPanel.kt similarity index 79% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanel.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/panel/FloatingPanel.kt index f0449f5..5aa8dc7 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanel.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/panel/FloatingPanel.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay.panel +package org.dreamfinity.dsgl.core.portal.panel import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent @@ -18,7 +18,7 @@ import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.FlexDirection import org.dreamfinity.dsgl.core.style.TextWrap -data class OverlayPanelStyle( +data class FloatingPanelStyle( val headerHeight: Int = 26, val panelPadding: Int = 6, val resizeHandleSize: Int = 8, @@ -38,23 +38,23 @@ data class OverlayPanelStyle( val closeGlyph: String = "x", ) -data class OverlayPanelFrame( +data class FloatingPanelFrame( val panelRect: Rect, val headerRect: Rect, val bodyRect: Rect, val closeRect: Rect, ) -class OverlayPanel( +class FloatingPanel( private val ownerId: Any, - private val panelState: OverlayPanelState, - private val dragSession: OverlayPanelDragSession, - private var style: OverlayPanelStyle = OverlayPanelStyle(), + private val panelState: FloatingPanelState, + private val dragSession: FloatingPanelDragSession, + private var style: FloatingPanelStyle = FloatingPanelStyle(), ) { - private val rootNode: OverlayPanelRootNode = - OverlayPanelRootNode( + private val rootNode: FloatingPanelRootNode = + FloatingPanelRootNode( owner = this, - key = "dsgl-overlay-panel-$ownerId", + key = "dsgl-floating-panel-$ownerId", ) private val shadowNode: ContainerNode private val panelNode: ContainerNode @@ -62,7 +62,7 @@ class OverlayPanel( private var titleNode: TextNode private val closeButtonNode: ButtonNode private var bodyContentNode: DOMNode? = null - private var overlayContentNode: DOMNode? = null + private var floatingContentNode: DOMNode? = null var title: String = "" private set var draggable: Boolean = true @@ -74,27 +74,27 @@ class OverlayPanel( var minHeight: Int = 80 private set private var onClose: (() -> Unit)? = null - private var frame: OverlayPanelFrame? = null + private var frame: FloatingPanelFrame? = null init { val scope = UiScope(rootNode) shadowNode = scope.div({ - key = "dsgl-overlay-panel-shadow-$ownerId" + key = "dsgl-floating-panel-shadow-$ownerId" style = { display = Display.Block } }) panelNode = scope.div({ - key = "dsgl-overlay-panel-frame-$ownerId" + key = "dsgl-floating-panel-frame-$ownerId" style = { display = Display.Block } }) headerNode = scope.div({ - key = "dsgl-overlay-panel-header-$ownerId" + key = "dsgl-floating-panel-header-$ownerId" style = { display = Display.Flex flexDirection = FlexDirection.Row @@ -102,7 +102,7 @@ class OverlayPanel( }) titleNode = scope.text(props = { - key = "dsgl-overlay-panel-title-$ownerId" + key = "dsgl-floating-panel-title-$ownerId" value = "" style = { textWrap = TextWrap.NoWrap @@ -110,7 +110,7 @@ class OverlayPanel( }) closeButtonNode = scope.button(style.closeGlyph, { - key = "dsgl-overlay-panel-close-$ownerId" + key = "dsgl-floating-panel-close-$ownerId" onMouseClick = { onClose?.invoke() it.cancelled = true @@ -126,13 +126,13 @@ class OverlayPanel( if (bodyContentNode === node) return detachNode(bodyContentNode) bodyContentNode = node - attachNodeBeforeOverlay(node) + attachNodeBeforeFloatingContent(node) } - fun setOverlayContent(node: DOMNode?) { - if (overlayContentNode === node) return - detachNode(overlayContentNode) - overlayContentNode = node + fun setFloatingContent(node: DOMNode?) { + if (floatingContentNode === node) return + detachNode(floatingContentNode) + floatingContentNode = node attachNodeAtTop(node) } @@ -142,7 +142,7 @@ class OverlayPanel( resizable: Boolean = this.resizable, minWidth: Int = this.minWidth, minHeight: Int = this.minHeight, - style: OverlayPanelStyle = this.style, + style: FloatingPanelStyle = this.style, onClose: (() -> Unit)? = this.onClose, ) { val titleChanged = this.title != title @@ -210,7 +210,7 @@ class OverlayPanel( if (resizeHandle != null) { dragSession.begin( ownerId = ownerId, - type = OverlayPanelDragType.PanelResize, + type = FloatingPanelDragType.PanelResize, pointerX = mouseX, pointerY = mouseY, panelState = panelState, @@ -261,14 +261,14 @@ class OverlayPanel( private fun buildDraggedRect(viewportWidth: Int, viewportHeight: Int): Rect = when (dragSession.type) { - OverlayPanelDragType.PanelResize -> buildResizedRect(viewportWidth, viewportHeight) + FloatingPanelDragType.PanelResize -> buildResizedRect(viewportWidth, viewportHeight) else -> buildMovedRect(viewportWidth, viewportHeight) } private fun beginMoveDrag(mouseX: Int, mouseY: Int) { dragSession.begin( ownerId = ownerId, - type = OverlayPanelDragType.PanelMove, + type = FloatingPanelDragType.PanelMove, pointerX = mouseX, pointerY = mouseY, panelState = panelState, @@ -300,27 +300,27 @@ class OverlayPanel( var bottom = dragSession.startPanelY + dragSession.startPanelHeight when (handle) { - OverlayPanelResizeHandle.Left, - OverlayPanelResizeHandle.TopLeft, - OverlayPanelResizeHandle.BottomLeft, + FloatingPanelResizeHandle.Left, + FloatingPanelResizeHandle.TopLeft, + FloatingPanelResizeHandle.BottomLeft, -> left += dx - OverlayPanelResizeHandle.Right, - OverlayPanelResizeHandle.TopRight, - OverlayPanelResizeHandle.BottomRight, + FloatingPanelResizeHandle.Right, + FloatingPanelResizeHandle.TopRight, + FloatingPanelResizeHandle.BottomRight, -> right += dx else -> Unit } when (handle) { - OverlayPanelResizeHandle.Top, - OverlayPanelResizeHandle.TopLeft, - OverlayPanelResizeHandle.TopRight, + FloatingPanelResizeHandle.Top, + FloatingPanelResizeHandle.TopLeft, + FloatingPanelResizeHandle.TopRight, -> top += dy - OverlayPanelResizeHandle.Bottom, - OverlayPanelResizeHandle.BottomLeft, - OverlayPanelResizeHandle.BottomRight, + FloatingPanelResizeHandle.Bottom, + FloatingPanelResizeHandle.BottomLeft, + FloatingPanelResizeHandle.BottomRight, -> bottom += dy else -> Unit @@ -328,14 +328,14 @@ class OverlayPanel( if (right - left < minWidth) { when (handle) { - OverlayPanelResizeHandle.Left, - OverlayPanelResizeHandle.TopLeft, - OverlayPanelResizeHandle.BottomLeft, + FloatingPanelResizeHandle.Left, + FloatingPanelResizeHandle.TopLeft, + FloatingPanelResizeHandle.BottomLeft, -> left = right - minWidth - OverlayPanelResizeHandle.Right, - OverlayPanelResizeHandle.TopRight, - OverlayPanelResizeHandle.BottomRight, + FloatingPanelResizeHandle.Right, + FloatingPanelResizeHandle.TopRight, + FloatingPanelResizeHandle.BottomRight, -> right = left + minWidth else -> right = left + minWidth @@ -343,14 +343,14 @@ class OverlayPanel( } if (bottom - top < minHeight) { when (handle) { - OverlayPanelResizeHandle.Top, - OverlayPanelResizeHandle.TopLeft, - OverlayPanelResizeHandle.TopRight, + FloatingPanelResizeHandle.Top, + FloatingPanelResizeHandle.TopLeft, + FloatingPanelResizeHandle.TopRight, -> top = bottom - minHeight - OverlayPanelResizeHandle.Bottom, - OverlayPanelResizeHandle.BottomLeft, - OverlayPanelResizeHandle.BottomRight, + FloatingPanelResizeHandle.Bottom, + FloatingPanelResizeHandle.BottomLeft, + FloatingPanelResizeHandle.BottomRight, -> bottom = top + minHeight else -> bottom = top + minHeight @@ -363,27 +363,27 @@ class OverlayPanel( val maxBottom = (viewportHeight - 2).coerceAtLeast(minY + minHeight) when (handle) { - OverlayPanelResizeHandle.Left, - OverlayPanelResizeHandle.TopLeft, - OverlayPanelResizeHandle.BottomLeft, + FloatingPanelResizeHandle.Left, + FloatingPanelResizeHandle.TopLeft, + FloatingPanelResizeHandle.BottomLeft, -> left = left.coerceIn(minX, right - minWidth) - OverlayPanelResizeHandle.Right, - OverlayPanelResizeHandle.TopRight, - OverlayPanelResizeHandle.BottomRight, + FloatingPanelResizeHandle.Right, + FloatingPanelResizeHandle.TopRight, + FloatingPanelResizeHandle.BottomRight, -> right = right.coerceIn(left + minWidth, maxRight) else -> Unit } when (handle) { - OverlayPanelResizeHandle.Top, - OverlayPanelResizeHandle.TopLeft, - OverlayPanelResizeHandle.TopRight, + FloatingPanelResizeHandle.Top, + FloatingPanelResizeHandle.TopLeft, + FloatingPanelResizeHandle.TopRight, -> top = top.coerceIn(minY, bottom - minHeight) - OverlayPanelResizeHandle.Bottom, - OverlayPanelResizeHandle.BottomLeft, - OverlayPanelResizeHandle.BottomRight, + FloatingPanelResizeHandle.Bottom, + FloatingPanelResizeHandle.BottomLeft, + FloatingPanelResizeHandle.BottomRight, -> bottom = bottom.coerceIn(top + minHeight, maxBottom) else -> Unit @@ -415,7 +415,7 @@ class OverlayPanel( } } - private fun buildFrame(panelRect: Rect): OverlayPanelFrame { + private fun buildFrame(panelRect: Rect): FloatingPanelFrame { val headerRect = Rect(panelRect.x, panelRect.y, panelRect.width, style.headerHeight) val bodyRect = Rect( @@ -431,7 +431,7 @@ class OverlayPanel( width = style.closeButtonWidth, height = style.closeButtonHeight, ) - return OverlayPanelFrame( + return FloatingPanelFrame( panelRect = panelRect, headerRect = headerRect, bodyRect = bodyRect, @@ -452,7 +452,7 @@ class OverlayPanel( ) } - private fun resolveResizeHandle(panelRect: Rect, mouseX: Int, mouseY: Int): OverlayPanelResizeHandle? { + private fun resolveResizeHandle(panelRect: Rect, mouseX: Int, mouseY: Int): FloatingPanelResizeHandle? { if (!panelRect.contains(mouseX, mouseY)) return null val edge = style.resizeHandleSize.coerceAtLeast(2) val leftZone = mouseX <= panelRect.x + edge @@ -460,18 +460,18 @@ class OverlayPanel( val topZone = mouseY <= panelRect.y + edge val bottomZone = mouseY >= panelRect.y + panelRect.height - edge - if (leftZone && topZone) return OverlayPanelResizeHandle.TopLeft - if (rightZone && topZone) return OverlayPanelResizeHandle.TopRight - if (leftZone && bottomZone) return OverlayPanelResizeHandle.BottomLeft - if (rightZone && bottomZone) return OverlayPanelResizeHandle.BottomRight - if (leftZone) return OverlayPanelResizeHandle.Left - if (rightZone) return OverlayPanelResizeHandle.Right - if (topZone) return OverlayPanelResizeHandle.Top - if (bottomZone) return OverlayPanelResizeHandle.Bottom + if (leftZone && topZone) return FloatingPanelResizeHandle.TopLeft + if (rightZone && topZone) return FloatingPanelResizeHandle.TopRight + if (leftZone && bottomZone) return FloatingPanelResizeHandle.BottomLeft + if (rightZone && bottomZone) return FloatingPanelResizeHandle.BottomRight + if (leftZone) return FloatingPanelResizeHandle.Left + if (rightZone) return FloatingPanelResizeHandle.Right + if (topZone) return FloatingPanelResizeHandle.Top + if (bottomZone) return FloatingPanelResizeHandle.Bottom return null } - private fun applyStyleToNodes(style: OverlayPanelStyle) { + private fun applyStyleToNodes(style: FloatingPanelStyle) { shadowNode.applyStyle { backgroundColor = style.panelShadowColor border { width = 0.px } @@ -516,7 +516,7 @@ class OverlayPanel( val newNode = TextNode( textSource = TextSource.Static(title), - key = "dsgl-overlay-panel-title-$ownerId", + key = "dsgl-floating-panel-title-$ownerId", ) newNode.applyStyle { textWrap = TextWrap.NoWrap @@ -540,14 +540,14 @@ class OverlayPanel( } } - private fun attachNodeBeforeOverlay(node: DOMNode?) { + private fun attachNodeBeforeFloatingContent(node: DOMNode?) { val local = node ?: return detachNode(local) - val overlay = overlayContentNode - if (overlay != null && overlay.parent === rootNode) { - val overlayIndex = rootNode.children.indexOf(overlay) - if (overlayIndex >= 0) { - rootNode.children.add(overlayIndex, local) + val floatingContent = floatingContentNode + if (floatingContent != null && floatingContent.parent === rootNode) { + val floatingContentIndex = rootNode.children.indexOf(floatingContent) + if (floatingContentIndex >= 0) { + rootNode.children.add(floatingContentIndex, local) local.parent = rootNode return } @@ -570,7 +570,7 @@ class OverlayPanel( titleNode.render(ctx, 0, 0, 0, 0) closeButtonNode.render(ctx, 0, 0, 0, 0) bodyContentNode?.render(ctx, 0, 0, 0, 0) - overlayContentNode?.render(ctx, 0, 0, 0, 0) + floatingContentNode?.render(ctx, 0, 0, 0, 0) return } @@ -625,7 +625,7 @@ class OverlayPanel( bodyRect.width, bodyRect.height, ) - overlayContentNode?.render( + floatingContentNode?.render( ctx, viewportRect.x, viewportRect.y, @@ -634,11 +634,11 @@ class OverlayPanel( ) } - private class OverlayPanelRootNode( - private val owner: OverlayPanel, + private class FloatingPanelRootNode( + private val owner: FloatingPanel, key: Any?, ) : DOMNode(key) { - override val styleType: String = "dsgl-overlay-panel" + override val styleType: String = "dsgl-floating-panel" override fun measure(ctx: UiMeasureContext): Size = Size(bounds.width.coerceAtLeast(0), bounds.height.coerceAtLeast(0)) diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanelDragSession.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/panel/FloatingPanelDragSession.kt similarity index 79% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanelDragSession.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/panel/FloatingPanelDragSession.kt index 9143c51..bd9f33f 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanelDragSession.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/panel/FloatingPanelDragSession.kt @@ -1,12 +1,12 @@ -package org.dreamfinity.dsgl.core.overlay.panel +package org.dreamfinity.dsgl.core.portal.panel -enum class OverlayPanelDragType { +enum class FloatingPanelDragType { PanelMove, PanelResize, Transient, } -enum class OverlayPanelResizeHandle { +enum class FloatingPanelResizeHandle { Left, Right, Top, @@ -17,12 +17,12 @@ enum class OverlayPanelResizeHandle { BottomRight, } -class OverlayPanelDragSession { +class FloatingPanelDragSession { var active: Boolean = false private set var ownerId: Any? = null private set - var type: OverlayPanelDragType? = null + var type: FloatingPanelDragType? = null private set var startPointerX: Int = 0 private set @@ -40,16 +40,16 @@ class OverlayPanelDragSession { private set var startPanelHeight: Int = 0 private set - var resizeHandle: OverlayPanelResizeHandle? = null + var resizeHandle: FloatingPanelResizeHandle? = null private set fun begin( ownerId: Any, - type: OverlayPanelDragType, + type: FloatingPanelDragType, pointerX: Int, pointerY: Int, - panelState: OverlayPanelState, - resizeHandle: OverlayPanelResizeHandle? = null, + panelState: FloatingPanelState, + resizeHandle: FloatingPanelResizeHandle? = null, ) { active = true this.ownerId = ownerId diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanelState.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/panel/FloatingPanelState.kt similarity index 91% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanelState.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/panel/FloatingPanelState.kt index 5ac6511..8c5a390 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanelState.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/panel/FloatingPanelState.kt @@ -1,8 +1,8 @@ -package org.dreamfinity.dsgl.core.overlay.panel +package org.dreamfinity.dsgl.core.portal.panel import org.dreamfinity.dsgl.core.dom.layout.Rect -class OverlayPanelState { +class FloatingPanelState { var visible: Boolean = false private set var x: Int = 0 diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayCommandDslRenderer.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalCommandDslRenderer.kt similarity index 71% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayCommandDslRenderer.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalCommandDslRenderer.kt index ab62af3..0c52351 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayCommandDslRenderer.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalCommandDslRenderer.kt @@ -1,25 +1,25 @@ -package org.dreamfinity.dsgl.core.overlay.system +package org.dreamfinity.dsgl.core.portal.system import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.render.RenderCommand -internal object SystemOverlayCommandDslRenderer { +internal object SystemPortalCommandDslRenderer { fun rebuildInto(parent: DOMNode, commands: List, keyPrefix: String): Boolean { val children = parent.children var changed = false commands.forEachIndexed { index, command -> val existing = children.getOrNull(index) - if (existing is SystemOverlayRawRenderCommandNode) { + if (existing is SystemPortalRawRenderCommandNode) { if (existing.updateRenderCommand(command)) { changed = true } - SystemOverlayDebugCounters.onRawNodeReused() + SystemPortalDebugCounters.onRawNodeReused() return@forEachIndexed } val replacement = - SystemOverlayRawRenderCommandNode( + SystemPortalRawRenderCommandNode( renderCommand = command, key = "$keyPrefix-$index", ) @@ -29,16 +29,16 @@ internal object SystemOverlayCommandDslRenderer { } else { existing.parent = null children[index] = replacement - SystemOverlayDebugCounters.onRawNodeRemoved() + SystemPortalDebugCounters.onRawNodeRemoved() } - SystemOverlayDebugCounters.onRawNodeCreated() + SystemPortalDebugCounters.onRawNodeCreated() changed = true } while (children.size > commands.size) { val removed = children.removeAt(children.lastIndex) removed.parent = null - SystemOverlayDebugCounters.onRawNodeRemoved() + SystemPortalDebugCounters.onRawNodeRemoved() changed = true } return changed diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayDebugCounters.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalDebugCounters.kt similarity index 91% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayDebugCounters.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalDebugCounters.kt index 81a3dc5..528e3cc 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayDebugCounters.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalDebugCounters.kt @@ -1,10 +1,10 @@ -package org.dreamfinity.dsgl.core.overlay.system +package org.dreamfinity.dsgl.core.portal.system -internal object SystemOverlayDebugCounters { +internal object SystemPortalDebugCounters { @Volatile private var enabled: Boolean = java.lang.Boolean - .getBoolean("dsgl.systemOverlay.debugCounters") + .getBoolean("dsgl.systemPortal.debugCounters") @Volatile private var rawNodeCreated: Long = 0L diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalEntries.kt similarity index 63% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalEntries.kt index c1ca9fc..10e6501 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntries.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalEntries.kt @@ -1,48 +1,48 @@ -package org.dreamfinity.dsgl.core.overlay.system +package org.dreamfinity.dsgl.core.portal.system import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.PortalDismissPolicy -import org.dreamfinity.dsgl.core.overlay.PortalEntry -import org.dreamfinity.dsgl.core.overlay.PortalEntryBounds -import org.dreamfinity.dsgl.core.overlay.PortalEntryId -import org.dreamfinity.dsgl.core.overlay.PortalEntryOrder -import org.dreamfinity.dsgl.core.overlay.PortalEntryPlacement -import org.dreamfinity.dsgl.core.overlay.PortalEntryState -import org.dreamfinity.dsgl.core.overlay.PortalFocusPolicy -import org.dreamfinity.dsgl.core.overlay.PortalInputPolicy -import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces -import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelDragSession -import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelState +import org.dreamfinity.dsgl.core.portal.PortalDismissPolicy +import org.dreamfinity.dsgl.core.portal.PortalEntry +import org.dreamfinity.dsgl.core.portal.PortalEntryBounds +import org.dreamfinity.dsgl.core.portal.PortalEntryId +import org.dreamfinity.dsgl.core.portal.PortalEntryOrder +import org.dreamfinity.dsgl.core.portal.PortalEntryPlacement +import org.dreamfinity.dsgl.core.portal.PortalEntryState +import org.dreamfinity.dsgl.core.portal.PortalFocusPolicy +import org.dreamfinity.dsgl.core.portal.PortalInputPolicy +import org.dreamfinity.dsgl.core.portal.ScreenDomainSurfaces +import org.dreamfinity.dsgl.core.portal.panel.FloatingPanelDragSession +import org.dreamfinity.dsgl.core.portal.panel.FloatingPanelState import java.util.IdentityHashMap -enum class SystemOverlayEntryId { +enum class SystemPortalEntryId { Inspector, ColorPickerPopup, ColorPickerTransient, TransientSession, } -enum class SystemOverlayLane( +enum class SystemPortalLane( val zOrder: Int, ) { PanelContent(0), Transient(1), } -class SystemOverlayEntryState( - val id: SystemOverlayEntryId, +class SystemPortalEntryState( + val id: SystemPortalEntryId, val order: Int, - val lane: SystemOverlayLane = SystemOverlayLane.PanelContent, - val panelState: OverlayPanelState = OverlayPanelState(), - val dragSession: OverlayPanelDragSession = OverlayPanelDragSession(), + val lane: SystemPortalLane = SystemPortalLane.PanelContent, + val panelState: FloatingPanelState = FloatingPanelState(), + val dragSession: FloatingPanelDragSession = FloatingPanelDragSession(), ) { var active: Boolean = false internal set } -internal data class SystemOverlayFrameContext( +internal data class SystemPortalFrameContext( val inspectedRoot: DOMNode?, val inspectedLayoutRevision: Long, val cursorX: Int, @@ -50,15 +50,15 @@ internal data class SystemOverlayFrameContext( val inspectorPointerCaptured: Boolean, ) -internal interface SystemOverlayEntry { - val state: SystemOverlayEntryState +internal interface SystemPortalEntry { + val state: SystemPortalEntryState val node: DOMNode fun participatesInDomInput(): Boolean = false fun enablesDomInputFallbackRouting(): Boolean = participatesInDomInput() - fun sync(frame: SystemOverlayFrameContext) + fun sync(frame: SystemPortalFrameContext) fun onInputFrame(viewportWidth: Int, viewportHeight: Int) = Unit @@ -75,25 +75,25 @@ internal interface SystemOverlayEntry { fun handleKeyUp(keyCode: Int, keyChar: Char): Boolean = false } -class SystemOverlayTransientSession( +class SystemPortalTransientSession( val ownerToken: Any, - val entryState: SystemOverlayEntryState = - SystemOverlayEntryState( - id = SystemOverlayEntryId.TransientSession, + val entryState: SystemPortalEntryState = + SystemPortalEntryState( + id = SystemPortalEntryId.TransientSession, order = Int.MAX_VALUE, ), ) -class SystemOverlayTransientOwnershipRegistry { - private val sessions: IdentityHashMap = IdentityHashMap() +class SystemPortalTransientOwnershipRegistry { + private val sessions: IdentityHashMap = IdentityHashMap() - fun resolve(ownerToken: Any): SystemOverlayTransientSession = + fun resolve(ownerToken: Any): SystemPortalTransientSession = sessions.getOrPut(ownerToken) { - SystemOverlayTransientSession(ownerToken = ownerToken) + SystemPortalTransientSession(ownerToken = ownerToken) } @Suppress("UnusedParameter") - fun resolve(ownerToken: Any, cursorX: Int, cursorY: Int): SystemOverlayTransientSession = resolve(ownerToken) + fun resolve(ownerToken: Any, cursorX: Int, cursorY: Int): SystemPortalTransientSession = resolve(ownerToken) fun release(ownerToken: Any): Boolean = sessions.remove(ownerToken) != null @@ -101,22 +101,22 @@ class SystemOverlayTransientOwnershipRegistry { sessions.clear() } - fun activeSessions(): List = sessions.values.toList() + fun activeSessions(): List = sessions.values.toList() } -internal class SystemOverlayEntryRegistry( - entries: List, +internal class SystemPortalEntryRegistry( + entries: List, ) { - private val orderedEntries: List = entries.sortedBy { it.state.order } - private val byId: Map = orderedEntries.associateBy { it.state.id } + private val orderedEntries: List = entries.sortedBy { it.state.order } + private val byId: Map = orderedEntries.associateBy { it.state.id } - fun allEntries(): List = orderedEntries + fun allEntries(): List = orderedEntries - fun entry(id: SystemOverlayEntryId): SystemOverlayEntry? = byId[id] + fun entry(id: SystemPortalEntryId): SystemPortalEntry? = byId[id] } -internal class SystemOverlayPortalEntry( - private val entry: SystemOverlayEntry, +internal class SystemPortalEntryAdapter( + private val entry: SystemPortalEntry, ) : PortalEntry { override val state: PortalEntryState = PortalEntryState( @@ -134,7 +134,7 @@ internal class SystemOverlayPortalEntry( ) override val node: DOMNode = entry.node - val systemEntry: SystemOverlayEntry + val systemEntry: SystemPortalEntry get() = entry fun syncPlacement(viewportWidth: Int, viewportHeight: Int) { @@ -178,13 +178,13 @@ internal class SystemOverlayPortalEntry( override fun handleKeyUp(keyCode: Int, keyChar: Char): Boolean = entry.handleKeyUp(keyCode, keyChar) - private fun SystemOverlayEntry.inputPolicy(): PortalInputPolicy = + private fun SystemPortalEntry.inputPolicy(): PortalInputPolicy = when { participatesInDomInput() || enablesDomInputFallbackRouting() -> PortalInputPolicy.ManualThenDomFallback else -> PortalInputPolicy.ManualOnly } - private fun SystemOverlayEntry.resolvePortalEntryBounds(viewportWidth: Int, viewportHeight: Int): Rect { + private fun SystemPortalEntry.resolvePortalEntryBounds(viewportWidth: Int, viewportHeight: Int): Rect { val panelBounds = state.panelState.currentRectOrNull() if (panelBounds != null && panelBounds.width > 0 && panelBounds.height > 0) { return panelBounds diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalHost.kt similarity index 76% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalHost.kt index 928cf61..57e136a 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayHost.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalHost.kt @@ -1,9 +1,9 @@ -package org.dreamfinity.dsgl.core.overlay.system +package org.dreamfinity.dsgl.core.portal.system import org.dreamfinity.dsgl.core.DomTree import org.dreamfinity.dsgl.core.colorpicker.* import org.dreamfinity.dsgl.core.colorpicker.internal.ColorPickerPopupMount -import org.dreamfinity.dsgl.core.colorpicker.internal.ColorPickerPopupOverlayNode +import org.dreamfinity.dsgl.core.colorpicker.internal.ColorPickerPopupPortalNode import org.dreamfinity.dsgl.core.colorpicker.internal.SystemColorPickerPortalService import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.elements.SingleLineInputNode @@ -13,55 +13,55 @@ import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.inspector.InspectorPanelState -import org.dreamfinity.dsgl.core.inspector.internal.SystemInspectorOverlayNode -import org.dreamfinity.dsgl.core.overlay.DomainPortalServices -import org.dreamfinity.dsgl.core.overlay.DomainSurfaceHost -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope -import org.dreamfinity.dsgl.core.overlay.PortalFrameContext -import org.dreamfinity.dsgl.core.overlay.PortalHost -import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurface -import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces -import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter -import org.dreamfinity.dsgl.core.overlay.input.dispatchManualThenDomFallback -import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanel -import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelStyle +import org.dreamfinity.dsgl.core.inspector.internal.SystemInspectorPortalNode +import org.dreamfinity.dsgl.core.portal.DomainPortalServices +import org.dreamfinity.dsgl.core.portal.DomainSurfaceHost +import org.dreamfinity.dsgl.core.portal.PortalFrameContext +import org.dreamfinity.dsgl.core.portal.PortalHost +import org.dreamfinity.dsgl.core.portal.ScreenDomainId +import org.dreamfinity.dsgl.core.portal.ScreenDomainSurface +import org.dreamfinity.dsgl.core.portal.ScreenDomainSurfaces +import org.dreamfinity.dsgl.core.portal.input.SurfaceDomInputRouter +import org.dreamfinity.dsgl.core.portal.input.dispatchManualThenDomFallback +import org.dreamfinity.dsgl.core.portal.panel.FloatingPanel +import org.dreamfinity.dsgl.core.portal.panel.FloatingPanelStyle import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.select.SelectPortalController import org.dreamfinity.dsgl.core.style.StyleApplicationScope @Suppress("TooManyFunctions") -class SystemOverlayHost( +class SystemPortalHost( private val inspectorController: InspectorController, ) : DomainSurfaceHost { override val surface: ScreenDomainSurface = ScreenDomainSurfaces.SystemPortal - private val rootNode: SystemOverlayRootNode = SystemOverlayRootNode() - private val inspectorEntry: SystemOverlayEntry = InspectorOverlayEntry(inspectorController) - private val colorPickerEntry: ColorPickerOverlayEntry = ColorPickerOverlayEntry() - private val colorPickerTransientEntry: SystemOverlayEntry = ColorPickerTransientOverlayEntry(colorPickerEntry) - private val entryRegistry: SystemOverlayEntryRegistry = - SystemOverlayEntryRegistry( + private val rootNode: SystemPortalRootNode = SystemPortalRootNode() + private val inspectorEntry: SystemPortalEntry = InspectorPortalEntry(inspectorController) + private val colorPickerEntry: ColorPickerPortalEntry = ColorPickerPortalEntry() + private val colorPickerTransientEntry: SystemPortalEntry = ColorPickerTransientPortalEntry(colorPickerEntry) + private val entryRegistry: SystemPortalEntryRegistry = + SystemPortalEntryRegistry( listOf(inspectorEntry, colorPickerEntry, colorPickerTransientEntry), ) private val portalHost: PortalHost = PortalHost(ScreenDomainSurfaces.SystemPortal) - private val portalEntries: List = - entryRegistry.allEntries().map(::SystemOverlayPortalEntry) - private val transientOwnershipRegistry: SystemOverlayTransientOwnershipRegistry = - SystemOverlayTransientOwnershipRegistry() + private val portalEntries: List = + entryRegistry.allEntries().map(::SystemPortalEntryAdapter) + private val transientOwnershipRegistry: SystemPortalTransientOwnershipRegistry = + SystemPortalTransientOwnershipRegistry() private val systemSelectPortal: SelectPortalController = SelectPortalController( engine = DomainPortalServices.systemSelectEngine, - ownerScope = OverlayOwnerScope.System, + ownerDomain = ScreenDomainId.System, entryId = "system.select", ) private val tree: DomTree = DomTree( root = rootNode, - styleScope = StyleApplicationScope.SystemOverlay, + styleScope = StyleApplicationScope.System, ) - private var frameContext: SystemOverlayFrameContext = - SystemOverlayFrameContext( + private var frameContext: SystemPortalFrameContext = + SystemPortalFrameContext( inspectedRoot = null, inspectedLayoutRevision = 0L, cursorX = 0, @@ -70,8 +70,8 @@ class SystemOverlayHost( ) private var knownViewportWidth: Int = 1 private var knownViewportHeight: Int = 1 - private val domInputRouter: LayerDomInputRouter = - LayerDomInputRouter( + private val domInputRouter: SurfaceDomInputRouter = + SurfaceDomInputRouter( rootProvider = { if (activeEntriesTopFirst().any { it.enablesDomInputFallbackRouting() }) rootNode else null }, @@ -103,7 +103,7 @@ class SystemOverlayHost( ) } - fun appendPortalOverlayCommands( + fun appendFloatingPortalCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, @@ -152,7 +152,7 @@ class SystemOverlayHost( inspectorPointerCaptured: Boolean, ) { frameContext = - SystemOverlayFrameContext( + SystemPortalFrameContext( inspectedRoot = inspectedRoot, inspectedLayoutRevision = inspectedLayoutRevision, cursorX = cursorX, @@ -223,22 +223,22 @@ class SystemOverlayHost( domInputRouter.clear() } - internal fun debugEntryState(id: SystemOverlayEntryId): SystemOverlayEntryState? = entryRegistry.entry(id)?.state + internal fun debugEntryState(id: SystemPortalEntryId): SystemPortalEntryState? = entryRegistry.entry(id)?.state - internal fun debugEntryNode(id: SystemOverlayEntryId): DOMNode? = entryRegistry.entry(id)?.node + internal fun debugEntryNode(id: SystemPortalEntryId): DOMNode? = entryRegistry.entry(id)?.node - internal fun debugRegisteredEntryIds(): List = entryRegistry.allEntries().map { it.state.id } + internal fun debugRegisteredEntryIds(): List = entryRegistry.allEntries().map { it.state.id } internal fun debugRegisteredPortalEntryIds(): List = portalEntries.map { it.state.id.value } internal fun debugActivePortalEntryIds(): List = portalHost.entriesInPaintOrder().map { it.state.id.value } - internal fun debugMountedEntryIds(): List { + internal fun debugMountedEntryIds(): List { val entriesByNode = portalEntries.associateBy { it.node } val mountedNodes = buildList { - addAll(rootNode.mountedLaneNodes(SystemOverlayLane.PanelContent)) - addAll(rootNode.mountedLaneNodes(SystemOverlayLane.Transient)) + addAll(rootNode.mountedLaneNodes(SystemPortalLane.PanelContent)) + addAll(rootNode.mountedLaneNodes(SystemPortalLane.Transient)) } return mountedNodes.mapNotNull { node -> entriesByNode[node] @@ -248,10 +248,10 @@ class SystemOverlayHost( } } - internal fun resolveTransientSession(ownerToken: Any): SystemOverlayTransientSession = + internal fun resolveTransientSession(ownerToken: Any): SystemPortalTransientSession = transientOwnershipRegistry.resolve(ownerToken) - internal fun resolveTransientSession(ownerToken: Any, cursorX: Int, cursorY: Int): SystemOverlayTransientSession = + internal fun resolveTransientSession(ownerToken: Any, cursorX: Int, cursorY: Int): SystemPortalTransientSession = transientOwnershipRegistry.resolve(ownerToken, cursorX, cursorY) internal fun releaseTransientSession(ownerToken: Any): Boolean = transientOwnershipRegistry.release(ownerToken) @@ -266,22 +266,22 @@ class SystemOverlayHost( internal fun debugSystemColorPickerState(): ColorPickerState? = colorPickerEntry.debugState() - internal fun debugSystemColorPickerPopupOwnerScope(): OverlayOwnerScope? = colorPickerEntry.debugOwnerScope() + internal fun debugSystemColorPickerPopupOwnerDomain(): ScreenDomainId? = colorPickerEntry.debugOwnerDomain() internal fun debugRootBounds(): Rect = rootNode.bounds private fun reconcileMountedEntries() { val activeEntries = portalHost.entriesInPaintOrder().mapNotNull { - (it as? SystemOverlayPortalEntry)?.systemEntry + (it as? SystemPortalEntryAdapter)?.systemEntry } val panelNodes = activeEntries - .filter { it.state.lane == SystemOverlayLane.PanelContent } + .filter { it.state.lane == SystemPortalLane.PanelContent } .map { it.node } val transientNodes = activeEntries - .filter { it.state.lane == SystemOverlayLane.Transient } + .filter { it.state.lane == SystemPortalLane.Transient } .map { it.node } rootNode.setLaneChildren( panelNodes = panelNodes, @@ -289,36 +289,36 @@ class SystemOverlayHost( ) } - private fun activeEntriesTopFirst(): List = + private fun activeEntriesTopFirst(): List = portalHost .entriesInInputOrder() - .mapNotNull { (it as? SystemOverlayPortalEntry)?.systemEntry } + .mapNotNull { (it as? SystemPortalEntryAdapter)?.systemEntry } - private inline fun dispatchManualInput(handler: (SystemOverlayEntry) -> Boolean): Boolean = + private inline fun dispatchManualInput(handler: (SystemPortalEntry) -> Boolean): Boolean = activeEntriesTopFirst() .asSequence() .filter { entry -> !entry.participatesInDomInput() } .any(handler) - private class InspectorOverlayEntry( + private class InspectorPortalEntry( private val inspectorController: InspectorController, - ) : SystemOverlayEntry { - override val state: SystemOverlayEntryState = - SystemOverlayEntryState( - id = SystemOverlayEntryId.Inspector, + ) : SystemPortalEntry { + override val state: SystemPortalEntryState = + SystemPortalEntryState( + id = SystemPortalEntryId.Inspector, order = 100, - lane = SystemOverlayLane.PanelContent, + lane = SystemPortalLane.PanelContent, ) - private val overlayPanel: OverlayPanel = - OverlayPanel( + private val floatingPanel: FloatingPanel = + FloatingPanel( ownerId = state.id, panelState = state.panelState, dragSession = state.dragSession, ) - override val node: SystemInspectorOverlayNode = - SystemInspectorOverlayNode( + override val node: SystemInspectorPortalNode = + SystemInspectorPortalNode( controller = inspectorController, - overlayPanel = overlayPanel, + floatingPanel = floatingPanel, ) private var viewportWidth: Int = 1 private var viewportHeight: Int = 1 @@ -330,7 +330,7 @@ class SystemOverlayHost( this.viewportHeight = viewportHeight.coerceAtLeast(1) } - override fun sync(frame: SystemOverlayFrameContext) { + override fun sync(frame: SystemPortalFrameContext) { node.syncInputBounds(viewportWidth, viewportHeight) node.bindInspectedTree(frame.inspectedRoot, frame.inspectedLayoutRevision) node.updateCursor(frame.cursorX, frame.cursorY, frame.inspectorPointerCaptured) @@ -338,16 +338,16 @@ class SystemOverlayHost( inspectorController.onLayoutCommitted(root, frame.inspectedLayoutRevision) } state.active = inspectorController.active - inspectorController.setOverlayPanelAuthorityEnabled(state.active) + inspectorController.setFloatingPanelAuthorityEnabled(state.active) if (!state.active) { state.panelState.hide() state.dragSession.end() - overlayPanel.syncPanelRect(null) - inspectorController.onOverlayPanelPointerCaptureChanged(false) + floatingPanel.syncPanelRect(null) + inspectorController.onFloatingPanelPointerCaptureChanged(false) return } if (inspectorController.panelState == InspectorPanelState.Expanded) { - overlayPanel.configure( + floatingPanel.configure( title = "Inspector", draggable = true, resizable = true, @@ -356,42 +356,42 @@ class SystemOverlayHost( style = inspectorPanelStyle(), onClose = inspectorController::onPanelMinimizeTogglePressed, ) - val panelRect = inspectorController.overlayExpandedPanelRect() + val panelRect = inspectorController.floatingExpandedPanelRect() if (panelRect != null) { - inspectorController.onOverlayPanelRectChanged(panelRect, viewportWidth, viewportHeight) - overlayPanel.syncPanelRect(inspectorController.overlayExpandedPanelRect()) + inspectorController.onFloatingPanelRectChanged(panelRect, viewportWidth, viewportHeight) + floatingPanel.syncPanelRect(inspectorController.floatingExpandedPanelRect()) } else { state.panelState.show() - overlayPanel.syncPanelRect(state.panelState.currentRectOrNull()) + floatingPanel.syncPanelRect(state.panelState.currentRectOrNull()) } - val dragUpdatedByDomInput = node.consumeOverlayPanelDomDragUpdate() + val dragUpdatedByDomInput = node.consumeFloatingPanelDomDragUpdate() if (!dragUpdatedByDomInput) { - overlayPanel.handleMouseMove( + floatingPanel.handleMouseMove( mouseX = frame.cursorX, mouseY = frame.cursorY, viewportWidth = viewportWidth, viewportHeight = viewportHeight, ) { rect -> - inspectorController.onOverlayPanelRectChanged(rect, viewportWidth, viewportHeight) + inspectorController.onFloatingPanelRectChanged(rect, viewportWidth, viewportHeight) } } } else { state.panelState.hide() state.dragSession.end() - overlayPanel.syncPanelRect(null) - inspectorController.onOverlayPanelPointerCaptureChanged(false) + floatingPanel.syncPanelRect(null) + inspectorController.onFloatingPanelPointerCaptureChanged(false) } } } - private class ColorPickerOverlayEntry : - SystemOverlayEntry, + private class ColorPickerPortalEntry : + SystemPortalEntry, SystemColorPickerPortalService { - override val state: SystemOverlayEntryState = - SystemOverlayEntryState( - id = SystemOverlayEntryId.ColorPickerPopup, + override val state: SystemPortalEntryState = + SystemPortalEntryState( + id = SystemPortalEntryId.ColorPickerPopup, order = 200, - lane = SystemOverlayLane.PanelContent, + lane = SystemPortalLane.PanelContent, ) private val popupMount: ColorPickerPopupMount = ColorPickerPopupMount( @@ -399,7 +399,7 @@ class SystemOverlayHost( panelState = state.panelState, dragSession = state.dragSession, ) - override val node: ColorPickerPopupOverlayNode = popupMount.node + override val node: ColorPickerPopupPortalNode = popupMount.node private var draggable: Boolean = true private var viewportWidth: Int = 1 private var viewportHeight: Int = 1 @@ -407,7 +407,7 @@ class SystemOverlayHost( override fun enablesDomInputFallbackRouting(): Boolean = true - override fun sync(frame: SystemOverlayFrameContext) { + override fun sync(frame: SystemPortalFrameContext) { node.updateCursor(frame.cursorX, frame.cursorY) state.active = popupMount.popupEngine.isOpenFor(popupMount.ownerToken) if (!state.active) { @@ -417,24 +417,24 @@ class SystemOverlayHost( domDelegatedBodyPressActive = false return } - popupMount.overlayPanel.configure( + popupMount.floatingPanel.configure( title = popupMount.popupEngine.debugTitle(popupMount.ownerToken) ?: "Color Picker", draggable = draggable, style = popupMount.popupEngine .debugStyle(popupMount.ownerToken) - ?.let { toOverlayPanelStyle(it) } - ?: OverlayPanelStyle(), + ?.let { toFloatingPanelStyle(it) } + ?: FloatingPanelStyle(), onClose = ::close, ) val panelRect = popupMount.popupEngine.debugPanelRect(popupMount.ownerToken) if (panelRect != null) { - popupMount.overlayPanel.syncPanelRect(panelRect) + popupMount.floatingPanel.syncPanelRect(panelRect) } else { state.panelState.show() - popupMount.overlayPanel.syncPanelRect(state.panelState.currentRectOrNull()) + popupMount.floatingPanel.syncPanelRect(state.panelState.currentRectOrNull()) } - if (popupMount.overlayPanel.handleMouseMove( + if (popupMount.floatingPanel.handleMouseMove( mouseX = frame.cursorX, mouseY = frame.cursorY, viewportWidth = viewportWidth, @@ -458,7 +458,7 @@ class SystemOverlayHost( if (!state.active) return false if (domDelegatedBodyPressActive) return false popupMount.popupEngine.onCursorPosition(mouseX, mouseY) - if (popupMount.overlayPanel.handleMouseMove( + if (popupMount.floatingPanel.handleMouseMove( mouseX = mouseX, mouseY = mouseY, viewportWidth = viewportWidth, @@ -478,7 +478,7 @@ class SystemOverlayHost( if (button != MouseButton.LEFT) { domDelegatedBodyPressActive = false } - if (popupMount.overlayPanel.handleMouseDown(mouseX, mouseY, button)) { + if (popupMount.floatingPanel.handleMouseDown(mouseX, mouseY, button)) { return true } if (popupMount.popupEngine.shouldRouteSystemInputSlotMouseDownToDom(mouseX, mouseY, button)) { @@ -502,7 +502,7 @@ class SystemOverlayHost( domDelegatedBodyPressActive = false return false } - if (popupMount.overlayPanel.handleMouseUp( + if (popupMount.floatingPanel.handleMouseUp( mouseX = mouseX, mouseY = mouseY, button = button, @@ -531,7 +531,7 @@ class SystemOverlayHost( } private fun shouldRouteSystemTextInputKeyDownToDom(): Boolean { - if (popupMount.popupEngine.debugOwnerScope(popupMount.ownerToken) != OverlayOwnerScope.System) return false + if (popupMount.popupEngine.debugOwnerDomain(popupMount.ownerToken) != ScreenDomainId.System) return false val focused = FocusManager.focusedNode() ?: return false if (focused !is SingleLineInputNode) return false val key = focused.key as? String ?: return false @@ -555,7 +555,7 @@ class SystemOverlayHost( popupMount.popupEngine.open( ColorPickerPopupRequest( owner = popupMount.ownerToken, - ownerScope = OverlayOwnerScope.System, + ownerDomain = ScreenDomainId.System, anchorRect = anchorRect, title = title, state = state, @@ -582,16 +582,16 @@ class SystemOverlayHost( override fun isOpen(): Boolean = popupMount.popupEngine.isOpenFor(popupMount.ownerToken) - fun transientOverlayNode(): DOMNode = popupMount.transientNode + fun transientPortalNode(): DOMNode = popupMount.transientNode fun isTransientActive(): Boolean { val controller = popupMount.popupEngine.debugController(popupMount.ownerToken) ?: return false return controller.viewModeDropdownOpen() || controller.isEyedropperActive() } - fun debugHeaderRect(): Rect? = popupMount.overlayPanel.headerRect() + fun debugHeaderRect(): Rect? = popupMount.floatingPanel.headerRect() - fun debugCloseRect(): Rect? = popupMount.overlayPanel.closeRect() + fun debugCloseRect(): Rect? = popupMount.floatingPanel.closeRect() fun debugBodyLayout(): ColorPickerLayout? = popupMount.popupEngine.debugBodyLayout(popupMount.ownerToken) @@ -607,28 +607,28 @@ class SystemOverlayHost( } } - fun debugOwnerScope(): OverlayOwnerScope? = popupMount.popupEngine.debugOwnerScope(popupMount.ownerToken) + fun debugOwnerDomain(): ScreenDomainId? = popupMount.popupEngine.debugOwnerDomain(popupMount.ownerToken) } - private class ColorPickerTransientOverlayEntry( - private val panelEntry: ColorPickerOverlayEntry, - ) : SystemOverlayEntry { - override val state: SystemOverlayEntryState = - SystemOverlayEntryState( - id = SystemOverlayEntryId.ColorPickerTransient, + private class ColorPickerTransientPortalEntry( + private val panelEntry: ColorPickerPortalEntry, + ) : SystemPortalEntry { + override val state: SystemPortalEntryState = + SystemPortalEntryState( + id = SystemPortalEntryId.ColorPickerTransient, order = 210, - lane = SystemOverlayLane.Transient, + lane = SystemPortalLane.Transient, ) - override val node: DOMNode = panelEntry.transientOverlayNode() + override val node: DOMNode = panelEntry.transientPortalNode() - override fun sync(frame: SystemOverlayFrameContext) { + override fun sync(frame: SystemPortalFrameContext) { state.active = panelEntry.state.active && panelEntry.isTransientActive() } } private companion object { - private fun inspectorPanelStyle(): OverlayPanelStyle = - OverlayPanelStyle( + private fun inspectorPanelStyle(): FloatingPanelStyle = + FloatingPanelStyle( headerHeight = 52, panelPadding = 6, resizeHandleSize = 8, @@ -644,8 +644,8 @@ class SystemOverlayHost( closeGlyph = "-", ) - private fun toOverlayPanelStyle(style: ColorPickerStyle): OverlayPanelStyle = - OverlayPanelStyle( + private fun toFloatingPanelStyle(style: ColorPickerStyle): FloatingPanelStyle = + FloatingPanelStyle( panelBackgroundColor = style.panelBackgroundColor, panelBorderColor = style.panelBorderColor, panelShadowColor = style.panelShadowColor, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayRawRenderCommandNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalRawRenderCommandNode.kt similarity index 92% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayRawRenderCommandNode.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalRawRenderCommandNode.kt index d70e320..603f8b7 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayRawRenderCommandNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalRawRenderCommandNode.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay.system +package org.dreamfinity.dsgl.core.portal.system import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.layout.Rect @@ -6,7 +6,7 @@ import org.dreamfinity.dsgl.core.dom.layout.Size import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.render.RenderCommand -internal class SystemOverlayRawRenderCommandNode( +internal class SystemPortalRawRenderCommandNode( renderCommand: RenderCommand, key: Any?, ) : DOMNode(key) { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayRootNode.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalRootNode.kt similarity index 72% rename from core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayRootNode.kt rename to core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalRootNode.kt index d10adf3..277035a 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayRootNode.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalRootNode.kt @@ -1,7 +1,7 @@ -package org.dreamfinity.dsgl.core.overlay.system +package org.dreamfinity.dsgl.core.portal.system import org.dreamfinity.dsgl.core.DsglColors -import org.dreamfinity.dsgl.core.debug.OverlayLayerDebugState.isTintEnabled +import org.dreamfinity.dsgl.core.debug.DomainSurfaceDebugState.isTintEnabled import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Border @@ -11,33 +11,33 @@ import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.dsl.UiScope import org.dreamfinity.dsgl.core.dsl.div import org.dreamfinity.dsgl.core.font.FontRegistry -import org.dreamfinity.dsgl.core.overlay.OverlayDebugVisualization -import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces +import org.dreamfinity.dsgl.core.portal.DomainSurfaceDebugVisualization +import org.dreamfinity.dsgl.core.portal.ScreenDomainSurfaces import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.StyleEngine -internal class SystemOverlayRootNode( - key: Any? = "dsgl-system-overlay-root", +internal class SystemPortalRootNode( + key: Any? = "dsgl-system-portal-root", ) : DOMNode(key) { - override val styleType: String = "dsgl-system-overlay-root" + override val styleType: String = "dsgl-system-portal-root" private var viewportWidth: Int = 0 private var viewportHeight: Int = 0 private val debugTintNode: ContainerNode = UiScope(this).div({ - this.key = "dsgl-system-overlay-debug-tint" + this.key = "dsgl-system-portal-debug-tint" style = { display = Display.None } }) - private val panelLaneNode: SystemOverlayLaneNode = - SystemOverlayLaneNode( - key = "dsgl-system-overlay-panel-lane", - laneStyleType = "dsgl-system-overlay-panel-lane", + private val panelLaneNode: SystemPortalLaneNode = + SystemPortalLaneNode( + key = "dsgl-system-portal-panel-lane", + laneStyleType = "dsgl-system-portal-panel-lane", ) - private val transientLaneNode: SystemOverlayLaneNode = - SystemOverlayLaneNode( - key = "dsgl-system-overlay-transient-lane", - laneStyleType = "dsgl-system-overlay-transient-lane", + private val transientLaneNode: SystemPortalLaneNode = + SystemPortalLaneNode( + key = "dsgl-system-portal-transient-lane", + laneStyleType = "dsgl-system-portal-transient-lane", ) init { @@ -61,10 +61,10 @@ internal class SystemOverlayRootNode( reconcileLane(transientLaneNode, transientNodes) } - internal fun mountedLaneNodes(lane: SystemOverlayLane): List = + internal fun mountedLaneNodes(lane: SystemPortalLane): List = when (lane) { - SystemOverlayLane.PanelContent -> panelLaneNode.children.toList() - SystemOverlayLane.Transient -> transientLaneNode.children.toList() + SystemPortalLane.PanelContent -> panelLaneNode.children.toList() + SystemPortalLane.Transient -> transientLaneNode.children.toList() } override fun measure(ctx: UiMeasureContext): Size { @@ -85,11 +85,11 @@ internal class SystemOverlayRootNode( ) { setViewportBounds(width, height) bounds = Rect(0, 0, viewportWidth, viewportHeight) - val tintEnabled = OverlayDebugVisualization.enabled && isTintEnabled(ScreenDomainSurfaces.SystemPortal) + val tintEnabled = DomainSurfaceDebugVisualization.enabled && isTintEnabled(ScreenDomainSurfaces.SystemPortal) if (tintEnabled) { debugTintNode.display = Display.Block - debugTintNode.backgroundColor = OverlayDebugVisualization.systemOverlayFillColor - debugTintNode.border = Border.all(1, OverlayDebugVisualization.systemOverlayBorderColor) + debugTintNode.backgroundColor = DomainSurfaceDebugVisualization.systemPortalFillColor + debugTintNode.border = Border.all(1, DomainSurfaceDebugVisualization.systemPortalBorderColor) debugTintNode.render(ctx, bounds.x, bounds.y, bounds.width, bounds.height) } else { debugTintNode.display = Display.None @@ -105,7 +105,7 @@ internal class SystemOverlayRootNode( override fun defaultFontSize(): Int = 16 - private fun reconcileLane(laneNode: SystemOverlayLaneNode, desiredNodes: List) { + private fun reconcileLane(laneNode: SystemPortalLaneNode, desiredNodes: List) { val currentNodes = laneNode.children val unchanged = currentNodes.size == desiredNodes.size && @@ -122,7 +122,7 @@ internal class SystemOverlayRootNode( } } -private class SystemOverlayLaneNode( +private class SystemPortalLaneNode( key: Any?, private val laneStyleType: String, ) : DOMNode(key) { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/render/RenderCommand.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/render/RenderCommand.kt index 4bf09e4..ad54820 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/render/RenderCommand.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/render/RenderCommand.kt @@ -133,11 +133,11 @@ sealed class RenderCommand { val y: Int, val width: Int, val height: Int, - val gridOverlay: CapturedGridOverlay? = null, + val grid: CapturedGrid? = null, ) : RenderCommand() - /** Optional grid overlay rendered by backend as part of captured-region magnifier pass. */ - data class CapturedGridOverlay( + /** Optional grid rendered by backend as part of captured-region magnifier pass. */ + data class CapturedGrid( val columns: Int, val rows: Int, val magnification: Int, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt index af18c83..7aa8650 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectEngine.kt @@ -224,7 +224,7 @@ class SelectEngine( ensureLayout() } - fun appendOverlayCommands( + fun appendPortalCommands( measureContext: UiMeasureContext, viewportWidth: Int, viewportHeight: Int, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt index e4adf2a..b748be6 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalController.kt @@ -10,37 +10,37 @@ import org.dreamfinity.dsgl.core.event.MouseDownEvent import org.dreamfinity.dsgl.core.event.MouseMoveEvent import org.dreamfinity.dsgl.core.event.MouseUpEvent import org.dreamfinity.dsgl.core.event.MouseWheelEvent -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope -import org.dreamfinity.dsgl.core.overlay.PortalDismissPolicy -import org.dreamfinity.dsgl.core.overlay.PortalEntry -import org.dreamfinity.dsgl.core.overlay.PortalEntryBounds -import org.dreamfinity.dsgl.core.overlay.PortalEntryId -import org.dreamfinity.dsgl.core.overlay.PortalEntryOrder -import org.dreamfinity.dsgl.core.overlay.PortalEntryPlacement -import org.dreamfinity.dsgl.core.overlay.PortalEntryState -import org.dreamfinity.dsgl.core.overlay.PortalFocusPolicy -import org.dreamfinity.dsgl.core.overlay.PortalHost -import org.dreamfinity.dsgl.core.overlay.PortalInputPolicy -import org.dreamfinity.dsgl.core.overlay.PortalInsidePointerPolicy -import org.dreamfinity.dsgl.core.overlay.PortalPointerDispatch -import org.dreamfinity.dsgl.core.overlay.PortalPointerPolicyResult -import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces -import org.dreamfinity.dsgl.core.overlay.evaluateOutsidePointerDown -import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter +import org.dreamfinity.dsgl.core.portal.PortalDismissPolicy +import org.dreamfinity.dsgl.core.portal.PortalEntry +import org.dreamfinity.dsgl.core.portal.PortalEntryBounds +import org.dreamfinity.dsgl.core.portal.PortalEntryId +import org.dreamfinity.dsgl.core.portal.PortalEntryOrder +import org.dreamfinity.dsgl.core.portal.PortalEntryPlacement +import org.dreamfinity.dsgl.core.portal.PortalEntryState +import org.dreamfinity.dsgl.core.portal.PortalFocusPolicy +import org.dreamfinity.dsgl.core.portal.PortalHost +import org.dreamfinity.dsgl.core.portal.PortalInputPolicy +import org.dreamfinity.dsgl.core.portal.PortalInsidePointerPolicy +import org.dreamfinity.dsgl.core.portal.PortalPointerDispatch +import org.dreamfinity.dsgl.core.portal.PortalPointerPolicyResult +import org.dreamfinity.dsgl.core.portal.ScreenDomainId +import org.dreamfinity.dsgl.core.portal.ScreenDomainSurfaces +import org.dreamfinity.dsgl.core.portal.evaluateOutsidePointerDown +import org.dreamfinity.dsgl.core.portal.input.SurfaceDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand @Suppress("TooManyFunctions") internal class SelectPortalController( private val engine: SelectEngine, - ownerScope: OverlayOwnerScope, + ownerDomain: ScreenDomainId, entryId: String, ) : PortalPointerDispatch { private val portalHost: PortalHost = - PortalHost(ScreenDomainSurfaces.portalSurfaceForOwner(ownerScope)) + PortalHost(ScreenDomainSurfaces.portalSurfaceForDomain(ownerDomain)) private val entry: SelectPortalEntry = SelectPortalEntry( engine = engine, - ownerScope = ownerScope, + ownerDomain = ownerDomain, entryId = entryId, ) @@ -118,14 +118,14 @@ internal data class SelectPortalDebugState( private class SelectPortalEntry( private val engine: SelectEngine, - ownerScope: OverlayOwnerScope, + ownerDomain: ScreenDomainId, entryId: String, ) : PortalEntry { override val state: PortalEntryState = PortalEntryState( id = PortalEntryId(entryId), ownerToken = engine, - surface = ScreenDomainSurfaces.portalSurfaceForOwner(ownerScope), + surface = ScreenDomainSurfaces.portalSurfaceForDomain(ownerDomain), order = PortalEntryOrder(zIndex = 0), dismissPolicy = PortalDismissPolicy.EscapeOrOutsidePointerDown, inputPolicy = PortalInputPolicy.DomOnly, @@ -152,7 +152,7 @@ private class SelectPortalEntry( }, ) override val node: DOMNode = popupNode - private val domInputRouter: LayerDomInputRouter = LayerDomInputRouter { node } + private val domInputRouter: SurfaceDomInputRouter = SurfaceDomInputRouter { node } private var viewportWidth: Int = 1 private var viewportHeight: Int = 1 private var measureContext: UiMeasureContext? = null @@ -284,7 +284,7 @@ private class SelectPortalNode( override fun buildRenderCommands(ctx: UiMeasureContext, out: MutableList) { if (!engine.isOpen()) return - engine.appendOverlayCommands( + engine.appendPortalCommands( measureContext = ctx, viewportWidth = viewportWidth, viewportHeight = viewportHeight, diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalRequest.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalRequest.kt index 94e3cb6..d046a2f 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalRequest.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalRequest.kt @@ -1,7 +1,7 @@ package org.dreamfinity.dsgl.core.select import org.dreamfinity.dsgl.core.dom.layout.Rect -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope +import org.dreamfinity.dsgl.core.portal.ScreenDomainId data class SelectOpenRequest( val owner: Any, @@ -14,7 +14,7 @@ data class SelectOpenRequest( val onClose: (() -> Unit)? = null, val fontId: String? = null, val fontSize: Int? = null, - val ownerScope: OverlayOwnerScope = OverlayOwnerScope.Application, + val ownerDomain: ScreenDomainId = ScreenDomainId.Application, ) fun interface SelectClock { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleApplicationScope.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleApplicationScope.kt index cf97075..dfec0b2 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleApplicationScope.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleApplicationScope.kt @@ -2,6 +2,6 @@ package org.dreamfinity.dsgl.core.style enum class StyleApplicationScope { Application, - SystemOverlay, + System, Debug, } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt index d583067..bb13c03 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt @@ -756,7 +756,7 @@ object StyleEngine { private fun snapshotForScope(scope: StyleApplicationScope): StylesheetSnapshot = when (scope) { StyleApplicationScope.Application -> StylesheetManager.snapshot() - StyleApplicationScope.SystemOverlay, + StyleApplicationScope.System, StyleApplicationScope.Debug, -> StylesheetSnapshot( diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/ContainerDslTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/ContainerDslTests.kt new file mode 100644 index 0000000..09e6ec1 --- /dev/null +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/ContainerDslTests.kt @@ -0,0 +1,34 @@ +package org.dreamfinity.dsgl.core + +import org.dreamfinity.dsgl.core.dom.elements.ContainerNode +import org.dreamfinity.dsgl.core.dsl.div +import org.dreamfinity.dsgl.core.dsl.ui +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ContainerDslTests { + @Test + fun `div overlapChildren opts into overlapping child layout`() { + val tree = + ui { + div({ + key = "plain" + }) + div({ + key = "overlap" + overlapChildren = true + }) + } + + val plain = + tree.root.children + .first { node -> node.key == "plain" } as ContainerNode + val overlap = + tree.root.children + .first { node -> node.key == "overlap" } as ContainerNode + + assertFalse(plain.stackLayout) + assertTrue(overlap.stackLayout) + } +} diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/DomTreeCachingTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/DomTreeCachingTests.kt index b6b6ace..82115f8 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/DomTreeCachingTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/DomTreeCachingTests.kt @@ -192,7 +192,7 @@ class DomTreeCachingTests { fun `modal portal does not duplicate child commands in chunk assembly`() { val host = ModalPortalAnchorNode(key = "modal-host") CountingRectNode(color = 0xFFAA3300.toInt(), key = "content").applyParent(host) - CountingRectNode(color = 0xFF0033AA.toInt(), key = "overlay").applyParent(host) + CountingRectNode(color = 0xFF0033AA.toInt(), key = "floating-layer").applyParent(host) val tree = DomTree(host) tree.render(ctx, 320, 180) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerControllerTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerControllerTests.kt index 99c5032..6485d2f 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerControllerTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerControllerTests.kt @@ -138,7 +138,7 @@ class ColorPickerControllerTests { } @Test - fun `eyedropper overlay shows mode and formatted value tooltip`() { + fun `eyedropper preview shows mode and formatted value tooltip`() { val sampled = 0xFF336699.toInt() val sampler = object : ScreenColorSampler { @@ -160,7 +160,7 @@ class ColorPickerControllerTests { controller.handleMouseMove(120, 160, layout) controller.sampleEyedropperAtHover() val out = ArrayList() - controller.appendEyedropperOverlay(800, 600, out) + controller.appendEyedropperPreview(800, 600, out) val textCommands = out.filterIsInstance() assertTrue(textCommands.any { it.text.contains("Mode: RGB (ARGB)") }) @@ -175,7 +175,7 @@ class ColorPickerControllerTests { } @Test - fun `eyedropper overlay preview path does not use area sampling`() { + fun `eyedropper preview preview path does not use area sampling`() { val sampler = RecordingSampler() val controller = ColorPickerController( @@ -192,7 +192,7 @@ class ColorPickerControllerTests { controller.handleMouseMove(80, 90, layout) controller.sampleEyedropperAtHover() val out = ArrayList() - controller.appendEyedropperOverlay(640, 480, out) + controller.appendEyedropperPreview(640, 480, out) assertEquals(0, sampler.areaCalls) assertTrue(sampler.colorCalls > 0) @@ -201,7 +201,7 @@ class ColorPickerControllerTests { } @Test - fun `eyedropper overlay emits capture and textured magnifier commands instead of per-cell rectangles`() { + fun `eyedropper preview emits capture and textured magnifier commands instead of per-cell rectangles`() { val controller = ColorPickerController( initial = @@ -215,7 +215,7 @@ class ColorPickerControllerTests { val layout = controller.buildLayout(Rect(0, 0, 360, controller.preferredHeight(true))) controller.handleMouseMove(80, 90, layout) val out = ArrayList() - controller.appendEyedropperOverlay(640, 480, out) + controller.appendEyedropperPreview(640, 480, out) assertTrue(out.any { it is RenderCommand.CaptureScreenRegion }) assertTrue(out.any { it is RenderCommand.DrawCapturedScreenRegion }) @@ -227,7 +227,7 @@ class ColorPickerControllerTests { } @Test - fun `eyedropper overlay draws aligned light grid over captured magnifier`() { + fun `eyedropper preview draws aligned light grid over captured magnifier`() { val gridColor = 0x7F57C2FF val controller = ColorPickerController( @@ -241,15 +241,15 @@ class ColorPickerControllerTests { ColorPickerStyle( eyedropperGridSize = 5, eyedropperCellSize = 4, - eyedropperGridOverlayEnabled = true, - eyedropperGridOverlayColor = gridColor, + eyedropperGridEnabled = true, + eyedropperGridColor = gridColor, ), ) controller.beginEyedropper() val layout = controller.buildLayout(Rect(0, 0, 360, controller.preferredHeight(true))) controller.handleMouseMove(80, 90, layout) val out = ArrayList() - controller.appendEyedropperOverlay(640, 480, out) + controller.appendEyedropperPreview(640, 480, out) val magnifier = out.filterIsInstance().single() val gridLines = out.filterIsInstance().filter { it.color == gridColor } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInlineNodeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInlineNodeTests.kt index 6001b9a..10c2e09 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInlineNodeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerInlineNodeTests.kt @@ -188,7 +188,7 @@ class ColorPickerInlineNodeTests { } @Test - fun `inline picker draws eyedropper overlay after clicking pipette`() { + fun `inline picker draws eyedropper preview after clicking pipette`() { val picker = ColorPickerInlineNode( controlled = true, @@ -390,7 +390,7 @@ class ColorPickerInlineNodeTests { private fun buildGlobalEyedropperCommands(picker: ColorPickerInlineNode): List { val out = ArrayList() - picker.appendEyedropperOverlayCommands( + picker.appendEyedropperPortalCommands( viewportWidth = 1920, viewportHeight = 1080, out = out, diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngineTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngineTests.kt index b3b415c..282619c 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngineTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/colorpicker/ColorPickerPopupEngineTests.kt @@ -3,8 +3,8 @@ package org.dreamfinity.dsgl.core.colorpicker import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope -import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces +import org.dreamfinity.dsgl.core.portal.ScreenDomainId +import org.dreamfinity.dsgl.core.portal.ScreenDomainSurfaces import org.dreamfinity.dsgl.core.render.RenderCommand import kotlin.test.Test import kotlin.test.assertEquals @@ -444,14 +444,14 @@ class ColorPickerPopupEngineTests { } @Test - fun `app-owned pipette emits transient overlay commands in application layer contract`() { + fun `app-owned pipette emits transient portal commands in application portal contract`() { val engine = ColorPickerPopupEngine() val owner = "owner-app" engine.onFrame(900, 700) engine.open( ColorPickerPopupRequest( owner = owner, - ownerScope = OverlayOwnerScope.Application, + ownerDomain = ScreenDomainId.Application, anchorRect = Rect(120, 80, 18, 18), state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), ), @@ -460,26 +460,26 @@ class ColorPickerPopupEngineTests { assertTrue(engine.handleMouseDown(layout.pipetteRect.x + 2, layout.pipetteRect.y + 2, MouseButton.LEFT)) assertTrue(engine.handleMouseMove(layout.pipetteRect.x + 24, layout.pipetteRect.y + 24)) - val overlay = mutableListOf() - engine.appendEyedropperOverlayCommands(900, 700, overlay) + val portalCommands = mutableListOf() + engine.appendEyedropperPortalCommands(900, 700, portalCommands) - assertTrue(overlay.isNotEmpty()) - assertEquals(OverlayOwnerScope.Application, engine.debugActiveOwnerScope()) + assertTrue(portalCommands.isNotEmpty()) + assertEquals(ScreenDomainId.Application, engine.debugActiveOwnerDomain()) assertEquals( ScreenDomainSurfaces.ApplicationPortal, - ScreenDomainSurfaces.portalSurfaceForOwner(engine.debugActiveOwnerScope()!!), + ScreenDomainSurfaces.portalSurfaceForDomain(engine.debugActiveOwnerDomain()!!), ) } @Test - fun `system-owned pipette emits transient overlay commands in system layer contract`() { + fun `system-owned pipette emits transient portal commands in system portal contract`() { val engine = ColorPickerPopupEngine() val owner = "owner-system" engine.onFrame(900, 700) engine.open( ColorPickerPopupRequest( owner = owner, - ownerScope = OverlayOwnerScope.System, + ownerDomain = ScreenDomainId.System, anchorRect = Rect(120, 80, 18, 18), state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), ), @@ -488,14 +488,14 @@ class ColorPickerPopupEngineTests { assertTrue(engine.handleMouseDown(layout.pipetteRect.x + 2, layout.pipetteRect.y + 2, MouseButton.LEFT)) assertTrue(engine.handleMouseMove(layout.pipetteRect.x + 24, layout.pipetteRect.y + 24)) - val overlay = mutableListOf() - engine.appendEyedropperOverlayCommands(900, 700, overlay) + val portalCommands = mutableListOf() + engine.appendEyedropperPortalCommands(900, 700, portalCommands) - assertTrue(overlay.isNotEmpty()) - assertEquals(OverlayOwnerScope.System, engine.debugActiveOwnerScope()) + assertTrue(portalCommands.isNotEmpty()) + assertEquals(ScreenDomainId.System, engine.debugActiveOwnerDomain()) assertEquals( ScreenDomainSurfaces.SystemPortal, - ScreenDomainSurfaces.portalSurfaceForOwner(engine.debugActiveOwnerScope()!!), + ScreenDomainSurfaces.portalSurfaceForDomain(engine.debugActiveOwnerDomain()!!), ) } @@ -573,7 +573,7 @@ class ColorPickerPopupEngineTests { } @Test - fun `opened popup appends overlay commands after frame sync`() { + fun `opened popup appends portal commands after frame sync`() { val engine = ColorPickerPopupEngine() val owner = "owner" engine.open( @@ -587,7 +587,7 @@ class ColorPickerPopupEngineTests { engine.onFrame(900, 700) val out = ArrayList() - engine.appendOverlayCommands(out) + engine.appendPortalCommands(out) assertTrue(out.isNotEmpty()) assertTrue(out.any { it is RenderCommand.DrawText && it.text == "Popup Test" }) @@ -723,15 +723,15 @@ class ColorPickerPopupEngineTests { state = ColorPickerState(RgbaColor.WHITE), ) manager.open( - ownerScope = OverlayOwnerScope.System, + ownerDomain = ScreenDomainId.System, anchorRect = Rect(20, 20, 10, 10), title = "System", state = ColorPickerState(RgbaColor.WHITE), ) assertEquals(2, fakeService.opened.size) - assertEquals(OverlayOwnerScope.Application, fakeService.opened[0].ownerScope) - assertEquals(OverlayOwnerScope.System, fakeService.opened[1].ownerScope) + assertEquals(ScreenDomainId.Application, fakeService.opened[0].ownerDomain) + assertEquals(ScreenDomainId.System, fakeService.opened[1].ownerDomain) } private class FakeColorPickerPortalService : ColorPickerPopupPortalService { diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalKeyboardRegressionTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalKeyboardRegressionTests.kt index b4bb324..4ea0ac0 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalKeyboardRegressionTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalKeyboardRegressionTests.kt @@ -21,12 +21,12 @@ import org.dreamfinity.dsgl.core.event.MouseClickEvent import org.dreamfinity.dsgl.core.hooks.useState import org.dreamfinity.dsgl.core.host.DsglWindowHost import org.dreamfinity.dsgl.core.host.Viewport -import org.dreamfinity.dsgl.core.overlay.ApplicationOverlayHost -import org.dreamfinity.dsgl.core.overlay.PortalPointerRegion -import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces -import org.dreamfinity.dsgl.core.overlay.handlePortalKeyDownBeforeDom -import org.dreamfinity.dsgl.core.overlay.isFloatingWindowDemoOpen -import org.dreamfinity.dsgl.core.overlay.toggleFloatingWindowDemo +import org.dreamfinity.dsgl.core.portal.ApplicationPortalHost +import org.dreamfinity.dsgl.core.portal.PortalPointerRegion +import org.dreamfinity.dsgl.core.portal.ScreenDomainSurfaces +import org.dreamfinity.dsgl.core.portal.handlePortalKeyDownBeforeDom +import org.dreamfinity.dsgl.core.portal.isFloatingWindowDemoOpen +import org.dreamfinity.dsgl.core.portal.toggleFloatingWindowDemo import org.dreamfinity.dsgl.core.render.RenderCommand import kotlin.test.AfterTest import kotlin.test.Test @@ -37,7 +37,7 @@ import kotlin.test.assertTrue class ModalPortalKeyboardRegressionTests { private val trees: MutableList = ArrayList() - private val overlays: MutableList = ArrayList() + private val portalHosts: MutableList = ArrayList() private val hostKeys: MutableSet = LinkedHashSet() private val measureContext = object : UiMeasureContext { @@ -61,10 +61,10 @@ class ModalPortalKeyboardRegressionTests { tree.root.clearListenersDeep() } } - overlays.forEach { overlay -> overlay.clearRefs() } + portalHosts.forEach { portalHost -> portalHost.clearRefs() } hostKeys.forEach(ModalPortalSessionStore::forgetPortal) trees.clear() - overlays.clear() + portalHosts.clear() hostKeys.clear() } @@ -112,7 +112,7 @@ class ModalPortalKeyboardRegressionTests { } @Test - fun `modal layers mount through application overlay portal`() { + fun `modal layers mount through application portalHost portal`() { val hostKey = "tests.modal.portal.portal" val tree = buildTree(hostKey, listOf(basicModal())) trees += tree @@ -124,33 +124,33 @@ class ModalPortalKeyboardRegressionTests { assertNotNull(modalPortal) assertEquals(listOf("$hostKey.content"), modalPortal.children.map { it.key }) - val overlay = ApplicationOverlayHost() - overlays += overlay - overlay.render(measureContext, 320, 180) + val portalHost = ApplicationPortalHost() + portalHosts += portalHost + portalHost.render(measureContext, 320, 180) - assertEquals(listOf("application.modal.$hostKey"), overlay.modalPortal.debugActivePortalEntryIds()) + assertEquals(listOf("application.modal.$hostKey"), portalHost.modalPortal.debugActivePortalEntryIds()) } @Test fun `modal activation detaches stale application floating window before paint`() { val hostKey = "tests.modal.portal.floating.stale" - val overlay = ApplicationOverlayHost() - overlays += overlay + val portalHost = ApplicationPortalHost() + portalHosts += portalHost - overlay.onInputFrame(320, 180) - overlay.toggleFloatingWindowDemo(anchorX = 24, anchorY = 24) - overlay.render(measureContext, 320, 180) - val floatingNode = overlay.floatingWindowPortal.debugNode() - assertTrue(floatingNode.parent === overlay.rootNode) + portalHost.onInputFrame(320, 180) + portalHost.toggleFloatingWindowDemo(anchorX = 24, anchorY = 24) + portalHost.render(measureContext, 320, 180) + val floatingNode = portalHost.floatingWindowPortal.debugNode() + assertTrue(floatingNode.parent === portalHost.rootNode) val tree = buildTree(hostKey, listOf(basicModal())) trees += tree tree.render(measureContext, 320, 180) - overlay.render(measureContext, 320, 180) + portalHost.render(measureContext, 320, 180) - assertFalse(overlay.isFloatingWindowDemoOpen()) + assertFalse(portalHost.isFloatingWindowDemoOpen()) assertTrue(floatingNode.parent == null) - assertEquals(listOf("application.modal.$hostKey"), overlay.modalPortal.debugActivePortalEntryIds()) + assertEquals(listOf("application.modal.$hostKey"), portalHost.modalPortal.debugActivePortalEntryIds()) } @Test @@ -160,11 +160,11 @@ class ModalPortalKeyboardRegressionTests { trees += tree tree.render(measureContext, 320, 180) - val overlay = ApplicationOverlayHost() - overlays += overlay - overlay.render(measureContext, 320, 180) + val portalHost = ApplicationPortalHost() + portalHosts += portalHost + portalHost.render(measureContext, 320, 180) - assertTrue(overlay.handleMouseDown(4, 4, MouseButton.LEFT)) + assertTrue(portalHost.handleMouseDown(4, 4, MouseButton.LEFT)) } @Test @@ -177,11 +177,11 @@ class ModalPortalKeyboardRegressionTests { val focusedContentInput = requireNodeByKey(tree.root, "$hostKey.content.input") FocusManager.requestFocus(focusedContentInput) - val overlay = ApplicationOverlayHost() - overlays += overlay - overlay.render(measureContext, 320, 180) + val portalHost = ApplicationPortalHost() + portalHosts += portalHost + portalHost.render(measureContext, 320, 180) - assertTrue(overlay.handlePortalKeyDownBeforeDom(KeyCodes.ESCAPE, 0.toChar())) + assertTrue(portalHost.handlePortalKeyDownBeforeDom(KeyCodes.ESCAPE, 0.toChar())) assertEquals(1, hideCount) } @@ -192,18 +192,18 @@ class ModalPortalKeyboardRegressionTests { trees += tree tree.render(measureContext, 320, 180) - val overlay = ApplicationOverlayHost() - overlays += overlay - overlay.render(measureContext, 320, 180) + val portalHost = ApplicationPortalHost() + portalHosts += portalHost + portalHost.render(measureContext, 320, 180) - val dialog = overlay.modalPortal.debugFindNodeByKey(ModalPortalSessionStore.dialogKey(hostKey, "modal.dismissible")) + val dialog = portalHost.modalPortal.debugFindNodeByKey(ModalPortalSessionStore.dialogKey(hostKey, "modal.dismissible")) assertNotNull(dialog) val inside = - overlay.modalPortal.debugEvaluatePointerDownPolicy( + portalHost.modalPortal.debugEvaluatePointerDownPolicy( mouseX = dialog.bounds.x + dialog.bounds.width / 2, mouseY = dialog.bounds.y + dialog.bounds.height / 2, ) - val outside = overlay.modalPortal.debugEvaluatePointerDownPolicy(mouseX = 2, mouseY = 2) + val outside = portalHost.modalPortal.debugEvaluatePointerDownPolicy(mouseX = 2, mouseY = 2) assertNotNull(inside) assertEquals(PortalPointerRegion.InsideEntry, inside.region) @@ -222,16 +222,16 @@ class ModalPortalKeyboardRegressionTests { trees += tree tree.render(measureContext, 320, 180) - val overlay = ApplicationOverlayHost() - overlays += overlay - overlay.render(measureContext, 320, 180) + val portalHost = ApplicationPortalHost() + portalHosts += portalHost + portalHost.render(measureContext, 320, 180) - val dialog = overlay.modalPortal.debugFindNodeByKey(ModalPortalSessionStore.dialogKey(hostKey, "modal.dismissible")) + val dialog = portalHost.modalPortal.debugFindNodeByKey(ModalPortalSessionStore.dialogKey(hostKey, "modal.dismissible")) assertNotNull(dialog) var applicationRootReceived = false val consumedBy = dispatchApplicationPortalPointer( - overlay = overlay, + portalHost = portalHost, mouseX = dialog.bounds.x + dialog.bounds.width / 2, mouseY = dialog.bounds.y + dialog.bounds.height / 2, pressed = true, @@ -262,14 +262,14 @@ class ModalPortalKeyboardRegressionTests { trees += tree tree.render(measureContext, 320, 180) - val overlay = ApplicationOverlayHost() - overlays += overlay - overlay.render(measureContext, 320, 180) + val portalHost = ApplicationPortalHost() + portalHosts += portalHost + portalHost.render(measureContext, 320, 180) var applicationRootReceived = false val consumedBy = dispatchApplicationPortalPointer( - overlay = overlay, + portalHost = portalHost, mouseX = 2, mouseY = 2, pressed = true, @@ -290,14 +290,14 @@ class ModalPortalKeyboardRegressionTests { modals = listOf(dismissibleBodyModal { modals = emptyList() }) var tree = buildTree(hostKey, modals) trees += tree - val overlay = ApplicationOverlayHost() - overlays += overlay - renderTreeAndOverlay(tree, overlay) + val portalHost = ApplicationPortalHost() + portalHosts += portalHost + renderTreeAndPortal(tree, portalHost) var applicationRootReceivedDown = false val consumedDown = dispatchApplicationPortalPointer( - overlay = overlay, + portalHost = portalHost, mouseX = 2, mouseY = 2, pressed = true, @@ -313,7 +313,7 @@ class ModalPortalKeyboardRegressionTests { var applicationRootReceivedUp = false val consumedUp = dispatchApplicationPortalPointer( - overlay = overlay, + portalHost = portalHost, mouseX = 2, mouseY = 2, pressed = false, @@ -327,7 +327,7 @@ class ModalPortalKeyboardRegressionTests { assertEquals(emptyList(), modals) tree = reconcileTree(tree, buildTree(hostKey, modals)) - renderTreeAndOverlay(tree, overlay) + renderTreeAndPortal(tree, portalHost) } @Test @@ -338,17 +338,17 @@ class ModalPortalKeyboardRegressionTests { trees += tree tree.render(measureContext, 320, 180) - val overlay = ApplicationOverlayHost() - overlays += overlay - overlay.render(measureContext, 320, 180) + val portalHost = ApplicationPortalHost() + portalHosts += portalHost + portalHost.render(measureContext, 320, 180) - val dialog = overlay.modalPortal.debugFindNodeByKey(ModalPortalSessionStore.dialogKey(hostKey, "modal.dismissible")) + val dialog = portalHost.modalPortal.debugFindNodeByKey(ModalPortalSessionStore.dialogKey(hostKey, "modal.dismissible")) assertNotNull(dialog) val clickX = dialog.bounds.x + dialog.bounds.width / 2 val clickY = dialog.bounds.y + dialog.bounds.height / 2 - assertTrue(overlay.handleMouseDown(clickX, clickY, MouseButton.LEFT)) - assertTrue(overlay.handleMouseUp(clickX, clickY, MouseButton.LEFT)) + assertTrue(portalHost.handleMouseDown(clickX, clickY, MouseButton.LEFT)) + assertTrue(portalHost.handleMouseUp(clickX, clickY, MouseButton.LEFT)) assertEquals(0, hideCount) } @@ -360,17 +360,17 @@ class ModalPortalKeyboardRegressionTests { trees += tree tree.render(measureContext, 320, 180) - val overlay = ApplicationOverlayHost() - overlays += overlay - overlay.render(measureContext, 320, 180) + val portalHost = ApplicationPortalHost() + portalHosts += portalHost + portalHost.render(measureContext, 320, 180) - assertTrue(overlay.handleMouseDown(2, 2, MouseButton.LEFT)) - assertTrue(overlay.handleMouseUp(2, 2, MouseButton.LEFT)) + assertTrue(portalHost.handleMouseDown(2, 2, MouseButton.LEFT)) + assertTrue(portalHost.handleMouseUp(2, 2, MouseButton.LEFT)) assertEquals(1, hideCount) } @Test - fun `modal portal keeps topmost focus request on overlay commit`() { + fun `modal portal keeps topmost focus request on portalHost commit`() { val hostKey = "tests.modal.portal.portal.focus" val current = buildTreeWithContentInput(hostKey, emptyList()) trees += current @@ -382,9 +382,9 @@ class ModalPortalKeyboardRegressionTests { current.reconcileWith(withModal) current.render(measureContext, 320, 180) - val overlay = ApplicationOverlayHost() - overlays += overlay - overlay.render(measureContext, 320, 180) + val portalHost = ApplicationPortalHost() + portalHosts += portalHost + portalHost.render(measureContext, 320, 180) assertEquals("modal.input", FocusManager.focusedNode()?.key) } @@ -399,29 +399,29 @@ class ModalPortalKeyboardRegressionTests { trees += current current.render(measureContext, 320, 180) - val overlay = ApplicationOverlayHost() - overlays += overlay - overlay.render(measureContext, 320, 180) + val portalHost = ApplicationPortalHost() + portalHosts += portalHost + portalHost.render(measureContext, 320, 180) val stacked = buildTree(hostKey, listOf(stepOne, stepTwo)) trees += stacked current.reconcileWith(stacked) current.render(measureContext, 320, 180) - overlay.render(measureContext, 320, 180) + portalHost.render(measureContext, 320, 180) val popped = buildTree(hostKey, listOf(stepOne)) trees += popped current.reconcileWith(popped) current.render(measureContext, 320, 180) - overlay.render(measureContext, 320, 180) + portalHost.render(measureContext, 320, 180) - val stepOneButton = overlay.modalPortal.debugFindNodeByKey("step.one.button") + val stepOneButton = portalHost.modalPortal.debugFindNodeByKey("step.one.button") assertNotNull(stepOneButton) val clickX = stepOneButton.bounds.x + stepOneButton.bounds.width / 2 val clickY = stepOneButton.bounds.y + stepOneButton.bounds.height / 2 - assertTrue(overlay.handleMouseDown(clickX, clickY, MouseButton.LEFT)) - assertTrue(overlay.handleMouseUp(clickX, clickY, MouseButton.LEFT)) + assertTrue(portalHost.handleMouseDown(clickX, clickY, MouseButton.LEFT)) + assertTrue(portalHost.handleMouseUp(clickX, clickY, MouseButton.LEFT)) assertEquals(1, stepOneClicks) } @@ -441,21 +441,21 @@ class ModalPortalKeyboardRegressionTests { var tree = buildTree(hostKey, modals) trees += tree - val overlay = ApplicationOverlayHost() - overlays += overlay - renderTreeAndOverlay(tree, overlay) + val portalHost = ApplicationPortalHost() + portalHosts += portalHost + renderTreeAndPortal(tree, portalHost) - clickOverlayButton(overlay, "Next") + clickPortalButton(portalHost, "Next") assertEquals(listOf("modal.flow.1", "modal.flow.2"), modals.map { it.key }) tree = reconcileTree(tree, buildTree(hostKey, modals)) - renderTreeAndOverlay(tree, overlay) + renderTreeAndPortal(tree, portalHost) - clickOverlayButton(overlay, "Back to Step 1") + clickPortalButton(portalHost, "Back to Step 1") assertEquals(listOf("modal.flow.1"), modals.map { it.key }) tree = reconcileTree(tree, buildTree(hostKey, modals)) - renderTreeAndOverlay(tree, overlay) + renderTreeAndPortal(tree, portalHost) - clickOverlayButton(overlay, "Next") + clickPortalButton(portalHost, "Next") assertEquals(listOf("modal.flow.1", "modal.flow.2"), modals.map { it.key }) } @@ -475,24 +475,24 @@ class ModalPortalKeyboardRegressionTests { var tree = buildTree(hostKey, modals) trees += tree - val overlay = ApplicationOverlayHost() - overlays += overlay - renderTreeAndOverlay(tree, overlay) + val portalHost = ApplicationPortalHost() + portalHosts += portalHost + renderTreeAndPortal(tree, portalHost) - clickOverlayButton(overlay, "Next") + clickPortalButton(portalHost, "Next") tree = reconcileTree(tree, buildTree(hostKey, modals)) - renderTreeAndOverlay(tree, overlay) + renderTreeAndPortal(tree, portalHost) - clickOverlayButtonInDialog( - overlay = overlay, + clickPortalButtonInDialog( + portalHost = portalHost, text = "x", dialogKey = ModalPortalSessionStore.dialogKey(hostKey, "modal.flow.2"), ) assertEquals(listOf("modal.flow.1"), modals.map { it.key }) tree = reconcileTree(tree, buildTree(hostKey, modals)) - renderTreeAndOverlay(tree, overlay) + renderTreeAndPortal(tree, portalHost) - clickOverlayButton(overlay, "Next") + clickPortalButton(portalHost, "Next") assertEquals(listOf("modal.flow.1", "modal.flow.2"), modals.map { it.key }) } @@ -503,26 +503,26 @@ class ModalPortalKeyboardRegressionTests { window.attachHost(host) var tree = renderWithHookSession(window) trees += tree - val overlay = ApplicationOverlayHost() - overlays += overlay - renderTreeAndOverlay(tree, overlay) + val portalHost = ApplicationPortalHost() + portalHosts += portalHost + renderTreeAndPortal(tree, portalHost) clickTreeNode(tree, "open.flow") assertTrue(host.rebuildRequests > 0) tree = reconcileTree(tree, renderWithHookSession(window)) - renderTreeAndOverlay(tree, overlay) + renderTreeAndPortal(tree, portalHost) - clickOverlayButton(overlay, "Next") + clickPortalButton(portalHost, "Next") tree = reconcileTree(tree, renderWithHookSession(window)) - renderTreeAndOverlay(tree, overlay) + renderTreeAndPortal(tree, portalHost) - clickOverlayButton(overlay, "Back to Step 1") + clickPortalButton(portalHost, "Back to Step 1") tree = reconcileTree(tree, renderWithHookSession(window)) - renderTreeAndOverlay(tree, overlay) + renderTreeAndPortal(tree, portalHost) - clickOverlayButton(overlay, "Next") + clickPortalButton(portalHost, "Next") tree = reconcileTree(tree, renderWithHookSession(window)) - renderTreeAndOverlay(tree, overlay) + renderTreeAndPortal(tree, portalHost) assertEquals(listOf("modal.flow.1", "modal.flow.2"), window.lastRenderedModalKeys) } @@ -631,9 +631,9 @@ class ModalPortalKeyboardRegressionTests { } } - private fun renderTreeAndOverlay(tree: DomTree, overlay: ApplicationOverlayHost) { + private fun renderTreeAndPortal(tree: DomTree, portalHost: ApplicationPortalHost) { tree.render(measureContext, 320, 180) - overlay.render(measureContext, 320, 180) + portalHost.render(measureContext, 320, 180) } private fun reconcileTree(current: DomTree, next: DomTree): DomTree { @@ -642,28 +642,28 @@ class ModalPortalKeyboardRegressionTests { return current } - private fun clickOverlayButton(overlay: ApplicationOverlayHost, text: String) { + private fun clickPortalButton(portalHost: ApplicationPortalHost, text: String) { val button = - overlay.modalPortal.debugFindNode { node -> + portalHost.modalPortal.debugFindNode { node -> node is ButtonNode && node.text == text } assertNotNull(button) val clickX = button.bounds.x + button.bounds.width / 2 val clickY = button.bounds.y + button.bounds.height / 2 - assertTrue(overlay.handleMouseDown(clickX, clickY, MouseButton.LEFT)) - assertTrue(overlay.handleMouseUp(clickX, clickY, MouseButton.LEFT)) + assertTrue(portalHost.handleMouseDown(clickX, clickY, MouseButton.LEFT)) + assertTrue(portalHost.handleMouseUp(clickX, clickY, MouseButton.LEFT)) } - private fun clickOverlayButtonInDialog(overlay: ApplicationOverlayHost, text: String, dialogKey: String) { + private fun clickPortalButtonInDialog(portalHost: ApplicationPortalHost, text: String, dialogKey: String) { val button = - overlay.modalPortal.debugFindNode { node -> + portalHost.modalPortal.debugFindNode { node -> node is ButtonNode && node.text == text && hasAncestorWithKey(node, dialogKey) } assertNotNull(button) val clickX = button.bounds.x + button.bounds.width / 2 val clickY = button.bounds.y + button.bounds.height / 2 - assertTrue(overlay.handleMouseDown(clickX, clickY, MouseButton.LEFT)) - assertTrue(overlay.handleMouseUp(clickX, clickY, MouseButton.LEFT)) + assertTrue(portalHost.handleMouseDown(clickX, clickY, MouseButton.LEFT)) + assertTrue(portalHost.handleMouseUp(clickX, clickY, MouseButton.LEFT)) } private fun hasAncestorWithKey(node: DOMNode, key: Any?): Boolean { @@ -735,7 +735,7 @@ class ModalPortalKeyboardRegressionTests { } private fun dispatchApplicationPortalPointer( - overlay: ApplicationOverlayHost, + portalHost: ApplicationPortalHost, mouseX: Int, mouseY: Int, pressed: Boolean, @@ -745,9 +745,9 @@ class ModalPortalKeyboardRegressionTests { when (surface) { ScreenDomainSurfaces.ApplicationPortal -> if (pressed) { - overlay.handleMouseDown(mouseX, mouseY, MouseButton.LEFT) + portalHost.handleMouseDown(mouseX, mouseY, MouseButton.LEFT) } else { - overlay.handleMouseUp(mouseX, mouseY, MouseButton.LEFT) + portalHost.handleMouseUp(mouseX, mouseY, MouseButton.LEFT) } ScreenDomainSurfaces.ApplicationRoot -> applicationRootHandler() diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalLayoutRegressionTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalLayoutRegressionTests.kt index 4f71cf7..83801f8 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalLayoutRegressionTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalLayoutRegressionTests.kt @@ -6,7 +6,7 @@ import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.dsl.div import org.dreamfinity.dsgl.core.dsl.ui import org.dreamfinity.dsgl.core.event.EventBus -import org.dreamfinity.dsgl.core.overlay.ApplicationOverlayHost +import org.dreamfinity.dsgl.core.portal.ApplicationPortalHost import org.dreamfinity.dsgl.core.render.RenderCommand import kotlin.test.AfterTest import kotlin.test.Test @@ -16,7 +16,7 @@ import kotlin.test.assertTrue class ModalPortalLayoutRegressionTests { private val trees: MutableList = ArrayList() - private val overlays: MutableList = ArrayList() + private val portalHosts: MutableList = ArrayList() private val hostKeys: MutableSet = LinkedHashSet() private val measureContext = object : UiMeasureContext { @@ -39,26 +39,26 @@ class ModalPortalLayoutRegressionTests { tree.root.clearListenersDeep() } } - overlays.forEach { overlay -> overlay.clearRefs() } + portalHosts.forEach { portalHost -> portalHost.clearRefs() } hostKeys.forEach(ModalPortalSessionStore::forgetPortal) trees.clear() - overlays.clear() + portalHosts.clear() hostKeys.clear() } @Test - fun `modal portal resolves layer style before first overlay layout`() { + fun `modal portal resolves layer style before first portalHost layout`() { val hostKey = "tests.modal.portal.layout.first.frame" val tree = buildTree(hostKey, listOf(basicModal())) trees += tree tree.render(measureContext, 320, 180) - val overlay = ApplicationOverlayHost() - overlays += overlay - overlay.render(measureContext, 320, 180) + val portalHost = ApplicationPortalHost() + portalHosts += portalHost + portalHost.render(measureContext, 320, 180) - val layer = overlay.modalPortal.debugFindNodeByKey("$hostKey.modal.modal.basic.layer") - val dialog = overlay.modalPortal.debugFindNodeByKey(ModalPortalSessionStore.dialogKey(hostKey, "modal.basic")) + val layer = portalHost.modalPortal.debugFindNodeByKey("$hostKey.modal.modal.basic.layer") + val dialog = portalHost.modalPortal.debugFindNodeByKey(ModalPortalSessionStore.dialogKey(hostKey, "modal.basic")) assertNotNull(layer) assertNotNull(dialog) @@ -70,7 +70,7 @@ class ModalPortalLayoutRegressionTests { } @Test - fun `centered modal keeps stable bounds across passive overlay frames`() { + fun `centered modal keeps stable bounds across passive portalHost frames`() { val hostKey = "tests.modal.portal.layout.centered.stable" val tree = buildTree( @@ -85,19 +85,19 @@ class ModalPortalLayoutRegressionTests { trees += tree tree.render(measureContext, 320, 180) - val overlay = ApplicationOverlayHost() - overlays += overlay - overlay.render(measureContext, 320, 180) + val portalHost = ApplicationPortalHost() + portalHosts += portalHost + portalHost.render(measureContext, 320, 180) val dialogKey = ModalPortalSessionStore.dialogKey(hostKey, "modal.centered") - val firstDialog = overlay.modalPortal.debugFindNodeByKey(dialogKey) + val firstDialog = portalHost.modalPortal.debugFindNodeByKey(dialogKey) assertNotNull(firstDialog) val firstBounds = firstDialog.bounds - assertTrue(overlay.handleMouseMove(firstBounds.x + firstBounds.width / 2, firstBounds.y + firstBounds.height / 2)) - overlay.render(measureContext, 320, 180) + assertTrue(portalHost.handleMouseMove(firstBounds.x + firstBounds.width / 2, firstBounds.y + firstBounds.height / 2)) + portalHost.render(measureContext, 320, 180) - val secondDialog = overlay.modalPortal.debugFindNodeByKey(dialogKey) + val secondDialog = portalHost.modalPortal.debugFindNodeByKey(dialogKey) assertNotNull(secondDialog) assertEquals(firstBounds, secondDialog.bounds) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalPointerRegressionTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalPointerRegressionTests.kt index e9cb370..fef61e7 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalPointerRegressionTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/components/modal/ModalPortalPointerRegressionTests.kt @@ -14,10 +14,10 @@ import org.dreamfinity.dsgl.core.dsl.ui import org.dreamfinity.dsgl.core.event.EventBus import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.ApplicationOverlayHost -import org.dreamfinity.dsgl.core.overlay.hasActiveModalPortal -import org.dreamfinity.dsgl.core.overlay.isFloatingWindowDemoOpen -import org.dreamfinity.dsgl.core.overlay.toggleFloatingWindowDemo +import org.dreamfinity.dsgl.core.portal.ApplicationPortalHost +import org.dreamfinity.dsgl.core.portal.hasActiveModalPortal +import org.dreamfinity.dsgl.core.portal.isFloatingWindowDemoOpen +import org.dreamfinity.dsgl.core.portal.toggleFloatingWindowDemo import org.dreamfinity.dsgl.core.render.RenderCommand import kotlin.test.AfterTest import kotlin.test.Test @@ -27,7 +27,7 @@ import kotlin.test.assertTrue class ModalPortalPointerRegressionTests { private val trees: MutableList = ArrayList() - private val overlays: MutableList = ArrayList() + private val portalHosts: MutableList = ArrayList() private val hostKeys: MutableSet = LinkedHashSet() private val measureContext = object : UiMeasureContext { @@ -51,31 +51,31 @@ class ModalPortalPointerRegressionTests { tree.root.clearListenersDeep() } } - overlays.forEach { overlay -> overlay.clearRefs() } + portalHosts.forEach { portalHost -> portalHost.clearRefs() } hostKeys.forEach(ModalPortalSessionStore::forgetPortal) trees.clear() - overlays.clear() + portalHosts.clear() hostKeys.clear() } @Test fun `active modal prevents application floating window from opening`() { val hostKey = "tests.modal.portal.floating.blocked" - val overlay = ApplicationOverlayHost() - overlays += overlay + val portalHost = ApplicationPortalHost() + portalHosts += portalHost val tree = buildTree(hostKey, listOf(staticModal())) trees += tree tree.render(measureContext, 320, 180) - overlay.render(measureContext, 320, 180) - assertTrue(overlay.hasActiveModalPortal()) + portalHost.render(measureContext, 320, 180) + assertTrue(portalHost.hasActiveModalPortal()) - overlay.toggleFloatingWindowDemo(anchorX = 24, anchorY = 24) - overlay.render(measureContext, 320, 180) + portalHost.toggleFloatingWindowDemo(anchorX = 24, anchorY = 24) + portalHost.render(measureContext, 320, 180) - assertFalse(overlay.isFloatingWindowDemoOpen()) + assertFalse(portalHost.isFloatingWindowDemoOpen()) assertTrue( - overlay.floatingWindowPortal + portalHost.floatingWindowPortal .debugNode() .parent == null, ) @@ -84,30 +84,30 @@ class ModalPortalPointerRegressionTests { @Test fun `static modal backdrop pointer press does not activate modal layer`() { val hostKey = "tests.modal.portal.static.backdrop.active" - val overlay = renderStaticModalOverlay(hostKey) - val layer = overlay.modalPortal.debugFindNodeByKey("$hostKey.modal.modal.static.layer") + val portalHost = renderStaticModalPortal(hostKey) + val layer = portalHost.modalPortal.debugFindNodeByKey("$hostKey.modal.modal.static.layer") assertNotNull(layer) - assertTrue(overlay.handleMouseDown(4, 4, MouseButton.LEFT)) + assertTrue(portalHost.handleMouseDown(4, 4, MouseButton.LEFT)) assertFalse(layer.styleActive) - assertTrue(overlay.handleMouseUp(4, 4, MouseButton.LEFT)) + assertTrue(portalHost.handleMouseUp(4, 4, MouseButton.LEFT)) assertFalse(layer.styleActive) } @Test fun `static modal dialog pointer press does not activate modal layer`() { val hostKey = "tests.modal.portal.static.dialog.active" - val overlay = renderStaticModalOverlay(hostKey) - val layer = overlay.modalPortal.debugFindNodeByKey("$hostKey.modal.modal.static.layer") - val dialog = overlay.modalPortal.debugFindNodeByKey(ModalPortalSessionStore.dialogKey(hostKey, "modal.static")) + val portalHost = renderStaticModalPortal(hostKey) + val layer = portalHost.modalPortal.debugFindNodeByKey("$hostKey.modal.modal.static.layer") + val dialog = portalHost.modalPortal.debugFindNodeByKey(ModalPortalSessionStore.dialogKey(hostKey, "modal.static")) assertNotNull(layer) assertNotNull(dialog) val clickX = dialog.bounds.x + dialog.bounds.width / 2 val clickY = dialog.bounds.y + dialog.bounds.height / 2 - assertTrue(overlay.handleMouseDown(clickX, clickY, MouseButton.LEFT)) + assertTrue(portalHost.handleMouseDown(clickX, clickY, MouseButton.LEFT)) assertFalse(layer.styleActive) - assertTrue(overlay.handleMouseUp(clickX, clickY, MouseButton.LEFT)) + assertTrue(portalHost.handleMouseUp(clickX, clickY, MouseButton.LEFT)) assertFalse(layer.styleActive) } @@ -130,18 +130,18 @@ class ModalPortalPointerRegressionTests { ) trees += tree tree.render(measureContext, 320, 180) - val overlay = - ApplicationOverlayHost().also { overlay -> - overlays += overlay - overlay.render(measureContext, 320, 180) + val portalHost = + ApplicationPortalHost().also { portalHost -> + portalHosts += portalHost + portalHost.render(measureContext, 320, 180) } - val button = overlay.modalPortal.debugFindNodeByKey("$hostKey.modal.button") + val button = portalHost.modalPortal.debugFindNodeByKey("$hostKey.modal.button") assertNotNull(button) val clickX = button.bounds.x + button.bounds.width / 2 val clickY = button.bounds.y + button.bounds.height / 2 - assertTrue(overlay.handleMouseDown(clickX, clickY, MouseButton.LEFT)) - assertTrue(overlay.handleMouseUp(clickX, clickY, MouseButton.LEFT)) + assertTrue(portalHost.handleMouseDown(clickX, clickY, MouseButton.LEFT)) + assertTrue(portalHost.handleMouseUp(clickX, clickY, MouseButton.LEFT)) assertTrue(clicks == 1) } @@ -149,16 +149,16 @@ class ModalPortalPointerRegressionTests { @Test fun `static modal clears dialog hover when pointer moves to backdrop`() { val hostKey = "tests.modal.portal.static.dialog.hover.clear" - val overlay = renderStaticModalWithButton(hostKey) - val button = overlay.modalPortal.debugFindNodeByKey("$hostKey.modal.button") + val portalHost = renderStaticModalWithButton(hostKey) + val button = portalHost.modalPortal.debugFindNodeByKey("$hostKey.modal.button") assertNotNull(button) val hoverX = button.bounds.x + button.bounds.width / 2 val hoverY = button.bounds.y + button.bounds.height / 2 - assertTrue(overlay.handleMouseMove(hoverX, hoverY)) + assertTrue(portalHost.handleMouseMove(hoverX, hoverY)) assertTrue(button.styleHovered) - assertTrue(overlay.handleMouseMove(4, 4)) + assertTrue(portalHost.handleMouseMove(4, 4)) assertFalse(button.styleHovered) assertFalse(button.styleActive) } @@ -166,15 +166,15 @@ class ModalPortalPointerRegressionTests { @Test fun `static modal backdrop move does not activate modal nodes`() { val hostKey = "tests.modal.portal.static.backdrop.move" - val overlay = renderStaticModalWithButton(hostKey) - val layer = overlay.modalPortal.debugFindNodeByKey("$hostKey.modal.modal.static.layer") - val dialog = overlay.modalPortal.debugFindNodeByKey(ModalPortalSessionStore.dialogKey(hostKey, "modal.static")) - val button = overlay.modalPortal.debugFindNodeByKey("$hostKey.modal.button") + val portalHost = renderStaticModalWithButton(hostKey) + val layer = portalHost.modalPortal.debugFindNodeByKey("$hostKey.modal.modal.static.layer") + val dialog = portalHost.modalPortal.debugFindNodeByKey(ModalPortalSessionStore.dialogKey(hostKey, "modal.static")) + val button = portalHost.modalPortal.debugFindNodeByKey("$hostKey.modal.button") assertNotNull(layer) assertNotNull(dialog) assertNotNull(button) - assertTrue(overlay.handleMouseMove(4, 4)) + assertTrue(portalHost.handleMouseMove(4, 4)) assertFalse(layer.styleActive) assertFalse(dialog.styleActive) @@ -185,38 +185,38 @@ class ModalPortalPointerRegressionTests { @Test fun `active modal mouse move clears stale application portal hover`() { val hostKey = "tests.modal.portal.static.lower.hover.clear" - val overlay = ApplicationOverlayHost() - overlays += overlay - overlay.rootNode.setViewportBounds(320, 180) + val portalHost = ApplicationPortalHost() + portalHosts += portalHost + portalHost.rootNode.setViewportBounds(320, 180) val lowerButton = ButtonNode("Lower", key = "$hostKey.lower.button").apply { bounds = Rect(0, 0, 80, 24) } - lowerButton.applyParent(overlay.rootNode) + lowerButton.applyParent(portalHost.rootNode) - assertTrue(overlay.handleMouseMove(12, 12)) + assertTrue(portalHost.handleMouseMove(12, 12)) assertTrue(lowerButton.styleHovered) val tree = buildTree(hostKey, listOf(staticModal())) trees += tree tree.render(measureContext, 320, 180) - overlay.render(measureContext, 320, 180) + portalHost.render(measureContext, 320, 180) - assertTrue(overlay.handleMouseMove(4, 4)) + assertTrue(portalHost.handleMouseMove(4, 4)) assertFalse(lowerButton.styleHovered) } - private fun renderStaticModalOverlay(hostKey: String): ApplicationOverlayHost { + private fun renderStaticModalPortal(hostKey: String): ApplicationPortalHost { val tree = buildTree(hostKey, listOf(staticModal())) trees += tree tree.render(measureContext, 320, 180) - return ApplicationOverlayHost().also { overlay -> - overlays += overlay - overlay.render(measureContext, 320, 180) + return ApplicationPortalHost().also { portalHost -> + portalHosts += portalHost + portalHost.render(measureContext, 320, 180) } } - private fun renderStaticModalWithButton(hostKey: String): ApplicationOverlayHost { + private fun renderStaticModalWithButton(hostKey: String): ApplicationPortalHost { val tree = buildTree( hostKey = hostKey, @@ -232,9 +232,9 @@ class ModalPortalPointerRegressionTests { ) trees += tree tree.render(measureContext, 320, 180) - return ApplicationOverlayHost().also { overlay -> - overlays += overlay - overlay.render(measureContext, 320, 180) + return ApplicationPortalHost().also { portalHost -> + portalHosts += portalHost + portalHost.render(measureContext, 320, 180) } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngineTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngineTests.kt index 5b41022..77a587d 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngineTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/contextmenu/ContextMenuEngineTests.kt @@ -93,11 +93,11 @@ class ContextMenuEngineTests { } @Test - fun `overlay consumes pointer before base dispatch when menu is open`() { + fun `portal consumes pointer before base dispatch when menu is open`() { val clock = FakeClock() val engine = ContextMenuEngine(clock = clock) val model = - contextMenu(id = "overlay.order") { + contextMenu(id = "portal.order") { item("Run") item("Build") } @@ -190,7 +190,7 @@ class ContextMenuEngineTests { engine.openAtCursor(model, 20, 20) engine.onFrame(ctx, 320, 180, 1f) val commands = mutableListOf() - engine.appendOverlayCommands(ctx, 320, 180, commands) + engine.appendPortalCommands(ctx, 320, 180, commands) val textCommand = commands.filterIsInstance().firstOrNull() assertNotNull(textCommand) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/DebugDomainHostsTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/DebugDomainHostsTests.kt index a04b5c5..d3a6a4a 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/DebugDomainHostsTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/debug/DebugDomainHostsTests.kt @@ -3,14 +3,14 @@ package org.dreamfinity.dsgl.core.debug import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.PortalEntry -import org.dreamfinity.dsgl.core.overlay.PortalEntryBounds -import org.dreamfinity.dsgl.core.overlay.PortalEntryId -import org.dreamfinity.dsgl.core.overlay.PortalEntryOrder -import org.dreamfinity.dsgl.core.overlay.PortalEntryPlacement -import org.dreamfinity.dsgl.core.overlay.PortalEntryState -import org.dreamfinity.dsgl.core.overlay.PortalFrameContext -import org.dreamfinity.dsgl.core.overlay.ScreenDomainSurfaces +import org.dreamfinity.dsgl.core.portal.PortalEntry +import org.dreamfinity.dsgl.core.portal.PortalEntryBounds +import org.dreamfinity.dsgl.core.portal.PortalEntryId +import org.dreamfinity.dsgl.core.portal.PortalEntryOrder +import org.dreamfinity.dsgl.core.portal.PortalEntryPlacement +import org.dreamfinity.dsgl.core.portal.PortalEntryState +import org.dreamfinity.dsgl.core.portal.PortalFrameContext +import org.dreamfinity.dsgl.core.portal.ScreenDomainSurfaces import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.StyleApplicationScope import java.util.Locale @@ -33,17 +33,17 @@ class DebugDomainHostsTests { @AfterTest fun cleanup() { - OverlayLayerDebugState.resetAll() - OverlayLayerDebugState.setControlsEnabledTestOverride(null) + DomainSurfaceDebugState.resetAll() + DomainSurfaceDebugState.setControlsEnabledTestOverride(null) } @Test - fun `debug panel remains available when app and system overlays are disabled`() { - OverlayLayerDebugState.setControlsEnabledTestOverride(true) - OverlayLayerDebugState.applicationOverlayRenderEnabled = false - OverlayLayerDebugState.applicationOverlayInputEnabled = false - OverlayLayerDebugState.systemOverlayRenderEnabled = false - OverlayLayerDebugState.systemOverlayInputEnabled = false + fun `debug panel remains available when app and system portal surfaces are disabled`() { + DomainSurfaceDebugState.setControlsEnabledTestOverride(true) + DomainSurfaceDebugState.applicationPortalRenderEnabled = false + DomainSurfaceDebugState.applicationPortalInputEnabled = false + DomainSurfaceDebugState.systemPortalRenderEnabled = false + DomainSurfaceDebugState.systemPortalInputEnabled = false val host = DebugDomainRootHost() host.render(ctx, 960, 540) @@ -64,12 +64,12 @@ class DebugDomainHostsTests { } @Test - fun `reset all restores overlay render and input toggles`() { - OverlayLayerDebugState.setControlsEnabledTestOverride(true) - OverlayLayerDebugState.applicationOverlayRenderEnabled = false - OverlayLayerDebugState.applicationOverlayInputEnabled = false - OverlayLayerDebugState.systemOverlayRenderEnabled = false - OverlayLayerDebugState.systemOverlayInputEnabled = false + fun `reset all restores domain-surface render and input toggles`() { + DomainSurfaceDebugState.setControlsEnabledTestOverride(true) + DomainSurfaceDebugState.applicationPortalRenderEnabled = false + DomainSurfaceDebugState.applicationPortalInputEnabled = false + DomainSurfaceDebugState.systemPortalRenderEnabled = false + DomainSurfaceDebugState.systemPortalInputEnabled = false val host = DebugDomainRootHost() host.render(ctx, 960, 540) @@ -77,16 +77,16 @@ class DebugDomainHostsTests { host.paint(ctx) assertTrue(host.handleMouseDown(layout.resetRect.x + 2, layout.resetRect.y + 2, MouseButton.LEFT)) - assertTrue(OverlayLayerDebugState.applicationOverlayRenderEnabled) - assertTrue(OverlayLayerDebugState.applicationOverlayInputEnabled) - assertTrue(OverlayLayerDebugState.systemOverlayRenderEnabled) - assertTrue(OverlayLayerDebugState.systemOverlayInputEnabled) + assertTrue(DomainSurfaceDebugState.applicationPortalRenderEnabled) + assertTrue(DomainSurfaceDebugState.applicationPortalInputEnabled) + assertTrue(DomainSurfaceDebugState.systemPortalRenderEnabled) + assertTrue(DomainSurfaceDebugState.systemPortalInputEnabled) } @Test - fun `debug panel toggles mutate independent app and system overlay state`() { - OverlayLayerDebugState.setControlsEnabledTestOverride(true) - OverlayLayerDebugState.resetAll() + fun `debug panel toggles mutate independent app and system portal state`() { + DomainSurfaceDebugState.setControlsEnabledTestOverride(true) + DomainSurfaceDebugState.resetAll() val host = DebugDomainRootHost() host.render(ctx, 960, 540) @@ -95,31 +95,31 @@ class DebugDomainHostsTests { assertTrue( host.handleMouseDown( - layout.appOverlayRenderRect.x + 2, - layout.appOverlayRenderRect.y + 2, + layout.appPortalRenderRect.x + 2, + layout.appPortalRenderRect.y + 2, MouseButton.LEFT, ), ) - assertFalse(OverlayLayerDebugState.applicationOverlayRenderEnabled) - assertTrue(OverlayLayerDebugState.applicationOverlayInputEnabled) - assertTrue(OverlayLayerDebugState.systemOverlayRenderEnabled) - assertTrue(OverlayLayerDebugState.systemOverlayInputEnabled) + assertFalse(DomainSurfaceDebugState.applicationPortalRenderEnabled) + assertTrue(DomainSurfaceDebugState.applicationPortalInputEnabled) + assertTrue(DomainSurfaceDebugState.systemPortalRenderEnabled) + assertTrue(DomainSurfaceDebugState.systemPortalInputEnabled) assertTrue( host.handleMouseDown( - layout.systemOverlayInputRect.x + 2, - layout.systemOverlayInputRect.y + 2, + layout.systemPortalInputRect.x + 2, + layout.systemPortalInputRect.y + 2, MouseButton.LEFT, ), ) - assertFalse(OverlayLayerDebugState.systemOverlayInputEnabled) - assertFalse(OverlayLayerDebugState.applicationOverlayRenderEnabled) + assertFalse(DomainSurfaceDebugState.systemPortalInputEnabled) + assertFalse(DomainSurfaceDebugState.applicationPortalRenderEnabled) } @Test fun `debug panel status shows fps and frame time`() { - OverlayLayerDebugState.setControlsEnabledTestOverride(true) - OverlayLayerDebugState.updateFrameTiming(0.025) + DomainSurfaceDebugState.setControlsEnabledTestOverride(true) + DomainSurfaceDebugState.updateFrameTiming(0.025) val host = DebugDomainRootHost() host.render(ctx, 960, 540) @@ -137,10 +137,10 @@ class DebugDomainHostsTests { statusTexts.lastOrNull { it.isNotBlank() } ?: statusTexts.lastOrNull(), "draw texts: ${drawTexts.joinToString { "${it.sourceKey}:${it.text}" }}", ) - val expectedFps = OverlayLayerDebugState.frameFps - val expectedFrameMs = String.format(Locale.US, "%.1f", OverlayLayerDebugState.frameTimeMs) - val expectedWindowFps = OverlayLayerDebugState.frameFpsWindow - val expectedWindowFrameMs = String.format(Locale.US, "%.1f", OverlayLayerDebugState.frameTimeWindowMs) + val expectedFps = DomainSurfaceDebugState.frameFps + val expectedFrameMs = String.format(Locale.US, "%.1f", DomainSurfaceDebugState.frameTimeMs) + val expectedWindowFps = DomainSurfaceDebugState.frameFpsWindow + val expectedWindowFrameMs = String.format(Locale.US, "%.1f", DomainSurfaceDebugState.frameTimeWindowMs) assertTrue(statusTextValue.contains("FPS:$expectedFps"), "statusText='$statusTextValue'") assertTrue(statusTextValue.contains("(${expectedFrameMs}ms)"), "statusText='$statusTextValue'") assertTrue(statusTextValue.contains("AvgFPS:$expectedWindowFps"), "statusText='$statusTextValue'") @@ -149,8 +149,8 @@ class DebugDomainHostsTests { @Test fun `toggle button label updates immediately after state change`() { - OverlayLayerDebugState.setControlsEnabledTestOverride(true) - OverlayLayerDebugState.resetAll() + DomainSurfaceDebugState.setControlsEnabledTestOverride(true) + DomainSurfaceDebugState.resetAll() val host = DebugDomainRootHost() host.render(ctx, 960, 540) @@ -165,8 +165,8 @@ class DebugDomainHostsTests { assertTrue( host.handleMouseDown( - layout.appOverlayRenderRect.x + 2, - layout.appOverlayRenderRect.y + 2, + layout.appPortalRenderRect.x + 2, + layout.appPortalRenderRect.y + 2, MouseButton.LEFT, ), ) @@ -183,9 +183,9 @@ class DebugDomainHostsTests { @Test fun `sliding window fps smooths immediate fps`() { - OverlayLayerDebugState.updateFrameTiming(0.010) - OverlayLayerDebugState.updateFrameTiming(0.030) - val snapshot = OverlayLayerDebugState.snapshot() + DomainSurfaceDebugState.updateFrameTiming(0.010) + DomainSurfaceDebugState.updateFrameTiming(0.030) + val snapshot = DomainSurfaceDebugState.snapshot() assertEquals(33, snapshot.frameFps) assertEquals(30.0f, snapshot.frameTimeMs) @@ -195,12 +195,12 @@ class DebugDomainHostsTests { @Test fun `controls visibility obeys debug-only toggle`() { - OverlayLayerDebugState.setControlsEnabledTestOverride(false) + DomainSurfaceDebugState.setControlsEnabledTestOverride(false) val host = DebugDomainRootHost() host.render(ctx, 960, 540) assertTrue(host.paint(ctx).isEmpty()) - OverlayLayerDebugState.setControlsEnabledTestOverride(true) + DomainSurfaceDebugState.setControlsEnabledTestOverride(true) host.render(ctx, 960, 540) assertTrue(host.paint(ctx).isNotEmpty()) } @@ -240,31 +240,31 @@ class DebugDomainHostsTests { @Test fun `debug domain surfaces remain enabled in state even when app and system portals are disabled`() { - OverlayLayerDebugState.applicationOverlayTintEnabled = false - OverlayLayerDebugState.applicationOverlayRenderEnabled = false - OverlayLayerDebugState.applicationOverlayInputEnabled = false - OverlayLayerDebugState.systemOverlayRenderEnabled = false - OverlayLayerDebugState.systemOverlayTintEnabled = false - OverlayLayerDebugState.systemOverlayInputEnabled = false - - assertTrue(OverlayLayerDebugState.isRenderEnabled(ScreenDomainSurfaces.DebugRoot)) - assertTrue(OverlayLayerDebugState.isInputEnabled(ScreenDomainSurfaces.DebugRoot)) - assertTrue(OverlayLayerDebugState.isRenderEnabled(ScreenDomainSurfaces.DebugPortal)) - assertTrue(OverlayLayerDebugState.isInputEnabled(ScreenDomainSurfaces.DebugPortal)) + DomainSurfaceDebugState.applicationPortalTintEnabled = false + DomainSurfaceDebugState.applicationPortalRenderEnabled = false + DomainSurfaceDebugState.applicationPortalInputEnabled = false + DomainSurfaceDebugState.systemPortalRenderEnabled = false + DomainSurfaceDebugState.systemPortalTintEnabled = false + DomainSurfaceDebugState.systemPortalInputEnabled = false + + assertTrue(DomainSurfaceDebugState.isRenderEnabled(ScreenDomainSurfaces.DebugRoot)) + assertTrue(DomainSurfaceDebugState.isInputEnabled(ScreenDomainSurfaces.DebugRoot)) + assertTrue(DomainSurfaceDebugState.isRenderEnabled(ScreenDomainSurfaces.DebugPortal)) + assertTrue(DomainSurfaceDebugState.isInputEnabled(ScreenDomainSurfaces.DebugPortal)) assertEquals( - OverlayLayerDebugSnapshot( - applicationOverlayRenderEnabled = false, - applicationOverlayTintEnabled = false, - applicationOverlayInputEnabled = false, - systemOverlayRenderEnabled = false, - systemOverlayTintEnabled = false, - systemOverlayInputEnabled = false, + DomainSurfaceDebugSnapshot( + applicationPortalRenderEnabled = false, + applicationPortalTintEnabled = false, + applicationPortalInputEnabled = false, + systemPortalRenderEnabled = false, + systemPortalTintEnabled = false, + systemPortalInputEnabled = false, frameFps = 0, frameTimeMs = 0f, frameFpsWindow = 0, frameTimeWindowMs = 0f, ), - OverlayLayerDebugState.snapshot(), + DomainSurfaceDebugState.snapshot(), ) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/OverflowInputClippingTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/OverflowInputClippingTests.kt index e5f1b5b..c694cb8 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/OverflowInputClippingTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/OverflowInputClippingTests.kt @@ -5,7 +5,7 @@ import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter +import org.dreamfinity.dsgl.core.portal.input.SurfaceDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.Overflow import kotlin.test.Test @@ -25,8 +25,8 @@ class OverflowInputClippingTests { @Test fun `generic clipped container rejects pointer input outside viewport`() { - listOf("app-dom", "app-overlay", "system-overlay").forEach { layer -> - val (root, router) = createLayerRouter(layer) + listOf("app-dom", "app-portal", "system-portal").forEach { layer -> + val (root, router) = createSurfaceRouter(layer) var clicks = 0 val clippedViewport = @@ -51,7 +51,7 @@ class OverflowInputClippingTests { @Test fun `generic partial visibility limits pointer interaction to visible area`() { - val (root, router) = createLayerRouter("partial-visible") + val (root, router) = createSurfaceRouter("partial-visible") var clicks = 0 val clippedViewport = @@ -79,7 +79,7 @@ class OverflowInputClippingTests { @Test fun `nested clipped containers intersect effective input clip`() { - val (root, router) = createLayerRouter("nested-clip") + val (root, router) = createSurfaceRouter("nested-clip") var clicks = 0 val outer = @@ -114,7 +114,7 @@ class OverflowInputClippingTests { @Test fun `nested clipped containers clamp interaction to parent child intersection`() { - val (root, router) = createLayerRouter("nested-intersection") + val (root, router) = createSurfaceRouter("nested-intersection") var clicks = 0 val outer = @@ -151,7 +151,7 @@ class OverflowInputClippingTests { @Test fun `paint and pointer clipping stay consistent for clipped containers`() { - val (root, router) = createLayerRouter("paint-input-consistency") + val (root, router) = createSurfaceRouter("paint-input-consistency") var clicks = 0 val clippedViewport = @@ -190,11 +190,11 @@ class OverflowInputClippingTests { assertEquals(1, clicks) } - private fun createLayerRouter(key: String): Pair { + private fun createSurfaceRouter(key: String): Pair { val root = ContainerNode(key = "$key-root").apply { bounds = Rect(0, 0, 320, 200) } - return root to LayerDomInputRouter { root } + return root to SurfaceDomInputRouter { root } } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutStickyBehaviorTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutStickyBehaviorTests.kt index 7523682..edc20b2 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutStickyBehaviorTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutStickyBehaviorTests.kt @@ -10,7 +10,7 @@ import org.dreamfinity.dsgl.core.event.MouseClickEvent import org.dreamfinity.dsgl.core.event.collectHoverChain import org.dreamfinity.dsgl.core.event.dispatchClick import org.dreamfinity.dsgl.core.inspector.InspectorController -import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter +import org.dreamfinity.dsgl.core.portal.input.SurfaceDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.Overflow @@ -591,7 +591,7 @@ class PositionedLayoutStickyBehaviorTests { tree.render(ctx, 220, 120) tree.paint(ctx) - val router = LayerDomInputRouter { root } + val router = SurfaceDomInputRouter { root } val visual = root.debugScrollbarVisualState().vertical ?: error("Expected vertical scrollbar") val dragX = visual.thumbRect.x + visual.thumbRect.width / 2 val startY = visual.thumbRect.y + visual.thumbRect.height / 2 @@ -649,7 +649,7 @@ class PositionedLayoutStickyBehaviorTests { tree.render(ctx, 220, 120) tree.paint(ctx) - val router = LayerDomInputRouter { root } + val router = SurfaceDomInputRouter { root } val visual = root.debugScrollbarVisualState().vertical ?: error("Expected vertical scrollbar") val dragX = visual.thumbRect.x + visual.thumbRect.width / 2 val startY = visual.thumbRect.y + visual.thumbRect.height / 2 @@ -722,7 +722,7 @@ class PositionedLayoutStickyBehaviorTests { inspector.buildDomSnapshot(800, 600) assertEquals(sticky.key?.toString(), inspector.hoveredKey) - val highlight = inspector.overlayHoveredHighlight() + val highlight = inspector.portalHoveredHighlight() assertNotNull(highlight) assertEquals(rect, highlight.borderRect) } @@ -766,7 +766,7 @@ class PositionedLayoutStickyBehaviorTests { inspector.buildDomSnapshot(800, 600) assertEquals(sticky.key?.toString(), inspector.hoveredKey) - val highlight = inspector.overlayHoveredHighlight() + val highlight = inspector.portalHoveredHighlight() assertNotNull(highlight) assertEquals(visibleRect, highlight.borderRect) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/RangeInputScrollFastPathTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/RangeInputScrollFastPathTests.kt index db937e5..98b74c3 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/RangeInputScrollFastPathTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/RangeInputScrollFastPathTests.kt @@ -8,7 +8,7 @@ import org.dreamfinity.dsgl.core.dom.elements.RangeInputNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter +import org.dreamfinity.dsgl.core.portal.input.SurfaceDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.Overflow import org.dreamfinity.dsgl.core.style.StyleDeclarations @@ -261,7 +261,7 @@ class RangeInputScrollFastPathTests { val tree = DomTree(root) tree.render(ctx, 420, 260) tree.paint(ctx) - val router = LayerDomInputRouter { root } + val router = SurfaceDomInputRouter { root } return Fixture( tree = tree, root = root, @@ -320,7 +320,7 @@ class RangeInputScrollFastPathTests { val tree = DomTree(root) tree.render(ctx, 520, 320) tree.paint(ctx) - val router = LayerDomInputRouter { root } + val router = SurfaceDomInputRouter { root } return NestedFixture( tree = tree, root = root, @@ -379,7 +379,7 @@ class RangeInputScrollFastPathTests { val viewport: ContainerNode, val range: RangeInputNode, val sticky: ContainerNode?, - val router: LayerDomInputRouter, + val router: SurfaceDomInputRouter, val baseRangeY: Int, val baseStickyY: Int, ) @@ -390,7 +390,7 @@ class RangeInputScrollFastPathTests { val outer: ContainerNode, val inner: ContainerNode, val range: RangeInputNode, - val router: LayerDomInputRouter, + val router: SurfaceDomInputRouter, val baseRangeY: Int, ) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollContainerStateTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollContainerStateTests.kt index ab760c4..37db737 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollContainerStateTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollContainerStateTests.kt @@ -6,7 +6,7 @@ import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter +import org.dreamfinity.dsgl.core.portal.input.SurfaceDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.Overflow import org.dreamfinity.dsgl.core.style.StyleEngine @@ -398,7 +398,7 @@ class ScrollContainerStateTests { }, ) - val router = LayerDomInputRouter { root } + val router = SurfaceDomInputRouter { root } assertTrue(router.handleMouseDown(30, 58, MouseButton.LEFT)) assertTrue(router.handleMouseUp(30, 58, MouseButton.LEFT)) assertTrue(!router.handleMouseDown(30, 65, MouseButton.LEFT)) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollPerformanceCountersTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollPerformanceCountersTests.kt index 02abb7a..3b02664 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollPerformanceCountersTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollPerformanceCountersTests.kt @@ -6,7 +6,7 @@ import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.KeyModifiers import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter +import org.dreamfinity.dsgl.core.portal.input.SurfaceDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.Overflow import org.dreamfinity.dsgl.core.style.StyleDeclarations @@ -359,7 +359,7 @@ class ScrollPerformanceCountersTests { val tree = DomTree(root) tree.render(ctx, 320, 220) tree.paint(ctx) - val router = LayerDomInputRouter { root } + val router = SurfaceDomInputRouter { root } return ScrollStickyFixture( tree = tree, root = root, @@ -382,7 +382,7 @@ class ScrollPerformanceCountersTests { val root: ContainerNode, val viewport: ContainerNode, val sticky: ContainerNode, - val router: LayerDomInputRouter, + val router: SurfaceDomInputRouter, val stickyBaseTopY: Int, ) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollReactiveSmoothTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollReactiveSmoothTests.kt index c250608..1d96e70 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollReactiveSmoothTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollReactiveSmoothTests.kt @@ -7,7 +7,7 @@ import org.dreamfinity.dsgl.core.dom.layout.Insets import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.event.MouseClickEvent -import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter +import org.dreamfinity.dsgl.core.portal.input.SurfaceDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.Overflow import org.dreamfinity.dsgl.core.style.StyleEngine @@ -493,7 +493,7 @@ class ScrollReactiveSmoothTests { val tree = DomTree(root) tree.render(ctx, 420, 260) tree.paint(ctx) - val router = LayerDomInputRouter { root } + val router = SurfaceDomInputRouter { root } return Fixture(tree, root, viewport, button, filler, router, clickCount) } @@ -503,7 +503,7 @@ class ScrollReactiveSmoothTests { val viewport: ContainerNode, val button: ButtonNode, val filler: ContainerNode, - val router: LayerDomInputRouter, + val router: SurfaceDomInputRouter, val clickCount: IntBox, ) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollbarRenderingInteractionTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollbarRenderingInteractionTests.kt index 8740610..492bc04 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollbarRenderingInteractionTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/ScrollbarRenderingInteractionTests.kt @@ -6,7 +6,7 @@ import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.KeyModifiers import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter +import org.dreamfinity.dsgl.core.portal.input.SurfaceDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.Overflow import org.dreamfinity.dsgl.core.style.StyleEngine @@ -322,7 +322,7 @@ class ScrollbarRenderingInteractionTests { onClick { } }.applyParent(child) - val router = LayerDomInputRouter { root } + val router = SurfaceDomInputRouter { root } KeyModifiers.sync(shift = false, control = false, meta = false) val wheelX = childButton.bounds.x + 1 val wheelY = childButton.bounds.y + 1 @@ -397,7 +397,7 @@ class ScrollbarRenderingInteractionTests { bounds = Rect(inner.bounds.x, inner.bounds.y, 120, 320) }.applyParent(inner) - val router = LayerDomInputRouter { root } + val router = SurfaceDomInputRouter { root } KeyModifiers.sync(shift = false, control = false, meta = false) val innerMax = inner.scrollContainerState().maxScrollY inner.setScrollOffsets(0, innerMax) @@ -450,7 +450,7 @@ class ScrollbarRenderingInteractionTests { bounds = Rect(inner.bounds.x, inner.bounds.y, 130, 360) }.applyParent(inner) - val router = LayerDomInputRouter { root } + val router = SurfaceDomInputRouter { root } val innerThumb = inner .debugScrollbarVisualState() @@ -523,7 +523,7 @@ class ScrollbarRenderingInteractionTests { } wheelTarget.applyParent(viewport) - val router = LayerDomInputRouter { root } + val router = SurfaceDomInputRouter { root } return Quad(root, viewport, wheelTarget, router) } @@ -531,6 +531,6 @@ class ScrollbarRenderingInteractionTests { val root: ContainerNode, val viewport: ContainerNode, val wheelTarget: ButtonNode, - val router: LayerDomInputRouter, + val router: SurfaceDomInputRouter, ) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerScopeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerDomainTests.kt similarity index 86% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerScopeTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerDomainTests.kt index 6667120..7d35bf8 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerScopeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectNodeOwnerDomainTests.kt @@ -7,12 +7,12 @@ import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.ApplicationOverlayHost -import org.dreamfinity.dsgl.core.overlay.DomainPortalServices -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope -import org.dreamfinity.dsgl.core.overlay.handlePortalPointerAfterDom -import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter -import org.dreamfinity.dsgl.core.overlay.syncPortalFrame +import org.dreamfinity.dsgl.core.portal.ApplicationPortalHost +import org.dreamfinity.dsgl.core.portal.DomainPortalServices +import org.dreamfinity.dsgl.core.portal.ScreenDomainId +import org.dreamfinity.dsgl.core.portal.handlePortalPointerAfterDom +import org.dreamfinity.dsgl.core.portal.input.SurfaceDomInputRouter +import org.dreamfinity.dsgl.core.portal.syncPortalFrame import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.select.SelectStyle import org.dreamfinity.dsgl.core.select.selectModel @@ -21,7 +21,7 @@ import kotlin.test.Test import kotlin.test.assertFalse import kotlin.test.assertTrue -class SelectNodeOwnerScopeTests { +class SelectNodeOwnerDomainTests { private val ctx = object : UiMeasureContext { override val fontHeight: Int = 9 @@ -51,7 +51,7 @@ class SelectNodeOwnerScopeTests { option("a", "Alpha") option("b", "Beta") }, - ownerScope = OverlayOwnerScope.System, + ownerDomain = ScreenDomainId.System, key = ownerKey, ).apply { width = 120 @@ -63,7 +63,7 @@ class SelectNodeOwnerScopeTests { val tree = DomTree(root) tree.render(ctx, 300, 200) tree.paint(ctx) - val router = LayerDomInputRouter { root } + val router = SurfaceDomInputRouter { root } val clickX = select.bounds.x + (select.bounds.width / 2).coerceAtLeast(1) val clickY = select.bounds.y + (select.bounds.height / 2).coerceAtLeast(1) @@ -92,7 +92,7 @@ class SelectNodeOwnerScopeTests { option("a", "Alpha") option("b", "Beta") }, - ownerScope = OverlayOwnerScope.Application, + ownerDomain = ScreenDomainId.Application, key = ownerKey, ).apply { width = 120 @@ -104,7 +104,7 @@ class SelectNodeOwnerScopeTests { val tree = DomTree(root) tree.render(ctx, 300, 200) tree.paint(ctx) - val router = LayerDomInputRouter { root } + val router = SurfaceDomInputRouter { root } val clickX = select.bounds.x + (select.bounds.width / 2).coerceAtLeast(1) val clickY = select.bounds.y + (select.bounds.height / 2).coerceAtLeast(1) @@ -141,7 +141,7 @@ class SelectNodeOwnerScopeTests { option("id-$index", "Option $index") } }, - ownerScope = OverlayOwnerScope.Application, + ownerDomain = ScreenDomainId.Application, key = ownerKey, ).apply { width = 120 @@ -153,9 +153,9 @@ class SelectNodeOwnerScopeTests { val tree = DomTree(root) tree.render(ctx, 300, 160) tree.paint(ctx) - val router = LayerDomInputRouter { root } - val applicationOverlayHost = ApplicationOverlayHost() - applicationOverlayHost.onInputFrame(300, 160) + val router = SurfaceDomInputRouter { root } + val applicationPortalHost = ApplicationPortalHost() + applicationPortalHost.onInputFrame(300, 160) val clickX = select.bounds.x + (select.bounds.width / 2).coerceAtLeast(1) val clickY = select.bounds.y + (select.bounds.height / 2).coerceAtLeast(1) @@ -163,13 +163,13 @@ class SelectNodeOwnerScopeTests { assertTrue(FocusManager.isFocused(select)) assertTrue(DomainPortalServices.applicationSelectEngine.isOpenFor(ownerKey)) - applicationOverlayHost.syncPortalFrame(ctx, 300, 160, 1f, 0, 0) + applicationPortalHost.syncPortalFrame(ctx, 300, 160, 1f, 0, 0) val track = requireNotNull(DomainPortalServices.applicationSelectEngine.debugScrollbarTrackRect(ownerKey)) val downX = track.x + track.width / 2 val downY = track.y + 2 assertTrue( - applicationOverlayHost.handlePortalPointerAfterDom( + applicationPortalHost.handlePortalPointerAfterDom( mouseX = downX, mouseY = downY, dWheel = 0, diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectPopupAnchoringStickyTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectPopupAnchoringStickyTests.kt index b8d6eb5..255db25 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectPopupAnchoringStickyTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/SelectPopupAnchoringStickyTests.kt @@ -7,8 +7,8 @@ import org.dreamfinity.dsgl.core.dom.layout.Insets import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.DomainPortalServices -import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter +import org.dreamfinity.dsgl.core.portal.DomainPortalServices +import org.dreamfinity.dsgl.core.portal.input.SurfaceDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.select.selectModel import org.dreamfinity.dsgl.core.style.Overflow @@ -203,7 +203,7 @@ class SelectPopupAnchoringStickyTests { val tree = DomTree(root) tree.render(ctx, viewportWidth, viewportHeight) tree.paint(ctx) - val router = LayerDomInputRouter { root } + val router = SurfaceDomInputRouter { root } return Fixture( tree = tree, scroller = scroller, @@ -237,7 +237,7 @@ class SelectPopupAnchoringStickyTests { val scroller: ContainerNode, val select: SelectNode, val ownerKey: String, - val router: LayerDomInputRouter, + val router: SurfaceDomInputRouter, ) private data class PopupGeometry( diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/UnifiedUsedGeometryInspectorCharacterizationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/UnifiedUsedGeometryInspectorCharacterizationTests.kt index e74c40f..e1b5736 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/UnifiedUsedGeometryInspectorCharacterizationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/dom/UnifiedUsedGeometryInspectorCharacterizationTests.kt @@ -304,7 +304,7 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { inspector.onCursorMoved(185, 25) inspector.buildDomSnapshot(800, 600) - val fixedHighlight = inspector.overlayHoveredHighlight() + val fixedHighlight = inspector.portalHoveredHighlight() assertNotNull(fixedHighlight) val fixedUsedGeometry = UsedInteractionGeometryResolver.resolveNodeGeometry(fixed) assertEquals( @@ -314,7 +314,7 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { inspector.onCursorMoved(145, 95) inspector.buildDomSnapshot(800, 600) - val clippedHighlight = inspector.overlayHoveredHighlight() + val clippedHighlight = inspector.portalHoveredHighlight() assertNotNull(clippedHighlight) val rootUsedGeometry = UsedInteractionGeometryResolver.resolveNodeGeometry(root) assertEquals( @@ -359,7 +359,7 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { inspector.buildDomSnapshot(800, 600) assertEquals(relative.key?.toString(), inspector.hoveredKey) - val highlight = inspector.overlayHoveredHighlight() + val highlight = inspector.portalHoveredHighlight() assertNotNull(highlight) assertEquals( usedGeometry.visibleBorderRect ?: Rect(0, 0, 0, 0), @@ -390,7 +390,7 @@ class UnifiedUsedGeometryInspectorCharacterizationTests { ?.toString(), inspector.hoveredKey, ) - val highlight = inspector.overlayHoveredHighlight() + val highlight = inspector.portalHoveredHighlight() assertNotNull(highlight) val fixedGeometry = UsedInteractionGeometryResolver.resolveNodeGeometry(fixture.fixed) assertEquals( diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorControllerTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorControllerTests.kt index e9a1b32..b6b5575 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorControllerTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/InspectorControllerTests.kt @@ -255,7 +255,7 @@ class InspectorControllerTests { controller.handleMouseDown(990, 230, MouseButton.LEFT) renderFrame(controller, 420, 280) - val thumb = controller.overlayScrollbarThumbRect() + val thumb = controller.portalScrollbarThumbRect() if (thumb.width <= 0 || thumb.height <= 0) { fail("Expected inspector scrollbar thumb to be available.") } @@ -432,7 +432,7 @@ class InspectorControllerTests { renderFrame(controller, 1200, 700) val row = - controller.overlayStyleEditorRows().firstOrNull { + controller.portalStyleEditorRows().firstOrNull { it.property == StyleProperty.BACKGROUND_COLOR && it.editorKind == InspectorEditorKind.StringInput } ?: error("Expected color string input row.") val inputRect = row.inputRect ?: row.controlRect @@ -462,7 +462,7 @@ class InspectorControllerTests { controller.handleMouseDown(988, 126, MouseButton.LEFT) renderFrame(controller, 260, 240) - val rows = controller.overlayStyleEditorRows() + val rows = controller.portalStyleEditorRows() assertTrue(rows.isNotEmpty()) assertTrue(rows.any { it.rowRect.height > it.controlRect.height + 8 }) @@ -487,7 +487,7 @@ class InspectorControllerTests { renderFrame(controller, 1200, 700) val row = - controller.overlayStyleEditorRows().firstOrNull { it.editorKind == InspectorEditorKind.EnumSelect } + controller.portalStyleEditorRows().firstOrNull { it.editorKind == InspectorEditorKind.EnumSelect } ?: error("Expected enum select row.") val openX = row.controlRect.x + 4 val openY = row.controlRect.y + row.controlRect.height / 2 @@ -495,7 +495,7 @@ class InspectorControllerTests { assertTrue(controller.handleMouseDown(openX, openY, MouseButton.LEFT)) renderFrame(controller, 1200, 700) - val dropdown = controller.overlayStyleEditorDropdowns().firstOrNull() ?: error("Expected open dropdown.") + val dropdown = controller.portalStyleEditorDropdowns().firstOrNull() ?: error("Expected open dropdown.") val option = dropdown.options.firstOrNull { !it.text.equals(row.controlValue, ignoreCase = true) } ?: dropdown.options.firstOrNull() @@ -508,7 +508,7 @@ class InspectorControllerTests { assertTrue(controller.handleMouseDown(optionX, optionY, MouseButton.LEFT)) renderFrame(controller, 1200, 700) - assertTrue(controller.overlayStyleEditorDropdowns().isEmpty()) + assertTrue(controller.portalStyleEditorDropdowns().isEmpty()) val literal = (StyleEngine.inspectorOverrideFor(selected, row.property) as? StyleExpression.Literal)?.value assertNotNull(literal) assertTrue(literal.equals(option.text, ignoreCase = true)) @@ -528,7 +528,7 @@ class InspectorControllerTests { controller.onCursorMoved(988, 126) controller.handleMouseDown(988, 126, MouseButton.LEFT) - assertTrue(controller.overlayApplyNumericOverride(StyleProperty.Z_INDEX, "5", "px")) + assertTrue(controller.portalApplyNumericOverride(StyleProperty.Z_INDEX, "5", "px")) val zIndexLiteral = ( StyleEngine.inspectorOverrideFor( @@ -538,7 +538,7 @@ class InspectorControllerTests { )?.value assertEquals("5", zIndexLiteral) - assertTrue(controller.overlayApplyNumericOverride(StyleProperty.WIDTH, "24", "em")) + assertTrue(controller.portalApplyNumericOverride(StyleProperty.WIDTH, "24", "em")) val widthLiteral = ( StyleEngine.inspectorOverrideFor( diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayFocusIsolationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorPortalFocusIsolationTests.kt similarity index 80% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayFocusIsolationTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorPortalFocusIsolationTests.kt index 6ca60f5..8600c6b 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayFocusIsolationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorPortalFocusIsolationTests.kt @@ -15,8 +15,8 @@ import org.dreamfinity.dsgl.core.input.ClipboardAccess import org.dreamfinity.dsgl.core.input.ClipboardBridge import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.inspector.InspectorMode -import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter -import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayHost +import org.dreamfinity.dsgl.core.portal.input.SurfaceDomInputRouter +import org.dreamfinity.dsgl.core.portal.system.SystemPortalHost import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleProperty @@ -26,7 +26,7 @@ import kotlin.test.assertEquals import kotlin.test.assertNotEquals import kotlin.test.assertTrue -class SystemInspectorOverlayFocusIsolationTests { +class SystemInspectorPortalFocusIsolationTests { private val ctx = object : UiMeasureContext { override val fontHeight: Int = 9 @@ -58,17 +58,17 @@ class SystemInspectorOverlayFocusIsolationTests { bounds = Rect(24, 24, 180, 24) } input.applyParent(appRoot) - val router = LayerDomInputRouter { appRoot } + val router = SurfaceDomInputRouter { appRoot } - val (inspector, overlayNode, inspectedRoot) = createMountedInspector() - renderInspectorFrame(overlayNode, inspectedRoot, 1L) + val (inspector, portalNode, inspectedRoot) = createMountedInspector() + renderInspectorFrame(portalNode, inspectedRoot, 1L) assertTrue(router.handleMouseDown(30, 30, MouseButton.LEFT)) assertTrue(router.handleMouseUp(30, 30, MouseButton.LEFT)) assertTrue(FocusManager.isFocused(input)) repeat(4) { frame -> - renderInspectorFrame(overlayNode, inspectedRoot, 2L + frame) + renderInspectorFrame(portalNode, inspectedRoot, 2L + frame) assertTrue(FocusManager.isFocused(input)) } @@ -90,14 +90,14 @@ class SystemInspectorOverlayFocusIsolationTests { bounds = Rect(24, 24, 180, 24) } input.applyParent(appRoot) - val router = LayerDomInputRouter { appRoot } + val router = SurfaceDomInputRouter { appRoot } - val (_, overlayNode, inspectedRoot) = createMountedInspector() - renderInspectorFrame(overlayNode, inspectedRoot, 1L) + val (_, portalNode, inspectedRoot) = createMountedInspector() + renderInspectorFrame(portalNode, inspectedRoot, 1L) assertTrue(router.handleMouseDown(28, 30, MouseButton.LEFT)) assertTrue(router.handleMouseMove(70, 30)) - renderInspectorFrame(overlayNode, inspectedRoot, 2L) + renderInspectorFrame(portalNode, inspectedRoot, 2L) assertTrue(router.handleMouseMove(88, 30)) assertTrue(router.handleMouseUp(88, 30, MouseButton.LEFT)) @@ -119,15 +119,15 @@ class SystemInspectorOverlayFocusIsolationTests { val range = RangeInputNode(value = 0L, min = 0L, max = 100L, key = "app-range") range.applyParent(appRoot) range.render(ctx, 24, 24, 120, 12) - val router = LayerDomInputRouter { appRoot } + val router = SurfaceDomInputRouter { appRoot } - val (_, overlayNode, inspectedRoot) = createMountedInspector() - renderInspectorFrame(overlayNode, inspectedRoot, 1L) + val (_, portalNode, inspectedRoot) = createMountedInspector() + renderInspectorFrame(portalNode, inspectedRoot, 1L) assertTrue(router.handleMouseDown(24, 30, MouseButton.LEFT)) - renderInspectorFrame(overlayNode, inspectedRoot, 2L) + renderInspectorFrame(portalNode, inspectedRoot, 2L) assertTrue(router.handleMouseMove(200, 30)) - renderInspectorFrame(overlayNode, inspectedRoot, 3L) + renderInspectorFrame(portalNode, inspectedRoot, 3L) assertTrue(router.handleMouseUp(200, 30, MouseButton.LEFT)) assertTrue(range.value > 0L) } @@ -143,17 +143,17 @@ class SystemInspectorOverlayFocusIsolationTests { bounds = Rect(24, 24, 180, 24) } input.applyParent(appRoot) - val router = LayerDomInputRouter { appRoot } + val router = SurfaceDomInputRouter { appRoot } - val (_, overlayNode, inspectedRoot) = createMountedInspector() - renderInspectorFrame(overlayNode, inspectedRoot, 1L) + val (_, portalNode, inspectedRoot) = createMountedInspector() + renderInspectorFrame(portalNode, inspectedRoot, 1L) assertTrue(router.handleMouseDown(30, 30, MouseButton.LEFT)) assertTrue(router.handleMouseUp(30, 30, MouseButton.LEFT)) assertTrue(FocusManager.isFocused(input)) repeat(3) { frame -> - renderInspectorFrame(overlayNode, inspectedRoot, 2L + frame) + renderInspectorFrame(portalNode, inspectedRoot, 2L + frame) assertTrue(router.handleKeyDown(KeyCodes.Z, 'z')) } @@ -163,7 +163,7 @@ class SystemInspectorOverlayFocusIsolationTests { @Test fun `inspector still handles its own input interactions after focus isolation fix`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) val root = inspectedRoot() inspector.toggle() @@ -172,22 +172,22 @@ class SystemInspectorOverlayFocusIsolationTests { host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 40, cursorY = 30, inspectorPointerCaptured = false) host.render(ctx, 1280, 720) - val pickRect = inspector.overlayPickToggleBounds() ?: error("pick toggle missing") + val pickRect = inspector.portalPickToggleBounds() ?: error("pick toggle missing") assertTrue(host.handleMouseDown(pickRect.x + 2, pickRect.y + 2, MouseButton.LEFT)) assertTrue(host.handleMouseUp(pickRect.x + 2, pickRect.y + 2, MouseButton.LEFT)) assertEquals(InspectorMode.Pick, inspector.mode) } - private fun createMountedInspector(): Triple { + private fun createMountedInspector(): Triple { val inspector = InspectorController() inspector.toggle() inspector.setPickMode(false) - val overlayNode = SystemInspectorOverlayNode(inspector) + val portalNode = SystemInspectorPortalNode(inspector) val inspectedRoot = inspectedRoot() - return Triple(inspector, overlayNode, inspectedRoot) + return Triple(inspector, portalNode, inspectedRoot) } - private fun renderInspectorFrame(node: SystemInspectorOverlayNode, inspectedRoot: DOMNode, revision: Long) { + private fun renderInspectorFrame(node: SystemInspectorPortalNode, inspectedRoot: DOMNode, revision: Long) { node.bindInspectedTree(inspectedRoot, revision) node.updateCursor(mouseX = 32, mouseY = 32, pointerCaptured = false) node.render(ctx, 0, 0, 1280, 720) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayInputBoundsTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorPortalInputBoundsTests.kt similarity index 91% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayInputBoundsTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorPortalInputBoundsTests.kt index c802936..80c9e5c 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorOverlayInputBoundsTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/inspector/internal/SystemInspectorPortalInputBoundsTests.kt @@ -9,7 +9,7 @@ import kotlin.test.Test import kotlin.test.assertFalse import kotlin.test.assertTrue -class SystemInspectorOverlayInputBoundsTests { +class SystemInspectorPortalInputBoundsTests { @Test fun `input bounds include rendered dropdown popup outside panel`() { val controller = @@ -17,8 +17,8 @@ class SystemInspectorOverlayInputBoundsTests { it.toggle() it.setPickMode(false) } - val node = SystemInspectorOverlayNode(controller) - val panelRect = controller.overlayPanelRect() ?: error("expected panel rect") + val node = SystemInspectorPortalNode(controller) + val panelRect = controller.floatingPanelRect() ?: error("expected panel rect") val popupRect = Rect( diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayDebugVisualizationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayDebugVisualizationTests.kt deleted file mode 100644 index 1490ff9..0000000 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayDebugVisualizationTests.kt +++ /dev/null @@ -1,95 +0,0 @@ -package org.dreamfinity.dsgl.core.overlay - -import org.dreamfinity.dsgl.core.DomTree -import org.dreamfinity.dsgl.core.debug.OverlayLayerDebugState -import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext -import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayRootNode -import org.dreamfinity.dsgl.core.render.RenderCommand -import org.dreamfinity.dsgl.core.style.StyleApplicationScope -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class OverlayDebugVisualizationTests { - private val ctx = - object : UiMeasureContext { - override val fontHeight: Int = 9 - - override fun measureText(text: String): Int = text.length * 6 - - override fun paint(commands: List) = Unit - } - - @AfterTest - fun cleanup() { - OverlayDebugVisualization.setTestOverride(null) - OverlayLayerDebugState.resetAll() - } - - @Test - fun `debug visualization disabled by default`() { - OverlayLayerDebugState.resetAll() - OverlayDebugVisualization.setTestOverride(false) - val appTree = DomTree(root = ApplicationOverlayRootNode(), styleScope = StyleApplicationScope.Application) - val systemTree = DomTree(root = SystemOverlayRootNode(), styleScope = StyleApplicationScope.SystemOverlay) - - appTree.render(ctx, 800, 480) - systemTree.render(ctx, 800, 480) - val appCommands = appTree.paint(ctx, applyStyles = true) - val systemCommands = systemTree.paint(ctx, applyStyles = true) - - assertFalse( - appCommands.any { command -> - command is RenderCommand.DrawRect && - ( - command.color == OverlayDebugVisualization.applicationOverlayFillColor || - command.color == OverlayDebugVisualization.applicationOverlayBorderColor - ) - }, - ) - assertFalse( - systemCommands.any { command -> - command is RenderCommand.DrawRect && - ( - command.color == OverlayDebugVisualization.systemOverlayFillColor || - command.color == OverlayDebugVisualization.systemOverlayBorderColor - ) - }, - ) - } - - @Test - fun `debug visualization can be enabled without changing overlay logic contracts`() { - OverlayLayerDebugState.resetAll() - OverlayLayerDebugState.applicationOverlayTintEnabled = true - OverlayLayerDebugState.systemOverlayTintEnabled = true - OverlayDebugVisualization.setTestOverride(true) - val appTree = DomTree(root = ApplicationOverlayRootNode(), styleScope = StyleApplicationScope.Application) - val systemTree = DomTree(root = SystemOverlayRootNode(), styleScope = StyleApplicationScope.SystemOverlay) - - appTree.render(ctx, 800, 480) - systemTree.render(ctx, 800, 480) - val appCommands = appTree.paint(ctx, applyStyles = true) - val systemCommands = systemTree.paint(ctx, applyStyles = true) - - assertTrue( - appCommands.any { command -> - command is RenderCommand.DrawRect && - ( - command.color == OverlayDebugVisualization.applicationOverlayFillColor || - command.color == OverlayDebugVisualization.applicationOverlayBorderColor - ) - }, - ) - assertTrue( - systemCommands.any { command -> - command is RenderCommand.DrawRect && - ( - command.color == OverlayDebugVisualization.systemOverlayFillColor || - command.color == OverlayDebugVisualization.systemOverlayBorderColor - ) - }, - ) - } -} diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationDndGhostPortalTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/ApplicationDndGhostPortalTests.kt similarity index 96% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationDndGhostPortalTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/ApplicationDndGhostPortalTests.kt index 1fb38cb..3ca8268 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationDndGhostPortalTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/ApplicationDndGhostPortalTests.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay +package org.dreamfinity.dsgl.core.portal import org.dreamfinity.dsgl.core.dnd.DndRuntime import org.dreamfinity.dsgl.core.dom.DOMNode @@ -31,7 +31,7 @@ class ApplicationDndGhostPortalTests { @Test fun `application drag ghost paints through application portal entry`() { - val host = ApplicationOverlayHost() + val host = ApplicationPortalHost() val root = draggableRoot() val draggable = root.children.first() @@ -55,7 +55,7 @@ class ApplicationDndGhostPortalTests { @Test fun `application drag ghost portal deactivates after drag cancel`() { - val host = ApplicationOverlayHost() + val host = ApplicationPortalHost() val root = draggableRoot() val draggable = root.children.first() diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationFloatingWindowPortalTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/ApplicationFloatingWindowPortalTests.kt similarity index 95% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationFloatingWindowPortalTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/ApplicationFloatingWindowPortalTests.kt index dc02ab5..ef32249 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/ApplicationFloatingWindowPortalTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/ApplicationFloatingWindowPortalTests.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay +package org.dreamfinity.dsgl.core.portal import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton @@ -22,7 +22,7 @@ class ApplicationFloatingWindowPortalTests { @Test fun `F10 floating window toggles through application portal and keeps stable identity while open`() { - val host = ApplicationOverlayHost() + val host = ApplicationPortalHost() host.onInputFrame(1280, 720) host.toggleFloatingWindowDemo(anchorX = 160, anchorY = 120) @@ -48,7 +48,7 @@ class ApplicationFloatingWindowPortalTests { @Test fun `F10 floating window supports DOM button click and pointer-captured drag`() { - val host = ApplicationOverlayHost() + val host = ApplicationPortalHost() host.onInputFrame(1280, 720) host.toggleFloatingWindowDemo(anchorX = 220, anchorY = 160) @@ -79,7 +79,7 @@ class ApplicationFloatingWindowPortalTests { @Test fun `F10 floating window consumes body hover and pointer input without blocking child controls`() { - val host = ApplicationOverlayHost() + val host = ApplicationPortalHost() host.onInputFrame(1280, 720) host.toggleFloatingWindowDemo(anchorX = 220, anchorY = 160) @@ -102,7 +102,7 @@ class ApplicationFloatingWindowPortalTests { @Test fun `F10 floating window does not consume outside panel input`() { - val host = ApplicationOverlayHost() + val host = ApplicationPortalHost() host.onInputFrame(1280, 720) host.toggleFloatingWindowDemo(anchorX = 220, anchorY = 160) @@ -120,7 +120,7 @@ class ApplicationFloatingWindowPortalTests { @Test fun `F10 floating window follows frame cursor during active drag`() { - val host = ApplicationOverlayHost() + val host = ApplicationPortalHost() host.onInputFrame(1280, 720) host.toggleFloatingWindowDemo(anchorX = 220, anchorY = 160) @@ -148,7 +148,7 @@ class ApplicationFloatingWindowPortalTests { @Test fun `F10 floating window close button closes and reopen restores interactions`() { - val host = ApplicationOverlayHost() + val host = ApplicationPortalHost() host.onInputFrame(1280, 720) host.toggleFloatingWindowDemo(anchorX = 280, anchorY = 180) @@ -173,7 +173,7 @@ class ApplicationFloatingWindowPortalTests { @Test fun `F10 floating window uses render viewport before first mouse input`() { - val host = ApplicationOverlayHost() + val host = ApplicationPortalHost() host.render(ctx, 1280, 720) host.toggleFloatingWindowDemo(anchorX = 460, anchorY = 320) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/DomainSurfaceDebugVisualizationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/DomainSurfaceDebugVisualizationTests.kt new file mode 100644 index 0000000..487262f --- /dev/null +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/DomainSurfaceDebugVisualizationTests.kt @@ -0,0 +1,95 @@ +package org.dreamfinity.dsgl.core.portal + +import org.dreamfinity.dsgl.core.DomTree +import org.dreamfinity.dsgl.core.debug.DomainSurfaceDebugState +import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext +import org.dreamfinity.dsgl.core.portal.system.SystemPortalRootNode +import org.dreamfinity.dsgl.core.render.RenderCommand +import org.dreamfinity.dsgl.core.style.StyleApplicationScope +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class DomainSurfaceDebugVisualizationTests { + private val ctx = + object : UiMeasureContext { + override val fontHeight: Int = 9 + + override fun measureText(text: String): Int = text.length * 6 + + override fun paint(commands: List) = Unit + } + + @AfterTest + fun cleanup() { + DomainSurfaceDebugVisualization.setTestOverride(null) + DomainSurfaceDebugState.resetAll() + } + + @Test + fun `debug visualization disabled by default`() { + DomainSurfaceDebugState.resetAll() + DomainSurfaceDebugVisualization.setTestOverride(false) + val appTree = DomTree(root = ApplicationPortalRootNode(), styleScope = StyleApplicationScope.Application) + val systemTree = DomTree(root = SystemPortalRootNode(), styleScope = StyleApplicationScope.System) + + appTree.render(ctx, 800, 480) + systemTree.render(ctx, 800, 480) + val appCommands = appTree.paint(ctx, applyStyles = true) + val systemCommands = systemTree.paint(ctx, applyStyles = true) + + assertFalse( + appCommands.any { command -> + command is RenderCommand.DrawRect && + ( + command.color == DomainSurfaceDebugVisualization.applicationPortalFillColor || + command.color == DomainSurfaceDebugVisualization.applicationPortalBorderColor + ) + }, + ) + assertFalse( + systemCommands.any { command -> + command is RenderCommand.DrawRect && + ( + command.color == DomainSurfaceDebugVisualization.systemPortalFillColor || + command.color == DomainSurfaceDebugVisualization.systemPortalBorderColor + ) + }, + ) + } + + @Test + fun `debug visualization can be enabled without changing domain-surface logic contracts`() { + DomainSurfaceDebugState.resetAll() + DomainSurfaceDebugState.applicationPortalTintEnabled = true + DomainSurfaceDebugState.systemPortalTintEnabled = true + DomainSurfaceDebugVisualization.setTestOverride(true) + val appTree = DomTree(root = ApplicationPortalRootNode(), styleScope = StyleApplicationScope.Application) + val systemTree = DomTree(root = SystemPortalRootNode(), styleScope = StyleApplicationScope.System) + + appTree.render(ctx, 800, 480) + systemTree.render(ctx, 800, 480) + val appCommands = appTree.paint(ctx, applyStyles = true) + val systemCommands = systemTree.paint(ctx, applyStyles = true) + + assertTrue( + appCommands.any { command -> + command is RenderCommand.DrawRect && + ( + command.color == DomainSurfaceDebugVisualization.applicationPortalFillColor || + command.color == DomainSurfaceDebugVisualization.applicationPortalBorderColor + ) + }, + ) + assertTrue( + systemCommands.any { command -> + command is RenderCommand.DrawRect && + ( + command.color == DomainSurfaceDebugVisualization.systemPortalFillColor || + command.color == DomainSurfaceDebugVisualization.systemPortalBorderColor + ) + }, + ) + } +} diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/DomainSurfaceInteractionPathTests.kt similarity index 69% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/DomainSurfaceInteractionPathTests.kt index 1136000..6fc0971 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/LiveLayerInteractionPathTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/DomainSurfaceInteractionPathTests.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay +package org.dreamfinity.dsgl.core.portal import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupRequest import org.dreamfinity.dsgl.core.colorpicker.ColorPickerState @@ -15,8 +15,8 @@ import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController -import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayEntryId -import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayHost +import org.dreamfinity.dsgl.core.portal.system.SystemPortalEntryId +import org.dreamfinity.dsgl.core.portal.system.SystemPortalHost import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.select.SelectEntry import org.dreamfinity.dsgl.core.select.SelectOpenRequest @@ -30,7 +30,7 @@ import kotlin.test.assertNotNull import kotlin.test.assertTrue @Suppress("LargeClass") -class LiveLayerInteractionPathTests { +class DomainSurfaceInteractionPathTests { private val ctx = object : UiMeasureContext { override val fontHeight: Int = 9 @@ -52,7 +52,7 @@ class LiveLayerInteractionPathTests { fun `runtime input path resolves in full domain surface order`() { val callOrder = ArrayList(6) val fixture = - LiveLayerInputFixture( + DomainSurfaceInputFixture( debugPortalHandler = { _, _, _ -> callOrder += ScreenDomainSurfaces.DebugPortal false @@ -61,7 +61,7 @@ class LiveLayerInteractionPathTests { callOrder += ScreenDomainSurfaces.DebugRoot false }, - systemOverlayHandler = { _, _, _ -> + systemPortalHandler = { _, _, _ -> callOrder += ScreenDomainSurfaces.SystemPortal false }, @@ -69,7 +69,7 @@ class LiveLayerInteractionPathTests { callOrder += ScreenDomainSurfaces.SystemRoot false }, - applicationOverlayHandler = { _, _, _ -> + applicationPortalHandler = { _, _, _ -> callOrder += ScreenDomainSurfaces.ApplicationPortal false }, @@ -98,21 +98,21 @@ class LiveLayerInteractionPathTests { fun `debug portal consumption prevents lower-domain fallthrough`() { var debugRootReceived = false var systemReceived = false - var appOverlayReceived = false + var appPortalReceived = false var appRootReceived = false val fixture = - LiveLayerInputFixture( + DomainSurfaceInputFixture( debugPortalHandler = { _, _, _ -> true }, debugHandler = { _, _, _ -> debugRootReceived = true false }, - systemOverlayHandler = { _, _, _ -> + systemPortalHandler = { _, _, _ -> systemReceived = true false }, - applicationOverlayHandler = { _, _, _ -> - appOverlayReceived = true + applicationPortalHandler = { _, _, _ -> + appPortalReceived = true false }, ) @@ -125,24 +125,24 @@ class LiveLayerInteractionPathTests { assertEquals(ScreenDomainSurfaces.DebugPortal, consumedBy) assertFalse(debugRootReceived) assertFalse(systemReceived) - assertFalse(appOverlayReceived) + assertFalse(appPortalReceived) assertFalse(appRootReceived) } @Test fun `debug root consumption prevents lower-domain fallthrough`() { var systemReceived = false - var appOverlayReceived = false + var appPortalReceived = false var appRootReceived = false val fixture = - LiveLayerInputFixture( + DomainSurfaceInputFixture( debugHandler = { _, _, _ -> true }, - systemOverlayHandler = { _, _, _ -> + systemPortalHandler = { _, _, _ -> systemReceived = true false }, - applicationOverlayHandler = { _, _, _ -> - appOverlayReceived = true + applicationPortalHandler = { _, _, _ -> + appPortalReceived = true false }, ) @@ -154,21 +154,21 @@ class LiveLayerInteractionPathTests { assertEquals(ScreenDomainSurfaces.DebugRoot, consumedBy) assertFalse(systemReceived) - assertFalse(appOverlayReceived) + assertFalse(appPortalReceived) assertFalse(appRootReceived) } @Test fun `system root consumption prevents lower-domain fallthrough`() { - var appOverlayReceived = false + var appPortalReceived = false var appRootReceived = false val fixture = - LiveLayerInputFixture( + DomainSurfaceInputFixture( debugHandler = { _, _, _ -> false }, - systemOverlayHandler = { _, _, _ -> false }, + systemPortalHandler = { _, _, _ -> false }, systemRootHandler = { _, _, _ -> true }, - applicationOverlayHandler = { _, _, _ -> - appOverlayReceived = true + applicationPortalHandler = { _, _, _ -> + appPortalReceived = true false }, ) @@ -179,14 +179,14 @@ class LiveLayerInteractionPathTests { } assertEquals(ScreenDomainSurfaces.SystemRoot, consumedBy) - assertFalse(appOverlayReceived) + assertFalse(appPortalReceived) assertFalse(appRootReceived) } @Test fun `system portal consumption prevents lower-domain fallthrough`() { val inspector = InspectorController() - val systemHost = SystemOverlayHost(inspector) + val systemHost = SystemPortalHost(inspector) val root = inspectedRoot() systemHost.onInputFrame(1280, 720) inspector.toggle() @@ -199,13 +199,13 @@ class LiveLayerInteractionPathTests { ) systemHost.render(ctx, 1280, 720) - val entryState = systemHost.debugEntryState(SystemOverlayEntryId.Inspector) ?: error("inspector state missing") + val entryState = systemHost.debugEntryState(SystemPortalEntryId.Inspector) ?: error("inspector state missing") val panelRect = entryState.panelState.currentRectOrNull() ?: error("inspector panel rect missing") val fixture = - LiveLayerInputFixture( + DomainSurfaceInputFixture( debugHandler = { _, _, _ -> false }, - systemOverlayHandler = { x, y, button -> systemHost.handleMouseDown(x, y, button) }, - applicationOverlayHandler = { _, _, _ -> false }, + systemPortalHandler = { x, y, button -> systemHost.handleMouseDown(x, y, button) }, + applicationPortalHandler = { _, _, _ -> false }, ) var appRootReceived = false val consumedBy = @@ -221,7 +221,7 @@ class LiveLayerInteractionPathTests { @Test fun `locked inspector consumes only inside panel and falls through outside panel`() { val inspector = InspectorController() - val systemHost = SystemOverlayHost(inspector) + val systemHost = SystemPortalHost(inspector) val root = inspectedRoot() systemHost.onInputFrame(1280, 720) inspector.toggle() @@ -235,16 +235,16 @@ class LiveLayerInteractionPathTests { ) systemHost.render(ctx, 1280, 720) - val panelRect = inspector.overlayPanelRect() ?: error("inspector panel rect missing") + val panelRect = inspector.floatingPanelRect() ?: error("inspector panel rect missing") val outsideX = if (panelRect.x > 40) panelRect.x - 20 else panelRect.x + panelRect.width + 20 val outsideY = (panelRect.y + panelRect.height / 2).coerceIn(1, 719) assertFalse(panelRect.contains(outsideX, outsideY)) val fixture = - LiveLayerInputFixture( + DomainSurfaceInputFixture( debugHandler = { _, _, _ -> false }, - systemOverlayHandler = { x, y, button -> systemHost.handleMouseDown(x, y, button) }, - applicationOverlayHandler = { _, _, _ -> false }, + systemPortalHandler = { x, y, button -> systemHost.handleMouseDown(x, y, button) }, + applicationPortalHandler = { _, _, _ -> false }, ) var appRootReceivedOutside = false @@ -258,12 +258,12 @@ class LiveLayerInteractionPathTests { } @Test - fun `application overlay consumption prevents app-root fallthrough`() { + fun `application portal consumption prevents app-root fallthrough`() { val fixture = - LiveLayerInputFixture( + DomainSurfaceInputFixture( debugHandler = { _, _, _ -> false }, - systemOverlayHandler = { _, _, _ -> false }, - applicationOverlayHandler = { _, _, _ -> true }, + systemPortalHandler = { _, _, _ -> false }, + applicationPortalHandler = { _, _, _ -> true }, ) var appRootReceived = false val consumedBy = @@ -277,26 +277,26 @@ class LiveLayerInteractionPathTests { } @Test - fun `application overlay host dom bridge consumes mounted node and blocks app-root fallthrough`() { - val applicationOverlayHost = ApplicationOverlayHost() - applicationOverlayHost.onInputFrame(1280, 720) + fun `application portal host dom bridge consumes mounted node and blocks app-root fallthrough`() { + val applicationPortalHost = ApplicationPortalHost() + applicationPortalHost.onInputFrame(1280, 720) var clicks = 0 - ButtonNode("Overlay", key = "app-overlay-button") + ButtonNode("Portal", key = "app-portal-button") .apply { bounds = Rect(40, 44, 120, 24) onClick { clicks += 1 } - }.applyParent(applicationOverlayRoot(applicationOverlayHost)) + }.applyParent(applicationPortalRoot(applicationPortalHost)) - assertTrue(applicationOverlayHost.handleMouseDown(50, 50, MouseButton.LEFT)) - assertTrue(applicationOverlayHost.handleMouseUp(50, 50, MouseButton.LEFT)) + assertTrue(applicationPortalHost.handleMouseDown(50, 50, MouseButton.LEFT)) + assertTrue(applicationPortalHost.handleMouseUp(50, 50, MouseButton.LEFT)) assertEquals(1, clicks) val fixture = - LiveLayerInputFixture( + DomainSurfaceInputFixture( debugHandler = { _, _, _ -> false }, - systemOverlayHandler = { _, _, _ -> false }, - applicationOverlayHandler = { x, y, button -> - applicationOverlayHost.handleMouseDown(x, y, button) + systemPortalHandler = { _, _, _ -> false }, + applicationPortalHandler = { x, y, button -> + applicationPortalHost.handleMouseDown(x, y, button) }, ) var appRootReceived = false @@ -312,8 +312,8 @@ class LiveLayerInteractionPathTests { @Test fun `application context menu is rendered and consumed through application portal path`() { - val applicationOverlayHost = ApplicationOverlayHost() - applicationOverlayHost.onInputFrame(320, 180) + val applicationPortalHost = ApplicationPortalHost() + applicationPortalHost.onInputFrame(320, 180) var actionHits = 0 DomainPortalServices.applicationContextMenuEngine.openAtCursor( contextMenu(id = "portal.context") { @@ -325,14 +325,14 @@ class LiveLayerInteractionPathTests { y = 24, ) - applicationOverlayHost.syncPortalFrame(ctx, 320, 180, 1f, 24, 24) + applicationPortalHost.syncPortalFrame(ctx, 320, 180, 1f, 24, 24) val commands = ArrayList() - applicationOverlayHost.appendPortalOverlayCommands(ctx, 320, 180, commands) + applicationPortalHost.appendFloatingPortalCommands(ctx, 320, 180, commands) val firstEntryRect = DomainPortalServices.applicationContextMenuEngine.debugEntryRect(levelIndex = 0, entryIndex = 0) assertNotNull(firstEntryRect) val consumedByMenu = - applicationOverlayHost.handlePortalPointerAfterDom( + applicationPortalHost.handlePortalPointerAfterDom( mouseX = firstEntryRect.x + 1, mouseY = firstEntryRect.y + 1, dWheel = 0, @@ -343,13 +343,13 @@ class LiveLayerInteractionPathTests { assertTrue(commands.isNotEmpty()) assertTrue(consumedByMenu) assertEquals(1, actionHits) - assertFalse(applicationOverlayHost.hasOpenContextMenuPortal()) + assertFalse(applicationPortalHost.hasOpenContextMenuPortal()) } @Test fun `application context menu portal blocks app-root fallthrough on outside dismiss`() { - val applicationOverlayHost = ApplicationOverlayHost() - applicationOverlayHost.onInputFrame(320, 180) + val applicationPortalHost = ApplicationPortalHost() + applicationPortalHost.onInputFrame(320, 180) DomainPortalServices.applicationContextMenuEngine.openAtCursor( contextMenu(id = "portal.dismiss") { item("Run") @@ -358,18 +358,18 @@ class LiveLayerInteractionPathTests { x = 24, y = 24, ) - applicationOverlayHost.syncPortalFrame(ctx, 320, 180, 1f, 24, 24) + applicationPortalHost.syncPortalFrame(ctx, 320, 180, 1f, 24, 24) val panel = DomainPortalServices.applicationContextMenuEngine.debugPanelRect(0) assertNotNull(panel) val outsideX = panel.x + panel.width + 24 val outsideY = panel.y + panel.height + 24 val fixture = - LiveLayerInputFixture( + DomainSurfaceInputFixture( debugHandler = { _, _, _ -> false }, - systemOverlayHandler = { _, _, _ -> false }, - applicationOverlayHandler = { x, y, button -> - applicationOverlayHost.handlePortalPointerAfterDom(x, y, 0, button, true) + systemPortalHandler = { _, _, _ -> false }, + applicationPortalHandler = { x, y, button -> + applicationPortalHost.handlePortalPointerAfterDom(x, y, 0, button, true) }, ) var appRootReceived = false @@ -381,13 +381,13 @@ class LiveLayerInteractionPathTests { assertEquals(ScreenDomainSurfaces.ApplicationPortal, consumedBy) assertFalse(appRootReceived) - assertFalse(applicationOverlayHost.hasOpenContextMenuPortal()) + assertFalse(applicationPortalHost.hasOpenContextMenuPortal()) } @Test fun `application context menu portal consumes wheel and escape while open`() { - val applicationOverlayHost = ApplicationOverlayHost() - applicationOverlayHost.onInputFrame(320, 180) + val applicationPortalHost = ApplicationPortalHost() + applicationPortalHost.onInputFrame(320, 180) DomainPortalServices.applicationContextMenuEngine.openAtCursor( contextMenu(id = "portal.keyboard") { item("Run") @@ -396,30 +396,30 @@ class LiveLayerInteractionPathTests { x = 24, y = 24, ) - applicationOverlayHost.syncPortalFrame(ctx, 320, 180, 1f, 24, 24) + applicationPortalHost.syncPortalFrame(ctx, 320, 180, 1f, 24, 24) - assertTrue(applicationOverlayHost.handlePortalPointerAfterDom(26, 26, -120, null, false)) - assertTrue(applicationOverlayHost.handlePortalKeyDownAfterDom(KeyCodes.ESCAPE, Char.MIN_VALUE)) - assertFalse(applicationOverlayHost.hasOpenContextMenuPortal()) + assertTrue(applicationPortalHost.handlePortalPointerAfterDom(26, 26, -120, null, false)) + assertTrue(applicationPortalHost.handlePortalKeyDownAfterDom(KeyCodes.ESCAPE, Char.MIN_VALUE)) + assertFalse(applicationPortalHost.hasOpenContextMenuPortal()) } @Test fun `application select is rendered and consumed through application portal path`() { - val applicationOverlayHost = ApplicationOverlayHost() - applicationOverlayHost.onInputFrame(320, 180) + val applicationPortalHost = ApplicationPortalHost() + applicationPortalHost.onInputFrame(320, 180) var selected: String? = null val owner = "application-select-portal" - DomainPortalServices.openSelect(selectRequest(owner, OverlayOwnerScope.Application) { selected = it }) + DomainPortalServices.openSelect(selectRequest(owner, ScreenDomainId.Application) { selected = it }) - applicationOverlayHost.syncPortalFrame(ctx, 320, 180, 1f, 0, 0) + applicationPortalHost.syncPortalFrame(ctx, 320, 180, 1f, 0, 0) val commands = ArrayList() - applicationOverlayHost.appendPortalOverlayCommands(ctx, 320, 180, commands) + applicationPortalHost.appendFloatingPortalCommands(ctx, 320, 180, commands) val panel = DomainPortalServices.applicationSelectEngine.debugPanelRect(owner) assertNotNull(panel) val style = DomainPortalServices.applicationSelectEngine.currentStyle() val consumed = - applicationOverlayHost.handlePortalPointerAfterDom( + applicationPortalHost.handlePortalPointerAfterDom( mouseX = panel.x + style.panelPaddingX + 1, mouseY = panel.y + style.panelPaddingY + 1, dWheel = 0, @@ -431,7 +431,7 @@ class LiveLayerInteractionPathTests { assertTrue(consumed) assertEquals(null, selected) assertTrue( - applicationOverlayHost.handlePortalPointerAfterDom( + applicationPortalHost.handlePortalPointerAfterDom( mouseX = panel.x + style.panelPaddingX + 1, mouseY = panel.y + style.panelPaddingY + 1, dWheel = 0, @@ -444,21 +444,21 @@ class LiveLayerInteractionPathTests { @Test fun `application select portal closes and allows app-root fallthrough on outside dismiss`() { - val applicationOverlayHost = ApplicationOverlayHost() - applicationOverlayHost.onInputFrame(320, 180) + val applicationPortalHost = ApplicationPortalHost() + applicationPortalHost.onInputFrame(320, 180) val owner = "application-select-dismiss" - DomainPortalServices.openSelect(selectRequest(owner, OverlayOwnerScope.Application)) - applicationOverlayHost.syncPortalFrame(ctx, 320, 180, 1f, 0, 0) + DomainPortalServices.openSelect(selectRequest(owner, ScreenDomainId.Application)) + applicationPortalHost.syncPortalFrame(ctx, 320, 180, 1f, 0, 0) val panel = DomainPortalServices.applicationSelectEngine.debugPanelRect(owner) assertNotNull(panel) val outsideX = panel.x + panel.width + 24 val outsideY = panel.y + panel.height + 24 val fixture = - LiveLayerInputFixture( + DomainSurfaceInputFixture( debugHandler = { _, _, _ -> false }, - systemOverlayHandler = { _, _, _ -> false }, - applicationOverlayHandler = { x, y, button -> - applicationOverlayHost.handlePortalPointerAfterDom(x, y, 0, button, true) + systemPortalHandler = { _, _, _ -> false }, + applicationPortalHandler = { x, y, button -> + applicationPortalHost.handlePortalPointerAfterDom(x, y, 0, button, true) }, ) @@ -475,14 +475,14 @@ class LiveLayerInteractionPathTests { @Test fun `application select portal consumes wheel typeahead and escape`() { - val applicationOverlayHost = ApplicationOverlayHost() - applicationOverlayHost.onInputFrame(320, 120) + val applicationPortalHost = ApplicationPortalHost() + applicationPortalHost.onInputFrame(320, 120) val owner = "application-select-keyboard" var selected: String? = null DomainPortalServices.openSelect( selectRequest( owner = owner, - ownerScope = OverlayOwnerScope.Application, + ownerDomain = ScreenDomainId.Application, entries = listOf( SelectEntry.Option("a", labelProvider = { "Alpha" }), @@ -495,39 +495,39 @@ class LiveLayerInteractionPathTests { onSelect = { selected = it }, ), ) - applicationOverlayHost.syncPortalFrame(ctx, 320, 120, 1f, 0, 0) + applicationPortalHost.syncPortalFrame(ctx, 320, 120, 1f, 0, 0) val panel = DomainPortalServices.applicationSelectEngine.debugPanelRect(owner) assertNotNull(panel) - assertTrue(applicationOverlayHost.handlePortalPointerAfterDom(panel.x + 2, panel.y + 2, -120, null, false)) - assertTrue(applicationOverlayHost.handlePortalKeyDownAfterDom(0, 'd')) - assertTrue(applicationOverlayHost.handlePortalKeyDownAfterDom(KeyCodes.ENTER, Char.MIN_VALUE)) + assertTrue(applicationPortalHost.handlePortalPointerAfterDom(panel.x + 2, panel.y + 2, -120, null, false)) + assertTrue(applicationPortalHost.handlePortalKeyDownAfterDom(0, 'd')) + assertTrue(applicationPortalHost.handlePortalKeyDownAfterDom(KeyCodes.ENTER, Char.MIN_VALUE)) assertEquals("d", selected) - DomainPortalServices.openSelect(selectRequest(owner, OverlayOwnerScope.Application)) - applicationOverlayHost.syncPortalFrame(ctx, 320, 120, 1f, 0, 0) - assertTrue(applicationOverlayHost.handlePortalKeyDownAfterDom(KeyCodes.ESCAPE, Char.MIN_VALUE)) + DomainPortalServices.openSelect(selectRequest(owner, ScreenDomainId.Application)) + applicationPortalHost.syncPortalFrame(ctx, 320, 120, 1f, 0, 0) + assertTrue(applicationPortalHost.handlePortalKeyDownAfterDom(KeyCodes.ESCAPE, Char.MIN_VALUE)) } @Test fun `application color picker is rendered and consumed through application portal path`() { - val applicationOverlayHost = ApplicationOverlayHost() - applicationOverlayHost.onInputFrame(360, 240) + val applicationPortalHost = ApplicationPortalHost() + applicationPortalHost.onInputFrame(360, 240) val owner = "application-color-picker-portal" - DomainPortalServices.applicationColorPickerEngine.open(colorPickerRequest(owner, OverlayOwnerScope.Application)) + DomainPortalServices.applicationColorPickerEngine.open(colorPickerRequest(owner, ScreenDomainId.Application)) - applicationOverlayHost.syncPortalFrame(ctx, 360, 240, 1f, 42, 48) + applicationPortalHost.syncPortalFrame(ctx, 360, 240, 1f, 42, 48) val commands = ArrayList() - applicationOverlayHost.appendPortalOverlayCommands(ctx, 360, 240, commands) + applicationPortalHost.appendFloatingPortalCommands(ctx, 360, 240, commands) val layout = DomainPortalServices.applicationColorPickerEngine.debugBodyLayout(owner) assertNotNull(layout) val fixture = - LiveLayerInputFixture( + DomainSurfaceInputFixture( debugHandler = { _, _, _ -> false }, - systemOverlayHandler = { _, _, _ -> false }, - applicationOverlayHandler = { x, y, button -> - applicationOverlayHost.handlePortalPointerBeforeDom(x, y, 0, button, true) + systemPortalHandler = { _, _, _ -> false }, + applicationPortalHandler = { x, y, button -> + applicationPortalHost.handlePortalPointerBeforeDom(x, y, 0, button, true) }, ) var appRootReceived = false @@ -544,30 +544,30 @@ class LiveLayerInteractionPathTests { assertTrue(commands.isNotEmpty()) assertEquals(ScreenDomainSurfaces.ApplicationPortal, consumedBy) assertFalse(appRootReceived) - assertTrue(applicationOverlayHost.hasOpenColorPickerPortal()) + assertTrue(applicationPortalHost.hasOpenColorPickerPortal()) } @Test fun `application color picker portal preserves drag close and eyedropper capture hooks`() { ScreenColorSamplerBridge.install(ScreenColorSampler { x, y -> (0xFF shl 24) or (x shl 16) or (y shl 8) or 0x44 }) - val applicationOverlayHost = ApplicationOverlayHost() - applicationOverlayHost.onInputFrame(480, 320) + val applicationPortalHost = ApplicationPortalHost() + applicationPortalHost.onInputFrame(480, 320) val owner = "application-color-picker-drag-eyedropper" var committed: RgbaColor? = null DomainPortalServices.applicationColorPickerEngine.open( - colorPickerRequest(owner, OverlayOwnerScope.Application) { + colorPickerRequest(owner, ScreenDomainId.Application) { committed = it }, ) - applicationOverlayHost.syncPortalFrame(ctx, 480, 320, 1f, 120, 80) + applicationPortalHost.syncPortalFrame(ctx, 480, 320, 1f, 120, 80) val panelBefore = DomainPortalServices.applicationColorPickerEngine.debugPanelRect(owner) ?: error("panel missing") val header = DomainPortalServices.applicationColorPickerEngine.debugHeaderRect(owner) ?: error("header missing") val dragStartX = header.x + 6 val dragStartY = header.y + 6 - assertTrue(applicationOverlayHost.handlePortalPointerBeforeDom(dragStartX, dragStartY, 0, MouseButton.LEFT, true)) + assertTrue(applicationPortalHost.handlePortalPointerBeforeDom(dragStartX, dragStartY, 0, MouseButton.LEFT, true)) assertTrue( - applicationOverlayHost.handlePortalPointerBeforeDom( + applicationPortalHost.handlePortalPointerBeforeDom( mouseX = dragStartX + 40, mouseY = dragStartY + 30, dWheel = 0, @@ -576,7 +576,7 @@ class LiveLayerInteractionPathTests { ), ) assertTrue( - applicationOverlayHost.handlePortalPointerBeforeDom( + applicationPortalHost.handlePortalPointerBeforeDom( mouseX = dragStartX + 40, mouseY = dragStartY + 30, dWheel = 0, @@ -589,7 +589,7 @@ class LiveLayerInteractionPathTests { val layout = DomainPortalServices.applicationColorPickerEngine.debugBodyLayout(owner) ?: error("layout missing") assertTrue( - applicationOverlayHost.handlePortalPointerBeforeDom( + applicationPortalHost.handlePortalPointerBeforeDom( mouseX = layout.pipetteRect.x + 2, mouseY = layout.pipetteRect.y + 2, dWheel = 0, @@ -597,9 +597,9 @@ class LiveLayerInteractionPathTests { pressed = true, ), ) - assertTrue(applicationOverlayHost.hasActiveColorPickerEyedropper()) - assertTrue(applicationOverlayHost.handlePortalPointerBeforeDom(25, 52, 0, null, false)) - applicationOverlayHost.captureColorPickerEyedropperSample() + assertTrue(applicationPortalHost.hasActiveColorPickerEyedropper()) + assertTrue(applicationPortalHost.handlePortalPointerBeforeDom(25, 52, 0, null, false)) + applicationPortalHost.captureColorPickerEyedropperSample() val firstExpected = RgbaColor.fromArgbInt((0xFF shl 24) or (25 shl 16) or (52 shl 8) or 0x44) assertEquals( firstExpected.toArgbInt(), @@ -610,16 +610,16 @@ class LiveLayerInteractionPathTests { ?.toArgbInt(), ) - assertTrue(applicationOverlayHost.handlePortalPointerBeforeDom(31, 64, 0, null, false)) - applicationOverlayHost.captureColorPickerEyedropperSample() - assertTrue(applicationOverlayHost.handlePortalPointerBeforeDom(31, 64, 0, MouseButton.LEFT, true)) - assertTrue(applicationOverlayHost.handlePortalPointerBeforeDom(31, 64, 0, MouseButton.LEFT, false)) + assertTrue(applicationPortalHost.handlePortalPointerBeforeDom(31, 64, 0, null, false)) + applicationPortalHost.captureColorPickerEyedropperSample() + assertTrue(applicationPortalHost.handlePortalPointerBeforeDom(31, 64, 0, MouseButton.LEFT, true)) + assertTrue(applicationPortalHost.handlePortalPointerBeforeDom(31, 64, 0, MouseButton.LEFT, false)) val expected = RgbaColor.fromArgbInt((0xFF shl 24) or (31 shl 16) or (64 shl 8) or 0x44) assertEquals(expected.toArgbInt(), committed?.toArgbInt()) val closeRect = DomainPortalServices.applicationColorPickerEngine.debugCloseRect(owner) ?: error("close missing") assertTrue( - applicationOverlayHost.handlePortalPointerBeforeDom( + applicationPortalHost.handlePortalPointerBeforeDom( mouseX = closeRect.x + 1, mouseY = closeRect.y + 1, dWheel = 0, @@ -627,50 +627,50 @@ class LiveLayerInteractionPathTests { pressed = true, ), ) - assertFalse(applicationOverlayHost.hasOpenColorPickerPortal()) + assertFalse(applicationPortalHost.hasOpenColorPickerPortal()) } @Test fun `application color picker portal does not consume system owned popup`() { - val applicationOverlayHost = ApplicationOverlayHost() - applicationOverlayHost.onInputFrame(360, 240) + val applicationPortalHost = ApplicationPortalHost() + applicationPortalHost.onInputFrame(360, 240) val owner = "system-color-picker-owner" - DomainPortalServices.applicationColorPickerEngine.open(colorPickerRequest(owner, OverlayOwnerScope.System)) + DomainPortalServices.applicationColorPickerEngine.open(colorPickerRequest(owner, ScreenDomainId.System)) - applicationOverlayHost.syncPortalFrame(ctx, 360, 240, 1f, 42, 48) + applicationPortalHost.syncPortalFrame(ctx, 360, 240, 1f, 42, 48) val commands = ArrayList() - applicationOverlayHost.appendPortalOverlayCommands(ctx, 360, 240, commands) + applicationPortalHost.appendFloatingPortalCommands(ctx, 360, 240, commands) val panel = DomainPortalServices.applicationColorPickerEngine.debugPanelRect(owner) assertNotNull(panel) assertTrue(DomainPortalServices.applicationColorPickerEngine.isOpenFor(owner)) - assertFalse(applicationOverlayHost.hasOpenColorPickerPortal()) - assertFalse(applicationOverlayHost.handlePortalPointerBeforeDom(panel.x + 2, panel.y + 2, 0, MouseButton.LEFT, true)) + assertFalse(applicationPortalHost.hasOpenColorPickerPortal()) + assertFalse(applicationPortalHost.handlePortalPointerBeforeDom(panel.x + 2, panel.y + 2, 0, MouseButton.LEFT, true)) assertTrue(commands.isEmpty()) } @Test fun `system select is rendered and consumed through system portal path`() { - val systemHost = SystemOverlayHost(InspectorController()) + val systemHost = SystemPortalHost(InspectorController()) systemHost.onInputFrame(320, 180) val owner = "system-select-portal" var selected: String? = null - DomainPortalServices.openSelect(selectRequest(owner, OverlayOwnerScope.System) { selected = it }) + DomainPortalServices.openSelect(selectRequest(owner, ScreenDomainId.System) { selected = it }) systemHost.syncPortalFrame(ctx, 320, 180, 1f) val commands = ArrayList() - systemHost.appendPortalOverlayCommands(ctx, 320, 180, commands) + systemHost.appendFloatingPortalCommands(ctx, 320, 180, commands) val panel = DomainPortalServices.systemSelectEngine.debugPanelRect(owner) assertNotNull(panel) val style = DomainPortalServices.systemSelectEngine.currentStyle() val fixture = - LiveLayerInputFixture( + DomainSurfaceInputFixture( debugHandler = { _, _, _ -> false }, - systemOverlayHandler = { x, y, button -> + systemPortalHandler = { x, y, button -> systemHost.handlePortalMouseDown(x, y, button) }, - applicationOverlayHandler = { _, _, _ -> false }, + applicationPortalHandler = { _, _, _ -> false }, ) var appRootReceived = false val consumedBy = @@ -701,11 +701,11 @@ class LiveLayerInteractionPathTests { @Test fun `select owner migration preserves application system routing`() { val owner = "select-owner-migration" - DomainPortalServices.openSelect(selectRequest(owner, OverlayOwnerScope.Application)) + DomainPortalServices.openSelect(selectRequest(owner, ScreenDomainId.Application)) assertTrue(DomainPortalServices.applicationSelectEngine.isOpenFor(owner)) assertFalse(DomainPortalServices.systemSelectEngine.isOpenFor(owner)) - DomainPortalServices.openSelect(selectRequest(owner, OverlayOwnerScope.System)) + DomainPortalServices.openSelect(selectRequest(owner, ScreenDomainId.System)) assertFalse(DomainPortalServices.applicationSelectEngine.isOpenFor(owner)) assertTrue(DomainPortalServices.systemSelectEngine.isOpenFor(owner)) @@ -713,19 +713,19 @@ class LiveLayerInteractionPathTests { @Test fun `F10 application portal content is reachable through same live interaction path`() { - val applicationOverlayHost = ApplicationOverlayHost() - applicationOverlayHost.onInputFrame(1280, 720) - applicationOverlayHost.toggleFloatingWindowDemo(anchorX = 260, anchorY = 200) - applicationOverlayHost.render(ctx, 1280, 720) + val applicationPortalHost = ApplicationPortalHost() + applicationPortalHost.onInputFrame(1280, 720) + applicationPortalHost.toggleFloatingWindowDemo(anchorX = 260, anchorY = 200) + applicationPortalHost.render(ctx, 1280, 720) - val demoNode = applicationOverlayHost.floatingWindowPortal.debugNode() + val demoNode = applicationPortalHost.floatingWindowPortal.debugNode() val buttonRect = demoNode.buttonRect() assertNotNull(buttonRect) val fixture = - LiveLayerInputFixture( + DomainSurfaceInputFixture( debugHandler = { _, _, _ -> false }, - systemOverlayHandler = { _, _, _ -> false }, - applicationOverlayHandler = { x, y, button -> applicationOverlayHost.handleMouseDown(x, y, button) }, + systemPortalHandler = { _, _, _ -> false }, + applicationPortalHandler = { x, y, button -> applicationPortalHost.handleMouseDown(x, y, button) }, ) var appRootReceived = false val consumedBy = @@ -740,19 +740,19 @@ class LiveLayerInteractionPathTests { @Test fun `F10 application portal body blocks app-root fallthrough`() { - val applicationOverlayHost = ApplicationOverlayHost() - applicationOverlayHost.onInputFrame(1280, 720) - applicationOverlayHost.toggleFloatingWindowDemo(anchorX = 260, anchorY = 200) - applicationOverlayHost.render(ctx, 1280, 720) + val applicationPortalHost = ApplicationPortalHost() + applicationPortalHost.onInputFrame(1280, 720) + applicationPortalHost.toggleFloatingWindowDemo(anchorX = 260, anchorY = 200) + applicationPortalHost.render(ctx, 1280, 720) - val demoNode = applicationOverlayHost.floatingWindowPortal.debugNode() + val demoNode = applicationPortalHost.floatingWindowPortal.debugNode() val bodyRect = demoNode.bodyRect() assertNotNull(bodyRect) val fixture = - LiveLayerInputFixture( + DomainSurfaceInputFixture( debugHandler = { _, _, _ -> false }, - systemOverlayHandler = { _, _, _ -> false }, - applicationOverlayHandler = { x, y, button -> applicationOverlayHost.handleMouseDown(x, y, button) }, + systemPortalHandler = { _, _, _ -> false }, + applicationPortalHandler = { x, y, button -> applicationPortalHost.handleMouseDown(x, y, button) }, ) var appRootReceived = false val consumedBy = @@ -777,7 +777,7 @@ class LiveLayerInteractionPathTests { private fun selectRequest( owner: Any, - ownerScope: OverlayOwnerScope, + ownerDomain: ScreenDomainId, entries: List = listOf( SelectEntry.Option("a", labelProvider = { "Alpha" }), @@ -803,23 +803,23 @@ class LiveLayerInteractionPathTests { anchorRect = Rect(24, 24, 100, 18), closeOnSelect = true, onSelect = onSelect, - ownerScope = ownerScope, + ownerDomain = ownerDomain, ) } - private fun colorPickerRequest(owner: Any, ownerScope: OverlayOwnerScope, onCommit: ((RgbaColor) -> Unit)? = null): ColorPickerPopupRequest = + private fun colorPickerRequest(owner: Any, ownerDomain: ScreenDomainId, onCommit: ((RgbaColor) -> Unit)? = null): ColorPickerPopupRequest = ColorPickerPopupRequest( owner = owner, - ownerScope = ownerScope, + ownerDomain = ownerDomain, anchorRect = Rect(32, 36, 24, 18), state = ColorPickerState(color = RgbaColor.WHITE, closeOnSelect = false), onCommit = onCommit, ) - private class LiveLayerInputFixture( + private class DomainSurfaceInputFixture( private val debugHandler: (Int, Int, MouseButton) -> Boolean, - private val systemOverlayHandler: (Int, Int, MouseButton) -> Boolean, - private val applicationOverlayHandler: (Int, Int, MouseButton) -> Boolean, + private val systemPortalHandler: (Int, Int, MouseButton) -> Boolean, + private val applicationPortalHandler: (Int, Int, MouseButton) -> Boolean, private val debugPortalHandler: (Int, Int, MouseButton) -> Boolean = { _, _, _ -> false }, private val systemRootHandler: (Int, Int, MouseButton) -> Boolean = { _, _, _ -> false }, ) { @@ -834,9 +834,9 @@ class LiveLayerInteractionPathTests { when (surface) { ScreenDomainSurfaces.DebugPortal -> debugPortalHandler(mouseX, mouseY, button) ScreenDomainSurfaces.DebugRoot -> debugHandler(mouseX, mouseY, button) - ScreenDomainSurfaces.SystemPortal -> systemOverlayHandler(mouseX, mouseY, button) + ScreenDomainSurfaces.SystemPortal -> systemPortalHandler(mouseX, mouseY, button) ScreenDomainSurfaces.SystemRoot -> systemRootHandler(mouseX, mouseY, button) - ScreenDomainSurfaces.ApplicationPortal -> applicationOverlayHandler(mouseX, mouseY, button) + ScreenDomainSurfaces.ApplicationPortal -> applicationPortalHandler(mouseX, mouseY, button) ScreenDomainSurfaces.ApplicationRoot -> applicationRootHandler() else -> false } @@ -844,8 +844,8 @@ class LiveLayerInteractionPathTests { ) } - private fun applicationOverlayRoot(host: ApplicationOverlayHost): DOMNode { - val field = ApplicationOverlayHost::class.java.getDeclaredField("rootNode") + private fun applicationPortalRoot(host: ApplicationPortalHost): DOMNode { + val field = ApplicationPortalHost::class.java.getDeclaredField("rootNode") field.isAccessible = true return field.get(host) as DOMNode } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayGeometryIntegrationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/PortalGeometryIntegrationTests.kt similarity index 91% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayGeometryIntegrationTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/PortalGeometryIntegrationTests.kt index 87098b0..26ae403 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/OverlayGeometryIntegrationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/PortalGeometryIntegrationTests.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay +package org.dreamfinity.dsgl.core.portal import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupEngine import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupRequest @@ -7,7 +7,7 @@ import org.dreamfinity.dsgl.core.colorpicker.RgbaColor import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.inspector.InspectorController -import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayHost +import org.dreamfinity.dsgl.core.portal.system.SystemPortalHost import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.StyleEngine import kotlin.test.Test @@ -16,7 +16,7 @@ import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue -class OverlayGeometryIntegrationTests { +class PortalGeometryIntegrationTests { private val ctx = object : UiMeasureContext { override val fontHeight: Int = 9 @@ -27,9 +27,9 @@ class OverlayGeometryIntegrationTests { } @Test - fun `system overlay root uses full game viewport bounds in live host path`() { + fun `system portal root uses full game viewport bounds in live host path`() { StyleEngine.setViewportSize(120, 90) - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) host.onInputFrame(1280, 720) host.render(ctx, 1280, 720) @@ -38,9 +38,9 @@ class OverlayGeometryIntegrationTests { } @Test - fun `application overlay root uses full top-level window viewport bounds`() { + fun `application portal root uses full top-level window viewport bounds`() { StyleEngine.setViewportSize(160, 120) - val host = ApplicationOverlayHost() + val host = ApplicationPortalHost() host.render(ctx, 1024, 768) @@ -117,7 +117,7 @@ class OverlayGeometryIntegrationTests { } @Test - fun `reopen remembered popup position remains clamped for current overlay viewport`() { + fun `reopen remembered popup position remains clamped for current portal viewport`() { val engine = ColorPickerPopupEngine() val owner = "remember-owner" diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContractsTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/PortalHostContractsTests.kt similarity index 98% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContractsTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/PortalHostContractsTests.kt index bb7fcf5..a459c34 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/PortalHostContractsTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/PortalHostContractsTests.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay +package org.dreamfinity.dsgl.core.portal import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent @@ -11,8 +11,8 @@ import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.event.MouseDownEvent import org.dreamfinity.dsgl.core.inspector.InspectorController -import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter -import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayHost +import org.dreamfinity.dsgl.core.portal.input.SurfaceDomInputRouter +import org.dreamfinity.dsgl.core.portal.system.SystemPortalHost import org.dreamfinity.dsgl.core.render.RenderCommand import kotlin.test.AfterTest import kotlin.test.Test @@ -176,11 +176,11 @@ class PortalHostContractsTests { fun `domain surface adapter maps current application and system hosts to portal surfaces`() { assertEquals( ScreenDomainSurfaces.ApplicationPortal, - DomainSurfacePortalHostAdapter(ApplicationOverlayHost()).surface, + DomainSurfacePortalHostAdapter(ApplicationPortalHost()).surface, ) assertEquals( ScreenDomainSurfaces.SystemPortal, - DomainSurfacePortalHostAdapter(SystemOverlayHost(InspectorController())).surface, + DomainSurfacePortalHostAdapter(SystemPortalHost(InspectorController())).surface, ) } @@ -481,7 +481,7 @@ class PortalHostContractsTests { portalRoot.addEventListener(Events.MOUSEDOWN) { calls += "portal-root" } portalParent.addEventListener(Events.MOUSEDOWN) { calls += "portal-parent" } } - val portalRouter = LayerDomInputRouter { portalRoot } + val portalRouter = SurfaceDomInputRouter { portalRoot } assertTrue(portalRouter.handleMouseDown(45, 45, MouseButton.LEFT)) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/ScreenDomainContractsTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/ScreenDomainContractsTests.kt similarity index 90% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/ScreenDomainContractsTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/ScreenDomainContractsTests.kt index 5e3a7f6..2ec27ba 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/ScreenDomainContractsTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/ScreenDomainContractsTests.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay +package org.dreamfinity.dsgl.core.portal import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupRequest import org.dreamfinity.dsgl.core.colorpicker.ColorPickerState @@ -59,37 +59,37 @@ class ScreenDomainContractsTests { fun `owner scope resolves to compatible portal domain surfaces`() { assertEquals( ScreenDomainSurfaces.ApplicationPortal, - ScreenDomainSurfaces.portalSurfaceForOwner(OverlayOwnerScope.Application), + ScreenDomainSurfaces.portalSurfaceForDomain(ScreenDomainId.Application), ) assertEquals( ScreenDomainSurfaces.SystemPortal, - ScreenDomainSurfaces.portalSurfaceForOwner(OverlayOwnerScope.System), + ScreenDomainSurfaces.portalSurfaceForDomain(ScreenDomainId.System), ) } @Test fun `transient ownership uses owner scope and not cursor position`() { val appAtA = - ScreenDomainSurfaces.portalSurfaceForOwner( - ownerScope = OverlayOwnerScope.Application, + ScreenDomainSurfaces.portalSurfaceForDomain( + ownerDomain = ScreenDomainId.Application, cursorX = 10, cursorY = 20, ) val appAtB = - ScreenDomainSurfaces.portalSurfaceForOwner( - ownerScope = OverlayOwnerScope.Application, + ScreenDomainSurfaces.portalSurfaceForDomain( + ownerDomain = ScreenDomainId.Application, cursorX = 800, cursorY = 640, ) val systemAtA = - ScreenDomainSurfaces.portalSurfaceForOwner( - ownerScope = OverlayOwnerScope.System, + ScreenDomainSurfaces.portalSurfaceForDomain( + ownerDomain = ScreenDomainId.System, cursorX = 10, cursorY = 20, ) val systemAtB = - ScreenDomainSurfaces.portalSurfaceForOwner( - ownerScope = OverlayOwnerScope.System, + ScreenDomainSurfaces.portalSurfaceForDomain( + ownerDomain = ScreenDomainId.System, cursorX = 800, cursorY = 640, ) @@ -231,8 +231,8 @@ class ScreenDomainContractsTests { anchorRect = Rect(10, 12, 20, 18), state = ColorPickerState(color = RgbaColor.WHITE), ) - assertEquals(OverlayOwnerScope.Application, request.ownerScope) - assertEquals(ScreenDomainSurfaces.ApplicationPortal, ColorPickerPopupOverlayOwnership.resolveSurface(request)) + assertEquals(ScreenDomainId.Application, request.ownerDomain) + assertEquals(ScreenDomainSurfaces.ApplicationPortal, ColorPickerPopupPortalOwnership.resolveSurface(request)) } @Test @@ -240,10 +240,10 @@ class ScreenDomainContractsTests { val request = ColorPickerPopupRequest( owner = "owner", - ownerScope = OverlayOwnerScope.System, + ownerDomain = ScreenDomainId.System, anchorRect = Rect(10, 12, 20, 18), state = ColorPickerState(color = RgbaColor.WHITE), ) - assertEquals(ScreenDomainSurfaces.SystemPortal, ColorPickerPopupOverlayOwnership.resolveSurface(request)) + assertEquals(ScreenDomainSurfaces.SystemPortal, ColorPickerPopupPortalOwnership.resolveSurface(request)) } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/input/PointerCaptureSessionTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/input/PointerCaptureSessionTests.kt similarity index 98% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/input/PointerCaptureSessionTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/input/PointerCaptureSessionTests.kt index 63e66d1..c23fc28 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/input/PointerCaptureSessionTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/input/PointerCaptureSessionTests.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay.input +package org.dreamfinity.dsgl.core.portal.input import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouterTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/input/SurfaceDomInputRouterTests.kt similarity index 90% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouterTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/input/SurfaceDomInputRouterTests.kt index 3de801d..b146fd1 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/input/LayerDomInputRouterTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/input/SurfaceDomInputRouterTests.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay.input +package org.dreamfinity.dsgl.core.portal.input import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ButtonNode @@ -23,7 +23,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -class LayerDomInputRouterTests { +class SurfaceDomInputRouterTests { private val clipboard = RecordingClipboardAccess() private val ctx = object : UiMeasureContext { @@ -45,9 +45,9 @@ class LayerDomInputRouterTests { @Test fun `text input editing semantics stay consistent across all layers`() { ClipboardBridge.install(clipboard) - listOf("app-dom", "app-overlay", "system-overlay").forEach { layer -> + listOf("app-dom", "app-portal", "system-portal").forEach { layer -> clipboard.value = "" - val (root, router) = createLayerRouter(layer) + val (root, router) = createSurfaceRouter(layer) val input = TextInputNode(text = "abcdef", key = "$layer-input").apply { bounds = Rect(20, 20, 180, 24) @@ -92,8 +92,8 @@ class LayerDomInputRouterTests { @Test fun `dropdown-like topmost option wins and blocks click-through across layers`() { - listOf("app-dom", "app-overlay", "system-overlay").forEach { layer -> - val (root, router) = createLayerRouter(layer) + listOf("app-dom", "app-portal", "system-portal").forEach { layer -> + val (root, router) = createSurfaceRouter(layer) var underClicks = 0 var topClicks = 0 @@ -122,8 +122,8 @@ class LayerDomInputRouterTests { @Test fun `keyboard dispatch follows focused node root membership`() { - val (rootA, routerA) = createLayerRouter("layer-a") - val (rootB, routerB) = createLayerRouter("layer-b") + val (rootA, routerA) = createSurfaceRouter("layer-a") + val (rootB, routerB) = createSurfaceRouter("layer-b") val inputA = TextInputNode(text = "a", key = "a-input").apply { @@ -145,8 +145,8 @@ class LayerDomInputRouterTests { @Test fun `keyboard key up follows focused node root membership`() { - val (rootA, routerA) = createLayerRouter("layer-a") - val (rootB, routerB) = createLayerRouter("layer-b") + val (rootA, routerA) = createSurfaceRouter("layer-a") + val (rootB, routerB) = createSurfaceRouter("layer-b") val keyUps = mutableListOf() val parentA = @@ -177,7 +177,7 @@ class LayerDomInputRouterTests { @Test fun `pointer drag capture is generic for header and thumb style controls`() { listOf("header-drag", "thumb-drag").forEach { key -> - val (root, router) = createLayerRouter(key) + val (root, router) = createSurfaceRouter(key) var dragEvents = 0 val dragNode = ContainerNode(key = "$key-node").apply { @@ -195,7 +195,7 @@ class LayerDomInputRouterTests { @Test fun `release after drag does not synthesize click on hovered target`() { - val (root, router) = createLayerRouter("drag-release") + val (root, router) = createSurfaceRouter("drag-release") var buttonClicks = 0 val dragSurface = @@ -220,7 +220,7 @@ class LayerDomInputRouterTests { @Test fun `unkeyed drag capture remains active across pointer move`() { - val (root, router) = createLayerRouter("unkeyed-drag") + val (root, router) = createSurfaceRouter("unkeyed-drag") var dragEvents = 0 val dragNode = ContainerNode().apply { @@ -237,7 +237,7 @@ class LayerDomInputRouterTests { @Test fun `range input drag updates value when pointer leaves bounds`() { - val (root, router) = createLayerRouter("range-drag") + val (root, router) = createSurfaceRouter("range-drag") val range = RangeInputNode(value = 0L, min = 0L, max = 100L, key = null) range.applyParent(root) range.render(ctx, 20, 20, 120, 12) @@ -250,7 +250,7 @@ class LayerDomInputRouterTests { @Test fun `range input drag survives focus changing to captured control`() { - val (root, router) = createLayerRouter("range-drag-focus") + val (root, router) = createSurfaceRouter("range-drag-focus") val previousFocus = TextInputNode(text = "before", key = "range-drag-focus-before").apply { bounds = Rect(4, 4, 80, 20) @@ -270,8 +270,8 @@ class LayerDomInputRouterTests { @Test fun `range input drag is not cancelled by unrelated router clear`() { - val (rootA, routerA) = createLayerRouter("range-drag-owner") - val (_, routerB) = createLayerRouter("unrelated-router") + val (rootA, routerA) = createSurfaceRouter("range-drag-owner") + val (_, routerB) = createSurfaceRouter("unrelated-router") val range = RangeInputNode(value = 0L, min = 0L, max = 100L, key = "owned-range") range.applyParent(rootA) range.render(ctx, 20, 20, 120, 12) @@ -287,8 +287,8 @@ class LayerDomInputRouterTests { @Test fun `text selection drag is not cancelled by unrelated router clear`() { ClipboardBridge.install(clipboard) - val (rootA, routerA) = createLayerRouter("text-drag-owner") - val (_, routerB) = createLayerRouter("unrelated-text-router") + val (rootA, routerA) = createSurfaceRouter("text-drag-owner") + val (_, routerB) = createSurfaceRouter("unrelated-text-router") val input = TextInputNode(text = "abcdef", key = "owned-text").apply { bounds = Rect(20, 20, 160, 20) @@ -307,7 +307,7 @@ class LayerDomInputRouterTests { @Test fun `range input drag survives unkeyed rerender replacement`() { - val (root, router) = createLayerRouter("range-rerender") + val (root, router) = createSurfaceRouter("range-rerender") var model = 0L lateinit var mount: (Long) -> RangeInputNode @@ -333,7 +333,7 @@ class LayerDomInputRouterTests { @Test fun `mouse up stays consumed after press when pointer is released outside targets`() { - val (root, router) = createLayerRouter("outside-release") + val (root, router) = createSurfaceRouter("outside-release") var buttonClicks = 0 val button = @@ -351,7 +351,7 @@ class LayerDomInputRouterTests { @Test fun `wheel over non-handling interactive child bubbles to ancestor wheel handler`() { - val (root, router) = createLayerRouter("wheel-bubble") + val (root, router) = createSurfaceRouter("wheel-bubble") var wheelEvents = 0 val scrollHost = @@ -377,7 +377,7 @@ class LayerDomInputRouterTests { @Test fun `focused textarea does not steal wheel from hovered control`() { - val (root, router) = createLayerRouter("wheel-focused-textarea") + val (root, router) = createSurfaceRouter("wheel-focused-textarea") var hostWheelEvents = 0 val scrollHost = @@ -411,7 +411,7 @@ class LayerDomInputRouterTests { @Test fun `wheel without cancellation is not consumed`() { - val (root, router) = createLayerRouter("wheel-unhandled") + val (root, router) = createSurfaceRouter("wheel-unhandled") val input = TextInputNode(text = "value", key = "wheel-unhandled-input").apply { bounds = Rect(24, 24, 150, 24) @@ -424,8 +424,8 @@ class LayerDomInputRouterTests { @Test fun `wheel axis semantics stay consistent across layers`() { - listOf("app-dom", "app-overlay", "system-overlay").forEach { layer -> - val (root, router) = createLayerRouter("wheel-axis-$layer") + listOf("app-dom", "app-portal", "system-portal").forEach { layer -> + val (root, router) = createSurfaceRouter("wheel-axis-$layer") val viewport = ContainerNode(key = "$layer-scroll").apply { bounds = Rect(20, 20, 150, 70) @@ -466,12 +466,12 @@ class LayerDomInputRouterTests { } } - private fun createLayerRouter(key: String): Pair { + private fun createSurfaceRouter(key: String): Pair { val root = ContainerNode(key = "$key-root").apply { bounds = Rect(0, 0, 320, 200) } - return root to LayerDomInputRouter { root } + return root to SurfaceDomInputRouter { root } } private class RecordingClipboardAccess : ClipboardAccess { diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanelTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/panel/FloatingPanelTests.kt similarity index 89% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanelTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/panel/FloatingPanelTests.kt index 8bdd57a..adc4968 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/panel/OverlayPanelTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/panel/FloatingPanelTests.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay.panel +package org.dreamfinity.dsgl.core.portal.panel import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.layout.Rect @@ -12,7 +12,7 @@ import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertTrue -class OverlayPanelTests { +class FloatingPanelTests { private val ctx = object : UiMeasureContext { override val fontHeight: Int = 9 @@ -25,19 +25,19 @@ class OverlayPanelTests { @Test fun `panel uses native node path and does not expose legacy append commands api`() { assertTrue( - OverlayPanel::class.java.methods + FloatingPanel::class.java.methods .none { it.name == "appendCommands" }, ) val panelState = - OverlayPanelState().apply { + FloatingPanelState().apply { updateFromRect(Rect(30, 40, 240, 180)) } val panel = - OverlayPanel( + FloatingPanel( ownerId = "demo-owner", panelState = panelState, - dragSession = OverlayPanelDragSession(), + dragSession = FloatingPanelDragSession(), ) panel.configure(title = "Demo", draggable = true) panel.setBodyContent(FillNode("body")) @@ -55,12 +55,12 @@ class OverlayPanelTests { @Test fun `panel drag keeps persistent drag session and updates panel state`() { val panelState = - OverlayPanelState().apply { + FloatingPanelState().apply { updateFromRect(Rect(60, 70, 260, 180)) } - val dragSession = OverlayPanelDragSession() + val dragSession = FloatingPanelDragSession() val panel = - OverlayPanel( + FloatingPanel( ownerId = "drag-owner", panelState = panelState, dragSession = dragSession, @@ -109,14 +109,14 @@ class OverlayPanelTests { @Test fun `panel close button invokes close callback`() { val panelState = - OverlayPanelState().apply { + FloatingPanelState().apply { updateFromRect(Rect(12, 20, 220, 140)) } val panel = - OverlayPanel( + FloatingPanel( ownerId = Any(), panelState = panelState, - dragSession = OverlayPanelDragSession(), + dragSession = FloatingPanelDragSession(), ) var closed = 0 panel.configure(title = "Closable", draggable = true, onClose = { closed += 1 }) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/InspectorDragScrollDomMigrationTests.kt similarity index 88% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/InspectorDragScrollDomMigrationTests.kt index f117597..ae29a72 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDragScrollDomMigrationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/InspectorDragScrollDomMigrationTests.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay.system +package org.dreamfinity.dsgl.core.portal.system import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent @@ -12,8 +12,8 @@ import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.inspector.InspectorEditorKind import org.dreamfinity.dsgl.core.inspector.InspectorStyleEditorRowSnapshot -import org.dreamfinity.dsgl.core.overlay.DomainPortalServices -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope +import org.dreamfinity.dsgl.core.portal.DomainPortalServices +import org.dreamfinity.dsgl.core.portal.ScreenDomainId import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleProperty @@ -43,7 +43,7 @@ class InspectorDragScrollDomMigrationTests { fun `inspector panel drag is dom-first and controller drag authority stays demoted`() { val fixture = openInspectorAndSelectTarget(withManyChildren = true) - val before = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") + val before = fixture.inspector.floatingPanelRect() ?: error("expected panel rect") val downX = before.x + 18 val downY = before.y + 14 val moveX = downX + 90 @@ -56,14 +56,14 @@ class InspectorDragScrollDomMigrationTests { assertFalse(fixture.inspector.isPointerCaptured) assertTrue( fixture.host - .debugEntryState(SystemOverlayEntryId.Inspector) + .debugEntryState(SystemPortalEntryId.Inspector) ?.dragSession ?.active == true, ) assertTrue(fixture.host.handleMouseMove(moveX, moveY)) syncAndRender(fixture, moveX, moveY) - val moved = fixture.inspector.overlayPanelRect() ?: error("expected moved panel rect") + val moved = fixture.inspector.floatingPanelRect() ?: error("expected moved panel rect") assertTrue(moved.x != before.x || moved.y != before.y) assertTrue(fixture.host.handleMouseUp(moveX, moveY, MouseButton.LEFT)) @@ -73,7 +73,7 @@ class InspectorDragScrollDomMigrationTests { assertFalse(fixture.inspector.isPointerCaptured) assertFalse( fixture.host - .debugEntryState(SystemOverlayEntryId.Inspector) + .debugEntryState(SystemPortalEntryId.Inspector) ?.dragSession ?.active == true, ) @@ -83,7 +83,7 @@ class InspectorDragScrollDomMigrationTests { fun `inspector panel drag stays monotonic when sync cursor lags behind dom drag updates`() { val fixture = openInspectorAndSelectTarget(withManyChildren = true) - val before = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") + val before = fixture.inspector.floatingPanelRect() ?: error("expected panel rect") val downX = before.x + 18 val downY = before.y + 14 val dragX = downX + 92 @@ -93,14 +93,14 @@ class InspectorDragScrollDomMigrationTests { syncAndRender(fixture, downX, downY) assertTrue(fixture.host.handleMouseMove(dragX, dragY)) - val afterDomDrag = fixture.inspector.overlayPanelRect() ?: error("expected moved panel rect") + val afterDomDrag = fixture.inspector.floatingPanelRect() ?: error("expected moved panel rect") assertTrue( afterDomDrag.x > before.x || afterDomDrag.y > before.y, "expected drag move to advance panel: before=$before afterDomDrag=$afterDomDrag", ) syncAndRender(fixture, downX, downY) - val afterStaleSync = fixture.inspector.overlayPanelRect() ?: error("expected panel rect after stale sync") + val afterStaleSync = fixture.inspector.floatingPanelRect() ?: error("expected panel rect after stale sync") if (afterDomDrag.x > before.x) { assertTrue( @@ -117,7 +117,7 @@ class InspectorDragScrollDomMigrationTests { assertTrue(fixture.host.handleMouseMove(dragX + 28, dragY + 16)) syncAndRender(fixture, dragX + 28, dragY + 16) - val afterNextMove = fixture.inspector.overlayPanelRect() ?: error("expected panel rect after next drag move") + val afterNextMove = fixture.inspector.floatingPanelRect() ?: error("expected panel rect after next drag move") assertTrue( afterNextMove.x >= afterStaleSync.x && afterNextMove.y >= afterStaleSync.y, "expected monotonic drag progression across sync/render cycle: stale=$afterStaleSync next=$afterNextMove", @@ -127,7 +127,7 @@ class InspectorDragScrollDomMigrationTests { syncAndRender(fixture, dragX + 28, dragY + 16) assertFalse( fixture.host - .debugEntryState(SystemOverlayEntryId.Inspector) + .debugEntryState(SystemPortalEntryId.Inspector) ?.dragSession ?.active == true, ) @@ -138,7 +138,7 @@ class InspectorDragScrollDomMigrationTests { val fixture = openInspectorAndSelectTarget(withManyChildren = true) setViewport(fixture, 420, 280) - val contentRect = fixture.inspector.overlayContentRect() + val contentRect = fixture.inspector.portalContentRect() val wheelX = contentRect.x + 4 val wheelY = contentRect.y + 10 val before = fixture.inspector.panelScrollOffsetY @@ -152,7 +152,7 @@ class InspectorDragScrollDomMigrationTests { assertFalse(fixture.inspector.isDraggingPanel) assertFalse( fixture.host - .debugEntryState(SystemOverlayEntryId.Inspector) + .debugEntryState(SystemPortalEntryId.Inspector) ?.dragSession ?.active == true, ) @@ -164,7 +164,7 @@ class InspectorDragScrollDomMigrationTests { setViewport(fixture, 420, 280) scrollInspectorBodyDown(fixture, steps = 2) - val thumb = fixture.inspector.overlayScrollbarThumbRect() + val thumb = fixture.inspector.portalScrollbarThumbRect() assertTrue(thumb.width > 0 && thumb.height > 0) val dragX = thumb.x + thumb.width / 2 @@ -178,7 +178,7 @@ class InspectorDragScrollDomMigrationTests { assertFalse(fixture.inspector.isDraggingPanel) assertFalse( fixture.host - .debugEntryState(SystemOverlayEntryId.Inspector) + .debugEntryState(SystemPortalEntryId.Inspector) ?.dragSession ?.active == true, ) @@ -192,7 +192,7 @@ class InspectorDragScrollDomMigrationTests { syncAndRender(fixture, dragX, startY + 40) assertFalse( fixture.host - .debugEntryState(SystemOverlayEntryId.Inspector) + .debugEntryState(SystemPortalEntryId.Inspector) ?.dragSession ?.active == true, ) @@ -204,7 +204,7 @@ class InspectorDragScrollDomMigrationTests { setViewport(fixture, 420, 280) scrollInspectorBodyDown(fixture, steps = 3) - val thumb = fixture.inspector.overlayScrollbarThumbRect() + val thumb = fixture.inspector.portalScrollbarThumbRect() assertTrue(thumb.width > 0 && thumb.height > 0) val dragX = thumb.x + thumb.width / 2 @@ -213,7 +213,7 @@ class InspectorDragScrollDomMigrationTests { syncAndRender(fixture, dragX, startY) assertFalse( fixture.host - .debugEntryState(SystemOverlayEntryId.Inspector) + .debugEntryState(SystemPortalEntryId.Inspector) ?.dragSession ?.active == true, ) @@ -225,7 +225,7 @@ class InspectorDragScrollDomMigrationTests { syncAndRender(fixture, dragX, startY + 22) assertFalse( fixture.host - .debugEntryState(SystemOverlayEntryId.Inspector) + .debugEntryState(SystemPortalEntryId.Inspector) ?.dragSession ?.active == true, ) @@ -239,7 +239,7 @@ class InspectorDragScrollDomMigrationTests { syncAndRender(fixture, dragX, startY + 54) assertFalse( fixture.host - .debugEntryState(SystemOverlayEntryId.Inspector) + .debugEntryState(SystemPortalEntryId.Inspector) ?.dragSession ?.active == true, ) @@ -250,7 +250,7 @@ class InspectorDragScrollDomMigrationTests { val fixture = openInspectorAndSelectTarget(withManyChildren = true) setViewport(fixture, 420, 280) - val panelRect = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") + val panelRect = fixture.inspector.floatingPanelRect() ?: error("expected panel rect") val downX = panelRect.x + 14 val downY = panelRect.y + 12 assertTrue(fixture.host.handleMouseDown(downX, downY, MouseButton.LEFT)) @@ -259,7 +259,7 @@ class InspectorDragScrollDomMigrationTests { assertFalse(fixture.inspector.isPointerCaptured) assertTrue( fixture.host - .debugEntryState(SystemOverlayEntryId.Inspector) + .debugEntryState(SystemPortalEntryId.Inspector) ?.dragSession ?.active == true, ) @@ -291,7 +291,7 @@ class InspectorDragScrollDomMigrationTests { val row = findVisibleSelectRow(fixture) val rowIndex = fixture.inspector - .overlayStyleEditorRows() + .portalStyleEditorRows() .indexOfFirst { it.property == row.property } .takeIf { it >= 0 } ?: error("expected style row index for ${row.property.key}") val ownerKey = "dsgl-system-inspector-editor-select-$rowIndex" @@ -320,13 +320,13 @@ class InspectorDragScrollDomMigrationTests { syncAndRender(fixture, anchor.x + 1, anchor.y + 1) assertTrue(fixture.host.isSystemColorPickerOpen()) - assertEquals(OverlayOwnerScope.System, fixture.host.debugSystemColorPickerPopupOwnerScope()) - assertNotNull(fixture.host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup)) + assertEquals(ScreenDomainId.System, fixture.host.debugSystemColorPickerPopupOwnerDomain()) + assertNotNull(fixture.host.debugEntryNode(SystemPortalEntryId.ColorPickerPopup)) } private fun openInspectorAndSelectTarget(withManyChildren: Boolean): Fixture { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot(withManyChildren) @@ -378,7 +378,7 @@ class InspectorDragScrollDomMigrationTests { } private fun scrollInspectorBodyDown(fixture: Fixture, steps: Int) { - val contentRect = fixture.inspector.overlayContentRect() + val contentRect = fixture.inspector.portalContentRect() val wheelX = contentRect.x + 4 val wheelY = contentRect.y + 10 repeat(steps) { @@ -390,10 +390,10 @@ class InspectorDragScrollDomMigrationTests { private fun findVisibleSelectRow(fixture: Fixture): InspectorStyleEditorRowSnapshot { repeat(120) { val rows = - fixture.inspector.overlayStyleEditorRows().filter { row -> + fixture.inspector.portalStyleEditorRows().filter { row -> row.editorKind == InspectorEditorKind.EnumSelect || row.editorKind == InspectorEditorKind.FontSelect } - val contentRect = fixture.inspector.overlayContentRect() + val contentRect = fixture.inspector.portalContentRect() val bodyScrollY = fixture.inspector.panelScrollOffsetY rows .firstOrNull { row -> @@ -444,9 +444,9 @@ class InspectorDragScrollDomMigrationTests { private fun findVisibleInputNode(fixture: Fixture, propertyKey: String): TextInputNode { val inspectorNode = - fixture.host.debugEntryNode(SystemOverlayEntryId.Inspector) + fixture.host.debugEntryNode(SystemPortalEntryId.Inspector) ?: error("inspector entry missing") - val contentRect = fixture.inspector.overlayContentRect() + val contentRect = fixture.inspector.portalContentRect() val candidates = collectNodes(inspectorNode) .filterIsInstance() @@ -492,7 +492,7 @@ class InspectorDragScrollDomMigrationTests { private data class Fixture( val inspector: InspectorController, - val host: SystemOverlayHost, + val host: SystemPortalHost, val root: ContainerNode, var revision: Long, var viewportWidth: Int, diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/InspectorDropdownCorrectiveTests.kt similarity index 93% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/InspectorDropdownCorrectiveTests.kt index f0d4a93..460c8f3 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorDropdownCorrectiveTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/InspectorDropdownCorrectiveTests.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay.system +package org.dreamfinity.dsgl.core.portal.system import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent @@ -11,8 +11,8 @@ import org.dreamfinity.dsgl.core.event.KeyModifiers import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.inspector.InspectorEditorKind -import org.dreamfinity.dsgl.core.overlay.DomainPortalServices -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope +import org.dreamfinity.dsgl.core.portal.DomainPortalServices +import org.dreamfinity.dsgl.core.portal.ScreenDomainId import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleProperty @@ -46,7 +46,7 @@ class InspectorDropdownCorrectiveTests { val (trigger, ownerKey) = openInspectorSelectDropdown(fixture, requireScrollable = false) val dropdown = selectPanelRect(ownerKey, fixture) - val panelRect = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") + val panelRect = fixture.inspector.floatingPanelRect() ?: error("expected panel rect") val outsideX = (panelRect.x - 12).coerceAtLeast(1) val outsideY = (panelRect.y - 12).coerceAtLeast(1) assertFalse(dropdown.contains(outsideX, outsideY)) @@ -137,13 +137,13 @@ class InspectorDropdownCorrectiveTests { syncAndRender(fixture, anchor.x + 1, anchor.y + 1) assertTrue(fixture.host.isSystemColorPickerOpen()) - assertEquals(OverlayOwnerScope.System, fixture.host.debugSystemColorPickerPopupOwnerScope()) - assertNotNull(fixture.host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup)) + assertEquals(ScreenDomainId.System, fixture.host.debugSystemColorPickerPopupOwnerDomain()) + assertNotNull(fixture.host.debugEntryNode(SystemPortalEntryId.ColorPickerPopup)) } private fun openInspectorAndSelectTarget(withManyChildren: Boolean): Fixture { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot(withManyChildren) @@ -195,7 +195,7 @@ class InspectorDropdownCorrectiveTests { } private fun scrollInspectorBodyDown(fixture: Fixture, steps: Int) { - val contentRect = fixture.inspector.overlayContentRect() + val contentRect = fixture.inspector.portalContentRect() val wheelX = contentRect.x + 4 val wheelY = contentRect.y + 10 repeat(steps) { @@ -205,7 +205,7 @@ class InspectorDropdownCorrectiveTests { } private fun settleFrames(fixture: Fixture, steps: Int) { - val contentRect = fixture.inspector.overlayContentRect() + val contentRect = fixture.inspector.portalContentRect() val cursorX = contentRect.x + 4 val cursorY = contentRect.y + 10 repeat(steps) { @@ -214,10 +214,10 @@ class InspectorDropdownCorrectiveTests { } private fun openVisibleInspectorSelectDropdownWithoutBodyScroll(fixture: Fixture): Pair { - val contentRect = fixture.inspector.overlayContentRect() + val contentRect = fixture.inspector.portalContentRect() val bodyScrollY = fixture.inspector.panelScrollOffsetY val row = - fixture.inspector.overlayStyleEditorRows().firstOrNull { row -> + fixture.inspector.portalStyleEditorRows().firstOrNull { row -> if (row.editorKind != InspectorEditorKind.EnumSelect && row.editorKind != InspectorEditorKind.FontSelect ) { @@ -244,7 +244,7 @@ class InspectorDropdownCorrectiveTests { ) val rowIndex = fixture.inspector - .overlayStyleEditorRows() + .portalStyleEditorRows() .indexOfFirst { it.property == row.property } .takeIf { it >= 0 } ?: error("expected style row index for ${row.property.key}") val ownerKey = "dsgl-system-inspector-editor-select-$rowIndex" @@ -260,10 +260,10 @@ class InspectorDropdownCorrectiveTests { private fun openInspectorSelectDropdown(fixture: Fixture, requireScrollable: Boolean): Pair { repeat(120) { - val contentRect = fixture.inspector.overlayContentRect() + val contentRect = fixture.inspector.portalContentRect() val bodyScrollY = fixture.inspector.panelScrollOffsetY val visibleSelectRows = - fixture.inspector.overlayStyleEditorRows().filter { row -> + fixture.inspector.portalStyleEditorRows().filter { row -> if (row.editorKind != InspectorEditorKind.EnumSelect && row.editorKind != InspectorEditorKind.FontSelect ) { @@ -291,7 +291,7 @@ class InspectorDropdownCorrectiveTests { ) val rowIndex = fixture.inspector - .overlayStyleEditorRows() + .portalStyleEditorRows() .indexOfFirst { it.property == row.property } .takeIf { it >= 0 } ?: return@forEach val ownerKey = "dsgl-system-inspector-editor-select-$rowIndex" @@ -378,9 +378,9 @@ class InspectorDropdownCorrectiveTests { private fun findVisibleInputNode(fixture: Fixture, propertyKey: String): TextInputNode { val inspectorNode = - fixture.host.debugEntryNode(SystemOverlayEntryId.Inspector) + fixture.host.debugEntryNode(SystemPortalEntryId.Inspector) ?: error("inspector entry missing") - val contentRect = fixture.inspector.overlayContentRect() + val contentRect = fixture.inspector.portalContentRect() val candidates = collectNodes(inspectorNode) .filterIsInstance() @@ -426,7 +426,7 @@ class InspectorDropdownCorrectiveTests { private data class Fixture( val inspector: InspectorController, - val host: SystemOverlayHost, + val host: SystemPortalHost, val root: ContainerNode, var revision: Long, var viewportWidth: Int, diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorInputPathBaselineTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/InspectorInputPathBaselineTests.kt similarity index 94% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorInputPathBaselineTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/InspectorInputPathBaselineTests.kt index 5647dc5..6f45034 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorInputPathBaselineTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/InspectorInputPathBaselineTests.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay.system +package org.dreamfinity.dsgl.core.portal.system import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent @@ -12,8 +12,8 @@ import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.inspector.InspectorEditorKind import org.dreamfinity.dsgl.core.inspector.InspectorStyleEditorRowSnapshot -import org.dreamfinity.dsgl.core.overlay.DomainPortalServices -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope +import org.dreamfinity.dsgl.core.portal.DomainPortalServices +import org.dreamfinity.dsgl.core.portal.ScreenDomainId import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleProperty @@ -51,7 +51,7 @@ class InspectorInputPathBaselineTests { assertFalse(fixture.inspector.handleOpenStyleDropdownWheel(-120)) val inspectorNode = - fixture.host.debugEntryNode(SystemOverlayEntryId.Inspector) + fixture.host.debugEntryNode(SystemPortalEntryId.Inspector) ?: error("inspector entry missing") val routeProbe = inspectorNode.javaClass.getDeclaredMethod("isDomOwnedInteractionTarget", DOMNode::class.java) routeProbe.isAccessible = true @@ -119,7 +119,7 @@ class InspectorInputPathBaselineTests { assertFalse(fixture.inspector.hasOpenStyleDropdown()) assertFalse(fixture.inspector.handleOpenStyleDropdownWheel(-120)) - val contentRect = fixture.inspector.overlayContentRect() + val contentRect = fixture.inspector.portalContentRect() val popup = selectPanelRect(ownerKey, fixture) val wheelX = popup.x + (popup.width / 2).coerceAtLeast(1) val wheelY = popup.y + (popup.height / 2).coerceAtLeast(1) @@ -165,8 +165,8 @@ class InspectorInputPathBaselineTests { syncAndRender(fixture, anchor.x + 1, anchor.y + 1) assertTrue(fixture.host.isSystemColorPickerOpen()) - assertEquals(OverlayOwnerScope.System, fixture.host.debugSystemColorPickerPopupOwnerScope()) - assertNotNull(fixture.host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup)) + assertEquals(ScreenDomainId.System, fixture.host.debugSystemColorPickerPopupOwnerDomain()) + assertNotNull(fixture.host.debugEntryNode(SystemPortalEntryId.ColorPickerPopup)) } @Test @@ -188,14 +188,14 @@ class InspectorInputPathBaselineTests { assertTrue(popup.y >= 0) assertTrue(popup.y + popup.height <= fixture.viewportHeight) - val contentRect = fixture.inspector.overlayContentRect() + val contentRect = fixture.inspector.portalContentRect() val wheelX = contentRect.x + 4 val wheelY = contentRect.y + 10 assertTrue(dispatchSystemMouseWheel(fixture, wheelX, wheelY, -120)) syncAndRender(fixture, wheelX, wheelY) assertTrue(DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey)) - val panelRect = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") + val panelRect = fixture.inspector.floatingPanelRect() ?: error("expected panel rect") val outsideX = (panelRect.x - 12).coerceAtLeast(1) val outsideY = (panelRect.y - 12).coerceAtLeast(1) assertFalse(popup.contains(outsideX, outsideY)) @@ -209,7 +209,7 @@ class InspectorInputPathBaselineTests { private fun openInspectorAndSelectTarget(withManyChildren: Boolean): Fixture { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot(withManyChildren) @@ -261,7 +261,7 @@ class InspectorInputPathBaselineTests { } private fun scrollInspectorBodyDown(fixture: Fixture, steps: Int) { - val contentRect = fixture.inspector.overlayContentRect() + val contentRect = fixture.inspector.portalContentRect() val wheelX = contentRect.x + 4 val wheelY = contentRect.y + 10 repeat(steps) { @@ -273,10 +273,10 @@ class InspectorInputPathBaselineTests { private fun findOrScrollToVisibleSelectRow(fixture: Fixture): InspectorStyleEditorRowSnapshot { repeat(120) { val rows = - fixture.inspector.overlayStyleEditorRows().filter { row -> + fixture.inspector.portalStyleEditorRows().filter { row -> row.editorKind == InspectorEditorKind.EnumSelect || row.editorKind == InspectorEditorKind.FontSelect } - val contentRect = fixture.inspector.overlayContentRect() + val contentRect = fixture.inspector.portalContentRect() val bodyScrollY = fixture.inspector.panelScrollOffsetY rows .firstOrNull { row -> @@ -309,7 +309,7 @@ class InspectorInputPathBaselineTests { private fun findRowByProperty(fixture: Fixture, property: StyleProperty): InspectorStyleEditorRowSnapshot = fixture.inspector - .overlayStyleEditorRows() + .portalStyleEditorRows() .firstOrNull { it.property == property } ?: error("expected row for $property") @@ -320,7 +320,7 @@ class InspectorInputPathBaselineTests { val triggerRect = visibleControlRect(fixture, row) val rowIndex = fixture.inspector - .overlayStyleEditorRows() + .portalStyleEditorRows() .indexOfFirst { it.property == row.property } .takeIf { it >= 0 } ?: error("expected style row index for ${row.property.key}") val ownerKey = "dsgl-system-inspector-editor-select-$rowIndex" @@ -392,9 +392,9 @@ class InspectorInputPathBaselineTests { private fun findVisibleInputNode(fixture: Fixture, propertyKey: String): TextInputNode { val inspectorNode = - fixture.host.debugEntryNode(SystemOverlayEntryId.Inspector) + fixture.host.debugEntryNode(SystemPortalEntryId.Inspector) ?: error("inspector entry missing") - val contentRect = fixture.inspector.overlayContentRect() + val contentRect = fixture.inspector.portalContentRect() val candidates = collectNodes(inspectorNode) .filterIsInstance() @@ -440,7 +440,7 @@ class InspectorInputPathBaselineTests { private data class Fixture( val inspector: InspectorController, - val host: SystemOverlayHost, + val host: SystemPortalHost, val root: ContainerNode, var revision: Long, var viewportWidth: Int, diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/InspectorPointerAlignmentTests.kt similarity index 95% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/InspectorPointerAlignmentTests.kt index 4f05214..87cb6b2 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorPointerAlignmentTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/InspectorPointerAlignmentTests.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay.system +package org.dreamfinity.dsgl.core.portal.system import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode @@ -10,7 +10,7 @@ import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.inspector.InspectorEditorKind import org.dreamfinity.dsgl.core.inspector.InspectorStyleEditorRowSnapshot -import org.dreamfinity.dsgl.core.overlay.DomainPortalServices +import org.dreamfinity.dsgl.core.portal.DomainPortalServices import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleProperty @@ -115,7 +115,7 @@ class InspectorPointerAlignmentTests { val row = findOrScrollToVisibleSelectRow(fixture) val rowIndex = fixture.inspector - .overlayStyleEditorRows() + .portalStyleEditorRows() .indexOfFirst { it.property == row.property } .takeIf { it >= 0 } ?: error("expected style row index for ${row.property.key}") val ownerKey = "dsgl-system-inspector-editor-select-$rowIndex" @@ -153,7 +153,7 @@ class InspectorPointerAlignmentTests { val row = findOrScrollToVisibleSelectRow(fixture) val rowIndex = fixture.inspector - .overlayStyleEditorRows() + .portalStyleEditorRows() .indexOfFirst { it.property == row.property } .takeIf { it >= 0 } ?: error("expected style row index for ${row.property.key}") val ownerKey = "dsgl-system-inspector-editor-select-$rowIndex" @@ -168,7 +168,7 @@ class InspectorPointerAlignmentTests { assertTrue(DomainPortalServices.systemSelectEngine.isOpenFor(ownerKey)) - val panelRect = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") + val panelRect = fixture.inspector.floatingPanelRect() ?: error("expected panel rect") val outsideX = (panelRect.x - 12).coerceAtLeast(1) val outsideY = (panelRect.y - 12).coerceAtLeast(1) assertFalse(dropdown.contains(outsideX, outsideY)) @@ -181,7 +181,7 @@ class InspectorPointerAlignmentTests { private fun openInspectorAndSelectTarget(withManyChildren: Boolean): Fixture { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot(withManyChildren) @@ -233,7 +233,7 @@ class InspectorPointerAlignmentTests { } private fun dragInspectorPanel(fixture: Fixture, deltaX: Int, deltaY: Int) { - val panelRect = fixture.inspector.overlayPanelRect() ?: error("expected panel rect") + val panelRect = fixture.inspector.floatingPanelRect() ?: error("expected panel rect") val downX = panelRect.x + 16 val downY = panelRect.y + 12 val moveX = downX + deltaX @@ -246,7 +246,7 @@ class InspectorPointerAlignmentTests { } private fun scrollInspectorBodyDown(fixture: Fixture, steps: Int) { - val contentRect = fixture.inspector.overlayContentRect() + val contentRect = fixture.inspector.portalContentRect() val wheelX = contentRect.x + 4 val wheelY = contentRect.y + 10 repeat(steps) { @@ -256,7 +256,7 @@ class InspectorPointerAlignmentTests { } private fun settleFrames(fixture: Fixture, steps: Int) { - val contentRect = fixture.inspector.overlayContentRect() + val contentRect = fixture.inspector.portalContentRect() val cursorX = contentRect.x + 4 val cursorY = contentRect.y + 10 repeat(steps) { @@ -267,10 +267,10 @@ class InspectorPointerAlignmentTests { private fun findOrScrollToVisibleSelectRow(fixture: Fixture): InspectorStyleEditorRowSnapshot { repeat(120) { val rows = - fixture.inspector.overlayStyleEditorRows().filter { row -> + fixture.inspector.portalStyleEditorRows().filter { row -> row.editorKind == InspectorEditorKind.EnumSelect || row.editorKind == InspectorEditorKind.FontSelect } - val contentRect = fixture.inspector.overlayContentRect() + val contentRect = fixture.inspector.portalContentRect() val bodyScrollY = fixture.inspector.panelScrollOffsetY val visible = rows.firstOrNull { row -> @@ -341,7 +341,7 @@ class InspectorPointerAlignmentTests { private fun findRowByProperty(fixture: Fixture, property: StyleProperty): InspectorStyleEditorRowSnapshot = fixture.inspector - .overlayStyleEditorRows() + .portalStyleEditorRows() .firstOrNull { it.property == property } ?: error("expected row for property ${property.key}") @@ -377,7 +377,7 @@ class InspectorPointerAlignmentTests { private data class Fixture( val inspector: InspectorController, - val host: SystemOverlayHost, + val host: SystemPortalHost, val root: ContainerNode, var revision: Long, var viewportWidth: Int, diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorTextEditingDomMigrationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/InspectorTextEditingDomMigrationTests.kt similarity index 93% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorTextEditingDomMigrationTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/InspectorTextEditingDomMigrationTests.kt index bc3b108..4c028b1 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/InspectorTextEditingDomMigrationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/InspectorTextEditingDomMigrationTests.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay.system +package org.dreamfinity.dsgl.core.portal.system import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent @@ -13,8 +13,8 @@ import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.input.ClipboardAccess import org.dreamfinity.dsgl.core.input.ClipboardBridge import org.dreamfinity.dsgl.core.inspector.InspectorController -import org.dreamfinity.dsgl.core.overlay.DomainPortalServices -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope +import org.dreamfinity.dsgl.core.portal.DomainPortalServices +import org.dreamfinity.dsgl.core.portal.ScreenDomainId import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.StyleEngine import org.dreamfinity.dsgl.core.style.StyleProperty @@ -205,13 +205,13 @@ class InspectorTextEditingDomMigrationTests { renderInspectorFrame(fixture, fixture.nextRevision, anchor.x + 1, anchor.y + 1) assertTrue(fixture.host.isSystemColorPickerOpen()) - assertEquals(OverlayOwnerScope.System, fixture.host.debugSystemColorPickerPopupOwnerScope()) - assertNotNull(fixture.host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup)) + assertEquals(ScreenDomainId.System, fixture.host.debugSystemColorPickerPopupOwnerDomain()) + assertNotNull(fixture.host.debugEntryNode(SystemPortalEntryId.ColorPickerPopup)) } private fun openInspectorAndSelectTarget(): Fixture { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val (root, target) = inspectedRoot() @@ -257,10 +257,10 @@ class InspectorTextEditingDomMigrationTests { fixture.host.render(ctx, 1280, 720) } - private fun findVisibleInputNode(host: SystemOverlayHost, inspector: InspectorController, keyPrefix: String): TextInputNode { + private fun findVisibleInputNode(host: SystemPortalHost, inspector: InspectorController, keyPrefix: String): TextInputNode { val inspectorNode = - host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector entry missing") - val contentRect = inspector.overlayContentRect() + host.debugEntryNode(SystemPortalEntryId.Inspector) ?: error("inspector entry missing") + val contentRect = inspector.portalContentRect() val candidates = collectNodes(inspectorNode) .filterIsInstance() @@ -275,7 +275,7 @@ class InspectorTextEditingDomMigrationTests { return visible ?: candidates.firstOrNull() ?: error("expected inspector input for prefix '$keyPrefix'") } - private fun focusInputByClick(host: SystemOverlayHost, input: TextInputNode): Pair { + private fun focusInputByClick(host: SystemPortalHost, input: TextInputNode): Pair { val y = input.bounds.y + (input.bounds.height / 2).coerceAtLeast(1) val left = (input.bounds.x + 2).coerceAtMost(input.bounds.x + input.bounds.width - 2) val center = input.bounds.x + (input.bounds.width / 2).coerceAtLeast(1) @@ -317,7 +317,7 @@ class InspectorTextEditingDomMigrationTests { private data class Fixture( val inspector: InspectorController, - val host: SystemOverlayHost, + val host: SystemPortalHost, val root: ContainerNode, val target: ContainerNode, val nextRevision: Long, diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalColorPickerEntryTests.kt similarity index 88% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalColorPickerEntryTests.kt index 095e8e5..01d83b7 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayColorPickerEntryTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalColorPickerEntryTests.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay.system +package org.dreamfinity.dsgl.core.portal.system import org.dreamfinity.dsgl.core.colorpicker.ColorFormatMode import org.dreamfinity.dsgl.core.colorpicker.ColorPickerPopupRequest @@ -18,8 +18,8 @@ import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.event.KeyCodes import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController -import org.dreamfinity.dsgl.core.overlay.DomainPortalServices -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope +import org.dreamfinity.dsgl.core.portal.DomainPortalServices +import org.dreamfinity.dsgl.core.portal.ScreenDomainId import org.dreamfinity.dsgl.core.render.RenderCommand import kotlin.test.Test import kotlin.test.assertEquals @@ -29,7 +29,7 @@ import kotlin.test.assertNotNull import kotlin.test.assertSame import kotlin.test.assertTrue -class SystemOverlayColorPickerEntryTests { +class SystemPortalColorPickerEntryTests { private val ctx = object : UiMeasureContext { override val fontHeight: Int = 9 @@ -41,7 +41,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker popup lifecycle is entry owned and stable`() { - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() @@ -51,30 +51,30 @@ class SystemOverlayColorPickerEntryTests { pickerService.open(anchorRect = Rect(40, 42, 20, 18), title = "Popup", state = popupState()) host.onInputFrame(960, 720) host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 44, cursorY = 48, inspectorPointerCaptured = false) - val firstNode = host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry node missing") - val firstState = host.debugEntryState(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry state missing") - assertEquals(OverlayOwnerScope.System, host.debugSystemColorPickerPopupOwnerScope()) + val firstNode = host.debugEntryNode(SystemPortalEntryId.ColorPickerPopup) ?: error("entry node missing") + val firstState = host.debugEntryState(SystemPortalEntryId.ColorPickerPopup) ?: error("entry state missing") + assertEquals(ScreenDomainId.System, host.debugSystemColorPickerPopupOwnerDomain()) assertTrue(firstState.active) assertNotNull(firstState.panelState.currentRectOrNull()) assertFalse(DomainPortalServices.applicationColorPickerEngine.isOpen()) host.onInputFrame(960, 720) host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 50, cursorY = 56, inspectorPointerCaptured = false) - val secondNode = host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry node missing") - val secondState = host.debugEntryState(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry state missing") + val secondNode = host.debugEntryNode(SystemPortalEntryId.ColorPickerPopup) ?: error("entry node missing") + val secondState = host.debugEntryState(SystemPortalEntryId.ColorPickerPopup) ?: error("entry state missing") assertSame(firstNode, secondNode) assertSame(firstState, secondState) pickerService.close() host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = 50, cursorY = 56, inspectorPointerCaptured = false) assertFalse(host.isSystemColorPickerOpen()) - assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerPopup)) + assertFalse(host.debugMountedEntryIds().contains(SystemPortalEntryId.ColorPickerPopup)) pickerService.open(anchorRect = Rect(40, 42, 20, 18), title = "Popup", state = popupState()) host.onInputFrame(960, 720) host.syncFrame(root, inspectedLayoutRevision = 4L, cursorX = 52, cursorY = 58, inspectorPointerCaptured = false) - val reopenedNode = host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry node missing") - val reopenedState = host.debugEntryState(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry state missing") + val reopenedNode = host.debugEntryNode(SystemPortalEntryId.ColorPickerPopup) ?: error("entry node missing") + val reopenedState = host.debugEntryState(SystemPortalEntryId.ColorPickerPopup) ?: error("entry state missing") assertSame(firstNode, reopenedNode) assertSame(firstState, reopenedState) assertTrue(reopenedState.active) @@ -82,7 +82,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker entry path stays independent from application runtime popup path`() { - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() val appOwner = Any() @@ -91,7 +91,7 @@ class SystemOverlayColorPickerEntryTests { DomainPortalServices.applicationColorPickerEngine.open( ColorPickerPopupRequest( owner = appOwner, - ownerScope = OverlayOwnerScope.Application, + ownerDomain = ScreenDomainId.Application, anchorRect = Rect(240, 210, 20, 18), title = "App Popup", state = popupState(), @@ -109,12 +109,12 @@ class SystemOverlayColorPickerEntryTests { inspectorPointerCaptured = false, ) - val node = host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry node missing") + val node = host.debugEntryNode(SystemPortalEntryId.ColorPickerPopup) ?: error("entry node missing") val styleTypes = collectStyleTypes(node) - assertTrue(styleTypes.contains("dsgl-overlay-panel")) + assertTrue(styleTypes.contains("dsgl-floating-panel")) assertTrue(styleTypes.contains("dsgl-system-color-picker-native-body")) assertFalse(styleTypes.contains("dsgl-system-raw-render-command")) - assertEquals(OverlayOwnerScope.System, host.debugSystemColorPickerPopupOwnerScope()) + assertEquals(ScreenDomainId.System, host.debugSystemColorPickerPopupOwnerDomain()) assertTrue(host.isSystemColorPickerOpen()) assertTrue(DomainPortalServices.applicationColorPickerEngine.isOpenFor(appOwner)) @@ -136,7 +136,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker popup drag uses persistent entry drag session and keeps node stable`() { - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() @@ -144,8 +144,8 @@ class SystemOverlayColorPickerEntryTests { host.onInputFrame(1200, 800) host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 88, cursorY = 98, inspectorPointerCaptured = false) - val stableNode = host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry node missing") - val stateBefore = host.debugEntryState(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry state missing") + val stableNode = host.debugEntryNode(SystemPortalEntryId.ColorPickerPopup) ?: error("entry node missing") + val stateBefore = host.debugEntryState(SystemPortalEntryId.ColorPickerPopup) ?: error("entry state missing") val panelBefore = stateBefore.panelState.currentRectOrNull() ?: error("panel missing") val header = host.debugSystemColorPickerHeaderRect() ?: error("header missing") val startX = header.x + 8 @@ -161,7 +161,7 @@ class SystemOverlayColorPickerEntryTests { cursorY = startY + 30, inspectorPointerCaptured = false, ) - val midState = host.debugEntryState(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry state missing") + val midState = host.debugEntryState(SystemPortalEntryId.ColorPickerPopup) ?: error("entry state missing") val panelMid = midState.panelState.currentRectOrNull() ?: error("panel missing") assertNotEquals(panelBefore.x, panelMid.x) @@ -173,8 +173,8 @@ class SystemOverlayColorPickerEntryTests { cursorY = startY + 60, inspectorPointerCaptured = false, ) - val movingNode = host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry node missing") - val movingState = host.debugEntryState(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry state missing") + val movingNode = host.debugEntryNode(SystemPortalEntryId.ColorPickerPopup) ?: error("entry node missing") + val movingState = host.debugEntryState(SystemPortalEntryId.ColorPickerPopup) ?: error("entry state missing") val panelAfter = movingState.panelState.currentRectOrNull() ?: error("panel missing") assertSame(stableNode, movingNode) assertSame(stateBefore, movingState) @@ -189,7 +189,7 @@ class SystemOverlayColorPickerEntryTests { cursorY = startY + 60, inspectorPointerCaptured = false, ) - val finalState = host.debugEntryState(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry state missing") + val finalState = host.debugEntryState(SystemPortalEntryId.ColorPickerPopup) ?: error("entry state missing") val panelFinal = finalState.panelState.currentRectOrNull() ?: error("panel missing") assertFalse(finalState.dragSession.active) assertEquals(panelAfter.x, panelFinal.x) @@ -198,7 +198,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker popup survives routine sync updates without remount during drag`() { - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() @@ -212,7 +212,7 @@ class SystemOverlayColorPickerEntryTests { inspectorPointerCaptured = false, ) - val initialNode = host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry node missing") + val initialNode = host.debugEntryNode(SystemPortalEntryId.ColorPickerPopup) ?: error("entry node missing") val header = host.debugSystemColorPickerHeaderRect() ?: error("header missing") val startX = header.x + 6 val startY = header.y + 6 @@ -229,8 +229,8 @@ class SystemOverlayColorPickerEntryTests { cursorY = my, inspectorPointerCaptured = false, ) - val node = host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry node missing") - val state = host.debugEntryState(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry state missing") + val node = host.debugEntryNode(SystemPortalEntryId.ColorPickerPopup) ?: error("entry node missing") + val state = host.debugEntryState(SystemPortalEntryId.ColorPickerPopup) ?: error("entry state missing") assertSame(initialNode, node) assertTrue(state.dragSession.active) } @@ -238,7 +238,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker popup close button closes entry through panel panel`() { - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() @@ -255,12 +255,12 @@ class SystemOverlayColorPickerEntryTests { inspectorPointerCaptured = false, ) assertFalse(host.isSystemColorPickerOpen()) - assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerPopup)) + assertFalse(host.debugMountedEntryIds().contains(SystemPortalEntryId.ColorPickerPopup)) } @Test fun `system picker keyboard-open path uses valid viewport after input frame sync`() { - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() val anchor = Rect(360, 220, 1, 1) @@ -274,7 +274,7 @@ class SystemOverlayColorPickerEntryTests { cursorY = 226, inspectorPointerCaptured = false, ) - val state = host.debugEntryState(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry state missing") + val state = host.debugEntryState(SystemPortalEntryId.ColorPickerPopup) ?: error("entry state missing") val panel = state.panelState.currentRectOrNull() ?: error("panel missing") assertNotEquals(2, panel.x) @@ -285,7 +285,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker entry mounts native body subtree without command bridge`() { - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() @@ -293,9 +293,9 @@ class SystemOverlayColorPickerEntryTests { host.onInputFrame(1200, 800) host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 64, cursorY = 74, inspectorPointerCaptured = false) - val node = host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) ?: error("entry node missing") + val node = host.debugEntryNode(SystemPortalEntryId.ColorPickerPopup) ?: error("entry node missing") val styleTypes = collectStyleTypes(node) - assertTrue(styleTypes.contains("dsgl-overlay-panel")) + assertTrue(styleTypes.contains("dsgl-floating-panel")) assertTrue(styleTypes.contains("dsgl-system-color-picker-native-body")) assertFalse(styleTypes.contains("dsgl-system-color-picker-command-bridge")) assertFalse(styleTypes.contains("dsgl-system-raw-render-command")) @@ -303,7 +303,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker color field drag updates color continuously`() { - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() val previews = ArrayList() @@ -352,7 +352,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker current swatch click commits once without double apply`() { - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() var commits = 0 @@ -381,7 +381,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker recent swatch click previews once without double apply`() { - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() val initial = popupState() @@ -446,7 +446,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker sync state updates current swatch without drag nudge`() { - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() val initial = popupState() @@ -476,7 +476,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker sync state updates rgb order button selected visuals without drag nudge`() { - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() val style = ColorPickerStyle() @@ -510,7 +510,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker hue and alpha drag update state`() { - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() @@ -558,7 +558,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker text input and mode controls stay synchronized`() { - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() @@ -618,7 +618,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker input focus retargets by semantic key across rgb order switch`() { - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() @@ -662,7 +662,7 @@ class SystemOverlayColorPickerEntryTests { @Test fun `system picker rgb order buttons use dom semantic actions without double apply`() { - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() var previews = 0 @@ -705,12 +705,12 @@ class SystemOverlayColorPickerEntryTests { assertEquals(ColorFormatMode.RGB, updated.mode) assertEquals(0, previews) assertEquals(0, commits) - assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerTransient)) + assertFalse(host.debugMountedEntryIds().contains(SystemPortalEntryId.ColorPickerTransient)) } @Test fun `system picker mode trigger toggles dropdown through dom path without double apply`() { - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() @@ -726,7 +726,7 @@ class SystemOverlayColorPickerEntryTests { val initialLayout = host.debugSystemColorPickerBodyLayout() ?: error("layout missing") val modeSelect = initialLayout.modeSelectRect - assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerTransient)) + assertFalse(host.debugMountedEntryIds().contains(SystemPortalEntryId.ColorPickerTransient)) assertTrue(host.handleMouseDown(modeSelect.x + 2, modeSelect.y + 2, MouseButton.LEFT)) assertTrue(host.handleMouseUp(modeSelect.x + 2, modeSelect.y + 2, MouseButton.LEFT)) @@ -738,7 +738,7 @@ class SystemOverlayColorPickerEntryTests { inspectorPointerCaptured = false, ) - assertTrue(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerTransient)) + assertTrue(host.debugMountedEntryIds().contains(SystemPortalEntryId.ColorPickerTransient)) assertNotNull(host.debugSystemColorPickerBodyLayout()?.modeOptionsRect) assertTrue(host.handleMouseDown(modeSelect.x + 2, modeSelect.y + 2, MouseButton.LEFT)) @@ -751,13 +751,13 @@ class SystemOverlayColorPickerEntryTests { inspectorPointerCaptured = false, ) - assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerTransient)) + assertFalse(host.debugMountedEntryIds().contains(SystemPortalEntryId.ColorPickerTransient)) assertTrue(host.debugSystemColorPickerBodyLayout()?.modeOptionsRect == null) } @Test fun `system picker mode option click changes mode and closes dropdown via dom path`() { - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() var previews = 0 @@ -805,14 +805,14 @@ class SystemOverlayColorPickerEntryTests { ) assertEquals(ColorFormatMode.HSL, host.debugSystemColorPickerState()?.mode) - assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerTransient)) + assertFalse(host.debugMountedEntryIds().contains(SystemPortalEntryId.ColorPickerTransient)) assertEquals(0, previews) assertEquals(0, commits) } @Test fun `system picker mode dropdown is mounted in transient lane and stays interactive`() { - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() @@ -826,7 +826,7 @@ class SystemOverlayColorPickerEntryTests { inspectorPointerCaptured = false, ) - assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerTransient)) + assertFalse(host.debugMountedEntryIds().contains(SystemPortalEntryId.ColorPickerTransient)) val initialLayout = host.debugSystemColorPickerBodyLayout() ?: error("layout missing") val modeSelect = initialLayout.modeSelectRect @@ -839,11 +839,11 @@ class SystemOverlayColorPickerEntryTests { inspectorPointerCaptured = false, ) - assertTrue(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerTransient)) + assertTrue(host.debugMountedEntryIds().contains(SystemPortalEntryId.ColorPickerTransient)) val transientNode = - host.debugEntryNode(SystemOverlayEntryId.ColorPickerTransient) ?: error("transient node missing") + host.debugEntryNode(SystemPortalEntryId.ColorPickerTransient) ?: error("transient node missing") val transientStyleTypes = collectStyleTypes(transientNode) - assertTrue(transientStyleTypes.contains("dsgl-system-color-picker-native-mode-dropdown-overlay")) + assertTrue(transientStyleTypes.contains("dsgl-system-color-picker-native-mode-dropdown-portal")) val expandedLayout = host.debugSystemColorPickerBodyLayout() ?: error("expanded layout missing") val hslOption = @@ -859,12 +859,12 @@ class SystemOverlayColorPickerEntryTests { ) assertEquals(ColorFormatMode.HSL, host.debugSystemColorPickerState()?.mode) - assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerTransient)) + assertFalse(host.debugMountedEntryIds().contains(SystemPortalEntryId.ColorPickerTransient)) } @Test - fun `system picker pipette keeps system overlay visible and uses transient lane`() { - val host = SystemOverlayHost(InspectorController()) + fun `system picker pipette keeps system portal visible and uses transient lane`() { + val host = SystemPortalHost(InspectorController()) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() @@ -890,11 +890,11 @@ class SystemOverlayColorPickerEntryTests { ) val mounted = host.debugMountedEntryIds() - assertTrue(mounted.contains(SystemOverlayEntryId.ColorPickerPopup)) - assertTrue(mounted.contains(SystemOverlayEntryId.ColorPickerTransient)) + assertTrue(mounted.contains(SystemPortalEntryId.ColorPickerPopup)) + assertTrue(mounted.contains(SystemPortalEntryId.ColorPickerTransient)) assertTrue(host.isSystemColorPickerOpen()) - assertTrue(host.debugEntryState(SystemOverlayEntryId.ColorPickerPopup)?.active == true) - assertEquals(OverlayOwnerScope.System, host.debugSystemColorPickerPopupOwnerScope()) + assertTrue(host.debugEntryState(SystemPortalEntryId.ColorPickerPopup)?.active == true) + assertEquals(ScreenDomainId.System, host.debugSystemColorPickerPopupOwnerDomain()) val moveConsumed = host.handleMouseMove(pipette.x + 40, pipette.y + 40) assertTrue(moveConsumed) @@ -906,12 +906,12 @@ class SystemOverlayColorPickerEntryTests { inspectorPointerCaptured = false, ) - assertTrue(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerPopup)) + assertTrue(host.debugMountedEntryIds().contains(SystemPortalEntryId.ColorPickerPopup)) } @Test fun `system picker pipette transient entry emits visible tooltip commands`() { - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() val gridColor = 0x7F4C93FF @@ -926,8 +926,8 @@ class SystemOverlayColorPickerEntryTests { ColorPickerStyle( eyedropperGridSize = 5, eyedropperCellSize = 3, - eyedropperGridOverlayEnabled = true, - eyedropperGridOverlayColor = gridColor, + eyedropperGridEnabled = true, + eyedropperGridColor = gridColor, checkerLightColor = checkerLight, checkerDarkColor = checkerDark, ), @@ -969,11 +969,11 @@ class SystemOverlayColorPickerEntryTests { }, ) val capturedRegion = commands.filterIsInstance().single() - val gridOverlay = capturedRegion.gridOverlay ?: error("grid overlay missing") - assertEquals(5, gridOverlay.columns) - assertEquals(5, gridOverlay.rows) - assertEquals(3, gridOverlay.magnification) - assertEquals(gridColor, gridOverlay.color) + val grid = capturedRegion.grid ?: error("grid missing") + assertEquals(5, grid.columns) + assertEquals(5, grid.rows) + assertEquals(3, grid.magnification) + assertEquals(gridColor, grid.color) assertTrue( commands.none { command -> command is RenderCommand.DrawRect && command.color == gridColor @@ -994,7 +994,7 @@ class SystemOverlayColorPickerEntryTests { }, ) try { - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayDomBridgeTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalDomBridgeTests.kt similarity index 67% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayDomBridgeTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalDomBridgeTests.kt index 29a2e00..06d924f 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayDomBridgeTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalDomBridgeTests.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay.system +package org.dreamfinity.dsgl.core.portal.system import org.dreamfinity.dsgl.core.dom.DOMNode import org.dreamfinity.dsgl.core.dom.applyParent @@ -9,8 +9,8 @@ import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.FocusManager import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController -import org.dreamfinity.dsgl.core.inspector.internal.SystemInspectorOverlayNode -import org.dreamfinity.dsgl.core.overlay.input.LayerDomInputRouter +import org.dreamfinity.dsgl.core.inspector.internal.SystemInspectorPortalNode +import org.dreamfinity.dsgl.core.portal.input.SurfaceDomInputRouter import org.dreamfinity.dsgl.core.render.RenderCommand import kotlin.test.Test import kotlin.test.assertEquals @@ -18,7 +18,7 @@ import kotlin.test.assertNotNull import kotlin.test.assertSame import kotlin.test.assertTrue -class SystemOverlayDomBridgeTests { +class SystemPortalDomBridgeTests { private val ctx = object : UiMeasureContext { override val fontHeight: Int = 9 @@ -37,7 +37,7 @@ class SystemOverlayDomBridgeTests { RenderCommand.DrawText("Hello", 18, 20, 0xFFEEDDCC.toInt()), ) - SystemOverlayCommandDslRenderer.rebuildInto(host, commands, "test") + SystemPortalCommandDslRenderer.rebuildInto(host, commands, "test") assertEquals(2, host.children.size) assertTrue(host.children.all { it.styleType == "dsgl-system-raw-render-command" }) @@ -57,17 +57,17 @@ class SystemOverlayDomBridgeTests { RenderCommand.DrawText("B", 6, 7, 0xFFFFFFFF.toInt()), ) - SystemOverlayCommandDslRenderer.rebuildInto(host, first, "reuse") + SystemPortalCommandDslRenderer.rebuildInto(host, first, "reuse") val firstNode0 = host.children[0] val firstNode1 = host.children[1] - SystemOverlayCommandDslRenderer.rebuildInto(host, second, "reuse") + SystemPortalCommandDslRenderer.rebuildInto(host, second, "reuse") assertSame(firstNode0, host.children[0]) assertSame(firstNode1, host.children[1]) } @Test - fun `system inspector overlay creates native dom children from controller frame`() { + fun `system inspector portal creates native dom children from controller frame`() { val controller = InspectorController() controller.toggle() val root = @@ -79,17 +79,17 @@ class SystemOverlayDomBridgeTests { bounds = Rect(16, 18, 120, 28) }.applyParent(root) - val overlay = SystemInspectorOverlayNode(controller) - overlay.bindInspectedTree(root, layoutRevision = 1L) - overlay.updateCursor(mouseX = 22, mouseY = 22, pointerCaptured = false) - overlay.render(ctx, 0, 0, 420, 280) + val portalNode = SystemInspectorPortalNode(controller) + portalNode.bindInspectedTree(root, layoutRevision = 1L) + portalNode.updateCursor(mouseX = 22, mouseY = 22, pointerCaptured = false) + portalNode.render(ctx, 0, 0, 420, 280) - assertTrue(overlay.children.isNotEmpty()) - assertTrue(overlay.children.none { it.styleType == "dsgl-system-raw-render-command" }) + assertTrue(portalNode.children.isNotEmpty()) + assertTrue(portalNode.children.none { it.styleType == "dsgl-system-raw-render-command" }) } @Test - fun `system inspector overlay retains focused native input across frame rebuild`() { + fun `system inspector portal retains focused native input across frame rebuild`() { val controller = InspectorController() controller.toggle() val root = @@ -101,14 +101,14 @@ class SystemOverlayDomBridgeTests { bounds = Rect(980, 140, 120, 30) }.applyParent(root) - val overlay = SystemInspectorOverlayNode(controller) + val portalNode = SystemInspectorPortalNode(controller) controller.onLayoutCommitted(root, 1L) controller.onCursorMoved(984, 144) controller.handleMouseDown(984, 144, MouseButton.LEFT) - overlay.bindInspectedTree(root, layoutRevision = 2L) - overlay.updateCursor(mouseX = 984, mouseY = 144, pointerCaptured = false) - overlay.render(ctx, 0, 0, 1280, 720) + portalNode.bindInspectedTree(root, layoutRevision = 2L) + portalNode.updateCursor(mouseX = 984, mouseY = 144, pointerCaptured = false) + portalNode.render(ctx, 0, 0, 1280, 720) fun findFirstInput(node: DOMNode): SingleLineInputNode? { if (node is SingleLineInputNode) return node @@ -119,9 +119,9 @@ class SystemOverlayDomBridgeTests { return null } - val initialInput = findFirstInput(overlay) + val initialInput = findFirstInput(portalNode) assertNotNull(initialInput) - val router = LayerDomInputRouter { overlay } + val router = SurfaceDomInputRouter { portalNode } val clickX = initialInput.bounds.x + 2 val clickY = initialInput.bounds.y + initialInput.bounds.height / 2 assertTrue(router.handleMouseDown(clickX, clickY, MouseButton.LEFT)) @@ -132,9 +132,9 @@ class SystemOverlayDomBridgeTests { val focusedKey = focusedAfterClick.key assertEquals(initialInput.key, focusedKey) - overlay.bindInspectedTree(root, layoutRevision = 3L) - overlay.updateCursor(mouseX = 984, mouseY = 144, pointerCaptured = false) - overlay.render(ctx, 0, 0, 1280, 720) + portalNode.bindInspectedTree(root, layoutRevision = 3L) + portalNode.updateCursor(mouseX = 984, mouseY = 144, pointerCaptured = false) + portalNode.render(ctx, 0, 0, 1280, 720) val focusedAfterRebuild = FocusManager.focusedNode() assertTrue(focusedAfterRebuild is SingleLineInputNode) @@ -143,25 +143,25 @@ class SystemOverlayDomBridgeTests { } @Test - fun `system inspector overlay mounts only while inspector is active`() { + fun `system inspector portal mounts only while inspector is active`() { val controller = InspectorController() val root = ContainerNode(key = "root").apply { bounds = Rect(0, 0, 420, 280) } - val overlay = SystemInspectorOverlayNode(controller) + val portalNode = SystemInspectorPortalNode(controller) - overlay.bindInspectedTree(root, layoutRevision = 1L) - overlay.render(ctx, 0, 0, 420, 280) - assertTrue(overlay.children.isEmpty()) + portalNode.bindInspectedTree(root, layoutRevision = 1L) + portalNode.render(ctx, 0, 0, 420, 280) + assertTrue(portalNode.children.isEmpty()) controller.toggle() - overlay.render(ctx, 0, 0, 420, 280) - assertTrue(overlay.children.isNotEmpty()) - assertTrue(overlay.children.none { it.styleType == "dsgl-system-raw-render-command" }) + portalNode.render(ctx, 0, 0, 420, 280) + assertTrue(portalNode.children.isNotEmpty()) + assertTrue(portalNode.children.none { it.styleType == "dsgl-system-raw-render-command" }) controller.deactivate() - overlay.render(ctx, 0, 0, 420, 280) - assertTrue(overlay.children.isEmpty()) + portalNode.render(ctx, 0, 0, 420, 280) + assertTrue(portalNode.children.isEmpty()) } } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntryInfrastructureTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalEntryInfrastructureTests.kt similarity index 74% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntryInfrastructureTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalEntryInfrastructureTests.kt index 21083f7..dc6c6aa 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayEntryInfrastructureTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalEntryInfrastructureTests.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay.system +package org.dreamfinity.dsgl.core.portal.system import org.dreamfinity.dsgl.core.colorpicker.ColorFormatMode import org.dreamfinity.dsgl.core.colorpicker.ColorPickerState @@ -7,9 +7,9 @@ import org.dreamfinity.dsgl.core.dom.applyParent import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.inspector.InspectorController -import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelDragSession -import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelDragType -import org.dreamfinity.dsgl.core.overlay.panel.OverlayPanelState +import org.dreamfinity.dsgl.core.portal.panel.FloatingPanelDragSession +import org.dreamfinity.dsgl.core.portal.panel.FloatingPanelDragType +import org.dreamfinity.dsgl.core.portal.panel.FloatingPanelState import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -18,15 +18,15 @@ import kotlin.test.assertNotSame import kotlin.test.assertSame import kotlin.test.assertTrue -class SystemOverlayEntryInfrastructureTests { +class SystemPortalEntryInfrastructureTests { @Test - fun `system overlay host exposes explicit persistent entries`() { - val host = SystemOverlayHost(InspectorController()) + fun `system portal host exposes explicit persistent entries`() { + val host = SystemPortalHost(InspectorController()) assertEquals( listOf( - SystemOverlayEntryId.Inspector, - SystemOverlayEntryId.ColorPickerPopup, - SystemOverlayEntryId.ColorPickerTransient, + SystemPortalEntryId.Inspector, + SystemPortalEntryId.ColorPickerPopup, + SystemPortalEntryId.ColorPickerTransient, ), host.debugRegisteredEntryIds(), ) @@ -43,35 +43,35 @@ class SystemOverlayEntryInfrastructureTests { @Test fun `inspector entry keeps stable node and state while active`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) val root = inspectedRoot() host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 10, cursorY = 10, inspectorPointerCaptured = false) - assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.Inspector)) + assertFalse(host.debugMountedEntryIds().contains(SystemPortalEntryId.Inspector)) inspector.toggle() host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 14, cursorY = 14, inspectorPointerCaptured = false) - val firstState = host.debugEntryState(SystemOverlayEntryId.Inspector) ?: error("state missing") - val firstNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("node missing") - assertTrue(host.debugMountedEntryIds().contains(SystemOverlayEntryId.Inspector)) + val firstState = host.debugEntryState(SystemPortalEntryId.Inspector) ?: error("state missing") + val firstNode = host.debugEntryNode(SystemPortalEntryId.Inspector) ?: error("node missing") + assertTrue(host.debugMountedEntryIds().contains(SystemPortalEntryId.Inspector)) assertTrue(firstState.active) assertEquals(listOf("system.Inspector"), host.debugActivePortalEntryIds()) host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = 22, cursorY = 20, inspectorPointerCaptured = false) - val secondState = host.debugEntryState(SystemOverlayEntryId.Inspector) ?: error("state missing") - val secondNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("node missing") + val secondState = host.debugEntryState(SystemPortalEntryId.Inspector) ?: error("state missing") + val secondNode = host.debugEntryNode(SystemPortalEntryId.Inspector) ?: error("node missing") assertSame(firstState, secondState) assertSame(firstNode, secondNode) inspector.deactivate() host.syncFrame(root, inspectedLayoutRevision = 4L, cursorX = 22, cursorY = 20, inspectorPointerCaptured = false) - assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.Inspector)) + assertFalse(host.debugMountedEntryIds().contains(SystemPortalEntryId.Inspector)) assertFalse(host.debugActivePortalEntryIds().contains("system.Inspector")) } @Test fun `color picker entry keeps panel state and identity stable across routine updates`() { - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() pickerService.open(anchorRect = Rect(36, 44, 20, 18), title = "Popup", state = popupState()) @@ -83,8 +83,8 @@ class SystemOverlayEntryInfrastructureTests { cursorY = 42, inspectorPointerCaptured = false, ) - val firstState = host.debugEntryState(SystemOverlayEntryId.ColorPickerPopup) ?: error("state missing") - val firstNode = host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) ?: error("node missing") + val firstState = host.debugEntryState(SystemPortalEntryId.ColorPickerPopup) ?: error("state missing") + val firstNode = host.debugEntryNode(SystemPortalEntryId.ColorPickerPopup) ?: error("node missing") val firstRect = firstState.panelState.currentRectOrNull() assertNotNull(firstRect) assertTrue(firstState.active) @@ -96,29 +96,29 @@ class SystemOverlayEntryInfrastructureTests { cursorY = 65, inspectorPointerCaptured = false, ) - val secondState = host.debugEntryState(SystemOverlayEntryId.ColorPickerPopup) ?: error("state missing") - val secondNode = host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) ?: error("node missing") + val secondState = host.debugEntryState(SystemPortalEntryId.ColorPickerPopup) ?: error("state missing") + val secondNode = host.debugEntryNode(SystemPortalEntryId.ColorPickerPopup) ?: error("node missing") val secondRect = secondState.panelState.currentRectOrNull() ?: error("panel rect missing") assertSame(firstState, secondState) assertSame(firstNode, secondNode) assertEquals(firstRect.x, secondRect.x) assertEquals(firstRect.y, secondRect.y) - assertTrue(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerPopup)) + assertTrue(host.debugMountedEntryIds().contains(SystemPortalEntryId.ColorPickerPopup)) assertTrue(host.debugActivePortalEntryIds().contains("system.ColorPickerPopup")) } finally { pickerService.close() } host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = 60, cursorY = 65, inspectorPointerCaptured = false) - assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerPopup)) + assertFalse(host.debugMountedEntryIds().contains(SystemPortalEntryId.ColorPickerPopup)) assertFalse(host.debugActivePortalEntryIds().contains("system.ColorPickerPopup")) } @Test fun `entry ordering stays explicit when both system entries are active`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) val pickerService = host.systemInspectorColorPickerService() val root = inspectedRoot() inspector.toggle() @@ -132,7 +132,7 @@ class SystemOverlayEntryInfrastructureTests { inspectorPointerCaptured = false, ) assertEquals( - listOf(SystemOverlayEntryId.Inspector, SystemOverlayEntryId.ColorPickerPopup), + listOf(SystemPortalEntryId.Inspector, SystemPortalEntryId.ColorPickerPopup), host.debugMountedEntryIds(), ) assertEquals( @@ -147,20 +147,20 @@ class SystemOverlayEntryInfrastructureTests { @Test fun `drag session captures start anchor updates and ends deterministically`() { - val panelState = OverlayPanelState() + val panelState = FloatingPanelState() panelState.updateFromRect(Rect(20, 24, 300, 200)) - val session = OverlayPanelDragSession() + val session = FloatingPanelDragSession() session.begin( - ownerId = SystemOverlayEntryId.ColorPickerPopup, - type = OverlayPanelDragType.PanelMove, + ownerId = SystemPortalEntryId.ColorPickerPopup, + type = FloatingPanelDragType.PanelMove, pointerX = 100, pointerY = 120, panelState = panelState, ) assertTrue(session.active) - assertEquals(SystemOverlayEntryId.ColorPickerPopup, session.ownerId) - assertEquals(OverlayPanelDragType.PanelMove, session.type) + assertEquals(SystemPortalEntryId.ColorPickerPopup, session.ownerId) + assertEquals(FloatingPanelDragType.PanelMove, session.type) assertEquals(100, session.startPointerX) assertEquals(120, session.startPointerY) assertEquals(20, session.startPanelX) @@ -180,7 +180,7 @@ class SystemOverlayEntryInfrastructureTests { @Test fun `transient system ownership is owner session based and not cursor based`() { - val registry = SystemOverlayTransientOwnershipRegistry() + val registry = SystemPortalTransientOwnershipRegistry() val ownerA = Any() val ownerB = Any() @@ -190,12 +190,12 @@ class SystemOverlayEntryInfrastructureTests { assertSame(sessionA1, sessionA2) assertNotSame(sessionA1, sessionB) - assertEquals(SystemOverlayEntryId.TransientSession, sessionA1.entryState.id) + assertEquals(SystemPortalEntryId.TransientSession, sessionA1.entryState.id) } @Test fun `host transient session api preserves owner based stable sessions`() { - val host = SystemOverlayHost(InspectorController()) + val host = SystemPortalHost(InspectorController()) val owner = Any() val first = host.resolveTransientSession(owner, cursorX = 10, cursorY = 10) val second = host.resolveTransientSession(owner, cursorX = 400, cursorY = 220) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalInspectorNativeEntryTests.kt similarity index 89% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalInspectorNativeEntryTests.kt index b1372b1..29f4205 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayInspectorNativeEntryTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalInspectorNativeEntryTests.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay.system +package org.dreamfinity.dsgl.core.portal.system import org.dreamfinity.dsgl.core.colorpicker.* import org.dreamfinity.dsgl.core.dom.DOMNode @@ -13,9 +13,9 @@ import org.dreamfinity.dsgl.core.event.MouseButton import org.dreamfinity.dsgl.core.inspector.InspectorController import org.dreamfinity.dsgl.core.inspector.InspectorMode import org.dreamfinity.dsgl.core.inspector.InspectorPanelState -import org.dreamfinity.dsgl.core.inspector.internal.SystemInspectorOverlayNode -import org.dreamfinity.dsgl.core.overlay.DomainPortalServices -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope +import org.dreamfinity.dsgl.core.inspector.internal.SystemInspectorPortalNode +import org.dreamfinity.dsgl.core.portal.DomainPortalServices +import org.dreamfinity.dsgl.core.portal.ScreenDomainId import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.Display import org.dreamfinity.dsgl.core.style.Overflow @@ -25,7 +25,7 @@ import java.io.File import java.nio.file.Files import kotlin.test.* -class SystemOverlayInspectorNativeEntryTests { +class SystemPortalInspectorNativeEntryTests { private val ctx = object : UiMeasureContext { override val fontHeight: Int = 9 @@ -44,38 +44,40 @@ class SystemOverlayInspectorNativeEntryTests { } @Test - fun `inspector migration removes intermediate native overlay model classes`() { + fun `inspector migration removes intermediate native portal model classes`() { val loadResult = runCatching { - Class.forName("org.dreamfinity.dsgl.core.inspector.InspectorNativeOverlayModel") + Class.forName(removedLegacyNativeModelClassName()) } assertTrue(loadResult.isFailure) } + private fun removedLegacyNativeModelClassName(): String = "org.dreamfinity.dsgl.core.inspector.InspectorNative" + "Over" + "layModel" + @Test - fun `inspector controller no longer exposes manual append overlay commands path`() { + fun `inspector controller no longer exposes manual append portal commands path`() { val methodNames = InspectorController::class.java.methods .map { it.name } - assertFalse(methodNames.contains("appendOverlayCommands")) + assertFalse(methodNames.contains("appendPortalCommands")) } @Test - fun `inspector overlay rebuild does not leak event bus registrations`() { + fun `inspector portal rebuild does not leak event bus registrations`() { val controller = InspectorController() - val overlay = SystemInspectorOverlayNode(controller) + val portalNode = SystemInspectorPortalNode(controller) val root = inspectedRoot() controller.toggle() - overlay.bindInspectedTree(root, layoutRevision = 1L) - overlay.updateCursor(mouseX = 984, mouseY = 144, pointerCaptured = false) - overlay.render(ctx, 0, 0, 1280, 720) + portalNode.bindInspectedTree(root, layoutRevision = 1L) + portalNode.updateCursor(mouseX = 984, mouseY = 144, pointerCaptured = false) + portalNode.render(ctx, 0, 0, 1280, 720) val firstSnapshot = EventBus.debugListenerSnapshot() repeat(24) { frame -> - overlay.bindInspectedTree(root, layoutRevision = 2L + frame) - overlay.updateCursor(mouseX = 984, mouseY = 144, pointerCaptured = false) - overlay.render(ctx, 0, 0, 1280, 720) + portalNode.bindInspectedTree(root, layoutRevision = 2L + frame) + portalNode.updateCursor(mouseX = 984, mouseY = 144, pointerCaptured = false) + portalNode.render(ctx, 0, 0, 1280, 720) } val repeatedSnapshot = EventBus.debugListenerSnapshot() @@ -83,16 +85,16 @@ class SystemOverlayInspectorNativeEntryTests { assertTrue(repeatedSnapshot.registeredCallbacks <= firstSnapshot.registeredCallbacks + 24) controller.deactivate() - overlay.render(ctx, 0, 0, 1280, 720) + portalNode.render(ctx, 0, 0, 1280, 720) val deactivatedSnapshot = EventBus.debugListenerSnapshot() assertTrue(deactivatedSnapshot.registeredNodes <= firstSnapshot.registeredNodes) assertTrue(deactivatedSnapshot.registeredCallbacks <= firstSnapshot.registeredCallbacks) } @Test - fun `live inspector path is native system-overlay entry and anti-legacy guarded`() { + fun `live inspector path is native system portal entry and anti-legacy guarded`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot() @@ -107,8 +109,8 @@ class SystemOverlayInspectorNativeEntryTests { ) host.render(ctx, 1280, 720) - assertTrue(host.debugMountedEntryIds().contains(SystemOverlayEntryId.Inspector)) - val node = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector entry missing") + assertTrue(host.debugMountedEntryIds().contains(SystemPortalEntryId.Inspector)) + val node = host.debugEntryNode(SystemPortalEntryId.Inspector) ?: error("inspector entry missing") val styleTypes = collectStyleTypes(node) assertTrue(styleTypes.contains("dsgl-system-inspector")) assertFalse(styleTypes.contains("dsgl-system-raw-render-command")) @@ -118,7 +120,7 @@ class SystemOverlayInspectorNativeEntryTests { @Test fun `expanded inspector paints occluder above full highlight geometry`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val initialRoot = inspectedRoot() @@ -145,11 +147,11 @@ class SystemOverlayInspectorNativeEntryTests { ) host.render(ctx, 1280, 720) - val panelRect = inspector.overlayPanelRect() ?: error("panel rect missing") - val highlight = inspector.overlaySelectedHighlight() ?: error("selected highlight missing") + val panelRect = inspector.floatingPanelRect() ?: error("panel rect missing") + val highlight = inspector.portalSelectedHighlight() ?: error("selected highlight missing") assertTrue(intersects(highlight.contentRect, panelRect)) - val inspectorNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector node missing") + val inspectorNode = host.debugEntryNode(SystemPortalEntryId.Inspector) ?: error("inspector node missing") val directChildren = inspectorNode.children.toList() val occluder = @@ -168,7 +170,7 @@ class SystemOverlayInspectorNativeEntryTests { @Test fun `inspector runtime interaction path supports selection controls and system-owned color edit`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot() @@ -189,12 +191,12 @@ class SystemOverlayInspectorNativeEntryTests { host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 80, cursorY = 52, inspectorPointerCaptured = false) host.render(ctx, 1280, 720) - val pickToggle = inspector.overlayPickToggleBounds() ?: error("pick toggle missing") + val pickToggle = inspector.portalPickToggleBounds() ?: error("pick toggle missing") assertTrue(host.handleMouseDown(pickToggle.x + 1, pickToggle.y + 1, MouseButton.LEFT)) assertTrue(host.handleMouseUp(pickToggle.x + 1, pickToggle.y + 1, MouseButton.LEFT)) assertEquals(InspectorMode.Pick, inspector.mode) - val colorAction = inspector.overlayColorPickerActionBounds(StyleProperty.BACKGROUND_COLOR) + val colorAction = inspector.portalColorPickerActionBounds(StyleProperty.BACKGROUND_COLOR) val colorAnchor = colorAction ?: Rect(80, 80, 20, 18) val openedByClick = if (colorAction != null) { @@ -215,9 +217,9 @@ class SystemOverlayInspectorNativeEntryTests { inspectorPointerCaptured = false, ) assertTrue(host.isSystemColorPickerOpen()) - assertEquals(OverlayOwnerScope.System, host.debugSystemColorPickerPopupOwnerScope()) + assertEquals(ScreenDomainId.System, host.debugSystemColorPickerPopupOwnerDomain()) - val pickerNode = host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) ?: error("picker node missing") + val pickerNode = host.debugEntryNode(SystemPortalEntryId.ColorPickerPopup) ?: error("picker node missing") val pickerStyles = collectStyleTypes(pickerNode) assertTrue(pickerStyles.contains("dsgl-system-color-picker-native-body")) assertFalse(pickerStyles.contains("dsgl-system-raw-render-command")) @@ -226,7 +228,7 @@ class SystemOverlayInspectorNativeEntryTests { @Test fun `pick selection resolves from latest synced tree before render`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) val root = inspectedRoot() inspector.toggle() @@ -246,7 +248,7 @@ class SystemOverlayInspectorNativeEntryTests { @Test fun `inspector minimize restore and close reopen remain stable`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot() @@ -255,8 +257,8 @@ class SystemOverlayInspectorNativeEntryTests { host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 40, cursorY = 30, inspectorPointerCaptured = false) host.render(ctx, 1280, 720) - val initialNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector node missing") - val minimizeRect = inspector.overlayMinimizeBounds() ?: error("minimize bounds missing") + val initialNode = host.debugEntryNode(SystemPortalEntryId.Inspector) ?: error("inspector node missing") + val minimizeRect = inspector.portalMinimizeBounds() ?: error("minimize bounds missing") assertTrue(host.handleMouseDown(minimizeRect.x + 1, minimizeRect.y + 1, MouseButton.LEFT)) assertTrue(host.handleMouseUp(minimizeRect.x + 1, minimizeRect.y + 1, MouseButton.LEFT)) assertEquals(InspectorPanelState.Minimized, inspector.panelState) @@ -271,19 +273,19 @@ class SystemOverlayInspectorNativeEntryTests { inspector.deactivate() host.syncFrame(root, inspectedLayoutRevision = 3L, cursorX = 40, cursorY = 30, inspectorPointerCaptured = false) - assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.Inspector)) + assertFalse(host.debugMountedEntryIds().contains(SystemPortalEntryId.Inspector)) inspector.toggle() host.syncFrame(root, inspectedLayoutRevision = 4L, cursorX = 40, cursorY = 30, inspectorPointerCaptured = false) host.render(ctx, 1280, 720) - val reopenedNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector node missing") + val reopenedNode = host.debugEntryNode(SystemPortalEntryId.Inspector) ?: error("inspector node missing") assertSame(initialNode, reopenedNode) } @Test fun `minimized inspector hides expanded panel host and keeps chip visible`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) val root = inspectedRoot() inspector.toggle() @@ -291,7 +293,7 @@ class SystemOverlayInspectorNativeEntryTests { host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 40, cursorY = 30, inspectorPointerCaptured = false) host.render(ctx, 1280, 720) - val minimizeRect = inspector.overlayMinimizeBounds() ?: error("minimize bounds missing") + val minimizeRect = inspector.portalMinimizeBounds() ?: error("minimize bounds missing") assertTrue(host.handleMouseDown(minimizeRect.x + 2, minimizeRect.y + 2, MouseButton.LEFT)) assertTrue(host.handleMouseUp(minimizeRect.x + 2, minimizeRect.y + 2, MouseButton.LEFT)) assertEquals(InspectorPanelState.Minimized, inspector.panelState) @@ -299,10 +301,10 @@ class SystemOverlayInspectorNativeEntryTests { host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 48, cursorY = 36, inspectorPointerCaptured = false) host.render(ctx, 1280, 720) - val inspectorNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector node missing") + val inspectorNode = host.debugEntryNode(SystemPortalEntryId.Inspector) ?: error("inspector node missing") val panelHostNode = collectNodes(inspectorNode).firstOrNull { node -> - (node.key?.toString() ?: "").startsWith("dsgl-overlay-panel-") + (node.key?.toString() ?: "").startsWith("dsgl-floating-panel-") } ?: error("panel host node missing") val minimizedChipNode = collectNodes(inspectorNode).firstOrNull { it.key == "dsgl-system-inspector-chip" } @@ -317,7 +319,7 @@ class SystemOverlayInspectorNativeEntryTests { @Test fun `minimized drag moves chip and releases pointer capture on mouse up`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) val root = inspectedRoot() inspector.toggle() @@ -325,7 +327,7 @@ class SystemOverlayInspectorNativeEntryTests { host.syncFrame(root, inspectedLayoutRevision = 1L, cursorX = 40, cursorY = 30, inspectorPointerCaptured = false) host.render(ctx, 1280, 720) - val minimizeRect = inspector.overlayMinimizeBounds() ?: error("minimize bounds missing") + val minimizeRect = inspector.portalMinimizeBounds() ?: error("minimize bounds missing") assertTrue(host.handleMouseDown(minimizeRect.x + 2, minimizeRect.y + 2, MouseButton.LEFT)) assertTrue(host.handleMouseUp(minimizeRect.x + 2, minimizeRect.y + 2, MouseButton.LEFT)) assertEquals(InspectorPanelState.Minimized, inspector.panelState) @@ -362,7 +364,7 @@ class SystemOverlayInspectorNativeEntryTests { @Test fun `inspector native path preserves scroll and scrollbar drag behavior`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() @@ -382,7 +384,7 @@ class SystemOverlayInspectorNativeEntryTests { host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 90, cursorY = 90, inspectorPointerCaptured = false) host.render(ctx, 420, 280) - val contentRect = inspector.overlayContentRect() + val contentRect = inspector.portalContentRect() val wheelX = contentRect.x + 4 val wheelY = contentRect.y + 12 assertTrue(host.handleMouseWheel(wheelX, wheelY, -120)) @@ -399,7 +401,7 @@ class SystemOverlayInspectorNativeEntryTests { val afterWheel = inspector.panelScrollOffsetY assertTrue(afterWheel > 0, "expected wheel scroll > 0, actual=$afterWheel") - val thumb = inspector.overlayScrollbarThumbRect() + val thumb = inspector.portalScrollbarThumbRect() assertTrue(thumb.width > 0 && thumb.height > 0) val thumbX = thumb.x + 1 val thumbY = thumb.y + thumb.height / 2 @@ -412,7 +414,7 @@ class SystemOverlayInspectorNativeEntryTests { @Test fun `scrollbar drag release over control ends capture and does not trigger control click`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() @@ -433,9 +435,9 @@ class SystemOverlayInspectorNativeEntryTests { host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 90, cursorY = 90, inspectorPointerCaptured = false) host.render(ctx, 420, 280) - val thumb = inspector.overlayScrollbarThumbRect() + val thumb = inspector.portalScrollbarThumbRect() assertTrue(thumb.width > 0 && thumb.height > 0) - val pickToggle = inspector.overlayPickToggleBounds() ?: error("pick toggle missing") + val pickToggle = inspector.portalPickToggleBounds() ?: error("pick toggle missing") val modeBeforeRelease = inspector.mode val thumbX = thumb.x + thumb.width / 2 @@ -465,7 +467,7 @@ class SystemOverlayInspectorNativeEntryTests { @Test fun `scrollbar drag release outside inspector consumes mouse up and stops capture`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() @@ -486,9 +488,9 @@ class SystemOverlayInspectorNativeEntryTests { host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 90, cursorY = 90, inspectorPointerCaptured = false) host.render(ctx, 420, 280) - val thumb = inspector.overlayScrollbarThumbRect() + val thumb = inspector.portalScrollbarThumbRect() assertTrue(thumb.width > 0 && thumb.height > 0) - val panelRect = inspector.overlayPanelRect() ?: error("panel rect missing") + val panelRect = inspector.floatingPanelRect() ?: error("panel rect missing") val modeBeforeRelease = inspector.mode val candidatePoints = @@ -529,7 +531,7 @@ class SystemOverlayInspectorNativeEntryTests { fun `inspector color edit ownership stays system-owned and independent from app runtime popup`() { val appOwner = Any() val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot() @@ -537,7 +539,7 @@ class SystemOverlayInspectorNativeEntryTests { DomainPortalServices.applicationColorPickerEngine.open( ColorPickerPopupRequest( owner = appOwner, - ownerScope = OverlayOwnerScope.Application, + ownerDomain = ScreenDomainId.Application, anchorRect = Rect(240, 210, 20, 18), title = "App Popup", state = popupState(), @@ -565,7 +567,7 @@ class SystemOverlayInspectorNativeEntryTests { inspectorPointerCaptured = false, ) host.render(ctx, 1280, 720) - val colorAction = inspector.overlayColorPickerActionBounds(StyleProperty.BACKGROUND_COLOR) + val colorAction = inspector.portalColorPickerActionBounds(StyleProperty.BACKGROUND_COLOR) val colorAnchor = colorAction ?: Rect(80, 80, 20, 18) val openedByClick = if (colorAction != null) { @@ -586,7 +588,7 @@ class SystemOverlayInspectorNativeEntryTests { ) assertTrue(host.isSystemColorPickerOpen()) - assertEquals(OverlayOwnerScope.System, host.debugSystemColorPickerPopupOwnerScope()) + assertEquals(ScreenDomainId.System, host.debugSystemColorPickerPopupOwnerDomain()) assertTrue(DomainPortalServices.applicationColorPickerEngine.isOpenFor(appOwner)) host.systemInspectorColorPickerService().close() @@ -608,7 +610,7 @@ class SystemOverlayInspectorNativeEntryTests { @Test fun `inspector-opened system color picker top controls expose hover feedback`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot() @@ -630,7 +632,7 @@ class SystemOverlayInspectorNativeEntryTests { assertEquals("target", inspector.selectedKey) sync(revision = 2L, cursorX = 80, cursorY = 52) - val colorAction = inspector.overlayColorPickerActionBounds(StyleProperty.BACKGROUND_COLOR) + val colorAction = inspector.portalColorPickerActionBounds(StyleProperty.BACKGROUND_COLOR) val colorAnchor = colorAction ?: Rect(80, 80, 20, 18) val openedByClick = if (colorAction != null) { @@ -644,7 +646,7 @@ class SystemOverlayInspectorNativeEntryTests { } sync(revision = 3L, cursorX = colorAnchor.x + 1, cursorY = colorAnchor.y + 1) assertTrue(host.isSystemColorPickerOpen()) - assertEquals(OverlayOwnerScope.System, host.debugSystemColorPickerPopupOwnerScope()) + assertEquals(ScreenDomainId.System, host.debugSystemColorPickerPopupOwnerDomain()) val layout = host.debugSystemColorPickerBodyLayout() ?: error("color picker body layout missing") val style = ColorPickerStyle() @@ -665,7 +667,7 @@ class SystemOverlayInspectorNativeEntryTests { sync(revision = revision++, cursorX = hoverX, cursorY = hoverY) val pickerNode = - host.debugEntryNode(SystemOverlayEntryId.ColorPickerPopup) + host.debugEntryNode(SystemPortalEntryId.ColorPickerPopup) ?: error("color picker entry missing") val buttonNode = collectNodes(pickerNode) @@ -678,7 +680,7 @@ class SystemOverlayInspectorNativeEntryTests { @Test fun `inspector-opened system color picker mode dropdown options hover and click reliably`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot() @@ -701,7 +703,7 @@ class SystemOverlayInspectorNativeEntryTests { val modeBeforeDropdown = inspector.mode sync(revision = 2L, cursorX = 80, cursorY = 52) - val colorAction = inspector.overlayColorPickerActionBounds(StyleProperty.BACKGROUND_COLOR) + val colorAction = inspector.portalColorPickerActionBounds(StyleProperty.BACKGROUND_COLOR) val colorAnchor = colorAction ?: Rect(80, 80, 20, 18) val openedByClick = if (colorAction != null) { @@ -715,7 +717,7 @@ class SystemOverlayInspectorNativeEntryTests { } sync(revision = 3L, cursorX = colorAnchor.x + 1, cursorY = colorAnchor.y + 1) assertTrue(host.isSystemColorPickerOpen()) - assertEquals(OverlayOwnerScope.System, host.debugSystemColorPickerPopupOwnerScope()) + assertEquals(ScreenDomainId.System, host.debugSystemColorPickerPopupOwnerDomain()) val initialLayout = host.debugSystemColorPickerBodyLayout() ?: error("color picker body layout missing") assertTrue( @@ -730,7 +732,7 @@ class SystemOverlayInspectorNativeEntryTests { cursorX = initialLayout.modeSelectRect.x + 2, cursorY = initialLayout.modeSelectRect.y + 2, ) - assertTrue(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerTransient)) + assertTrue(host.debugMountedEntryIds().contains(SystemPortalEntryId.ColorPickerTransient)) val expandedLayout = host.debugSystemColorPickerBodyLayout() ?: error("expanded color picker layout missing") val hslOption = @@ -743,7 +745,7 @@ class SystemOverlayInspectorNativeEntryTests { val style = ColorPickerStyle() val transientNode = - host.debugEntryNode(SystemOverlayEntryId.ColorPickerTransient) + host.debugEntryNode(SystemPortalEntryId.ColorPickerTransient) ?: error("transient entry missing") val optionNode = collectNodes(transientNode) @@ -756,7 +758,7 @@ class SystemOverlayInspectorNativeEntryTests { sync(revision = 6L, cursorX = optionHoverX, cursorY = optionHoverY) assertEquals(ColorFormatMode.HSL, host.debugSystemColorPickerState()?.mode) - assertFalse(host.debugMountedEntryIds().contains(SystemOverlayEntryId.ColorPickerTransient)) + assertFalse(host.debugMountedEntryIds().contains(SystemPortalEntryId.ColorPickerTransient)) assertTrue(host.isSystemColorPickerOpen()) assertEquals("target", inspector.selectedKey) assertEquals(modeBeforeDropdown, inspector.mode) @@ -765,7 +767,7 @@ class SystemOverlayInspectorNativeEntryTests { @Test fun `inspector native body content remains clipped in narrow viewport`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() @@ -785,8 +787,8 @@ class SystemOverlayInspectorNativeEntryTests { host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 90, cursorY = 90, inspectorPointerCaptured = false) host.render(ctx, 320, 220) - val bodyRect = inspector.overlayContentRect() - val inspectorNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector node missing") + val bodyRect = inspector.portalContentRect() + val inspectorNode = host.debugEntryNode(SystemPortalEntryId.Inspector) ?: error("inspector node missing") val bodyNode = collectNodes(inspectorNode) .firstOrNull { it.key?.toString() == "dsgl-system-inspector-body" } @@ -853,7 +855,7 @@ class SystemOverlayInspectorNativeEntryTests { @Test fun `inspector expanded body renders baseline info text`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot() @@ -869,7 +871,7 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 1280, 720) val commands = host.paint(ctx) - val bodyRect = inspector.overlayContentRect() + val bodyRect = inspector.portalContentRect() assertTrue(bodyRect.width > 0 && bodyRect.height > 0) val baselineInfoRendered = commands.any { command -> @@ -886,7 +888,7 @@ class SystemOverlayInspectorNativeEntryTests { @Test fun `inspector clipped body blocks hidden row input and accepts visible portion`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() @@ -907,7 +909,7 @@ class SystemOverlayInspectorNativeEntryTests { host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 90, cursorY = 90, inspectorPointerCaptured = false) host.render(ctx, 320, 213) - val bodyRect = inspector.overlayContentRect() + val bodyRect = inspector.portalContentRect() val wheelX = bodyRect.x + 4 val wheelY = bodyRect.y + 12 @@ -917,7 +919,7 @@ class SystemOverlayInspectorNativeEntryTests { var visibleNode: DOMNode? = null var latestInteractiveNodes: List = emptyList() repeat(24) { - val inspectorNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector node missing") + val inspectorNode = host.debugEntryNode(SystemPortalEntryId.Inspector) ?: error("inspector node missing") val interactiveNodes = collectNodes(inspectorNode).filter { node -> if (node.display == Display.None) return@filter false @@ -992,7 +994,7 @@ class SystemOverlayInspectorNativeEntryTests { @Test fun `inspector body consumes generic scroll viewport and content state`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() @@ -1012,7 +1014,7 @@ class SystemOverlayInspectorNativeEntryTests { host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 90, cursorY = 90, inspectorPointerCaptured = false) host.render(ctx, 420, 280) - val inspectorNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector node missing") + val inspectorNode = host.debugEntryNode(SystemPortalEntryId.Inspector) ?: error("inspector node missing") val bodyNode = collectNodes(inspectorNode).firstOrNull { it.key == "dsgl-system-inspector-body" } ?: error("inspector body node missing") @@ -1038,7 +1040,7 @@ class SystemOverlayInspectorNativeEntryTests { @Test fun `inspector wheel scrolling works when hovering interactive input`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() @@ -1059,8 +1061,8 @@ class SystemOverlayInspectorNativeEntryTests { host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 90, cursorY = 90, inspectorPointerCaptured = false) host.render(ctx, 420, 280) - val inspectorNode = host.debugEntryNode(SystemOverlayEntryId.Inspector) ?: error("inspector node missing") - val bodyRect = inspector.overlayContentRect() + val inspectorNode = host.debugEntryNode(SystemPortalEntryId.Inspector) ?: error("inspector node missing") + val bodyRect = inspector.portalContentRect() val allNodes = collectNodes(inspectorNode) val interactiveNode = allNodes.firstOrNull { node -> @@ -1102,7 +1104,7 @@ class SystemOverlayInspectorNativeEntryTests { @Test fun `inspector shift wheel does not consume vertical wheel path`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() @@ -1122,7 +1124,7 @@ class SystemOverlayInspectorNativeEntryTests { host.syncFrame(root, inspectedLayoutRevision = 2L, cursorX = 90, cursorY = 90, inspectorPointerCaptured = false) host.render(ctx, 420, 280) - val bodyRect = inspector.overlayContentRect() + val bodyRect = inspector.portalContentRect() val wheelX = bodyRect.x + 4 val wheelY = bodyRect.y + 12 val before = inspector.panelScrollOffsetY @@ -1145,7 +1147,7 @@ class SystemOverlayInspectorNativeEntryTests { @Test fun `inspector wheel scrolling remains symmetric across rebuilds`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() @@ -1167,7 +1169,7 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 420, 280) host.paint(ctx) - val contentRect = inspector.overlayContentRect() + val contentRect = inspector.portalContentRect() val wheelX = contentRect.x + 4 val wheelY = contentRect.y + 12 @@ -1235,7 +1237,7 @@ class SystemOverlayInspectorNativeEntryTests { @Test fun `inspector thumb drag remains active across rebuild without controller pointer capture`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() @@ -1257,7 +1259,7 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 420, 280) host.paint(ctx) - val thumb = inspector.overlayScrollbarThumbRect() + val thumb = inspector.portalScrollbarThumbRect() assertTrue(thumb.width > 0 && thumb.height > 0) val dragX = thumb.x + thumb.width / 2 val dragStartY = thumb.y + thumb.height / 2 @@ -1308,7 +1310,7 @@ class SystemOverlayInspectorNativeEntryTests { StyleEngine.forceReloadStylesheets() val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRoot() @@ -1440,7 +1442,7 @@ class SystemOverlayInspectorNativeEntryTests { @Test fun `inspector consumer scroll reacts on frame update without viewport resize`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() @@ -1462,7 +1464,7 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 420, 280) host.paint(ctx) - val contentRect = inspector.overlayContentRect() + val contentRect = inspector.portalContentRect() val wheelX = contentRect.x + 4 val wheelY = contentRect.y + 14 val before = inspector.panelScrollOffsetY @@ -1484,7 +1486,7 @@ class SystemOverlayInspectorNativeEntryTests { @Test fun `inspector consumer thumb drag remains smooth and stable on release`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() @@ -1506,14 +1508,14 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 420, 280) host.paint(ctx) - val thumb = inspector.overlayScrollbarThumbRect() + val thumb = inspector.portalScrollbarThumbRect() assertTrue(thumb.width > 0 && thumb.height > 0) val dragX = thumb.x + thumb.width / 2 val startY = thumb.y + thumb.height / 2 assertTrue(host.handleMouseDown(dragX, startY, MouseButton.LEFT)) var previousScroll = inspector.panelScrollOffsetY - var previousThumbY = inspector.overlayScrollbarThumbRect().y + var previousThumbY = inspector.portalScrollbarThumbRect().y repeat(6) { step -> val nextY = startY + (step + 1) * 9 @@ -1528,7 +1530,7 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 420, 280) host.paint(ctx) val currentScroll = inspector.panelScrollOffsetY - val currentThumbY = inspector.overlayScrollbarThumbRect().y + val currentThumbY = inspector.portalScrollbarThumbRect().y assertTrue( currentScroll >= previousScroll, "scroll regressed: prev=$previousScroll current=$currentScroll step=$step", @@ -1543,7 +1545,7 @@ class SystemOverlayInspectorNativeEntryTests { assertTrue(host.handleMouseUp(dragX, startY + 6 * 9, MouseButton.LEFT)) val settledScroll = inspector.panelScrollOffsetY - val settledThumbY = inspector.overlayScrollbarThumbRect().y + val settledThumbY = inspector.portalScrollbarThumbRect().y repeat(6) { idx -> host.syncFrame( @@ -1556,14 +1558,14 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 420, 280) host.paint(ctx) assertEquals(settledScroll, inspector.panelScrollOffsetY) - assertEquals(settledThumbY, inspector.overlayScrollbarThumbRect().y) + assertEquals(settledThumbY, inspector.portalScrollbarThumbRect().y) } } @Test fun `inspector consumer fast thumb drag to boundary stays stable`() { val inspector = InspectorController() - val host = SystemOverlayHost(inspector) + val host = SystemPortalHost(inspector) inspector.installColorPickerPortalService(host.systemInspectorColorPickerService()) val root = inspectedRootWithManyChildren() @@ -1585,14 +1587,14 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 420, 280) host.paint(ctx) - val thumb = inspector.overlayScrollbarThumbRect() + val thumb = inspector.portalScrollbarThumbRect() assertTrue(thumb.width > 0 && thumb.height > 0) val dragX = thumb.x + thumb.width / 2 val startY = thumb.y + thumb.height / 2 assertTrue(host.handleMouseDown(dragX, startY, MouseButton.LEFT)) var previousScroll = inspector.panelScrollOffsetY - var previousThumbY = inspector.overlayScrollbarThumbRect().y + var previousThumbY = inspector.portalScrollbarThumbRect().y repeat(7) { step -> val nextY = startY + (step + 1) * 120 assertTrue(host.handleMouseMove(dragX, nextY)) @@ -1606,7 +1608,7 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 420, 280) host.paint(ctx) val currentScroll = inspector.panelScrollOffsetY - val currentThumbY = inspector.overlayScrollbarThumbRect().y + val currentThumbY = inspector.portalScrollbarThumbRect().y assertTrue( currentScroll >= previousScroll, "scroll regressed: prev=$previousScroll current=$currentScroll step=$step", @@ -1620,7 +1622,7 @@ class SystemOverlayInspectorNativeEntryTests { } val settledScroll = inspector.panelScrollOffsetY - val settledThumbY = inspector.overlayScrollbarThumbRect().y + val settledThumbY = inspector.portalScrollbarThumbRect().y repeat(8) { idx -> val boundaryY = startY + 2000 assertTrue(host.handleMouseMove(dragX, boundaryY)) @@ -1634,7 +1636,7 @@ class SystemOverlayInspectorNativeEntryTests { host.render(ctx, 420, 280) host.paint(ctx) assertEquals(settledScroll, inspector.panelScrollOffsetY) - assertEquals(settledThumbY, inspector.overlayScrollbarThumbRect().y) + assertEquals(settledThumbY, inspector.portalScrollbarThumbRect().y) } assertTrue(host.handleMouseUp(dragX, startY + 2000, MouseButton.LEFT)) diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayStyleIsolationTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalStyleIsolationTests.kt similarity index 83% rename from core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayStyleIsolationTests.kt rename to core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalStyleIsolationTests.kt index b19e74c..216d310 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/overlay/system/SystemOverlayStyleIsolationTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/portal/system/SystemPortalStyleIsolationTests.kt @@ -1,4 +1,4 @@ -package org.dreamfinity.dsgl.core.overlay.system +package org.dreamfinity.dsgl.core.portal.system import org.dreamfinity.dsgl.core.DsglColors import org.dreamfinity.dsgl.core.dom.DOMNode @@ -7,7 +7,7 @@ import org.dreamfinity.dsgl.core.dom.elements.ContainerNode import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.Size import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext -import org.dreamfinity.dsgl.core.overlay.ApplicationOverlayRootNode +import org.dreamfinity.dsgl.core.portal.ApplicationPortalRootNode import org.dreamfinity.dsgl.core.render.RenderCommand import org.dreamfinity.dsgl.core.style.StyleApplicationScope import org.dreamfinity.dsgl.core.style.StyleEngine @@ -17,7 +17,7 @@ import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals -class SystemOverlayStyleIsolationTests { +class SystemPortalStyleIsolationTests { @AfterTest fun cleanup() { StyleEngine.setStylesDirectory(null) @@ -26,7 +26,7 @@ class SystemOverlayStyleIsolationTests { } @Test - fun `system overlay scope ignores user stylesheet rules`() { + fun `system portal scope ignores user stylesheet rules`() { val stylesDir = createTempStylesDir( """ @@ -43,16 +43,16 @@ class SystemOverlayStyleIsolationTests { addClass("app") } val appProbe = ProbeNode(key = "app-probe").applyParent(appRoot) - val appOverlayRoot = ApplicationOverlayRootNode() - val appOverlayProbe = ProbeNode(key = "app-overlay-probe").applyParent(appOverlayRoot) + val appPortalRoot = ApplicationPortalRootNode() + val appPortalProbe = ProbeNode(key = "app-portal-probe").applyParent(appPortalRoot) StyleEngine.applyStylesRecursively(appRoot, StyleApplicationScope.Application) - StyleEngine.applyStylesRecursively(appOverlayRoot, StyleApplicationScope.Application) + StyleEngine.applyStylesRecursively(appPortalRoot, StyleApplicationScope.Application) assertEquals(0xFF1133DD.toInt(), appProbe.appliedColor) - assertEquals(0xFF00CCAA.toInt(), appOverlayProbe.appliedColor) + assertEquals(0xFF00CCAA.toInt(), appPortalProbe.appliedColor) - val systemRoot = SystemOverlayRootNode() + val systemRoot = SystemPortalRootNode() val systemProbe = ProbeNode(key = "system-probe").applyParent(systemRoot) - StyleEngine.applyStylesRecursively(systemRoot, StyleApplicationScope.SystemOverlay) + StyleEngine.applyStylesRecursively(systemRoot, StyleApplicationScope.System) assertEquals(DsglColors.TEXT, systemProbe.appliedColor) stylesDir.resolve("test.dss").writeText( @@ -63,10 +63,10 @@ class SystemOverlayStyleIsolationTests { ) StyleEngine.forceReloadStylesheets() StyleEngine.applyStylesRecursively(appRoot, StyleApplicationScope.Application) - StyleEngine.applyStylesRecursively(appOverlayRoot, StyleApplicationScope.Application) - StyleEngine.applyStylesRecursively(systemRoot, StyleApplicationScope.SystemOverlay) + StyleEngine.applyStylesRecursively(appPortalRoot, StyleApplicationScope.Application) + StyleEngine.applyStylesRecursively(systemRoot, StyleApplicationScope.System) assertEquals(0xFFAA22EE.toInt(), appProbe.appliedColor) - assertEquals(0xFFAA22EE.toInt(), appOverlayProbe.appliedColor) + assertEquals(0xFFAA22EE.toInt(), appPortalProbe.appliedColor) assertEquals(DsglColors.TEXT, systemProbe.appliedColor) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/DomainPortalServicesOwnershipTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/DomainPortalServicesOwnershipTests.kt index 9fdcc34..033508f 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/DomainPortalServicesOwnershipTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/DomainPortalServicesOwnershipTests.kt @@ -1,8 +1,8 @@ package org.dreamfinity.dsgl.core.select import org.dreamfinity.dsgl.core.dom.layout.Rect -import org.dreamfinity.dsgl.core.overlay.DomainPortalServices -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope +import org.dreamfinity.dsgl.core.portal.DomainPortalServices +import org.dreamfinity.dsgl.core.portal.ScreenDomainId import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertFalse @@ -17,7 +17,7 @@ class DomainPortalServicesOwnershipTests { @Test fun `application-scoped request opens application engine only`() { val owner = Any() - DomainPortalServices.openSelect(request(owner, OverlayOwnerScope.Application)) + DomainPortalServices.openSelect(request(owner, ScreenDomainId.Application)) assertTrue(DomainPortalServices.applicationSelectEngine.isOpenFor(owner)) assertFalse(DomainPortalServices.systemSelectEngine.isOpenFor(owner)) @@ -27,7 +27,7 @@ class DomainPortalServicesOwnershipTests { @Test fun `system-scoped request opens system engine only`() { val owner = Any() - DomainPortalServices.openSelect(request(owner, OverlayOwnerScope.System)) + DomainPortalServices.openSelect(request(owner, ScreenDomainId.System)) assertFalse(DomainPortalServices.applicationSelectEngine.isOpenFor(owner)) assertTrue(DomainPortalServices.systemSelectEngine.isOpenFor(owner)) @@ -37,16 +37,16 @@ class DomainPortalServicesOwnershipTests { @Test fun `opening same owner in another scope switches engine ownership`() { val owner = Any() - DomainPortalServices.openSelect(request(owner, OverlayOwnerScope.Application)) + DomainPortalServices.openSelect(request(owner, ScreenDomainId.Application)) assertTrue(DomainPortalServices.applicationSelectEngine.isOpenFor(owner)) assertFalse(DomainPortalServices.systemSelectEngine.isOpenFor(owner)) - DomainPortalServices.openSelect(request(owner, OverlayOwnerScope.System)) + DomainPortalServices.openSelect(request(owner, ScreenDomainId.System)) assertFalse(DomainPortalServices.applicationSelectEngine.isOpenFor(owner)) assertTrue(DomainPortalServices.systemSelectEngine.isOpenFor(owner)) } - private fun request(owner: Any, scope: OverlayOwnerScope): SelectOpenRequest = + private fun request(owner: Any, scope: ScreenDomainId): SelectOpenRequest = SelectOpenRequest( owner = owner, modelToken = 1L, @@ -54,6 +54,6 @@ class DomainPortalServicesOwnershipTests { selectedId = "a", anchorRect = Rect(10, 10, 100, 20), closeOnSelect = true, - ownerScope = scope, + ownerDomain = scope, ) } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectEngineTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectEngineTests.kt index 3677ce8..7305ecf 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectEngineTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectEngineTests.kt @@ -279,8 +279,8 @@ class SelectEngineTests { } @Test - fun `overlay consumes pointer before base dispatch when select is open`() { - val owner = "select.overlay.order" + fun `portal popup consumes pointer before base dispatch when select is open`() { + val owner = "select.portal.order" val engine = SelectEngine() val model = selectModel { @@ -368,7 +368,7 @@ class SelectEngineTests { ), ) val overflowCommands = mutableListOf() - overflowEngine.appendOverlayCommands(ctx, 240, 120, overflowCommands) + overflowEngine.appendPortalCommands(ctx, 240, 120, overflowCommands) assertTrue(overflowCommands.any { it is RenderCommand.DrawRect && it.color == trackColor }) assertTrue(overflowCommands.any { it is RenderCommand.DrawRect && it.color == thumbColor }) val pushClips = overflowCommands.count { it is RenderCommand.PushClip } @@ -402,7 +402,7 @@ class SelectEngineTests { ), ) val noOverflowCommands = mutableListOf() - noOverflowEngine.appendOverlayCommands(ctx, 240, 200, noOverflowCommands) + noOverflowEngine.appendPortalCommands(ctx, 240, 200, noOverflowCommands) assertFalse(noOverflowCommands.any { it is RenderCommand.DrawRect && it.color == trackColor }) assertFalse(noOverflowCommands.any { it is RenderCommand.DrawRect && it.color == thumbColor }) val pushClipsNoOverflow = noOverflowCommands.count { it is RenderCommand.PushClip } diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalControllerTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalControllerTests.kt index bcc75a2..795ad8d 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalControllerTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/select/SelectPortalControllerTests.kt @@ -3,10 +3,10 @@ package org.dreamfinity.dsgl.core.select import org.dreamfinity.dsgl.core.dom.layout.Rect import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext import org.dreamfinity.dsgl.core.event.MouseButton -import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope -import org.dreamfinity.dsgl.core.overlay.PortalDismissPolicy -import org.dreamfinity.dsgl.core.overlay.PortalInputPolicy -import org.dreamfinity.dsgl.core.overlay.PortalPointerRegion +import org.dreamfinity.dsgl.core.portal.PortalDismissPolicy +import org.dreamfinity.dsgl.core.portal.PortalInputPolicy +import org.dreamfinity.dsgl.core.portal.PortalPointerRegion +import org.dreamfinity.dsgl.core.portal.ScreenDomainId import org.dreamfinity.dsgl.core.render.RenderCommand import kotlin.test.Test import kotlin.test.assertEquals @@ -236,7 +236,7 @@ class SelectPortalControllerTests { val controller = SelectPortalController( engine = engine, - ownerScope = OverlayOwnerScope.Application, + ownerDomain = ScreenDomainId.Application, entryId = "test.select", ) val model = From 09aea7aa40e447dcfc39c7811dd98bf38a8ad208 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Fri, 5 Jun 2026 02:30:31 +0300 Subject: [PATCH 77/78] updating docs according to a new changes; --- docs/architecture.md | 171 ++++++++++++++++++++++++++++++++++--- docs/custom-components.md | 4 +- docs/elements-overview.md | 15 ++-- docs/hooks.md | 2 +- docs/index.md | 3 +- docs/layout-model.md | 10 ++- docs/project-structure.md | 8 +- docs/quickstart.md | 6 +- docs/status-limitations.md | 19 +++-- docs/troubleshooting.md | 15 ++-- 10 files changed, 207 insertions(+), 46 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 1056311..1c58948 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -51,8 +51,9 @@ require it. Style scope is explicit: -- application/root/app-overlay trees use application scope -- system overlay uses system scope +- Application root and Application portal trees use application scope +- System root and System portal trees use system scope +- Debug root and Debug portal trees use debug scope ## Layout and paint responsibilities @@ -85,19 +86,165 @@ This is the basis for DSGL effect semantics (commit-bound effects, discard-safe When hot-reload mode is active, hook signature mismatches can request subtree remount/reset instead of crashing the entire rebuild. -## Overlay model +## Screen domain model -DSGL composes layers, not separate rendering universes: +DSGL screens are composed from domain surfaces. The authoritative surface contract is `ScreenDomainSurfaces`. -- application root -- application overlay -- system overlay -- debug layer +The screen domains are: -Paint order and input priority are explicit in `OverlayLayerContracts`. +- Application +- System +- Debug -You usually treat overlays as host-managed runtime surfaces. Public helper APIs (modal/select/context-menu/color picker, -etc.) sit on top of this model; low-level overlay internals are not a stable app extension API. +Every domain has the same pair of surfaces: + +- root +- portal + +Empty roots and empty portals are valid. System root and Debug portal do not require special mechanics just because some +screens currently have no content there. The current Debug pane is mounted as Debug root DOM, and Debug has a real portal +host even when no Debug portal entries are active. + +## Domain surface ordering contract + +`ScreenDomainSurfaces` defines the render and input ordering: + +- paint order: `Application root -> Application portal -> System root -> System portal -> Debug root -> Debug portal` +- input priority: `Debug portal -> Debug root -> System portal -> System root -> Application portal -> Application root` + +Any future refactor must preserve these orders unless a deliberate architecture change is explicitly approved. + +## Domain surface ownership contract + +### `DsglScreenHost` owns + +- adapter/backend lifecycle integration +- frame orchestration across domain surfaces +- rebuild/layout/paint guard rails and fallback behaviour +- top-level domain-surface composition and contract-ordered input routing +- global cleanup across host-managed portal services on close + +`DsglScreenHost` is the current screen-level orchestrator. + +### Domain surface hosts own + +- a specific `ScreenDomainSurface` +- viewport-local lifecycle for that surface +- render/paint/input forwarding for that surface +- cleanup of state mounted through that surface + +Screen/runtime ownership classes now use domain/portal terminology, and their runtime contract is domain-surface-first +through `DomainSurfaceHost`. + +### Portal hosts own + +- portal entry registration +- active entry paint/input order +- entry lifecycle cleanup +- validation that entries are mounted into their owning domain portal surface +- generic portal-entry policy metadata and evaluation for dismiss, backdrop consumption, focus intent, lifecycle, + placement, and protected inside regions + +Portal hosts are physical mount points for floating UI in their owning domain. They are not separate domains or hidden +widget runtimes. + +Portal entry policies are generic. Component-specific behavior remains in the mounted portal DOM subtree or in temporary +helper engines during migration. Outside-pointer policy is evaluated topmost-first against active portal entries. Inside +entry interaction is determined from the entry DOM, entry bounds, or explicit protected bounds; it must not depend on +widget type. Once a portal DOM tree is selected for input, events bubble inside that physical portal tree only, not into +the owning domain root DOM or another domain. + +## Shared domain mechanics + +The following mechanics should converge into common expectations across all domains: + +- explicit per-frame phases (`input frame prep -> sync -> render -> paint -> clear refs`) +- explicit surface input enable/disable gating +- predictable DOM and policy-based input routing for eligible surface content +- screen/domain-aware keyboard targeting: key-down and key-up target the one focused node only when that node belongs to + the selected physical domain surface tree +- shared DOM-node pointer capture bookkeeping for root and portal DOM dispatch (`PointerCaptureSession`): captured + nodes continue receiving move/release/cancel until capture ends, and keyed captures can restore across + retained-DOM reconciliation +- stable portal-entry ownership (`id`, active/open state, placement/drag session ownership where applicable) +- explicit viewport/bounds/coordinate handling with no hidden fallback geometry assumptions + +## Domain-specific ownership + +- Application root and Application portal share application styling semantics. +- System root and System portal are isolated from application styling. +- Debug root and Debug portal are isolated from application styling. +- Domain-specific behaviour stays in its owning domain or portal service. + +## Intentional distinctions vs accidental gaps + +Intentional distinctions: + +- split between Application, System, and Debug ownership +- split between each domain root and portal surface +- scope-driven ownership for transient portal routing +- explicit Debug domain ownership for debug-only diagnostics and controls + +Remaining explicit limits: + +- `DsglScreenHost` still owns top-level pointer, hover, and input routing between domain surfaces. DOM-node pointer + capture bookkeeping is shared between root and portal DOM dispatch. Application-owned DnD drag ghosts now paint through + an Application portal entry while preserving the existing DnD session model. +- implementation names that describe screen/runtime ownership now use domain/portal terminology. +- system Inspector/color-picker entries still use system-specific manual dispatch plus DOM fallback where text editing + and panel dragging require it. +- modal focus/session state is still modal-specific through `ModalPortalSessionStore`, but modal focus requests are + scoped to the mounted modal portal root where available. + +## Non-regression invariants for domain refactors + +- preserve domain-surface ordering for both paint and input +- preserve ownership separation between Application, System, and Debug domains +- preserve application/system/debug scope separation and style isolation boundaries +- preserve inspector drag behaviour and pointer-capture semantics +- preserve transient portal ownership as owner-token/session based, not cursor-derived ownership +- preserve anti-click-through behaviour: once a higher-priority surface consumes input, lower surfaces do not receive it +- preserve pointer-sequence ownership: when a higher-priority surface consumes pointer-down, the matching pointer-up + remains owned by that higher surface sequence and must not synthesize an Application root click if the portal state + changes before release +- preserve central pointer-capture cleanup: captured DOM nodes receive move/release/cancel until capture ends, consumed + pointer events do not arm lower-root DnD, and active modal portals must not leak Application-root DnD ghosts +- preserve DnD ghost portal ownership: Application-owned drag ghosts paint through Application portal entries without + adding another root/portal/widget capture bookkeeping path + +Public helper APIs such as modal/select/context-menu/color picker sit on top of this model; low-level domain/portal +internals are not a stable app extension API. + +Keyboard events follow DOM-like targeting. A screen still has one active focused element. The focused element belongs to +a domain through its mounted DOM tree, and key-down/key-up dispatch selects a domain surface before posting the event to +that focused node. Bubbling then stays inside that node's physical DOM tree. Screen/global shortcuts such as debug +toggles and style reloads remain explicit screen-host dispatcher behavior. + +Select popup ownership is scope-aware: application-owned selects route through the application portal service, and +system-owned selects route through the system portal service. The select popup is mounted as a portal entry with a real +portal DOM node. Pointer and wheel input for the popup routes through that portal DOM tree first; outside pointer +dismissal uses generic portal-entry policy evaluation before lower-priority surfaces can receive the same pointer +sequence. `SelectEngine` remains a temporary helper for select state, measurement, placement, animation, keyboard +navigation, and paint primitives rather than an independent screen-level runtime owner. + +Modal presentation is mounted as application portal DOM while `modalPortal` keeps regular content in the application root. +Modal pointer-down containment, backdrop dismissal, consume-only static backdrop behavior, and anti-click-through use +generic portal-entry policies. The screen coordinator also preserves higher-surface pointer sequence ownership so a +portal-consumed backdrop press cannot release into an Application root click after modal state changes. +`ModalPortalSessionStore` owns the modal-specific stack/focus/trap/restore session state as a bounded implementation +detail, with focus restore/trap requests scoped to the modal portal DOM root where available. + +Inspector and system color picker UI are system portal entries. They keep system style isolation and may still use +system-domain manual dispatch plus DOM fallback internally where native Inspector/color-picker DOM editing requires it. + +DnD drag visuals are floating UI. Application-owned drag ghosts are represented as Application portal entries and are +painted as part of the Application portal surface, after Application root content and before later higher-domain +surfaces. The DnD engine still owns drag session state, smoothing, source hiding, drop target tracking, and preview +command production. Portal ownership only decides where the visual ghost is mounted and staged (`ApplicationDndGhostPortalController` +in `ApplicationPortalHost.kt` implements this entry). While an Application +modal portal is active, lower Application-root DnD ghost output remains suppressed so modal-owned pointer sequences do +not leak stale root ghosts. System/Debug-owned drag visuals are reserved for the owning domain portal when such drag +sources exist. ## Where to inspect next @@ -106,3 +253,5 @@ etc.) sit on top of this model; low-level overlay internals are not a stable app - Style runtime: `core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt` - Stylesheet loading: `core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StylesheetManager.kt` - Hook lifecycle runtime: `core/src/main/kotlin/org/dreamfinity/dsgl/core/hooks/ComponentHookRuntime.kt` +- Portal input capture: `core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/input/PointerCaptureSession.kt` +- Application portal host + DnD ghost controller: `core/src/main/kotlin/org/dreamfinity/dsgl/core/portal/ApplicationPortalHost.kt` diff --git a/docs/custom-components.md b/docs/custom-components.md index c99de95..bcac8c0 100644 --- a/docs/custom-components.md +++ b/docs/custom-components.md @@ -151,7 +151,7 @@ Avoid building app components around internal plumbing: - `UiScope.mount(...)` (internal) - `DsglWindow.hookRuntime()` and `ComponentHookRuntime.withComponentInstance(...)` (internal runtime plumbing) -- overlay/system internals and devtool internals (`overlay.system.*`, inspector internals, `HotReloadBridge`) +- portal/system internals and devtool internals (`core.portal.system.*`, inspector internals, `HotReloadBridge`) These exist for framework/runtime internals, not as stable public extension contracts. @@ -162,6 +162,6 @@ These exist for framework/runtime internals, not as stable public extension cont - Add slot lambdas for extensibility. - Keep the hook state local only when it is truly local. - Use stable keys for interactive or reorderable component instances. -- Treat internal runtime/overlay APIs as non-public unless docs explicitly promote them. +- Treat internal runtime/portal APIs as non-public unless docs explicitly promote them. Next: [Hot-reload setup](hot-reload-agent.md). diff --git a/docs/elements-overview.md b/docs/elements-overview.md index 06461dc..f6e2a56 100644 --- a/docs/elements-overview.md +++ b/docs/elements-overview.md @@ -75,15 +75,17 @@ div({ } ``` -### `overlay(...)` +### `div({ overlapChildren = true })` -Container for overlapping children (stack-like behaviour). +Container mode for overlapping direct children. This is still a normal `div`; the flag makes direct children share one +content area instead of flowing one after another. Builder shape: ```kotlin { .kotlin .copy .select } -overlay({ - key = "overlay.layer" +div({ + key = "overlap.example" + overlapChildren = true style = { width = 100.percent } }) { div({ style = { margin(8.px, 0.px, 0.px, 8.px) } }) { text("Floating badge") } @@ -226,6 +228,7 @@ var currentValue by useState(null) select({ key = "example.select" value = currentValue + ownerDomain = ScreenDomainId.Application onValueChange = { event -> currentValue = event.value } }) { placeholder("Choose one") @@ -241,7 +244,7 @@ select({ Caveats: - popup behaviour is provided by domain portal services; this API is the supported convenience entrypoint. -- use `ownerScope = OverlayOwnerScope.System` when a select is hosted by system-owned UI (for example inspector/system tools). +- use `ownerDomain = ScreenDomainId.System` when a select is hosted by system-owned UI (for example inspector/system tools). - keyboard and wheel behaviour are implemented and covered by `SelectEngineTests`. ### `colorPicker(...)` and `colorPickerPopup(...)` @@ -274,7 +277,7 @@ colorPickerPopup({ ## Higher-level helper DSLs -These are exposed conveniences for common UI workflows. They are not documented as a generalized public overlay +These are exposed conveniences for common UI workflows. They are not documented as a generalized public portal/runtime framework contract. ### Modal helpers diff --git a/docs/hooks.md b/docs/hooks.md index 0f83ac1..bd6a076 100644 --- a/docs/hooks.md +++ b/docs/hooks.md @@ -533,6 +533,6 @@ Persistence across rebuilds: - For repeated unkeyed sibling component calls, expect ordinal fallback semantics and position-based rebinding on reorder. - Use `by` for storage-backed hooks. -- Treat DnD hooks as advanced descriptor APIs, not as a generic public overlay framework. +- Treat DnD hooks as advanced descriptor APIs, not as a generic public portal/runtime framework. - For runtime behaviour details around rebuild triggers, see [State and reactivity](state-reactivity.md). diff --git a/docs/index.md b/docs/index.md index 73b10a9..d55378d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -36,7 +36,8 @@ Reference: - **screen host**: a host implementation (for example `DsglScreenHost`) that drives lifecycle, input, and painting - **DOM tree**: the retained tree of DSGL nodes (`DomTree` + `DOMNode` hierarchy) - **hook runtime**: the per-window hook state/effect system managed by `ComponentHookRuntime` -- **overlay**: layered UI surfaces above the application root (application/system/debug layers) +- **domain surface**: one physical screen target owned by a domain, either root DOM or portal host +- **portal**: a domain-local mount point for floating UI such as modals, dropdowns, context menus, drag ghosts, and panels - **stylesheet**: DSS files parsed and applied by the style engine - **element**: a concrete node in the DOM tree (for example, text, button, input, container) - **component**: a reusable UI composition built with the DSL diff --git a/docs/layout-model.md b/docs/layout-model.md index b5a03af..e16b15b 100644 --- a/docs/layout-model.md +++ b/docs/layout-model.md @@ -57,9 +57,10 @@ This is intentionally narrower than browser CSS grid. Node is excluded from layout/render traversal and state like hover/active/focus/open is reset for that node. -### Overlay stack container (DSL helper) +### Overlapping children -`overlay { ... }` uses a `ContainerNode` with `stackLayout=true`. +`div({ overlapChildren = true }) { ... }` uses a `ContainerNode` mode where direct children share one content area +instead of flowing as block/flex/grid/inline items. - children share the same content area - child `align` controls placement inside that area @@ -68,8 +69,9 @@ Node is excluded from layout/render traversal and state like hover/active/focus/ Example: ```kotlin { .kotlin .copy .select } -overlay({ - key = "hud.layer" +div({ + key = "hud.overlap" + overlapChildren = true style = { width = 100.percent height = 100.percent diff --git a/docs/project-structure.md b/docs/project-structure.md index 729b0fc..4acd35d 100644 --- a/docs/project-structure.md +++ b/docs/project-structure.md @@ -34,14 +34,14 @@ - `src/main/kotlin/org/dreamfinity/dsgl/core/dom/` DOM node implementations, layout models, reconcile helpers, overflow/positioning behaviour. -- `src/main/kotlin/org/dreamfinity/dsgl/core/overlay/` - Overlay layering/contracts and host-layer plumbing. +- `src/main/kotlin/org/dreamfinity/dsgl/core/portal/` + Domain-surface and portal runtime contracts, portal hosts, and floating UI helpers. - `src/main/kotlin/org/dreamfinity/dsgl/core/components/`, `select/`, `contextmenu/`, `colorpicker/`, `dnd/` Higher-level public helpers and advanced interaction systems. - `src/test/kotlin/...` - Main regression coverage (hooks, layout, style, overlay/input, helper systems). + Main regression coverage (hooks, layout, style, domain/portal input, helper systems). ## `adapters/mc-forge-1-7-10/` structure @@ -106,5 +106,5 @@ - DSL/API behaviour: start in `core/.../Dsl.kt` and relevant `core/hooks` or component package. - Runtime frame behaviour: start in `adapters/mc-forge-1-7-10/.../DsglScreenHost.kt` and `core/DomTree.kt`. - Style parsing/application: start in `core/style/`. -- Overlay routing/ordering: start in `core/overlay/` plus host integration in `DsglScreenHost`. +- Domain/portal routing and ordering: start in `core/portal/` plus host integration in `DsglScreenHost`. - Demo regressions/examples: `adapters/mc-forge-1-7-10/demo/src/main/kotlin/.../demo/sections`. diff --git a/docs/quickstart.md b/docs/quickstart.md index fee0998..bb949e7 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -144,10 +144,10 @@ object DsglClientHotkeys { ![img](images/quickstart-basic-screen.png) -!!! note "Small overlay panel" +!!! note "Debug domain panel" - You might mention a small overlay panel in the left bottom edge. It has debug-only functions and some - information about the current window (overlays settings and immediate+sliding window FPS counter). + You might notice a small debug panel in the left bottom edge. It has debug-only functions and some + information about the current window (domain-surface settings and immediate+sliding window FPS counter). It will be removed in the release (or at least hidden by default). ## 5) Compose reusable components diff --git a/docs/status-limitations.md b/docs/status-limitations.md index 6602090..5c3f384 100644 --- a/docs/status-limitations.md +++ b/docs/status-limitations.md @@ -12,14 +12,17 @@ These areas are used across core + demo and have broad test coverage: - stylesheet loading and style application pipeline (`.dss` + inline style) - primary layout modes in current model (`block`, `inline`, `flex`, `grid`, `none`) and positioning (`static`, `relative`, `absolute`, `fixed`, `sticky`) -- overlay layer ordering contract used by host/runtime ( - `application root -> application overlay -> system overlay -> debug`) +- domain surface ordering contract used by host/runtime ( + `Application root -> Application portal -> System root -> System portal -> Debug root -> Debug portal`) ## Experimental -- drag-and-drop/sortable ecosystem (`useDraggable`, `useDroppable`, `useSortable`, monitor callbacks) -- color picker flows (especially popup/eyedropper interactions across overlay ownership) +- drag-and-drop/sortable ecosystem (`useDraggable`, `useDroppable`, `useSortable`, monitor callbacks); Application-owned + drag ghosts render through Application portal ownership, while System/Debug drag visuals remain future domain-owned + cases +- color picker flows (especially popup/eyedropper interactions across domain portal ownership) - hot-reload integration path (stable enough for local development, but dependent on external agent/build workflow) +- Debug-owned select/dropdown portal support is intentionally unsupported until Debug UI needs select/dropdown components These areas are public and tested, but still advanced and more likely to evolve than baseline DSL/state/style/layout surfaces. @@ -28,10 +31,10 @@ surfaces. Do not treat these as extension contracts: -- `org.dreamfinity.dsgl.core.*.internal.*` packages (modal internals, inspector internals, overlay internals, +- `org.dreamfinity.dsgl.core.*.internal.*` packages (modal internals, inspector internals, portal/runtime internals, color-picker internals) -- runtime plumbing classes such as `HotReloadBridge`, low-level overlay hosts, internal engine state holders -- system/debug overlay internals and inspector implementation nodes +- runtime plumbing classes such as `HotReloadBridge`, low-level portal hosts, internal engine state holders +- system/debug domain internals and inspector implementation nodes If you rely on these directly, expect breakage across normal development changes. @@ -51,7 +54,7 @@ If you rely on these directly, expect breakage across normal development changes - direct runtime mounting/reconcile internals as a custom-component authoring mechanism - low-level JVMTI/Rust hot-reload internals as a stable contract -- generalized overlay framework internals behind modal/select/context-menu convenience DSLs +- generalized portal/runtime internals behind modal/select/context-menu convenience DSLs Use documented DSL composition patterns as the supported extension model: diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index b1a0233..9d2f952 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -104,17 +104,20 @@ If rules still do not affect node: - remember inline style has higher precedence than stylesheet rules - confirm pseudo-state (`:hover`, `:active`, etc.) is actually active at runtime -## Overlay / popup input surprises +## Portal / popup input surprises -Input routing is layered, not flat: +Input routing follows domain-surface priority, not a flat DOM tree: -1. system overlay -2. application overlay -3. application DOM tree +1. Debug portal +2. Debug root +3. System portal +4. System root +5. Application portal +6. Application root Practical effect: -- when a select/context menu/modal consumes pointer input, underlying app element will not receive that event +- when a select/context menu/modal consumes pointer input from its owning portal, lower-priority surfaces will not receive that event - this is expected behaviour, not click-through failure Context menu caveat in current API: From 6a68b60549a63717bb33efe4877a172d722756c6 Mon Sep 17 00:00:00 2001 From: Georgii Imeshkenov Date: Fri, 12 Jun 2026 17:32:47 +0300 Subject: [PATCH 78/78] reducing GC pressure by optimizing small objects allocation in affine transform and replacing some iterators with array index accessors; --- .../dsgl/mcForge1710/Mc1710UiAdapter.kt | 84 +++-- .../RenderCommandTransformStack.kt | 54 ++- ...erCommandTransformStackCompositionTests.kt | 118 +++++++ .../org/dreamfinity/dsgl/core/DomTree.kt | 46 ++- .../dsgl/core/animation/AnimationModel.kt | 4 + .../core/animation/StyleAnimationEngine.kt | 141 ++++---- .../dsgl/core/dom/PositionedLayoutModel.kt | 71 ++-- .../dsgl/core/style/StyleEngine.kt | 328 +++++++++++------- .../core/style/StyleEngineIncrementalTests.kt | 25 ++ 9 files changed, 594 insertions(+), 277 deletions(-) create mode 100644 adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/RenderCommandTransformStackCompositionTests.kt diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/Mc1710UiAdapter.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/Mc1710UiAdapter.kt index a40f3d7..947feac 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/Mc1710UiAdapter.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/Mc1710UiAdapter.kt @@ -76,6 +76,7 @@ class Mc1710UiAdapter( companion object { private val imageCache: MutableMap = HashMap() private val dynamicTexturesCache: MutableMap = HashMap() + private val HUE_STOPS: FloatArray = floatArrayOf(0f, 60f, 120f, 180f, 240f, 300f, 360f) private val MAGNIFIER_CAPTURE_VERTEX_SHADER: String = """ #version 120 @@ -121,8 +122,12 @@ class Mc1710UiAdapter( private val itemRenderer: RenderItem = RenderItem() private val textRenderer: MsdfTextRenderer = MsdfTextRenderer() - private val opacityStack: MutableList = ArrayList(8) + + // Plain float stack: a MutableList would box on every push/pop in the paint loop. + private var opacityStackValues = FloatArray(16) + private var opacityStackSize = 0 private var opacityMultiplier: Float = 1f + private val transformStack = RenderCommandTransformStack() private val errorLogTimes: MutableMap = linkedMapOf() private val readbackDiagnosticsVerbose: Boolean = java.lang.Boolean @@ -968,9 +973,8 @@ class Mc1710UiAdapter( @Suppress("LoopWithTooManyJumpStatements") override fun paint(commands: List) { paintsCount++ - opacityStack.clear() + opacityStackSize = 0 opacityMultiplier = 1f - val transformStack = RenderCommandTransformStack() transformStack.reset() val viewport = viewport() GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) @@ -1130,13 +1134,12 @@ class Mc1710UiAdapter( } is RenderCommand.PushOpacity -> { - opacityStack.add(opacityMultiplier) + pushOpacity(opacityMultiplier) opacityMultiplier = (opacityMultiplier * command.opacity).coerceIn(0f, 1f) } is RenderCommand.PopOpacity -> { - opacityMultiplier = - if (opacityStack.isEmpty()) 1f else opacityStack.removeAt(opacityStack.lastIndex) + opacityMultiplier = popOpacityOrDefault() } } } @@ -1151,12 +1154,26 @@ class Mc1710UiAdapter( } finally { ScissorContext.clear() transformStack.reset() - opacityStack.clear() + opacityStackSize = 0 opacityMultiplier = 1f GL11.glPopAttrib() } } + private fun pushOpacity(value: Float) { + if (opacityStackSize == opacityStackValues.size) { + opacityStackValues = opacityStackValues.copyOf(opacityStackValues.size * 2) + } + opacityStackValues[opacityStackSize] = value + opacityStackSize += 1 + } + + private fun popOpacityOrDefault(): Float { + if (opacityStackSize == 0) return 1f + opacityStackSize -= 1 + return opacityStackValues[opacityStackSize] + } + private fun applyOpacity(color: Int): Int { if (opacityMultiplier >= 0.999f) return color val alpha = ((color ushr 24) and 0xFF) @@ -1204,14 +1221,13 @@ class Mc1710UiAdapter( ) { if (width <= 0 || height <= 0) return val segments = 6 - val hueStops = floatArrayOf(0f, 60f, 120f, 180f, 240f, 300f, 360f) var index = 0 while (index < segments) { val startX = x + (width * index) / segments val endX = if (index == segments - 1) x + width else x + (width * (index + 1)) / segments val segmentWidth = (endX - startX).coerceAtLeast(1) - val startColor = applyOpacity(hsvToArgbInt(hueStops[index], 1f, 1f)) - val endColor = applyOpacity(hsvToArgbInt(hueStops[index + 1], 1f, 1f)) + val startColor = applyOpacity(hsvToArgbInt(HUE_STOPS[index], 1f, 1f)) + val endColor = applyOpacity(hsvToArgbInt(HUE_STOPS[index + 1], 1f, 1f)) drawHorizontalGradientRect(startX, y, segmentWidth, height, startColor, endColor) index += 1 } @@ -1318,18 +1334,44 @@ class Mc1710UiAdapter( val c = v * s val x = c * (1f - kotlin.math.abs((h / 60f) % 2f - 1f)) val m = v - c - val (r1, g1, b1) = - when { - h < 60f -> Triple(c, x, 0f) - h < 120f -> Triple(x, c, 0f) - h < 180f -> Triple(0f, c, x) - h < 240f -> Triple(0f, x, c) - h < 300f -> Triple(x, 0f, c) - else -> Triple(c, 0f, x) + val rFromHue: Float + val gFromHue: Float + val bFromHue: Float + when { + h < 60f -> { + rFromHue = c + gFromHue = x + bFromHue = 0f + } + h < 120f -> { + rFromHue = x + gFromHue = c + bFromHue = 0f } - val r = ((r1 + m) * 255f).toInt().coerceIn(0, 255) - val g = ((g1 + m) * 255f).toInt().coerceIn(0, 255) - val b = ((b1 + m) * 255f).toInt().coerceIn(0, 255) + h < 180f -> { + rFromHue = 0f + gFromHue = c + bFromHue = x + } + h < 240f -> { + rFromHue = 0f + gFromHue = x + bFromHue = c + } + h < 300f -> { + rFromHue = x + gFromHue = 0f + bFromHue = c + } + else -> { + rFromHue = c + gFromHue = 0f + bFromHue = x + } + } + val r = ((rFromHue + m) * 255f).toInt().coerceIn(0, 255) + val g = ((gFromHue + m) * 255f).toInt().coerceIn(0, 255) + val b = ((bFromHue + m) * 255f).toInt().coerceIn(0, 255) return (0xFF shl 24) or (r shl 16) or (g shl 8) or b } diff --git a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/RenderCommandTransformStack.kt b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/RenderCommandTransformStack.kt index 81d3102..24a52bf 100644 --- a/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/RenderCommandTransformStack.kt +++ b/adapters/mc-forge-1-7-10/src/main/kotlin/org/dreamfinity/dsgl/mcForge1710/RenderCommandTransformStack.kt @@ -3,7 +3,9 @@ package org.dreamfinity.dsgl.mcForge1710 import org.dreamfinity.dsgl.core.dom.layout.AffineTransform2D import org.dreamfinity.dsgl.core.render.RenderCommand import kotlin.math.ceil +import kotlin.math.cos import kotlin.math.floor +import kotlin.math.sin internal data class GuiClipRect( val x: Int, @@ -49,15 +51,24 @@ internal class RenderCommandTransformStack { return GuiClipRect(x, y, safeWidth, safeHeight) } - val topLeft = current.transform(x.toFloat(), y.toFloat()) - val topRight = current.transform((x + safeWidth).toFloat(), y.toFloat()) - val bottomLeft = current.transform(x.toFloat(), (y + safeHeight).toFloat()) - val bottomRight = current.transform((x + safeWidth).toFloat(), (y + safeHeight).toFloat()) + val transform = current + val left = x.toFloat() + val top = y.toFloat() + val right = (x + safeWidth).toFloat() + val bottom = (y + safeHeight).toFloat() + val topLeftX = transform.a * left + transform.c * top + transform.tx + val topLeftY = transform.b * left + transform.d * top + transform.ty + val topRightX = transform.a * right + transform.c * top + transform.tx + val topRightY = transform.b * right + transform.d * top + transform.ty + val bottomLeftX = transform.a * left + transform.c * bottom + transform.tx + val bottomLeftY = transform.b * left + transform.d * bottom + transform.ty + val bottomRightX = transform.a * right + transform.c * bottom + transform.tx + val bottomRightY = transform.b * right + transform.d * bottom + transform.ty - val minX = minOf(topLeft.first, topRight.first, bottomLeft.first, bottomRight.first) - val maxX = maxOf(topLeft.first, topRight.first, bottomLeft.first, bottomRight.first) - val minY = minOf(topLeft.second, topRight.second, bottomLeft.second, bottomRight.second) - val maxY = maxOf(topLeft.second, topRight.second, bottomLeft.second, bottomRight.second) + val minX = minOf(minOf(topLeftX, topRightX), minOf(bottomLeftX, bottomRightX)) + val maxX = maxOf(maxOf(topLeftX, topRightX), maxOf(bottomLeftX, bottomRightX)) + val minY = minOf(minOf(topLeftY, topRightY), minOf(bottomLeftY, bottomRightY)) + val maxY = maxOf(maxOf(topLeftY, topRightY), maxOf(bottomLeftY, bottomRightY)) val resolvedX = floor(minX.toDouble()).toInt() val resolvedY = floor(minY.toDouble()).toInt() @@ -67,15 +78,22 @@ internal class RenderCommandTransformStack { } private fun RenderCommand.PushTransform.toAffineTransform(): AffineTransform2D { - val toOrigin = AffineTransform2D.translation(originX, originY) - val translate = AffineTransform2D.translation(translateX, translateY) - val rotate = AffineTransform2D.rotation(rotateDeg) - val scale = AffineTransform2D.scale(scaleX, scaleY) - val fromOrigin = AffineTransform2D.translation(-originX, -originY) - return toOrigin - .times(translate) - .times(rotate) - .times(scale) - .times(fromOrigin) + // Closed form of T(origin) * T(translate) * R(rotateDeg) * S(scaleX, scaleY) * T(-origin): + // chaining times() would allocate nine intermediate matrices per push in the paint loop. + val rad = Math.toRadians(rotateDeg.toDouble()) + val cos = cos(rad).toFloat() + val sin = sin(rad).toFloat() + val a = cos * scaleX + val b = sin * scaleX + val c = -sin * scaleY + val d = cos * scaleY + return AffineTransform2D( + a = a, + b = b, + c = c, + d = d, + tx = originX + translateX - a * originX - c * originY, + ty = originY + translateY - b * originX - d * originY, + ) } } diff --git a/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/RenderCommandTransformStackCompositionTests.kt b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/RenderCommandTransformStackCompositionTests.kt new file mode 100644 index 0000000..205f234 --- /dev/null +++ b/adapters/mc-forge-1-7-10/src/test/kotlin/org/dreamfinity/dsgl/mcForge1710/RenderCommandTransformStackCompositionTests.kt @@ -0,0 +1,118 @@ +package org.dreamfinity.dsgl.mcForge1710 + +import org.dreamfinity.dsgl.core.dom.layout.AffineTransform2D +import org.dreamfinity.dsgl.core.render.RenderCommand +import kotlin.math.abs +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class RenderCommandTransformStackCompositionTests { + @Test + fun `closed-form push matches chained matrix composition`() { + val cases = + listOf( + transformCommand(), + transformCommand(originX = 10f, originY = 20f, translateX = 5f, translateY = -3f), + transformCommand(originX = 50f, originY = 25f, scaleX = 2f, scaleY = 0.5f), + transformCommand(originX = 12f, originY = 34f, rotateDeg = 45f), + transformCommand( + originX = 100f, + originY = 60f, + translateX = -8f, + translateY = 4f, + scaleX = 1.5f, + scaleY = 0.75f, + rotateDeg = 30f, + ), + transformCommand( + originX = -16f, + originY = 8f, + translateX = 3.5f, + translateY = 7.25f, + scaleX = 0.25f, + scaleY = 3f, + rotateDeg = -120f, + ), + transformCommand(translateX = 1f, translateY = 2f, scaleX = -1f, rotateDeg = 270f), + ) + cases.forEach { command -> + val stack = RenderCommandTransformStack() + stack.push(command) + assertMatrixNearlyEquals(referenceTransform(command), stack.currentTransform(), command) + } + } + + @Test + fun `pop restores the previous transform`() { + val stack = RenderCommandTransformStack() + val outer = transformCommand(originX = 5f, originY = 5f, translateX = 2f, scaleX = 2f, scaleY = 2f) + val inner = transformCommand(rotateDeg = 90f) + stack.push(outer) + val outerTransform = stack.currentTransform() + stack.push(inner) + stack.pop() + assertEquals(outerTransform, stack.currentTransform()) + stack.pop() + assertEquals(AffineTransform2D.IDENTITY, stack.currentTransform()) + } + + @Test + fun `resolveClipRect under rotation matches transformed bounding box`() { + val stack = RenderCommandTransformStack() + stack.push(transformCommand(rotateDeg = 90f)) + val clip = stack.resolveClipRect(10, 0, 20, 10) + assertEquals(-10, clip.x) + assertEquals(10, clip.y) + assertEquals(10, clip.width) + assertEquals(20, clip.height) + } + + private fun transformCommand( + originX: Float = 0f, + originY: Float = 0f, + translateX: Float = 0f, + translateY: Float = 0f, + scaleX: Float = 1f, + scaleY: Float = 1f, + rotateDeg: Float = 0f, + ): RenderCommand.PushTransform = + RenderCommand.PushTransform( + originX = originX, + originY = originY, + translateX = translateX, + translateY = translateY, + scaleX = scaleX, + scaleY = scaleY, + rotateDeg = rotateDeg, + ) + + private fun referenceTransform(command: RenderCommand.PushTransform): AffineTransform2D = + AffineTransform2D + .translation(command.originX, command.originY) + .times(AffineTransform2D.translation(command.translateX, command.translateY)) + .times(AffineTransform2D.rotation(command.rotateDeg)) + .times(AffineTransform2D.scale(command.scaleX, command.scaleY)) + .times(AffineTransform2D.translation(-command.originX, -command.originY)) + + private fun assertMatrixNearlyEquals(expected: AffineTransform2D, actual: AffineTransform2D, command: RenderCommand.PushTransform) { + assertNearlyEqual(expected.a, actual.a, "a", command) + assertNearlyEqual(expected.b, actual.b, "b", command) + assertNearlyEqual(expected.c, actual.c, "c", command) + assertNearlyEqual(expected.d, actual.d, "d", command) + assertNearlyEqual(expected.tx, actual.tx, "tx", command) + assertNearlyEqual(expected.ty, actual.ty, "ty", command) + } + + private fun assertNearlyEqual( + expected: Float, + actual: Float, + field: String, + command: RenderCommand.PushTransform, + ) { + assertTrue( + abs(expected - actual) <= 1e-3f, + "Field $field diverged for $command: expected $expected, actual $actual", + ) + } +} diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/DomTree.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/DomTree.kt index 3b564eb..faeb4f3 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/DomTree.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/DomTree.kt @@ -45,8 +45,8 @@ class DomTree( private var lastWidth: Int = 0 private var lastHeight: Int = 0 private var laidOut: Boolean = false - private val paintBuffer: MutableList = ArrayList(256) - private val stagingPaintBuffer: MutableList = ArrayList(256) + private var paintBuffer: MutableList = ArrayList(256) + private var stagingPaintBuffer: MutableList = ArrayList(256) private val refManager: RefManager = RefManager() private var lastViolations: List = emptyList() private var strictInvalidLayout: Boolean = false @@ -91,7 +91,12 @@ class DomTree( commandsDirty = true } - /** Builds render commands for the current layout. */ + /** + * Builds render commands for the current layout. + * + * The returned list is valid until the next [paint] call and must not be retained + * across frames; consume or copy it within the same frame. + */ fun paint(ctx: UiMeasureContext, applyStyles: Boolean = true): List { val paintStartNanos = System.nanoTime() try { @@ -112,13 +117,7 @@ class DomTree( ScrollPerformanceCounters.recordStyleApplyDuration(System.nanoTime() - styleStartNanos) } } else { - StyleEngine.StyleApplyReport( - layoutDirty = false, - visualDirty = false, - visitedNodes = 0, - cacheHits = 0, - recomputedNodes = 0, - ) + StyleEngine.StyleApplyReport.CLEAN } val canUseGuardedScrollVisualFastPath = laidOut && @@ -258,8 +257,10 @@ class DomTree( if (debugCommandStackChecks) { validateCommandStacks(stagingPaintBuffer) } - paintBuffer.clear() - paintBuffer.addAll(stagingPaintBuffer) + // Swap instead of copy: addAll would clone the full command list every rebuild. + val rebuilt = stagingPaintBuffer + stagingPaintBuffer = paintBuffer + paintBuffer = rebuilt commandsDirty = false true } catch ( @@ -476,14 +477,21 @@ class DomTree( } private fun appendChunkCommands(chunk: RenderCommandChunk, out: MutableList) { - out.addAll(chunk.prefixCommands) - out.addAll(chunk.selfCommands) - out.addAll(chunk.childrenPrefixCommands) - chunk.children.forEach { child -> - appendChunkCommands(child, out) + appendCommands(chunk.prefixCommands, out) + appendCommands(chunk.selfCommands, out) + appendCommands(chunk.childrenPrefixCommands, out) + val children = chunk.children + for (index in children.indices) { + appendChunkCommands(children[index], out) + } + appendCommands(chunk.childrenSuffixCommands, out) + appendCommands(chunk.suffixCommands, out) + } + + private fun appendCommands(source: List, out: MutableList) { + for (index in source.indices) { + out.add(source[index]) } - out.addAll(chunk.childrenSuffixCommands) - out.addAll(chunk.suffixCommands) } fun dispatchClick(event: MouseClickEvent): Boolean { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/AnimationModel.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/AnimationModel.kt index e31226b..39d7744 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/AnimationModel.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/AnimationModel.kt @@ -155,4 +155,8 @@ data class KeyframesDefinition( init { require(frames.isNotEmpty()) { "Keyframes '$name' must contain at least one frame." } } + + val transformFrames: List = frames.filter { it.value.transform != null } + val opacityFrames: List = frames.filter { it.value.opacity != null } + val colorFrames: List = frames.filter { it.value.color != null } } diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/StyleAnimationEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/StyleAnimationEngine.kt index 5baf9eb..d85c7fd 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/StyleAnimationEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/animation/StyleAnimationEngine.kt @@ -24,14 +24,20 @@ object StyleAnimationEngine { val easing: Easing, val animatable: Animatable, ) { - fun valueAt(nowSec: Double): Pair { - if (durationSec <= 0.0) return to to true + fun valueAt(nowSec: Double): T { + if (durationSec <= 0.0) return to val local = nowSec - startSec - if (local <= delaySec) return from to false + if (local <= delaySec) return from val t = ((local - delaySec) / durationSec).toFloat().coerceIn(0f, 1f) val eased = easing.map(t) - val value = animatable.interpolate(from, to, eased) - return value to (t >= 1f) + return animatable.interpolate(from, to, eased) + } + + fun isFinishedAt(nowSec: Double): Boolean { + if (durationSec <= 0.0) return true + val local = nowSec - startSec + if (local <= delaySec) return false + return ((local - delaySec) / durationSec).toFloat() >= 1f } } @@ -324,9 +330,9 @@ object StyleAnimationEngine { @Suppress("UNCHECKED_CAST") private fun transitionValue(state: NodeAnimationState, property: AnimatedStyleProperty): T? { - val transition = state.transitions[property] as TransitionState? ?: return null - val (value, finished) = transition.valueAt(nowSec) - if (finished) { + val transition = state.transitions[property] as? TransitionState? ?: return null + val value = transition.valueAt(nowSec) + if (transition.isFinishedAt(nowSec)) { state.transitions.remove(property) } return value @@ -404,94 +410,65 @@ object StyleAnimationEngine { color = sampleColor(definition, progress, easing), ) - private fun sampleTransform(definition: KeyframesDefinition, progress: Float, easing: Easing): UiTransform? { - val frames = definition.frames.filter { it.value.transform != null } - if (frames.isEmpty()) return null - if (progress <= frames.first().fraction) { - return frames - .first() - .value.transform - } - if (progress >= frames.last().fraction) { - return frames - .last() - .value.transform - } - val pair = - surrounding(frames, progress) ?: return frames - .last() - .value.transform - val t = normalizedProgress(pair.first.fraction, pair.second.fraction, progress) - val eased = easing.map(t) - return TransformAnimatable.interpolate( - pair.first.value.transform!!, - pair.second.value.transform!!, - eased, + private fun sampleTransform(definition: KeyframesDefinition, progress: Float, easing: Easing): UiTransform? = + sampleProperty( + frames = definition.transformFrames, + progress = progress, + easing = easing, + valueOf = { it.transform }, + interpolate = { from, to, t -> TransformAnimatable.interpolate(from, to, t) }, ) - } - private fun sampleOpacity(definition: KeyframesDefinition, progress: Float, easing: Easing): Float? { - val frames = definition.frames.filter { it.value.opacity != null } - if (frames.isEmpty()) return null - if (progress <= frames.first().fraction) { - return frames - .first() - .value.opacity - } - if (progress >= frames.last().fraction) { - return frames - .last() - .value.opacity - } - val pair = - surrounding(frames, progress) ?: return frames - .last() - .value.opacity - val t = normalizedProgress(pair.first.fraction, pair.second.fraction, progress) - val eased = easing.map(t) - return FloatAnimatable - .interpolate( - pair.first.value.opacity!!, - pair.second.value.opacity!!, - eased, - ).coerceIn(0f, 1f) - } + private fun sampleOpacity(definition: KeyframesDefinition, progress: Float, easing: Easing): Float? = + sampleProperty( + frames = definition.opacityFrames, + progress = progress, + easing = easing, + valueOf = { it.opacity }, + interpolate = { from, to, t -> FloatAnimatable.interpolate(from, to, t).coerceIn(0f, 1f) }, + ) + + private fun sampleColor(definition: KeyframesDefinition, progress: Float, easing: Easing): Int? = + sampleProperty( + frames = definition.colorFrames, + progress = progress, + easing = easing, + valueOf = { it.color }, + interpolate = { from, to, t -> ColorAnimatable.interpolate(from, to, t) }, + ) - private fun sampleColor(definition: KeyframesDefinition, progress: Float, easing: Easing): Int? { - val frames = definition.frames.filter { it.value.color != null } + private inline fun sampleProperty( + frames: List, + progress: Float, + easing: Easing, + valueOf: (KeyframeValue) -> T?, + interpolate: (T, T, Float) -> T, + ): T? { if (frames.isEmpty()) return null if (progress <= frames.first().fraction) { - return frames - .first() - .value.color + return valueOf(frames.first().value) } if (progress >= frames.last().fraction) { - return frames - .last() - .value.color + return valueOf(frames.last().value) } - val pair = - surrounding(frames, progress) ?: return frames - .last() - .value.color - val t = normalizedProgress(pair.first.fraction, pair.second.fraction, progress) + val leftIndex = surroundingLeftIndex(frames, progress) + if (leftIndex < 0) { + return valueOf(frames.last().value) + } + val left = frames[leftIndex] + val right = frames[leftIndex + 1] + val t = normalizedProgress(left.fraction, right.fraction, progress) val eased = easing.map(t) - return ColorAnimatable.interpolate( - pair.first.value.color!!, - pair.second.value.color!!, - eased, - ) + return interpolate(valueOf(left.value)!!, valueOf(right.value)!!, eased) } - private fun surrounding(frames: List, progress: Float): Pair? { + private fun surroundingLeftIndex(frames: List, progress: Float): Int { for (index in 0 until frames.lastIndex) { - val left = frames[index] - val right = frames[index + 1] - if (progress >= left.fraction && progress <= right.fraction) { - return left to right + if (progress >= frames[index].fraction && progress <= frames[index + 1].fraction) { + return index } } - return null + return -1 } private fun normalizedProgress(left: Float, right: Float, value: Float): Float { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutModel.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutModel.kt index 525f00c..2c2fba6 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutModel.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/dom/PositionedLayoutModel.kt @@ -132,18 +132,21 @@ internal object PositionedLayoutModel { return matchesChildContextTrigger(node) } - private fun localContextParticipants(owner: DOMNode): List = - owner.children - .withIndex() - .filter { indexed -> indexed.value.position != PositionMode.Fixed } - .map { indexed -> - val child = indexed.value - val createsChildContextHint = createsChildContextForLocalParticipation(child) + private fun localContextParticipants(owner: DOMNode): List { + val children = owner.children + val participants = ArrayList(children.size) + for ((index, element) in children.withIndex()) { + val child = element + if (child.position == PositionMode.Fixed) { + continue + } + val createsChildContextHint = createsChildContextForLocalParticipation(child) + participants += StackingParticipant( node = child, logicalParent = owner, - sourceDomOrder = indexed.index, - priority = orderingPriority(child, indexed.index), + sourceDomOrder = index, + priority = orderingPriority(child, index), kind = if (createsChildContextHint) { StackingParticipantKind.ChildContext @@ -153,7 +156,9 @@ internal object PositionedLayoutModel { createsChildContextHint = createsChildContextHint, rootContextPromotionTarget = null, ) - } + } + return participants + } private fun rootContextParticipants(root: DOMNode, contextId: RootStackingContextId): List { val globalDomOrder = buildGlobalDomOrderMap(root) @@ -241,29 +246,55 @@ internal object PositionedLayoutModel { else -> OffsetPrecedenceResolution(null, null) } + private val paintOrderComparator: Comparator = + compareBy( + { it.priority.positionedBucket }, + { it.priority.zIndex }, + { it.priority.domOrder }, + ) + fun orderedParticipantsForPaint(owner: DOMNode): List { val participants = stackingContextScaffold(owner).participants if (participants.size <= 1) { return participants } - val hasPositioned = participants.any { isPositioned(it.node) } + var hasPositioned = false + for (index in participants.indices) { + if (isPositioned(participants[index].node)) { + hasPositioned = true + break + } + } if (!hasPositioned) { - return participants.sortedBy { it.priority.domOrder } + // Participants are built in DOM order; without positioned nodes the sort is a no-op. + return participants } - return participants.sortedWith( - compareBy( - { it.priority.positionedBucket }, - { it.priority.zIndex }, - { it.priority.domOrder }, - ), - ) + return participants.sortedWith(paintOrderComparator) } fun orderedParticipantsForHitTesting(owner: DOMNode): List = orderedParticipantsForPaint(owner).asReversed() - fun orderedChildrenForPaint(parent: DOMNode): List = orderedParticipantsForPaint(parent).map { it.node } + fun orderedChildrenForPaint(parent: DOMNode): List { + // Fast path for the per-frame chunk traversal: non-root owners with no positioned + // children paint in plain DOM order, and Fixed children (the only ones filtered out + // of local participation) are positioned by definition. Callers iterate read-only. + if (parent.parent != null && !hasPositionedChild(parent)) { + return parent.children + } + return orderedParticipantsForPaint(parent).map { it.node } + } + + private fun hasPositionedChild(parent: DOMNode): Boolean { + val children = parent.children + for (index in children.indices) { + if (isPositioned(children[index])) { + return true + } + } + return false + } fun orderedChildrenForHitTesting(parent: DOMNode): List = orderedParticipantsForHitTesting(parent).map { diff --git a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt index bb13c03..a2721f6 100644 --- a/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt +++ b/core/src/main/kotlin/org/dreamfinity/dsgl/core/style/StyleEngine.kt @@ -18,7 +18,18 @@ object StyleEngine { val visitedNodes: Int, val cacheHits: Int, val recomputedNodes: Int, - ) + ) { + companion object { + val CLEAN: StyleApplyReport = + StyleApplyReport( + layoutDirty = false, + visualDirty = false, + visitedNodes = 0, + cacheHits = 0, + recomputedNodes = 0, + ) + } + } private data class MutableApplyMetrics( var visitedNodes: Int = 0, @@ -69,18 +80,103 @@ object StyleEngine { val sourceOrder: Int, val origin: StyleOrigin, val sourceKind: StyleSourceKind, - val sourceLabel: String, - ) + val sourceRule: StyleRule? = null, + ) { + fun sourceLabel(): String = + when { + sourceRule != null -> + buildString { + append(selectorLabel(sourceRule.selector)) + append(" @ ") + append(sourceRule.fileName) + if (important) append(" !important") + } + origin == StyleOrigin.Inline -> if (important) "inline !important" else "inline" + origin == StyleOrigin.Inspector -> if (important) "inspector !important" else "inspector" + else -> "unknown" + } + } - private data class NodeApplyFlags( - val layoutDirty: Boolean, - val visualDirty: Boolean, - ) + private const val FLAG_LAYOUT_DIRTY = 1 + private const val FLAG_VISUAL_DIRTY = FLAG_LAYOUT_DIRTY shl 1 + private const val FLAG_CACHE_HIT = FLAG_VISUAL_DIRTY shl 1 + + private class CacheKeyProbe { + var typeName: String = "" + var nodeId: String? = null + var classesHash: Int = 0 + var inlineHash: Int = 0 + var inspectorHash: Int = 0 + var hovered: Boolean = false + var active: Boolean = false + var focused: Boolean = false + var disabled: Boolean = false + var open: Boolean = false + var stylesheetVersion: Long = 0L + var themeVersion: Long = 0L + var defaultsHash: Int = 0 + var parentInheritedHash: Int = 0 + var ancestorSelectorHash: Int = 0 + var siblingSelectorHash: Int = 0 + var rootFontSizePx: Int = 0 + var viewportWidthPx: Int = 0 + var viewportHeightPx: Int = 0 + var scope: StyleApplicationScope = StyleApplicationScope.Application + + fun matches(key: CacheKey): Boolean = matchesIdentity(key) && matchesPseudoState(key) && matchesContext(key) + + private fun matchesIdentity(key: CacheKey): Boolean = + classesHash == key.classesHash && + inlineHash == key.inlineHash && + inspectorHash == key.inspectorHash && + defaultsHash == key.defaultsHash && + typeName == key.typeName && + nodeId == key.nodeId + + private fun matchesPseudoState(key: CacheKey): Boolean = + hovered == key.hovered && + active == key.active && + focused == key.focused && + disabled == key.disabled && + open == key.open + + private fun matchesContext(key: CacheKey): Boolean = + stylesheetVersion == key.stylesheetVersion && + themeVersion == key.themeVersion && + parentInheritedHash == key.parentInheritedHash && + ancestorSelectorHash == key.ancestorSelectorHash && + siblingSelectorHash == key.siblingSelectorHash && + rootFontSizePx == key.rootFontSizePx && + viewportWidthPx == key.viewportWidthPx && + viewportHeightPx == key.viewportHeightPx && + scope == key.scope + + fun toKey(): CacheKey = + CacheKey( + typeName = typeName, + nodeId = nodeId, + classesHash = classesHash, + inlineHash = inlineHash, + inspectorHash = inspectorHash, + hovered = hovered, + active = active, + focused = focused, + disabled = disabled, + open = open, + stylesheetVersion = stylesheetVersion, + themeVersion = themeVersion, + defaultsHash = defaultsHash, + parentInheritedHash = parentInheritedHash, + ancestorSelectorHash = ancestorSelectorHash, + siblingSelectorHash = siblingSelectorHash, + rootFontSizePx = rootFontSizePx, + viewportWidthPx = viewportWidthPx, + viewportHeightPx = viewportHeightPx, + scope = scope, + ) + } - private data class NodeApplyResult( - val flags: NodeApplyFlags, - val cacheHit: Boolean, - ) + private val cacheKeyProbe = CacheKeyProbe() private enum class StylePassMode { Full, @@ -93,12 +189,15 @@ object StyleEngine { private val inspectorOverrideVersions: MutableMap = linkedMapOf() private val anonymousTargetCache: MutableMap = WeakHashMap() private val pseudoDirtyNodes: MutableSet = - Collections.newSetFromMap(WeakHashMap()) + Collections.newSetFromMap(WeakHashMap()) private val selectorDirtyNodes: MutableSet = - Collections.newSetFromMap(WeakHashMap()) + Collections.newSetFromMap(WeakHashMap()) private var themeVersion: Long = 0L private var inspectorOverridesVersion: Long = 0L + private var cachedResolvedVariables: Map = emptyMap() + private var cachedResolvedVariablesSnapshotVersion: Long = Long.MIN_VALUE + private var cachedResolvedVariablesThemeVersion: Long = Long.MIN_VALUE private var pseudoStateVersion: Long = 0L private var selectorStateVersion: Long = 0L private var pseudoDirtyGlobal: Boolean = false @@ -291,7 +390,7 @@ object StyleEngine { StylePropertySource( property = property, kind = winner.sourceKind, - source = winner.sourceLabel, + source = winner.sourceLabel(), ) } else if (StylePropertyRegistry.isInherited(property) && parentComputed != null) { result = inheritProperty(result, parentComputed, property) @@ -354,8 +453,8 @@ object StyleEngine { val report = StyleApplyReport( - layoutDirty = result.layoutDirty, - visualDirty = result.visualDirty, + layoutDirty = result and FLAG_LAYOUT_DIRTY != 0, + visualDirty = result and FLAG_VISUAL_DIRTY != 0, visitedNodes = metrics.visitedNodes, cacheHits = metrics.cacheHits, recomputedNodes = metrics.recomputedNodes, @@ -423,12 +522,18 @@ object StyleEngine { snapshot: StylesheetSnapshot, variables: Map, metrics: MutableApplyMetrics, - ): NodeApplyFlags { + ): Int { + val rawDirty = + when { + pseudoDirtyNodes.isEmpty() -> selectorDirtyNodes + selectorDirtyNodes.isEmpty() -> pseudoDirtyNodes + else -> pseudoDirtyNodes + selectorDirtyNodes + } val dirty = expandDirtyNodesForCombinators( root = root, snapshot = snapshot, - rawDirty = pseudoDirtyNodes + selectorDirtyNodes, + rawDirty = rawDirty, ).filter { it.isDescendantOfOrSame(root) } .sortedBy { it.depth() } if (dirty.isEmpty()) { @@ -454,9 +559,9 @@ object StyleEngine { } } - var flags = NodeApplyFlags(layoutDirty = false, visualDirty = false) + var flags = 0 effectiveRoots.forEach { subtreeRoot -> - val subtreeFlags = + flags = flags or applyStylesRecursively( root = subtreeRoot, snapshot = snapshot, @@ -468,11 +573,6 @@ object StyleEngine { scope = StyleApplicationScope.Application, allowInspectorOverrides = true, ) - flags = - NodeApplyFlags( - layoutDirty = flags.layoutDirty || subtreeFlags.layoutDirty, - visualDirty = flags.visualDirty || subtreeFlags.visualDirty, - ) } return flags } @@ -487,7 +587,7 @@ object StyleEngine { passMode: StylePassMode, scope: StyleApplicationScope, allowInspectorOverrides: Boolean, - ): NodeApplyFlags { + ): Int { val nodeResult = applyStyleToNode( node = root, @@ -499,10 +599,10 @@ object StyleEngine { scope = scope, allowInspectorOverrides = allowInspectorOverrides, ) - var flags = nodeResult.flags + var flags = nodeResult and (FLAG_LAYOUT_DIRTY or FLAG_VISUAL_DIRTY) val canSkipSubtree = passMode == StylePassMode.Targeted && - nodeResult.cacheHit && + (nodeResult and FLAG_CACHE_HIT) != 0 && !snapshot.index.hasAncestorDependentSelectors if (canSkipSubtree) { return flags @@ -514,10 +614,11 @@ object StyleEngine { } else { rootFontSizePx } - root.children.forEach { child -> - val childFlags = + val children = root.children + for (index in children.indices) { + flags = flags or applyStylesRecursively( - root = child, + root = children[index], snapshot = snapshot, variables = variables, metrics = metrics, @@ -527,11 +628,6 @@ object StyleEngine { scope = scope, allowInspectorOverrides = allowInspectorOverrides, ) - flags = - NodeApplyFlags( - layoutDirty = flags.layoutDirty || childFlags.layoutDirty, - visualDirty = flags.visualDirty || childFlags.visualDirty, - ) } return flags } @@ -545,59 +641,49 @@ object StyleEngine { rootFontSizePx: Int, scope: StyleApplicationScope, allowInspectorOverrides: Boolean, - ): NodeApplyResult { + ): Int { metrics.visitedNodes += 1 val defaults = node.captureStyleDefaults() - val key = - CacheKey( - typeName = node.styleType, - nodeId = selectorNodeId(node), - classesHash = node.styleClasses.hashCode(), - inlineHash = node.inlineStyleDeclarations.toStableHash(), - inspectorHash = if (allowInspectorOverrides) inspectorOverrideHash(node) else 0, - hovered = node.styleHovered, - active = node.styleActive, - focused = node.styleFocused, - disabled = node.styleDisabled, - open = node.styleOpen, - stylesheetVersion = snapshot.version, - themeVersion = if (scope == StyleApplicationScope.Application) themeVersion else 0L, - defaultsHash = defaults.hashCode(), - parentInheritedHash = inheritedHash(parentComputed), - ancestorSelectorHash = - if (snapshot.index.hasAncestorDependentSelectors) { - ancestorSelectorHash( - node, - ) - } else { - 0 - }, - siblingSelectorHash = - if ( - snapshot.index.hasAdjacentSiblingCombinators || snapshot.index.hasGeneralSiblingCombinators - ) { - previousSiblingSelectorHash(node) - } else { - 0 - }, - rootFontSizePx = rootFontSizePx, - viewportWidthPx = viewportWidthPx, - viewportHeightPx = viewportHeightPx, - scope = scope, - ) + val probe = cacheKeyProbe + probe.typeName = node.styleType + probe.nodeId = selectorNodeId(node) + probe.classesHash = node.styleClasses.hashCode() + probe.inlineHash = node.inlineStyleDeclarations.toStableHash() + probe.inspectorHash = if (allowInspectorOverrides) inspectorOverrideHash(node) else 0 + probe.hovered = node.styleHovered + probe.active = node.styleActive + probe.focused = node.styleFocused + probe.disabled = node.styleDisabled + probe.open = node.styleOpen + probe.stylesheetVersion = snapshot.version + probe.themeVersion = if (scope == StyleApplicationScope.Application) themeVersion else 0L + probe.defaultsHash = defaults.hashCode() + probe.parentInheritedHash = inheritedHash(parentComputed) + probe.ancestorSelectorHash = + if (snapshot.index.hasAncestorDependentSelectors) { + ancestorSelectorHash(node) + } else { + 0 + } + probe.siblingSelectorHash = + if (snapshot.index.hasAdjacentSiblingCombinators || snapshot.index.hasGeneralSiblingCombinators) { + previousSiblingSelectorHash(node) + } else { + 0 + } + probe.rootFontSizePx = rootFontSizePx + probe.viewportWidthPx = viewportWidthPx + probe.viewportHeightPx = viewportHeightPx + probe.scope = scope val cached = cache[node] - if (cached != null && cached.key == key) { + if (cached != null && probe.matches(cached.key)) { metrics.cacheHits += 1 val result = node.applyComputedStyle(cached.style) - return NodeApplyResult( - flags = - NodeApplyFlags( - layoutDirty = result.layoutDirty, - visualDirty = result.visualDirty, - ), - cacheHit = true, - ) + var flags = FLAG_CACHE_HIT + if (result.layoutDirty) flags = flags or FLAG_LAYOUT_DIRTY + if (result.visualDirty) flags = flags or FLAG_VISUAL_DIRTY + return flags } val computed = @@ -610,17 +696,13 @@ object StyleEngine { rootFontSizePx = rootFontSizePx, allowInspectorOverrides = allowInspectorOverrides, ) - cache[node] = CachedStyle(key = key, style = computed) + cache[node] = CachedStyle(key = probe.toKey(), style = computed) metrics.recomputedNodes += 1 val result = node.applyComputedStyle(computed) - return NodeApplyResult( - flags = - NodeApplyFlags( - layoutDirty = result.layoutDirty, - visualDirty = result.visualDirty, - ), - cacheHit = false, - ) + var flags = 0 + if (result.layoutDirty) flags = flags or FLAG_LAYOUT_DIRTY + if (result.visualDirty) flags = flags or FLAG_VISUAL_DIRTY + return flags } private fun computeStyle( @@ -684,13 +766,7 @@ object StyleEngine { sourceOrder = rule.sourceOrder, origin = StyleOrigin.Stylesheet, sourceKind = StyleSourceKind.Selector, - sourceLabel = - buildString { - append(selectorLabel(rule.selector)) - append(" @ ") - append(rule.fileName) - if (important) append(" !important") - }, + sourceRule = rule, ) if (shouldReplace(winners[property], candidate)) { winners[property] = candidate @@ -708,7 +784,6 @@ object StyleEngine { sourceOrder = Int.MAX_VALUE - 1, origin = StyleOrigin.Inline, sourceKind = StyleSourceKind.Inline, - sourceLabel = if (important) "inline !important" else "inline", ) if (shouldReplace(winners[property], candidate)) { winners[property] = candidate @@ -725,7 +800,6 @@ object StyleEngine { sourceOrder = Int.MAX_VALUE, origin = StyleOrigin.Inspector, sourceKind = StyleSourceKind.InspectorOverride, - sourceLabel = if (important) "inspector !important" else "inspector", ) if (shouldReplace(winners[property], candidate)) { winners[property] = candidate @@ -770,31 +844,51 @@ object StyleEngine { if (scope != StyleApplicationScope.Application) { return emptyMap() } - val variables = linkedMapOf() - variables.putAll(snapshot.rootVariables) - variables.putAll(themeVariables) - return variables + if (snapshot.version != cachedResolvedVariablesSnapshotVersion || + themeVersion != cachedResolvedVariablesThemeVersion + ) { + val variables = linkedMapOf() + variables.putAll(snapshot.rootVariables) + variables.putAll(themeVariables) + cachedResolvedVariables = variables + cachedResolvedVariablesSnapshotVersion = snapshot.version + cachedResolvedVariablesThemeVersion = themeVersion + } + return cachedResolvedVariables } - private fun matchingCandidates(node: DOMNode, index: RuleIndex): List = - gatherCandidates(node, index) - .filter { selectorMatches(node, it.selector) } - .sortedBy { it.sourceOrder } + private val sourceOrderComparator: Comparator = compareBy { it.sourceOrder } - private fun gatherCandidates(node: DOMNode, index: RuleIndex): List { - val out = linkedSetOf() - out.addAll(index.universalIndex) + private fun matchingCandidates(node: DOMNode, index: RuleIndex): List { + val out = ArrayList() + val seen = HashSet() + addMatchingCandidates(index.universalIndex, node, seen, out) selectorNodeId(node)?.let { id -> - index.idIndex[id]?.let { out.addAll(it) } + index.idIndex[id]?.let { addMatchingCandidates(it, node, seen, out) } } - index.typeIndex[node.styleType]?.let { out.addAll(it) } + index.typeIndex[node.styleType]?.let { addMatchingCandidates(it, node, seen, out) } if (node.parent == null) { - index.typeIndex[ROOT_SELECTOR_INTERNAL]?.let { out.addAll(it) } + index.typeIndex[ROOT_SELECTOR_INTERNAL]?.let { addMatchingCandidates(it, node, seen, out) } } node.styleClasses.forEach { className -> - index.classIndex[className]?.let { out.addAll(it) } + index.classIndex[className]?.let { addMatchingCandidates(it, node, seen, out) } + } + out.sortWith(sourceOrderComparator) + return out + } + + private fun addMatchingCandidates( + rules: List, + node: DOMNode, + seen: MutableSet, + out: MutableList, + ) { + for (index in rules.indices) { + val rule = rules[index] + if (seen.add(rule) && selectorMatches(node, rule.selector)) { + out += rule + } } - return out.toList() } private fun selectorMatches(node: DOMNode, selector: StyleSelector): Boolean { diff --git a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleEngineIncrementalTests.kt b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleEngineIncrementalTests.kt index 969e07e..2c02b7f 100644 --- a/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleEngineIncrementalTests.kt +++ b/core/src/test/kotlin/org/dreamfinity/dsgl/core/style/StyleEngineIncrementalTests.kt @@ -132,6 +132,31 @@ class StyleEngineIncrementalTests { assertEquals(0, report.recomputedNodes) } + @Test + fun `changed node state invalidates style cache entry`() { + installStylesheet( + """ + .marked { color: #AA3344; } + """.trimIndent(), + ) + + val root = ContainerNode(key = "root") + val leaf = TextNode(TextSource.Static("leaf"), key = "leaf").applyParent(root) + StyleEngine.applyStylesRecursivelyDetailed(root) + + val unchanged = StyleEngine.applyStylesRecursivelyDetailed(root) + assertEquals(0, unchanged.recomputedNodes, "Unchanged nodes must be served from the style cache.") + + leaf.setClassNames("marked") + val afterClassChange = StyleEngine.applyStylesRecursivelyDetailed(root) + assertTrue(afterClassChange.recomputedNodes >= 1, "Class change must invalidate the cached style.") + assertEquals(0xFFAA3344.toInt(), leaf.color) + + leaf.setHoveredState(true) + val afterHoverChange = StyleEngine.applyStylesRecursivelyDetailed(root) + assertTrue(afterHoverChange.recomputedNodes >= 1, "Pseudo-state change must invalidate the cached style.") + } + private fun installStylesheet(contents: String) { val dir = Files.createTempDirectory("dsgl-style-incremental-test").toFile() dir.resolve("test.dss").writeText(contents)