mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 19:41:10 +00:00
Session: draw the program's own pointer - resize arrows at window edges, I-beams, its hide
The compositor threw every wl_pointer.set_cursor image away and the app drew one fixed arrow, so the desktop never showed labwc's resize arrows at a window's edges and corners. Ported from Bannerlator (7f19c669): the cursor surface gets a role, its image is snapshotted on commit with its hotspot, and a NULL surface or buffer is an authoritative hide; the app's overlay draws the image at the session's scale. New here: labwc on Vulkan/GLES hands its cursor over as a dma-buf (wlroots' nested backend, usually UBWC), so it is read back through a small linear host-visible copy (vkp_image_readback). The built-in arrow stays until a program sets a cursor.
This commit is contained in:
1 parent
347a5a4dcb
commit
c331836bce
7 files changed
+305
-6
No files matched your search
@@ -226,7 +226,7 @@ static void on_client_created(struct wl_listener *l, void *data) {
|
||||
|
||||
/* ------------------------------------------------------------------ surfaces */
|
||||
|
||||
enum surface_role { ROLE_NONE, ROLE_TOPLEVEL, ROLE_SUBSURFACE, ROLE_DESKTOP };
|
||||
enum surface_role { ROLE_NONE, ROLE_TOPLEVEL, ROLE_SUBSURFACE, ROLE_DESKTOP, ROLE_CURSOR };
|
||||
|
||||
struct surface {
|
||||
struct wl_resource *resource;
|
||||
@@ -474,6 +474,69 @@ static struct seat_keyboard g_kbs[MAX_PTRS];
|
||||
static int g_nkbs;
|
||||
static struct seat_touch g_touches[MAX_PTRS];
|
||||
static int g_ntouches;
|
||||
|
||||
/* ---- the client's cursor (wl_pointer.set_cursor) -------------------------------------------
|
||||
* Not composited: under zero-copy the program's buffer goes straight to an Android layer and there
|
||||
* is nothing to composite into. The cursor surface is snapshotted here instead and the app draws it
|
||||
* as its overlay, above every present path (Bannerlator's design). What it buys: the real SHAPE -
|
||||
* labwc's resize arrows at a window's edges and corners, a text field's I-beam - and an
|
||||
* authoritative HIDE (set_cursor with a NULL surface, what a mouse-look game does).
|
||||
* wl_shm cursors (Wine, pixman) are copied directly; a GPU cursor (labwc on Vulkan/GLES, which
|
||||
* wlroots hands over as a dma-buf, usually UBWC) is read back through vkp_image_readback. */
|
||||
#define CURSOR_MAX_PX (256 * 256)
|
||||
static pthread_mutex_t g_cursor_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static struct surface *g_cursor_surface; /* compositor thread only */
|
||||
static int g_cursor_hx, g_cursor_hy; /* hotspot, surface-local */
|
||||
static uint32_t g_cursor_px[CURSOR_MAX_PX]; /* guarded by g_cursor_lock: ARGB8888 snapshot */
|
||||
static uint32_t g_cursor_rb[CURSOR_MAX_PX]; /* compositor thread: GPU readback staging */
|
||||
static int g_cursor_w, g_cursor_h; /* 0 = nothing to draw */
|
||||
static int g_cursor_hidden = 1; /* the client asked for no pointer */
|
||||
static int g_cursor_serial; /* bumped on every change; the app polls it */
|
||||
|
||||
static void cursor_publish_hidden(void) {
|
||||
pthread_mutex_lock(&g_cursor_lock);
|
||||
if (!g_cursor_hidden) { g_cursor_hidden = 1; g_cursor_serial++; }
|
||||
pthread_mutex_unlock(&g_cursor_lock);
|
||||
}
|
||||
|
||||
static void cursor_publish_pixels(const uint8_t *src, int w, int h, size_t stride, int hx, int hy) {
|
||||
if (w <= 0 || h <= 0 || w * h > CURSOR_MAX_PX) return;
|
||||
pthread_mutex_lock(&g_cursor_lock);
|
||||
for (int y = 0; y < h; y++)
|
||||
memcpy(&g_cursor_px[y * w], src + (size_t)y * stride, (size_t)w * 4);
|
||||
g_cursor_w = w; g_cursor_h = h;
|
||||
g_cursor_hx = hx; g_cursor_hy = hy;
|
||||
g_cursor_hidden = 0;
|
||||
g_cursor_serial++;
|
||||
pthread_mutex_unlock(&g_cursor_lock);
|
||||
}
|
||||
|
||||
static void cursor_publish_shm(struct wl_shm_buffer *shm, int hx, int hy) {
|
||||
int32_t w = wl_shm_buffer_get_width(shm), h = wl_shm_buffer_get_height(shm);
|
||||
if (w <= 0 || h <= 0 || w * h > CURSOR_MAX_PX) return;
|
||||
wl_shm_buffer_begin_access(shm);
|
||||
cursor_publish_pixels((const uint8_t *)wl_shm_buffer_get_data(shm), w, h,
|
||||
(size_t)wl_shm_buffer_get_stride(shm), hx, hy);
|
||||
wl_shm_buffer_end_access(shm);
|
||||
}
|
||||
|
||||
/* Read by the app (UI thread). out = [serial, hidden, w, h, hotspotX, hotspotY, pixels...].
|
||||
* Returns the number of ints written, or 0 if out is too small. */
|
||||
int banner_cursor_snapshot(int *out, int cap) {
|
||||
int n = 0;
|
||||
pthread_mutex_lock(&g_cursor_lock);
|
||||
int need = 6 + (g_cursor_hidden ? 0 : g_cursor_w * g_cursor_h);
|
||||
if (cap >= need) {
|
||||
out[0] = g_cursor_serial; out[1] = g_cursor_hidden;
|
||||
out[2] = g_cursor_w; out[3] = g_cursor_h;
|
||||
out[4] = g_cursor_hx; out[5] = g_cursor_hy;
|
||||
if (!g_cursor_hidden)
|
||||
memcpy(out + 6, g_cursor_px, (size_t)g_cursor_w * g_cursor_h * 4);
|
||||
n = need;
|
||||
}
|
||||
pthread_mutex_unlock(&g_cursor_lock);
|
||||
return n;
|
||||
}
|
||||
struct active_touch { struct surface *surface; int pointer_fallback; };
|
||||
static struct active_touch g_touch_points[MAX_TOUCH_POINTS];
|
||||
static void touch_cancel_surface(struct surface *surface);
|
||||
@@ -948,8 +1011,26 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) {
|
||||
s->pending_buffer = NULL;
|
||||
s->pending_attach = 0;
|
||||
|
||||
if (s->role == ROLE_NONE) {
|
||||
/* Cursor or role-less surface: never drawn (the app draws its own pointer). */
|
||||
if (s->role == ROLE_CURSOR) {
|
||||
/* The client's pointer image, copied out for the app's overlay (see cursor_publish_*)
|
||||
* rather than composited, so it survives the zero-copy and HDR layer paths. */
|
||||
if (!buffer) {
|
||||
cursor_publish_hidden(); /* wlroots clears its cursor surface to hide the pointer */
|
||||
} else if (shm) {
|
||||
cursor_publish_shm(shm, g_cursor_hx, g_cursor_hy);
|
||||
} else if (db && db->n_planes >= 1 && db->width * db->height <= CURSOR_MAX_PX) {
|
||||
if (!db->img && !db->import_failed) {
|
||||
db->img = vkp_image_from_dmabuf(db->fd[0], db->format, db->modifier, db->width, db->height,
|
||||
db->stride[0], db->offset[0]);
|
||||
if (!db->img) db->import_failed = 1;
|
||||
}
|
||||
if (db->img && vkp_image_readback(db->img, g_cursor_rb, CURSOR_MAX_PX) == 0)
|
||||
cursor_publish_pixels((const uint8_t *)g_cursor_rb, db->width, db->height,
|
||||
(size_t)db->width * 4, g_cursor_hx, g_cursor_hy);
|
||||
}
|
||||
if (buffer) wl_buffer_send_release(buffer);
|
||||
} else if (s->role == ROLE_NONE) {
|
||||
/* Role-less surface: never drawn. */
|
||||
if (buffer) wl_buffer_send_release(buffer);
|
||||
} else if (db) {
|
||||
take_dmabuf(s, db, buffer);
|
||||
@@ -1021,6 +1102,7 @@ static void surface_resource_destroy(struct wl_resource *r) {
|
||||
for (int i = 0; i < g_nptrs; i++) if (g_ptrs[i].focus == r) g_ptrs[i].focus = NULL;
|
||||
for (int i = 0; i < g_nkbs; i++) if (g_kbs[i].focus == r) g_kbs[i].focus = NULL;
|
||||
touch_cancel_surface(s);
|
||||
if (g_cursor_surface == s) { g_cursor_surface = NULL; cursor_publish_hidden(); }
|
||||
if (g_grab == s) g_grab = NULL;
|
||||
if (g_key_target == s) g_key_target = NULL;
|
||||
if (g_ime_click == s) g_ime_click = NULL;
|
||||
@@ -2397,9 +2479,23 @@ static uint32_t now_ms(void) {
|
||||
return (uint32_t)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
|
||||
}
|
||||
|
||||
/* wl_pointer.set_cursor: the client tells us its pointer image, or asks for none. A NULL surface
|
||||
* means "no cursor" and is authoritative. The surface itself is snapshotted on its next commit. */
|
||||
static void pointer_set_cursor(struct wl_client *c, struct wl_resource *r, uint32_t serial,
|
||||
struct wl_resource *surface, int32_t hx, int32_t hy) {
|
||||
/* The app draws its own pointer; cursor surfaces stay role-less and aren't drawn. */
|
||||
if (!surface) {
|
||||
if (g_cursor_surface) g_cursor_surface->role = ROLE_NONE;
|
||||
g_cursor_surface = NULL;
|
||||
cursor_publish_hidden();
|
||||
return;
|
||||
}
|
||||
struct surface *s = wl_resource_get_user_data(surface);
|
||||
if (!s) return;
|
||||
if (s->role != ROLE_NONE && s->role != ROLE_CURSOR) return; /* already has another role */
|
||||
s->role = ROLE_CURSOR;
|
||||
g_cursor_surface = s;
|
||||
g_cursor_hx = hx;
|
||||
g_cursor_hy = hy;
|
||||
}
|
||||
static void pointer_release(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); }
|
||||
static const struct wl_pointer_interface pointer_impl = {
|
||||
|
||||
@@ -1910,6 +1910,104 @@ int vkp_blit_image(struct vkp_image *src, struct vkp_image *dst, int wait_fd) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* A client's GPU cursor read back to the CPU, for the app's pointer overlay (compositor.c,
|
||||
* wl_pointer.set_cursor). wlroots hands a nested compositor its cursor as a dma-buf from the parent's
|
||||
* formats - on Adreno usually UBWC, which the CPU cannot read - so it is blitted into a small linear,
|
||||
* host-visible image first. Rare (a cursor changes shape, it is not redrawn per frame) and waited
|
||||
* for on the CPU. out: w*h pixels, B8G8R8A8 in memory = 0xAARRGGBB ints. 0 = ok. */
|
||||
static struct vkp_image *g_rb; /* the readback image, kept for the next shape */
|
||||
|
||||
int vkp_image_readback(struct vkp_image *src, uint32_t *out, int max_px) {
|
||||
if (!src || !out || src->w <= 0 || src->h <= 0 || src->w * src->h > max_px ||
|
||||
g_dev_state == -2 || dev_init() != 0)
|
||||
return -1;
|
||||
if (g_rb && (g_rb->w != src->w || g_rb->h != src->h)) { vkp_image_destroy(g_rb); g_rb = NULL; }
|
||||
if (!g_rb) {
|
||||
struct vkp_image *img = calloc(1, sizeof(*img));
|
||||
if (!img) return -1;
|
||||
img->w = src->w; img->h = src->h;
|
||||
VkImageCreateInfo ici = {
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D,
|
||||
.format = VK_FORMAT_B8G8R8A8_UNORM, .extent = {src->w, src->h, 1}, .mipLevels = 1,
|
||||
.arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_LINEAR,
|
||||
.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT, .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
|
||||
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED};
|
||||
if (g_vk.CreateImage(g_dev, &ici, NULL, &img->image) != VK_SUCCESS) { free(img); return -1; }
|
||||
VkMemoryRequirements req;
|
||||
g_vk.GetImageMemoryRequirements(g_dev, img->image, &req);
|
||||
int idx = memory_type(req.memoryTypeBits,
|
||||
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
|
||||
VkMemoryAllocateInfo mai = {.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
|
||||
.allocationSize = req.size, .memoryTypeIndex = (uint32_t)idx};
|
||||
if (idx < 0 || g_vk.AllocateMemory(g_dev, &mai, NULL, &img->mem) != VK_SUCCESS) {
|
||||
g_vk.DestroyImage(g_dev, img->image, NULL); free(img); return -1;
|
||||
}
|
||||
g_vk.BindImageMemory(g_dev, img->image, img->mem, 0);
|
||||
VkImageSubresource subr = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0};
|
||||
VkSubresourceLayout lay;
|
||||
g_vk.GetImageSubresourceLayout(g_dev, img->image, &subr, &lay);
|
||||
img->offset = lay.offset;
|
||||
img->row_pitch = lay.rowPitch;
|
||||
if (g_vk.MapMemory(g_dev, img->mem, 0, req.size, 0, &img->map) != VK_SUCCESS) {
|
||||
vkp_image_destroy(img); return -1;
|
||||
}
|
||||
g_rb = img;
|
||||
}
|
||||
VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
VkCommandBuffer cmd = g_cmds[0];
|
||||
g_vk.ResetCommandBuffer(cmd, 0);
|
||||
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
|
||||
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
|
||||
g_vk.BeginCommandBuffer(cmd, &bi);
|
||||
VkImageMemoryBarrier acq[2] = {
|
||||
{.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
.srcQueueFamilyIndex = src->dmabuf ? VK_QUEUE_FAMILY_FOREIGN_EXT : VK_QUEUE_FAMILY_IGNORED,
|
||||
.dstQueueFamilyIndex = src->dmabuf ? g_qfam : VK_QUEUE_FAMILY_IGNORED,
|
||||
.image = src->image, .subresourceRange = range, .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT},
|
||||
{.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
.newLayout = VK_IMAGE_LAYOUT_GENERAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = g_rb->image, .subresourceRange = range,
|
||||
.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT}};
|
||||
g_vk.CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
0, 0, NULL, 0, NULL, 2, acq);
|
||||
VkImageBlit blit = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
|
||||
.srcOffsets = {{0, 0, 0}, {src->w, src->h, 1}},
|
||||
.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
|
||||
.dstOffsets = {{0, 0, 0}, {src->w, src->h, 1}}};
|
||||
g_vk.CmdBlitImage(cmd, src->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
g_rb->image, VK_IMAGE_LAYOUT_GENERAL, 1, &blit, VK_FILTER_NEAREST);
|
||||
VkImageMemoryBarrier done[2] = {
|
||||
{.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||
.newLayout = VK_IMAGE_LAYOUT_GENERAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = g_rb->image, .subresourceRange = range,
|
||||
.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, .dstAccessMask = VK_ACCESS_HOST_READ_BIT},
|
||||
/* the client's buffer goes back to it */
|
||||
{.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||
.srcQueueFamilyIndex = src->dmabuf ? g_qfam : VK_QUEUE_FAMILY_IGNORED,
|
||||
.dstQueueFamilyIndex = src->dmabuf ? VK_QUEUE_FAMILY_FOREIGN_EXT : VK_QUEUE_FAMILY_IGNORED,
|
||||
.image = src->image, .subresourceRange = range, .srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT}};
|
||||
g_vk.CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
VK_PIPELINE_STAGE_HOST_BIT | VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
|
||||
0, 0, NULL, 0, NULL, 2, done);
|
||||
g_vk.EndCommandBuffer(cmd);
|
||||
VkSubmitInfo si = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, .commandBufferCount = 1, .pCommandBuffers = &cmd};
|
||||
g_vk.ResetFences(g_dev, 1, &g_fence);
|
||||
VkResult qr = g_vk.QueueSubmit(g_queue, 1, &si, g_fence);
|
||||
if (qr != VK_SUCCESS) {
|
||||
if (qr == VK_ERROR_DEVICE_LOST) device_lost("cursor readback");
|
||||
return -1;
|
||||
}
|
||||
VkResult fr = timed_wait(g_fence, 1000000000ULL);
|
||||
if (fr == VK_ERROR_DEVICE_LOST) { device_lost("cursor readback"); return -1; }
|
||||
if (fr != VK_SUCCESS) return -1;
|
||||
for (int y = 0; y < src->h; y++)
|
||||
memcpy(out + (size_t)y * src->w, (const uint8_t *)g_rb->map + g_rb->offset + (size_t)y * g_rb->row_pitch,
|
||||
(size_t)src->w * 4);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- the compositor pass, off-screen
|
||||
*
|
||||
* Layer mode with screen effects on: the game's frame still has to go through the compositor pass
|
||||
|
||||
@@ -146,6 +146,9 @@ int vkp_pass_begin_hdr(int scene_w, int scene_h, const struct vkp_draw *draws, i
|
||||
* wait_fd: a sync_file the copy must wait for before writing dst (the display's release fence of that
|
||||
* buffer), -1 = none. Always consumed (handed to the GPU, or waited for and closed). */
|
||||
int vkp_blit_image(struct vkp_image *src, struct vkp_image *dst, int wait_fd);
|
||||
/* A client's GPU cursor (an imported dma-buf, often UBWC) read back as w*h 0xAARRGGBB pixels through a
|
||||
* linear host-visible copy; waited for on the CPU. Compositor thread, between frames. 0 = ok. */
|
||||
int vkp_image_readback(struct vkp_image *src, uint32_t *out, int max_px);
|
||||
/* Refresh the scene -> output mapping for this scene size without presenting (creates the
|
||||
* swapchain if needed, since the mapping is in output pixels). 0 = mapping valid. */
|
||||
int vkp_update_map(int scene_w, int scene_h);
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
extern int banner_wayland_run(void);
|
||||
extern void banner_wayland_send_pointer(int action, int x, int y);
|
||||
extern void banner_wayland_send_touch(int action, int pointer_id, int x, int y);
|
||||
extern int banner_cursor_snapshot(int *out, int cap);
|
||||
extern void banner_wayland_send_key(int evdev, int state);
|
||||
extern void banner_wayland_send_scene_input(int type, int a, int b);
|
||||
extern void banner_wayland_vsync(int64_t frame_time_ns);
|
||||
@@ -278,6 +279,21 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendTouch(
|
||||
banner_wayland_send_touch(action, pointer_id, x, y);
|
||||
}
|
||||
|
||||
/* The client's pointer image and whether it wants one at all (wl_pointer.set_cursor).
|
||||
* out = [serial, hidden, w, h, hotspotX, hotspotY, ARGB pixels...]; returns ints written, 0 if the
|
||||
* array is too small. The app polls the serial and only rebuilds its overlay when it changes. */
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCursorSnapshot(
|
||||
JNIEnv *env, jclass clazz, jintArray out) {
|
||||
if (!out) return 0;
|
||||
jint cap = (*env)->GetArrayLength(env, out);
|
||||
jint *buf = (*env)->GetIntArrayElements(env, out, NULL);
|
||||
if (!buf) return 0;
|
||||
int n = banner_cursor_snapshot((int *)buf, (int)cap);
|
||||
(*env)->ReleaseIntArrayElements(env, out, buf, 0);
|
||||
return n;
|
||||
}
|
||||
|
||||
/* Inject a key event. evdev = Linux input keycode (KEY_A=30…); state 1=down 0=up. */
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendKey(
|
||||
|
||||
@@ -57,6 +57,7 @@ import com.droiddeck.launcher.wayland.HdrSupport
|
||||
import com.droiddeck.launcher.session.SessionService
|
||||
import com.droiddeck.launcher.session.SessionState
|
||||
import com.droiddeck.launcher.ui.CursorOverlay
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import com.droiddeck.launcher.ui.DrawerActions
|
||||
import com.droiddeck.launcher.ui.HudText
|
||||
import com.droiddeck.launcher.ui.LoadingOverlay
|
||||
@@ -96,6 +97,17 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private val cursorHide = Runnable { cursorVisible = false }
|
||||
/** When the pad or the on-screen controls were last used (uptimeMillis); see [showCursor]. */
|
||||
private var lastPadInputMs = 0L
|
||||
// The program's own pointer, from wl_pointer.set_cursor (see syncClientCursor): its image in
|
||||
// session pixels, hotspot, and whether it asked for no pointer at all.
|
||||
private val cursorBuf = IntArray(WaylandCompositor.CURSOR_BUF_INTS)
|
||||
private var cursorSerial = 0
|
||||
private var clientHidesCursor = false
|
||||
private var cursorImage by mutableStateOf<androidx.compose.ui.graphics.ImageBitmap?>(null)
|
||||
private var cursorHotX by mutableStateOf(0)
|
||||
private var cursorHotY by mutableStateOf(0)
|
||||
private var cursorImageScale by mutableStateOf(1f)
|
||||
/** labwc changes the shape a moment after the move that reached an edge: look again then. */
|
||||
private val cursorResync = Runnable { if (syncClientCursor() && clientHidesCursor) cursorVisible = false }
|
||||
private val uiHandler = Handler(Looper.getMainLooper())
|
||||
private var pendingBackAction: Runnable? = null
|
||||
private var drawerDirectionKey = KeyEvent.KEYCODE_UNKNOWN
|
||||
@@ -240,7 +252,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
isFocusableInTouchMode = true
|
||||
setContent {
|
||||
DroidDeckTheme {
|
||||
CursorOverlay(cursorPos, cursorVisible, resources.displayMetrics.density)
|
||||
CursorOverlay(cursorPos, cursorVisible, resources.displayMetrics.density,
|
||||
cursorImage, cursorHotX, cursorHotY, cursorImageScale)
|
||||
if (hud.text.isNotEmpty()) HudText(hud.text)
|
||||
if (loading.visible) LoadingOverlay(loading.step, loading.percent, loading.elapsed, loading.hint, loading.ended)
|
||||
// Opening the drawer takes the controller away from the game: release its pad.
|
||||
@@ -1000,12 +1013,52 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
*/
|
||||
private fun showCursor(x: Float, y: Float) {
|
||||
cursorPos = androidx.compose.ui.geometry.Offset(x, y)
|
||||
syncClientCursor()
|
||||
uiHandler.removeCallbacks(cursorResync)
|
||||
uiHandler.postDelayed(cursorResync, 60)
|
||||
// A program that asked for no pointer (a mouse-look game) wins over everything below.
|
||||
if (clientHidesCursor) { cursorVisible = false; return }
|
||||
if (android.os.SystemClock.uptimeMillis() - lastPadInputMs < CURSOR_PAD_HOLD_MS) return
|
||||
cursorVisible = true
|
||||
uiHandler.removeCallbacks(cursorHide)
|
||||
if (SessionState.mode == SessionService.MODE_STEAM && SessionState.steamUi != "desktop") uiHandler.postDelayed(cursorHide, 2500)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulls the program's pointer (wl_pointer.set_cursor) if it changed: labwc's resize arrows at a
|
||||
* window's edges and corners, a text field's I-beam, or a request for no pointer. Serial 0 = no
|
||||
* program has set one yet, and the built-in arrow stays. Returns true when something changed.
|
||||
*/
|
||||
private fun syncClientCursor(): Boolean {
|
||||
if (!CompositorHost.isStarted) return false
|
||||
val n = WaylandCompositor.nativeCursorSnapshot(cursorBuf)
|
||||
if (n < 6) return false
|
||||
val serial = cursorBuf[0]
|
||||
if (serial == 0 || serial == cursorSerial) return false
|
||||
cursorSerial = serial
|
||||
clientHidesCursor = cursorBuf[1] != 0
|
||||
if (clientHidesCursor) return true
|
||||
val w = cursorBuf[2]
|
||||
val h = cursorBuf[3]
|
||||
if (w <= 0 || h <= 0 || n < 6 + w * h) return false
|
||||
val bitmap = try {
|
||||
android.graphics.Bitmap.createBitmap(cursorBuf, 6, w, w, h, android.graphics.Bitmap.Config.ARGB_8888)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "cursor bitmap failed", e)
|
||||
return false
|
||||
}
|
||||
cursorImage = bitmap.asImageBitmap()
|
||||
cursorHotX = cursorBuf[4]
|
||||
cursorHotY = cursorBuf[5]
|
||||
// Session pixels -> screen pixels, as the session's picture is scaled (movePointer).
|
||||
val width = surfaceView.width.takeIf { it > 0 }
|
||||
val height = surfaceView.height.takeIf { it > 0 }
|
||||
val out = SessionState.outputSize
|
||||
if (width != null && height != null)
|
||||
cursorImageScale = minOf(width / out.first.toFloat(), height / out.second.toFloat())
|
||||
return true
|
||||
}
|
||||
|
||||
private fun refreshSecondScreenDisplays() {
|
||||
if (!::displayManager.isInitialized) return
|
||||
val primaryId = display?.displayId ?: windowManager.defaultDisplay.displayId
|
||||
|
||||
@@ -6,13 +6,35 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.FilterQuality
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
|
||||
/**
|
||||
* The session's pointer. [image]: the program's own cursor (wl_pointer.set_cursor - labwc's resize
|
||||
* arrows, a text field's I-beam), in session pixels, drawn [imageScale] times larger with its hotspot
|
||||
* ([hotX], [hotY]) on [position]; null = the program has not set one, and the built-in arrow is drawn.
|
||||
*/
|
||||
@Composable
|
||||
fun CursorOverlay(position: Offset, visible: Boolean, scale: Float) {
|
||||
fun CursorOverlay(
|
||||
position: Offset, visible: Boolean, scale: Float,
|
||||
image: ImageBitmap? = null, hotX: Int = 0, hotY: Int = 0, imageScale: Float = 1f,
|
||||
) {
|
||||
if (!visible) return
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
if (image != null) {
|
||||
val w = (image.width * imageScale).toInt().coerceAtLeast(1)
|
||||
val h = (image.height * imageScale).toInt().coerceAtLeast(1)
|
||||
drawImage(
|
||||
image,
|
||||
dstOffset = androidx.compose.ui.unit.IntOffset(
|
||||
(position.x - hotX * imageScale).toInt(), (position.y - hotY * imageScale).toInt()),
|
||||
dstSize = androidx.compose.ui.unit.IntSize(w, h),
|
||||
filterQuality = FilterQuality.Low,
|
||||
)
|
||||
return@Canvas
|
||||
}
|
||||
val s = 11f * scale
|
||||
val path = Path().apply {
|
||||
moveTo(position.x, position.y)
|
||||
|
||||
@@ -171,6 +171,17 @@ public final class WaylandCompositor {
|
||||
|
||||
public static native void nativeSendTouch(int action, int pointerId, int x, int y);
|
||||
|
||||
/**
|
||||
* The client's pointer image, straight from wl_pointer.set_cursor (labwc's resize arrows, a text
|
||||
* field's I-beam). out = [serial, hidden, w, h, hotspotX, hotspotY, ARGB8888 pixels...]; returns
|
||||
* the number of ints written, or 0 if {@code out} is too small. Serial 0 = the client has not
|
||||
* set one yet; [1] = 1 means it asked for NO pointer.
|
||||
*/
|
||||
public static native int nativeCursorSnapshot(int[] out);
|
||||
|
||||
/** Enough for the header plus a 256x256 cursor, the largest the compositor snapshots. */
|
||||
public static final int CURSOR_BUF_INTS = 6 + 256 * 256;
|
||||
|
||||
/** Inject a key event. evdev = Linux input keycode (KEY_A=30…); state: 1=down, 0=up. */
|
||||
public static native void nativeSendKey(int evdev, int state);
|
||||
|
||||
|
||||
Reference in new issue
Block a user