From c719a7eb9e74fc26ca10b6c03c8139119434e51e Mon Sep 17 00:00:00 2001 From: BREW Date: Sun, 18 Oct 2026 12:55:37 +0900 Subject: [PATCH 1/9] Feat: The gate checks the macOS title bar and what a screen reader is told, and a Kotlin/Native macOS cell hello-awtfree's self-check on macOS walks its own accessibility elements in process (a hosted runner trusts no outside client) and fails unless the system buttons are there, the content runs under the bar, no title is drawn, the caption matches the buttons' frames, the corner radius is the measured table's, and the field and the button are found by role and name. The gate runs it with the Normal and the Simple title bar and, on macOS 26, requires Normal's buttons further in. The system AX API's answer is printed with its error code. "One executable (Kotlin/Native, macos-arm64)" links the same content (hello-awtfree/src/shared) in native-macos with the native desktop plugin against the core fork's macosArm64 Compose and runs the same gate. The core Compose cache is keyed by the sources the build reads rather than by commit. FR-19.4, FR-19.7, NFR-8. Closes #62. --- .github/workflows/extended-native-image.yml | 101 ++++++- extended/ci/core-compose/action.yml | 16 +- .../hello-awtfree-kn/build.gradle.kts | 39 +++ .../hello-awtfree-kn/gradle.properties | 2 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43453 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + .../native-desktop/hello-awtfree-kn/gradlew | 249 ++++++++++++++++++ .../hello-awtfree-kn/settings.gradle.kts | 19 ++ .../src/macosMain/kotlin/Main.kt | 156 +++++++++++ extended/native-image/README.md | 18 ++ extended/native-image/check-one-executable.sh | 30 +++ .../hello-awtfree/build.gradle.kts | 3 + .../src/linux/kotlin/Platform.kt | 11 + .../src/macos/kotlin/Platform.kt | 63 +++++ .../hello-awtfree/src/main/kotlin/Main.kt | 75 ++---- .../src/shared/kotlin/Content.kt | 70 +++++ .../{main => shared}/kotlin/TextSelfCheck.kt | 0 17 files changed, 792 insertions(+), 67 deletions(-) create mode 100644 extended/native-desktop/hello-awtfree-kn/build.gradle.kts create mode 100644 extended/native-desktop/hello-awtfree-kn/gradle.properties create mode 100644 extended/native-desktop/hello-awtfree-kn/gradle/wrapper/gradle-wrapper.jar create mode 100644 extended/native-desktop/hello-awtfree-kn/gradle/wrapper/gradle-wrapper.properties create mode 100755 extended/native-desktop/hello-awtfree-kn/gradlew create mode 100644 extended/native-desktop/hello-awtfree-kn/settings.gradle.kts create mode 100644 extended/native-desktop/hello-awtfree-kn/src/macosMain/kotlin/Main.kt create mode 100644 extended/native-image/hello-awtfree/src/shared/kotlin/Content.kt rename extended/native-image/hello-awtfree/src/{main => shared}/kotlin/TextSelfCheck.kt (100%) diff --git a/.github/workflows/extended-native-image.yml b/.github/workflows/extended-native-image.yml index 9b04b327f39..12fe644e8fe 100644 --- a/.github/workflows/extended-native-image.yml +++ b/.github/workflows/extended-native-image.yml @@ -23,7 +23,7 @@ permissions: env: # thisisthepy/compose-multiplatform-core-extended, the commit the window modules and # extended/skiko come from. - CORE_EXTENDED_REF: 57b4a0e102d837ba3271699297b69278fcefedef + CORE_EXTENDED_REF: fcc18659834c343cf60181919de0d96ad513919f # The plugin as a local development build, asking for the core fork's Compose at its own # version. The core fork's desktop Compose is built once from CORE_EXTENDED_REF, by the # core-compose job (extended/ci/core-compose), and handed to the jobs that need it. @@ -220,11 +220,7 @@ jobs: - name: linux-arm64 os: ubuntu-24.04-arm nik: vm-openjdk25.0.4.1+2-25.0.4.1+1-linux-aarch64 - # Kotlin/Native packaging cells go here. Each one runs check-native-config.sh (already - # scans extended/window/native) and, after linking, `nm` on the executable for - # Objective-C or class-name lookups; see the K/N section of the plugin change list. - # Kotlin/Native packaging cells go here. The K/N window agent adds them: one per - # platform, building the native window modules into an executable and starting it. + # The Kotlin/Native cells are the package-kn job below. runs-on: ${{ matrix.os }} timeout-minutes: 240 env: @@ -445,3 +441,96 @@ jobs: extended/native-image/*/build/tmp/packageNativeImage/**/*.log extended/native-image/*/build/reports/ if-no-files-found: ignore + + # The same gate for a Kotlin/Native executable: hello-awtfree's content in the core fork's + # Kotlin/Native window (native-macos), linked by Gradle with the native desktop plugin against + # the core fork's macosArm64 Compose, then checked as the GraalVM images are. Linux cells join + # this matrix with the Linux window; Windows comes later. + package-kn: + name: One executable (Kotlin/Native, ${{ matrix.name }}) + strategy: + fail-fast: false + matrix: + include: + - name: macos-arm64 + os: macos-26 + runs-on: ${{ matrix.os }} + timeout-minutes: 300 + env: + SAMPLE_KN: extended/native-desktop/hello-awtfree-kn + steps: + - uses: actions/checkout@v4 + + - uses: actions/checkout@v4 + with: + repository: thisisthepy/compose-multiplatform-core-extended + ref: ${{ env.CORE_EXTENDED_REF }} + path: core-extended + sparse-checkout: extended/window + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + + # Built once per CORE_EXTENDED_REF and restored from the cache after that. + - name: The core fork's Compose for macosArm64 + uses: ./extended/ci/core-compose + with: + ref: ${{ env.CORE_EXTENDED_REF }} + targets: macosArm64 + + - name: Cache the Kotlin toolchain and konan + uses: actions/cache@v4 + with: + path: | + ~/Library/Caches/JetBrains/Kotlin + ~/.konan + key: kn-window-${{ matrix.name }}-${{ env.CORE_EXTENDED_REF }}-${{ hashFiles('extended/native-desktop/hello-awtfree-kn/*.kts') }} + restore-keys: | + kn-window-${{ matrix.name }}- + + - name: Publish the Kotlin/Native window modules to mavenLocal + shell: bash + working-directory: core-extended/extended/window + run: | + set -euo pipefail + cp project.yaml project.yaml.all + printf 'modules:\n - common\n - native/macos\n' > project.yaml + ./kotlin publish mavenLocal + cp project.yaml.all project.yaml + find ~/.m2/repository/org/thisisthepy/compose/window -name '*.module' | sort + + - name: The native desktop plugin to mavenLocal + working-directory: gradle-plugins + run: ./gradlew --no-daemon --console=plain :kotlin-native-desktop:publishToMavenLocal -Pdeploy.version=0.1.0-extended-dev + + - name: Link the Kotlin/Native sample + working-directory: ${{ env.SAMPLE_KN }} + run: ./gradlew --no-daemon --console=plain linkReleaseExecutableMacosArm64 + + - name: Stage the executable alone + shell: bash + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/kn-out" + cp "$SAMPLE_KN/build/bin/macosArm64/releaseExecutable/hello-awtfree-kn.kexe" "$RUNNER_TEMP/kn-out/hello-awtfree-kn" + ls -la "$RUNNER_TEMP/kn-out" + + - name: Take every ICU data file off the runner + shell: bash + run: | + { find /usr /opt /Library /Applications /Users -name 'icudtl.dat' 2>/dev/null || true; } | + while IFS= read -r file; do echo "removing $file"; sudo rm -f "$file"; done + + - name: One file, system libraries only, Korean with no ICU data, run alone + shell: bash + run: bash extended/native-image/check-one-executable.sh "$RUNNER_TEMP/kn-out" "$RUNNER_TEMP/one-executable" + + - name: Keep the self-check drawing + if: always() + uses: actions/upload-artifact@v4 + with: + name: one-executable-kn-${{ matrix.name }} + path: ${{ runner.temp }}/one-executable/*.png + if-no-files-found: ignore diff --git a/extended/ci/core-compose/action.yml b/extended/ci/core-compose/action.yml index a66e9c5454c..06f66a6a47b 100644 --- a/extended/ci/core-compose/action.yml +++ b/extended/ci/core-compose/action.yml @@ -17,12 +17,26 @@ inputs: runs: using: composite steps: + # Keyed by what the Compose build reads, not by the commit: the whole tree except extended/ + # and .github/ (the fork's own modules and CI), plus the publishing script, as the core + # fork's own fork-compose action keys it. A commit that only changes the window modules + # then reuses the Compose already built, which for macosArm64 takes hours. Trees only are + # fetched, so working the key out costs seconds. - name: Cache key id: key shell: bash run: | + set -euo pipefail targets="$(echo '${{ inputs.targets }}' | tr ' ' '\n' | sort | paste -sd- -)" - echo "key=core-compose-${{ runner.os }}-${{ runner.arch }}-$targets-${{ inputs.ref }}" >> "$GITHUB_OUTPUT" + probe="$RUNNER_TEMP/core-compose-key" + rm -rf "$probe" + git init -q "$probe" + git -C "$probe" fetch -q --depth=1 --filter=blob:none \ + https://github.com/thisisthepy/compose-multiplatform-core-extended.git '${{ inputs.ref }}' + sources="$(git -C "$probe" ls-tree FETCH_HEAD | awk -F'\t' '$2 != "extended" && $2 != ".github" { print }' | git -C "$probe" hash-object --stdin)" + script="$(git -C "$probe" rev-parse FETCH_HEAD:extended/compose)" + rm -rf "$probe" + echo "key=core-compose-${{ runner.os }}-${{ runner.arch }}-$targets-$sources-$script" >> "$GITHUB_OUTPUT" - name: Restore the core fork's Compose id: restore diff --git a/extended/native-desktop/hello-awtfree-kn/build.gradle.kts b/extended/native-desktop/hello-awtfree-kn/build.gradle.kts new file mode 100644 index 00000000000..1cf6bdcce9c --- /dev/null +++ b/extended/native-desktop/hello-awtfree-kn/build.gradle.kts @@ -0,0 +1,39 @@ +// The AWT-free sample as a Kotlin/Native macOS executable: the same Compose content as +// extended/native-image/hello-awtfree, in the core fork's Kotlin/Native AppKit and Metal window +// (org.thisisthepy.compose.window:native-macos), with no JVM at all. The one-executable gate +// checks it the way it checks the GraalVM images. +// +// ./gradlew linkReleaseExecutableMacosArm64 +// +// Kotlin has to be at least the version the window klibs were compiled with: a klib from a newer +// compiler does not link. +plugins { + kotlin("multiplatform") version "2.4.10" + kotlin("plugin.compose") version "2.4.10" + id("org.thisisthepy.kotlin.native.desktop") version "0.1.0-extended-dev" +} + +// The core fork's Compose and window modules, as compose-multiplatform-core-extended publishes +// them to the local Maven repository. +val compose = "1.11.1-ext.1" +val window = "0.1.0" + +kotlin { + macosArm64 { + binaries.executable { + baseName = "hello-awtfree-kn" + entryPoint = "awtfree.main" + } + } + sourceSets.macosMain { + // The content and the text check hello-awtfree draws and runs. + kotlin.srcDir("../../native-image/hello-awtfree/src/shared/kotlin") + dependencies { + implementation("org.thisisthepy.compose.window:common:$window") + implementation("org.thisisthepy.compose.window:native-macos:$window") + implementation("org.thisisthepy.compose.runtime:runtime:$compose") + implementation("org.thisisthepy.compose.foundation:foundation:$compose") + implementation("org.thisisthepy.compose.ui:ui:$compose") + } + } +} diff --git a/extended/native-desktop/hello-awtfree-kn/gradle.properties b/extended/native-desktop/hello-awtfree-kn/gradle.properties new file mode 100644 index 00000000000..47099b1aff8 --- /dev/null +++ b/extended/native-desktop/hello-awtfree-kn/gradle.properties @@ -0,0 +1,2 @@ +kotlin.code.style=official +org.gradle.jvmargs=-Xmx3g diff --git a/extended/native-desktop/hello-awtfree-kn/gradle/wrapper/gradle-wrapper.jar b/extended/native-desktop/hello-awtfree-kn/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e6441136f3d4ba8a0da8d277868979cfbc8ad796 GIT binary patch literal 43453 zcma&N1CXTcmMvW9vTb(Rwr$&4wr$(C?dmSu>@vG-+vuvg^_??!{yS%8zW-#zn-LkA z5&1^$^{lnmUON?}LBF8_K|(?T0Ra(xUH{($5eN!MR#ZihR#HxkUPe+_R8Cn`RRs(P z_^*#_XlXmGv7!4;*Y%p4nw?{bNp@UZHv1?Um8r6)Fei3p@ClJn0ECfg1hkeuUU@Or zDaPa;U3fE=3L}DooL;8f;P0ipPt0Z~9P0)lbStMS)ag54=uL9ia-Lm3nh|@(Y?B`; zx_#arJIpXH!U{fbCbI^17}6Ri*H<>OLR%c|^mh8+)*h~K8Z!9)DPf zR2h?lbDZQ`p9P;&DQ4F0sur@TMa!Y}S8irn(%d-gi0*WxxCSk*A?3lGh=gcYN?FGl z7D=Js!i~0=u3rox^eO3i@$0=n{K1lPNU zwmfjRVmLOCRfe=seV&P*1Iq=^i`502keY8Uy-WNPwVNNtJFx?IwAyRPZo2Wo1+S(xF37LJZ~%i)kpFQ3Fw=mXfd@>%+)RpYQLnr}B~~zoof(JVm^^&f zxKV^+3D3$A1G;qh4gPVjhrC8e(VYUHv#dy^)(RoUFM?o%W-EHxufuWf(l*@-l+7vt z=l`qmR56K~F|v<^Pd*p~1_y^P0P^aPC##d8+HqX4IR1gu+7w#~TBFphJxF)T$2WEa zxa?H&6=Qe7d(#tha?_1uQys2KtHQ{)Qco)qwGjrdNL7thd^G5i8Os)CHqc>iOidS} z%nFEDdm=GXBw=yXe1W-ShHHFb?Cc70+$W~z_+}nAoHFYI1MV1wZegw*0y^tC*s%3h zhD3tN8b=Gv&rj}!SUM6|ajSPp*58KR7MPpI{oAJCtY~JECm)*m_x>AZEu>DFgUcby z1Qaw8lU4jZpQ_$;*7RME+gq1KySGG#Wql>aL~k9tLrSO()LWn*q&YxHEuzmwd1?aAtI zBJ>P=&$=l1efe1CDU;`Fd+_;&wI07?V0aAIgc(!{a z0Jg6Y=inXc3^n!U0Atk`iCFIQooHqcWhO(qrieUOW8X(x?(RD}iYDLMjSwffH2~tB z)oDgNBLB^AJBM1M^c5HdRx6fBfka`(LD-qrlh5jqH~);#nw|iyp)()xVYak3;Ybik z0j`(+69aK*B>)e_p%=wu8XC&9e{AO4c~O1U`5X9}?0mrd*m$_EUek{R?DNSh(=br# z#Q61gBzEpmy`$pA*6!87 zSDD+=@fTY7<4A?GLqpA?Pb2z$pbCc4B4zL{BeZ?F-8`s$?>*lXXtn*NC61>|*w7J* z$?!iB{6R-0=KFmyp1nnEmLsA-H0a6l+1uaH^g%c(p{iT&YFrbQ$&PRb8Up#X3@Zsk zD^^&LK~111%cqlP%!_gFNa^dTYT?rhkGl}5=fL{a`UViaXWI$k-UcHJwmaH1s=S$4 z%4)PdWJX;hh5UoK?6aWoyLxX&NhNRqKam7tcOkLh{%j3K^4Mgx1@i|Pi&}<^5>hs5 zm8?uOS>%)NzT(%PjVPGa?X%`N2TQCKbeH2l;cTnHiHppPSJ<7y-yEIiC!P*ikl&!B z%+?>VttCOQM@ShFguHVjxX^?mHX^hSaO_;pnyh^v9EumqSZTi+#f&_Vaija0Q-e*| z7ulQj6Fs*bbmsWp{`auM04gGwsYYdNNZcg|ph0OgD>7O}Asn7^Z=eI>`$2*v78;sj-}oMoEj&@)9+ycEOo92xSyY344^ z11Hb8^kdOvbf^GNAK++bYioknrpdN>+u8R?JxG=!2Kd9r=YWCOJYXYuM0cOq^FhEd zBg2puKy__7VT3-r*dG4c62Wgxi52EMCQ`bKgf*#*ou(D4-ZN$+mg&7$u!! z-^+Z%;-3IDwqZ|K=ah85OLwkO zKxNBh+4QHh)u9D?MFtpbl)us}9+V!D%w9jfAMYEb>%$A;u)rrI zuBudh;5PN}_6J_}l55P3l_)&RMlH{m!)ai-i$g)&*M`eN$XQMw{v^r@-125^RRCF0 z^2>|DxhQw(mtNEI2Kj(;KblC7x=JlK$@78`O~>V!`|1Lm-^JR$-5pUANAnb(5}B}JGjBsliK4& zk6y(;$e&h)lh2)L=bvZKbvh@>vLlreBdH8No2>$#%_Wp1U0N7Ank!6$dFSi#xzh|( zRi{Uw%-4W!{IXZ)fWx@XX6;&(m_F%c6~X8hx=BN1&q}*( zoaNjWabE{oUPb!Bt$eyd#$5j9rItB-h*5JiNi(v^e|XKAj*8(k<5-2$&ZBR5fF|JA z9&m4fbzNQnAU}r8ab>fFV%J0z5awe#UZ|bz?Ur)U9bCIKWEzi2%A+5CLqh?}K4JHi z4vtM;+uPsVz{Lfr;78W78gC;z*yTch~4YkLr&m-7%-xc ztw6Mh2d>_iO*$Rd8(-Cr1_V8EO1f*^@wRoSozS) zy1UoC@pruAaC8Z_7~_w4Q6n*&B0AjOmMWa;sIav&gu z|J5&|{=a@vR!~k-OjKEgPFCzcJ>#A1uL&7xTDn;{XBdeM}V=l3B8fE1--DHjSaxoSjNKEM9|U9#m2<3>n{Iuo`r3UZp;>GkT2YBNAh|b z^jTq-hJp(ebZh#Lk8hVBP%qXwv-@vbvoREX$TqRGTgEi$%_F9tZES@z8Bx}$#5eeG zk^UsLBH{bc2VBW)*EdS({yw=?qmevwi?BL6*=12k9zM5gJv1>y#ML4!)iiPzVaH9% zgSImetD@dam~e>{LvVh!phhzpW+iFvWpGT#CVE5TQ40n%F|p(sP5mXxna+Ev7PDwA zamaV4m*^~*xV+&p;W749xhb_X=$|LD;FHuB&JL5?*Y2-oIT(wYY2;73<^#46S~Gx| z^cez%V7x$81}UWqS13Gz80379Rj;6~WdiXWOSsdmzY39L;Hg3MH43o*y8ibNBBH`(av4|u;YPq%{R;IuYow<+GEsf@R?=@tT@!}?#>zIIn0CoyV!hq3mw zHj>OOjfJM3F{RG#6ujzo?y32m^tgSXf@v=J$ELdJ+=5j|=F-~hP$G&}tDZsZE?5rX ztGj`!S>)CFmdkccxM9eGIcGnS2AfK#gXwj%esuIBNJQP1WV~b~+D7PJTmWGTSDrR` zEAu4B8l>NPuhsk5a`rReSya2nfV1EK01+G!x8aBdTs3Io$u5!6n6KX%uv@DxAp3F@{4UYg4SWJtQ-W~0MDb|j-$lwVn znAm*Pl!?Ps&3wO=R115RWKb*JKoexo*)uhhHBncEDMSVa_PyA>k{Zm2(wMQ(5NM3# z)jkza|GoWEQo4^s*wE(gHz?Xsg4`}HUAcs42cM1-qq_=+=!Gk^y710j=66(cSWqUe zklbm8+zB_syQv5A2rj!Vbw8;|$@C!vfNmNV!yJIWDQ>{+2x zKjuFX`~~HKG~^6h5FntRpnnHt=D&rq0>IJ9#F0eM)Y-)GpRjiN7gkA8wvnG#K=q{q z9dBn8_~wm4J<3J_vl|9H{7q6u2A!cW{bp#r*-f{gOV^e=8S{nc1DxMHFwuM$;aVI^ zz6A*}m8N-&x8;aunp1w7_vtB*pa+OYBw=TMc6QK=mbA-|Cf* zvyh8D4LRJImooUaSb7t*fVfih<97Gf@VE0|z>NcBwBQze);Rh!k3K_sfunToZY;f2 z^HmC4KjHRVg+eKYj;PRN^|E0>Gj_zagfRbrki68I^#~6-HaHg3BUW%+clM1xQEdPYt_g<2K+z!$>*$9nQ>; zf9Bei{?zY^-e{q_*|W#2rJG`2fy@{%6u0i_VEWTq$*(ZN37|8lFFFt)nCG({r!q#9 z5VK_kkSJ3?zOH)OezMT{!YkCuSSn!K#-Rhl$uUM(bq*jY? zi1xbMVthJ`E>d>(f3)~fozjg^@eheMF6<)I`oeJYx4*+M&%c9VArn(OM-wp%M<-`x z7sLP1&3^%Nld9Dhm@$3f2}87!quhI@nwd@3~fZl_3LYW-B?Ia>ui`ELg z&Qfe!7m6ze=mZ`Ia9$z|ARSw|IdMpooY4YiPN8K z4B(ts3p%2i(Td=tgEHX z0UQ_>URBtG+-?0E;E7Ld^dyZ;jjw0}XZ(}-QzC6+NN=40oDb2^v!L1g9xRvE#@IBR zO!b-2N7wVfLV;mhEaXQ9XAU+>=XVA6f&T4Z-@AX!leJ8obP^P^wP0aICND?~w&NykJ#54x3_@r7IDMdRNy4Hh;h*!u(Ol(#0bJdwEo$5437-UBjQ+j=Ic>Q2z` zJNDf0yO6@mr6y1#n3)s(W|$iE_i8r@Gd@!DWDqZ7J&~gAm1#~maIGJ1sls^gxL9LLG_NhU!pTGty!TbhzQnu)I*S^54U6Yu%ZeCg`R>Q zhBv$n5j0v%O_j{QYWG!R9W?5_b&67KB$t}&e2LdMvd(PxN6Ir!H4>PNlerpBL>Zvyy!yw z-SOo8caEpDt(}|gKPBd$qND5#a5nju^O>V&;f890?yEOfkSG^HQVmEbM3Ugzu+UtH zC(INPDdraBN?P%kE;*Ae%Wto&sgw(crfZ#Qy(<4nk;S|hD3j{IQRI6Yq|f^basLY; z-HB&Je%Gg}Jt@={_C{L$!RM;$$|iD6vu#3w?v?*;&()uB|I-XqEKqZPS!reW9JkLewLb!70T7n`i!gNtb1%vN- zySZj{8-1>6E%H&=V}LM#xmt`J3XQoaD|@XygXjdZ1+P77-=;=eYpoEQ01B@L*a(uW zrZeZz?HJsw_4g0vhUgkg@VF8<-X$B8pOqCuWAl28uB|@r`19DTUQQsb^pfqB6QtiT z*`_UZ`fT}vtUY#%sq2{rchyfu*pCg;uec2$-$N_xgjZcoumE5vSI{+s@iLWoz^Mf; zuI8kDP{!XY6OP~q5}%1&L}CtfH^N<3o4L@J@zg1-mt{9L`s^z$Vgb|mr{@WiwAqKg zp#t-lhrU>F8o0s1q_9y`gQNf~Vb!F%70f}$>i7o4ho$`uciNf=xgJ>&!gSt0g;M>*x4-`U)ysFW&Vs^Vk6m%?iuWU+o&m(2Jm26Y(3%TL; zA7T)BP{WS!&xmxNw%J=$MPfn(9*^*TV;$JwRy8Zl*yUZi8jWYF>==j~&S|Xinsb%c z2?B+kpet*muEW7@AzjBA^wAJBY8i|#C{WtO_or&Nj2{=6JTTX05}|H>N2B|Wf!*3_ z7hW*j6p3TvpghEc6-wufFiY!%-GvOx*bZrhZu+7?iSrZL5q9}igiF^*R3%DE4aCHZ zqu>xS8LkW+Auv%z-<1Xs92u23R$nk@Pk}MU5!gT|c7vGlEA%G^2th&Q*zfg%-D^=f z&J_}jskj|Q;73NP4<4k*Y%pXPU2Thoqr+5uH1yEYM|VtBPW6lXaetokD0u z9qVek6Q&wk)tFbQ8(^HGf3Wp16gKmr>G;#G(HRBx?F`9AIRboK+;OfHaLJ(P>IP0w zyTbTkx_THEOs%Q&aPrxbZrJlio+hCC_HK<4%f3ZoSAyG7Dn`=X=&h@m*|UYO-4Hq0 z-Bq&+Ie!S##4A6OGoC~>ZW`Y5J)*ouaFl_e9GA*VSL!O_@xGiBw!AF}1{tB)z(w%c zS1Hmrb9OC8>0a_$BzeiN?rkPLc9%&;1CZW*4}CDDNr2gcl_3z+WC15&H1Zc2{o~i) z)LLW=WQ{?ricmC`G1GfJ0Yp4Dy~Ba;j6ZV4r{8xRs`13{dD!xXmr^Aga|C=iSmor% z8hi|pTXH)5Yf&v~exp3o+sY4B^^b*eYkkCYl*T{*=-0HniSA_1F53eCb{x~1k3*`W zr~};p1A`k{1DV9=UPnLDgz{aJH=-LQo<5%+Em!DNN252xwIf*wF_zS^!(XSm(9eoj z=*dXG&n0>)_)N5oc6v!>-bd(2ragD8O=M|wGW z!xJQS<)u70m&6OmrF0WSsr@I%T*c#Qo#Ha4d3COcX+9}hM5!7JIGF>7<~C(Ear^Sn zm^ZFkV6~Ula6+8S?oOROOA6$C&q&dp`>oR-2Ym3(HT@O7Sd5c~+kjrmM)YmgPH*tL zX+znN>`tv;5eOfX?h{AuX^LK~V#gPCu=)Tigtq9&?7Xh$qN|%A$?V*v=&-2F$zTUv z`C#WyIrChS5|Kgm_GeudCFf;)!WH7FI60j^0o#65o6`w*S7R@)88n$1nrgU(oU0M9 zx+EuMkC>(4j1;m6NoGqEkpJYJ?vc|B zOlwT3t&UgL!pX_P*6g36`ZXQ; z9~Cv}ANFnJGp(;ZhS(@FT;3e)0)Kp;h^x;$*xZn*k0U6-&FwI=uOGaODdrsp-!K$Ac32^c{+FhI-HkYd5v=`PGsg%6I`4d9Jy)uW0y%) zm&j^9WBAp*P8#kGJUhB!L?a%h$hJgQrx!6KCB_TRo%9{t0J7KW8!o1B!NC)VGLM5! zpZy5Jc{`r{1e(jd%jsG7k%I+m#CGS*BPA65ZVW~fLYw0dA-H_}O zrkGFL&P1PG9p2(%QiEWm6x;U-U&I#;Em$nx-_I^wtgw3xUPVVu zqSuKnx&dIT-XT+T10p;yjo1Y)z(x1fb8Dzfn8e yu?e%!_ptzGB|8GrCfu%p?(_ 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
NnX)EH+Nua)3Y(c z&{(nX_ht=QbJ%DzAya}!&uNu!4V0xI)QE$SY__m)SAKcN0P(&JcoK*Lxr@P zY&P=}&B3*UWNlc|&$Oh{BEqwK2+N2U$4WB7Fd|aIal`FGANUa9E-O)!gV`((ZGCc$ zBJA|FFrlg~9OBp#f7aHodCe{6= zay$6vN~zj1ddMZ9gQ4p32(7wD?(dE>KA2;SOzXRmPBiBc6g`eOsy+pVcHu=;Yd8@{ zSGgXf@%sKKQz~;!J;|2fC@emm#^_rnO0esEn^QxXgJYd`#FPWOUU5b;9eMAF zZhfiZb|gk8aJIw*YLp4!*(=3l8Cp{(%p?ho22*vN9+5NLV0TTazNY$B5L6UKUrd$n zjbX%#m7&F#U?QNOBXkiiWB*_tk+H?N3`vg;1F-I+83{M2!8<^nydGr5XX}tC!10&e z7D36bLaB56WrjL&HiiMVtpff|K%|*{t*ltt^5ood{FOG0<>k&1h95qPio)2`eL${YAGIx(b4VN*~nKn6E~SIQUuRH zQ+5zP6jfnP$S0iJ@~t!Ai3o`X7biohli;E zT#yXyl{bojG@-TGZzpdVDXhbmF%F9+-^YSIv|MT1l3j zrxOFq>gd2%U}?6}8mIj?M zc077Zc9fq(-)4+gXv?Az26IO6eV`RAJz8e3)SC7~>%rlzDwySVx*q$ygTR5kW2ds- z!HBgcq0KON9*8Ff$X0wOq$`T7ml(@TF)VeoF}x1OttjuVHn3~sHrMB++}f7f9H%@f z=|kP_?#+fve@{0MlbkC9tyvQ_R?lRdRJ@$qcB(8*jyMyeME5ns6ypVI1Xm*Zr{DuS zZ!1)rQfa89c~;l~VkCiHI|PCBd`S*2RLNQM8!g9L6?n`^evQNEwfO@&JJRme+uopQX0%Jo zgd5G&#&{nX{o?TQwQvF1<^Cg3?2co;_06=~Hcb6~4XWpNFL!WU{+CK;>gH%|BLOh7@!hsa(>pNDAmpcuVO-?;Bic17R}^|6@8DahH)G z!EmhsfunLL|3b=M0MeK2vqZ|OqUqS8npxwge$w-4pFVXFq$_EKrZY?BuP@Az@(k`L z`ViQBSk`y+YwRT;&W| z2e3UfkCo^uTA4}Qmmtqs+nk#gNr2W4 zTH%hhErhB)pkXR{B!q5P3-OM+M;qu~f>}IjtF%>w{~K-0*jPVLl?Chz&zIdxp}bjx zStp&Iufr58FTQ36AHU)0+CmvaOpKF;W@sMTFpJ`j;3d)J_$tNQI^c<^1o<49Z(~K> z;EZTBaVT%14(bFw2ob@?JLQ2@(1pCdg3S%E4*dJ}dA*v}_a4_P(a`cHnBFJxNobAv zf&Zl-Yt*lhn-wjZsq<9v-IsXxAxMZ58C@e0!rzhJ+D@9^3~?~yllY^s$?&oNwyH!#~6x4gUrfxplCvK#!f z$viuszW>MFEcFL?>ux*((!L$;R?xc*myjRIjgnQX79@UPD$6Dz0jutM@7h_pq z0Zr)#O<^y_K6jfY^X%A-ip>P%3saX{!v;fxT-*0C_j4=UMH+Xth(XVkVGiiKE#f)q z%Jp=JT)uy{&}Iq2E*xr4YsJ5>w^=#-mRZ4vPXpI6q~1aFwi+lQcimO45V-JXP;>(Q zo={U`{=_JF`EQj87Wf}{Qy35s8r1*9Mxg({CvOt}?Vh9d&(}iI-quvs-rm~P;eRA@ zG5?1HO}puruc@S{YNAF3vmUc2B4!k*yi))<5BQmvd3tr}cIs#9)*AX>t`=~{f#Uz0 z0&Nk!7sSZwJe}=)-R^$0{yeS!V`Dh7w{w5rZ9ir!Z7Cd7dwZcK;BT#V0bzTt>;@Cl z#|#A!-IL6CZ@eHH!CG>OO8!%G8&8t4)Ro@}USB*k>oEUo0LsljsJ-%5Mo^MJF2I8- z#v7a5VdJ-Cd%(a+y6QwTmi+?f8Nxtm{g-+WGL>t;s#epv7ug>inqimZCVm!uT5Pf6 ziEgQt7^%xJf#!aPWbuC_3Nxfb&CFbQy!(8ANpkWLI4oSnH?Q3f?0k1t$3d+lkQs{~(>06l&v|MpcFsyAv zin6N!-;pggosR*vV=DO(#+}4ps|5$`udE%Kdmp?G7B#y%H`R|i8skKOd9Xzx8xgR$>Zo2R2Ytktq^w#ul4uicxW#{ zFjG_RNlBroV_n;a7U(KIpcp*{M~e~@>Q#Av90Jc5v%0c>egEdY4v3%|K1XvB{O_8G zkTWLC>OZKf;XguMH2-Pw{BKbFzaY;4v2seZV0>^7Q~d4O=AwaPhP3h|!hw5aqOtT@ z!SNz}$of**Bl3TK209@F=Tn1+mgZa8yh(Png%Zd6Mt}^NSjy)etQrF zme*llAW=N_8R*O~d2!apJnF%(JcN??=`$qs3Y+~xs>L9x`0^NIn!8mMRFA_tg`etw z3k{9JAjnl@ygIiJcNHTy02GMAvBVqEss&t2<2mnw!; zU`J)0>lWiqVqo|ex7!+@0i>B~BSU1A_0w#Ee+2pJx0BFiZ7RDHEvE*ptc9md(B{&+ zKE>TM)+Pd>HEmdJao7U@S>nL(qq*A)#eLOuIfAS@j`_sK0UEY6OAJJ-kOrHG zjHx`g!9j*_jRcJ%>CE9K2MVf?BUZKFHY?EpV6ai7sET-tqk=nDFh-(65rhjtlKEY% z@G&cQ<5BKatfdA1FKuB=i>CCC5(|9TMW%K~GbA4}80I5%B}(gck#Wlq@$nO3%@QP_ z8nvPkJFa|znk>V92cA!K1rKtr)skHEJD;k8P|R8RkCq1Rh^&}Evwa4BUJz2f!2=MH zo4j8Y$YL2313}H~F7@J7mh>u%556Hw0VUOz-Un@ZASCL)y8}4XXS`t1AC*^>PLwIc zUQok5PFS=*#)Z!3JZN&eZ6ZDP^-c@StY*t20JhCnbMxXf=LK#;`4KHEqMZ-Ly9KsS zI2VUJGY&PmdbM+iT)zek)#Qc#_i4uH43 z@T5SZBrhNCiK~~esjsO9!qBpaWK<`>!-`b71Y5ReXQ4AJU~T2Njri1CEp5oKw;Lnm)-Y@Z3sEY}XIgSy%xo=uek(kAAH5MsV$V3uTUsoTzxp_rF=tx zV07vlJNKtJhCu`b}*#m&5LV4TAE&%KtHViDAdv#c^x`J7bg z&N;#I2GkF@SIGht6p-V}`!F_~lCXjl1BdTLIjD2hH$J^YFN`7f{Q?OHPFEM$65^!u zNwkelo*5+$ZT|oQ%o%;rBX$+?xhvjb)SHgNHE_yP%wYkkvXHS{Bf$OiKJ5d1gI0j< zF6N}Aq=(WDo(J{e-uOecxPD>XZ@|u-tgTR<972`q8;&ZD!cep^@B5CaqFz|oU!iFj zU0;6fQX&~15E53EW&w1s9gQQ~Zk16X%6 zjG`j0yq}4deX2?Tr(03kg>C(!7a|b9qFI?jcE^Y>-VhudI@&LI6Qa}WQ>4H_!UVyF z((cm&!3gmq@;BD#5P~0;_2qgZhtJS|>WdtjY=q zLnHH~Fm!cxw|Z?Vw8*~?I$g#9j&uvgm7vPr#&iZgPP~v~BI4jOv;*OQ?jYJtzO<^y z7-#C={r7CO810!^s(MT!@@Vz_SVU)7VBi(e1%1rvS!?PTa}Uv`J!EP3s6Y!xUgM^8 z4f!fq<3Wer_#;u!5ECZ|^c1{|q_lh3m^9|nsMR1#Qm|?4Yp5~|er2?W^7~cl;_r4WSme_o68J9p03~Hc%X#VcX!xAu%1`R!dfGJCp zV*&m47>s^%Ib0~-2f$6oSgn3jg8m%UA;ArcdcRyM5;}|r;)?a^D*lel5C`V5G=c~k zy*w_&BfySOxE!(~PI$*dwG><+-%KT5p?whOUMA*k<9*gi#T{h3DAxzAPxN&Xws8o9Cp*`PA5>d9*Z-ynV# z9yY*1WR^D8|C%I@vo+d8r^pjJ$>eo|j>XiLWvTWLl(^;JHCsoPgem6PvegHb-OTf| zvTgsHSa;BkbG=(NgPO|CZu9gUCGr$8*EoH2_Z#^BnxF0yM~t`|9ws_xZ8X8iZYqh! zAh;HXJ)3P&)Q0(&F>!LN0g#bdbis-cQxyGn9Qgh`q+~49Fqd2epikEUw9caM%V6WgP)532RMRW}8gNS%V%Hx7apSz}tn@bQy!<=lbhmAH=FsMD?leawbnP5BWM0 z5{)@EEIYMu5;u)!+HQWhQ;D3_Cm_NADNeb-f56}<{41aYq8p4=93d=-=q0Yx#knGYfXVt z+kMxlus}t2T5FEyCN~!}90O_X@@PQpuy;kuGz@bWft%diBTx?d)_xWd_-(!LmVrh**oKg!1CNF&LX4{*j|) zIvjCR0I2UUuuEXh<9}oT_zT#jOrJAHNLFT~Ilh9hGJPI1<5`C-WA{tUYlyMeoy!+U zhA#=p!u1R7DNg9u4|QfED-2TuKI}>p#2P9--z;Bbf4Op*;Q9LCbO&aL2i<0O$ByoI z!9;Ght733FC>Pz>$_mw(F`zU?`m@>gE`9_p*=7o=7av`-&ifU(^)UU`Kg3Kw`h9-1 z6`e6+im=|m2v`pN(2dE%%n8YyQz;#3Q-|x`91z?gj68cMrHl}C25|6(_dIGk*8cA3 zRHB|Nwv{@sP4W+YZM)VKI>RlB`n=Oj~Rzx~M+Khz$N$45rLn6k1nvvD^&HtsMA4`s=MmuOJID@$s8Ph4E zAmSV^+s-z8cfv~Yd(40Sh4JG#F~aB>WFoX7ykaOr3JaJ&Lb49=B8Vk-SQT9%7TYhv z?-Pprt{|=Y5ZQ1?od|A<_IJU93|l4oAfBm?3-wk{O<8ea+`}u%(kub(LFo2zFtd?4 zwpN|2mBNywv+d^y_8#<$r>*5+$wRTCygFLcrwT(qc^n&@9r+}Kd_u@Ithz(6Qb4}A zWo_HdBj#V$VE#l6pD0a=NfB0l^6W^g`vm^sta>Tly?$E&{F?TTX~DsKF~poFfmN%2 z4x`Dc{u{Lkqz&y!33;X}weD}&;7p>xiI&ZUb1H9iD25a(gI|`|;G^NwJPv=1S5e)j z;U;`?n}jnY6rA{V^ zxTd{bK)Gi^odL3l989DQlN+Zs39Xe&otGeY(b5>rlIqfc7Ap4}EC?j<{M=hlH{1+d zw|c}}yx88_xQr`{98Z!d^FNH77=u(p-L{W6RvIn40f-BldeF-YD>p6#)(Qzf)lfZj z?3wAMtPPp>vMehkT`3gToPd%|D8~4`5WK{`#+}{L{jRUMt zrFz+O$C7y8$M&E4@+p+oV5c%uYzbqd2Y%SSgYy#xh4G3hQv>V*BnuKQhBa#=oZB~w{azUB+q%bRe_R^ z>fHBilnRTUfaJ201czL8^~Ix#+qOHSO)A|xWLqOxB$dT2W~)e-r9;bm=;p;RjYahB z*1hegN(VKK+ztr~h1}YP@6cfj{e#|sS`;3tJhIJK=tVJ-*h-5y9n*&cYCSdg#EHE# zSIx=r#qOaLJoVVf6v;(okg6?*L_55atl^W(gm^yjR?$GplNP>BZsBYEf_>wM0Lc;T zhf&gpzOWNxS>m+mN92N0{;4uw`P+9^*|-1~$uXpggj4- z^SFc4`uzj2OwdEVT@}Q`(^EcQ_5(ZtXTql*yGzdS&vrS_w>~~ra|Nb5abwf}Y!uq6R5f&6g2ge~2p(%c< z@O)cz%%rr4*cRJ5f`n@lvHNk@lE1a*96Kw6lJ~B-XfJW%?&-y?;E&?1AacU@`N`!O z6}V>8^%RZ7SQnZ-z$(jsX`amu*5Fj8g!3RTRwK^`2_QHe;_2y_n|6gSaGyPmI#kA0sYV<_qOZc#-2BO%hX)f$s-Z3xlI!ub z^;3ru11DA`4heAu%}HIXo&ctujzE2!6DIGE{?Zs>2}J+p&C$rc7gJC35gxhflorvsb%sGOxpuWhF)dL_&7&Z99=5M0b~Qa;Mo!j&Ti_kXW!86N%n= zSC@6Lw>UQ__F&+&Rzv?gscwAz8IP!n63>SP)^62(HK98nGjLY2*e^OwOq`3O|C92? z;TVhZ2SK%9AGW4ZavTB9?)mUbOoF`V7S=XM;#3EUpR+^oHtdV!GK^nXzCu>tpR|89 zdD{fnvCaN^^LL%amZ^}-E+214g&^56rpdc@yv0b<3}Ys?)f|fXN4oHf$six)-@<;W&&_kj z-B}M5U*1sb4)77aR=@%I?|Wkn-QJVuA96an25;~!gq(g1@O-5VGo7y&E_srxL6ZfS z*R%$gR}dyONgju*D&?geiSj7SZ@ftyA|}(*Y4KbvU!YLsi1EDQQCnb+-cM=K1io78o!v*);o<XwjaQH%)uIP&Zm?)Nfbfn;jIr z)d#!$gOe3QHp}2NBak@yYv3m(CPKkwI|{;d=gi552u?xj9ObCU^DJFQp4t4e1tPzM zvsRIGZ6VF+{6PvqsplMZWhz10YwS={?`~O0Ec$`-!klNUYtzWA^f9m7tkEzCy<_nS z=&<(awFeZvt51>@o_~>PLs05CY)$;}Oo$VDO)?l-{CS1Co=nxjqben*O1BR>#9`0^ zkwk^k-wcLCLGh|XLjdWv0_Hg54B&OzCE^3NCP}~OajK-LuRW53CkV~Su0U>zN%yQP zH8UH#W5P3-!ToO-2k&)}nFe`t+mdqCxxAHgcifup^gKpMObbox9LFK;LP3}0dP-UW z?Zo*^nrQ6*$FtZ(>kLCc2LY*|{!dUn$^RW~m9leoF|@Jy|M5p-G~j%+P0_#orRKf8 zvuu5<*XO!B?1E}-*SY~MOa$6c%2cM+xa8}_8x*aVn~57v&W(0mqN1W`5a7*VN{SUH zXz98DDyCnX2EPl-`Lesf`=AQT%YSDb`$%;(jUTrNen$NPJrlpPDP}prI>Ml!r6bCT;mjsg@X^#&<}CGf0JtR{Ecwd&)2zuhr#nqdgHj+g2n}GK9CHuwO zk>oZxy{vcOL)$8-}L^iVfJHAGfwN$prHjYV0ju}8%jWquw>}_W6j~m<}Jf!G?~r5&Rx)!9JNX!ts#SGe2HzobV5); zpj@&`cNcO&q+%*<%D7za|?m5qlmFK$=MJ_iv{aRs+BGVrs)98BlN^nMr{V_fcl_;jkzRju+c-y?gqBC_@J0dFLq-D9@VN&-`R9U;nv$Hg?>$oe4N&Ht$V_(JR3TG^! zzJsbQbi zFE6-{#9{G{+Z}ww!ycl*7rRdmU#_&|DqPfX3CR1I{Kk;bHwF6jh0opI`UV2W{*|nn zf_Y@%wW6APb&9RrbEN=PQRBEpM(N1w`81s=(xQj6 z-eO0k9=Al|>Ej|Mw&G`%q8e$2xVz1v4DXAi8G};R$y)ww638Y=9y$ZYFDM$}vzusg zUf+~BPX>(SjA|tgaFZr_e0{)+z9i6G#lgt=F_n$d=beAt0Sa0a7>z-?vcjl3e+W}+ z1&9=|vC=$co}-Zh*%3588G?v&U7%N1Qf-wNWJ)(v`iO5KHSkC5&g7CrKu8V}uQGcfcz zmBz#Lbqwqy#Z~UzHgOQ;Q-rPxrRNvl(&u6ts4~0=KkeS;zqURz%!-ERppmd%0v>iRlEf+H$yl{_8TMJzo0 z>n)`On|7=WQdsqhXI?#V{>+~}qt-cQbokEbgwV3QvSP7&hK4R{Z{aGHVS3;+h{|Hz z6$Js}_AJr383c_+6sNR|$qu6dqHXQTc6?(XWPCVZv=)D#6_;D_8P-=zOGEN5&?~8S zl5jQ?NL$c%O)*bOohdNwGIKM#jSAC?BVY={@A#c9GmX0=T(0G}xs`-%f3r=m6-cpK z!%waekyAvm9C3%>sixdZj+I(wQlbB4wv9xKI*T13DYG^T%}zZYJ|0$Oj^YtY+d$V$ zAVudSc-)FMl|54n=N{BnZTM|!>=bhaja?o7s+v1*U$!v!qQ%`T-6fBvmdPbVmro&d zk07TOp*KuxRUSTLRrBj{mjsnF8`d}rMViY8j`jo~Hp$fkv9F_g(jUo#Arp;Xw0M$~ zRIN!B22~$kx;QYmOkos@%|5k)!QypDMVe}1M9tZfkpXKGOxvKXB!=lo`p?|R1l=tA zp(1}c6T3Fwj_CPJwVsYtgeRKg?9?}%oRq0F+r+kdB=bFUdVDRPa;E~~>2$w}>O>v=?|e>#(-Lyx?nbg=ckJ#5U6;RT zNvHhXk$P}m9wSvFyU3}=7!y?Y z=fg$PbV8d7g25&-jOcs{%}wTDKm>!Vk);&rr;O1nvO0VrU&Q?TtYVU=ir`te8SLlS zKSNmV=+vF|ATGg`4$N1uS|n??f}C_4Sz!f|4Ly8#yTW-FBfvS48Tef|-46C(wEO_%pPhUC5$-~Y?!0vFZ^Gu`x=m7X99_?C-`|h zfmMM&Y@zdfitA@KPw4Mc(YHcY1)3*1xvW9V-r4n-9ZuBpFcf{yz+SR{ zo$ZSU_|fgwF~aakGr(9Be`~A|3)B=9`$M-TWKipq-NqRDRQc}ABo*s_5kV%doIX7LRLRau_gd@Rd_aLFXGSU+U?uAqh z8qusWWcvgQ&wu{|sRXmv?sl=xc<$6AR$+cl& zFNh5q1~kffG{3lDUdvEZu5c(aAG~+64FxdlfwY^*;JSS|m~CJusvi-!$XR`6@XtY2 znDHSz7}_Bx7zGq-^5{stTRy|I@N=>*y$zz>m^}^{d&~h;0kYiq8<^Wq7Dz0w31ShO^~LUfW6rfitR0(=3;Uue`Y%y@ex#eKPOW zO~V?)M#AeHB2kovn1v=n^D?2{2jhIQd9t|_Q+c|ZFaWt+r&#yrOu-!4pXAJuxM+Cx z*H&>eZ0v8Y`t}8{TV6smOj=__gFC=eah)mZt9gwz>>W$!>b3O;Rm^Ig*POZP8Rl0f zT~o=Nu1J|lO>}xX&#P58%Yl z83`HRs5#32Qm9mdCrMlV|NKNC+Z~ z9OB8xk5HJ>gBLi+m@(pvpw)1(OaVJKs*$Ou#@Knd#bk+V@y;YXT?)4eP9E5{J%KGtYinNYJUH9PU3A}66c>Xn zZ{Bn0<;8$WCOAL$^NqTjwM?5d=RHgw3!72WRo0c;+houoUA@HWLZM;^U$&sycWrFd zE7ekt9;kb0`lps{>R(}YnXlyGY}5pPd9zBpgXeJTY_jwaJGSJQC#-KJqmh-;ad&F- z-Y)E>!&`Rz!HtCz>%yOJ|v(u7P*I$jqEY3}(Z-orn4 zlI?CYKNl`6I){#2P1h)y(6?i;^z`N3bxTV%wNvQW+eu|x=kbj~s8rhCR*0H=iGkSj zk23lr9kr|p7#qKL=UjgO`@UnvzU)`&fI>1Qs7ubq{@+lK{hH* zvl6eSb9%yngRn^T<;jG1SVa)eA>T^XX=yUS@NCKpk?ovCW1D@!=@kn;l_BrG;hOTC z6K&H{<8K#dI(A+zw-MWxS+~{g$tI7|SfP$EYKxA}LlVO^sT#Oby^grkdZ^^lA}uEF zBSj$weBJG{+Bh@Yffzsw=HyChS(dtLE3i*}Zj@~!_T-Ay7z=B)+*~3|?w`Zd)Co2t zC&4DyB!o&YgSw+fJn6`sn$e)29`kUwAc+1MND7YjV%lO;H2}fNy>hD#=gT ze+-aFNpyKIoXY~Vq-}OWPBe?Rfu^{ps8>Xy%42r@RV#*QV~P83jdlFNgkPN=T|Kt7 zV*M`Rh*30&AWlb$;ae130e@}Tqi3zx2^JQHpM>j$6x`#{mu%tZlwx9Gj@Hc92IuY* zarmT|*d0E~vt6<+r?W^UW0&#U&)8B6+1+;k^2|FWBRP9?C4Rk)HAh&=AS8FS|NQaZ z2j!iZ)nbEyg4ZTp-zHwVlfLC~tXIrv(xrP8PAtR{*c;T24ycA-;auWsya-!kF~CWZ zw_uZ|%urXgUbc@x=L=_g@QJ@m#5beS@6W195Hn7>_}z@Xt{DIEA`A&V82bc^#!q8$ zFh?z_Vn|ozJ;NPd^5uu(9tspo8t%&-U9Ckay-s@DnM*R5rtu|4)~e)`z0P-sy?)kc zs_k&J@0&0!q4~%cKL)2l;N*T&0;mqX5T{Qy60%JtKTQZ-xb%KOcgqwJmb%MOOKk7N zgq})R_6**{8A|6H?fO+2`#QU)p$Ei2&nbj6TpLSIT^D$|`TcSeh+)}VMb}LmvZ{O| ze*1IdCt3+yhdYVxcM)Q_V0bIXLgr6~%JS<<&dxIgfL=Vnx4YHuU@I34JXA|+$_S3~ zy~X#gO_X!cSs^XM{yzDGNM>?v(+sF#<0;AH^YrE8smx<36bUsHbN#y57K8WEu(`qHvQ6cAZPo=J5C(lSmUCZ57Rj6cx!e^rfaI5%w}unz}4 zoX=nt)FVNV%QDJH`o!u9olLD4O5fl)xp+#RloZlaA92o3x4->?rB4`gS$;WO{R;Z3>cG3IgFX2EA?PK^M}@%1%A;?f6}s&CV$cIyEr#q5;yHdNZ9h{| z-=dX+a5elJoDo?Eq&Og!nN6A)5yYpnGEp}?=!C-V)(*~z-+?kY1Q7qs#Rsy%hu_60rdbB+QQNr?S1 z?;xtjUv|*E3}HmuNyB9aFL5H~3Ho0UsmuMZELp1a#CA1g`P{-mT?BchuLEtK}!QZ=3AWakRu~?f9V~3F;TV`5%9Pcs_$gq&CcU}r8gOO zC2&SWPsSG{&o-LIGTBqp6SLQZPvYKp$$7L4WRRZ0BR$Kf0I0SCFkqveCp@f)o8W)! z$%7D1R`&j7W9Q9CGus_)b%+B#J2G;l*FLz#s$hw{BHS~WNLODV#(!u_2Pe&tMsq={ zdm7>_WecWF#D=?eMjLj=-_z`aHMZ=3_-&E8;ibPmM}61i6J3is*=dKf%HC>=xbj4$ zS|Q-hWQ8T5mWde6h@;mS+?k=89?1FU<%qH9B(l&O>k|u_aD|DY*@~(`_pb|B#rJ&g zR0(~(68fpUPz6TdS@4JT5MOPrqDh5_H(eX1$P2SQrkvN8sTxwV>l0)Qq z0pzTuvtEAKRDkKGhhv^jk%|HQ1DdF%5oKq5BS>szk-CIke{%js?~%@$uaN3^Uz6Wf z_iyx{bZ(;9y4X&>LPV=L=d+A}7I4GkK0c1Xts{rrW1Q7apHf-))`BgC^0^F(>At1* za@e7{lq%yAkn*NH8Q1{@{lKhRg*^TfGvv!Sn*ed*x@6>M%aaqySxR|oNadYt1mpUZ z6H(rupHYf&Z z29$5g#|0MX#aR6TZ$@eGxxABRKakDYtD%5BmKp;HbG_ZbT+=81E&=XRk6m_3t9PvD zr5Cqy(v?gHcYvYvXkNH@S#Po~q(_7MOuCAB8G$a9BC##gw^5mW16cML=T=ERL7wsk zzNEayTG?mtB=x*wc@ifBCJ|irFVMOvH)AFRW8WE~U()QT=HBCe@s$dA9O!@`zAAT) zaOZ7l6vyR+Nk_OOF!ZlZmjoImKh)dxFbbR~z(cMhfeX1l7S_`;h|v3gI}n9$sSQ>+3@AFAy9=B_y$)q;Wdl|C-X|VV3w8 z2S#>|5dGA8^9%Bu&fhmVRrTX>Z7{~3V&0UpJNEl0=N32euvDGCJ>#6dUSi&PxFW*s zS`}TB>?}H(T2lxBJ!V#2taV;q%zd6fOr=SGHpoSG*4PDaiG0pdb5`jelVipkEk%FV zThLc@Hc_AL1#D&T4D=w@UezYNJ%0=f3iVRuVL5H?eeZM}4W*bomebEU@e2d`M<~uW zf#Bugwf`VezG|^Qbt6R_=U0}|=k;mIIakz99*>FrsQR{0aQRP6ko?5<7bkDN8evZ& zB@_KqQG?ErKL=1*ZM9_5?Pq%lcS4uLSzN(Mr5=t6xHLS~Ym`UgM@D&VNu8e?_=nSFtF$u@hpPSmI4Vo_t&v?>$~K4y(O~Rb*(MFy_igM7 z*~yYUyR6yQgzWnWMUgDov!!g=lInM+=lOmOk4L`O?{i&qxy&D*_qorRbDwj6?)!ef z#JLd7F6Z2I$S0iYI={rZNk*<{HtIl^mx=h>Cim*04K4+Z4IJtd*-)%6XV2(MCscPiw_a+y*?BKbTS@BZ3AUao^%Zi#PhoY9Vib4N>SE%4>=Jco0v zH_Miey{E;FkdlZSq)e<{`+S3W=*ttvD#hB8w=|2aV*D=yOV}(&p%0LbEWH$&@$X3x~CiF-?ejQ*N+-M zc8zT@3iwkdRT2t(XS`d7`tJQAjRmKAhiw{WOqpuvFp`i@Q@!KMhwKgsA}%@sw8Xo5Y=F zhRJZg)O4uqNWj?V&&vth*H#je6T}}p_<>!Dr#89q@uSjWv~JuW(>FqoJ5^ho0%K?E z9?x_Q;kmcsQ@5=}z@tdljMSt9-Z3xn$k)kEjK|qXS>EfuDmu(Z8|(W?gY6-l z@R_#M8=vxKMAoi&PwnaIYw2COJM@atcgfr=zK1bvjW?9B`-+Voe$Q+H$j!1$Tjn+* z&LY<%)L@;zhnJlB^Og6I&BOR-m?{IW;tyYC%FZ!&Z>kGjHJ6cqM-F z&19n+e1=9AH1VrVeHrIzqlC`w9=*zfmrerF?JMzO&|Mmv;!4DKc(sp+jy^Dx?(8>1 zH&yS_4yL7m&GWX~mdfgH*AB4{CKo;+egw=PrvkTaoBU+P-4u?E|&!c z)DKc;>$$B6u*Zr1SjUh2)FeuWLWHl5TH(UHWkf zLs>7px!c5n;rbe^lO@qlYLzlDVp(z?6rPZel=YB)Uv&n!2{+Mb$-vQl=xKw( zve&>xYx+jW_NJh!FV||r?;hdP*jOXYcLCp>DOtJ?2S^)DkM{{Eb zS$!L$e_o0(^}n3tA1R3-$SNvgBq;DOEo}fNc|tB%%#g4RA3{|euq)p+xd3I8^4E&m zFrD%}nvG^HUAIKe9_{tXB;tl|G<%>yk6R;8L2)KUJw4yHJXUOPM>(-+jxq4R;z8H#>rnJy*)8N+$wA$^F zN+H*3t)eFEgxLw+Nw3};4WV$qj&_D`%ADV2%r zJCPCo%{=z7;`F98(us5JnT(G@sKTZ^;2FVitXyLe-S5(hV&Ium+1pIUB(CZ#h|g)u zSLJJ<@HgrDiA-}V_6B^x1>c9B6%~847JkQ!^KLZ2skm;q*edo;UA)~?SghG8;QbHh z_6M;ouo_1rq9=x$<`Y@EA{C%6-pEV}B(1#sDoe_e1s3^Y>n#1Sw;N|}8D|s|VPd+g z-_$QhCz`vLxxrVMx3ape1xu3*wjx=yKSlM~nFgkNWb4?DDr*!?U)L_VeffF<+!j|b zZ$Wn2$TDv3C3V@BHpSgv3JUif8%hk%OsGZ=OxH@8&4`bbf$`aAMchl^qN>Eyu3JH} z9-S!x8-s4fE=lad%Pkp8hAs~u?|uRnL48O|;*DEU! zuS0{cpk%1E0nc__2%;apFsTm0bKtd&A0~S3Cj^?72-*Owk3V!ZG*PswDfS~}2<8le z5+W^`Y(&R)yVF*tU_s!XMcJS`;(Tr`J0%>p=Z&InR%D3@KEzzI+-2)HK zuoNZ&o=wUC&+*?ofPb0a(E6(<2Amd6%uSu_^-<1?hsxs~0K5^f(LsGqgEF^+0_H=uNk9S0bb!|O8d?m5gQjUKevPaO+*VfSn^2892K~%crWM8+6 z25@V?Y@J<9w%@NXh-2!}SK_(X)O4AM1-WTg>sj1{lj5@=q&dxE^9xng1_z9w9DK>| z6Iybcd0e zyi;Ew!KBRIfGPGytQ6}z}MeXCfLY0?9%RiyagSp_D1?N&c{ zyo>VbJ4Gy`@Fv+5cKgUgs~na$>BV{*em7PU3%lloy_aEovR+J7TfQKh8BJXyL6|P8un-Jnq(ghd!_HEOh$zlv2$~y3krgeH;9zC}V3f`uDtW(%mT#944DQa~^8ZI+zAUu4U(j0YcDfKR$bK#gvn_{JZ>|gZ5+)u?T$w7Q%F^;!Wk?G z(le7r!ufT*cxS}PR6hIVtXa)i`d$-_1KkyBU>qmgz-=T};uxx&sKgv48akIWQ89F{ z0XiY?WM^~;|T8zBOr zs#zuOONzH?svv*jokd5SK8wG>+yMC)LYL|vLqm^PMHcT=`}V$=nIRHe2?h)8WQa6O zPAU}d`1y(>kZiP~Gr=mtJLMu`i<2CspL|q2DqAgAD^7*$xzM`PU4^ga`ilE134XBQ z99P(LhHU@7qvl9Yzg$M`+dlS=x^(m-_3t|h>S}E0bcFMn=C|KamQ)=w2^e)35p`zY zRV8X?d;s^>Cof2SPR&nP3E+-LCkS0J$H!eh8~k0qo$}00b=7!H_I2O+Ro@3O$nPdm ztmbOO^B+IHzQ5w>@@@J4cKw5&^_w6s!s=H%&byAbUtczPQ7}wfTqxxtQNfn*u73Qw zGuWsrky_ajPx-5`R<)6xHf>C(oqGf_Fw|-U*GfS?xLML$kv;h_pZ@Kk$y0X(S+K80 z6^|z)*`5VUkawg}=z`S;VhZhxyDfrE0$(PMurAxl~<>lfZa>JZ288ULK7D` zl9|#L^JL}Y$j*j`0-K6kH#?bRmg#5L3iB4Z)%iF@SqT+Lp|{i`m%R-|ZE94Np7Pa5 zCqC^V3}B(FR340pmF*qaa}M}+h6}mqE~7Sh!9bDv9YRT|>vBNAqv09zXHMlcuhKD| zcjjA(b*XCIwJ33?CB!+;{)vX@9xns_b-VO{i0y?}{!sdXj1GM8+$#v>W7nw;+O_9B z_{4L;C6ol?(?W0<6taGEn1^uG=?Q3i29sE`RfYCaV$3DKc_;?HsL?D_fSYg}SuO5U zOB_f4^vZ_x%o`5|C@9C5+o=mFy@au{s)sKw!UgC&L35aH(sgDxRE2De%(%OT=VUdN ziVLEmdOvJ&5*tCMKRyXctCwQu_RH%;m*$YK&m;jtbdH#Ak~13T1^f89tn`A%QEHWs~jnY~E}p_Z$XC z=?YXLCkzVSK+Id`xZYTegb@W8_baLt-Fq`Tv|=)JPbFsKRm)4UW;yT+J`<)%#ue9DPOkje)YF2fsCilK9MIIK>p*`fkoD5nGfmLwt)!KOT+> zOFq*VZktDDyM3P5UOg`~XL#cbzC}eL%qMB=Q5$d89MKuN#$6|4gx_Jt0Gfn8w&q}%lq4QU%6#jT*MRT% zrLz~C8FYKHawn-EQWN1B75O&quS+Z81(zN)G>~vN8VwC+e+y(`>HcxC{MrJ;H1Z4k zZWuv$w_F0-Ub%MVcpIc){4PGL^I7M{>;hS?;eH!;gmcOE66z3;Z1Phqo(t zVP(Hg6q#0gIKgsg7L7WE!{Y#1nI(45tx2{$34dDd#!Z0NIyrm)HOn5W#7;f4pQci# zDW!FI(g4e668kI9{2+mLwB+=#9bfqgX%!B34V-$wwSN(_cm*^{y0jQtv*4}eO^sOV z*9xoNvX)c9isB}Tgx&ZRjp3kwhTVK?r9;n!x>^XYT z@Q^7zp{rkIs{2mUSE^2!Gf6$6;j~&4=-0cSJJDizZp6LTe8b45;{AKM%v99}{{FfC zz709%u0mC=1KXTo(=TqmZQ;c?$M3z(!xah>aywrj40sc2y3rKFw4jCq+Y+u=CH@_V zxz|qeTwa>+<|H%8Dz5u>ZI5MmjTFwXS-Fv!TDd*`>3{krWoNVx$<133`(ftS?ZPyY z&4@ah^3^i`vL$BZa>O|Nt?ucewzsF)0zX3qmM^|waXr=T0pfIb0*$AwU=?Ipl|1Y; z*Pk6{C-p4MY;j@IJ|DW>QHZQJcp;Z~?8(Q+Kk3^0qJ}SCk^*n4W zu9ZFwLHUx-$6xvaQ)SUQcYd6fF8&x)V`1bIuX@>{mE$b|Yd(qomn3;bPwnDUc0F=; zh*6_((%bqAYQWQ~odER?h>1mkL4kpb3s7`0m@rDKGU*oyF)$j~Ffd4fXV$?`f~rHf zB%Y)@5SXZvfwm10RY5X?TEo)PK_`L6qgBp=#>fO49$D zDq8Ozj0q6213tV5Qq=;fZ0$|KroY{Dz=l@lU^J)?Ko@ti20TRplXzphBi>XGx4bou zEWrkNjz0t5j!_ke{g5I#PUlEU$Km8g8TE|XK=MkU@PT4T><2OVamoK;wJ}3X0L$vX zgd7gNa359*nc)R-0!`2X@FOTB`+oETOPc=ubp5R)VQgY+5BTZZJ2?9QwnO=dnulIUF3gFn;BODC2)65)HeVd%t86sL7Rv^Y+nbn+&l z6BAJY(ETvwI)Ts$aiE8rht4KD*qNyE{8{x6R|%akbTBzw;2+6Echkt+W+`u^XX z_z&x%n '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/extended/native-desktop/hello-awtfree-kn/settings.gradle.kts b/extended/native-desktop/hello-awtfree-kn/settings.gradle.kts new file mode 100644 index 00000000000..7e3589802b7 --- /dev/null +++ b/extended/native-desktop/hello-awtfree-kn/settings.gradle.kts @@ -0,0 +1,19 @@ +pluginManagement { + repositories { + // org.thisisthepy.kotlin.native.desktop, published by the workflow from gradle-plugins. + mavenLocal() + gradlePluginPortal() + mavenCentral() + google() + } +} +dependencyResolutionManagement { + repositories { + // The core fork's Compose for macosArm64 and its window modules, published by the + // workflow from compose-multiplatform-core-extended. + mavenLocal() + mavenCentral() + google() + } +} +rootProject.name = "hello-awtfree-kn" diff --git a/extended/native-desktop/hello-awtfree-kn/src/macosMain/kotlin/Main.kt b/extended/native-desktop/hello-awtfree-kn/src/macosMain/kotlin/Main.kt new file mode 100644 index 00000000000..bcfaa047aed --- /dev/null +++ b/extended/native-desktop/hello-awtfree-kn/src/macosMain/kotlin/Main.kt @@ -0,0 +1,156 @@ +@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) + +package awtfree + +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.toKString +import kotlinx.cinterop.usePinned +import org.jetbrains.skia.Bitmap +import org.jetbrains.skia.EncodedImageFormat +import org.jetbrains.skia.Surface +import org.thisisthepy.compose.window.MacosAx +import org.thisisthepy.compose.window.TitleBarStyle +import org.thisisthepy.compose.window.WindowConfig +import org.thisisthepy.compose.window.WindowEvent +import org.thisisthepy.compose.window.WindowListener +import org.thisisthepy.compose.window.closeButtonInset +import org.thisisthepy.compose.window.formatMacosAxNode +import org.thisisthepy.compose.window.macos.MacosWindowPlatform +import org.thisisthepy.compose.window.macos.axApiProbe +import org.thisisthepy.compose.window.macosAccessibleContentFindings +import org.thisisthepy.compose.window.macosChromeFindings +import org.thisisthepy.compose.window.window +import platform.AppKit.NSApplication +import platform.AppKit.NSApplicationActivationPolicy +import platform.Foundation.NSData +import platform.Foundation.NSProcessInfo +import platform.Foundation.create +import platform.Foundation.writeToFile +import platform.posix.getenv +import kotlin.system.exitProcess + +/** + * The AWT-free sample in the Kotlin/Native window. With HELLO_SELF_CHECK naming a PNG it proves, + * with nobody watching, what hello-awtfree proves in the GraalVM window: + * 1. Korean is shaped, broken into words and wrapped correctly by what is linked in, with no + * ICU data file on the machine; + * 2. the sample text inks pixels, drawn into a raster surface and written to the PNG; + * 3. the window opens and presents frames of the Compose content, each drawn at the size the + * window had when it was presented; + * 4. the title bar is the one the configuration asked for, and a screen reader finds the field + * and the button by their names. + * The exit status is 0 only when all four hold. HELLO_TITLE_BAR=Simple opens the window with + * the plain title bar. + */ +fun main() { + val check = getenv("HELLO_SELF_CHECK")?.toKString() + var ok = true + if (check != null) { + ok = textSelfCheck() && drawSample(check) + } + val application = NSApplication.sharedApplication() + // An executable outside a bundle is not, by default, something the system puts in front + // or gives the keyboard. + application.setActivationPolicy(NSApplicationActivationPolicy.NSApplicationActivationPolicyRegular) + val titleBar = if (getenv("HELLO_TITLE_BAR")?.toKString() == "Simple") TitleBarStyle.Simple else TitleBarStyle.Normal + val config = WindowConfig("안녕하세요, AwtFree", 520, 360, minWidth = 240, minHeight = 160, titleBar = titleBar) + val platform = MacosWindowPlatform { Content() } + val listener = object : WindowListener { + override fun onEvent(event: WindowEvent) {} + } + if (!platform.open(config, listener)) { + println("self-check: the window did not open") + exitProcess(2) + } + application.activateIgnoringOtherApps(true) + if (check == null) { + application.run() + return + } + val started = NSProcessInfo.processInfo.systemUptime + fun presented() = platform.resizeStats?.presented ?: 0L + // A frame is drawn when something changed, and nothing changes in this content on its own, + // so each turn asks for one: 30 frames drawn and presented in a row. + while (presented() < 30 && NSProcessInfo.processInfo.systemUptime - started < 20.0) { + platform.requestFrame() + platform.pump(16) + } + val stats = platform.resizeStats + println("scene: presented ${stats?.presented ?: 0} frames of Compose content, ${stats?.stale ?: 0} at a size other than the window's") + if ((stats?.presented ?: 0) == 0L || (stats?.stale ?: 0) != 0L) ok = false + if (!readerChecks(platform, config)) ok = false + exitProcess(if (ok) 0 else 1) +} + +/** + * The title bar and what a screen reader is told, asked in process: an outside client needs an + * accessibility trust a CI runner has not given. What the system's API answers an outside + * client is printed with its error number. + */ +private fun readerChecks(platform: MacosWindowPlatform, config: WindowConfig): Boolean { + // Asking is what tells the window a reader is listening; it describes itself from the next + // frame on. + platform.accessibilitySnapshot() + repeat(20) { + platform.requestFrame() + platform.pump(16) + } + val snapshot = platform.accessibilitySnapshot() + val caption = platform.caption() + val style = config.titleBar + var ok = true + val chrome = macosChromeFindings(snapshot, config.title, platform.macosTitleBar, caption) + println("chrome: $style caption $caption, close button inset ${snapshot.closeButtonInset(config.title)} pt, macOS ${snapshot.macosMajor}") + if (chrome.isEmpty()) { + println("chrome: ok ($style)") + } else { + ok = false + chrome.forEach { println("chrome: fail: $it") } + } + val content = macosAccessibleContentFindings( + snapshot, + config.title, + listOf(MacosAx.TEXT_FIELD to FIELD_LABEL, MacosAx.BUTTON to BUTTON_LABEL), + ) + if (content.isEmpty()) { + println("accessibility: ok (a text field '$FIELD_LABEL' and a button '$BUTTON_LABEL')") + } else { + ok = false + content.forEach { println("accessibility: fail: $it") } + } + println("accessibility: ${axApiProbe().describe()}") + if (!ok) { + println("accessibility: what the window told a reader:") + snapshot.window(config.title).forEach { println(" ${formatMacosAxNode(it)}") } + } + return ok +} + +/** Draws the sample text into a raster surface, counts what it inked and writes the PNG. */ +private fun drawSample(png: String): Boolean { + val surface = Surface.makeRasterN32Premul(480, 240) + surface.canvas.clear(org.jetbrains.skia.Color.WHITE) + sampleParagraph(440f).paint(surface.canvas, 20f, 20f) + val bitmap = Bitmap().apply { allocN32Pixels(480, 240) } + surface.readPixels(bitmap, 0, 0) + var inked = 0 + for (y in 0 until 240) for (x in 0 until 480) if (bitmap.getColor(x, y) != org.jetbrains.skia.Color.WHITE) inked++ + if (inked == 0) { + println("self-check: the sample text drew no pixels") + return false + } + println("self-check: the sample text inked $inked pixels") + val bytes = surface.makeImageSnapshot().encodeToData(EncodedImageFormat.PNG)?.bytes ?: run { + println("self-check: Skia did not encode the PNG") + return false + } + val written = bytes.usePinned { + NSData.create(bytes = it.addressOf(0), length = bytes.size.toULong()).writeToFile(png, atomically = true) + } + if (!written) { + println("self-check: the PNG could not be written to $png") + return false + } + println("self-check: drew ${bytes.size} bytes of PNG to $png") + return true +} diff --git a/extended/native-image/README.md b/extended/native-image/README.md index 570b4a2edb4..20105c4a1a4 100644 --- a/extended/native-image/README.md +++ b/extended/native-image/README.md @@ -154,6 +154,24 @@ that does not end within 120 s is stopped with every thread's stack printed. An executable that needs an ICU data file shows it there: a double click would select one syllable, and lines would wrap inside words. +On macOS the gate also checks the window as a screen reader meets it, and runs the executable +twice, with `HELLO_TITLE_BAR` unset (the Normal title bar) and set to `Simple`. Each run walks +its own accessibility elements in process, through the objects AppKit's accessibility server +answers every client from, because a hosted runner has not trusted any process for +accessibility and an outside client is refused there. The run prints what the system's API +answered an outside client, with its error number (`kAXErrorAPIDisabled`, -25211, on a runner +that has not granted the trust). It fails unless the three system buttons are there, the content +runs under the title bar, no title text is drawn, the caption's height and button room match the +buttons' own frames, the corner radius is the core fork's measured table for that style and +release, and the field and the button are found by their roles and names. On macOS 26 the +Normal title bar's buttons must sit further in than the Simple one's. + +The "One executable (Kotlin/Native, )" jobs run the same gate on +`extended/native-desktop/hello-awtfree-kn`: the same content (`hello-awtfree/src/shared`) in the +core fork's Kotlin/Native window (`native-macos`), linked by Gradle with the native desktop +plugin against the core fork's `macosArm64` Compose, with no JVM. The macOS cell is there; the +Linux cells come with the Linux window, and Windows after it. + Measured by the gate, run 39028163166 (2026-10-17, NIK 25.0.4.1), before `hello-awtfree` hosted a Compose scene: it is one executable of 37.62 MB on macOS arm64, 46.27 MB on Linux x64 and 44.96 MB on Linux arm64, naming diff --git a/extended/native-image/check-one-executable.sh b/extended/native-image/check-one-executable.sh index 1fd42ff387e..da079dee87e 100755 --- a/extended/native-image/check-one-executable.sh +++ b/extended/native-image/check-one-executable.sh @@ -189,4 +189,34 @@ if grep -E '^(java|kotlin|org)\.[A-Za-z0-9_.$]*(Exception|Error)\b' "$log"; then fi [[ "$status" -eq 0 ]] || fail "the executable did not run to the end on its own (exit $status)" +# 5. On macOS, the title bar and what a screen reader is told, for both title bar styles: the +# report above is the Normal one, and the executable runs once more with the Simple title bar. +if [[ "$os" == macos ]]; then + grep -q '^chrome: ok (Normal)' "$log" || + fail "the Normal title bar is not the one the configuration asked for" "Its own report is above." + grep -q '^accessibility: ok' "$log" || + fail "a screen reader does not find the field and the button by their names" "Its own report is above." + simple="$scratch/run-simple" + simple_log="$scratch/run-simple.log" + mkdir -p "$simple" + cp "$binary" "$simple/$name" + simple_status=0 + ( cd "$simple" && HELLO_TITLE_BAR=Simple HELLO_SELF_CHECK="$scratch/self-check-simple.png" \ + perl -e 'alarm 120; exec @ARGV' "./$name" ) > "$simple_log" 2>&1 || simple_status=$? + echo "== the application's own report, with the Simple title bar" + cat "$simple_log" + grep -q '^chrome: ok (Simple)' "$simple_log" || + fail "the Simple title bar is not the one the configuration asked for" "Its own report is above." + [[ "$simple_status" -eq 0 ]] || fail "the Simple run did not end on its own (exit $simple_status)" + inset() { sed -n 's/.*close button inset \([0-9.]*\) pt, macOS \([0-9]*\).*/\1 \2/p' "$1" | head -1; } + read -r normal_inset major <<< "$(inset "$log")" + read -r simple_inset _ <<< "$(inset "$simple_log")" + echo "-- close button inset: Normal ${normal_inset:-?} pt, Simple ${simple_inset:-?} pt, macOS ${major:-?}" + # macOS 26 sets a unified toolbar window's buttons further in; earlier releases do not. + if [[ "${major:-0}" -ge 26 ]]; then + awk -v n="${normal_inset:-0}" -v s="${simple_inset:-0}" 'BEGIN { exit !(n > s) }' || + fail "the Normal title bar's buttons are not set further in than the Simple one's on macOS $major" + fi +fi + echo "ok $name is one executable: system libraries only, Korean laid out with no ICU data file" diff --git a/extended/native-image/hello-awtfree/build.gradle.kts b/extended/native-image/hello-awtfree/build.gradle.kts index 304870d7b91..5c037f8f91a 100644 --- a/extended/native-image/hello-awtfree/build.gradle.kts +++ b/extended/native-image/hello-awtfree/build.gradle.kts @@ -15,6 +15,9 @@ val windowArtifact = if (macos) "graalvm-macos" else "graalvm-linux" sourceSets.main { kotlin.srcDir(if (macos) "src/macos/kotlin" else "src/linux/kotlin") + // The content and the text check, shared with the Kotlin/Native sample + // (extended/native-desktop/hello-awtfree-kn). + kotlin.srcDir("src/shared/kotlin") } dependencies { diff --git a/extended/native-image/hello-awtfree/src/linux/kotlin/Platform.kt b/extended/native-image/hello-awtfree/src/linux/kotlin/Platform.kt index ff699a62a6d..9d73142572a 100644 --- a/extended/native-image/hello-awtfree/src/linux/kotlin/Platform.kt +++ b/extended/native-image/hello-awtfree/src/linux/kotlin/Platform.kt @@ -1,6 +1,8 @@ package awtfree +import org.thisisthepy.compose.window.WindowConfig import org.thisisthepy.compose.window.WindowPlatform +import org.thisisthepy.compose.window.scene.ComposeWindowHost import org.thisisthepy.compose.window.graalvm.linux.X11Window import org.thisisthepy.compose.window.graalvm.linux.x11WindowSurface import org.thisisthepy.compose.window.scene.WindowSurface @@ -11,3 +13,12 @@ internal fun newSurface(platform: WindowPlatform): WindowSurface? = x11WindowSur /** X11 takes calls from any one thread, so where main runs does not matter here. */ internal fun onFirstThread(): Boolean = true + +/** + * The platform's checks of the running window. None on this desktop yet: the X11 title bar + * and accessibility checks are the Linux window's to add. + */ +internal fun platformChecks(host: ComposeWindowHost, platform: WindowPlatform, config: WindowConfig): Boolean { + println("chrome: not checked on this desktop; the X11 window's title bar and accessibility checks are added with it") + return true +} diff --git a/extended/native-image/hello-awtfree/src/macos/kotlin/Platform.kt b/extended/native-image/hello-awtfree/src/macos/kotlin/Platform.kt index 06b6df7d284..f92cdfb31c6 100644 --- a/extended/native-image/hello-awtfree/src/macos/kotlin/Platform.kt +++ b/extended/native-image/hello-awtfree/src/macos/kotlin/Platform.kt @@ -1,7 +1,15 @@ package awtfree import org.graalvm.nativeimage.c.function.CFunction +import org.thisisthepy.compose.window.MacosAx +import org.thisisthepy.compose.window.WindowConfig import org.thisisthepy.compose.window.WindowPlatform +import org.thisisthepy.compose.window.closeButtonInset +import org.thisisthepy.compose.window.formatMacosAxNode +import org.thisisthepy.compose.window.macosAccessibleContentFindings +import org.thisisthepy.compose.window.macosChromeFindings +import org.thisisthepy.compose.window.window +import org.thisisthepy.compose.window.scene.ComposeWindowHost import org.thisisthepy.compose.window.graalvm.macos.AppKitWindowPlatform import org.thisisthepy.compose.window.graalvm.macos.appKitWindowSurface import org.thisisthepy.compose.window.graalvm.macos.installApplicationMenu @@ -27,3 +35,58 @@ private external fun pthreadMainNp(): Int * forever. */ internal fun onFirstThread(): Boolean = pthreadMainNp() != 0 + +/** + * Checks the running window as a reader who cannot see it meets it, and prints what it found. + * + * Asked in process, because an outside accessibility client needs a trust a CI runner has not + * given: the window walks its own accessibility elements, the objects AppKit's accessibility + * server answers every client from. What the system's API answers an outside client is + * printed too, with its error number. + * + * - The title bar the configuration asked for: the three system buttons, the content under + * the bar, no title drawn, the caption's height and button room as the buttons' own frames + * say, and the corner radius from the measured table. + * - The field and the button, by role and by the names a screen reader is told. + * + * Returns whether both hold. + */ +internal fun platformChecks(host: ComposeWindowHost, platform: WindowPlatform, config: WindowConfig): Boolean { + val window = platform as AppKitWindowPlatform + // Asking is what tells the window a reader is listening; the scene describes itself from + // the next frame on. + window.accessibilitySnapshot() + repeat(20) { + host.requestFrame() + host.turn(16) + } + val snapshot = window.accessibilitySnapshot() + val caption = window.caption() + val style = config.titleBar + var ok = true + val chrome = macosChromeFindings(snapshot, config.title, window.macosTitleBar, caption) + println("chrome: $style caption $caption, close button inset ${snapshot.closeButtonInset(config.title)} pt, macOS ${snapshot.macosMajor}") + if (chrome.isEmpty()) { + println("chrome: ok ($style)") + } else { + ok = false + chrome.forEach { println("chrome: fail: $it") } + } + val content = macosAccessibleContentFindings( + snapshot, + config.title, + listOf(MacosAx.TEXT_FIELD to FIELD_LABEL, MacosAx.BUTTON to BUTTON_LABEL), + ) + if (content.isEmpty()) { + println("accessibility: ok (a text field '$FIELD_LABEL' and a button '$BUTTON_LABEL')") + } else { + ok = false + content.forEach { println("accessibility: fail: $it") } + } + println("accessibility: ${window.accessibilityApiProbe().describe()}") + if (!ok) { + println("accessibility: what the window told a reader:") + snapshot.window(config.title).forEach { println(" ${formatMacosAxNode(it)}") } + } + return ok +} diff --git a/extended/native-image/hello-awtfree/src/main/kotlin/Main.kt b/extended/native-image/hello-awtfree/src/main/kotlin/Main.kt index 084926a0e92..8e0a839211b 100644 --- a/extended/native-image/hello-awtfree/src/main/kotlin/Main.kt +++ b/extended/native-image/hello-awtfree/src/main/kotlin/Main.kt @@ -1,69 +1,14 @@ package awtfree -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.text.BasicText -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import java.io.File import kotlin.system.exitProcess import org.jetbrains.skia.Bitmap import org.jetbrains.skia.EncodedImageFormat import org.jetbrains.skia.Surface +import org.thisisthepy.compose.window.TitleBarStyle import org.thisisthepy.compose.window.WindowConfig import org.thisisthepy.compose.window.scene.ComposeWindowHost -/** - * Text, a field and a control that changes when it is clicked. Text needs a font manager, a - * shaper and a layout pass, the field needs the platform's text input, and the control shows - * that pointer input reaches the scene. - */ -@Composable -fun Content() { - var clicks by remember { mutableStateOf(0) } - var typed by remember { mutableStateOf("") } - Box(Modifier.fillMaxSize().background(Color(0xFF12321A))) { - Column(Modifier.padding(top = 32.dp, start = 24.dp)) { - BasicText("안녕하세요, no AWT here", style = TextStyle(color = Color.White, fontSize = 24.sp)) - BasicText( - "composed by Compose, painted by Skia, shown by a window of our own", - style = TextStyle(color = Color(0xFF9CCC9C), fontSize = 14.sp), - ) - BasicTextField( - value = typed, - onValueChange = { typed = it }, - modifier = Modifier.padding(top = 16.dp).size(260.dp, 32.dp).background(Color(0xFF1E4620)), - textStyle = TextStyle(color = Color.White, fontSize = 16.sp), - cursorBrush = SolidColor(Color.White), - ) - Box( - Modifier.padding(top = 24.dp).size(220.dp, 56.dp).background(Color(0xFF2E7D32)).clickable { clicks++ }, - ) { - BasicText( - if (clicks == 0) "click me" else "clicked $clicks", - Modifier.padding(16.dp), - style = TextStyle(color = Color.White, fontSize = 18.sp), - ) - } - } - } -} - /** * A Compose application with no AWT in it: the window is the core fork's GraalVM window, the * scene is hosted by its scene module, and Skia draws into the window's own surface, Metal on @@ -75,8 +20,11 @@ fun Content() { * ([textSelfCheck]), with no ICU data file on the machine; * 2. the sample text inks pixels, drawn into a raster surface and written to the PNG; * 3. the window opens and presents frames of the Compose content, each drawn at the size the - * window had when it was presented. - * The exit status is 0 only when all three hold. + * window had when it was presented; + * 4. the platform's checks of the running window pass: on macOS, the title bar the + * configuration asked for, and the field and the button with their names where a screen + * reader looks. + * The exit status is 0 only when all four hold. */ fun main() { val check = System.getenv("HELLO_SELF_CHECK") @@ -89,9 +37,13 @@ fun main() { System.err.println("self-check: main is not on the process's first thread, which the window's calls need") exitProcess(3) } + // HELLO_TITLE_BAR=Simple opens the window with the plain title bar; the default is Normal. + val titleBar = if (System.getenv("HELLO_TITLE_BAR") == "Simple") TitleBarStyle.Simple else TitleBarStyle.Normal + val platform = newPlatform() + val config = WindowConfig("안녕하세요, AwtFree", 520, 360, minWidth = 240, minHeight = 160, titleBar = titleBar) val host = ComposeWindowHost( - platform = newPlatform(), - config = WindowConfig("안녕하세요, AwtFree", 520, 360, minWidth = 240, minHeight = 160), + platform = platform, + config = config, surfaceFor = ::newSurface, content = { Content() }, ) @@ -111,6 +63,9 @@ fun main() { host.requestFrame() host.turn(16) } + // 4. What the platform's own checks say about the running window: on macOS the title + // bar and what a screen reader is told (platformChecks in Platform.kt). + if (!host.closed && !platformChecks(host, platform, config)) ok = false } finally { host.close() } diff --git a/extended/native-image/hello-awtfree/src/shared/kotlin/Content.kt b/extended/native-image/hello-awtfree/src/shared/kotlin/Content.kt new file mode 100644 index 00000000000..cba666209e9 --- /dev/null +++ b/extended/native-image/hello-awtfree/src/shared/kotlin/Content.kt @@ -0,0 +1,70 @@ +package awtfree + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +// The content both AWT-free samples draw: hello-awtfree in the GraalVM window, and +// extended/native-desktop/hello-awtfree-kn in the Kotlin/Native one. One source, so both +// executables are checked against the same scene. + +/** The field's and the button's names, as a screen reader is told them. */ +const val FIELD_LABEL = "Your name" +const val BUTTON_LABEL = "click me" + +/** + * Text, a field and a control that changes when it is clicked. Text needs a font manager, a + * shaper and a layout pass, the field needs the platform's text input, and the control shows + * that pointer input reaches the scene. + */ +@Composable +fun Content() { + var clicks by remember { mutableStateOf(0) } + var typed by remember { mutableStateOf("") } + Box(Modifier.fillMaxSize().background(Color(0xFF12321A))) { + Column(Modifier.padding(top = 32.dp, start = 24.dp)) { + BasicText("안녕하세요, no AWT here", style = TextStyle(color = Color.White, fontSize = 24.sp)) + BasicText( + "composed by Compose, painted by Skia, shown by a window of our own", + style = TextStyle(color = Color(0xFF9CCC9C), fontSize = 14.sp), + ) + BasicTextField( + value = typed, + onValueChange = { typed = it }, + // What a screen reader calls the field; the self-check looks for it by this name. + modifier = Modifier.padding(top = 16.dp).size(260.dp, 32.dp).background(Color(0xFF1E4620)) + .semantics { contentDescription = FIELD_LABEL }, + textStyle = TextStyle(color = Color.White, fontSize = 16.sp), + cursorBrush = SolidColor(Color.White), + ) + Box( + Modifier.padding(top = 24.dp).size(220.dp, 56.dp).background(Color(0xFF2E7D32)).clickable { clicks++ }, + ) { + BasicText( + if (clicks == 0) BUTTON_LABEL else "clicked $clicks", + Modifier.padding(16.dp), + style = TextStyle(color = Color.White, fontSize = 18.sp), + ) + } + } + } +} diff --git a/extended/native-image/hello-awtfree/src/main/kotlin/TextSelfCheck.kt b/extended/native-image/hello-awtfree/src/shared/kotlin/TextSelfCheck.kt similarity index 100% rename from extended/native-image/hello-awtfree/src/main/kotlin/TextSelfCheck.kt rename to extended/native-image/hello-awtfree/src/shared/kotlin/TextSelfCheck.kt From 6f187c3e27dcfb01d44f54ed7bcb456cd1edb0cb Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sat, 24 Oct 2026 12:09:12 +0900 Subject: [PATCH 2/9] Fix: Correct CORE_EXTENDED_REF commit hash to merged extended head --- .github/workflows/extended-native-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/extended-native-image.yml b/.github/workflows/extended-native-image.yml index 9fd5eb26ba0..c214fb9bf69 100644 --- a/.github/workflows/extended-native-image.yml +++ b/.github/workflows/extended-native-image.yml @@ -23,7 +23,7 @@ permissions: env: # thisisthepy/compose-multiplatform-core-extended, the commit the window modules and # extended/skiko come from. - CORE_EXTENDED_REF: fd733c8395f32eb42ae9289230554dbdaebffad9 + CORE_EXTENDED_REF: fd733c8395fb24d107752a56d6893f486e46a62a # The plugin as a local development build, asking for the core fork's Compose at its own # version. The core fork's desktop Compose is built once from CORE_EXTENDED_REF, by the # core-compose job (extended/ci/core-compose), and handed to the jobs that need it. From a9b6f58194df625650517054dcbf135ccc6ff7c6 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sat, 24 Oct 2026 12:33:24 +0900 Subject: [PATCH 3/9] Fix: Update window module dependencies to 1.11.1-ext.1 and match scene platforms --- .github/workflows/extended-native-image.yml | 4 ++-- extended/native-desktop/hello-awtfree-kn/build.gradle.kts | 2 +- extended/native-image/hello-awtfree-link/build.gradle.kts | 4 ++-- extended/native-image/hello-awtfree/build.gradle.kts | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/extended-native-image.yml b/.github/workflows/extended-native-image.yml index c214fb9bf69..214b32766a7 100644 --- a/.github/workflows/extended-native-image.yml +++ b/.github/workflows/extended-native-image.yml @@ -178,7 +178,7 @@ jobs: shell: bash working-directory: core-extended/extended/window run: | - perl -0pi -e 's/^ platforms: \[ jvm, macosArm64 \]$/ platforms: [ jvm ]/m' scene/module.yaml + perl -0pi -e 's/^ platforms: \[.*\]$/ platforms: [ jvm ]/m' scene/module.yaml grep -q '^ platforms: \[ jvm \]$' scene/module.yaml perl -0pi -e 's/^ platforms: \[.*\]$/ platforms: [ jvm ]/m' common/module.yaml grep -q '^ platforms: \[ jvm \]$' common/module.yaml @@ -300,7 +300,7 @@ jobs: shell: bash working-directory: core-extended/extended/window run: | - perl -0pi -e 's/^ platforms: \[ jvm, macosArm64 \]$/ platforms: [ jvm ]/m' scene/module.yaml + perl -0pi -e 's/^ platforms: \[.*\]$/ platforms: [ jvm ]/m' scene/module.yaml grep -q '^ platforms: \[ jvm \]$' scene/module.yaml perl -0pi -e 's/^ platforms: \[.*\]$/ platforms: [ jvm ]/m' common/module.yaml grep -q '^ platforms: \[ jvm \]$' common/module.yaml diff --git a/extended/native-desktop/hello-awtfree-kn/build.gradle.kts b/extended/native-desktop/hello-awtfree-kn/build.gradle.kts index 1cf6bdcce9c..533fcdfdb87 100644 --- a/extended/native-desktop/hello-awtfree-kn/build.gradle.kts +++ b/extended/native-desktop/hello-awtfree-kn/build.gradle.kts @@ -16,7 +16,7 @@ plugins { // The core fork's Compose and window modules, as compose-multiplatform-core-extended publishes // them to the local Maven repository. val compose = "1.11.1-ext.1" -val window = "0.1.0" +val window = "1.11.1-ext.1" kotlin { macosArm64 { diff --git a/extended/native-image/hello-awtfree-link/build.gradle.kts b/extended/native-image/hello-awtfree-link/build.gradle.kts index d3f59bdf994..4efbd27cbca 100644 --- a/extended/native-image/hello-awtfree-link/build.gradle.kts +++ b/extended/native-image/hello-awtfree-link/build.gradle.kts @@ -16,8 +16,8 @@ sourceSets.main { } dependencies { - implementation("org.thisisthepy.compose.window:common:0.1.0") - implementation("org.thisisthepy.compose.window:$windowArtifact:0.1.0") + implementation("org.thisisthepy.compose.window:common:1.11.1-ext.1") + implementation("org.thisisthepy.compose.window:$windowArtifact:1.11.1-ext.1") // skiko is linked into every image this plugin makes; the sample draws nothing with it. implementation(compose.desktop.currentOs) // skiko-awt with the AWT switch, from compose-multiplatform-core-extended's diff --git a/extended/native-image/hello-awtfree/build.gradle.kts b/extended/native-image/hello-awtfree/build.gradle.kts index 5c037f8f91a..8e044c0d49c 100644 --- a/extended/native-image/hello-awtfree/build.gradle.kts +++ b/extended/native-image/hello-awtfree/build.gradle.kts @@ -21,10 +21,10 @@ sourceSets.main { } dependencies { - implementation("org.thisisthepy.compose.window:common:0.1.0") - implementation("org.thisisthepy.compose.window:$windowArtifact:0.1.0") + implementation("org.thisisthepy.compose.window:common:1.11.1-ext.1") + implementation("org.thisisthepy.compose.window:$windowArtifact:1.11.1-ext.1") // The scene host, and the core fork's foundation the content is written with. - implementation("org.thisisthepy.compose.window:scene:0.1.0") + implementation("org.thisisthepy.compose.window:scene:1.11.1-ext.1") implementation(compose.foundation) // @CFunction, to ask the system which thread main runs on. compileOnly("org.graalvm.sdk:nativeimage:25.0.1") From f52e4bd148350d46fe45e425c58d0368999331e1 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sat, 24 Oct 2026 12:46:24 +0900 Subject: [PATCH 4/9] Fix: Update Compose material3 version to 1.11.1-ext.1 --- .github/workflows/extended-native-image.yml | 4 ++-- extended/native-image/hello/build.gradle.kts | 2 +- extended/samples/hello-jvm/build.gradle.kts | 2 +- gradle-plugins/gradle.properties | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/extended-native-image.yml b/.github/workflows/extended-native-image.yml index 214b32766a7..29a196acd12 100644 --- a/.github/workflows/extended-native-image.yml +++ b/.github/workflows/extended-native-image.yml @@ -128,7 +128,7 @@ jobs: - name: Plugin to mavenLocal working-directory: gradle-plugins - run: ./gradlew --no-daemon -q :compose:publishToMavenLocal :jdk-version-probe:publishToMavenLocal -Pdeploy.version=${{ env.PLUGIN_VERSION }} -Pcompose.version=${{ env.COMPOSE_VERSION }} + run: ./gradlew --no-daemon -q :compose:publishToMavenLocal :jdk-version-probe:publishToMavenLocal -Pdeploy.version=${{ env.PLUGIN_VERSION }} -Pcompose.version=${{ env.COMPOSE_VERSION }} -Pcompose.material3.version=${{ env.COMPOSE_VERSION }} - name: createDistributable working-directory: extended/samples/hello-jvm run: ./gradlew --no-daemon createDistributable @@ -342,7 +342,7 @@ jobs: - name: Plugin to mavenLocal working-directory: gradle-plugins - run: ./gradlew --no-daemon -q :compose:publishToMavenLocal :jdk-version-probe:publishToMavenLocal -Pdeploy.version=${{ env.PLUGIN_VERSION }} -Pcompose.version=${{ env.COMPOSE_VERSION }} + run: ./gradlew --no-daemon -q :compose:publishToMavenLocal :jdk-version-probe:publishToMavenLocal -Pdeploy.version=${{ env.PLUGIN_VERSION }} -Pcompose.version=${{ env.COMPOSE_VERSION }} -Pcompose.material3.version=${{ env.COMPOSE_VERSION }} # The AwtFree sample: packageNativeImage with the no-JAWT archive and the window layer. - name: packageNativeImage (AwtFree sample) diff --git a/extended/native-image/hello/build.gradle.kts b/extended/native-image/hello/build.gradle.kts index b5052f0b5cb..7def8acf8ce 100644 --- a/extended/native-image/hello/build.gradle.kts +++ b/extended/native-image/hello/build.gradle.kts @@ -11,7 +11,7 @@ plugins { // (-Pextended.publication.dev=true), which is what the plugin above is built to ask for. dependencies { implementation(compose.desktop.currentOs) - implementation("org.thisisthepy.compose.material3:material3:1.11.0-alpha07-ext.1-dev") + implementation("org.thisisthepy.compose.material3:material3:1.11.1-ext.1-dev") } compose.desktop.application { diff --git a/extended/samples/hello-jvm/build.gradle.kts b/extended/samples/hello-jvm/build.gradle.kts index 6f5ce02cb62..964041eb7ec 100644 --- a/extended/samples/hello-jvm/build.gradle.kts +++ b/extended/samples/hello-jvm/build.gradle.kts @@ -9,7 +9,7 @@ plugins { dependencies { implementation(compose.desktop.currentOs) - implementation("org.thisisthepy.compose.material3:material3:1.11.0-alpha07-ext.1") + implementation("org.thisisthepy.compose.material3:material3:1.11.1-ext.1") } compose.desktop.application { diff --git a/gradle-plugins/gradle.properties b/gradle-plugins/gradle.properties index b771a010a1c..f5ec85acb42 100644 --- a/gradle-plugins/gradle.properties +++ b/gradle-plugins/gradle.properties @@ -12,7 +12,7 @@ dev.junit.parallel=false # published as org.thisisthepy.compose.* at -ext.. A plugin that should # resolve a local development build of those libraries passes -Pcompose.version=...-ext.-dev. compose.version=1.11.1-ext.1 -compose.material3.version=1.11.0-alpha07-ext.1 +compose.material3.version=1.11.1-ext.1 # The latest version of Kotlin compatible with compose.tests.compiler.version. Used only in tests/CI. compose.tests.kotlin.version=2.4.20 # __SUPPORTED_GRADLE_VERSIONS__ From c41203395f0b98bcbcdde7013208123bb8b7a3f9 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sat, 24 Oct 2026 12:59:14 +0900 Subject: [PATCH 5/9] Fix: Update skiko-awt dependency to org.thisisthepy.compose.skiko:skiko-awt:1.11.1-ext.1 --- extended/native-image/hello-awtfree-link/build.gradle.kts | 2 +- extended/native-image/hello-awtfree/build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extended/native-image/hello-awtfree-link/build.gradle.kts b/extended/native-image/hello-awtfree-link/build.gradle.kts index 4efbd27cbca..e78a2d1ed50 100644 --- a/extended/native-image/hello-awtfree-link/build.gradle.kts +++ b/extended/native-image/hello-awtfree-link/build.gradle.kts @@ -23,7 +23,7 @@ dependencies { // skiko-awt with the AWT switch, from compose-multiplatform-core-extended's // extended/skiko/build-skiko-awt.sh. Its version sorts above the 0.144.6 Compose asks for, so // it is the one the image takes. - implementation("org.jetbrains.skiko:skiko-awt:0.144.6.1") + implementation("org.thisisthepy.compose.skiko:skiko-awt:1.11.1-ext.1") } compose.desktop.application { diff --git a/extended/native-image/hello-awtfree/build.gradle.kts b/extended/native-image/hello-awtfree/build.gradle.kts index 8e044c0d49c..55c9f625e17 100644 --- a/extended/native-image/hello-awtfree/build.gradle.kts +++ b/extended/native-image/hello-awtfree/build.gradle.kts @@ -33,7 +33,7 @@ dependencies { // skiko-awt with the AWT switch, from compose-multiplatform-core-extended's // extended/skiko/build-skiko-awt.sh. Its version sorts above the 0.144.6 Compose asks for, so // it is the one the image takes. - implementation("org.jetbrains.skiko:skiko-awt:0.144.6.1") + implementation("org.thisisthepy.compose.skiko:skiko-awt:1.11.1-ext.1") } compose.desktop.application { From 6b4a60c0b96de1898db6917539a4f9747cd2e99a Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:30:25 +0900 Subject: [PATCH 6/9] Fix: Resolve skiko-awt capability conflict by selecting highest version --- extended/native-image/hello-awtfree-link/build.gradle.kts | 6 ++++++ extended/native-image/hello-awtfree/build.gradle.kts | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/extended/native-image/hello-awtfree-link/build.gradle.kts b/extended/native-image/hello-awtfree-link/build.gradle.kts index e78a2d1ed50..e23b0e7cfe4 100644 --- a/extended/native-image/hello-awtfree-link/build.gradle.kts +++ b/extended/native-image/hello-awtfree-link/build.gradle.kts @@ -26,6 +26,12 @@ dependencies { implementation("org.thisisthepy.compose.skiko:skiko-awt:1.11.1-ext.1") } +configurations.all { + resolutionStrategy.capabilitiesResolution.withCapability("org.jetbrains.skiko:skiko-awt") { + selectHighestVersion() + } +} + compose.desktop.application { mainClass = "link.MainKt" windowing = org.jetbrains.compose.desktop.application.dsl.ApplicationWindowing.AwtFree diff --git a/extended/native-image/hello-awtfree/build.gradle.kts b/extended/native-image/hello-awtfree/build.gradle.kts index 55c9f625e17..b5a1c3bcd6c 100644 --- a/extended/native-image/hello-awtfree/build.gradle.kts +++ b/extended/native-image/hello-awtfree/build.gradle.kts @@ -36,6 +36,12 @@ dependencies { implementation("org.thisisthepy.compose.skiko:skiko-awt:1.11.1-ext.1") } +configurations.all { + resolutionStrategy.capabilitiesResolution.withCapability("org.jetbrains.skiko:skiko-awt") { + selectHighestVersion() + } +} + compose.desktop.application { mainClass = "awtfree.MainKt" windowing = org.jetbrains.compose.desktop.application.dsl.ApplicationWindowing.AwtFree From f560f23e346dabc0ffdc7ee570a3a4778065935f Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:50:46 +0900 Subject: [PATCH 7/9] Fix: Substitute skiko-awt with fork version and strip rpath from K/N executable --- .github/workflows/extended-native-image.yml | 3 +++ extended/native-image/hello-awtfree-link/build.gradle.kts | 6 ++++-- extended/native-image/hello-awtfree/build.gradle.kts | 6 ++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/extended-native-image.yml b/.github/workflows/extended-native-image.yml index 29a196acd12..1ba80ef462d 100644 --- a/.github/workflows/extended-native-image.yml +++ b/.github/workflows/extended-native-image.yml @@ -543,6 +543,9 @@ jobs: set -euo pipefail mkdir -p "$RUNNER_TEMP/kn-out" cp "$SAMPLE_KN/build/bin/macosArm64/releaseExecutable/hello-awtfree-kn.kexe" "$RUNNER_TEMP/kn-out/hello-awtfree-kn" + otool -l "$RUNNER_TEMP/kn-out/hello-awtfree-kn" | grep -A 2 LC_RPATH | awk '/path /{print $2}' | while read -r rpath; do + install_name_tool -delete_rpath "$rpath" "$RUNNER_TEMP/kn-out/hello-awtfree-kn" + done ls -la "$RUNNER_TEMP/kn-out" - name: Take every ICU data file off the runner diff --git a/extended/native-image/hello-awtfree-link/build.gradle.kts b/extended/native-image/hello-awtfree-link/build.gradle.kts index e23b0e7cfe4..40e91cc0109 100644 --- a/extended/native-image/hello-awtfree-link/build.gradle.kts +++ b/extended/native-image/hello-awtfree-link/build.gradle.kts @@ -27,8 +27,10 @@ dependencies { } configurations.all { - resolutionStrategy.capabilitiesResolution.withCapability("org.jetbrains.skiko:skiko-awt") { - selectHighestVersion() + resolutionStrategy.dependencySubstitution { + substitute(module("org.jetbrains.skiko:skiko-awt")) + .using(module("org.thisisthepy.compose.skiko:skiko-awt:1.11.1-ext.1")) + .because("AWT-free skiko-awt with AwtSwitch") } } diff --git a/extended/native-image/hello-awtfree/build.gradle.kts b/extended/native-image/hello-awtfree/build.gradle.kts index b5a1c3bcd6c..4928407ce2a 100644 --- a/extended/native-image/hello-awtfree/build.gradle.kts +++ b/extended/native-image/hello-awtfree/build.gradle.kts @@ -37,8 +37,10 @@ dependencies { } configurations.all { - resolutionStrategy.capabilitiesResolution.withCapability("org.jetbrains.skiko:skiko-awt") { - selectHighestVersion() + resolutionStrategy.dependencySubstitution { + substitute(module("org.jetbrains.skiko:skiko-awt")) + .using(module("org.thisisthepy.compose.skiko:skiko-awt:1.11.1-ext.1")) + .because("AWT-free skiko-awt with AwtSwitch") } } From f248dbee74c5ffb3d96ab1a83f4413542cc47dff Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:30:44 +0900 Subject: [PATCH 8/9] Fix: Remove redundant brace in hello-awtfree build scripts --- extended/native-image/hello-awtfree-link/build.gradle.kts | 1 - extended/native-image/hello-awtfree/build.gradle.kts | 1 - 2 files changed, 2 deletions(-) diff --git a/extended/native-image/hello-awtfree-link/build.gradle.kts b/extended/native-image/hello-awtfree-link/build.gradle.kts index cb76c0514bb..3cc805c4a0d 100644 --- a/extended/native-image/hello-awtfree-link/build.gradle.kts +++ b/extended/native-image/hello-awtfree-link/build.gradle.kts @@ -30,7 +30,6 @@ configurations.all { resolutionStrategy.capabilitiesResolution.withCapability("org.jetbrains.skiko:skiko-awt") { select("org.thisisthepy.compose.skiko:skiko-awt:1.11.1-ext.1") } - } } compose.desktop.application { diff --git a/extended/native-image/hello-awtfree/build.gradle.kts b/extended/native-image/hello-awtfree/build.gradle.kts index dc63bf74009..30be4a796ac 100644 --- a/extended/native-image/hello-awtfree/build.gradle.kts +++ b/extended/native-image/hello-awtfree/build.gradle.kts @@ -40,7 +40,6 @@ configurations.all { resolutionStrategy.capabilitiesResolution.withCapability("org.jetbrains.skiko:skiko-awt") { select("org.thisisthepy.compose.skiko:skiko-awt:1.11.1-ext.1") } - } } compose.desktop.application { From 50c23f51178ecfd7bf74a99e67592ddb006d45b9 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:45:07 +0900 Subject: [PATCH 9/9] Fix: Consolidate AwtFree sample content into main source directory with symlinks --- .../hello-awtfree-kn/build.gradle.kts | 2 - .../src/macosMain/kotlin/Content.kt | 1 + .../src/macosMain/kotlin/TextSelfCheck.kt | 1 + .../hello-awtfree/build.gradle.kts | 3 - .../{shared => main}/kotlin/TextSelfCheck.kt | 0 .../src/shared/kotlin/Content.kt | 70 ------------------- 6 files changed, 2 insertions(+), 75 deletions(-) create mode 120000 extended/native-desktop/hello-awtfree-kn/src/macosMain/kotlin/Content.kt create mode 120000 extended/native-desktop/hello-awtfree-kn/src/macosMain/kotlin/TextSelfCheck.kt rename extended/native-image/hello-awtfree/src/{shared => main}/kotlin/TextSelfCheck.kt (100%) delete mode 100644 extended/native-image/hello-awtfree/src/shared/kotlin/Content.kt diff --git a/extended/native-desktop/hello-awtfree-kn/build.gradle.kts b/extended/native-desktop/hello-awtfree-kn/build.gradle.kts index 533fcdfdb87..59afa01f43f 100644 --- a/extended/native-desktop/hello-awtfree-kn/build.gradle.kts +++ b/extended/native-desktop/hello-awtfree-kn/build.gradle.kts @@ -26,8 +26,6 @@ kotlin { } } sourceSets.macosMain { - // The content and the text check hello-awtfree draws and runs. - kotlin.srcDir("../../native-image/hello-awtfree/src/shared/kotlin") dependencies { implementation("org.thisisthepy.compose.window:common:$window") implementation("org.thisisthepy.compose.window:native-macos:$window") diff --git a/extended/native-desktop/hello-awtfree-kn/src/macosMain/kotlin/Content.kt b/extended/native-desktop/hello-awtfree-kn/src/macosMain/kotlin/Content.kt new file mode 120000 index 00000000000..ce134cc3cbd --- /dev/null +++ b/extended/native-desktop/hello-awtfree-kn/src/macosMain/kotlin/Content.kt @@ -0,0 +1 @@ +../../../../../native-image/hello-awtfree/src/main/kotlin/Content.kt \ No newline at end of file diff --git a/extended/native-desktop/hello-awtfree-kn/src/macosMain/kotlin/TextSelfCheck.kt b/extended/native-desktop/hello-awtfree-kn/src/macosMain/kotlin/TextSelfCheck.kt new file mode 120000 index 00000000000..c672265666f --- /dev/null +++ b/extended/native-desktop/hello-awtfree-kn/src/macosMain/kotlin/TextSelfCheck.kt @@ -0,0 +1 @@ +../../../../../native-image/hello-awtfree/src/main/kotlin/TextSelfCheck.kt \ No newline at end of file diff --git a/extended/native-image/hello-awtfree/build.gradle.kts b/extended/native-image/hello-awtfree/build.gradle.kts index 30be4a796ac..65664739b85 100644 --- a/extended/native-image/hello-awtfree/build.gradle.kts +++ b/extended/native-image/hello-awtfree/build.gradle.kts @@ -15,9 +15,6 @@ val windowArtifact = if (macos) "graalvm-macos" else "graalvm-linux" sourceSets.main { kotlin.srcDir(if (macos) "src/macos/kotlin" else "src/linux/kotlin") - // The content and the text check, shared with the Kotlin/Native sample - // (extended/native-desktop/hello-awtfree-kn). - kotlin.srcDir("src/shared/kotlin") } dependencies { diff --git a/extended/native-image/hello-awtfree/src/shared/kotlin/TextSelfCheck.kt b/extended/native-image/hello-awtfree/src/main/kotlin/TextSelfCheck.kt similarity index 100% rename from extended/native-image/hello-awtfree/src/shared/kotlin/TextSelfCheck.kt rename to extended/native-image/hello-awtfree/src/main/kotlin/TextSelfCheck.kt diff --git a/extended/native-image/hello-awtfree/src/shared/kotlin/Content.kt b/extended/native-image/hello-awtfree/src/shared/kotlin/Content.kt deleted file mode 100644 index cba666209e9..00000000000 --- a/extended/native-image/hello-awtfree/src/shared/kotlin/Content.kt +++ /dev/null @@ -1,70 +0,0 @@ -package awtfree - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.text.BasicText -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp - -// The content both AWT-free samples draw: hello-awtfree in the GraalVM window, and -// extended/native-desktop/hello-awtfree-kn in the Kotlin/Native one. One source, so both -// executables are checked against the same scene. - -/** The field's and the button's names, as a screen reader is told them. */ -const val FIELD_LABEL = "Your name" -const val BUTTON_LABEL = "click me" - -/** - * Text, a field and a control that changes when it is clicked. Text needs a font manager, a - * shaper and a layout pass, the field needs the platform's text input, and the control shows - * that pointer input reaches the scene. - */ -@Composable -fun Content() { - var clicks by remember { mutableStateOf(0) } - var typed by remember { mutableStateOf("") } - Box(Modifier.fillMaxSize().background(Color(0xFF12321A))) { - Column(Modifier.padding(top = 32.dp, start = 24.dp)) { - BasicText("안녕하세요, no AWT here", style = TextStyle(color = Color.White, fontSize = 24.sp)) - BasicText( - "composed by Compose, painted by Skia, shown by a window of our own", - style = TextStyle(color = Color(0xFF9CCC9C), fontSize = 14.sp), - ) - BasicTextField( - value = typed, - onValueChange = { typed = it }, - // What a screen reader calls the field; the self-check looks for it by this name. - modifier = Modifier.padding(top = 16.dp).size(260.dp, 32.dp).background(Color(0xFF1E4620)) - .semantics { contentDescription = FIELD_LABEL }, - textStyle = TextStyle(color = Color.White, fontSize = 16.sp), - cursorBrush = SolidColor(Color.White), - ) - Box( - Modifier.padding(top = 24.dp).size(220.dp, 56.dp).background(Color(0xFF2E7D32)).clickable { clicks++ }, - ) { - BasicText( - if (clicks == 0) BUTTON_LABEL else "clicked $clicks", - Modifier.padding(16.dp), - style = TextStyle(color = Color.White, fontSize = 18.sp), - ) - } - } - } -}