diff --git a/build.gradle.kts b/build.gradle.kts index fecf18df6..034d7d2fe 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -37,6 +37,7 @@ subprojects { version = "1.0-SNAPSHOT" repositories { + mavenLocal() jcenter() } diff --git a/gradle.properties b/gradle.properties index 53919c6d4..37aa6ff2c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,7 +22,7 @@ # SOFTWARE. # bootstrapVersion=4.5.2 -coroutinesVersion=1.4.1 +coroutinesVersion=1.4.3 dnsjavaVersion=2.1.9 electronVersion=^10.1.2 electronPackagerVersion=^15.1.0 @@ -31,10 +31,10 @@ inlineStylePrefixerVersion=~6.0.0 javassistVersion=3.27.0-GA javaWebSocketVersion=1.5.1 jqueryVersion=3.5.1 -kotlinVersion=1.4.20 -kotlinExtensionsVersion=1.0.1-pre.129-kotlin-1.4.10 -kotlinReactVersion=16.14.0-pre.125-kotlin-1.4.10 -kotlinStyledComponentsVersion=5.2.0-pre.129-kotlin-1.4.10 +kotlinVersion=1.4.30 +kotlinExtensionsVersion=1.0.1-pre.146-kotlin-1.4.30 +kotlinReactVersion= 17.0.1-pre.146-kotlin-1.4.30 +kotlinStyledComponentsVersion=5.2.1-pre.146-kotlin-1.4.30 ktorVersion=1.4.1 openVersion=^7.2.1 radiumVersion=0.26.1 diff --git a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/DrawEvent.kt b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/DrawEvent.kt index ded1151ac..0419a5df1 100644 --- a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/DrawEvent.kt +++ b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/DrawEvent.kt @@ -23,10 +23,11 @@ */ package org.jetbrains.projector.client.common +import org.jetbrains.projector.common.protocol.toClient.ServerWindowEvent import org.jetbrains.projector.common.protocol.toClient.ServerWindowPaintEvent import org.jetbrains.projector.common.protocol.toClient.ServerWindowStateEvent data class DrawEvent( - val prerequisites: List, + val prerequisites: List, val paintEvent: ServerWindowPaintEvent, ) diff --git a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/Renderer.kt b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/Renderer.kt index 6d8ca2a16..3e9980869 100644 --- a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/Renderer.kt +++ b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/Renderer.kt @@ -23,21 +23,17 @@ */ package org.jetbrains.projector.client.common +import org.jetbrains.projector.client.common.canvas.* import org.jetbrains.projector.client.common.canvas.Canvas.ImageSource -import org.jetbrains.projector.client.common.canvas.Context2d import org.jetbrains.projector.client.common.canvas.Context2d.FillRule -import org.jetbrains.projector.client.common.canvas.Context2d.Matrix import org.jetbrains.projector.client.common.canvas.Context2d.Matrix.Companion.IDENTITY_LIST import org.jetbrains.projector.client.common.canvas.Extensions.applyStrokeData import org.jetbrains.projector.client.common.canvas.Extensions.toContext2dRule import org.jetbrains.projector.client.common.canvas.Extensions.toFillRule -import org.jetbrains.projector.client.common.canvas.Extensions.toFontFaceName -import org.jetbrains.projector.client.common.canvas.PaintColor import org.jetbrains.projector.client.common.canvas.PaintColor.SolidColor import org.jetbrains.projector.client.common.canvas.buffering.RenderingSurface import org.jetbrains.projector.client.common.misc.ParamsProvider import org.jetbrains.projector.client.common.misc.ParamsProvider.REPAINT_AREA -import org.jetbrains.projector.client.common.misc.RepaintAreaSetting import org.jetbrains.projector.common.misc.Defaults import org.jetbrains.projector.common.misc.Do import org.jetbrains.projector.common.protocol.data.* @@ -47,33 +43,38 @@ import kotlin.random.Random class Renderer(private val renderingSurface: RenderingSurface) { private val ctx: Context2d - get() = renderingSurface.canvas.context2d + get() = renderingSurface.canvas.context2d() private val canvasState = CanvasRenderingState() val requestedState = RequestedRenderingState() + private val textScaleCache = HashMap() + private fun applyFillStyle(newFillStyle: PaintColor?) { ctx.setFillStyle(newFillStyle) canvasState.fillStyle = newFillStyle } private fun ensureFillStyle() { - fun realEnsureFillStyle() { + if (REPAINT_AREA.not()) { requestedState.paint?.let { requestedPaint -> - canvasState.fillStyle.let { currentPaint -> - if (currentPaint != requestedPaint) { - applyFillStyle(requestedPaint) + if( canvasState.fillStyle == null ){ + applyFillStyle(requestedPaint) + }else{ + val fillStyle = canvasState.fillStyle?: SolidColor(0xFFFFFF) + if( fillStyle.tpe.ordinal == requestedPaint.tpe.ordinal ){ + when(fillStyle.tpe.ordinal){ + PaintColorType.SolidColor.ordinal -> if(fillStyle.argb != requestedPaint.argb){ + applyFillStyle(requestedPaint) + } + PaintColorType.Gradient.ordinal -> applyFillStyle(requestedPaint) + } } } } } - - Do exhaustive when (val repaintArea = REPAINT_AREA) { - is RepaintAreaSetting.Disabled -> realEnsureFillStyle() - is RepaintAreaSetting.Enabled -> Do exhaustive when (repaintArea.show) { - false -> realEnsureFillStyle() - true -> applyFillStyle(createNextRandomColor()) - } + else { + applyFillStyle(createNextRandomColor()) } } @@ -83,59 +84,80 @@ class Renderer(private val renderingSurface: RenderingSurface) { } private fun ensureStrokeStyle() { - fun realEnsureStrokeStyle() { + + if (REPAINT_AREA) { + applyStrokeStyle(createNextRandomColor()) + } + else { requestedState.paint?.let { requestedPaint -> - canvasState.strokeStyle.let { currentPaint -> - if (currentPaint != requestedPaint) { - applyStrokeStyle(requestedPaint) + if( canvasState.strokeStyle == null){ + applyStrokeStyle(requestedPaint) + }else{ + val strokeStyle = canvasState.strokeStyle ?: SolidColor(0xFFFFFF) + if( strokeStyle.tpe.ordinal == requestedPaint.tpe.ordinal ){ + when(strokeStyle.tpe.ordinal){ + PaintColorType.SolidColor.ordinal -> if(strokeStyle.argb != requestedPaint.argb){ + applyStrokeStyle(requestedPaint) + } + PaintColorType.Gradient.ordinal -> applyStrokeStyle(requestedPaint) + } } } } } - - Do exhaustive when (val repaintArea = REPAINT_AREA) { - is RepaintAreaSetting.Disabled -> realEnsureStrokeStyle() - is RepaintAreaSetting.Enabled -> Do exhaustive when (repaintArea.show) { - false -> realEnsureStrokeStyle() - true -> applyStrokeStyle(createNextRandomColor()) - } - } } - private fun applyTransform(newTransform: List) { + private fun applyTransform(newTransform: DoubleArray) { renderingSurface.scalingRatio.let { - ctx.setTransform(it, 0.0, 0.0, it, 0.0, 0.0) + with(newTransform) { + ctx.setTransform(it,.0,.0,it,.0,.0) + ctx.transform(get(0),get(1), get(2), get(3), get(4), get(5)) + } } - - with(Matrix(newTransform)) { ctx.transform(a, b, c, d, e, f) } - canvasState.transform = newTransform } private fun ensureTransform() { requestedState.transform.let { requestedTransform -> canvasState.transform.let { currentTransform -> - if (currentTransform != requestedTransform) { + if (!(requestedTransform[0] == currentTransform[0] && + requestedTransform[1] == currentTransform[1] && + requestedTransform[2] == currentTransform[2] && + requestedTransform[3] == currentTransform[3] && + requestedTransform[4] == currentTransform[4] && + requestedTransform[5] == currentTransform[5] + )) { applyTransform(requestedTransform) } } } } - private fun applyClip(newIdentitySpaceClip: CommonShape?) { - ctx.restore() - ctx.save() - - renderingSurface.scalingRatio.let { - ctx.setTransform(it, 0.0, 0.0, it, 0.0, 0.0) + private fun doClip(){ + canvasState.identitySpaceClip?.apply { + when(this.tpe.ordinal){ + CommonShapeType.CommonRectangle.ordinal -> ctx.clip() + CommonShapeType.CommonPath.ordinal -> ctx.clip(this.asUnsafe().winding.toFillRule()) + } } + } + private fun applyClip(newIdentitySpaceClip: CommonShape?) { newIdentitySpaceClip?.apply { ctx.beginPath() - Do exhaustive when (this) { - is CommonRectangle -> ctx.rect(x, y, width, height) - is CommonPath -> ctx.moveBySegments(segments) + renderingSurface.scalingRatio.let { + ctx.setTransform(it, 0.0, 0.0, it, 0.0, 0.0) + } + + when(this.tpe.ordinal){ + CommonShapeType.CommonRectangle.ordinal -> with(this.asUnsafe()){ + ctx.rect(x,y,width,height) + } + + CommonShapeType.CommonPath.ordinal -> with(this.asUnsafe()){ + ctx.moveBySegments(segments) + } } if (ParamsProvider.CLIPPING_BORDERS) { @@ -151,30 +173,16 @@ class Renderer(private val renderingSurface: RenderingSurface) { ctx.restore() } - Do exhaustive when (this) { - is CommonRectangle -> ctx.clip() - is CommonPath -> ctx.clip(winding.toFillRule()) - } } - + canvasState.transform = IDENTITY_LIST canvasState.identitySpaceClip = newIdentitySpaceClip - - with(canvasState) { - applyTransform(transform) - applyStrokeStyle(strokeStyle) - applyFillStyle(fillStyle) - applyStroke(strokeData) - applyFont(font) - } } - private fun ensureClip() { - requestedState.identitySpaceClip.let { requestedClip -> - canvasState.identitySpaceClip.let { currentClip -> - if (currentClip != requestedClip) { - applyClip(requestedClip) - } - } + private fun drawClipRegion() { + if(requestedState.identitySpaceClip == null){ + applyClip(canvasState.identitySpaceClip) + }else{ + applyClip(requestedState.identitySpaceClip) } } @@ -184,11 +192,17 @@ class Renderer(private val renderingSurface: RenderingSurface) { } private fun ensureStroke() { - requestedState.strokeData.let { requestedStroke -> - canvasState.strokeData.let { currentStroke -> - if (currentStroke != requestedStroke) { - applyStroke(requestedStroke) - } + requestedState.strokeData?.asUnsafe().let { + val curr = canvasState.strokeData.asUnsafe() + if(!( + it.dashPhase == curr.dashPhase && + it.lineWidth == curr.lineWidth && + it.miterLimit == curr.miterLimit && + it.endCap.ordinal == curr.endCap.ordinal && + it.lineJoin.ordinal == curr.lineJoin.ordinal && + it.dashArray?.size == curr.dashArray?.size ) + ){ + applyStroke(it) } } } @@ -204,18 +218,15 @@ class Renderer(private val renderingSurface: RenderingSurface) { } } - private fun applyFont(newFont: String) { - ctx.setFont(newFont) - canvasState.font = newFont + private fun applyFont(fontSize: Int, fontName: String) { + ctx.setFont(Extensions.fontSizeStrCache[fontSize] + fontName) + canvasState.fontSize = fontSize + canvasState.fontName = fontName } private fun ensureFont() { - requestedState.font.let { requestedFont -> - canvasState.font.let { currentFont -> - if (currentFont != requestedFont) { - applyFont(requestedFont) - } - } + if (requestedState.fontSize != canvasState.fontSize || requestedState.fontName.hashCode() != canvasState.fontName.hashCode()) { + applyFont(requestedState.fontSize, requestedState.fontName) } } @@ -225,12 +236,8 @@ class Renderer(private val renderingSurface: RenderingSurface) { } private fun ensureRule() { - requestedState.rule.let { requestedRule -> - canvasState.rule.let { currentRule -> - if (currentRule != requestedRule) { - applyRule(requestedRule) - } - } + if (canvasState.rule != requestedState.rule) { + applyRule(requestedState.rule) } } @@ -240,12 +247,8 @@ class Renderer(private val renderingSurface: RenderingSurface) { } private fun ensureAlpha() { - requestedState.alpha.let { requestedAlpha -> - canvasState.alpha.let { currentAlpha -> - if (currentAlpha != requestedAlpha) { - applyAlpha(requestedAlpha) - } - } + if (canvasState.alpha != requestedState.alpha) { + applyAlpha(requestedState.alpha) } } @@ -284,7 +287,7 @@ class Renderer(private val renderingSurface: RenderingSurface) { } fun drawString(string: String, x: Double, y: Double, desiredWidth: Double) { - ensureClip() + drawClipRegion() ensureTransform() ensureFillStyle() ensureFont() @@ -294,74 +297,91 @@ class Renderer(private val renderingSurface: RenderingSurface) { } ctx.apply { - val textDimensions = measureText(string) - save() + doClip() + + val textRescale = if (!textScaleCache.containsKey(canvasState.fontSize)) { + val dimension = ctx.measureText(string) + val scale = desiredWidth / dimension.x + textScaleCache.put(canvasState.fontSize, scale) + scale + } + else { + textScaleCache.get(canvasState.fontSize) ?: 1.0 + } - val width = textDimensions.x - translate(x, y) - scale(desiredWidth / width, 1.0) - fillText(string, 0.0, 0.0) - restore() + // + // + + + if (textRescale < 1 - 1e-4 || textRescale > 1 + 1e-4) { + translate(x, y) + scale(textRescale, 1.0) + fillText(string, 0.0, 0.0) + } else { + fillText(string, x, y) + } + if (ParamsProvider.SHOW_TEXT_WIDTH) { beginPath() moveTo(x, y) lineTo(x + desiredWidth, y) stroke() - - val height = textDimensions.y - beginPath() - moveTo(x, y - height) - lineTo(x + width, y - height) - stroke() } + restore() } } fun drawLine(x1: Double, y1: Double, x2: Double, y2: Double) { - ensureClip() + drawClipRegion() ensureTransform() ensureStrokeStyle() ensureStroke() ensureComposite() - - ctx.apply { + with(ctx) { + save() + doClip() beginPath() moveTo(x1, y1) lineTo(x2, y2) stroke() + restore() } } fun paintRoundRect(paintType: PaintType, x: Double, y: Double, w: Double, h: Double, r1: Double, r2: Double) { - ensureClip() + drawClipRegion() ensureTransform() ensurePaint(paintType) ensureComposite() @Suppress("NAME_SHADOWING") val r1 = minOf(r1, w / 2) @Suppress("NAME_SHADOWING") val r2 = minOf(r2, h / 2) - ctx.apply { + with(ctx) { + save() + doClip() beginPath() roundedRect(x, y, w, h, r1, r2) + when (paintType.ordinal) { + PaintType.FILL.ordinal -> fill() - Do exhaustive when (paintType) { - PaintType.FILL -> fill() - - PaintType.DRAW -> stroke() + PaintType.DRAW.ordinal -> stroke() } + restore() } } fun paintPath(paintType: PaintType, path: CommonPath) { - ensureClip() + drawClipRegion() ensureTransform() ensurePaint(paintType) ensureComposite() - ctx.apply { + with(ctx) { + save() + doClip() beginPath() moveBySegments(path.segments) @@ -370,29 +390,41 @@ class Renderer(private val renderingSurface: RenderingSurface) { PaintType.DRAW -> stroke() } + restore() } } fun paintRect(paintType: PaintType, x: Double, y: Double, width: Double, height: Double) { - ensureClip() + drawClipRegion() ensureTransform() ensurePaint(paintType) ensureComposite() - Do exhaustive when (paintType) { - PaintType.FILL -> ctx.fillRect(x, y, width, height) - - PaintType.DRAW -> ctx.strokeRect(x, y, width, height) + with(ctx) { + save() + doClip() + //TODO: hacks, kotlin's `when` pattern matching with type casting is expensive. + /* + using a type enum signature to avoid type checking in JS runtime which is expensive. + */ + when (paintType.ordinal) { + PaintType.FILL.ordinal -> fillRect(x, y, width, height) + + PaintType.DRAW.ordinal -> strokeRect(x, y, width, height) + } + restore() } } fun paintPolygon(paintType: PaintType, points: List) { - ensureClip() + drawClipRegion() ensureTransform() ensurePaint(paintType) ensureComposite() ctx.apply { + save() + doClip() beginPath() moveByPoints(points) @@ -401,16 +433,11 @@ class Renderer(private val renderingSurface: RenderingSurface) { PaintType.DRAW -> stroke() } + restore() } } fun drawPolyline(points: List) { - ensureClip() - ensureTransform() - ensureStrokeStyle() - ensureStroke() - ensureComposite() - points .windowed(2) .forEach { (p1, p2) -> @@ -422,21 +449,13 @@ class Renderer(private val renderingSurface: RenderingSurface) { requestedState.identitySpaceClip = identitySpaceClip } - fun setTransform(tx: List) { + fun setTransform(tx: DoubleArray) { requestedState.transform = tx } - fun setFont(fontId: Short?, fontSize: Int, ligaturesOn: Boolean) { - val font = if (fontId == null) { - logger.debug { "null is used as a font ID. Using Arial..." } - - "${fontSize}px Arial" - } - else { - "${fontSize}px ${fontId.toFontFaceName()}" - } - - requestedState.font = font + fun setFont(fontId: Int?, fontSize: Int, ligaturesOn: Boolean) { + requestedState.fontSize = fontSize + requestedState.fontName = fontId?.let { Extensions.serverFontNameCache[fontId] } ?: "Arial" renderingSurface.canvas.fontVariantLigatures = ligaturesOn.toLigatureVariant() renderingSurface.canvas } @@ -448,46 +467,55 @@ class Renderer(private val renderingSurface: RenderingSurface) { fun drawImage(image: ImageSource, x: Double, y: Double) { if (image.isEmpty()) return - ensureClip() + drawClipRegion() ensureTransform() ensureComposite() + ctx.save() + doClip() ctx.drawImage(image, x, y) + ctx.restore() } fun drawImage(image: ImageSource, x: Double, y: Double, width: Double, height: Double) { if (image.isEmpty()) return - ensureClip() + drawClipRegion() ensureTransform() ensureComposite() + ctx.save() + doClip() ctx.drawImage(image, x, y, width, height) + ctx.restore() } fun drawImage(image: ImageSource, sx: Double, sy: Double, sw: Double, sh: Double, dx: Double, dy: Double, dw: Double, dh: Double) { if (image.isEmpty()) return - ensureClip() + drawClipRegion() ensureTransform() ensureComposite() + ctx.save() + doClip() ctx.drawImage( image, sx = sx, sy = sy, sw = sw, sh = sh, dx = dx, dy = dy, dw = dw, dh = dh ) + ctx.restore() } - fun drawImage(image: ImageSource, tx: List) { + fun drawImage(image: ImageSource, tx: DoubleArray) { if (image.isEmpty()) return - ensureClip() + drawClipRegion() ensureTransform() ensureComposite() ctx.apply { save() - + doClip() transform(tx[0], tx[1], tx[2], tx[3], tx[4], tx[5]) ctx.drawImage(image, 0.0, 0.0) @@ -510,12 +538,14 @@ class Renderer(private val renderingSurface: RenderingSurface) { } fun paintOval(paintType: PaintType, x: Double, y: Double, width: Double, height: Double) { - ensureClip() + drawClipRegion() ensureTransform() ensurePaint(paintType) ensureComposite() ctx.apply { + save() + doClip() beginPath() ellipse(x + width / 2, y + height / 2, width / 2, height / 2, 0.0, 0.0, 2 * PI) when (paintType) { @@ -523,11 +553,12 @@ class Renderer(private val renderingSurface: RenderingSurface) { PaintType.FILL -> fill() } + restore() } } fun copyArea(x: Double, y: Double, width: Double, height: Double, dx: Double, dy: Double) { - ensureClip() + drawClipRegion() ensureTransform() ensureComposite() @@ -545,6 +576,7 @@ class Renderer(private val renderingSurface: RenderingSurface) { // Finally, we paint the image. save() + doClip() // 1: beginPath() @@ -581,9 +613,10 @@ class Renderer(private val renderingSurface: RenderingSurface) { private data class CanvasRenderingState( var identitySpaceClip: CommonShape? = DEFAULT_IDENTITY_SPACE_CLIP, - var transform: List = DEFAULT_TRANSFORM, + var transform: DoubleArray = DEFAULT_TRANSFORM, var strokeData: StrokeData = DEFAULT_STROKE_DATA, - var font: String = DEFAULT_FONT, + var fontSize: Int = Defaults.FONT_SIZE, + var fontName: String = Defaults.FONT_NAME, var rule: AlphaCompositeRule = DEFAULT_RULE, var alpha: Double = DEFAULT_ALPHA, var fillStyle: PaintColor? = DEFAULT_FILL_STYLE, @@ -594,19 +627,50 @@ class Renderer(private val renderingSurface: RenderingSurface) { identitySpaceClip = DEFAULT_IDENTITY_SPACE_CLIP transform = DEFAULT_TRANSFORM strokeData = DEFAULT_STROKE_DATA - font = DEFAULT_FONT - rule = DEFAULT_RULE + fontSize = Defaults.FONT_SIZE + fontName = Defaults.FONT_NAME alpha = DEFAULT_ALPHA fillStyle = DEFAULT_FILL_STYLE strokeStyle = DEFAULT_STROKE_STYLE } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as CanvasRenderingState + + if (identitySpaceClip != other.identitySpaceClip) return false + if (!transform.contentEquals(other.transform)) return false + if (strokeData != other.strokeData) return false + if (fontSize != other.fontSize) return false + if (fontName != other.fontName) return false + if (rule != other.rule) return false + if (alpha != other.alpha) return false + if (fillStyle != other.fillStyle) return false + if (strokeStyle != other.strokeStyle) return false + + return true + } + + override fun hashCode(): Int { + var result = identitySpaceClip?.hashCode() ?: 0 + result = 31 * result + transform.contentHashCode() + result = 31 * result + strokeData.hashCode() + result = 31 * result + fontSize + result = 31 * result + fontName.hashCode() + result = 31 * result + rule.hashCode() + result = 31 * result + alpha.hashCode() + result = 31 * result + (fillStyle?.hashCode() ?: 0) + result = 31 * result + (strokeStyle?.hashCode() ?: 0) + return result + } + companion object { - private val DEFAULT_IDENTITY_SPACE_CLIP: CommonShape? = null - private val DEFAULT_TRANSFORM: List = IDENTITY_LIST + private val DEFAULT_IDENTITY_SPACE_CLIP: CommonShape? = CommonRectangle(0.0,0.0,0.0,0.0) + private val DEFAULT_TRANSFORM: DoubleArray = IDENTITY_LIST private val DEFAULT_STROKE_DATA: StrokeData = Defaults.STROKE - private var DEFAULT_FONT: String = "${Defaults.FONT_SIZE}px Arial" private val DEFAULT_RULE: AlphaCompositeRule = AlphaCompositeRule.SRC_OVER private const val DEFAULT_ALPHA: Double = 1.0 private val DEFAULT_FILL_STYLE: PaintColor? = null @@ -616,12 +680,14 @@ class Renderer(private val renderingSurface: RenderingSurface) { data class RequestedRenderingState( var identitySpaceClip: CommonShape? = null, - var transform: List = IDENTITY_LIST, + var transform: DoubleArray = IDENTITY_LIST, var strokeData: StrokeData = Defaults.STROKE, var font: String = "${Defaults.FONT_SIZE}px Arial", var rule: AlphaCompositeRule = AlphaCompositeRule.SRC_OVER, var alpha: Double = 1.0, var paint: PaintColor? = SolidColor(Defaults.FOREGROUND_COLOR_ARGB), + var fontSize: Int = Defaults.FONT_SIZE, + var fontName: String = "Arial", ) } } diff --git a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/RenderingSurfaceProcessor.kt b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/RenderingSurfaceProcessor.kt new file mode 100644 index 000000000..1c777c94b --- /dev/null +++ b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/RenderingSurfaceProcessor.kt @@ -0,0 +1,31 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 JetBrains s.r.o. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jetbrains.projector.client.common + +import org.jetbrains.projector.common.protocol.toClient.ServerWindowEvent + +interface RenderingSurfaceProcessor { + + fun process(events: List): List? +} diff --git a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/SingleRenderingSurfaceProcessor.kt b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/SingleRenderingSurfaceProcessor.kt index 6aa5ac658..033e22666 100644 --- a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/SingleRenderingSurfaceProcessor.kt +++ b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/SingleRenderingSurfaceProcessor.kt @@ -25,115 +25,125 @@ package org.jetbrains.projector.client.common import org.jetbrains.projector.client.common.Renderer.Companion.RequestedRenderingState import org.jetbrains.projector.client.common.canvas.Canvas +import org.jetbrains.projector.client.common.canvas.asUnsafe import org.jetbrains.projector.client.common.canvas.buffering.RenderingSurface import org.jetbrains.projector.client.common.misc.ImageCacher import org.jetbrains.projector.client.common.misc.ParamsProvider import org.jetbrains.projector.common.misc.Do import org.jetbrains.projector.common.protocol.data.ImageEventInfo import org.jetbrains.projector.common.protocol.data.PaintValue +import org.jetbrains.projector.common.protocol.data.PaintValueType import org.jetbrains.projector.common.protocol.toClient.* import org.jetbrains.projector.util.logging.Logger -class SingleRenderingSurfaceProcessor(renderingSurface: RenderingSurface) { +class SingleRenderingSurfaceProcessor(renderingSurface: RenderingSurface) : RenderingSurfaceProcessor { private val renderer = Renderer(renderingSurface) private val stateSaver = StateSaver(renderer, renderingSurface) @OptIn(ExperimentalStdlibApi::class) - fun process(drawEvents: ArrayDeque) { + override fun process(drawEvents: List): List? { stateSaver.restoreIfNeeded() - - var removing = true - - drawEvents.removeAll { drawEvent -> - val drawIsSuccessful = handleDrawEvent(drawEvent) - - if (!drawIsSuccessful && removing) { - stateSaver.save() - - removing = false + var start = 0 + var curr = 0 + var pendingEvents: MutableList? = null + for (drawEvent in drawEvents) { + curr ++ + if(drawEvent.tpe.isDrawEvent()){ + if(!handleDrawEvents(drawEvents.subList(start,curr))){ + if(start == 0 && curr == drawEvents.size){ + return drawEvents + }else { + if (pendingEvents == null) { + pendingEvents = mutableListOf() + } + pendingEvents.addAll(drawEvents.subList(start, curr)) + } + } + start = curr } - - removing } + + return pendingEvents } - private fun handleDrawEvent(command: DrawEvent): Boolean { - command.prerequisites.forEach { - Do exhaustive when (it) { - is ServerSetCompositeEvent -> renderer.setComposite(it.composite) + private fun handleDrawEvents(events: List): Boolean { + var succeed = true + events.forEach { + when (it.tpe.ordinal) { + EventType.ServerSetCompositeEvent.ordinal -> renderer.setComposite(it.asUnsafe().composite) + + EventType.ServerSetPaintEvent.ordinal -> (it.asUnsafe()).paint.let { paintValue -> + when (paintValue.tpe.ordinal) { + PaintValueType.Color.ordinal -> renderer.setColor(paintValue.asUnsafe().argb) + + PaintValueType.Gradient.ordinal -> with(paintValue.asUnsafe()) { + renderer.setGradientPaint( + p1 = p1, + p2 = p2, + color1 = argb1, + color2 = argb2 + ) + } + PaintValueType.Unknown.ordinal -> logUnsupportedCommand(it) + } + } - is ServerSetPaintEvent -> it.paint.let { paintValue -> - when (paintValue) { - is PaintValue.Color -> renderer.setColor(paintValue.argb) + EventType.ServerSetClipEvent.ordinal -> renderer.setClip((it.asUnsafe()).shape) - is PaintValue.Gradient -> renderer.setGradientPaint( - p1 = paintValue.p1, - p2 = paintValue.p2, - color1 = paintValue.argb1, - color2 = paintValue.argb2 - ) + EventType.ServerSetFontEvent.ordinal -> (it.asUnsafe()).apply{ renderer.setFont(fontId.asUnsafe(),fontSize, ligaturesOn) } - is PaintValue.Unknown -> logUnsupportedCommand(it) - } - } + EventType.ServerSetStrokeEvent.ordinal -> renderer.setStroke((it.asUnsafe()).strokeData) - is ServerSetClipEvent -> renderer.setClip(it.shape) + EventType.ServerSetTransformEvent.ordinal -> renderer.setTransform((it.asUnsafe()).tx) - is ServerSetFontEvent -> renderer.setFont(it.fontId, it.fontSize, it.ligaturesOn) - is ServerSetStrokeEvent -> renderer.setStroke(it.strokeData) - is ServerSetTransformEvent -> renderer.setTransform(it.tx) + EventType.ServerDrawLineEvent.ordinal -> (it.asUnsafe()).apply { + renderer.drawLine( + x1 = x1.asUnsafe(), + y1 = y1.asUnsafe(), + x2 = x2.asUnsafe(), + y2 = y2.asUnsafe() + ) + } - is ServerWindowToDoStateEvent -> logUnsupportedCommand(it) - } - } + EventType.ServerDrawStringEvent.ordinal -> (it.asUnsafe()).apply { + renderer.drawString( + string = str, + x = x, + y = y, + desiredWidth = desiredWidth + ) + } - var success = true - - with(command.paintEvent) { - Do exhaustive when (this) { - is ServerDrawLineEvent -> renderer.drawLine( - x1 = x1.toDouble(), - y1 = y1.toDouble(), - x2 = x2.toDouble(), - y2 = y2.toDouble() - ) - - is ServerDrawStringEvent -> renderer.drawString( - string = str, - x = x, - y = y, - desiredWidth = desiredWidth - ) - - is ServerPaintRectEvent -> renderer.paintRect( - paintType = paintType, - x = x, - y = y, - width = width, - height = height - ) + EventType.ServerPaintRectEvent.ordinal -> (it.asUnsafe()).apply { + renderer.paintRect( + paintType = paintType, + x = x, + y = y, + width = width, + height = height + ) + } - is ServerPaintRoundRectEvent -> renderer.paintRoundRect( - paintType = paintType, - x = x.toDouble(), - y = y.toDouble(), - w = width.toDouble(), - h = height.toDouble(), - r1 = arcWidth.toDouble(), - r2 = arcHeight.toDouble() - ) - - is ServerDrawImageEvent -> { + EventType.ServerPaintRoundRectEvent.ordinal -> (it.asUnsafe() ).apply { + renderer.paintRoundRect( + paintType = paintType, + x = x.asUnsafe(), + y = y.asUnsafe(), + w = width.asUnsafe(), + h = height.asUnsafe(), + r1 = arcWidth.asUnsafe(), + r2 = arcHeight.asUnsafe() + ) + } + + EventType.ServerDrawImageEvent.ordinal -> (it.asUnsafe()).apply{ val image = ImageCacher.getImageData(imageId) - if (image == null) { - success = false - } - else { + if (image != null){ val info = imageEventInfo Do exhaustive when (info) { @@ -146,68 +156,71 @@ class SingleRenderingSurfaceProcessor(renderingSurface: RenderingSurface) { renderer.drawImage( image = image, - dx = info.dx1.toDouble(), - dy = info.dy1.toDouble(), - dw = dw.toDouble(), - dh = dh.toDouble(), - sx = info.sx1.toDouble(), - sy = info.sy1.toDouble(), - sw = sw.toDouble(), - sh = sh.toDouble() + dx = info.dx1.asUnsafe(), + dy = info.dy1.asUnsafe(), + dw = dw.asUnsafe(), + dh = dh.asUnsafe(), + sx = info.sx1.asUnsafe(), + sy = info.sy1.asUnsafe(), + sw = sw.asUnsafe(), + sh = sh.asUnsafe() ) } is ImageEventInfo.Xy -> { // todo: manage bgcolor - renderer.drawImage(image, info.x.toDouble(), info.y.toDouble()) + renderer.drawImage(image, info.x.asUnsafe(), info.y.asUnsafe()) } is ImageEventInfo.XyWh -> { // todo: manage bgcolor renderer.drawImage( image = image, - x = info.x.toDouble(), - y = info.y.toDouble(), - width = info.width.toDouble(), - height = info.height.toDouble() + x = info.x.asUnsafe(), + y = info.y.asUnsafe(), + width = info.width.asUnsafe(), + height = info.height.asUnsafe() ) } is ImageEventInfo.Transformed -> renderer.drawImage(image, info.tx) } + }else{ + succeed = false } } - is ServerPaintPathEvent -> renderer.paintPath(paintType, path) + EventType.ServerPaintPathEvent.ordinal -> (it.asUnsafe()).apply{ renderer.paintPath(paintType, path)} - is ServerPaintOvalEvent -> renderer.paintOval( + EventType.ServerPaintOvalEvent.ordinal -> (it.asUnsafe()).apply{ renderer.paintOval( paintType = paintType, - x = x.toDouble(), - y = y.toDouble(), - width = width.toDouble(), - height = height.toDouble() - ) - - is ServerPaintPolygonEvent -> renderer.paintPolygon(paintType, points) - - is ServerDrawPolylineEvent -> renderer.drawPolyline(points) - - is ServerCopyAreaEvent -> renderer.copyArea( - x = x.toDouble(), - y = y.toDouble(), - width = width.toDouble(), - height = height.toDouble(), - dx = dx.toDouble(), - dy = dy.toDouble() - ) - - is ServerWindowToDoPaintEvent -> logUnsupportedCommand(this) + x = x.asUnsafe(), + y = y.asUnsafe(), + width = width.asUnsafe(), + height = height.asUnsafe() + )} + + EventType.ServerPaintPolygonEvent.ordinal -> (it.asUnsafe()).apply{ renderer.paintPolygon(paintType, points)} + + EventType.ServerDrawPolylineEvent.ordinal -> renderer.drawPolyline((it.asUnsafe()).points) + + EventType.ServerCopyAreaEvent.ordinal -> (it.asUnsafe()).apply { + renderer.copyArea( + x = x.asUnsafe(), + y = y.asUnsafe(), + width = width.asUnsafe(), + height = height.asUnsafe(), + dx = dx.asUnsafe(), + dy = dy.asUnsafe() + ) + } + + else -> logUnsupportedCommand(it) } } - - return success + return succeed } - + private class StateSaver(private val renderer: Renderer, private val renderingSurface: RenderingSurface) { private var lastSuccessfulState: LastSuccessfulState? = null @@ -237,7 +250,7 @@ class SingleRenderingSurfaceProcessor(renderingSurface: RenderingSurface) { } } - private class LastSuccessfulState(val renderingState: RequestedRenderingState, val image: Canvas.Snapshot) + private class LastSuccessfulState(val renderingState: RequestedRenderingState, val image: Canvas.ImageSource) } companion object { @@ -249,28 +262,5 @@ class SingleRenderingSurfaceProcessor(renderingSurface: RenderingSurface) { logger.debug { "Unsupported: $command" } } } - - fun List.shrinkByPaintEvents(): List { - val result = mutableListOf() - - var prerequisites = mutableListOf() - - this.forEach { - Do exhaustive when (it) { - is ServerWindowStateEvent -> prerequisites.add(it) - - is ServerWindowPaintEvent -> { - result.add(DrawEvent(prerequisites, it)) - prerequisites = mutableListOf() - } - } - } - - if (prerequisites.isNotEmpty()) { - logger.error { "Bad commands received from server: ${prerequisites.size} state events are at the end" } - } - - return result - } } } diff --git a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/Canvas.kt b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/Canvas.kt index 361b7b2ad..72b26223d 100644 --- a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/Canvas.kt +++ b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/Canvas.kt @@ -47,7 +47,9 @@ package org.jetbrains.projector.client.common.canvas */ interface Canvas { - val context2d: Context2d + fun context2d(): Context2d + fun bitmapContext(): ContextBitmapRenderer + var width: Int var height: Int var fontVariantLigatures: String @@ -55,12 +57,10 @@ interface Canvas { set(value) {} val imageSource: ImageSource - fun takeSnapshot(): Snapshot + fun takeSnapshot(): ImageSource interface ImageSource { fun isEmpty(): Boolean } - // Unchangeable copy of canvas data - interface Snapshot : ImageSource } diff --git a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/CanvasFactory.kt b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/CanvasFactory.kt index d593206b9..2cf9a4804 100644 --- a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/CanvasFactory.kt +++ b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/CanvasFactory.kt @@ -25,6 +25,7 @@ package org.jetbrains.projector.client.common.canvas expect object CanvasFactory { fun create(): Canvas + fun create(offscreen: Boolean): Canvas fun createImageSource(pngBase64: String, onLoad: (Canvas.ImageSource) -> Unit) fun createEmptyImageSource(onLoad: (Canvas.ImageSource) -> Unit) } diff --git a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/Context2d.kt b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/Context2d.kt index 5c9b9b6ce..21d89cd94 100644 --- a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/Context2d.kt +++ b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/Context2d.kt @@ -28,7 +28,7 @@ import org.jetbrains.projector.client.common.canvas.PaintColor.Gradient import org.jetbrains.projector.common.protocol.data.PathSegment import org.jetbrains.projector.common.protocol.data.Point -interface Context2d { +interface Context2d{ fun clearRect(x: Double, y: Double, w: Double, h: Double) fun drawImage(imageSource: ImageSource, x: Double, y: Double) fun drawImage(imageSource: ImageSource, x: Double, y: Double, dw: Double, dh: Double) @@ -89,7 +89,8 @@ interface Context2d { fun setTextBaseline(baseline: TextBaseline) fun setTextAlign(align: TextAlign) fun setTransform(m11: Double, m12: Double, m21: Double, m22: Double, dx: Double, dy: Double) - fun setLineDash(lineDash: DoubleArray) + fun setTransform(matrix: Matrix) + fun setLineDash(lineDash: Array) fun setLineDashOffset(offset: Double) fun measureText(str: String): Point @@ -145,12 +146,12 @@ interface Context2d { * f Vertical translation (moving). */ class Matrix(val a: Double, val b: Double, val c: Double, val d: Double, val e: Double, val f: Double) { - constructor(list: List) : this(list[0], list[1], list[2], list[3], list[4], list[5]) + constructor(list: DoubleArray) : this(list[0], list[1], list[2], list[3], list[4], list[5]) companion object { private val IDENTITY = Matrix(1.0, 0.0, 0.0, 1.0, 0.0, 0.0) - val IDENTITY_LIST = with(IDENTITY) { listOf(a, b, c, d, e, f) } + val IDENTITY_LIST = with(IDENTITY) { doubleArrayOf(a,b,c,d,e,f) } } } } diff --git a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/ContextBitmapRenderer.kt b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/ContextBitmapRenderer.kt new file mode 100644 index 000000000..5306b8420 --- /dev/null +++ b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/ContextBitmapRenderer.kt @@ -0,0 +1,30 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 JetBrains s.r.o. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jetbrains.projector.client.common.canvas + +interface ContextBitmapRenderer { + + fun transferFromImageBitmap(bitmap: Canvas.ImageSource) + +} diff --git a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/Extensions.kt b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/Extensions.kt index c581da12a..16dc66097 100644 --- a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/Extensions.kt +++ b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/Extensions.kt @@ -53,7 +53,7 @@ object Extensions { this.height = height if (scalingChanged) { - this.context2d.drawImage( + this.context2d().drawImage( snapshot, 0.0, 0.0, @@ -62,7 +62,7 @@ object Extensions { ) // save previous image with scaling } else { - this.context2d.drawImage(snapshot, 0.0, 0.0) // save previous image with the same size to avoid stretching + this.context2d().drawImage(snapshot, 0.0, 0.0) // save previous image with the same size to avoid stretching } } @@ -78,7 +78,7 @@ object Extensions { setLineCap(strokeData.endCap.toCanvasLineCap()) setLineJoin(strokeData.lineJoin.toCanvasLineJoin()) setMiterLimit(strokeData.miterLimit.toDouble()) - setLineDash(strokeData.dashArray?.map(Float::toDouble)?.toDoubleArray() ?: DoubleArray(0)) + strokeData.dashArray?.let(::setLineDash) setLineDashOffset(strokeData.dashPhase.toDouble()) } } @@ -118,18 +118,11 @@ object Extensions { } } - fun Short.toFontFaceName(): String = "serverFont$this" + val serverFontNameCache = IntRange(0,255).map { "serverFont$it" }.toTypedArray() + val fontSizeStrCache = IntRange(0,255).map { "${it}px " }.toTypedArray() + val fontSizeScalingFactorCache = HashMap() - /* Creates an rgba(...) string (JS-like) by an ARGB number (Java-like). */ - fun Int.argbIntToRgbaString(): String { - val colorValue = this - - val b = colorValue and 0xFF - val g = (colorValue ushr 8) and 0xFF - val r = (colorValue ushr 16) and 0xFF - val a = ((colorValue ushr 24) and 0xFF) / 255.0 - - return "rgba($r,$g,$b,$a)" - } } +expect fun Any?.asUnsafe(): T + diff --git a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/PaintColor.kt b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/PaintColor.kt index 248c15e5c..c976d21ee 100644 --- a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/PaintColor.kt +++ b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/PaintColor.kt @@ -24,11 +24,27 @@ package org.jetbrains.projector.client.common.canvas sealed class PaintColor { - data class SolidColor(val argb: Int) : PaintColor() { + + abstract val tpe: PaintColorType + abstract val argb: Int + + data class SolidColor(override val argb: Int) : PaintColor() { + override val tpe: PaintColorType + get() = PaintColorType.SolidColor + constructor(argb: Long) : this(argb.toInt()) } abstract class Gradient : PaintColor() { + + override val tpe: PaintColorType + get() = PaintColorType.Gradient + abstract fun addColorStop(offset: Double, argb: Int) } } + +enum class PaintColorType { + SolidColor, + Gradient +} diff --git a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/RenderingContext.kt b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/RenderingContext.kt new file mode 100644 index 000000000..2b5fc6f18 --- /dev/null +++ b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/RenderingContext.kt @@ -0,0 +1,27 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 JetBrains s.r.o. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jetbrains.projector.client.common.canvas + +interface RenderingContext { +} diff --git a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/buffering/DoubleBufferedRenderingSurface.kt b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/buffering/DoubleBufferedRenderingSurface.kt index e8eeb2ddf..603654d88 100644 --- a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/buffering/DoubleBufferedRenderingSurface.kt +++ b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/canvas/buffering/DoubleBufferedRenderingSurface.kt @@ -25,7 +25,6 @@ package org.jetbrains.projector.client.common.canvas.buffering import org.jetbrains.projector.client.common.canvas.Canvas import org.jetbrains.projector.client.common.canvas.CanvasFactory -import org.jetbrains.projector.client.common.canvas.Extensions.resizeSavingImage class DoubleBufferedRenderingSurface(private val target: Canvas) : RenderingSurface { @@ -37,21 +36,34 @@ class DoubleBufferedRenderingSurface(private val target: Canvas) : RenderingSurf get() = buffer override fun setBounds(width: Int, height: Int) { - buffer.resizeSavingImage(width, height) - target.resizeSavingImage(width, height) + //buffer.resizeSavingImage(width, height) + buffer.width = width + buffer.height = height + if( width != target.width ){ + target.width = width + } + if( height != target.height){ + target.height = height + } } override fun flush() { if (buffer.width == 0 || buffer.height == 0) return - - target.context2d.drawImage(buffer.imageSource, 0.0, 0.0) + //target.bitmapContext().transferFromImageBitmap(snapshot) + target.context2d().drawImage(buffer.imageSource,0.0,0.0) } private fun createBuffer(): Canvas { - return CanvasFactory.create() + return CanvasFactory.create(true) .apply { width = target.width height = target.height } } + + init { + //target.bitmapContext() + target.context2d() + buffer.context2d() + } } diff --git a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/misc/ImageCacher.kt b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/misc/ImageCacher.kt index 937ddd0f2..e384ac3c0 100644 --- a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/misc/ImageCacher.kt +++ b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/misc/ImageCacher.kt @@ -23,9 +23,11 @@ */ package org.jetbrains.projector.client.common.misc +import org.jetbrains.projector.client.common.RenderingSurfaceProcessor import org.jetbrains.projector.client.common.SingleRenderingSurfaceProcessor import org.jetbrains.projector.client.common.canvas.Canvas import org.jetbrains.projector.client.common.canvas.CanvasFactory +import org.jetbrains.projector.client.common.canvas.buffering.RenderingSurface import org.jetbrains.projector.client.common.canvas.buffering.UnbufferedRenderingSurface import org.jetbrains.projector.common.misc.Do import org.jetbrains.projector.common.protocol.data.ImageData @@ -41,8 +43,8 @@ object ImageCacher { private data class OffscreenImage( val width: Int, val height: Int, - val singleRenderingSurfaceProcessor: SingleRenderingSurfaceProcessor, - val offscreenCanvas: Canvas.ImageSource, + val processor: RenderingSurfaceProcessor, + val offscreenCanvas: Canvas, ) private var currentSize = 0 @@ -69,7 +71,7 @@ object ImageCacher { } fun getImageData(imageId: ImageId): Canvas.ImageSource? = when (imageId) { - is ImageId.PVolatileImageId -> offscreenImages[imageId.id]?.offscreenCanvas + is ImageId.PVolatileImageId -> offscreenImages[imageId.id]?.offscreenCanvas?.imageSource is ImageId.BufferedImageId -> { val imageData = cache[imageId]?.apply { lastUsageTimestamp = TimeStamp.current }?.data @@ -79,7 +81,6 @@ object ImageCacher { imagesToRequest.add(imageId) requestedImages[imageId] = LivingEntity(TimeStamp.current, 0, null) } - null } else { @@ -94,29 +95,29 @@ object ImageCacher { } } - fun getOffscreenProcessor(offscreenTarget: ServerDrawCommandsEvent.Target.Offscreen): SingleRenderingSurfaceProcessor { + fun getOffscreenProcessor(offscreenTarget: ServerDrawCommandsEvent.Target.Offscreen, processorFactory: (RenderingSurface) -> RenderingSurfaceProcessor): RenderingSurfaceProcessor { val image = offscreenImages[offscreenTarget.pVolatileImageId] if (image == null || image.width != offscreenTarget.width || image.height != offscreenTarget.height) { - val offScreenCanvas = CanvasFactory.create().apply { + val offScreenCanvas = CanvasFactory.create(true).apply { width = offscreenTarget.width height = offscreenTarget.height } val offScreenRenderingSurface = UnbufferedRenderingSurface(offScreenCanvas) - val offScreenCommandProcessor = SingleRenderingSurfaceProcessor(offScreenRenderingSurface) + val offScreenCommandProcessor = processorFactory(offScreenRenderingSurface) offscreenImages[offscreenTarget.pVolatileImageId] = OffscreenImage( width = offscreenTarget.width, height = offscreenTarget.height, - singleRenderingSurfaceProcessor = offScreenCommandProcessor, - offscreenCanvas = offScreenCanvas.imageSource + processor = offScreenCommandProcessor, + offscreenCanvas = offScreenCanvas ) return offScreenCommandProcessor } - return image.singleRenderingSurfaceProcessor + return image.processor } fun extractImagesToRequest(): List { diff --git a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/misc/ParamsProvider.kt b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/misc/ParamsProvider.kt index f497d78e6..f37f62c48 100644 --- a/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/misc/ParamsProvider.kt +++ b/projector-client-common/src/commonMain/kotlin/org/jetbrains/projector/client/common/misc/ParamsProvider.kt @@ -27,15 +27,8 @@ expect object ParamsProvider { val CLIPPING_BORDERS: Boolean val SHOW_TEXT_WIDTH: Boolean - val REPAINT_AREA: RepaintAreaSetting + val REPAINT_AREA: Boolean val LOG_UNSUPPORTED_EVENTS: Boolean val IMAGE_TTL: Double val IMAGE_CACHE_SIZE_CHARS: Int } - -sealed class RepaintAreaSetting { - - object Disabled : RepaintAreaSetting() - - data class Enabled(var show: Boolean) : RepaintAreaSetting() -} diff --git a/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/CanvasFactory.kt b/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/CanvasFactory.kt index 7d015adb9..7cf350c49 100644 --- a/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/CanvasFactory.kt +++ b/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/CanvasFactory.kt @@ -28,10 +28,19 @@ import org.w3c.dom.CanvasImageSource import org.w3c.dom.CanvasRenderingContext2D import org.w3c.dom.HTMLCanvasElement import org.w3c.dom.Image +import org.w3c.xhr.XMLHttpRequest actual object CanvasFactory { actual fun create(): Canvas { - return DomCanvas(document.createElement("canvas") as HTMLCanvasElement) + return create(false) + } + + actual fun create(offscreen: Boolean): Canvas { + return if(offscreen){ + DomOffscreenCanvas(OffscreenCanvas(0,0)) + }else{ + DomCanvas(document.createElement("canvas") as HTMLCanvasElement) + } } actual fun createImageSource(pngBase64: String, onLoad: (Canvas.ImageSource) -> Unit) { diff --git a/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/DomCanvas.kt b/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/DomCanvas.kt index 95ff68c0c..4f39b6cde 100644 --- a/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/DomCanvas.kt +++ b/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/DomCanvas.kt @@ -23,15 +23,29 @@ */ package org.jetbrains.projector.client.common.canvas -import kotlinx.browser.document import org.jetbrains.projector.client.common.canvas.Canvas.ImageSource -import org.jetbrains.projector.client.common.canvas.Canvas.Snapshot import org.jetbrains.projector.util.logging.Logger import org.w3c.dom.* class DomCanvas(private val myCanvas: HTMLCanvasElement) : Canvas { - override val context2d: Context2d = DomContext2d(myCanvas.getContext("2d") as CanvasRenderingContext2D) + var _context2d : Context2d? = null + + var _bitmapContext: ContextBitmapRenderer? = null + + override fun context2d(): Context2d { + if( _context2d == null){ + _context2d = DomContext2d(myCanvas.getContext("2d",js("{ desynchronized: true }")) as CanvasRenderingContext2D) + } + return _context2d!! + } + + override fun bitmapContext(): ContextBitmapRenderer { + if( _bitmapContext == null){ + _bitmapContext = DomContextBitmapRenderer(myCanvas.getContext("bitmaprenderer") as ImageBitmapRenderingContext) + } + return _bitmapContext!! + } override var width: Int get() = myCanvas.width @@ -52,18 +66,24 @@ class DomCanvas(private val myCanvas: HTMLCanvasElement) : Canvas { } override val imageSource: ImageSource = DomImageSource(myCanvas) - override fun takeSnapshot(): Snapshot { - val copy = document.createElement("canvas") as HTMLCanvasElement + override fun takeSnapshot(): ImageSource { + //val copy = document.createElement("canvas") as HTMLCanvasElement + // + //copy.apply { + // width = myCanvas.width + // height = myCanvas.height + // if (myCanvas.width != 0 && myCanvas.height != 0) { + // (getContext("2d") as CanvasRenderingContext2D).drawImage(myCanvas, 0.0, 0.0) + // } + //} - copy.apply { - width = myCanvas.width - height = myCanvas.height - if (myCanvas.width != 0 && myCanvas.height != 0) { - (getContext("2d") as CanvasRenderingContext2D).drawImage(myCanvas, 0.0, 0.0) - } - } + val copy = OffscreenCanvas(myCanvas.width,myCanvas.height) + + val context = copy.getContext("2d",js("{ alpha: false }")) as OffscreenCanvasRenderingContext2D + + context.drawImage(myCanvas,0.0,0.0) - return object : DomImageSource(copy), Snapshot {} + return DomImageSource(copy) } open class DomImageSource(val canvasElement: CanvasImageSource) : ImageSource { @@ -83,6 +103,7 @@ class DomCanvas(private val myCanvas: HTMLCanvasElement) : Canvas { is HTMLVideoElement -> width == 0 || height == 0 is ImageBitmap -> width == 0 || height == 0 is ImageData -> width == 0 || height == 0 + is OffscreenCanvas -> width == 0 || height == 0 else -> { logger.error { "Unknown type ${this::class}" } false diff --git a/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/DomContext2d.kt b/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/DomContext2d.kt index 9ba1fe102..0318f891b 100644 --- a/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/DomContext2d.kt +++ b/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/DomContext2d.kt @@ -25,7 +25,7 @@ package org.jetbrains.projector.client.common.canvas import org.jetbrains.projector.client.common.canvas.Canvas.ImageSource import org.jetbrains.projector.client.common.canvas.Context2d.* -import org.jetbrains.projector.client.common.canvas.Extensions.argbIntToRgbaString +import org.jetbrains.projector.client.common.canvas.JsExtensions.argbIntToRgbaString import org.jetbrains.projector.client.common.canvas.PaintColor.Gradient import org.jetbrains.projector.client.common.canvas.PaintColor.SolidColor import org.jetbrains.projector.common.misc.Do @@ -192,12 +192,29 @@ internal class DomContext2d(private val myContext2d: CanvasRenderingContext2D) : myContext2d.restore() } + @Suppress("UNUSED_VARIABLE") override fun setFillStyle(color: PaintColor?) { - myContext2d.fillStyle = color?.extract() + color?.let { paintColor -> + when(paintColor.tpe.ordinal) { + PaintColorType.SolidColor.ordinal -> + myContext2d.fillStyle = paintColor.argb.argbIntToRgbaString() + PaintColorType.Gradient.ordinal -> + myContext2d.fillStyle = paintColor.unsafeCast().canvasGradient + } + } } + + @Suppress("UNUSED_VARIABLE") override fun setStrokeStyle(color: PaintColor?) { - myContext2d.strokeStyle = color?.extract() + color?.let { paintColor -> + when(paintColor.tpe.ordinal) { + PaintColorType.SolidColor.ordinal -> + myContext2d.strokeStyle = paintColor.argb.argbIntToRgbaString() + PaintColorType.Gradient.ordinal -> + myContext2d.strokeStyle = paintColor.unsafeCast().canvasGradient + } + } } override fun setGlobalAlpha(alpha: Double) { @@ -276,8 +293,8 @@ internal class DomContext2d(private val myContext2d: CanvasRenderingContext2D) : myContext2d.setTransform(m11, m12, m21, m22, dx, dy) } - override fun setLineDash(lineDash: DoubleArray) { - myContext2d.setLineDash(lineDash.toTypedArray()) + override fun setLineDash(lineDash: Array) { + myContext2d.setLineDash(lineDash) } override fun setLineDashOffset(offset: Double) { @@ -299,6 +316,12 @@ internal class DomContext2d(private val myContext2d: CanvasRenderingContext2D) : return DOMGradient(myContext2d.createLinearGradient(x0, y0, x1, y1)) } + override fun setTransform(matrix: Matrix) { + with(matrix){ + myContext2d.setTransform(a,b,c,d,e,f) + } + } + override fun getTransform(): Matrix { return with(myContext2d.getTransform()) { Matrix(a, b, c, d, e, f) @@ -323,6 +346,9 @@ internal class DomContext2d(private val myContext2d: CanvasRenderingContext2D) : override fun addColorStop(offset: Double, argb: Int) { canvasGradient.addColorStop(offset, argb.argbIntToRgbaString()) } + + override val argb: Int + get() = 0 } companion object { diff --git a/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/DomContextBitmapRenderer.kt b/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/DomContextBitmapRenderer.kt new file mode 100644 index 000000000..5adf26163 --- /dev/null +++ b/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/DomContextBitmapRenderer.kt @@ -0,0 +1,39 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 JetBrains s.r.o. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jetbrains.projector.client.common.canvas + +import org.w3c.dom.CanvasImageSource +import org.w3c.dom.ImageBitmap +import org.w3c.dom.ImageBitmapRenderingContext + +class DomContextBitmapRenderer(private val imageRenderer: ImageBitmapRenderingContext): ContextBitmapRenderer { + + private fun Canvas.ImageSource.asPlatformImageSource(): DomCanvas.DomImageSource { + return this as DomCanvas.DomImageSource + } + + override fun transferFromImageBitmap(bitmap: Canvas.ImageSource) { + imageRenderer.transferFromImageBitmap(bitmap.asPlatformImageSource().canvasElement as ImageBitmap) + } +} diff --git a/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/DomOffscreenCanvas.kt b/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/DomOffscreenCanvas.kt new file mode 100644 index 000000000..208340bed --- /dev/null +++ b/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/DomOffscreenCanvas.kt @@ -0,0 +1,61 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 JetBrains s.r.o. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jetbrains.projector.client.common.canvas + +import org.w3c.dom.CanvasImageSource +import org.w3c.dom.CanvasRenderingContext2D +import org.w3c.dom.ImageBitmapRenderingContext +import org.w3c.dom.RenderingContext + +class DomOffscreenCanvas(private val offscreenCanvas: OffscreenCanvas) : Canvas { + + var _context2d : Context2d? = null + + var _bitmapContext: ContextBitmapRenderer? = null + + override fun context2d(): Context2d { + if( _context2d == null){ + _context2d = DomContext2d(offscreenCanvas.getContext("2d") as OffscreenCanvasRenderingContext2D) + } + return _context2d!! + } + + override fun bitmapContext(): ContextBitmapRenderer { + TODO("Not yet implemented") + } + + override var width: Int + get() = offscreenCanvas.width + set(value) { offscreenCanvas.width = value } + override var height: Int + get() = offscreenCanvas.height + set(value) { offscreenCanvas.height = value} + + override val imageSource: Canvas.ImageSource + get() = DomCanvas.DomImageSource(offscreenCanvas) + + override fun takeSnapshot(): Canvas.ImageSource { + return DomCanvas.DomImageSource(offscreenCanvas) + } +} diff --git a/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/ExperimentalAPI.kt b/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/ExperimentalAPI.kt new file mode 100644 index 000000000..7c1607512 --- /dev/null +++ b/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/ExperimentalAPI.kt @@ -0,0 +1,49 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 JetBrains s.r.o. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jetbrains.projector.client.common.canvas + +import org.w3c.dom.CanvasImageSource +import org.w3c.dom.CanvasRenderingContext2D +import org.w3c.dom.ImageBitmap +import org.w3c.dom.RenderingContext + +object ExperimentalAPI { +} + +public open external class OffscreenCanvas : CanvasImageSource { + + + + open var width: Int + open var height: Int + + constructor(width: Int, height: Int) { definedExternally } + + fun getContext(contextId: String, vararg arguments: Any?): RenderingContext? + + fun transferToImageBitmap(): ImageBitmap + +} + +public external abstract class OffscreenCanvasRenderingContext2D: CanvasRenderingContext2D diff --git a/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/JsExtensions.kt b/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/JsExtensions.kt new file mode 100644 index 000000000..823bb2f10 --- /dev/null +++ b/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/JsExtensions.kt @@ -0,0 +1,53 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 JetBrains s.r.o. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jetbrains.projector.client.common.canvas + +import org.w3c.dom.HTMLCanvasElement + + +object JsExtensions { + + fun Long.argbIntToRgbaString(): String { + val argb = this.toInt() + return argb.argbIntToRgbaString(); + } + + val rgbStrCache = IntRange(0,255) + .map { i -> if(i < 0x10) "0" + i.toString(16) else i.toString(16) } + .toTypedArray() + + /** + * ARGB -> RGBA + */ + @Suppress("UNUSED_VARIABLE") + fun Int.argbIntToRgbaString(): String { + val argb = this + val strCache = rgbStrCache + return js(""" + "#" + strCache[(argb >>> 16) & 0xff] + strCache[(argb >>> 8) & 0xff] + strCache[argb & 0xff] + strCache[(argb >>> 24) & 0xff]; + """).unsafeCast() + } + + +} diff --git a/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/asUnsafe.kt b/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/asUnsafe.kt new file mode 100644 index 000000000..6a9160e2d --- /dev/null +++ b/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/canvas/asUnsafe.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019-2021, JetBrains s.r.o. and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. JetBrains designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact JetBrains, Na Hrebenech II 1718/10, Prague, 14000, Czech Republic + * if you need additional information or have any questions. + */ +package org.jetbrains.projector.client.common.canvas + +actual fun Any?.asUnsafe(): T { + return this.unsafeCast() +} diff --git a/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/misc/ParamsProvider.kt b/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/misc/ParamsProvider.kt index 560ce9afc..86084cf59 100644 --- a/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/misc/ParamsProvider.kt +++ b/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/misc/ParamsProvider.kt @@ -77,7 +77,7 @@ actual object ParamsProvider { val PING_AVERAGE_COUNT: Int? val PING_INTERVAL: Int val SHOW_PROCESSING_TIME: Boolean - actual val REPAINT_AREA: RepaintAreaSetting + actual val REPAINT_AREA: Boolean val SPECULATIVE_TYPING: Boolean val ENABLE_WSS: Boolean val HANDSHAKE_TOKEN: String? @@ -118,10 +118,7 @@ actual object ParamsProvider { USER_SCALING_RATIO = searchParams.get("userScalingRatio")?.toDoubleOrNull() ?: DEFAULT_USER_SCALING_RATIO PING_INTERVAL = searchParams.get("pingInterval")?.toIntOrNull() ?: DEFAULT_PING_INTERVAL SHOW_PROCESSING_TIME = searchParams.has("showProcessingTime") - REPAINT_AREA = when (searchParams.has("repaintArea")) { - false -> RepaintAreaSetting.Disabled - true -> RepaintAreaSetting.Enabled(show = false) - } + REPAINT_AREA = searchParams.has("repaintArea") SPECULATIVE_TYPING = searchParams.has("speculativeTyping") ENABLE_WSS = searchParams.has("wss") || window.location.protocol == "https:" HANDSHAKE_TOKEN = searchParams.get("token") diff --git a/projector-client-common/src/jvmMain/kotlin/org/jetbrains/projector/client/common/canvas/CanvasFactory.kt b/projector-client-common/src/jvmMain/kotlin/org/jetbrains/projector/client/common/canvas/CanvasFactory.kt index 5aca4b946..0f4cded76 100644 --- a/projector-client-common/src/jvmMain/kotlin/org/jetbrains/projector/client/common/canvas/CanvasFactory.kt +++ b/projector-client-common/src/jvmMain/kotlin/org/jetbrains/projector/client/common/canvas/CanvasFactory.kt @@ -32,6 +32,10 @@ actual object CanvasFactory { return SwingCanvas() } + actual fun create(offscreen: Boolean): Canvas { + TODO("Not yet implemented") + } + actual fun createImageSource( pngBase64: String, onLoad: (Canvas.ImageSource) -> Unit, @@ -43,4 +47,4 @@ actual object CanvasFactory { actual fun createEmptyImageSource(onLoad: (Canvas.ImageSource) -> Unit) { onLoad(SwingCanvas.SwingImageSource()) } -} \ No newline at end of file +} diff --git a/projector-client-common/src/jvmMain/kotlin/org/jetbrains/projector/client/common/canvas/SwingCanvas.kt b/projector-client-common/src/jvmMain/kotlin/org/jetbrains/projector/client/common/canvas/SwingCanvas.kt index a25fcf0c8..32f126625 100644 --- a/projector-client-common/src/jvmMain/kotlin/org/jetbrains/projector/client/common/canvas/SwingCanvas.kt +++ b/projector-client-common/src/jvmMain/kotlin/org/jetbrains/projector/client/common/canvas/SwingCanvas.kt @@ -32,8 +32,13 @@ class SwingCanvas() : Canvas { var image = BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB) private set - override var context2d: Context2d = SwingContext2d(image.createGraphics()) - private set + var _context2d: Context2d = SwingContext2d(image.createGraphics()) + + override fun context2d(): Context2d = _context2d + override fun bitmapContext(): ContextBitmapRenderer { + TODO("Not yet implemented") + } + override var width: Int get() = image.width @@ -46,16 +51,16 @@ class SwingCanvas() : Canvas { val newImage = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) newImage.createGraphics().drawImage(image, 0, 0, min(width, image.width), min(height, image.height), null) image = newImage - context2d = SwingContext2d(image.createGraphics()) + _context2d = SwingContext2d(image.createGraphics()) } override val imageSource: Canvas.ImageSource get() = SwingImageSource(image) - override fun takeSnapshot(): Canvas.Snapshot { + override fun takeSnapshot(): Canvas.ImageSource { val newImage = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) newImage.createGraphics().drawImage(image, 0, 0, null) - return object : SwingImageSource(newImage), Canvas.Snapshot {} + return SwingImageSource(newImage) } open class SwingImageSource(val image: Image) : Canvas.ImageSource { @@ -65,4 +70,4 @@ class SwingCanvas() : Canvas { return image.getWidth(null) == 1 && image.getHeight(null) == 1 } } -} \ No newline at end of file +} diff --git a/projector-client-common/src/jvmMain/kotlin/org/jetbrains/projector/client/common/canvas/SwingContext2d.kt b/projector-client-common/src/jvmMain/kotlin/org/jetbrains/projector/client/common/canvas/SwingContext2d.kt index 9d27c22af..54c6ced9b 100644 --- a/projector-client-common/src/jvmMain/kotlin/org/jetbrains/projector/client/common/canvas/SwingContext2d.kt +++ b/projector-client-common/src/jvmMain/kotlin/org/jetbrains/projector/client/common/canvas/SwingContext2d.kt @@ -30,10 +30,8 @@ import org.jetbrains.projector.common.protocol.data.PathSegment import org.jetbrains.projector.common.protocol.data.Point import java.awt.* import java.awt.geom.AffineTransform -import java.awt.geom.Arc2D import java.awt.geom.Path2D import java.awt.geom.RoundRectangle2D -import java.io.File class SwingContext2d(graphics: Graphics2D) : Context2d { private var savedGraphics: Graphics2D = graphics.create() as Graphics2D @@ -265,7 +263,11 @@ class SwingContext2d(graphics: Graphics2D) : Context2d { graphics.transform = AffineTransform(m11, m12, m21, m22, dx, dy) } - override fun setLineDash(lineDash: DoubleArray) { + override fun setTransform(matrix: Context2d.Matrix) { + TODO("Not yet implemented") + } + + override fun setLineDash(lineDash: Array) { myCurrentStroke = myCurrentStroke.change(dash = if(lineDash.isEmpty()) null else FloatArray(lineDash.size) { lineDash[it].toFloat() }) } @@ -307,6 +309,9 @@ class SwingContext2d(graphics: Graphics2D) : Context2d { points.add(offset to argb) } + override val argb: Int + get() = 0 + fun getPaint() : Paint { return when(points.size) { 0 -> error("No points in gradient paint") diff --git a/projector-client-common/src/jvmMain/kotlin/org/jetbrains/projector/client/common/canvas/asUnsafe.kt b/projector-client-common/src/jvmMain/kotlin/org/jetbrains/projector/client/common/canvas/asUnsafe.kt new file mode 100644 index 000000000..57b5c0d96 --- /dev/null +++ b/projector-client-common/src/jvmMain/kotlin/org/jetbrains/projector/client/common/canvas/asUnsafe.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019-2021, JetBrains s.r.o. and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. JetBrains designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact JetBrains, Na Hrebenech II 1718/10, Prague, 14000, Czech Republic + * if you need additional information or have any questions. + */ +package org.jetbrains.projector.client.common.canvas + +actual fun Any?.asUnsafe(): T { + return this as T +} diff --git a/projector-client-common/src/jvmMain/kotlin/org/jetbrains/projector/client/common/misc/ParamsProvider.kt b/projector-client-common/src/jvmMain/kotlin/org/jetbrains/projector/client/common/misc/ParamsProvider.kt index 7face6593..7474670e4 100644 --- a/projector-client-common/src/jvmMain/kotlin/org/jetbrains/projector/client/common/misc/ParamsProvider.kt +++ b/projector-client-common/src/jvmMain/kotlin/org/jetbrains/projector/client/common/misc/ParamsProvider.kt @@ -28,7 +28,7 @@ actual object ParamsProvider { get() = JvmParamsProviderImpl.CLIPPING_BORDERS actual val SHOW_TEXT_WIDTH: Boolean get() = JvmParamsProviderImpl.SHOW_TEXT_WIDTH - actual val REPAINT_AREA: RepaintAreaSetting + actual val REPAINT_AREA: Boolean get() = JvmParamsProviderImpl.REPAINT_AREA actual val LOG_UNSUPPORTED_EVENTS: Boolean get() = JvmParamsProviderImpl.LOG_UNSUPPORTED_EVENTS @@ -41,8 +41,8 @@ actual object ParamsProvider { object JvmParamsProviderImpl { var CLIPPING_BORDERS: Boolean = false var SHOW_TEXT_WIDTH: Boolean = false - var REPAINT_AREA: RepaintAreaSetting = RepaintAreaSetting.Disabled + var REPAINT_AREA: Boolean = false var LOG_UNSUPPORTED_EVENTS: Boolean = true var IMAGE_TTL: Double = 0.0 var IMAGE_CACHE_SIZE_CHARS: Int = 0 -} \ No newline at end of file +} diff --git a/projector-client-swing/src/main/kotlin/org/jetbrains/projector/client/swing/AbstractWindowManager.kt b/projector-client-swing/src/main/kotlin/org/jetbrains/projector/client/swing/AbstractWindowManager.kt index 669ba29b1..3d9ae4133 100644 --- a/projector-client-swing/src/main/kotlin/org/jetbrains/projector/client/swing/AbstractWindowManager.kt +++ b/projector-client-swing/src/main/kotlin/org/jetbrains/projector/client/swing/AbstractWindowManager.kt @@ -25,10 +25,11 @@ package org.jetbrains.projector.client.swing import org.jetbrains.projector.client.common.DrawEvent import org.jetbrains.projector.client.common.SingleRenderingSurfaceProcessor -import org.jetbrains.projector.client.common.SingleRenderingSurfaceProcessor.Companion.shrinkByPaintEvents import org.jetbrains.projector.client.common.canvas.SwingCanvas import org.jetbrains.projector.client.common.canvas.buffering.DoubleBufferedRenderingSurface -import org.jetbrains.projector.common.protocol.toClient.* +import org.jetbrains.projector.common.protocol.toClient.ServerWindowEvent +import org.jetbrains.projector.common.protocol.toClient.ServerWindowSetChangedEvent +import org.jetbrains.projector.common.protocol.toClient.WindowData import org.jetbrains.projector.util.logging.Logger abstract class AbstractWindowManager { @@ -69,9 +70,8 @@ abstract class AbstractWindowManager { return } - window.drawEvents.addAll(drawEvents.shrinkByPaintEvents()) - window.processor.process(window.drawEvents) + window.processor.process(drawEvents) window.surface.flush() @@ -81,7 +81,7 @@ abstract class AbstractWindowManager { inner class FrameData( val frame: FrameType, var windowData: WindowData, - val drawEvents: ArrayDeque, + var drawEvents: List, val surface: DoubleBufferedRenderingSurface, val processor: SingleRenderingSurfaceProcessor, ) diff --git a/projector-client-swing/src/main/kotlin/org/jetbrains/projector/client/swing/SwingClient.kt b/projector-client-swing/src/main/kotlin/org/jetbrains/projector/client/swing/SwingClient.kt index e5ddf79a6..f40ede021 100644 --- a/projector-client-swing/src/main/kotlin/org/jetbrains/projector/client/swing/SwingClient.kt +++ b/projector-client-swing/src/main/kotlin/org/jetbrains/projector/client/swing/SwingClient.kt @@ -26,7 +26,7 @@ package org.jetbrains.projector.client.swing import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.future.await import kotlinx.coroutines.launch -import org.jetbrains.projector.client.common.SingleRenderingSurfaceProcessor.Companion.shrinkByPaintEvents +import org.jetbrains.projector.client.common.SingleRenderingSurfaceProcessor import org.jetbrains.projector.client.common.SwingFontCache import org.jetbrains.projector.client.common.misc.ImageCacher import org.jetbrains.projector.client.common.protocol.KotlinxJsonToClientHandshakeDecoder @@ -41,7 +41,6 @@ import org.jetbrains.projector.util.logging.Logger import java.util.* import javax.swing.SwingUtilities import javax.swing.Timer -import kotlin.collections.ArrayDeque class SwingClient(val transport: ProjectorTransport, val windowManager: AbstractWindowManager<*>) { val logger = Logger() @@ -72,15 +71,17 @@ class SwingClient(val transport: ProjectorTransport, val windowManager: Abstract Do exhaustive when(target) { is ServerDrawCommandsEvent.Target.Onscreen -> windowManager.doWindowDraw(target.windowId, serverEvent.drawEvents) is ServerDrawCommandsEvent.Target.Offscreen -> { - val processor = ImageCacher.getOffscreenProcessor(target) - val deque = ArrayDeque(serverEvent.drawEvents.shrinkByPaintEvents()) - processor.process(deque) + val processor = ImageCacher.getOffscreenProcessor(target,::SingleRenderingSurfaceProcessor) + processor.process(serverEvent.drawEvents) } + else -> + logger.error { "Unknown target for ServerDrawCommandsEvent : ${serverEvent}" } } } is ServerCaretInfoChangedEvent -> logger.debug { "Received and discarded caret info event: $serverEvent" } is ServerMarkdownEvent -> logger.debug { "Received and discarded markdown event: $serverEvent" } is ServerWindowColorsEvent -> logger.debug { "Received and discarded color event: $serverEvent" } + else -> logger.error { "Unknown event : ${serverEvent}" } } } @@ -117,6 +118,7 @@ class SwingClient(val transport: ProjectorTransport, val windowManager: Abstract } } is ToClientHandshakeFailureEvent -> error("Handshake failed: ${serverHandshake.reason}") + else -> error("unknown message: ${serverHandshake}") } transport.send("Unused string meaning fonts loading is done") @@ -135,4 +137,4 @@ class SwingClient(val transport: ProjectorTransport, val windowManager: Abstract } } } -} \ No newline at end of file +} diff --git a/projector-client-web/build.gradle.kts b/projector-client-web/build.gradle.kts index aed1f9759..b1a3e73b6 100644 --- a/projector-client-web/build.gradle.kts +++ b/projector-client-web/build.gradle.kts @@ -56,10 +56,14 @@ dependencies { kotlin { js { browser { + webpackTask{ + + } @OptIn(ExperimentalDceDsl::class) dceTask { keep.add("projector-client-projector-client-web.org.jetbrains.projector.client.web.onLoad") } + } } } diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/Application.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/Application.kt index 73812b4d9..64f85fcd3 100644 --- a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/Application.kt +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/Application.kt @@ -28,6 +28,7 @@ import org.jetbrains.projector.client.common.misc.ParamsProvider import org.jetbrains.projector.client.web.state.ClientAction import org.jetbrains.projector.client.web.state.ClientStateMachine import org.jetbrains.projector.util.logging.Logger +import org.w3c.dom.* import kotlin.js.Json import kotlin.js.Promise import kotlin.js.json @@ -42,9 +43,6 @@ external class PermissionStatus class Application { - private val stateMachine = ClientStateMachine() - private val windowSizeController = WindowSizeController(stateMachine) - fun start() { val url = when (ParamsProvider.ENABLE_WSS) { false -> "ws://${ParamsProvider.HOST}:${ParamsProvider.PORT}" @@ -52,6 +50,9 @@ class Application { true -> "wss://${ParamsProvider.HOST}:${ParamsProvider.PORT}" } + val stateMachine = ClientStateMachine() + val windowSizeController = WindowSizeController(stateMachine) + try { setClipboardPermissions() } diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/ServerEventsProcessor.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/ServerEventsProcessor.kt index c308e9b4d..1b0ddd60f 100644 --- a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/ServerEventsProcessor.kt +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/ServerEventsProcessor.kt @@ -24,8 +24,7 @@ package org.jetbrains.projector.client.web import kotlinx.browser.window -import org.jetbrains.projector.client.common.DrawEvent -import org.jetbrains.projector.client.common.SingleRenderingSurfaceProcessor.Companion.shrinkByPaintEvents +import org.jetbrains.projector.client.common.SingleRenderingSurfaceProcessor import org.jetbrains.projector.client.common.misc.ImageCacher import org.jetbrains.projector.client.web.component.MarkdownPanelManager import org.jetbrains.projector.client.web.misc.PingStatistics @@ -95,13 +94,12 @@ class ServerEventsProcessor(private val windowDataEventsProcessor: WindowDataEve is ServerDrawCommandsEvent.Target.Onscreen -> windowDataEventsProcessor.draw(target.windowId, event.drawEvents) is ServerDrawCommandsEvent.Target.Offscreen -> { - val offscreenProcessor = ImageCacher.getOffscreenProcessor(target) + val offscreenProcessor = ImageCacher.getOffscreenProcessor(target,::SingleRenderingSurfaceProcessor) // todo: don't create this deque every time - val drawEvents = ArrayDeque().apply { addAll(event.drawEvents.shrinkByPaintEvents()) } - - offscreenProcessor.process(drawEvents) + offscreenProcessor.process(event.drawEvents) + windowDataEventsProcessor.redrawWindows() } } diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/input/InputController.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/input/InputController.kt index d234d3d09..cc8db9489 100644 --- a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/input/InputController.kt +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/input/InputController.kt @@ -28,7 +28,6 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.jetbrains.projector.client.common.misc.ParamsProvider -import org.jetbrains.projector.client.common.misc.RepaintAreaSetting import org.jetbrains.projector.client.common.misc.TimeStamp import org.jetbrains.projector.client.web.input.layout.FrAzerty import org.jetbrains.projector.client.web.input.layout.KeyboardApiLayout @@ -398,11 +397,6 @@ class InputController( ClientStats.printStats() } - if (type == ClientKeyEvent.KeyEventType.DOWN && vk == VK.F11 && KeyModifier.CTRL_KEY in message.modifiers) { // todo: move to client state - (ParamsProvider.REPAINT_AREA as? RepaintAreaSetting.Enabled)?.let { - it.show = !it.show - } - } stateMachine.fire(ClientAction.AddEvent(message)) diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/misc/FontFaceAppender.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/misc/FontFaceAppender.kt index cf1b2764d..a2c8d3b56 100644 --- a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/misc/FontFaceAppender.kt +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/misc/FontFaceAppender.kt @@ -24,7 +24,7 @@ package org.jetbrains.projector.client.web.misc import kotlinx.browser.document -import org.jetbrains.projector.client.common.canvas.Extensions.toFontFaceName +import org.jetbrains.projector.client.common.canvas.Extensions import org.jetbrains.projector.common.protocol.data.TtfFontData import kotlin.js.Promise @@ -37,8 +37,8 @@ object FontFaceAppender { private var loadedFonts = 0 - fun appendFontFaceToPage(fontId: Short, fontData: TtfFontData, onLoad: (Int) -> Unit) { - val fontFaceName = fontId.toFontFaceName() + fun appendFontFaceToPage(fontId: Int, fontData: TtfFontData, onLoad: (Int) -> Unit) { + val fontFaceName = Extensions.serverFontNameCache[fontId] // todo: try to use the ByteArray variant: val fontFace = FontFace(fontFaceName, "url(data:font/truetype;base64,${fontData.ttfBase64}) format('truetype')") diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/protocol/ManualJsonToClientMessageDecoder.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/protocol/ManualJsonToClientMessageDecoder.kt index 004c107bd..23359db1c 100644 --- a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/protocol/ManualJsonToClientMessageDecoder.kt +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/protocol/ManualJsonToClientMessageDecoder.kt @@ -43,29 +43,29 @@ object ManualJsonToClientMessageDecoder : ToClientMessageDecoder { } private fun Array.toEvent(): ServerEvent { - val type = this[0] as String + val type = this[0].unsafeCast() val content = this[1].unsafeCast() return when (type) { "a" -> ServerImageDataReplyEvent(content["a"].unsafeCast>().toImageId(), content["b"].unsafeCast>().toImageData()) - "b" -> ServerPingReplyEvent(content["a"] as Int, content["b"] as Int) - "c" -> ServerClipboardEvent(content["a"] as String) + "b" -> ServerPingReplyEvent(content["a"].unsafeCast(), content["b"].unsafeCast()) + "c" -> ServerClipboardEvent(content["a"].unsafeCast()) "d" -> ServerWindowSetChangedEvent(content["a"].unsafeCast>().map { it.toWindowData() }) "e" -> ServerDrawCommandsEvent( content["a"].unsafeCast>().toTarget(), content["b"].unsafeCast>>().map { it.toWindowEvent() } ) "f" -> ServerCaretInfoChangedEvent(content["a"].unsafeCast>().toCaretInfoChange()) - "g" -> ServerMarkdownEvent.ServerMarkdownShowEvent(content["a"] as Int, content["b"] as Boolean) - "h" -> ServerMarkdownEvent.ServerMarkdownResizeEvent(content["a"] as Int, content["b"].unsafeCast().toCommonIntSize()) - "i" -> ServerMarkdownEvent.ServerMarkdownMoveEvent(content["a"] as Int, content["b"].unsafeCast().toPoint()) - "j" -> ServerMarkdownEvent.ServerMarkdownDisposeEvent(content["a"] as Int) - "k" -> ServerMarkdownEvent.ServerMarkdownPlaceToWindowEvent(content["a"] as Int, content["b"] as Int) - "l" -> ServerMarkdownEvent.ServerMarkdownSetHtmlEvent(content["a"] as Int, content["b"] as String) - "m" -> ServerMarkdownEvent.ServerMarkdownSetCssEvent(content["a"] as Int, content["b"] as String) - "n" -> ServerMarkdownEvent.ServerMarkdownScrollEvent(content["a"] as Int, content["b"] as Int) - "o" -> ServerMarkdownEvent.ServerMarkdownBrowseUriEvent(content["a"] as String) + "g" -> ServerMarkdownEvent.ServerMarkdownShowEvent(content["a"].unsafeCast(), content["b"].unsafeCast()) + "h" -> ServerMarkdownEvent.ServerMarkdownResizeEvent(content["a"].unsafeCast(), content["b"].unsafeCast().toCommonIntSize()) + "i" -> ServerMarkdownEvent.ServerMarkdownMoveEvent(content["a"].unsafeCast(), content["b"].unsafeCast().toPoint()) + "j" -> ServerMarkdownEvent.ServerMarkdownDisposeEvent(content["a"].unsafeCast()) + "k" -> ServerMarkdownEvent.ServerMarkdownPlaceToWindowEvent(content["a"].unsafeCast(), content["b"].unsafeCast()) + "l" -> ServerMarkdownEvent.ServerMarkdownSetHtmlEvent(content["a"].unsafeCast(), content["b"].unsafeCast()) + "m" -> ServerMarkdownEvent.ServerMarkdownSetCssEvent(content["a"].unsafeCast(), content["b"].unsafeCast()) + "n" -> ServerMarkdownEvent.ServerMarkdownScrollEvent(content["a"].unsafeCast(), content["b"].unsafeCast()) + "o" -> ServerMarkdownEvent.ServerMarkdownBrowseUriEvent(content["a"].unsafeCast()) "p" -> ServerWindowColorsEvent(content["a"].unsafeCast().toColorsStorage()) else -> throw IllegalArgumentException("Unsupported event type: ${JSON.stringify(this)}") } @@ -83,18 +83,18 @@ object ManualJsonToClientMessageDecoder : ToClientMessageDecoder { } private fun Array.toCaretInfoChange(): ServerCaretInfoChangedEvent.CaretInfoChange { - val type = this[0] as String + val type = this[0].unsafeCast() val content = this[1].unsafeCast() return when (type) { "a" -> ServerCaretInfoChangedEvent.CaretInfoChange.NoCarets "b" -> ServerCaretInfoChangedEvent.CaretInfoChange.Carets( content["a"].unsafeCast>().map { it.toCaretInfo() }, - content["b"] as Short?, - content["c"] as Int, - content["d"] as Int, - content["e"] as Float, - content["f"] as Int, + content["b"]?.unsafeCast(), + content["c"].unsafeCast(), + content["d"].unsafeCast(), + content["e"].unsafeCast(), + content["f"].unsafeCast(), content["g"].unsafeCast().toCommonRectangle() ) else -> throw IllegalArgumentException("Unsupported caret info type: ${JSON.stringify(this)}") @@ -106,35 +106,35 @@ object ManualJsonToClientMessageDecoder : ToClientMessageDecoder { } private fun Array.toTarget(): ServerDrawCommandsEvent.Target { - val type = this[0] as String + val type = this[0].unsafeCast() val content = this[1].unsafeCast() return when (type) { - "a" -> ServerDrawCommandsEvent.Target.Onscreen(content["a"] as Int) - "b" -> ServerDrawCommandsEvent.Target.Offscreen((content["a"] as Double).roundToLong(), content["b"] as Int, - content["c"] as Int) // todo: is it a correct way of decoding Long? + "a" -> ServerDrawCommandsEvent.Target.Onscreen(content["a"].unsafeCast()) + "b" -> ServerDrawCommandsEvent.Target.Offscreen((content["a"].unsafeCast()).roundToLong(), content["b"].unsafeCast(), + content["c"].unsafeCast()) // todo: is it a correct way of decoding Long? else -> throw IllegalArgumentException("Unsupported target type: ${JSON.stringify(this)}") } } private fun Json.toCommonIntSize(): CommonIntSize { - return CommonIntSize(this["a"] as Int, this["b"] as Int) + return CommonIntSize(this["a"].unsafeCast(), this["b"].unsafeCast()) } private fun Json.toWindowData(): WindowData { return WindowData( - this["a"] as Int, - this["b"] as String?, + this["a"].unsafeCast(), + this["b"]?.unsafeCast(), this["c"].unsafeCast>?>()?.map { it.toImageId() }, - this["d"] as Boolean, - this["e"] as Int, + this["d"].unsafeCast(), + this["e"].unsafeCast(), this["f"].unsafeCast().toCommonRectangle(), - (this["g"] as String?)?.toCursorType(), - this["h"] as Boolean, - this["i"] as Boolean, - this["j"] as Boolean, - (this["k"] as String).toWindowType(), - this["l"] as Int? + (this["g"]?.unsafeCast())?.toCursorType(), + this["h"].unsafeCast(), + this["i"].unsafeCast(), + this["j"].unsafeCast(), + (this["k"].unsafeCast()).toWindowType(), + this["l"]?.unsafeCast() ) } @@ -169,67 +169,67 @@ object ManualJsonToClientMessageDecoder : ToClientMessageDecoder { } private fun Json.toCommonRectangle(): CommonRectangle { - return CommonRectangle(this["a"] as Double, this["b"] as Double, this["c"] as Double, this["d"] as Double) + return CommonRectangle(this["a"].unsafeCast(), this["b"].unsafeCast(), this["c"].unsafeCast(), this["d"].unsafeCast()) } private fun Array.toWindowEvent(): ServerWindowEvent { - val type = this[0] as String + val type = this[0].unsafeCast() val content = this[1].unsafeCast() return when (type) { "a" -> ServerPaintArcEvent( - (content["a"] as String).toPaintType(), - content["b"] as Int, - content["c"] as Int, - content["d"] as Int, - content["e"] as Int, - content["f"] as Int, - content["g"] as Int + (content["a"].unsafeCast()).toPaintType(), + content["b"].unsafeCast(), + content["c"].unsafeCast(), + content["d"].unsafeCast(), + content["e"].unsafeCast(), + content["f"].unsafeCast(), + content["g"].unsafeCast() ) "b" -> ServerPaintOvalEvent( - (content["a"] as String).toPaintType(), - content["b"] as Int, - content["c"] as Int, - content["d"] as Int, - content["e"] as Int + (content["a"].unsafeCast()).toPaintType(), + content["b"].unsafeCast(), + content["c"].unsafeCast(), + content["d"].unsafeCast(), + content["e"].unsafeCast() ) "c" -> ServerPaintRoundRectEvent( - (content["a"] as String).toPaintType(), - content["b"] as Int, - content["c"] as Int, - content["d"] as Int, - content["e"] as Int, - content["f"] as Int, - content["g"] as Int + (content["a"].unsafeCast()).toPaintType(), + content["b"].unsafeCast(), + content["c"].unsafeCast(), + content["d"].unsafeCast(), + content["e"].unsafeCast(), + content["f"].unsafeCast(), + content["g"].unsafeCast() ) "d" -> ServerPaintRectEvent( - (content["a"] as String).toPaintType(), - content["b"] as Double, - content["c"] as Double, - content["d"] as Double, - content["e"] as Double + (content["a"].unsafeCast()).toPaintType(), + content["b"].unsafeCast(), + content["c"].unsafeCast(), + content["d"].unsafeCast(), + content["e"].unsafeCast() ) "e" -> ServerDrawLineEvent( - content["a"] as Int, - content["b"] as Int, - content["c"] as Int, - content["d"] as Int + content["a"].unsafeCast(), + content["b"].unsafeCast(), + content["c"].unsafeCast(), + content["d"].unsafeCast() ) "f" -> ServerCopyAreaEvent( - content["a"] as Int, - content["b"] as Int, - content["c"] as Int, - content["d"] as Int, - content["e"] as Int, - content["f"] as Int + content["a"].unsafeCast(), + content["b"].unsafeCast(), + content["c"].unsafeCast(), + content["d"].unsafeCast(), + content["e"].unsafeCast(), + content["f"].unsafeCast() ) - "g" -> ServerSetFontEvent(content["a"] as Short, content["b"] as Int, content["c"] as Boolean) + "g" -> ServerSetFontEvent(content["a"].unsafeCast(), content["b"].unsafeCast(), content["c"].unsafeCast()) "h" -> ServerSetClipEvent(content["a"].unsafeCast?>()?.toCommonShape()) @@ -242,21 +242,21 @@ object ManualJsonToClientMessageDecoder : ToClientMessageDecoder { "l" -> ServerDrawImageEvent(content["a"].unsafeCast>().toImageId(), content["b"].unsafeCast>().toImageEventInfo()) - "m" -> ServerDrawStringEvent(content["a"] as String, content["b"] as Double, content["c"] as Double, content["d"] as Double) + "m" -> ServerDrawStringEvent(content["a"].unsafeCast(), content["b"].unsafeCast(), content["c"].unsafeCast(), content["d"].unsafeCast()) - "n" -> ServerPaintPolygonEvent((content["a"] as String).toPaintType(), content["b"].unsafeCast>().map { it.toPoint() }) + "n" -> ServerPaintPolygonEvent((content["a"].unsafeCast()).toPaintType(), content["b"].unsafeCast>().map { it.toPoint() }) "o" -> ServerDrawPolylineEvent(content["a"].unsafeCast>().map { it.toPoint() }) - "p" -> ServerSetTransformEvent(content["a"].unsafeCast>().toList()) + "p" -> ServerSetTransformEvent(content["a"].unsafeCast()) - "q" -> ServerPaintPathEvent((content["a"] as String).toPaintType(), content["b"].unsafeCast().toCommonPath()) + "q" -> ServerPaintPathEvent((content["a"].unsafeCast()).toPaintType(), content["b"].unsafeCast().toCommonPath()) "r" -> ServerSetCompositeEvent(content["a"].unsafeCast>().toCommonComposite()) "s" -> ServerSetPaintEvent(content["a"].unsafeCast>().toPaintValue()) - "t" -> ServerSetUnknownStrokeEvent(content["a"] as String) + "t" -> ServerSetUnknownStrokeEvent(content["a"].unsafeCast()) else -> throw IllegalArgumentException("Unsupported event type: ${JSON.stringify(this)}") } @@ -278,15 +278,15 @@ object ManualJsonToClientMessageDecoder : ToClientMessageDecoder { ) private fun Array.toCommonComposite(): CommonComposite { - val type = this[0] as String + val type = this[0].unsafeCast() val content = this[1].unsafeCast() return when (type) { "a" -> CommonAlphaComposite( - alphaCompositeRuleMap[content["a"] as String] ?: throw IllegalArgumentException("Unsupported rule: ${content["a"]}"), - content["b"] as Float + alphaCompositeRuleMap[content["a"].unsafeCast()] ?: throw IllegalArgumentException("Unsupported rule: ${content["a"]}"), + content["b"].unsafeCast() ) - "b" -> UnknownComposite(content["a"] as String) + "b" -> UnknownComposite(content["a"].unsafeCast()) else -> throw IllegalArgumentException("Unsupported common composite type: ${JSON.stringify(this)}") } } @@ -300,75 +300,75 @@ object ManualJsonToClientMessageDecoder : ToClientMessageDecoder { } private fun Json.toColor(): PaintValue.Color { - return PaintValue.Color(this["a"] as Int) + return PaintValue.Color(this["a"].unsafeCast()) } private fun Array.toPaintValue(): PaintValue { - val type = this[0] as String + val type = this[0].unsafeCast() val content = this[1].unsafeCast() return when (type) { "a" -> content.toColor() "b" -> PaintValue.Gradient( content["a"].unsafeCast().toPoint(), content["b"].unsafeCast().toPoint(), - content["c"] as Int, content["d"] as Int + content["c"].unsafeCast(), content["d"].unsafeCast() ) - "c" -> PaintValue.Unknown(content["a"] as String) + "c" -> PaintValue.Unknown(content["a"].unsafeCast()) else -> throw IllegalArgumentException("Unsupported paint value type: ${JSON.stringify(this)}") } } private fun Array.toImageEventInfo(): ImageEventInfo { - val type = this[0] as String + val type = this[0].unsafeCast() val content = this[1].unsafeCast() return when (type) { - "a" -> ImageEventInfo.Xy(content["a"] as Int, content["b"] as Int, content["c"] as Int?) + "a" -> ImageEventInfo.Xy(content["a"].unsafeCast(), content["b"].unsafeCast(), content["c"]?.unsafeCast()) "b" -> ImageEventInfo.XyWh( - content["a"] as Int, - content["b"] as Int, - content["c"] as Int, - content["d"] as Int, - content["e"] as Int? + content["a"].unsafeCast(), + content["b"].unsafeCast(), + content["c"].unsafeCast(), + content["d"].unsafeCast(), + content["e"]?.unsafeCast() ) "c" -> ImageEventInfo.Ds( - content["a"] as Int, - content["b"] as Int, - content["c"] as Int, - content["d"] as Int, - content["e"] as Int, - content["f"] as Int, - content["g"] as Int, - content["h"] as Int, - content["i"] as Int? + content["a"].unsafeCast(), + content["b"].unsafeCast(), + content["c"].unsafeCast(), + content["d"].unsafeCast(), + content["e"].unsafeCast(), + content["f"].unsafeCast(), + content["g"].unsafeCast(), + content["h"].unsafeCast(), + content["i"]?.unsafeCast() ) - "d" -> ImageEventInfo.Transformed(content["a"].unsafeCast>().toList()) + "d" -> ImageEventInfo.Transformed(content["a"].unsafeCast()) else -> throw IllegalArgumentException("Unsupported image info type: ${JSON.stringify(this)}") } } private fun Array.toStrokeData(): StrokeData { - val thisType = this[0] as String + val thisType = this[0].unsafeCast() val content = this[1].unsafeCast() return when (thisType) { "a" -> StrokeData.Basic( - content["a"] as Float, - when (val type = content["b"] as String) { + content["a"].unsafeCast(), + when (val type = content["b"].unsafeCast()) { "a" -> StrokeData.Basic.JoinType.MITER "b" -> StrokeData.Basic.JoinType.ROUND "c" -> StrokeData.Basic.JoinType.BEVEL else -> throw IllegalArgumentException("Unsupported join type: $type") }, - when (val type = content["c"] as String) { + when (val type = content["c"].unsafeCast()) { "a" -> StrokeData.Basic.CapType.BUTT "b" -> StrokeData.Basic.CapType.ROUND "c" -> StrokeData.Basic.CapType.SQUARE else -> throw IllegalArgumentException("Unsupported cap type: $type") }, - content["d"] as Float, - content["e"] as Float, - content["f"].unsafeCast?>()?.toList() + content["d"].unsafeCast(), + content["e"].unsafeCast(), + content["f"].unsafeCast?>() ) else -> throw IllegalArgumentException("Unsupported stroke type: ${JSON.stringify(this)}") @@ -376,30 +376,30 @@ object ManualJsonToClientMessageDecoder : ToClientMessageDecoder { } private fun Array.toImageId(): ImageId { - val type = this[0] as String + val type = this[0].unsafeCast() val content = this[1].unsafeCast() return when (type) { - "a" -> ImageId.BufferedImageId(content["a"] as Int, content["b"] as Int) - "b" -> ImageId.PVolatileImageId((content["a"] as Double).roundToLong()) // todo: is it a correct way? - "c" -> ImageId.Unknown(content["a"] as String) + "a" -> ImageId.BufferedImageId(content["a"].unsafeCast(), content["b"].unsafeCast()) + "b" -> ImageId.PVolatileImageId((content["a"].unsafeCast()).roundToLong()) // todo: is it a correct way? + "c" -> ImageId.Unknown(content["a"].unsafeCast()) else -> throw IllegalArgumentException("Invalid image id type: ${JSON.stringify(this)}") } } private fun Array.toImageData(): ImageData { - val type = this[0] as String + val type = this[0].unsafeCast() val content = this[1].unsafeCast() return when (type) { - "a" -> ImageData.PngBase64(content["a"] as String) + "a" -> ImageData.PngBase64(content["a"].unsafeCast()) "b" -> ImageData.Empty else -> throw IllegalArgumentException("Invalid image data type: $${JSON.stringify(this)}") } } private fun Array.toCommonShape(): CommonShape { - val type = this[0] as String + val type = this[0].unsafeCast() val content = this[1].unsafeCast() return when (type) { @@ -411,7 +411,7 @@ object ManualJsonToClientMessageDecoder : ToClientMessageDecoder { private fun Json.toCommonPath(): CommonPath { val segments = this["a"].unsafeCast>>().map { it.toPathSegment() } - val winding = when (val type = this["b"] as String) { + val winding = when (val type = this["b"].unsafeCast()) { "a" -> CommonPath.WindingType.EVEN_ODD "b" -> CommonPath.WindingType.NON_ZERO else -> throw IllegalArgumentException("Invalid winding type: $type") @@ -421,7 +421,7 @@ object ManualJsonToClientMessageDecoder : ToClientMessageDecoder { } private fun Array.toPathSegment(): PathSegment { - val type = this[0] as String + val type = this[0].unsafeCast() val content = this[1].unsafeCast() return when (type) { @@ -439,6 +439,6 @@ object ManualJsonToClientMessageDecoder : ToClientMessageDecoder { } private fun Json.toPoint(): Point { - return Point(this["a"] as Double, this["b"] as Double) + return Point(this["a"].unsafeCast(), this["b"].unsafeCast()) } } diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/speculative/Typing.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/speculative/Typing.kt index 5f8b37989..bf1fadf35 100644 --- a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/speculative/Typing.kt +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/speculative/Typing.kt @@ -24,7 +24,7 @@ package org.jetbrains.projector.client.web.speculative import kotlinx.browser.document -import org.jetbrains.projector.client.common.canvas.Extensions.toFontFaceName +import org.jetbrains.projector.client.common.canvas.Extensions import org.jetbrains.projector.client.common.misc.ParamsProvider.SCALING_RATIO import org.jetbrains.projector.common.protocol.toClient.ServerCaretInfoChangedEvent import org.w3c.dom.CanvasRenderingContext2D @@ -141,7 +141,7 @@ sealed class Typing { putImageData(imageData, firstCaretLocation.x + currentCarets.plainSpaceWidth, firstCaretLocation.y) - val fontFace = currentCarets.fontId?.toFontFaceName() ?: "Arial" + val fontFace = Extensions.serverFontNameCache[currentCarets.fontId?.unsafeCast()?: 0] val fontSize = "${currentCarets.fontSize}px" font = "$fontSize $fontFace" // todo: use a proper font style diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ClientState.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ClientState.kt index f21ea3650..38ef661a0 100644 --- a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ClientState.kt +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ClientState.kt @@ -236,7 +236,7 @@ sealed class ClientState { ) command.fontDataHolders.forEach { fontDataHolder -> - FontFaceAppender.appendFontFaceToPage(fontDataHolder.fontId, fontDataHolder.fontData) { loadedFontCount -> + FontFaceAppender.appendFontFaceToPage(fontDataHolder.fontId.unsafeCast(), fontDataHolder.fontData) { loadedFontCount -> if (loadedFontCount == command.fontDataHolders.size) { logger.info { "${command.fontDataHolders.size} font(s) loaded" } OnScreenMessenger.hide() @@ -333,6 +333,9 @@ sealed class ClientState { timeout = ParamsProvider.REPAINT_INTERVAL_MS, ) + //private var repainter:Int = Int.MIN_VALUE + + private val serverEventsProcessor = ServerEventsProcessor(windowDataEventsProcessor) private val messagingPolicy = ( @@ -518,22 +521,23 @@ sealed class ClientState { is ClientAction.WebSocket.Close -> { Do exhaustive when (action) { is ClientAction.WebSocket.Close.FinishNormal -> { - logger.info { "Connection is closed..." } - - window.clearInterval(repainter) - pingStatistics.onClose() - windowDataEventsProcessor.onClose() - inputController.removeListeners() - windowSizeController.removeListener() - typing.dispose() - markdownPanelManager.disposeAll() - mobileKeyboardHelper.dispose() - closeBlocker.removeListener() - selectionBlocker.unblockSelection() - connectionWatcher.removeWatcher() - - showDisconnectedMessage(webSocket.url, action.closeCode) - Disconnected + //logger.info { "Connection is closed..." } + // + //window.clearInterval(repainter) + //pingStatistics.onClose() + //windowDataEventsProcessor.onClose() + //inputController.removeListeners() + //windowSizeController.removeListener() + //typing.dispose() + //markdownPanelManager.disposeAll() + //mobileKeyboardHelper.dispose() + //closeBlocker.removeListener() + //selectionBlocker.unblockSelection() + //connectionWatcher.removeWatcher() + // + //showDisconnectedMessage(webSocket.url, action.closeCode) + //Disconnected + reloadConnection("Connection is closed, retrying the connection...") } is ClientAction.WebSocket.Close.FinishError -> @@ -551,6 +555,7 @@ sealed class ClientState { logger.info { messageText } window.clearInterval(repainter) + //window.cancelAnimationFrame(repainter) pingStatistics.onClose() inputController.removeListeners() windowSizeController.removeListener() diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ClientStateMachine.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ClientStateMachine.kt index b7a85cc4c..c24da16fa 100644 --- a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ClientStateMachine.kt +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ClientStateMachine.kt @@ -50,6 +50,7 @@ class ClientStateMachine { currentState = currentState.consume(action) } catch (t: Throwable) { + t.printStackTrace() logger.error(t) { "Error consuming action, skipping the action" } } } diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ProjectorUI.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ProjectorUI.kt index 4f2a17bc7..69b6f72e4 100644 --- a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ProjectorUI.kt +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ProjectorUI.kt @@ -23,7 +23,7 @@ */ package org.jetbrains.projector.client.web.state -import org.jetbrains.projector.client.common.canvas.Extensions.argbIntToRgbaString +import org.jetbrains.projector.client.common.canvas.JsExtensions.argbIntToRgbaString import org.jetbrains.projector.common.protocol.toClient.ServerWindowColorsEvent interface LafListener { diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/window/OnScreenMessenger.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/window/OnScreenMessenger.kt index dc1e0f534..17f1daece 100644 --- a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/window/OnScreenMessenger.kt +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/window/OnScreenMessenger.kt @@ -24,7 +24,7 @@ package org.jetbrains.projector.client.web.window import kotlinx.browser.document -import org.jetbrains.projector.client.common.canvas.Extensions.argbIntToRgbaString +import org.jetbrains.projector.client.common.canvas.JsExtensions.argbIntToRgbaString import org.jetbrains.projector.client.common.misc.ParamsProvider import org.jetbrains.projector.client.web.misc.toDisplayType import org.jetbrains.projector.client.web.state.LafListener diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/window/Window.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/window/Window.kt index 0bf358a3c..d20d3e045 100644 --- a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/window/Window.kt +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/window/Window.kt @@ -25,7 +25,6 @@ package org.jetbrains.projector.client.web.window import kotlinx.browser.document import kotlinx.dom.addClass -import org.jetbrains.projector.client.common.DrawEvent import org.jetbrains.projector.client.common.SingleRenderingSurfaceProcessor import org.jetbrains.projector.client.common.canvas.DomCanvas import org.jetbrains.projector.client.common.canvas.buffering.DoubleBufferedRenderingSurface @@ -39,6 +38,7 @@ import org.jetbrains.projector.client.web.state.LafListener import org.jetbrains.projector.client.web.state.ProjectorUI import org.jetbrains.projector.common.protocol.data.CommonRectangle import org.jetbrains.projector.common.protocol.data.CursorType +import org.jetbrains.projector.common.protocol.toClient.ServerWindowEvent import org.jetbrains.projector.common.protocol.toClient.WindowData import org.jetbrains.projector.common.protocol.toClient.WindowType import org.jetbrains.projector.common.protocol.toServer.ClientWindowCloseEvent @@ -52,8 +52,8 @@ class Window(windowData: WindowData, private val stateMachine: ClientStateMachin val id = windowData.id - @OptIn(ExperimentalStdlibApi::class) - val drawEvents = ArrayDeque() + + var pendingEvents: List? = null var title: String? = null set(value) { @@ -82,8 +82,11 @@ class Window(windowData: WindowData, private val stateMachine: ClientStateMachin private var header: WindowHeader? = null private var headerVerticalPosition: Double = 0.0 private var headerHeight: Double = 0.0 + private val border = WindowBorder(windowData.resizable) + private var isDirty: Boolean = true + private val commandProcessor = SingleRenderingSurfaceProcessor(renderingSurface) var bounds: CommonRectangle = CommonRectangle(0.0, 0.0, 0.0, 0.0) @@ -114,49 +117,49 @@ class Window(windowData: WindowData, private val stateMachine: ClientStateMachin } init { - applyBounds() + applyBounds() - if (windowData.windowType == WindowType.IDEA_WINDOW || windowData.windowType == WindowType.POPUP) { - canvas.style.border = "none" - } - else if (windowData.windowType == WindowType.WINDOW) { - if (windowData.undecorated) { - canvas.style.border = ProjectorUI.borderStyle + if (windowData.windowType == WindowType.IDEA_WINDOW || windowData.windowType == WindowType.POPUP) { + canvas.style.border = "none" } - else { - // If the window has a header on the host, its sizes are included in the window bounds. - // The client header is drawn above the window, outside its bounds. At the same time, - // the coordinates of the contents of the window come taking into account the size - // of the header. As a result, on client an empty space is obtained between header - // and the contents of the window. To get rid of this, we transfer the height of the system - // window header and if it > 0, we draw the heading not over the window but inside - // the window's bounds, filling in the empty space. - - header = WindowHeader(windowData.title) - header!!.undecorated = windowData.undecorated - header!!.onMove = ::onMove - header!!.onClose = ::onClose - - headerVerticalPosition = when (windowData.headerHeight) { - 0, null -> ProjectorUI.headerHeight - else -> 0.0 + else if (windowData.windowType == WindowType.WINDOW) { + if (windowData.undecorated) { + canvas.style.border = ProjectorUI.borderStyle } - - headerHeight = when (windowData.headerHeight) { - 0, null -> ProjectorUI.headerHeight - else -> windowData.headerHeight!!.toDouble() + else { + // If the window has a header on the host, its sizes are included in the window bounds. + // The client header is drawn above the window, outside its bounds. At the same time, + // the coordinates of the contents of the window come taking into account the size + // of the header. As a result, on client an empty space is obtained between header + // and the contents of the window. To get rid of this, we transfer the height of the system + // window header and if it > 0, we draw the heading not over the window but inside + // the window's bounds, filling in the empty space. + + header = WindowHeader(windowData.title) + header!!.undecorated = windowData.undecorated + header!!.onMove = ::onMove + header!!.onClose = ::onClose + + headerVerticalPosition = when (windowData.headerHeight) { + 0, null -> ProjectorUI.headerHeight + else -> 0.0 + } + + headerHeight = when (windowData.headerHeight) { + 0, null -> ProjectorUI.headerHeight + else -> windowData.headerHeight!!.toDouble() + } + + canvas.style.borderBottom = ProjectorUI.borderStyle + canvas.style.borderLeft = ProjectorUI.borderStyle + canvas.style.borderRight = ProjectorUI.borderStyle + canvas.style.borderRadius = "0 0 ${ProjectorUI.borderRadius}px ${ProjectorUI.borderRadius}px" } - - canvas.style.borderBottom = ProjectorUI.borderStyle - canvas.style.borderLeft = ProjectorUI.borderStyle - canvas.style.borderRight = ProjectorUI.borderStyle - canvas.style.borderRadius = "0 0 ${ProjectorUI.borderRadius}px ${ProjectorUI.borderRadius}px" } - } - if (windowData.resizable) { - border.onResize = ::onResize - } + if (windowData.resizable) { + border.onResize = ::onResize + } } override fun lookAndFeelChanged() { @@ -242,6 +245,8 @@ class Window(windowData: WindowData, private val stateMachine: ClientStateMachin } renderingSurface.scalingRatio = scalingRatio + + renderingSurface.setBounds( width = (bounds.width * scalingRatio).roundToInt(), height = (bounds.height * scalingRatio).roundToInt() @@ -254,11 +259,40 @@ class Window(windowData: WindowData, private val stateMachine: ClientStateMachin header?.dispose() } + fun redraw() { + if (pendingEvents != null) { + drawBufferedEvents(emptyList()) + } + } + @OptIn(ExperimentalStdlibApi::class) - fun drawBufferedEvents() { - commandProcessor.process(drawEvents) - renderingSurface.flush() - header?.draw() + fun drawBufferedEvents(events: List) { + if (events.isNotEmpty() || pendingEvents != null) { + if (pendingEvents == null && events.isNotEmpty()) { + pendingEvents = commandProcessor.process(events) + } + else if (events.isNotEmpty()) { + pendingEvents = commandProcessor.process(pendingEvents!! + events) + } + else if (pendingEvents?.isNotEmpty() == true) { + pendingEvents = commandProcessor.process(pendingEvents!!) + } + else { + return + } + isDirty = true + renderingSurface.flush() + header?.draw() + } + + } + + fun flush() { + if (isDirty) { + renderingSurface.flush() + header?.draw() + isDirty = false + } } companion object { diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/window/WindowDataEventsProcessor.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/window/WindowDataEventsProcessor.kt index 5834ce2c5..7dfbbc406 100644 --- a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/window/WindowDataEventsProcessor.kt +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/window/WindowDataEventsProcessor.kt @@ -24,7 +24,6 @@ package org.jetbrains.projector.client.web.window import kotlinx.browser.document -import org.jetbrains.projector.client.common.SingleRenderingSurfaceProcessor.Companion.shrinkByPaintEvents import org.jetbrains.projector.client.common.misc.ImageCacher import org.jetbrains.projector.client.common.misc.ParamsProvider import org.jetbrains.projector.common.misc.firstNotNullOrNull @@ -37,7 +36,6 @@ import org.jetbrains.projector.util.logging.Logger import org.w3c.dom.HTMLCanvasElement import org.w3c.dom.HTMLImageElement import org.w3c.dom.HTMLLinkElement -import kotlin.collections.isNotEmpty class WindowDataEventsProcessor(private val windowManager: WindowManager) { @@ -46,7 +44,7 @@ class WindowDataEventsProcessor(private val windowManager: WindowManager) { @OptIn(ExperimentalStdlibApi::class) fun redrawWindows() { synchronized(windowManager) { - windowManager.forEach(Window::drawBufferedEvents) + windowManager.forEach(Window::redraw) } } @@ -134,11 +132,9 @@ class WindowDataEventsProcessor(private val windowManager: WindowManager) { return } - val newEvents = commands.shrinkByPaintEvents() - if (newEvents.isNotEmpty()) { - window.drawEvents.addAll(newEvents) - window.drawBufferedEvents() + if (commands.isNotEmpty()) { + window.drawBufferedEvents(commands) } } } @@ -153,7 +149,17 @@ class WindowDataEventsProcessor(private val windowManager: WindowManager) { fun onResized() { synchronized(windowManager) { - windowManager.forEach(Window::applyBounds) + windowManager.forEach{ window -> + window.applyBounds() + } + } + } + + fun flush() { + windowManager.forEach { window -> + if (window.isShowing) { + window.flush() + } } } diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/window/WindowHeader.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/window/WindowHeader.kt index a2d581be5..05cce07f3 100644 --- a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/window/WindowHeader.kt +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/window/WindowHeader.kt @@ -176,7 +176,7 @@ class WindowHeader(var title: String? = null) : DragEventsInterceptor, LafListen fun draw() { - val context = headerRenderingSurface.canvas.context2d + val context = headerRenderingSurface.canvas.context2d() val offset = ProjectorUI.crossOffset * headerRenderingSurface.scalingRatio // Fill header background. diff --git a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/misc/Defaults.kt b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/misc/Defaults.kt index 71f79be4f..2905197e2 100644 --- a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/misc/Defaults.kt +++ b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/misc/Defaults.kt @@ -28,6 +28,7 @@ import org.jetbrains.projector.common.protocol.data.StrokeData object Defaults { const val FONT_SIZE = 12 + const val FONT_NAME = "Arial" const val FOREGROUND_COLOR_ARGB = 0xFF_00_00_00.toInt() const val BACKGROUND_COLOR_ARGB = 0xFF_FF_FF_FF.toInt() diff --git a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/data/CommonShape.kt b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/data/CommonShape.kt index 00b762130..cc5618c93 100644 --- a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/data/CommonShape.kt +++ b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/data/CommonShape.kt @@ -27,7 +27,16 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -sealed class CommonShape +sealed class CommonShape { + + abstract val tpe: CommonShapeType + +} + +enum class CommonShapeType { + CommonRectangle, + CommonPath; +} @Serializable @SerialName("a") @@ -45,6 +54,8 @@ data class CommonRectangle( fun contains(x: Int, y: Int) = this.x <= x && x < this.x + this.width && this.y <= y && y < this.y + this.height fun createExtended(extend: Double) = CommonRectangle(x - extend, y - extend, width + extend * 2, height + extend * 2) + override val tpe: CommonShapeType + get() = CommonShapeType.CommonRectangle } @Serializable @@ -65,4 +76,7 @@ data class CommonPath( @SerialName("b") NON_ZERO, } + + override val tpe: CommonShapeType + get() = CommonShapeType.CommonPath } diff --git a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/data/ImageEventInfo.kt b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/data/ImageEventInfo.kt index 658e8213b..18c751830 100644 --- a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/data/ImageEventInfo.kt +++ b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/data/ImageEventInfo.kt @@ -82,6 +82,6 @@ sealed class ImageEventInfo { @SerialName("d") data class Transformed( @SerialName("a") - val tx: List = emptyList(), // todo: remove default after https://github.com/Kotlin/kotlinx.serialization/issues/806 + val tx: DoubleArray = doubleArrayOf(), // todo: remove default after https://github.com/Kotlin/kotlinx.serialization/issues/806 ) : ImageEventInfo() } diff --git a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/data/PaintValue.kt b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/data/PaintValue.kt index 31cb9b9ea..36ee108c8 100644 --- a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/data/PaintValue.kt +++ b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/data/PaintValue.kt @@ -29,12 +29,16 @@ import kotlinx.serialization.Serializable @Serializable sealed class PaintValue { + abstract val tpe: PaintValueType + @Serializable @SerialName("a") data class Color( @SerialName("a") val argb: Int, - ) : PaintValue() + ) : PaintValue() { + override val tpe: PaintValueType = PaintValueType.Color + } @Serializable @SerialName("b") @@ -47,12 +51,20 @@ sealed class PaintValue { val argb1: Int, @SerialName("d") val argb2: Int, - ) : PaintValue() + ) : PaintValue() { + override val tpe: PaintValueType = PaintValueType.Gradient + } @Serializable @SerialName("c") data class Unknown( @SerialName("a") val info: String, - ) : PaintValue() + ) : PaintValue() { + override val tpe: PaintValueType = PaintValueType.Unknown + } +} + +enum class PaintValueType { + Color,Gradient,Unknown } diff --git a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/data/StrokeData.kt b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/data/StrokeData.kt index 47e2274a5..36f0502e0 100644 --- a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/data/StrokeData.kt +++ b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/data/StrokeData.kt @@ -43,7 +43,7 @@ sealed class StrokeData { @SerialName("e") val dashPhase: Float, @SerialName("f") - val dashArray: List? = null, + val dashArray: Array? = null, ) : StrokeData() { @Serializable diff --git a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/handshake/Constant.kt b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/handshake/Constant.kt index 32d9b9059..1b04d6fcd 100644 --- a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/handshake/Constant.kt +++ b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/handshake/Constant.kt @@ -42,4 +42,5 @@ val commonVersionList = listOf( -625612891, -560999684, 471600343, + 1196659574 ) diff --git a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toClient/ServerWindowEvent.kt b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toClient/ServerWindowEvent.kt index 0b5eda318..8479c4162 100644 --- a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toClient/ServerWindowEvent.kt +++ b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toClient/ServerWindowEvent.kt @@ -25,10 +25,67 @@ package org.jetbrains.projector.common.protocol.toClient import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient import org.jetbrains.projector.common.protocol.data.* @Serializable -sealed class ServerWindowEvent +sealed class ServerWindowEvent { + //TODO: hacking to avoid `isType` in `when` pattern match + abstract val tpe: EventType +} + +/* + * hacking to avoid `isType` in `when` pattern match + */ +enum class EventType { + ServerPaintSrcEvent, + ServerWindowEvent, + ServerWindowPaintEvent, + ServerWindowToDoPaintEvent, + ServerWindowStateEvent, + ServerWindowToDoStateEvent, + ServerPaintArcEvent, + ServerPaintOvalEvent, + ServerPaintRoundRectEvent, + ServerPaintRectEvent, + ServerDrawLineEvent, + ServerCopyAreaEvent, + ServerSetFontEvent, + ServerSetClipEvent, + ServerSetStrokeEvent, + ServerDrawRenderedImageEvent, + ServerDrawRenderableImageEvent, + ServerDrawImageEvent, + ServerDrawStringEvent, + ServerPaintPolygonEvent, + ServerDrawPolylineEvent, + ServerSetTransformEvent, + ServerPaintPathEvent, + ServerSetCompositeEvent, + ServerSetPaintEvent, + ServerSetUnknownStrokeEvent; + + fun isDrawEvent():Boolean { + return when(this.ordinal){ + ServerDrawRenderedImageEvent.ordinal , + ServerDrawRenderableImageEvent.ordinal , + ServerDrawImageEvent.ordinal , + ServerDrawStringEvent.ordinal , + ServerDrawLineEvent.ordinal , + ServerDrawPolylineEvent.ordinal , + ServerPaintSrcEvent.ordinal , + ServerPaintArcEvent.ordinal , + ServerPaintOvalEvent.ordinal , + ServerPaintRoundRectEvent.ordinal , + ServerPaintRectEvent.ordinal , + ServerPaintPolygonEvent.ordinal , + ServerPaintPathEvent.ordinal , + ServerCopyAreaEvent.ordinal + -> true + else -> false + } + } +} @Serializable sealed class ServerWindowPaintEvent : ServerWindowEvent() @@ -59,7 +116,10 @@ data class ServerPaintArcEvent( val startAngle: Int, @SerialName("g") val arcAngle: Int, -) : ServerWindowToDoPaintEvent() +) : ServerWindowToDoPaintEvent() { + @Transient + override val tpe = EventType.ServerPaintArcEvent +} @Serializable @SerialName("b") @@ -74,7 +134,10 @@ data class ServerPaintOvalEvent( val width: Int, @SerialName("e") val height: Int, -) : ServerWindowPaintEvent() +) : ServerWindowPaintEvent() { + @Transient + override val tpe = EventType.ServerPaintOvalEvent +} @Serializable @SerialName("c") @@ -93,7 +156,10 @@ data class ServerPaintRoundRectEvent( val arcWidth: Int, @SerialName("g") val arcHeight: Int, -) : ServerWindowPaintEvent() +) : ServerWindowPaintEvent() { + @Transient + override val tpe = EventType.ServerPaintRoundRectEvent +} @Serializable @SerialName("d") @@ -108,7 +174,10 @@ data class ServerPaintRectEvent( val width: Double, @SerialName("e") val height: Double, -) : ServerWindowPaintEvent() +) : ServerWindowPaintEvent() { + @Transient + override val tpe = EventType.ServerPaintRectEvent +} @Serializable @SerialName("e") @@ -121,7 +190,10 @@ data class ServerDrawLineEvent( val x2: Int, @SerialName("d") val y2: Int, -) : ServerWindowPaintEvent() +) : ServerWindowPaintEvent() { + @Transient + override val tpe = EventType.ServerDrawLineEvent +} @Serializable @SerialName("f") @@ -138,7 +210,10 @@ data class ServerCopyAreaEvent( val dx: Int, @SerialName("f") val dy: Int, -) : ServerWindowPaintEvent() +) : ServerWindowPaintEvent() { + @Transient + override val tpe = EventType.ServerCopyAreaEvent +} @Serializable @SerialName("g") @@ -149,29 +224,44 @@ data class ServerSetFontEvent( val fontSize: Int, @SerialName("c") val ligaturesOn: Boolean = false, -) : ServerWindowStateEvent() +) : ServerWindowStateEvent() { + @Transient + override val tpe = EventType.ServerSetFontEvent +} @Serializable @SerialName("h") data class ServerSetClipEvent( @SerialName("a") val shape: CommonShape? = null, -) : ServerWindowStateEvent() +) : ServerWindowStateEvent() { + @Transient + override val tpe = EventType.ServerSetClipEvent +} @Serializable @SerialName("i") data class ServerSetStrokeEvent( @SerialName("a") val strokeData: StrokeData, -) : ServerWindowStateEvent() +) : ServerWindowStateEvent() { + @Transient + override val tpe = EventType.ServerSetStrokeEvent +} @Serializable @SerialName("j") -object ServerDrawRenderedImageEvent : ServerWindowToDoPaintEvent() +object ServerDrawRenderedImageEvent : ServerWindowToDoPaintEvent() { + @Transient + override val tpe = EventType.ServerDrawRenderedImageEvent +} @Serializable @SerialName("k") -object ServerDrawRenderableImageEvent : ServerWindowToDoPaintEvent() +object ServerDrawRenderableImageEvent : ServerWindowToDoPaintEvent() { + @Transient + override val tpe = EventType.ServerDrawRenderableImageEvent +} @Serializable @SerialName("l") @@ -180,7 +270,10 @@ data class ServerDrawImageEvent( val imageId: ImageId, @SerialName("b") val imageEventInfo: ImageEventInfo, -) : ServerWindowPaintEvent() +) : ServerWindowPaintEvent() { + @Transient + override val tpe = EventType.ServerDrawImageEvent +} @Serializable @SerialName("m") @@ -193,7 +286,10 @@ data class ServerDrawStringEvent( val y: Double, @SerialName("d") val desiredWidth: Double, -) : ServerWindowPaintEvent() +) : ServerWindowPaintEvent() { + @Transient + override val tpe = EventType.ServerDrawStringEvent +} @Serializable @SerialName("n") @@ -202,21 +298,30 @@ data class ServerPaintPolygonEvent( val paintType: PaintType, @SerialName("b") val points: List = emptyList(), // todo: remove default after https://github.com/Kotlin/kotlinx.serialization/issues/806 -) : ServerWindowPaintEvent() +) : ServerWindowPaintEvent() { + @Transient + override val tpe = EventType.ServerPaintPolygonEvent +} @Serializable @SerialName("o") data class ServerDrawPolylineEvent( @SerialName("a") val points: List = emptyList(), // todo: remove default after https://github.com/Kotlin/kotlinx.serialization/issues/806 -) : ServerWindowPaintEvent() +) : ServerWindowPaintEvent() { + @Transient + override val tpe = EventType.ServerDrawPolylineEvent +} @Serializable @SerialName("p") data class ServerSetTransformEvent( @SerialName("a") - val tx: List = emptyList(), // todo: remove default after https://github.com/Kotlin/kotlinx.serialization/issues/806 -) : ServerWindowStateEvent() + val tx: DoubleArray = doubleArrayOf(), // todo: remove default after https://github.com/Kotlin/kotlinx.serialization/issues/806 +) : ServerWindowStateEvent() { + @Transient + override val tpe = EventType.ServerSetTransformEvent +} @Serializable @SerialName("q") @@ -225,25 +330,37 @@ data class ServerPaintPathEvent( val paintType: PaintType, @SerialName("b") val path: CommonPath, -) : ServerWindowPaintEvent() +) : ServerWindowPaintEvent() { + @Transient + override val tpe = EventType.ServerPaintPathEvent +} @Serializable @SerialName("r") data class ServerSetCompositeEvent( @SerialName("a") val composite: CommonComposite, -) : ServerWindowStateEvent() +) : ServerWindowStateEvent() { + @Transient + override val tpe = EventType.ServerSetCompositeEvent +} @Serializable @SerialName("s") data class ServerSetPaintEvent( @SerialName("a") val paint: PaintValue, -) : ServerWindowStateEvent() +) : ServerWindowStateEvent() { + @Transient + override val tpe = EventType.ServerSetPaintEvent +} @Serializable @SerialName("t") data class ServerSetUnknownStrokeEvent( @SerialName("a") val className: String, -) : ServerWindowToDoStateEvent() +) : ServerWindowToDoStateEvent() { + @Transient + override val tpe = EventType.ServerSetUnknownStrokeEvent +} diff --git a/projector-launcher/build.gradle.kts b/projector-launcher/build.gradle.kts index 38a23a0f7..60a6ba601 100644 --- a/projector-launcher/build.gradle.kts +++ b/projector-launcher/build.gradle.kts @@ -109,7 +109,8 @@ val generateDistPackageJson by tasks.creating(Task::class) { "license" to "MIT", "name" to "projector-launcher", "description" to "Desktop launcher for Projector, written in Kotlin, Electron and Node", - "version" to project.version.toString() + "version" to project.version.toString(), + "v8Flags" to "--max-semi-space-size=4096 --min-semi-space-size=256 --max-old-space-size=4096 --initial-old-space-size=64" ) project.file("$distDir/package.json").writeText(packageJson.toJsonString()) diff --git a/projector-launcher/src/main/kotlin/ElectronApp.kt b/projector-launcher/src/main/kotlin/ElectronApp.kt index 23cf54435..cd6090cd5 100644 --- a/projector-launcher/src/main/kotlin/ElectronApp.kt +++ b/projector-launcher/src/main/kotlin/ElectronApp.kt @@ -24,10 +24,12 @@ @file:Suppress("JSCODE_ARGUMENT_SHOULD_BE_CONSTANT") import Electron.* +import kotlinext.js.jsObject import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.await import kotlinx.coroutines.launch import org.w3c.dom.url.URL +import kotlin.js.Promise external fun encodeURI(data: String): String @@ -44,6 +46,7 @@ class ElectronApp(val url: String) { lateinit var mainWindowUrl: String lateinit var mainWindowPrevUrl: String lateinit var mainWindowNextUrl: String + var loading = true var initialized = false fun navigateMainWindow(url: String) { @@ -77,9 +80,18 @@ class ElectronApp(val url: String) { } } """ - - var preloadPath = path.normalize(path.join(__dirname, "../assets/js/preload.js")) - this.mainWindow = BrowserWindow(js(windowOptions)(workAreaSize.width, workAreaSize.height, preloadPath)) + //js(windowOptions)(workAreaSize.width, workAreaSize.height, preloadPath) + val preloadPath: String = path.normalize(path.join(__dirname, "../app/assets/js/preload.js")) + this.mainWindow = BrowserWindow( jsObject { + width = workAreaSize.width + height = workAreaSize.height + webPreferences = jsObject { + contextIsolation = true + webSecurity = false + worldSafeExecuteJavaScript = true + preload = preloadPath + } + }) if (process.platform.unsafeCast().toLowerCase() !in setOf("win32", "darwin")) { // change icon for Linux and other systems: @@ -113,7 +125,9 @@ class ElectronApp(val url: String) { console.log("Can't load the URL: $validatedURL") console.log("errorDescription: $errorDescription, errorCode: $errorCode") + loading = true that.mainWindow.loadFile("openurl.html").await() + loading = false } else { Logger.direct( @@ -167,9 +181,71 @@ class ElectronApp(val url: String) { } fun registerApplicationLevelEvents() { + Menu.setApplicationMenu(Menu.buildFromTemplate( + arrayOf( + jsObject{ + label = app.name + submenu = arrayOf( + jsObject { + role = "about" + }, + jsObject { + role = "quit" + } + ) + }, + jsObject { + label = "Edit" + submenu = arrayOf( + jsObject { + role = "cut" + }, + jsObject { + role = "copy" + }, + jsObject { + role = "paste" + }, + jsObject { + role = "undo" + }, + jsObject { + role = "redo" + } + ) + + }, + jsObject{ + label = "View" + submenu = arrayOf( + jsObject { + role = "forceReload" + }, + jsObject{ + label = "Go To Landing Page" + click = { item,window,event -> + if(!loading){ + loading = true + that.mainWindow.loadFile("openurl.html") + .then{ loading = false } + } + } + } + ) + }, + jsObject { + label = "Developer" + submenu = arrayOf( + jsObject { + role = "toggleDevTools" + } + ) + } + ) + )) app.whenReady().then { this.createWindow() - ElectronUtil.disableAllStandardShortcuts() + //ElectronUtil.disableAllStandardShortcuts() if (GlobalSettings.DEVELOPER_TOOLS_ENABLED) { this.mainWindow.webContents.openDevTools() @@ -181,8 +257,6 @@ class ElectronApp(val url: String) { } ipcMain.on("projector-dom-ready") { event, _ -> - ElectronUtil.disableAllStandardShortcuts() - var defaultUrl = this.configData.defaultUrl if (null != defaultUrl) { event.sender.send("projector-set-url", defaultUrl) diff --git a/projector-launcher/src/main/kotlin/app.kt b/projector-launcher/src/main/kotlin/app.kt index caa140a43..7e5d5c8e2 100644 --- a/projector-launcher/src/main/kotlin/app.kt +++ b/projector-launcher/src/main/kotlin/app.kt @@ -31,7 +31,7 @@ fun main() { val argv = commandLineArguments() var url = argv.last() - if (url.endsWith("projector.exe")) { + if (url == null || !(url.startsWith("http") || url.startsWith("ws"))) { url = "" } diff --git a/projector-launcher/src/main/resources/assets/js/main.js b/projector-launcher/src/main/resources/assets/js/main.js index ebe078038..77ab4f45a 100644 --- a/projector-launcher/src/main/resources/assets/js/main.js +++ b/projector-launcher/src/main/resources/assets/js/main.js @@ -23,46 +23,48 @@ */ import {cacheNewUrlValue, populateDataList, projectorLauncherStorageKey, storage, urlCache} from './modules/urlcache.js'; -window.onload = function () { - document.getElementById("url-text-field").focus(); -}; +(function () { -if (storage.getItem(projectorLauncherStorageKey)) { - let parse = JSON.parse(storage.getItem(projectorLauncherStorageKey)); - parse.forEach(url => urlCache.set(url, url)); - populateDataList(); -} -document.querySelector('#connect-button').addEventListener('click', function () { - connect() -}); -document.querySelector('#url-text-field').addEventListener('keypress', function (e) { - if (e.key === 'Enter') { - connect() - } -}); -function connect() { - let url = document.getElementById("url-text-field").value; - if (isEmpty(url)) { - return; + window.onload = function () { + document.getElementById("url-text-field").focus(); + }; + + if (storage.getItem(projectorLauncherStorageKey)) { + let parse = JSON.parse(storage.getItem(projectorLauncherStorageKey)); + parse.forEach(url => urlCache.set(url, url)); + populateDataList(); } - const {ipcRenderer} = require('electron') - ipcRenderer.send("projector-connect", url); + document.querySelector('#connect-button').addEventListener('click', function () { + connect() + }); + + document.querySelector('#url-text-field').addEventListener('keypress', function (e) { + if (e.key === 'Enter') { + connect() + } + }); - cacheNewUrlValue(url); -} + function connect() { + let url = document.getElementById("url-text-field").value; + if (isEmpty(url)) { + return; + } -//$( document ).ready(function() { -const {ipcRenderer} = require('electron') -ipcRenderer.on('projector-set-url', (event, arg) => { - console.log("New URL: " + arg); - document.getElementById("url-text-field").value = arg -}) -//}); + window.ipcRenderer.send("projector-connect", url); -function isEmpty(str) { - return (!str || 0 === str.length); -} + cacheNewUrlValue(url); + } + + window.ipcRenderer.on('projector-set-url', (event, arg) => { + console.log("New URL: " + arg); + document.getElementById("url-text-field").value = arg + }) + + function isEmpty(str) { + return (!str || 0 === str.length); + } +})(); diff --git a/projector-launcher/src/main/resources/assets/js/preload.js b/projector-launcher/src/main/resources/assets/js/preload.js index e4e599efc..b5d2317fd 100644 --- a/projector-launcher/src/main/resources/assets/js/preload.js +++ b/projector-launcher/src/main/resources/assets/js/preload.js @@ -21,6 +21,8 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ +const electron = require('electron'); + function domReady(fn) { if (document.readyState === "complete" || document.readyState === "interactive") { setTimeout(fn, 1); @@ -30,6 +32,11 @@ function domReady(fn) { } } +electron.contextBridge.exposeInMainWorld('ipcRenderer', { + send: (name,param) => electron.ipcRenderer.send(name, param), + on: (name,callback) => electron.ipcRenderer.on(name,callback) +}) + domReady(function () { console.log("DOM loaded") const {ipcRenderer} = require('electron') diff --git a/projector-server-core/src/main/kotlin/org/jetbrains/projector/server/core/convert/toClient/Convert.kt b/projector-server-core/src/main/kotlin/org/jetbrains/projector/server/core/convert/toClient/Convert.kt index 5797313a9..79e9e26ae 100644 --- a/projector-server-core/src/main/kotlin/org/jetbrains/projector/server/core/convert/toClient/Convert.kt +++ b/projector-server-core/src/main/kotlin/org/jetbrains/projector/server/core/convert/toClient/Convert.kt @@ -105,7 +105,7 @@ public fun StrokeData.toStroke(): Stroke { cap, join, miterLimit, - dashArray?.toFloatArray(), + dashArray?.map { double -> double.toFloat() }?.toFloatArray(), dashPhase ) } @@ -135,7 +135,7 @@ public fun BasicStroke.toBasicStrokeData(): StrokeData.Basic { endCap = cap, miterLimit = miterLimit, dashPhase = dashPhase, - dashArray = dashArray?.toList() + dashArray = dashArray?.map { float -> float.toDouble() }?.toTypedArray() ) } diff --git a/projector-server-core/src/main/kotlin/org/jetbrains/projector/server/core/convert/toClient/Transform.kt b/projector-server-core/src/main/kotlin/org/jetbrains/projector/server/core/convert/toClient/Transform.kt index 0a9d5542b..42d4f4e6b 100644 --- a/projector-server-core/src/main/kotlin/org/jetbrains/projector/server/core/convert/toClient/Transform.kt +++ b/projector-server-core/src/main/kotlin/org/jetbrains/projector/server/core/convert/toClient/Transform.kt @@ -39,7 +39,7 @@ public fun extractData(iterable: MutableIterable): List { private fun Rectangle2D.isVisible(clip: ServerSetClipEvent, tx: ServerSetTransformEvent): Boolean { val clipRect = clip.shape as? CommonRectangle ?: return true // can't tell - val identityTransformStrBounds = AffineTransform(tx.tx.toDoubleArray()).createTransformedShape(this) + val identityTransformStrBounds = AffineTransform(tx.tx).createTransformedShape(this) return identityTransformStrBounds.intersects(clipRect.x, clipRect.y, clipRect.width, clipRect.height) }