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
|
private var scrollPump: Job? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watchdog автоскрытия курсора: после [CURSOR_HIDE_MS] бездействия курсор гасится. Перезапускается
|
||||||
|
* на каждом действии тачпада. Курсор появится вновь при следующем move/click/scroll — место
|
||||||
|
* появления не принципиально (новое — от последнего moveCursor).
|
||||||
|
*/
|
||||||
|
private var cursorHideJob: Job? = null
|
||||||
@Volatile private var appContext: Context? = null
|
@Volatile private var appContext: Context? = null
|
||||||
|
|
||||||
/** Сериализует создание/освобождение VirtualDisplay — параллельный acquire оставил бы висеть второй UserService. */
|
/** Сериализует создание/освобождение VirtualDisplay — параллельный acquire оставил бы висеть второй UserService. */
|
||||||
@@ -159,6 +166,7 @@ class VmGlassesController {
|
|||||||
x = x.coerceIn(0f, VmDisplay.WIDTH - 1f),
|
x = x.coerceIn(0f, VmDisplay.WIDTH - 1f),
|
||||||
y = y.coerceIn(0f, VmDisplay.HEIGHT - 1f),
|
y = y.coerceIn(0f, VmDisplay.HEIGHT - 1f),
|
||||||
)
|
)
|
||||||
|
scheduleCursorHide()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,6 +180,7 @@ class VmGlassesController {
|
|||||||
val pos = _cursor.value ?: return
|
val pos = _cursor.value ?: return
|
||||||
val binder = src.shizukuBinder
|
val binder = src.shizukuBinder
|
||||||
val displayId = src.virtualDisplayId
|
val displayId = src.virtualDisplayId
|
||||||
|
scheduleCursorHide()
|
||||||
testScope.launch {
|
testScope.launch {
|
||||||
val ok = ShizukuVdBinder.tap(binder, displayId, pos.x, pos.y)
|
val ok = ShizukuVdBinder.tap(binder, displayId, pos.x, pos.y)
|
||||||
if (!ok) Log.w(TAG, "clickCursor: tap failed (displayId=$displayId)")
|
if (!ok) Log.w(TAG, "clickCursor: tap failed (displayId=$displayId)")
|
||||||
@@ -192,10 +201,30 @@ class VmGlassesController {
|
|||||||
*/
|
*/
|
||||||
fun scrollCursor(dx: Float, dy: Float) {
|
fun scrollCursor(dx: Float, dy: Float) {
|
||||||
if (dx == 0f && dy == 0f) return
|
if (dx == 0f && dy == 0f) return
|
||||||
|
scheduleCursorHide()
|
||||||
scrollChannel.trySend(floatArrayOf(dx, dy))
|
scrollChannel.trySend(floatArrayOf(dx, dy))
|
||||||
ensureScrollPump()
|
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() {
|
private fun ensureScrollPump() {
|
||||||
if (scrollPump != null) return
|
if (scrollPump != null) return
|
||||||
@@ -353,6 +382,7 @@ class VmGlassesController {
|
|||||||
acquireMutex.withLock {
|
acquireMutex.withLock {
|
||||||
killActiveGuest()
|
killActiveGuest()
|
||||||
releaseFrameSource()
|
releaseFrameSource()
|
||||||
|
cancelCursorHide()
|
||||||
_cursor.value = null
|
_cursor.value = null
|
||||||
setState(GlassesScreenState.NoAppSelected)
|
setState(GlassesScreenState.NoAppSelected)
|
||||||
}
|
}
|
||||||
@@ -551,6 +581,9 @@ class VmGlassesController {
|
|||||||
/** Скролл-драг: как часто сливаем накопленные дельты, порог и сброс «хвоста». */
|
/** Скролл-драг: как часто сливаем накопленные дельты, порог и сброс «хвоста». */
|
||||||
private const val SCROLL_TICK_MS = 90L
|
private const val SCROLL_TICK_MS = 90L
|
||||||
|
|
||||||
|
/** Через сколько мс простоя тачпада прятать курсор. */
|
||||||
|
private const val CURSOR_HIDE_MS = 3_000L
|
||||||
|
|
||||||
/** Минимальная длина драга: короче гость трактует как клик по элементу (проверено на Settings ~50-60 px). */
|
/** Минимальная длина драга: короче гость трактует как клик по элементу (проверено на Settings ~50-60 px). */
|
||||||
private const val SCROLL_MIN_PX = 96f
|
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.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import pw.binom.mercury.binocular.Binocular
|
||||||
import pw.binom.mercury.binocular.BinocularRendererView
|
import pw.binom.mercury.binocular.BinocularRendererView
|
||||||
import pw.binom.mercury.binocular.NoOpFrameSource
|
import pw.binom.mercury.binocular.NoOpFrameSource
|
||||||
import pw.binom.rayneovm.glasses.protocol.VmGlassesController
|
import pw.binom.rayneovm.glasses.protocol.VmGlassesController
|
||||||
@@ -55,34 +56,44 @@ fun GlassesScreen(
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Compose overlay поверх GL
|
// Compose overlay поверх GL. Простые статусы рисуем бинокулярно (по тексту на каждый глаз)
|
||||||
|
// через [Binocular]; GL-слой и курсор мыши внутри Binocular НЕ оборачиваем — они сами
|
||||||
|
// справляются (рендер и точки в обеих половинах).
|
||||||
when (val s = state) {
|
when (val s = state) {
|
||||||
is GlassesScreenState.Starting -> StatusOverlay("Подготавливаем очки...")
|
is GlassesScreenState.Starting -> Binocular { StatusOverlay("Подготавливаем очки...") }
|
||||||
is GlassesScreenState.ShizukuMissing -> StatusOverlay(
|
is GlassesScreenState.ShizukuMissing -> Binocular {
|
||||||
title = "Shizuku не активен",
|
StatusOverlay(
|
||||||
subtitle = "Без Shizuku приложение не может создать виртуальный экран.\n" +
|
title = "Shizuku не активен",
|
||||||
"Тапните здесь, чтобы дать разрешение.",
|
subtitle = "Без Shizuku приложение не может создать виртуальный экран.\n" +
|
||||||
onClick = onRequestShizukuPermission,
|
"Тапните здесь, чтобы дать разрешение.",
|
||||||
)
|
onClick = onRequestShizukuPermission,
|
||||||
is GlassesScreenState.NoConnection -> StatusOverlay(
|
)
|
||||||
title = "Нет связи с телефоном",
|
}
|
||||||
subtitle = "Убедитесь, что телефон и очки в одной WiFi-сети."
|
is GlassesScreenState.NoConnection -> Binocular {
|
||||||
)
|
StatusOverlay(
|
||||||
is GlassesScreenState.NoAppSelected -> StatusOverlay(
|
title = "Нет связи с телефоном",
|
||||||
title = "Выберите приложение на телефоне",
|
subtitle = "Убедитесь, что телефон и очки в одной WiFi-сети."
|
||||||
subtitle = "Откройте приложение «RayNeo VM» и выберите приложение для запуска.\n\n[DEBUG] Тап-тест: запустить Zona сейчас",
|
)
|
||||||
onClick = {
|
}
|
||||||
controller.startTestLaunch("free.zona")
|
is GlassesScreenState.NoAppSelected -> Binocular {
|
||||||
}
|
StatusOverlay(
|
||||||
)
|
title = "Выберите приложение на телефоне",
|
||||||
|
subtitle = "Откройте приложение «RayNeo VM» и выберите приложение для запуска.\n\n[DEBUG] Тап-тест: запустить Zona сейчас",
|
||||||
|
onClick = {
|
||||||
|
controller.startTestLaunch("free.zona")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
is GlassesScreenState.Running -> {
|
is GlassesScreenState.Running -> {
|
||||||
// Ничего поверх GL, кроме курсора мыши: FrameSource уже установлен контроллером.
|
// Ничего поверх GL, кроме курсора мыши: FrameSource уже установлен контроллером.
|
||||||
cursor?.let { CursorOverlay(it.x, it.y) }
|
cursor?.let { CursorOverlay(it.x, it.y) }
|
||||||
}
|
}
|
||||||
is GlassesScreenState.Error -> StatusOverlay(
|
is GlassesScreenState.Error -> Binocular {
|
||||||
title = "Ошибка",
|
StatusOverlay(
|
||||||
subtitle = s.message,
|
title = "Ошибка",
|
||||||
)
|
subtitle = s.message,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user