Pantrix UI

Boyutlandırma imleçleri

Compose Multiplatform'da sürüklenince bir şeyi büyüten ya da küçülten bir denetimin üstünde imlecin neden kendiliğinden değişmediği, kütüphanenin bunun için ne verdiği ve bunun nasıl sınandığı. Sorun Pantrix'e özgü değildir: iki panel arasında bir tutamacı, genişliği sürüklenen bir tablo kolonu ya da kenarından büyütülen bir paneli olan her Compose Multiplatform projesinde aynı şekilde görülür.

Compose hakkında burada söylenenler Compose Multiplatform 1.12.2'nin kaynaklarında (PointerIcon.kt, PointerIcon.desktop.kt, PointerIcon.web.kt, PointerIcon.ios.kt, RootNodeOwner.skiko.kt) ve Android'de androidx.compose.ui'nin PointerIcon.android.kt'sinde okunmuştur. Doğrulanamayanlar 7. bölümde ayrıca sayılır.

1. Sorun

Belirti

Neden

PointerIcon ortak kodda yalnızca bir arayüzdür; dört hazır değeri platformların actual'larıdır. Başka bir imleci kurmanın yolu her platformda başkadır:

PlatformCompose'un hazır dördüBaşka bir imleç nasıl kurulurBoyutlandırma imleci
AndroidAndroidPointerIconType(TYPE_ARROW), TYPE_CROSSHAIR, TYPE_TEXT, TYPE_HANDPointerIcon(pointerIconType: Int) ya da PointerIcon(android.view.PointerIcon)TYPE_HORIZONTAL_DOUBLE_ARROW, TYPE_VERTICAL_DOUBLE_ARROW
Masaüstü (JVM)AwtCursor(Cursor(Cursor.DEFAULT_CURSOR)) ve ötekilerPointerIcon(cursor: java.awt.Cursor)Cursor.E_RESIZE_CURSOR, Cursor.N_RESIZE_CURSOR
Web (js, wasmJs)BrowserCursor("default"), "crosshair", "text", "pointer"PointerIcon.fromKeyword(keyword: String), @ExperimentalComposeUiApiCSS'in col-resize ve row-resize'ı
iOSUIKitCursor("default") ve ötekilerYok: sınıf privateYok

İki ayrıntı işi büyütür:

  1. iOS'ta Compose hiçbir imleci şekillendirmez. Ortak sahne imleci platformContext.setPointerIcon(…)'a verir (RootNodeOwner.skiko.kt); bu çağrının varsayılanı boştur (PlatformContext.skiko.kt) ve iOS onu karşılamaz. Compose'un kendi dördü de iPad'in imlecini değiştirmez. iOS'un karşılığı ancak bir ad olarak yazılır.

  2. Web'in kurucusu deneyseldir. fromKeyword @ExperimentalComposeUiApi taşır; kullanan yer @OptIn ister. Verilen kelime tuvalin style.cursor'ına olduğu gibi yazılır (ComposeWindowInternal.web.kt): yanlış yazılan bir kelime hata vermez, imleç ok kalır.

2. Çözüm

İki boyutlandırma imleci ortak kodda bir expect değeri olarak bildirilir, her platform kendi türüyle karşılar. Kullanan yer yalnızca ortak adı görür ve onu Compose'un pointerHoverIcon değiştiricisine verir. Değiştirici imleci, işaretçi düğümün üstüne girince gösterir, çıkınca bırakır.

3. Pantrix ile

import com.pantrixui.foundation.platform.PantrixPointerIcons

Box(
    Modifier
        .width(12.dp)
        .fillMaxHeight()
        .pointerHoverIcon(PantrixPointerIcons.ResizeHorizontal)
        .draggable(rememberDraggableState { by -> width += with(density) { by.toDp() } }, Orientation.Horizontal)
)

Kütüphanede kullanıldığı yerler:

BileşenNerede
PantrixListDetailScaffold(resizable = true)Liste ile ayrıntı arasındaki tutamaç
PantrixDataTableGenişliği değişen bir kolonun başlığının ucu; yalnızca fareyle kullanılan platformda, çünkü parmak bu şeridi sürüklemez
Grafiklerin gezgini (ChartZoom(navigator = true))Görünen parçanın iki ucu ve bir uç sürüklenirken. Yatay çubuk grafiğin dikey gezgininde ResizeVertical

3.1. Konuma göre değişen imleç

Gezgin tek bir düğümdür: imleç düğümün her yerinde aynı değildir. Görünen parçanın ucunun üstünde boyutlandırma imleci, ortasında ve dışında oktur. Bunun için işaretçinin konumu dinlenir ve imleç bir durumdan okunur:

var hovered by remember { mutableStateOf<Grip?>(null) }
Box(
    Modifier
        .pointerInput(Unit) {
            awaitPointerEventScope {
                while (true) {
                    val event = awaitPointerEvent()
                    val x = event.changes.first().position.x
                    hovered = if (event.type == PointerEventType.Exit) null else gripUnder(x)
                }
            }
        }
        .pointerHoverIcon(if (held ?: hovered != null) PantrixPointerIcons.ResizeHorizontal else PointerIcon.Default)
)

Compose, imleç değişince işaretçi düğümün üstündeyse yenisini hemen gösterir (HoverIconModifierNode.icon'un atayıcısı). Sürüklenen uç (held) sürükleme bitene kadar tutulur: uç bir sınıra dayanıp işaretçi ondan uzaklaşsa da imleç boyutlandırma imleci kalır.

3.2. Klavye ve ekran okuyucu

İmleç yalnızca fareyi tutana söyler. Gerçek bir tutamaç klavyeye ve ekran okuyucuya da açıktır: odak alır, ok tuşlarıyla adım adım, Home ve End ile uçlarına gider ve ekran okuyucuya ayarlanabilir bir değer olarak söylenir (progressBarRangeInfo ve setProgress). PantrixListDetailScaffold'un tutamacı ve PantrixDataTable'ın kolon ucu öyledir; demo'nun "Platform ve sistem" sayfasındaki örnek, imleci tek başına göstermek için bunları yazmaz.

4. Pantrix olmadan

4.1. Ortak kod (commonMain)

package com.example.pointer

import androidx.compose.ui.input.pointer.PointerIcon

object ResizePointers {
    val Horizontal: PointerIcon get() = resizeHorizontal
    val Vertical: PointerIcon get() = resizeVertical
}

internal expect val resizeHorizontal: PointerIcon
internal expect val resizeVertical: PointerIcon

4.2. Platform karşılıkları

androidMain:

internal actual val resizeHorizontal: PointerIcon = PointerIcon(android.view.PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW)
internal actual val resizeVertical: PointerIcon = PointerIcon(android.view.PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW)

jvmMain:

import java.awt.Cursor

internal actual val resizeHorizontal: PointerIcon = PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR))
internal actual val resizeVertical: PointerIcon = PointerIcon(Cursor(Cursor.N_RESIZE_CURSOR))

webMain (js ile wasmJs'nin ortak kaynak kümesi; fromKeyword ikisinde de aynı bildirimdir):

import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.input.pointer.fromKeyword

@OptIn(ExperimentalComposeUiApi::class)
internal actual val resizeHorizontal: PointerIcon = PointerIcon.fromKeyword("col-resize")

@OptIn(ExperimentalComposeUiApi::class)
internal actual val resizeVertical: PointerIcon = PointerIcon.fromKeyword("row-resize")

iosMain:

internal actual val resizeHorizontal: PointerIcon = PointerIcon.Default
internal actual val resizeVertical: PointerIcon = PointerIcon.Default

Web'de ew-resize ve ns-resize de olur. CSS col-resize ile row-resize'ı bir kolonun ya da satırın boyutunun değişeceğini söyleyen imleç olarak tanımlar; iki panelin arasındaki tutamaç ve bir kolonun ucu tam bu iştir.

5. Test etmek

İmleç anlambilimde (semantics) yoktur: bir testte SemanticsProperties ile okunamaz. Ama pointerHoverIcon'un ürettiği öğe bir data class'tır (PointerHoverIconModifierElement), eşitliği imlecinkidir; düğümün değiştiricileri arasında aranabilir:

fun SemanticsNodeInteraction.showsPointer(icon: PointerIcon): Boolean =
    fetchSemanticsNode().layoutInfo.getModifierInfo().any { it.modifier == Modifier.pointerHoverIcon(icon) }

onNodeWithTag("ListDetailHandle_mail").showsPointer(PantrixPointerIcons.ResizeHorizontal)

Bu, değiştiricinin etiketle aynı düğümde olmasını ister: pantrixTestTag ile pointerHoverIcon aynı Box'ta. Konuma göre değişen bir imleç için önce fare oraya götürülür:

onNodeWithTag(NAVIGATOR).performMouseInput { moveTo(Offset(x, centerY)) }
waitForIdle()
assertTrue(onNodeWithTag(NAVIGATOR).showsPointer(PantrixPointerIcons.ResizeHorizontal))

Platformun gerçek imleci de sınanır: her platformun testi PantrixPointerIcons'ın o platformun beklenen imleci olduğuna bakar. Masaüstünde PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR))'a eşittir (AwtCursor eşitliği imlecin türüyle kurar), Android'de PointerIcon(TYPE_HORIZONTAL_DOUBLE_ARROW)'a, web'de PointerIcon.fromKeyword("col-resize")'a, iOS'ta PointerIcon.Default'a.

6. Tuzaklar

7. Doğrulama

Testler

NeNeredeHedef
Her platformun imleciui/foundation/src/{jvmTest,androidHostTest,webTest,iosTest}/…/platform/PointerIconsOn…Test.ktJVM, Android, JS, Wasm, iOS
Tutamacın imleciListDetailScaffoldTest.the_handle_makes_the_list_wider_and_narrowerOrtak testler: JVM, iOS, JS, Wasm
Kolon ucunun imleci, parmakta imleç yokDataTableTest.the_end_of_a_resizable_header_is_dragged_to_a_width, a_finger_does_not_drag_a_header_s_end_and_a_keyboard_still_moves_itOrtak testler
Gezginde uca göre değişen imleç, sürüklerken kalmasıChartZoomToolsTest.a_pointer_over_an_end_of_the_part_in_view_is_one_that_resizes, ChartTurnedZoomTest.the_part_in_view_is_dragged_down_the_navigator_of_a_horizontal_chartOrtak testler

Elle bozulan davranışlar

Yedi mutant elle denendi, yedisi de testlerle öldü: tutamaçtan ve kolon ucundan değiştiricinin silinmesi, gezginin konumu dinlememesi, dikey gezginin yatay imleç vermesi, uç yakınlığının sıfırlanması, sürüklenen ucun imlece katılmaması ve sürükleme bitince bırakılmaması.

Doğrulanmayanlar

8. Bu depoda nerede