feat(#1,#3): binocular start screen via Binocular{}; cursor auto-hide after 3s idle
This commit is contained in:
+33
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user