diff --git a/README.md b/README.md index 58f8aa1..cb1b019 100644 --- a/README.md +++ b/README.md @@ -334,7 +334,7 @@ Kick.overlay.set("isWsConnected", true) You can also show/hide the panel programmatically if needed: ```kotlin -Kick.overlay.show(context) // show floating panel +Kick.overlay.show() // show floating panel Kick.overlay.hide() // hide it ``` @@ -348,6 +348,28 @@ Kick.overlay.set("fps", 42, "Performance") Kick.overlay.set("isWsConnected", true, "Network") ``` +#### Providers + +Overlay modules can populate categories automatically through `OverlayProvider`s. By default `OverlayModule` registers the built-in `PerformanceOverlayProvider`, which exposes CPU and memory usage in the "Performance" category whenever the floating panel is visible. + +Pass custom providers to `OverlayModule` to emit additional metrics: + +```kotlin +Kick.init(context) { + module( + OverlayModule( + context = context, + providers = listOf( + PerformanceOverlayProvider(), + MyCustomOverlayProvider(), // implements OverlayProvider + ), + ), + ) +} +``` + +Implement `OverlayProvider` to decide when your provider should run, which categories it contributes to, and how it updates values via `Kick.overlay.set` inside the supplied coroutine scope. + ### Advanced Module Configuration You don't need to add all the available modules. Just include the ones you need. Here only logging and network inspection are enabled: diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 64d051f..89781d5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,6 +44,8 @@ room-driver = { module = "androidx.sqlite:sqlite-bundled", version.ref = "room-d napier = { module = "io.github.aakira:napier", version.ref = "napier" } settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatform-settings" } settings-noArg = { module = "com.russhwolf:multiplatform-settings-no-arg", version.ref = "multiplatform-settings" } +settings-make-observable = { module = "com.russhwolf:multiplatform-settings-make-observable", version.ref = "multiplatform-settings" } +settings-coroutines = { module = "com.russhwolf:multiplatform-settings-coroutines", version.ref = "multiplatform-settings" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } diff --git a/kotlin-js-store/wasm/yarn.lock b/kotlin-js-store/wasm/yarn.lock new file mode 100644 index 0000000..5f0c8fa --- /dev/null +++ b/kotlin-js-store/wasm/yarn.lock @@ -0,0 +1,285 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@cashapp/sqldelight-sqljs-worker@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@cashapp/sqldelight-sqljs-worker/-/sqldelight-sqljs-worker-2.1.0.tgz#4ab898698aca9487f47fc9a42107c606c3ce81c5" + integrity sha512-odvBljb1rUOCk3UUZgjdiAChEohYI4Fy6Tj3NUy3l6u3WV/we+tjDTJ/kC25CJKD4pv0ZlH5AL1sKsZ5clKCew== + +"@js-joda/core@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273" + integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg== + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@types/json-schema@^7.0.8": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +copy-webpack-plugin@9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-9.1.0.tgz#2d2c460c4c4695ec0a58afb2801a1205256c4e6b" + integrity sha512-rxnR7PaGigJzhqETHGmAcxKnLZSR5u1Y3/bcIv/1FnqXedcL/E2ewK7ZCNrArJKCiSv8yVXhTqetJh8inDvfsA== + dependencies: + fast-glob "^3.2.7" + glob-parent "^6.0.1" + globby "^11.0.3" + normalize-path "^3.0.0" + schema-utils "^3.1.1" + serialize-javascript "^6.0.0" + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.7, fast-glob@^3.2.9: + version "3.3.3" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.8" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fastq@^1.6.0: + version "1.19.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5" + integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ== + dependencies: + reusify "^1.0.4" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +format-util@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271" + integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg== + +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +globby@^11.0.3: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +ignore@^5.2.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.1, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +reusify@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" + integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +schema-utils@^3.1.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +serialize-javascript@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +sql.js@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/sql.js/-/sql.js-1.8.0.tgz#cb45d957e17a2239662fe2f614c9b678990867a6" + integrity sha512-3HD8pSkZL+5YvYUI8nlvNILs61ALqq34xgmF+BHpqxe68yZIJ1H+sIVIODvni25+CcxHUxDyrTJUL0lE/m7afw== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +ws@8.18.3: + version "8.18.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" + integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== diff --git a/module/logging/overlay-stub/build.gradle.kts b/module/logging/overlay-stub/build.gradle.kts index 43088b0..9ee0a4c 100644 --- a/module/logging/overlay-stub/build.gradle.kts +++ b/module/logging/overlay-stub/build.gradle.kts @@ -49,6 +49,7 @@ kotlin { implementation(libs.decompose) implementation(libs.decompose.extensions.compose) implementation(libs.decompose.essenty.lifecycle.coroutines) + implementation(libs.kotlinx.coroutines.core) } commonTest.dependencies { implementation(libs.kotlin.test) diff --git a/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt index 0af7f72..d2286c9 100644 --- a/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt +++ b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt @@ -10,9 +10,13 @@ import ru.bartwell.kick.core.component.StubConfig import ru.bartwell.kick.core.data.Module import ru.bartwell.kick.core.data.ModuleDescription import ru.bartwell.kick.core.data.PlatformContext +import ru.bartwell.kick.module.overlay.core.provider.OverlayProvider -@Suppress("UnusedPrivateProperty", "EmptyFunctionBlock") -public class OverlayModule(context: PlatformContext) : Module { +@Suppress("UnusedPrivateProperty", "EmptyFunctionBlock", "UNUSED_PARAMETER") +public class OverlayModule( + context: PlatformContext, + providers: List = emptyList(), +) : Module { override val description: ModuleDescription = ModuleDescription.OVERLAY override val startConfig: Config = StubConfig(description) diff --git a/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt new file mode 100644 index 0000000..555c360 --- /dev/null +++ b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt @@ -0,0 +1,11 @@ +package ru.bartwell.kick.module.overlay.core.provider + +import kotlinx.coroutines.CoroutineScope + +public interface OverlayProvider { + public val categories: Set + + public fun start(scope: CoroutineScope) + + public fun stop() +} diff --git a/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt new file mode 100644 index 0000000..117ad51 --- /dev/null +++ b/module/logging/overlay-stub/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt @@ -0,0 +1,17 @@ +package ru.bartwell.kick.module.overlay.core.provider + +import kotlinx.coroutines.CoroutineScope + +public class PerformanceOverlayProvider : OverlayProvider { + override val categories: Set = setOf(CATEGORY) + + @Suppress("EmptyFunctionBlock") + override fun start(scope: CoroutineScope) {} + + @Suppress("EmptyFunctionBlock") + override fun stop() {} + + public companion object { + public const val CATEGORY: String = "Performance" + } +} diff --git a/module/logging/overlay/build.gradle.kts b/module/logging/overlay/build.gradle.kts index 5f04549..df2dd62 100644 --- a/module/logging/overlay/build.gradle.kts +++ b/module/logging/overlay/build.gradle.kts @@ -51,7 +51,10 @@ kotlin { implementation(libs.decompose) implementation(libs.decompose.extensions.compose) implementation(libs.decompose.essenty.lifecycle.coroutines) + implementation(libs.kotlinx.coroutines.core) implementation(libs.settings) + implementation(libs.settings.make.observable) + implementation(libs.settings.coroutines) implementation(libs.settings.noArg) } commonTest.dependencies { diff --git a/module/logging/overlay/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/overlay/OverlaySettingsCategoriesTest.kt b/module/logging/overlay/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/overlay/OverlaySettingsCategoriesTest.kt index 0fee8ea..c08d543 100644 --- a/module/logging/overlay/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/overlay/OverlaySettingsCategoriesTest.kt +++ b/module/logging/overlay/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/overlay/OverlaySettingsCategoriesTest.kt @@ -48,7 +48,7 @@ class OverlaySettingsCategoriesTest { component = DefaultOverlayComponent( componentContext = DefaultComponentContext(LifecycleRegistry()), onEnabledChangeCallback = {}, - onBackCallback = {} + onBackCallback = {}, ) activity.setContent { OverlayContent(component = component) } } diff --git a/module/logging/overlay/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/overlay/OverlayUiTest.kt b/module/logging/overlay/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/overlay/OverlayUiTest.kt index 62f9f4d..d106c68 100644 --- a/module/logging/overlay/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/overlay/OverlayUiTest.kt +++ b/module/logging/overlay/src/androidInstrumentedTest/kotlin/ru/bartwell/kick/module/overlay/OverlayUiTest.kt @@ -81,9 +81,9 @@ class OverlayUiTest { component = DefaultOverlayComponent( componentContext = DefaultComponentContext(LifecycleRegistry()), onEnabledChangeCallback = { enabled -> - if (enabled) KickOverlay.show(platformContext) else KickOverlay.hide() + if (enabled) KickOverlay.show() else KickOverlay.hide() }, - onBackCallback = {} + onBackCallback = {}, ) activity.setContent { OverlayContent(component = component) } } diff --git a/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuTimes.kt b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuTimes.kt new file mode 100644 index 0000000..7ba4a77 --- /dev/null +++ b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuTimes.kt @@ -0,0 +1,3 @@ +package ru.bartwell.kick.module.overlay.core.data + +internal data class CpuTimes(val idle: Long, val total: Long) diff --git a/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/data/MemoryInfo.kt b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/data/MemoryInfo.kt new file mode 100644 index 0000000..20eb026 --- /dev/null +++ b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/data/MemoryInfo.kt @@ -0,0 +1,3 @@ +package ru.bartwell.kick.module.overlay.core.data + +internal data class MemoryInfo(val used: Long?, val total: Long?) diff --git a/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.android.kt b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.android.kt index 9195140..9c8ab45 100644 --- a/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.android.kt +++ b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.android.kt @@ -20,7 +20,7 @@ public actual object KickOverlay { installed = true } - public actual fun show(context: PlatformContext) { + public actual fun show() { OverlaySettings.setEnabled(true) callbacks.currentActivity.get()?.let { callbacks.attach(it) } } diff --git a/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.android.kt b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.android.kt new file mode 100644 index 0000000..8d0194a --- /dev/null +++ b/module/logging/overlay/src/androidMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.android.kt @@ -0,0 +1,126 @@ +package ru.bartwell.kick.module.overlay.core.provider + +import android.os.Build +import android.os.Process +import android.os.SystemClock +import ru.bartwell.kick.module.overlay.core.data.CpuTimes +import ru.bartwell.kick.module.overlay.core.data.MemoryInfo +import java.io.File + +private const val PROC_STAT_PATH: String = "/proc/stat" +private const val PROC_MEMINFO_PATH: String = "/proc/meminfo" +private const val CPU_TOKEN_PREFIX: String = "cpu" +private const val CPU_MIN_TOKEN_COUNT: Int = 5 +private const val CPU_IDLE_INDEX: Int = 3 +private const val CPU_IOWAIT_INDEX: Int = 4 +private const val CPU_PERCENT_FACTOR: Double = 100.0 +private const val SPACE_DELIMITER: String = " " +private const val KEY_VALUE_DELIMITER: String = ":" +private const val KIB_IN_BYTES: Long = 1024L + +private var previousCpuTimes: CpuTimes? = null +private var lastAppCpuTimeMs = 0L +private var lastWallTimeMs = 0L + +internal actual fun readPerformanceSnapshot(): PerformanceSnapshot { + val cpuUsage = readCpuUsage() + val memoryInfo = readMemoryInfo() + return PerformanceSnapshot( + cpuUsagePercent = cpuUsage, + usedMemoryBytes = memoryInfo?.used, + totalMemoryBytes = memoryInfo?.total, + ) +} + +private fun readCpuUsage(): Double? { + var result: Double? = null + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val nowWall = SystemClock.uptimeMillis() + val nowCpu = Process.getElapsedCpuTime() + val prevWall = lastWallTimeMs + val prevCpu = lastAppCpuTimeMs + lastWallTimeMs = nowWall + lastAppCpuTimeMs = nowCpu + + if (prevWall != 0L) { + val deltaWall = (nowWall - prevWall).coerceAtLeast(1L) + val deltaCpu = (nowCpu - prevCpu).coerceAtLeast(0L) + val cores = Runtime.getRuntime().availableProcessors().coerceAtLeast(1) + val percent = deltaCpu.toDouble() / (deltaWall.toDouble() * cores) * CPU_PERCENT_FACTOR + result = percent.coerceIn(0.0, CPU_PERCENT_FACTOR) + } + } else { + result = runCatching { + val header = run { + var line: String? = null + File(PROC_STAT_PATH).useLines { seq -> + line = seq.firstOrNull { it.startsWith("$CPU_TOKEN_PREFIX ") } + } + line + } ?: return@runCatching null + + val tokens = header + .trim() + .split(Regex("\\s+")) + .takeIf { it.size >= CPU_MIN_TOKEN_COUNT && it.first() == CPU_TOKEN_PREFIX } + ?.drop(1) + ?.mapNotNull(String::toLongOrNull) + ?: return@runCatching null + + val idle = (tokens.getOrNull(CPU_IDLE_INDEX) ?: 0L) + + (tokens.getOrNull(CPU_IOWAIT_INDEX) ?: 0L) + val total = tokens.sum() + + val prev = previousCpuTimes + val current = CpuTimes(idle = idle, total = total) + previousCpuTimes = current + + if (prev != null) { + val deltaIdle = idle - prev.idle + val deltaTotal = total - prev.total + if (deltaTotal > 0) { + (1.0 - deltaIdle.toDouble() / deltaTotal.toDouble()) * CPU_PERCENT_FACTOR + } else { + null + } + } else { + (total - idle).toDouble() / total.toDouble() * CPU_PERCENT_FACTOR + } + }.getOrNull() + } + + return result +} + +private fun readMemoryInfo(): MemoryInfo? = runCatching { + val memInfo = File(PROC_MEMINFO_PATH) + if (!memInfo.exists()) { + return@runCatching null + } + + val values = mutableMapOf() + memInfo.useLines { sequence -> + sequence.forEach { line -> + val parts = line.split(KEY_VALUE_DELIMITER, limit = 2) + if (parts.size == 2) { + val key = parts[0].trim() + val value = parts[1].trim().split(SPACE_DELIMITER).firstOrNull()?.toLongOrNull() + if (value != null) { + values[key] = value * KIB_IN_BYTES + } + } + } + } + + val total = values["MemTotal"] + val available = values["MemAvailable"] ?: run { + val free = values["MemFree"] ?: 0L + val buffers = values["Buffers"] ?: 0L + val cached = values["Cached"] ?: 0L + free + buffers + cached + } + + val used = if (total != null) total - available else null + MemoryInfo(used = used, total = total) +}.getOrNull() diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/Kick.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/Kick.kt index c78ba83..b0fe0ef 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/Kick.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/Kick.kt @@ -1,7 +1,6 @@ package ru.bartwell.kick.module.overlay import ru.bartwell.kick.Kick -import ru.bartwell.kick.core.data.PlatformContext import ru.bartwell.kick.module.overlay.core.overlay.KickOverlay import ru.bartwell.kick.module.overlay.core.store.OverlayStore @@ -27,6 +26,6 @@ public object OverlayAccessor { public fun set(key: String, value: String, category: String) { OverlayStore.set(key, value, category) } public fun set(key: String, value: Boolean, category: String) { OverlayStore.set(key, value.toString(), category) } - public fun show(context: PlatformContext) { KickOverlay.show(context) } + public fun show() { KickOverlay.show() } public fun hide() { KickOverlay.hide() } } diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt index 6f416ae..77fa5f9 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/OverlayModule.kt @@ -6,6 +6,11 @@ import androidx.compose.ui.Modifier import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.pop +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn import kotlinx.serialization.modules.PolymorphicModuleBuilder import ru.bartwell.kick.core.component.Child import ru.bartwell.kick.core.component.Config @@ -16,22 +21,47 @@ import ru.bartwell.kick.module.overlay.core.component.child.OverlayChild import ru.bartwell.kick.module.overlay.core.component.config.OverlayConfig import ru.bartwell.kick.module.overlay.core.overlay.KickOverlay import ru.bartwell.kick.module.overlay.core.persists.OverlaySettings +import ru.bartwell.kick.module.overlay.core.provider.OverlayProvider +import ru.bartwell.kick.module.overlay.core.provider.PerformanceOverlayProvider import ru.bartwell.kick.module.overlay.core.store.OverlayStore import ru.bartwell.kick.module.overlay.feature.settings.presentation.DefaultOverlayComponent import ru.bartwell.kick.module.overlay.feature.settings.presentation.OverlayContent -public class OverlayModule(private val context: PlatformContext) : Module { +public class OverlayModule( + context: PlatformContext, + private val providers: List = listOf(PerformanceOverlayProvider()), +) : Module { override val description: ModuleDescription = ModuleDescription.OVERLAY override val startConfig: Config = OverlayConfig + private val providerScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + init { OverlaySettings(context) - // Initialize selected category from persisted settings + observeFloatingWindowState() OverlayStore.selectCategory(OverlaySettings.getSelectedCategory()) KickOverlay.init(context) if (OverlaySettings.isEnabled()) { - KickOverlay.show(context) + KickOverlay.show() + } + } + + private fun observeFloatingWindowState() { + combine(OverlaySettings.observeEnabled(), OverlayStore.selectedCategory) { isWindowEnabled, currentCategory -> + providers.forEach { provider -> + provider.categories.forEach(OverlayStore::addCategory) + + val shouldStart = isWindowEnabled && provider.isAvailable && + provider.categories.any { it == currentCategory } + + if (shouldStart) { + provider.start(providerScope) + } else { + provider.stop() + } + } } + .launchIn(providerScope) } override fun getComponent( @@ -42,7 +72,8 @@ public class OverlayModule(private val context: PlatformContext) : Module { OverlayChild( DefaultOverlayComponent( componentContext = componentContext, - onEnabledChangeCallback = { enabled -> if (enabled) KickOverlay.show(context) else KickOverlay.hide() }, + providers = providers, + onEnabledChangeCallback = { enabled -> if (enabled) KickOverlay.show() else KickOverlay.hide() }, onBackCallback = { nav.pop() }, ) ) diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.kt index 9680358..1f87acf 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.kt @@ -4,6 +4,6 @@ import ru.bartwell.kick.core.data.PlatformContext public expect object KickOverlay { public fun init(context: PlatformContext) - public fun show(context: PlatformContext) + public fun show() public fun hide() } diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/OverlayWindow.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/OverlayWindow.kt index 345376b..a9e0fff 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/OverlayWindow.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/OverlayWindow.kt @@ -29,7 +29,7 @@ import androidx.compose.ui.unit.dp import ru.bartwell.kick.module.overlay.core.store.OverlayStore @Composable -internal fun OverlayWindow(onCloseClick: () -> Unit, measureFull: Boolean = false) { +internal fun OverlayWindow(onCloseClick: () -> Unit) { val lines by OverlayStore.items.collectAsState() val shape = RectangleShape @@ -48,11 +48,6 @@ internal fun OverlayWindow(onCloseClick: () -> Unit, measureFull: Boolean = fals ) { Spacer(Modifier.height(2.dp)) lines.forEach { (k, v) -> - val (ml, sw, of) = if (measureFull) { - Triple(Int.MAX_VALUE, false, TextOverflow.Clip) - } else { - Triple(1, false, TextOverflow.Ellipsis) - } Text( text = "$k: $v", style = MaterialTheme.typography.bodySmall, diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/persists/OverlaySettings.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/persists/OverlaySettings.kt index 3e251cf..e4bf415 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/persists/OverlaySettings.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/persists/OverlaySettings.kt @@ -1,20 +1,29 @@ package ru.bartwell.kick.module.overlay.core.persists -import com.russhwolf.settings.Settings +import com.russhwolf.settings.ExperimentalSettingsApi +import com.russhwolf.settings.ObservableSettings +import com.russhwolf.settings.coroutines.getBooleanFlow +import com.russhwolf.settings.observable.makeObservable +import kotlinx.coroutines.flow.Flow import ru.bartwell.kick.core.data.PlatformContext import ru.bartwell.kick.module.overlay.core.store.DEFAULT_CATEGORY internal object OverlaySettings { - private lateinit var settings: Settings + private lateinit var settings: ObservableSettings private const val KEY_ENABLED = "enabled" private const val KEY_SELECTED_CATEGORY = "selected_category" + @OptIn(ExperimentalSettingsApi::class) operator fun invoke(context: PlatformContext) { settings = PlatformSettingsFactory.create(context = context, name = "kick_overlay_prefs") + .makeObservable() } fun isEnabled(): Boolean = settings.getBoolean(KEY_ENABLED, false) + @OptIn(ExperimentalSettingsApi::class) + fun observeEnabled(): Flow = settings.getBooleanFlow(KEY_ENABLED, false) + fun setEnabled(value: Boolean) { settings.putBoolean(KEY_ENABLED, value) } diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt new file mode 100644 index 0000000..fcfcba2 --- /dev/null +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/OverlayProvider.kt @@ -0,0 +1,18 @@ +package ru.bartwell.kick.module.overlay.core.provider + +import kotlinx.coroutines.CoroutineScope + +public interface OverlayProvider { + + /** + * Categories + * List all of categories used by provider. It is important for correct start/stop provider. + */ + public val categories: Set + + public val isAvailable: Boolean + + public fun start(scope: CoroutineScope) + + public fun stop() +} diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt new file mode 100644 index 0000000..c42f2ad --- /dev/null +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceOverlayProvider.kt @@ -0,0 +1,111 @@ +package ru.bartwell.kick.module.overlay.core.provider + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import ru.bartwell.kick.Kick +import ru.bartwell.kick.core.data.Platform +import ru.bartwell.kick.core.util.PlatformUtils +import ru.bartwell.kick.module.overlay.overlay +import kotlin.math.roundToInt +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +private const val PERCENT_PRECISION_MULTIPLIER: Int = 10 +private const val PERCENT_PRECISION_DIVISOR: Double = 10.0 +private const val MIN_PERCENT: Double = 0.0 +private const val MAX_PERCENT: Double = 100.0 +private const val DECIMAL_SEPARATOR: String = "." +private const val DEFAULT_DECIMAL_SUFFIX: String = ".0" +private const val UNIT_STEP: Double = 1024.0 +public const val CATEGORY: String = "Performance" +private const val CPU_USAGE_KEY: String = "CPU" +private const val MEMORY_USAGE_KEY: String = "RAM" +private const val NOT_AVAILABLE_VALUE: String = "—" +private val BYTE_UNITS = arrayOf("B", "KB", "MB", "GB", "TB", "PB") + +public class PerformanceOverlayProvider( + private val updateIntervalMillis: Duration = 1.seconds, +) : OverlayProvider { + + override val categories: Set = setOf(CATEGORY) + override val isAvailable: Boolean + get() = PlatformUtils.getPlatform() != Platform.WEB + private var job: Job? = null + + override fun start(scope: CoroutineScope) { + Kick.overlay.set(CPU_USAGE_KEY, NOT_AVAILABLE_VALUE, CATEGORY) + Kick.overlay.set(MEMORY_USAGE_KEY, NOT_AVAILABLE_VALUE, CATEGORY) + + job = scope.launch { + while (isActive) { + val snapshot = readPerformanceSnapshot() + Kick.overlay.set( + key = CPU_USAGE_KEY, + value = snapshot.cpuUsagePercent?.let(::formatPercent) ?: NOT_AVAILABLE_VALUE, + category = CATEGORY, + ) + Kick.overlay.set( + key = MEMORY_USAGE_KEY, + value = formatMemory(snapshot), + category = CATEGORY, + ) + delay(updateIntervalMillis) + } + } + } + + override fun stop() { + job?.cancel() + job = null + } + + private fun formatPercent(value: Double): String { + val normalized = if (!value.isFinite()) { + return NOT_AVAILABLE_VALUE + } else { + ((value * PERCENT_PRECISION_MULTIPLIER).roundToInt() / PERCENT_PRECISION_DIVISOR) + .coerceIn(MIN_PERCENT, MAX_PERCENT) + } + + val displayValue = normalized.takeIf { it >= 0 } ?: 0.0 + val formatted = displayValue.toString() + return if (formatted.contains(DECIMAL_SEPARATOR)) "$formatted %" else "$formatted$DEFAULT_DECIMAL_SUFFIX %" + } + + private fun formatMemory(snapshot: PerformanceSnapshot): String { + val used = snapshot.usedMemoryBytes + val total = snapshot.totalMemoryBytes + + return when { + used != null && total != null -> "${formatBytes(used)} / ${formatBytes(total)}" + used != null -> formatBytes(used) + total != null -> formatBytes(total) + else -> NOT_AVAILABLE_VALUE + } + } + + private fun formatBytes(value: Long): String { + if (value <= 0L) return "0 B" + + var unitIndex = 0 + var remaining = value.toDouble() + while (remaining >= UNIT_STEP && unitIndex < BYTE_UNITS.lastIndex) { + remaining /= UNIT_STEP + unitIndex++ + } + + val rounded = (remaining * PERCENT_PRECISION_MULTIPLIER).roundToInt() / PERCENT_PRECISION_DIVISOR + val normalized = if (rounded % 1.0 == 0.0) { + rounded.roundToInt().toString() + } else { + rounded.toString() + } + + return "$normalized ${BYTE_UNITS[unitIndex]}" + } + + private fun Double.isFinite(): Boolean = !isNaN() && !isInfinite() +} diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.kt new file mode 100644 index 0000000..aceb3d7 --- /dev/null +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.kt @@ -0,0 +1,9 @@ +package ru.bartwell.kick.module.overlay.core.provider + +internal data class PerformanceSnapshot( + val cpuUsagePercent: Double?, + val usedMemoryBytes: Long?, + val totalMemoryBytes: Long?, +) + +internal expect fun readPerformanceSnapshot(): PerformanceSnapshot diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/store/OverlayStore.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/store/OverlayStore.kt index ac46566..f563d4d 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/store/OverlayStore.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/core/store/OverlayStore.kt @@ -12,12 +12,17 @@ internal object OverlayStore { private val _items = MutableStateFlow>>(emptyList()) val items: StateFlow>> = _items.asStateFlow() - private val _categories = MutableStateFlow>(listOf(DEFAULT_CATEGORY)) + private val _categories = MutableStateFlow(listOf(DEFAULT_CATEGORY)) val categories: StateFlow> = _categories.asStateFlow() private val _selectedCategory = MutableStateFlow(DEFAULT_CATEGORY) val selectedCategory: StateFlow = _selectedCategory.asStateFlow() + internal fun addCategory(category: String) { + categoriesMap.getOrPut(category) { LinkedHashMap() } + updateCategoriesList(category) + } + fun set(key: String, value: String) { set(key = key, value = value, category = DEFAULT_CATEGORY) } diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/data/ProviderDescription.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/data/ProviderDescription.kt new file mode 100644 index 0000000..a3fa3eb --- /dev/null +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/data/ProviderDescription.kt @@ -0,0 +1,7 @@ +package ru.bartwell.kick.module.overlay.feature.settings.data + +internal data class ProviderDescription( + val name: String, + val categories: Set, + val isAvailable: Boolean, +) diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/DefaultOverlayComponent.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/DefaultOverlayComponent.kt index 2336670..ef5e217 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/DefaultOverlayComponent.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/DefaultOverlayComponent.kt @@ -4,23 +4,27 @@ import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.value.MutableValue import com.arkivanov.decompose.value.Value import ru.bartwell.kick.core.data.PlatformContext +import ru.bartwell.kick.core.util.PlatformUtils import ru.bartwell.kick.module.overlay.core.persists.OverlaySettings +import ru.bartwell.kick.module.overlay.core.provider.OverlayProvider import ru.bartwell.kick.module.overlay.core.store.OverlayStore +import ru.bartwell.kick.module.overlay.feature.settings.data.ProviderDescription internal class DefaultOverlayComponent( componentContext: ComponentContext, private val onEnabledChangeCallback: (Boolean) -> Unit, private val onBackCallback: () -> Unit, + providers: List, ) : OverlayComponent, ComponentContext by componentContext { - private val _model = MutableValue(OverlayState()) + private val _model = MutableValue(OverlayState(providers = providers.toDescriptions())) override val model: Value = _model override fun init(context: PlatformContext) { OverlaySettings(context) val enabled = OverlaySettings.isEnabled() val category = OverlaySettings.getSelectedCategory() - _model.value = OverlayState(enabled) + _model.value = model.value.copy(enabled = enabled) onEnabledChangeCallback(enabled) OverlayStore.selectCategory(category) } @@ -36,5 +40,21 @@ internal class DefaultOverlayComponent( override fun onCategoryChange(category: String) { OverlaySettings.setSelectedCategory(category) OverlayStore.selectCategory(category) + val unavailableProviders = model.value.providers.filter { it.categories.contains(category) && !it.isAvailable } + if (unavailableProviders.isEmpty()) { + _model.value = model.value.copy(warning = null) + } else { + val platform = PlatformUtils.getPlatform().name + val providersText = unavailableProviders.joinToString(", ") { it.name } + _model.value = model.value.copy(warning = "$providersText is not available on $platform") + } } } + +private fun List.toDescriptions(): List = map { provider -> + ProviderDescription( + name = provider::class.simpleName ?: "Unknown", + categories = provider.categories, + isAvailable = provider.isAvailable + ) +} diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/OverlayComponent.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/OverlayComponent.kt index de3a252..28ec68c 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/OverlayComponent.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/OverlayComponent.kt @@ -3,6 +3,7 @@ package ru.bartwell.kick.module.overlay.feature.settings.presentation import com.arkivanov.decompose.value.Value import ru.bartwell.kick.core.component.Component import ru.bartwell.kick.core.data.PlatformContext +import ru.bartwell.kick.module.overlay.feature.settings.data.ProviderDescription internal interface OverlayComponent : Component { val model: Value @@ -15,4 +16,6 @@ internal interface OverlayComponent : Component { internal data class OverlayState( val enabled: Boolean = false, + val providers: List, + val warning: String? = null, ) diff --git a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/OverlayContent.kt b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/OverlayContent.kt index b117889..202d170 100644 --- a/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/OverlayContent.kt +++ b/module/logging/overlay/src/commonMain/kotlin/ru/bartwell/kick/module/overlay/feature/settings/presentation/OverlayContent.kt @@ -85,6 +85,7 @@ internal fun OverlayContent( CategoryPicker( selectedCategory = selectedCategory, categories = categories, + warning = state.value.warning, onSelect = { component.onCategoryChange(it) } ) Spacer(modifier = Modifier.height(32.dp)) @@ -111,6 +112,7 @@ internal fun OverlayContent( private fun CategoryPicker( selectedCategory: String, categories: List, + warning: String?, onSelect: (String) -> Unit, ) { var expanded by rememberSaveable { mutableStateOf(false) } @@ -119,15 +121,17 @@ private fun CategoryPicker( onExpandedChange = { expanded = it }, ) { OutlinedTextField( - readOnly = true, + modifier = Modifier + .menuAnchor() + .padding(horizontal = 16.dp) + .fillMaxWidth(), value = selectedCategory, onValueChange = {}, + supportingText = { warning?.let { Text(text = it) } }, + isError = warning != null, label = { Text("Category") }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, - modifier = Modifier - .menuAnchor() - .padding(horizontal = 16.dp) - .fillMaxWidth() + readOnly = true, ) DropdownMenu( diff --git a/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuSample.kt b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuSample.kt new file mode 100644 index 0000000..9cea769 --- /dev/null +++ b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuSample.kt @@ -0,0 +1,3 @@ +package ru.bartwell.kick.module.overlay.core.data + +internal data class CpuSample(val user: ULong, val nice: ULong, val system: ULong, val idle: ULong) diff --git a/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuState.kt b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuState.kt new file mode 100644 index 0000000..0294420 --- /dev/null +++ b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/data/CpuState.kt @@ -0,0 +1,8 @@ +package ru.bartwell.kick.module.overlay.core.data + +import kotlin.native.concurrent.ThreadLocal + +@ThreadLocal +internal object CpuState { + var previous: CpuSample? = null +} diff --git a/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.ios.kt b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.ios.kt index 89ee249..0d853fb 100644 --- a/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.ios.kt +++ b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.ios.kt @@ -84,14 +84,14 @@ public actual object KickOverlay { queue = NSOperationQueue.mainQueue ) { _: NSNotification? -> if (OverlaySettings.isEnabled()) { - show(context) + show() } } } } } - public actual fun show(context: PlatformContext) { + public actual fun show() { dispatch_async(dispatch_get_main_queue()) { OverlaySettings.setEnabled(true) @@ -110,7 +110,7 @@ public actual object KickOverlay { queue = NSOperationQueue.mainQueue ) { _: NSNotification? -> if (overlayWindow == null && OverlaySettings.isEnabled()) { - show(context) + show() } windowObserver?.let { NSNotificationCenter.defaultCenter.removeObserver(it) } windowObserver = null diff --git a/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.ios.kt b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.ios.kt new file mode 100644 index 0000000..5589fd9 --- /dev/null +++ b/module/logging/overlay/src/iosMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.ios.kt @@ -0,0 +1,111 @@ +@file:OptIn(ExperimentalForeignApi::class) + +package ru.bartwell.kick.module.overlay.core.provider + +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.UIntVar +import kotlinx.cinterop.alloc +import kotlinx.cinterop.convert +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.plus +import kotlinx.cinterop.pointed +import kotlinx.cinterop.ptr +import kotlinx.cinterop.reinterpret +import kotlinx.cinterop.value +import platform.Foundation.NSProcessInfo +import platform.darwin.CPU_STATE_IDLE +import platform.darwin.CPU_STATE_NICE +import platform.darwin.CPU_STATE_SYSTEM +import platform.darwin.CPU_STATE_USER +import platform.darwin.HOST_CPU_LOAD_INFO +import platform.darwin.HOST_CPU_LOAD_INFO_COUNT +import platform.darwin.KERN_SUCCESS +import platform.darwin.MACH_TASK_BASIC_INFO +import platform.darwin.MACH_TASK_BASIC_INFO_COUNT +import platform.darwin.host_cpu_load_info_data_t +import platform.darwin.host_statistics +import platform.darwin.mach_host_self +import platform.darwin.mach_msg_type_number_tVar +import platform.darwin.mach_task_basic_info +import platform.darwin.mach_task_self_ +import platform.darwin.task_info +import ru.bartwell.kick.module.overlay.core.data.CpuSample +import ru.bartwell.kick.module.overlay.core.data.CpuState + +private const val PERCENT_FACTOR: Double = 100.0 + +internal actual fun readPerformanceSnapshot(): PerformanceSnapshot { + val cpu = readCpuUsage() + val memory = readMemoryUsage() + + return PerformanceSnapshot( + cpuUsagePercent = cpu, + usedMemoryBytes = memory?.first, + totalMemoryBytes = memory?.second, + ) +} + +private fun readCpuUsage(): Double? = memScoped { + val cpuInfo = alloc() + val count = alloc().apply { value = HOST_CPU_LOAD_INFO_COUNT } + + val result = host_statistics( + mach_host_self(), + HOST_CPU_LOAD_INFO, + cpuInfo.ptr.reinterpret(), + count.ptr, + ) + + if (result != KERN_SUCCESS) { + return null + } + + val ticksPtr: CPointer = cpuInfo.cpu_ticks + + fun tick(cpuStateIndex: Int) = (ticksPtr + cpuStateIndex)!!.pointed.value.toULong() + + val sample = CpuSample( + user = tick(CPU_STATE_USER), + nice = tick(CPU_STATE_NICE), + system = tick(CPU_STATE_SYSTEM), + idle = tick(CPU_STATE_IDLE), + ) + + val previousSample = CpuState.previous + CpuState.previous = sample + + if (previousSample == null) { + return null + } + + val userDelta = sample.activeTicks() - previousSample.activeTicks() + val totalDelta = userDelta + (sample.idle - previousSample.idle) + + if (totalDelta.toLong() <= 0L) { + return null + } + + userDelta.toDouble() / totalDelta.toDouble() * PERCENT_FACTOR +} + +private fun readMemoryUsage(): Pair? = memScoped { + val count = alloc().apply { + value = MACH_TASK_BASIC_INFO_COUNT.convert() + } + val info = alloc() + + val result = task_info( + target_task = mach_task_self_, + flavor = MACH_TASK_BASIC_INFO.toUInt(), + task_info_out = info.ptr.reinterpret(), + task_info_outCnt = count.ptr, + ) + + val used = if (result == KERN_SUCCESS) info.resident_size.toLong() else null + val total = NSProcessInfo.processInfo.physicalMemory.takeIf { it > 0uL }?.toLong() + + used to total +} + +private fun CpuSample.activeTicks(): ULong = user + nice + system diff --git a/module/logging/overlay/src/jvmMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.jvm.kt b/module/logging/overlay/src/jvmMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.jvm.kt index 4395384..56ea008 100644 --- a/module/logging/overlay/src/jvmMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.jvm.kt +++ b/module/logging/overlay/src/jvmMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.jvm.kt @@ -29,7 +29,7 @@ public actual object KickOverlay { @Suppress("EmptyFunctionBlock") public actual fun init(context: PlatformContext) {} - public actual fun show(context: PlatformContext) { + public actual fun show() { if (window != null) { window!!.isVisible = true window!!.toFront() diff --git a/module/logging/overlay/src/jvmMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.jvm.kt b/module/logging/overlay/src/jvmMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.jvm.kt new file mode 100644 index 0000000..9e3ce88 --- /dev/null +++ b/module/logging/overlay/src/jvmMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.jvm.kt @@ -0,0 +1,22 @@ +package ru.bartwell.kick.module.overlay.core.provider + +import com.sun.management.OperatingSystemMXBean +import java.lang.management.ManagementFactory + +private const val PERCENT_FACTOR: Double = 100.0 + +private val osBean: OperatingSystemMXBean? = + ManagementFactory.getOperatingSystemMXBean() as? OperatingSystemMXBean + +internal actual fun readPerformanceSnapshot(): PerformanceSnapshot { + val cpu = osBean?.systemCpuLoad?.takeIf { it >= 0 }?.let { it * PERCENT_FACTOR } + val totalMemory = osBean?.totalPhysicalMemorySize?.takeIf { it > 0 } + val freeMemory = osBean?.freePhysicalMemorySize?.takeIf { it >= 0 } + val usedMemory = if (totalMemory != null && freeMemory != null) totalMemory - freeMemory else null + + return PerformanceSnapshot( + cpuUsagePercent = cpu, + usedMemoryBytes = usedMemory, + totalMemoryBytes = totalMemory, + ) +} diff --git a/module/logging/overlay/src/jvmTest/kotlin/ru/bartwell/kick/module/overlay/OverlayCategoriesJvmTest.kt b/module/logging/overlay/src/jvmTest/kotlin/ru/bartwell/kick/module/overlay/OverlayCategoriesJvmTest.kt index 3eb029a..20d9497 100644 --- a/module/logging/overlay/src/jvmTest/kotlin/ru/bartwell/kick/module/overlay/OverlayCategoriesJvmTest.kt +++ b/module/logging/overlay/src/jvmTest/kotlin/ru/bartwell/kick/module/overlay/OverlayCategoriesJvmTest.kt @@ -33,16 +33,17 @@ class OverlayCategoriesJvmTest { @Test fun set_withCategory_isIsolated_perCategory_and_switchingUpdatesItems() { + val category = "Performance" OverlayStore.set("k1", "v1") // Default - OverlayStore.set("k2", "v2", "Perf") + OverlayStore.set("k2", "v2", category) // Still on Default assertEquals(DEFAULT_CATEGORY, OverlayStore.selectedCategory.value) assertEquals(listOf("k1" to "v1"), OverlayStore.items.value) - assertTrue(OverlayStore.categories.value.containsAll(listOf(DEFAULT_CATEGORY, "Perf"))) + assertTrue(OverlayStore.categories.value.containsAll(listOf(DEFAULT_CATEGORY, category))) - // Switch to Perf - OverlayStore.selectCategory("Perf") + // Switch to the declared category + OverlayStore.selectCategory(category) assertEquals(listOf("k2" to "v2"), OverlayStore.items.value) } @@ -52,4 +53,24 @@ class OverlayCategoriesJvmTest { assertTrue(OverlayStore.categories.value.contains("NewCat")) assertTrue(OverlayStore.items.value.isEmpty()) } + + @Test + fun set_withEmbeddedCategoryPrefix_routesToCategory_and_usesNormalizedKey() { + val category = "Performance" + val key = "$category::fps" + + OverlayStore.set(key, "60") + + OverlayStore.selectCategory(category) + assertEquals(listOf("fps" to "60"), OverlayStore.items.value) + } + + @Test + fun declareCategories_addsThemToCategoriesList() { + val categories = listOf("Performance", "Analytics") + + OverlayStore.declareCategories(categories) + + assertTrue(OverlayStore.categories.value.containsAll(categories + DEFAULT_CATEGORY)) + } } diff --git a/module/logging/overlay/src/jvmTest/kotlin/ru/bartwell/kick/module/overlay/OverlayJvmTest.kt b/module/logging/overlay/src/jvmTest/kotlin/ru/bartwell/kick/module/overlay/OverlayJvmTest.kt index 079ed9e..77303f2 100644 --- a/module/logging/overlay/src/jvmTest/kotlin/ru/bartwell/kick/module/overlay/OverlayJvmTest.kt +++ b/module/logging/overlay/src/jvmTest/kotlin/ru/bartwell/kick/module/overlay/OverlayJvmTest.kt @@ -24,7 +24,7 @@ class OverlayJvmTest { // Show KickOverlay.init(ctx) - KickOverlay.show(ctx) + KickOverlay.show() // Wait up to 2 seconds for window to appear run { val start = System.currentTimeMillis() diff --git a/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/AutosizeMeasure.wasmJs.kt b/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/AutosizeMeasure.wasmJs.kt deleted file mode 100644 index 812cc2b..0000000 --- a/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/AutosizeMeasure.wasmJs.kt +++ /dev/null @@ -1,43 +0,0 @@ -package ru.bartwell.kick.module.overlay.core.overlay - -import androidx.compose.runtime.Composable -import androidx.compose.ui.layout.SubcomposeLayout -import androidx.compose.ui.unit.IntSize - -@Composable -internal fun AutosizeMeasure( - onSizes: (desired: IntSize, actual: IntSize) -> Unit, - desiredContent: @Composable () -> Unit, - actualContent: @Composable () -> Unit, -) { - SubcomposeLayout { constraints -> - val desiredPlaceables = subcompose("desired", desiredContent).map { - it.measure( - constraints.copy( - minWidth = 0, - maxWidth = Int.MAX_VALUE, - minHeight = 0, - maxHeight = Int.MAX_VALUE - ) - ) - } - val desiredW = desiredPlaceables.maxOfOrNull { it.width } ?: 0 - val desiredH = desiredPlaceables.sumOf { it.height }.coerceAtLeast(0) - val desired = IntSize(desiredW, desiredH) - - val fixed = constraints.copy( - minWidth = desiredW, - maxWidth = desiredW, - minHeight = desiredH, - maxHeight = desiredH - ) - val actualPlaceables = subcompose("actual", actualContent).map { it.measure(fixed) } - val actual = IntSize(desiredW, desiredH) - - onSizes(desired, actual) - - layout(desiredW, desiredH) { - actualPlaceables.forEach { it.place(0, 0) } - } - } -} diff --git a/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.wasmJs.kt b/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.wasmJs.kt index 1650e26..7835a16 100644 --- a/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.wasmJs.kt +++ b/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/KickOverlay.wasmJs.kt @@ -1,11 +1,19 @@ package ru.bartwell.kick.module.overlay.core.overlay -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.window.ComposeViewport import kotlinx.browser.document +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import org.w3c.dom.HTMLElement +import org.w3c.dom.HTMLSpanElement +import org.w3c.dom.events.Event +import org.w3c.dom.events.MouseEvent import ru.bartwell.kick.core.data.PlatformContext import ru.bartwell.kick.module.overlay.core.persists.OverlaySettings +import ru.bartwell.kick.module.overlay.core.store.OverlayStore private const val INITIAL_WINDOW_X_PX = 50 private const val INITIAL_WINDOW_Y_PX = 200 @@ -13,16 +21,20 @@ private const val INITIAL_WINDOW_Y_PX = 200 @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") public actual object KickOverlay { private var overlayRoot: HTMLElement? = null + private var overlayLines: HTMLElement? = null + private var mouseMoveListener: ((Event) -> Unit)? = null + private var mouseUpListener: ((Event) -> Unit)? = null + private var scope: CoroutineScope? = null + private var itemsJob: Job? = null @Suppress("EmptyFunctionBlock") public actual fun init(context: PlatformContext) {} - @OptIn(ExperimentalComposeUiApi::class) - public actual fun show(context: PlatformContext) { - val existing = overlayRoot - if (existing != null) { - existing.style.display = "" - existing.style.visibility = "visible" + @Suppress("LongMethod", "StringLiteralDuplication") + public actual fun show() { + if (overlayRoot != null) { + overlayRoot!!.style.display = "" + overlayRoot!!.style.visibility = "visible" OverlaySettings.setEnabled(true) return } @@ -36,23 +48,73 @@ public actual object KickOverlay { top = "${INITIAL_WINDOW_Y_PX}px" left = "${INITIAL_WINDOW_X_PX}px" zIndex = "2147483647" - width = "180px" - height = "60px" - backgroundColor = "transparent" - borderRadius = "0px" setProperty("pointer-events", "auto") + setProperty("user-select", "none") + display = "inline-block" + // Auto-size to content + width = "max-content" + height = "max-content" + } + } + + val container = (document.createElement("div") as HTMLElement).apply { + id = "kick-overlay-content" + style.apply { + position = "relative" + backgroundColor = "white" + setProperty("border", "1px solid rgba(0,0,0,0.12)") + setProperty("box-shadow", "0 6px 24px rgba(0,0,0,0.15)") + borderRadius = "0px" + padding = "2px 16px 2px 6px" + color = "black" + fontFamily = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, " + + "Ubuntu, Cantarell, 'Helvetica Neue', Arial, sans-serif" + fontSize = "12px" + setProperty("line-height", "16px") + setProperty("white-space", "nowrap") + } + } + + val close = (document.createElement("span") as HTMLSpanElement).apply { + id = "kick-overlay-close" + textContent = "\u00D7" + style.apply { + position = "absolute" + right = "2px" + top = "2px" + width = "20px" + height = "20px" + textAlign = "center" + cursor = "pointer" + borderRadius = "10px" + setProperty("line-height", "16px") + setProperty("user-select", "none") } + addEventListener("click") { _ -> onCloseClicked() } + addEventListener("mousedown") { e -> e.stopPropagation() } } + + val lines = (document.createElement("div") as HTMLElement).apply { + id = "kick-overlay-lines" + style.apply { setProperty("white-space", "nowrap") } + } + + container.appendChild(close) + container.appendChild(lines) + root.appendChild(container) document.body?.appendChild(root) + overlayRoot = root + overlayLines = lines + // Drag handling var dragging = false var startX = 0.0 var startY = 0.0 var offsetX = 0.0 var offsetY = 0.0 - val onMouseDown: (org.w3c.dom.events.MouseEvent) -> Unit = { ev -> + val onMouseDown: (MouseEvent) -> Unit = { ev -> dragging = true startX = ev.clientX.toDouble() startY = ev.clientY.toDouble() @@ -60,7 +122,7 @@ public actual object KickOverlay { offsetY = root.offsetTop.toDouble() ev.preventDefault() } - val onMouseMove: (org.w3c.dom.events.MouseEvent) -> Unit = { ev -> + val onMouseMove: (MouseEvent) -> Unit = { ev -> if (dragging) { val dx = ev.clientX - startX val dy = ev.clientY - startY @@ -68,32 +130,63 @@ public actual object KickOverlay { root.style.top = "${(offsetY + dy).toInt()}px" } } - val onMouseUp: (org.w3c.dom.events.MouseEvent) -> Unit = { _ -> dragging = false } + val onMouseUp: (MouseEvent) -> Unit = { _ -> dragging = false } - root.addEventListener("mousedown") { e: org.w3c.dom.events.Event -> - onMouseDown(e as org.w3c.dom.events.MouseEvent) - } - document.addEventListener("mousemove") { e: org.w3c.dom.events.Event -> - onMouseMove(e as org.w3c.dom.events.MouseEvent) - } - document.addEventListener("mouseup") { e: org.w3c.dom.events.Event -> - onMouseUp(e as org.w3c.dom.events.MouseEvent) - } + root.addEventListener("mousedown") { e: Event -> onMouseDown(e as MouseEvent) } + val moveListener: (Event) -> Unit = { e -> onMouseMove(e as MouseEvent) } + val upListener: (Event) -> Unit = { e -> onMouseUp(e as MouseEvent) } + document.addEventListener("mousemove", moveListener) + document.addEventListener("mouseup", upListener) + mouseMoveListener = moveListener + mouseUpListener = upListener - ComposeViewport(root) { - Overlay( - root = root, - onCloseClick = ::onCloseClicked, - ) + // Subscribe to data updates + scope = CoroutineScope(Dispatchers.Default).also { s -> + itemsJob = s.launch { + OverlayStore.items.collect { currentItems -> + renderLines(currentItems) + } + } } + + // Render initial state + renderLines(OverlayStore.items.value) } public actual fun hide() { OverlaySettings.setEnabled(false) - overlayRoot?.let { el -> - el.parentElement?.removeChild(el) - } + + itemsJob?.cancel() + itemsJob = null + scope?.cancel() + scope = null + + mouseMoveListener?.let { document.removeEventListener("mousemove", it) } + mouseUpListener?.let { document.removeEventListener("mouseup", it) } + mouseMoveListener = null + mouseUpListener = null + + overlayRoot?.let { el -> el.parentElement?.removeChild(el) } overlayRoot = null + overlayLines = null + } + + private fun renderLines(items: List>) { + val container = overlayLines ?: return + while (container.firstChild != null) { + container.removeChild(container.firstChild!!) + } + for ((key, value) in items) { + val line = document.createElement("div") as HTMLElement + line.textContent = "$key: $value" + line.style.apply { + setProperty("white-space", "nowrap") + setProperty("margin", "2px 0") + setProperty("overflow", "hidden") + setProperty("text-overflow", "clip") + } + container.appendChild(line) + } } private fun onCloseClicked() { diff --git a/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/Overlay.wasmJs.kt b/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/Overlay.wasmJs.kt deleted file mode 100644 index 02c2f65..0000000 --- a/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/overlay/Overlay.wasmJs.kt +++ /dev/null @@ -1,33 +0,0 @@ -package ru.bartwell.kick.module.overlay.core.overlay - -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.unit.IntSize -import org.w3c.dom.HTMLElement -import kotlin.math.max - -@Composable -internal fun Overlay( - root: HTMLElement, - onCloseClick: () -> Unit, -) { - MaterialTheme { - var desiredPx by remember { mutableStateOf(IntSize(0, 0)) } - - AutosizeMeasure( - onSizes = { desired, _ -> desiredPx = desired }, - desiredContent = { OverlayWindow(onCloseClick = onCloseClick, measureFull = true) }, - actualContent = { OverlayWindow(onCloseClick = onCloseClick, measureFull = false) } - ) - - LaunchedEffect(desiredPx) { - root.style.width = "${max(1, desiredPx.width)}px" - root.style.height = "${max(1, desiredPx.height)}px" - } - } -} diff --git a/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.wasmJs.kt b/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.wasmJs.kt new file mode 100644 index 0000000..fe0b738 --- /dev/null +++ b/module/logging/overlay/src/wasmJsMain/kotlin/ru/bartwell/kick/module/overlay/core/provider/PerformanceSnapshot.wasmJs.kt @@ -0,0 +1,4 @@ +package ru.bartwell.kick.module.overlay.core.provider + +internal actual fun readPerformanceSnapshot(): PerformanceSnapshot = + PerformanceSnapshot(cpuUsagePercent = null, usedMemoryBytes = null, totalMemoryBytes = null) diff --git a/module/logging/overlay/src/wasmJsTest/kotlin/ru/bartwell/kick/module/overlay/OverlayWasmJsTest.kt b/module/logging/overlay/src/wasmJsTest/kotlin/ru/bartwell/kick/module/overlay/OverlayWasmJsTest.kt index f5be33e..532f794 100644 --- a/module/logging/overlay/src/wasmJsTest/kotlin/ru/bartwell/kick/module/overlay/OverlayWasmJsTest.kt +++ b/module/logging/overlay/src/wasmJsTest/kotlin/ru/bartwell/kick/module/overlay/OverlayWasmJsTest.kt @@ -21,7 +21,7 @@ class OverlayWasmJsTest { val ctx = getPlatformContext() KickOverlay.init(ctx) - KickOverlay.show(ctx) + KickOverlay.show() val el = document.getElementById(OVERLAY_ELEMENT_ID) as? HTMLElement assertNotNull(el) diff --git a/sample/ios/iosSample.xcodeproj/xcshareddata/xcschemes/iosSample.xcscheme b/sample/ios/iosSample.xcodeproj/xcshareddata/xcschemes/iosSample.xcscheme index 24e1e88..c7c9f26 100644 --- a/sample/ios/iosSample.xcodeproj/xcshareddata/xcschemes/iosSample.xcscheme +++ b/sample/ios/iosSample.xcodeproj/xcshareddata/xcschemes/iosSample.xcscheme @@ -50,6 +50,10 @@ ReferencedContainer = "container:iosSample.xcodeproj"> + +