@@ -30,8 +30,14 @@ import androidx.compose.ui.geometry.Rect
3030import androidx.compose.ui.graphics.asComposeCanvas
3131import androidx.compose.ui.input.InputMode
3232import androidx.compose.ui.input.InputModeManager
33+ import androidx.compose.ui.input.key.Key
3334import androidx.compose.ui.input.key.KeyEvent
35+ import androidx.compose.ui.input.key.KeyEventType
36+ import androidx.compose.ui.input.key.isCtrlPressed
37+ import androidx.compose.ui.input.key.isMetaPressed
38+ import androidx.compose.ui.input.key.key
3439import androidx.compose.ui.input.key.toComposeEvent
40+ import androidx.compose.ui.input.key.type
3541import androidx.compose.ui.input.pointer.BrowserCursor
3642import androidx.compose.ui.input.pointer.PointerButtons
3743import androidx.compose.ui.input.pointer.PointerEventType
@@ -41,6 +47,7 @@ import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
4147import androidx.compose.ui.input.pointer.PointerType
4248import androidx.compose.ui.input.pointer.composeButton
4349import androidx.compose.ui.input.pointer.composeButtons
50+ import androidx.compose.ui.internal.focusExt
4451import androidx.compose.ui.navigationevent.BackNavigationEventInput
4552import androidx.compose.ui.platform.DefaultArchitectureComponentsOwner
4653import androidx.compose.ui.platform.DefaultInputModeManager
@@ -90,13 +97,16 @@ import kotlinx.coroutines.launch
9097import org.jetbrains.skia.Canvas
9198import org.jetbrains.skiko.SkiaLayer
9299import org.jetbrains.skiko.SkikoRenderDelegate
100+ import org.jetbrains.skiko.hostOs
93101import org.w3c.dom.AddEventListenerOptions
94102import org.w3c.dom.Element
95103import org.w3c.dom.HTMLCanvasElement
96104import org.w3c.dom.HTMLDivElement
97105import org.w3c.dom.HTMLElement
98106import org.w3c.dom.HTMLStyleElement
99107import org.w3c.dom.HTMLTitleElement
108+ import org.w3c.dom.HTMLTextAreaElement
109+ import org.w3c.dom.LOADING
100110import org.w3c.dom.MediaQueryListEvent
101111import org.w3c.dom.Node
102112import org.w3c.dom.OPEN
@@ -186,6 +196,7 @@ internal class DefaultWindowState(private val viewportContainer: Element) : Comp
186196internal class ComposeWindow (
187197 private val canvas : HTMLCanvasElement ,
188198 private val rootNode : Node ,
199+ private val layerRoot : HTMLElement ,
189200 private val interopContainerElement : HTMLDivElement ,
190201 private val a11yContainerElement : HTMLDivElement ? ,
191202 private val configuration : ComposeViewportConfiguration ,
@@ -214,6 +225,8 @@ internal class ComposeWindow(
214225 // Used in WebTextInputService. Also see https://youtrack.jetbrains.com/issue/CMP-8611
215226 private var activeTouchOffset: Offset ? = null
216227
228+ private val clipTarget = clipTargetElement(canvas)
229+
217230 private val platformContext: PlatformContext = object : PlatformContext by PlatformContext .Empty () {
218231 override val windowInfo get() = _windowInfo
219232 override val architectureComponentsOwner get() = archComponentsOwner
@@ -331,7 +344,27 @@ internal class ComposeWindow(
331344 val keyEvent = keyboardEvent.toComposeEvent()
332345 val processed = scene.sendKeyEvent(keyEvent) ||
333346 navigationEventInput.onKeyEvent(keyEvent)
334- if (processed) keyboardEvent.preventDefault()
347+
348+ if (processed) {
349+ keyboardEvent.preventDefault()
350+ } else if (keyEvent.type == KeyEventType .KeyDown ){
351+ processClipKeyDown(keyEvent)
352+ }
353+ }
354+
355+ private val isMacOS = hostOs.isMacOS
356+
357+ private fun processClipKeyDown (keyEvent : KeyEvent ) {
358+ val mod = if (isMacOS) keyEvent.isMetaPressed else keyEvent.isCtrlPressed
359+ if (! mod) return
360+ if (keyEvent.key == Key .C || keyEvent.key == Key .V || keyEvent.key == Key .X ) {
361+ // A browser is about to dispatch a Clipboard Event.
362+ // Some browsers do not dispatch Clipboard events to <canvas> despite it having focus,
363+ // so let it dispatch the event to clipTarget (text area).
364+ // By focusing on it, we let a browser dispatch the event to it.
365+ layerRoot.appendChild(clipTarget)
366+ focusExt(clipTarget, true )
367+ }
335368 }
336369
337370 private fun initEvents (canvas : HTMLCanvasElement ) {
@@ -421,13 +454,15 @@ internal class ComposeWindow(
421454
422455 val interopContainer = WebInteropContainer (InteropViewGroup (interopContainerElement))
423456
457+ val clipEventsTargetProvider: () -> HTMLElement = {
458+ (platformContext.textInputService as WebTextInputService ).getBackingInput()
459+ ? : clipTarget
460+ }
424461 scene.setContent {
425462 CompositionLocalProvider (
426463 LocalSystemTheme provides systemThemeObserver.currentSystemTheme.value,
427464 LocalInteropContainer provides interopContainer,
428- LocalActiveClipEventsTarget provides {
429- (platformContext.textInputService as WebTextInputService ).getBackingInput() ? : canvas
430- },
465+ LocalActiveClipEventsTarget provides clipEventsTargetProvider,
431466 content = {
432467 interopContainer.TrackInteropPlacementContainer {
433468 content()
@@ -681,6 +716,7 @@ fun CanvasBasedWindow(
681716 canvas = canvas,
682717 rootNode = canvas.getRootNode(),
683718 // a detached container
719+ layerRoot = document.createElement(" div" ) as HTMLDivElement ,
684720 interopContainerElement = document.createElement(" div" ) as HTMLDivElement ,
685721 a11yContainerElement = document.createElement(" div" ) as HTMLDivElement ,
686722 content = content,
@@ -781,10 +817,43 @@ fun ComposeViewport(
781817 ComposeWindow (
782818 canvas = canvas,
783819 rootNode = shadowRoot,
820+ layerRoot = layerRoot,
784821 interopContainerElement = interopContainerElement,
785822 a11yContainerElement = a11yContainerElement,
786823 content = content,
787824 configuration = configuration,
788825 state = DefaultWindowState (viewportContainer)
789826 )
827+ }
828+
829+ /* *
830+ * The purpose of the clipTarget element is to briefly steal the focus to let the browser dispatch
831+ * ClipboardEvent to it. Then it returns the focus to the canvas.
832+ */
833+ private fun clipTargetElement (canvas : HTMLCanvasElement ): HTMLTextAreaElement {
834+ val clipTarget = (document.createElement(" textarea" ) as HTMLTextAreaElement ).apply {
835+ tabIndex = - 1
836+ setAttribute(" aria-hidden" , " true" )
837+ style.position = " fixed"
838+ style.left = " -1000px"
839+ style.top = " 0"
840+ style.opacity = " 0"
841+ style.width = " 1px"
842+ style.height = " 1px"
843+ }
844+
845+ val clipEventListener: (Event ) -> Unit = { _ ->
846+ window.requestAnimationFrame {
847+ focusExt(canvas, true )
848+ clipTarget.remove()
849+ }
850+ }
851+
852+ // Here just return the focus to canvas.
853+ // For the actual event handling see rememberClipboardEventsHandler implementations.
854+ clipTarget.addEventListener(" copy" , clipEventListener)
855+ clipTarget.addEventListener(" cut" , clipEventListener)
856+ clipTarget.addEventListener(" paste" , clipEventListener)
857+
858+ return clipTarget
790859}
0 commit comments