From 3ac76daca128e847ee5a52a4041bc92ce7b532d3 Mon Sep 17 00:00:00 2001 From: subochev Date: Sun, 27 Sep 2026 06:42:26 +0300 Subject: [PATCH] feat(#1,#3): binocular start screen via Binocular{}; cursor auto-hide after 3s idle --- .../glasses/protocol/VmGlassesController.kt | 33 +++++++++++ .../rayneovm/glasses/ui/GlassesScreen.kt | 57 +++++++++++-------- 2 files changed, 67 insertions(+), 23 deletions(-) diff --git a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/protocol/VmGlassesController.kt b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/protocol/VmGlassesController.kt index ca55992..c3ba69c 100644 --- a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/protocol/VmGlassesController.kt +++ b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/protocol/VmGlassesController.kt @@ -74,6 +74,13 @@ class VmGlassesController { /** Насос скролла: один на приложение. */ private var scrollPump: Job? = null + + /** + * Watchdog автоскрытия курсора: после [CURSOR_HIDE_MS] бездействия курсор гасится. Перезапускается + * на каждом действии тачпада. Курсор появится вновь при следующем move/click/scroll — место + * появления не принципиально (новое — от последнего moveCursor). + */ + private var cursorHideJob: Job? = null @Volatile private var appContext: Context? = null /** Сериализует создание/освобождение VirtualDisplay — параллельный acquire оставил бы висеть второй UserService. */ @@ -159,6 +166,7 @@ class VmGlassesController { x = x.coerceIn(0f, VmDisplay.WIDTH - 1f), y = y.coerceIn(0f, VmDisplay.HEIGHT - 1f), ) + scheduleCursorHide() } /** @@ -172,6 +180,7 @@ class VmGlassesController { val pos = _cursor.value ?: return val binder = src.shizukuBinder val displayId = src.virtualDisplayId + scheduleCursorHide() testScope.launch { val ok = ShizukuVdBinder.tap(binder, displayId, pos.x, pos.y) if (!ok) Log.w(TAG, "clickCursor: tap failed (displayId=$displayId)") @@ -192,10 +201,30 @@ class VmGlassesController { */ fun scrollCursor(dx: Float, dy: Float) { if (dx == 0f && dy == 0f) return + scheduleCursorHide() scrollChannel.trySend(floatArrayOf(dx, dy)) ensureScrollPump() } + /** + * Перезапускает watchdog автоскрытия курсора. Если гость работает и через [CURSOR_HIDE_MS] не + * будет новых действий с тачпада — прячем курсор. + */ + private fun scheduleCursorHide() { + if (activeSource == null) return + cursorHideJob?.cancel() + cursorHideJob = testScope.launch { + delay(CURSOR_HIDE_MS) + _cursor.value = null + } + } + + /** Останавливает watchdog и очищает курсор (вызов из [stopGuest]). */ + private fun cancelCursorHide() { + cursorHideJob?.cancel() + cursorHideJob = null + } + /** Один насос на приложение: копит дельты и инжектит пачкой. */ private fun ensureScrollPump() { if (scrollPump != null) return @@ -353,6 +382,7 @@ class VmGlassesController { acquireMutex.withLock { killActiveGuest() releaseFrameSource() + cancelCursorHide() _cursor.value = null setState(GlassesScreenState.NoAppSelected) } @@ -551,6 +581,9 @@ class VmGlassesController { /** Скролл-драг: как часто сливаем накопленные дельты, порог и сброс «хвоста». */ private const val SCROLL_TICK_MS = 90L + /** Через сколько мс простоя тачпада прятать курсор. */ + private const val CURSOR_HIDE_MS = 3_000L + /** Минимальная длина драга: короче гость трактует как клик по элементу (проверено на Settings ~50-60 px). */ private const val SCROLL_MIN_PX = 96f diff --git a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/ui/GlassesScreen.kt b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/ui/GlassesScreen.kt index 1ea36af..69b1b60 100644 --- a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/ui/GlassesScreen.kt +++ b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/ui/GlassesScreen.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import pw.binom.mercury.binocular.Binocular import pw.binom.mercury.binocular.BinocularRendererView import pw.binom.mercury.binocular.NoOpFrameSource import pw.binom.rayneovm.glasses.protocol.VmGlassesController @@ -55,34 +56,44 @@ fun GlassesScreen( modifier = Modifier.fillMaxSize(), ) - // Compose overlay поверх GL + // Compose overlay поверх GL. Простые статусы рисуем бинокулярно (по тексту на каждый глаз) + // через [Binocular]; GL-слой и курсор мыши внутри Binocular НЕ оборачиваем — они сами + // справляются (рендер и точки в обеих половинах). when (val s = state) { - is GlassesScreenState.Starting -> StatusOverlay("Подготавливаем очки...") - is GlassesScreenState.ShizukuMissing -> StatusOverlay( - title = "Shizuku не активен", - subtitle = "Без Shizuku приложение не может создать виртуальный экран.\n" + - "Тапните здесь, чтобы дать разрешение.", - onClick = onRequestShizukuPermission, - ) - is GlassesScreenState.NoConnection -> StatusOverlay( - title = "Нет связи с телефоном", - subtitle = "Убедитесь, что телефон и очки в одной WiFi-сети." - ) - is GlassesScreenState.NoAppSelected -> StatusOverlay( - title = "Выберите приложение на телефоне", - subtitle = "Откройте приложение «RayNeo VM» и выберите приложение для запуска.\n\n[DEBUG] Тап-тест: запустить Zona сейчас", - onClick = { - controller.startTestLaunch("free.zona") - } - ) + is GlassesScreenState.Starting -> Binocular { StatusOverlay("Подготавливаем очки...") } + is GlassesScreenState.ShizukuMissing -> Binocular { + StatusOverlay( + title = "Shizuku не активен", + subtitle = "Без Shizuku приложение не может создать виртуальный экран.\n" + + "Тапните здесь, чтобы дать разрешение.", + onClick = onRequestShizukuPermission, + ) + } + is GlassesScreenState.NoConnection -> Binocular { + StatusOverlay( + title = "Нет связи с телефоном", + subtitle = "Убедитесь, что телефон и очки в одной WiFi-сети." + ) + } + is GlassesScreenState.NoAppSelected -> Binocular { + StatusOverlay( + title = "Выберите приложение на телефоне", + subtitle = "Откройте приложение «RayNeo VM» и выберите приложение для запуска.\n\n[DEBUG] Тап-тест: запустить Zona сейчас", + onClick = { + controller.startTestLaunch("free.zona") + } + ) + } is GlassesScreenState.Running -> { // Ничего поверх GL, кроме курсора мыши: FrameSource уже установлен контроллером. cursor?.let { CursorOverlay(it.x, it.y) } } - is GlassesScreenState.Error -> StatusOverlay( - title = "Ошибка", - subtitle = s.message, - ) + is GlassesScreenState.Error -> Binocular { + StatusOverlay( + title = "Ошибка", + subtitle = s.message, + ) + } } } }