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:
The412Banner committed 2026-09-25 08:28:26 -04:00
1 parent 347a5a4dcb
commit c331836bce
7 files changed
+305 -6

No files matched your search

+100 -4
View File
@@ -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);