diff --git a/README.md b/README.md index 2de0aaf..cda0482 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,45 @@ submap = reset bind=$mainMod,g,exec,hyprctl keyword cursor:inactive_timeout 0; hyprctl keyword cursor:hide_on_key_press false; hyprctl dispatch submap cursor ``` +## Multi-monitor support + +By default, `wl-kbptr` shows its overlay only on the currently focused output. The `--all-outputs` / `-A` flag spans the overlay across all connected outputs simultaneously, so you don't need to focus the right display before invoking it. + +> **Note:** Multi-monitor mode is implemented for **tile mode** and **floating mode**. For floating mode with `mode_floating.source=detect`, targets are detected on each output independently and combined. For floating mode with stdin input, pass areas in global coordinates. + +### Usage + +```bash +wl-kbptr -A -o modes=tile,click +``` + +Or enable it permanently in your configuration file: + +```ini +[general] +all_outputs=true +``` + +### How it works + +- One overlay surface is created per output, each covering its respective monitor. +- The first surface gets exclusive keyboard focus; the compositor routes all key events there regardless of which monitor the cursor is on. +- Each monitor is assigned its **exclusive pixels** — its full logical bounds minus any area that overlaps with a previously processed monitor. Labels are indexed continuously across all resulting regions. +- After you type a label, the cursor moves to the correct output automatically. + +This exclusive-region approach handles arbitrary overlap topologies correctly: + +| Layout | Behaviour | +| ------ | --------- | +| Side-by-side / stacked (no overlap) | One region per monitor, as expected. | +| Corner overlap | The first monitor keeps its full area; the second monitor's exclusive area is 2 strips (the non-overlapping edges). The shared corner belongs to the first monitor. | +| Landscape + portrait overlap | The first-listed monitor keeps its full area. If landscape is first, the portrait monitor covers only the strip extending beyond the landscape area. If portrait is first, landscape gets left/right columns beside the portrait. | +| Full mirror (same logical position) | The second monitor has no exclusive area and receives no labels. Its overlay surface shows only the background tint. | + +### Cell density + +Cell size is computed from the **average logical monitor area**, keeping density consistent with single-output mode — each monitor gets roughly the same number of cells as it would on its own. With multiple monitors the total label count scales with the number of outputs, so labels may require more keystrokes (e.g. 3 characters with 3 monitors). + ## Configuration `wl-kbptr` can be configured with a configuration file. See [`config.example`](./config.example) for an example and run `wl-kbptr --help-config` for help. diff --git a/config.example b/config.example index 9d6dbac..452faeb 100644 --- a/config.example +++ b/config.example @@ -7,6 +7,9 @@ home_row_keys= modes=tile,bisect cancellation_status_code=0 +# Span the overlay across all connected outputs simultaneously (tile and floating modes). +# Equivalent to the -A / --all-outputs command-line flag. +all_outputs=false [mode_tile] label_color=#fffd diff --git a/src/config.c b/src/config.c index 0c02ea1..9a9452b 100644 --- a/src/config.c +++ b/src/config.c @@ -85,6 +85,21 @@ static int parse_double(void *dest, char *value) { return 0; } +static int parse_bool(void *dest, char *value) { + bool *out = dest; + if (strcmp(value, "true") == 0 || strcmp(value, "1") == 0) { + *out = true; + } else if (strcmp(value, "false") == 0 || strcmp(value, "0") == 0) { + *out = false; + } else { + LOG_ERR( + "Invalid boolean value '%s'. Should be 'true' or 'false'.", value + ); + return 1; + } + return 0; +} + static int parse_uint8(void *dest, char *value) { int decoded = atoi(value); if (decoded < 0 || decoded >= 256) { @@ -360,7 +375,8 @@ static struct section_def section_defs[] = { general, G_FIELD(home_row_keys, "", parse_home_row_keys, free_home_row_keys), G_FIELD(modes, "tile,bisect", parse_str, free_str), - G_FIELD(cancellation_status_code, "0", parse_uint8, noop) + G_FIELD(cancellation_status_code, "0", parse_uint8, noop), + G_FIELD(all_outputs, "false", parse_bool, noop) ), SECTION( mode_tile, MT_FIELD(label_color, "#fffd", parse_color, noop), diff --git a/src/config.h b/src/config.h index 1b9c7d0..daad239 100644 --- a/src/config.h +++ b/src/config.h @@ -3,12 +3,14 @@ #include "utils.h" +#include #include struct general_config { char **home_row_keys; char *modes; uint8_t cancellation_status_code; + bool all_outputs; }; struct relative_font_size { diff --git a/src/main.c b/src/main.c index 547b6c7..cafd8b9 100644 --- a/src/main.c +++ b/src/main.c @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -23,19 +24,18 @@ #include #include -static void send_frame(struct state *state) { - int32_t scale_120 = state->fractional_scale; +static void send_frame_for_overlay(struct overlay_surface *overlay) { + struct state *state = overlay->state; + + int32_t scale_120 = overlay->fractional_scale_val; if (scale_120 == 0) { - // Falling back to the output scale if fractional scale is not received. - scale_120 = - (state->current_output == NULL ? 1 : state->current_output->scale) * - 120; + // Fall back to the output's integer scale if fractional scale not yet received. + scale_120 = (overlay->output == NULL ? 1 : overlay->output->scale) * 120; } struct surface_buffer *surface_buffer = get_next_buffer( - state->wl_shm, &state->surface_buffer_pool, - state->surface_width * scale_120 / 120, - state->surface_height * scale_120 / 120 + state->wl_shm, &overlay->surface_buffer_pool, + overlay->width * scale_120 / 120, overlay->height * scale_120 / 120 ); if (surface_buffer == NULL) { return; @@ -45,29 +45,31 @@ static void send_frame(struct state *state) { cairo_t *cairo = surface_buffer->cairo; cairo_identity_matrix(cairo); cairo_scale(cairo, scale_120 / 120.0, scale_120 / 120.0); - mode_render(state, cairo); - wl_surface_set_buffer_scale(state->wl_surface, 1); + // In all-outputs mode, translate so global coordinates rendered by the mode + // map to this output's local surface coordinates. + if (state->config.general.all_outputs && overlay->output != NULL) { + cairo_translate(cairo, -overlay->output->x, -overlay->output->y); + } - wl_surface_attach(state->wl_surface, surface_buffer->wl_buffer, 0, 0); - wp_viewport_set_destination( - state->wp_viewport, state->surface_width, state->surface_height - ); - wl_surface_damage( - state->wl_surface, 0, 0, state->surface_width, state->surface_height - ); - wl_surface_commit(state->wl_surface); + mode_render(state, cairo); + + wl_surface_set_buffer_scale(overlay->wl_surface, 1); + wl_surface_attach(overlay->wl_surface, surface_buffer->wl_buffer, 0, 0); + wp_viewport_set_destination(overlay->wp_viewport, overlay->width, overlay->height); + wl_surface_damage(overlay->wl_surface, 0, 0, overlay->width, overlay->height); + wl_surface_commit(overlay->wl_surface); } /** - * Send a 1x1px transparent surface. - * - * This is used so that the surface is shown on the screen which triggers the - * `surface.enter()` event callback. + * Send a 1x1px transparent surface to trigger the `surface.enter()` event. + * Only needed for single-output mode when the output is not yet known. */ -static void send_transparent_frame(struct state *state) { +static void send_transparent_frame_to_overlay(struct overlay_surface *overlay) { + struct state *state = overlay->state; + struct surface_buffer *surface_buffer = - get_next_buffer(state->wl_shm, &state->surface_buffer_pool, 1, 1); + get_next_buffer(state->wl_shm, &overlay->surface_buffer_pool, 1, 1); if (surface_buffer == NULL) { return; } @@ -76,22 +78,20 @@ static void send_transparent_frame(struct state *state) { cairo_set_operator(cairo, CAIRO_OPERATOR_SOURCE); cairo_set_source_rgba(cairo, 0, 0, 0, 0); cairo_fill(cairo); - wl_surface_attach(state->wl_surface, surface_buffer->wl_buffer, 0, 0); - wp_viewport_set_destination( - state->wp_viewport, state->surface_width, state->surface_height - ); - wl_surface_damage(state->wl_surface, 0, 0, 1, 1); - wl_surface_commit(state->wl_surface); + wl_surface_attach(overlay->wl_surface, surface_buffer->wl_buffer, 0, 0); + wp_viewport_set_destination(overlay->wp_viewport, overlay->width, overlay->height); + wl_surface_damage(overlay->wl_surface, 0, 0, 1, 1); + wl_surface_commit(overlay->wl_surface); } static void surface_callback_done( void *data, struct wl_callback *callback, uint32_t callback_data ) { - struct state *state = data; - send_frame(state); + struct overlay_surface *overlay = data; + send_frame_for_overlay(overlay); - wl_callback_destroy(state->wl_surface_callback); - state->wl_surface_callback = NULL; + wl_callback_destroy(overlay->wl_surface_callback); + overlay->wl_surface_callback = NULL; } const struct wl_callback_listener surface_callback_listener = { @@ -99,23 +99,49 @@ const struct wl_callback_listener surface_callback_listener = { }; static void request_frame(struct state *state) { - if (state->wl_surface_callback != NULL) { - return; + struct overlay_surface *overlay; + wl_list_for_each (overlay, &state->overlay_surfaces, link) { + if (overlay->wl_surface_callback != NULL || !overlay->configured) { + continue; + } + overlay->wl_surface_callback = wl_surface_frame(overlay->wl_surface); + wl_callback_add_listener( + overlay->wl_surface_callback, &surface_callback_listener, overlay + ); + wl_surface_commit(overlay->wl_surface); } - - state->wl_surface_callback = wl_surface_frame(state->wl_surface); - wl_callback_add_listener( - state->wl_surface_callback, &surface_callback_listener, state - ); - wl_surface_commit(state->wl_surface); } bool compute_initial_area(struct state *state, struct rect *initial_area) { + if (state->config.general.all_outputs) { + // Compute the bounding box of all outputs in global coordinates. + int32_t min_x = INT32_MAX, min_y = INT32_MAX; + int32_t max_x = INT32_MIN, max_y = INT32_MIN; + struct overlay_surface *overlay; + wl_list_for_each (overlay, &state->overlay_surfaces, link) { + struct output *o = overlay->output; + if (o == NULL) continue; + if (o->x < min_x) min_x = o->x; + if (o->y < min_y) min_y = o->y; + if (o->x + o->width > max_x) max_x = o->x + o->width; + if (o->y + o->height > max_y) max_y = o->y + o->height; + } + initial_area->x = min_x; + initial_area->y = min_y; + initial_area->w = max_x - min_x; + initial_area->h = max_y - min_y; + return true; + } + + // Single-output path: get dimensions from the one overlay surface. + struct overlay_surface *overlay = + wl_container_of(state->overlay_surfaces.next, overlay, link); + if (initial_area->w == -1) { initial_area->x = 0; initial_area->y = 0; - initial_area->w = state->surface_width; - initial_area->h = state->surface_height; + initial_area->w = overlay->width; + initial_area->h = overlay->height; } else { if (initial_area->x < 0) { initial_area->w += initial_area->x; @@ -418,28 +444,42 @@ static void load_xdg_outputs(struct state *state) { } static void enter_first_mode(struct state *state) { - if (state->current_mode == NO_MODE_ENTERED) { - if (!compute_initial_area(state, &state->initial_area)) { - state->running = false; + if (state->current_mode != NO_MODE_ENTERED) { + return; + } + + // Wait until every overlay surface is configured and has its output set. + struct overlay_surface *overlay; + wl_list_for_each (overlay, &state->overlay_surfaces, link) { + if (!overlay->configured || overlay->output == NULL) { return; } + } - LOG_DEBUG( - "Initial area: %dx%d+%d+%d", state->initial_area.w, - state->initial_area.h, state->initial_area.x, state->initial_area.y - ); + if (!compute_initial_area(state, &state->initial_area)) { + state->running = false; + return; + } + + LOG_DEBUG( + "Initial area: %dx%d+%d+%d", state->initial_area.w, + state->initial_area.h, state->initial_area.x, state->initial_area.y + ); + if (state->current_output != NULL) { LOG_DEBUG( "Output: %s (position: %dx%d+%d+%d, transform: %d)", state->current_output->name, state->current_output->width, state->current_output->height, state->current_output->x, state->current_output->y, state->current_output->transform ); + } - enter_next_mode(state, state->initial_area); + enter_next_mode(state, state->initial_area); - if (state->running) { - send_frame(state); + if (state->running) { + wl_list_for_each (overlay, &state->overlay_surfaces, link) { + send_frame_for_overlay(overlay); } } } @@ -447,12 +487,17 @@ static void enter_first_mode(struct state *state) { static void handle_surface_enter( void *data, struct wl_surface *surface, struct wl_output *wl_output ) { - struct state *state = data; - struct output *output = - find_output_from_wl_output(&state->outputs, wl_output); - state->current_output = output; + struct overlay_surface *overlay = data; + struct state *state = overlay->state; + + // Only update output if not already known (single-output, no -O/-r flag). + if (overlay->output == NULL) { + overlay->output = + find_output_from_wl_output(&state->outputs, wl_output); + state->current_output = overlay->output; + } - if (state->surface_configured) { + if (overlay->configured) { enter_first_mode(state); } } @@ -542,25 +587,29 @@ static void handle_layer_surface_configure( void *data, struct zwlr_layer_surface_v1 *layer_surface, uint32_t serial, uint32_t width, uint32_t height ) { - struct state *state = data; - state->surface_width = width; - state->surface_height = height; + struct overlay_surface *overlay = data; + struct state *state = overlay->state; + + overlay->width = width; + overlay->height = height; zwlr_layer_surface_v1_ack_configure(layer_surface, serial); - if (state->current_output != NULL) { + bool was_configured = overlay->configured; + overlay->configured = true; + + if (overlay->output != NULL) { enter_first_mode(state); - } else if (!state->surface_configured) { - send_transparent_frame(state); + } else if (!was_configured) { + // Output not yet known; send a transparent frame to get surface.enter. + send_transparent_frame_to_overlay(overlay); } - - state->surface_configured = true; } static void handle_layer_surface_closed( void *data, struct zwlr_layer_surface_v1 *layer_surface ) { - struct state *state = data; - state->running = false; + struct overlay_surface *overlay = data; + overlay->state->running = false; } const struct zwlr_layer_surface_v1_listener wl_layer_surface_listener = { @@ -571,12 +620,12 @@ const struct zwlr_layer_surface_v1_listener wl_layer_surface_listener = { static void fractional_scale_preferred( void *data, struct wp_fractional_scale_v1 *fractional_scale, uint32_t scale ) { - struct state *state = data; - int32_t old_scale = state->fractional_scale; - state->fractional_scale = scale; + struct overlay_surface *overlay = data; + int32_t old_scale = overlay->fractional_scale_val; + overlay->fractional_scale_val = scale; if (old_scale != 0 && old_scale != scale) { - request_frame(state); + request_frame(overlay->state); } } @@ -632,6 +681,65 @@ static void print_result(struct state *state) { ); } +/** + * In all-outputs mode the result rect is in global coordinates. Find which + * output contains the result centre, make `state->current_output` point to it, + * and convert the result rect to output-local coordinates so that move_pointer + * (which works in output-local space) functions correctly. + */ +static void resolve_result_output(struct state *state) { + if (!state->config.general.all_outputs) { + return; + } + + int32_t cx = state->result.x + state->result.w / 2; + int32_t cy = state->result.y + state->result.h / 2; + struct output *output; + + // First try: exact hit. + wl_list_for_each (output, &state->outputs, link) { + if (cx >= output->x && cx < output->x + output->width && + cy >= output->y && cy < output->y + output->height) { + state->current_output = output; + state->result.x -= output->x; + state->result.y -= output->y; + return; + } + } + + // Result centre is not on any output (e.g. a mode that doesn't yet handle + // per-region placement). Find the nearest output and snap to its edge. + struct output *best = NULL; + int32_t best_dist = INT32_MAX; + wl_list_for_each (output, &state->outputs, link) { + int32_t dx = cx < output->x ? output->x - cx + : cx >= output->x + output->width ? cx - (output->x + output->width - 1) + : 0; + int32_t dy = cy < output->y ? output->y - cy + : cy >= output->y + output->height ? cy - (output->y + output->height - 1) + : 0; + int32_t dist = dx + dy; + if (dist < best_dist) { + best_dist = dist; + best = output; + } + } + + if (best == NULL) { + best = wl_container_of(state->outputs.next, best, link); + } + + // Clamp the result rect so it falls within the chosen output. + int32_t snapped_cx = max(best->x, min(cx, best->x + best->width - 1)); + int32_t snapped_cy = max(best->y, min(cy, best->y + best->height - 1)); + state->result.x = snapped_cx - state->result.w / 2; + state->result.y = snapped_cy - state->result.h / 2; + + state->current_output = best; + state->result.x -= best->x; + state->result.y -= best->y; +} + static void print_usage() { puts("wl-kbptr [OPTION...]\n"); @@ -642,6 +750,7 @@ static void print_usage() { puts(" -r, --restrict=AREA restrict to given area (wxh+x+y)"); puts(" -o, --option set configuration option"); puts(" -O, --output specify display output to use"); + puts(" -A, --all-outputs show overlay on all outputs simultaneously"); puts(" -p, --only-print only print, don't move the cursor or click"); } @@ -653,24 +762,99 @@ static void print_version() { puts(""); } +/** + * Allocate, initialise, and wire up a single overlay_surface for `output`. + * If `output` is NULL the compositor will choose (single-output, no -O flag). + * `keyboard` controls whether this surface gets exclusive keyboard focus. + */ +static struct overlay_surface *create_overlay_surface( + struct state *state, struct output *output, bool keyboard +) { + struct overlay_surface *overlay = calloc(1, sizeof(*overlay)); + overlay->state = state; + overlay->output = output; + + surface_buffer_pool_init(&overlay->surface_buffer_pool); + + overlay->wl_surface = wl_compositor_create_surface(state->wl_compositor); + wl_surface_add_listener(overlay->wl_surface, &surface_listener, overlay); + + overlay->wl_layer_surface = zwlr_layer_shell_v1_get_layer_surface( + state->wl_layer_shell, overlay->wl_surface, + output == NULL ? NULL : output->wl_output, + ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, "selection" + ); + zwlr_layer_surface_v1_add_listener( + overlay->wl_layer_surface, &wl_layer_surface_listener, overlay + ); + zwlr_layer_surface_v1_set_exclusive_zone(overlay->wl_layer_surface, -1); + zwlr_layer_surface_v1_set_anchor( + overlay->wl_layer_surface, + ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT | + ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM + ); + zwlr_layer_surface_v1_set_keyboard_interactivity( + overlay->wl_layer_surface, keyboard + ); + + if (state->fractional_scale_mgr) { + overlay->fractional_scale = + wp_fractional_scale_manager_v1_get_fractional_scale( + state->fractional_scale_mgr, overlay->wl_surface + ); + wp_fractional_scale_v1_add_listener( + overlay->fractional_scale, &fractional_scale_listener, overlay + ); + } + + overlay->wp_viewport = + wp_viewporter_get_viewport(state->wp_viewporter, overlay->wl_surface); + + // Empty input region so the overlay doesn't capture pointer events. + struct wl_region *region = + wl_compositor_create_region(state->wl_compositor); + wl_region_add(region, 0, 0, 0, 0); + wl_surface_set_input_region(overlay->wl_surface, region); + wl_region_destroy(region); + + wl_surface_commit(overlay->wl_surface); + + return overlay; +} + +static void free_overlay_surface(struct overlay_surface *overlay) { + if (overlay->fractional_scale) { + wp_fractional_scale_v1_destroy(overlay->fractional_scale); + } + wp_viewport_destroy(overlay->wp_viewport); + zwlr_layer_surface_v1_destroy(overlay->wl_layer_surface); + wl_surface_destroy(overlay->wl_surface); + surface_buffer_pool_destroy(&overlay->surface_buffer_pool); + wl_list_remove(&overlay->link); + free(overlay); +} + +static void free_overlay_surfaces(struct wl_list *overlay_surfaces) { + struct overlay_surface *overlay; + struct overlay_surface *tmp; + wl_list_for_each_safe (overlay, tmp, overlay_surfaces, link) { + free_overlay_surface(overlay); + } +} + int main(int argc, char **argv) { struct state state = { - .wl_display = NULL, - .wl_registry = NULL, - .wl_compositor = NULL, - .wl_shm = NULL, - .wl_layer_shell = NULL, - .wl_surface = NULL, - .wl_surface_callback = NULL, - .wl_layer_surface = NULL, - .surface_configured = false, + .wl_display = NULL, + .wl_registry = NULL, + .wl_compositor = NULL, + .wl_shm = NULL, + .wl_layer_shell = NULL, #if OPENCV_ENABLED .wl_screencopy_manager = NULL, #endif .wp_viewporter = NULL, .fractional_scale_mgr = NULL, .running = true, - .fractional_scale = 0, .result = (struct rect){-1, -1, -1, -1}, .initial_area = (struct rect){-1, -1, -1, -1}, .home_row = (char *[]){"", "", "", "", "", "", "", "", "", "", ""}, @@ -688,6 +872,7 @@ int main(int argc, char **argv) { {"restrict", required_argument, 0, 'r'}, {"config", required_argument, 0, 'c'}, {"output", required_argument, 0, 'O'}, + {"all-outputs", no_argument, 0, 'A'}, {"only-print", no_argument, 0, 'p'}, {NULL, 0, NULL, 0} }; @@ -701,7 +886,7 @@ int main(int argc, char **argv) { char *selected_output_name = NULL; bool only_print = false; while ((option_char = getopt_long( - argc, argv, "hvr:o:c:O:Rp", long_options, &option_index + argc, argv, "hvr:o:c:O:ARp", long_options, &option_index )) != -1) { switch (option_char) { case 'h': @@ -747,6 +932,17 @@ int main(int argc, char **argv) { selected_output_name = strdup(optarg); break; + case 'A': + // Push as a cli_config so it is applied after the config file, + // giving the CLI flag precedence over any file setting. + if (num_cli_configs >= cli_configs_len) { + cli_configs_len += 10; + cli_configs = + realloc(cli_configs, cli_configs_len * sizeof(char *)); + } + cli_configs[num_cli_configs++] = "all_outputs=true"; + break; + case 'p': only_print = true; break; @@ -776,6 +972,16 @@ int main(int argc, char **argv) { free(cli_configs); cli_configs = NULL; + if (state.config.general.all_outputs && selected_output_name != NULL) { + LOG_ERR("--all-outputs and --output are mutually exclusive."); + return 1; + } + + if (state.config.general.all_outputs && state.initial_area.w != -1) { + LOG_ERR("--all-outputs and --restrict are mutually exclusive."); + return 1; + } + if (state.config.general.home_row_keys != NULL) { state.home_row = state.config.general.home_row_keys; } @@ -787,6 +993,7 @@ int main(int argc, char **argv) { wl_list_init(&state.outputs); wl_list_init(&state.seats); + wl_list_init(&state.overlay_surfaces); state.wl_display = wl_display_connect(NULL); if (state.wl_display == NULL) { @@ -839,85 +1046,55 @@ int main(int argc, char **argv) { // home row keys. wl_display_roundtrip(state.wl_display); - if (selected_output_name) { - state.current_output = - find_output_by_name(&state, selected_output_name); - - if (!state.current_output) { - LOG_ERR("Could not find output '%s'.", selected_output_name); - return 1; + if (state.config.general.all_outputs) { + // Create one overlay surface per output. Only the first gets keyboard + // interactivity; the compositor routes all keys there via exclusive grab. + bool first = true; + struct output *output; + wl_list_for_each (output, &state.outputs, link) { + struct overlay_surface *overlay = + create_overlay_surface(&state, output, first); + wl_list_insert(state.overlay_surfaces.prev, &overlay->link); + first = false; } - - free(selected_output_name); - selected_output_name = NULL; - } else if (state.initial_area.w != -1) { - state.current_output = - find_output_from_rect(&state, &state.initial_area); - - if (!state.current_output) { - LOG_ERR("Could not find output containing given area."); - return 1; + } else { + // Single-output mode: resolve the target output from -O / -r flags. + if (selected_output_name) { + state.current_output = + find_output_by_name(&state, selected_output_name); + if (!state.current_output) { + LOG_ERR("Could not find output '%s'.", selected_output_name); + return 1; + } + free(selected_output_name); + selected_output_name = NULL; + } else if (state.initial_area.w != -1) { + state.current_output = + find_output_from_rect(&state, &state.initial_area); + if (!state.current_output) { + LOG_ERR("Could not find output containing given area."); + return 1; + } + state.initial_area.x -= state.current_output->x; + state.initial_area.y -= state.current_output->y; } - state.initial_area.x -= state.current_output->x; - state.initial_area.y -= state.current_output->y; - } - - surface_buffer_pool_init(&state.surface_buffer_pool); - - state.wl_surface = wl_compositor_create_surface(state.wl_compositor); - wl_surface_add_listener(state.wl_surface, &surface_listener, &state); - state.wl_layer_surface = zwlr_layer_shell_v1_get_layer_surface( - state.wl_layer_shell, state.wl_surface, - state.current_output == NULL ? NULL : state.current_output->wl_output, - ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, "selection" - ); - zwlr_layer_surface_v1_add_listener( - state.wl_layer_surface, &wl_layer_surface_listener, &state - ); - zwlr_layer_surface_v1_set_exclusive_zone(state.wl_layer_surface, -1); - zwlr_layer_surface_v1_set_anchor( - state.wl_layer_surface, ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | - ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT | - ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | - ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM - ); - zwlr_layer_surface_v1_set_keyboard_interactivity( - state.wl_layer_surface, true - ); - - struct wp_fractional_scale_v1 *fractional_scale = NULL; - if (state.fractional_scale_mgr) { - fractional_scale = wp_fractional_scale_manager_v1_get_fractional_scale( - state.fractional_scale_mgr, state.wl_surface - ); - wp_fractional_scale_v1_add_listener( - fractional_scale, &fractional_scale_listener, &state - ); + struct overlay_surface *overlay = + create_overlay_surface(&state, state.current_output, true); + wl_list_insert(&state.overlay_surfaces, &overlay->link); } - state.wp_viewport = - wp_viewporter_get_viewport(state.wp_viewporter, state.wl_surface); - - struct wl_region *wl_region = - wl_compositor_create_region(state.wl_compositor); - wl_region_add(wl_region, 0, 0, 0, 0); - wl_surface_set_input_region(state.wl_surface, wl_region); - - wl_surface_commit(state.wl_surface); while (state.running && wl_display_dispatch(state.wl_display)) {} - wp_viewport_destroy(state.wp_viewport); + wl_display_roundtrip(state.wl_display); - zwlr_layer_surface_v1_destroy(state.wl_layer_surface); - wl_surface_destroy(state.wl_surface); - wl_region_destroy(wl_region); + free_overlay_surfaces(&state.overlay_surfaces); - surface_buffer_pool_destroy(&state.surface_buffer_pool); wl_display_roundtrip(state.wl_display); int status_code = 0; if (state.result.x != -1) { + resolve_result_output(&state); print_result(&state); if (!only_print) { move_pointer( @@ -939,7 +1116,6 @@ int main(int argc, char **argv) { zxdg_output_manager_v1_destroy(state.xdg_output_manager); if (state.fractional_scale_mgr) { - wp_fractional_scale_v1_destroy(fractional_scale); wp_fractional_scale_manager_v1_destroy(state.fractional_scale_mgr); } diff --git a/src/mode_floating.c b/src/mode_floating.c index 4d927d0..9a32be3 100644 --- a/src/mode_floating.c +++ b/src/mode_floating.c @@ -61,7 +61,8 @@ static void get_area_from_screenshot( area.h -= 2; area.w -= 2; - struct scrcpy_buffer *scrcpy_buffer = query_screenshot(state, area); + struct scrcpy_buffer *scrcpy_buffer = + query_screenshot(state, state->current_output->wl_output, area); enum wl_output_transform output_transform = state->current_output->transform; ms->num_areas = compute_target_from_img_buffer( @@ -72,6 +73,55 @@ static void get_area_from_screenshot( destroy_scrcpy_buffer(scrcpy_buffer); } +// Detect targets on all outputs and combine results into global coordinates. +static void get_areas_from_all_screenshots( + struct state *state, struct floating_mode_state *ms +) { + size_t areas_cap = 256; + struct rect *all_areas = malloc(sizeof(struct rect) * areas_cap); + int total = 0; + + struct overlay_surface *ov; + wl_list_for_each (ov, &state->overlay_surfaces, link) { + if (ov->output == NULL) continue; + struct output *o = ov->output; + + // Capture the full output, excluding a 1px border to avoid window frame + // edges interfering with contour detection. + struct rect region = { + .x = 1, .y = 1, .w = o->width - 2, .h = o->height - 2, + }; + + struct scrcpy_buffer *buf = + query_screenshot(state, o->wl_output, region); + if (buf == NULL) continue; + + struct rect *detected = NULL; + int n = compute_target_from_img_buffer( + buf->data, buf->height, buf->width, buf->stride, buf->format, + o->transform, region, &detected + ); + destroy_scrcpy_buffer(buf); + + // Shift output-local coordinates to global coordinates. + for (int i = 0; i < n; i++) { + if (total >= (int)areas_cap) { + areas_cap *= 2; + all_areas = realloc(all_areas, sizeof(struct rect) * areas_cap); + } + all_areas[total].x = detected[i].x + o->x; + all_areas[total].y = detected[i].y + o->y; + all_areas[total].w = detected[i].w; + all_areas[total].h = detected[i].h; + total++; + } + free(detected); + } + + ms->areas = all_areas; + ms->num_areas = total; +} + #endif void *floating_mode_enter(struct state *state, struct rect area) { @@ -93,7 +143,11 @@ void *floating_mode_enter(struct state *state, struct rect area) { break; case FLOATING_MODE_SOURCE_DETECT: #if OPENCV_ENABLED - get_area_from_screenshot(state, ms, area); + if (state->config.general.all_outputs) { + get_areas_from_all_screenshots(state, ms); + } else { + get_area_from_screenshot(state, ms, area); + } #else // This should not happen as the value is checked when loading the // configuration. diff --git a/src/mode_tile.c b/src/mode_tile.c index 2e3af52..0714ce3 100644 --- a/src/mode_tile.c +++ b/src/mode_tile.c @@ -12,31 +12,50 @@ #include #define MIN_SUB_AREA_SIZE (25 * 50) +// Upper bound on pending sub-rectangles when computing one output's exclusive +// area. In any real monitor layout this will never be reached. +#define MAX_PENDING_RECTS 64 + +// Returns the intersection of a and b. w or h will be 0 if they are disjoint. +static struct rect rect_intersect(struct rect a, struct rect b) { + int32_t x1 = max(a.x, b.x); + int32_t y1 = max(a.y, b.y); + int32_t x2 = min(a.x + a.w, b.x + b.w); + int32_t y2 = min(a.y + a.h, b.y + b.h); + return (struct rect){ + .x = x1, .y = y1, + .w = x2 > x1 ? x2 - x1 : 0, + .h = y2 > y1 ? y2 - y1 : 0, + }; +} + +// Subtract rectangle b from rectangle a using a cross decomposition: +// full-height left/right strips, then top/bottom middle strips. +// Stores up to 4 non-empty results in out[] and returns the count. +// If a and b do not intersect, returns 1 with out[0] = a. +static int rect_subtract(struct rect a, struct rect b, struct rect out[4]) { + struct rect i = rect_intersect(a, b); + if (i.w <= 0 || i.h <= 0) { + out[0] = a; + return 1; + } + int n = 0; + if (i.x > a.x) + out[n++] = (struct rect){.x = a.x, .y = a.y, .w = i.x - a.x, .h = a.h}; + if (i.x + i.w < a.x + a.w) + out[n++] = (struct rect){.x = i.x + i.w, .y = a.y, .w = (a.x + a.w) - (i.x + i.w), .h = a.h}; + if (i.y > a.y) + out[n++] = (struct rect){.x = i.x, .y = a.y, .w = i.w, .h = i.y - a.y}; + if (i.y + i.h < a.y + a.h) + out[n++] = (struct rect){.x = i.x, .y = i.y + i.h, .w = i.w, .h = (a.y + a.h) - (i.y + i.h)}; + return n; +} void *tile_mode_enter(struct state *state, struct rect area) { - struct tile_mode_state *ms = malloc(sizeof(*ms)); + struct tile_mode_state *ms = calloc(1, sizeof(*ms)); ms->area = area; const int max_num_sub_areas = 26 * 26; - const int area_size = ms->area.w * ms->area.h; - const int sub_area_size = - max(area_size / max_num_sub_areas, MIN_SUB_AREA_SIZE); - - ms->sub_area_height = sqrt(sub_area_size / 2.); - ms->sub_area_rows = ms->area.h / ms->sub_area_height; - if (ms->sub_area_rows == 0) { - ms->sub_area_rows = 1; - } - ms->sub_area_height_off = ms->area.h % ms->sub_area_rows; - ms->sub_area_height = ms->area.h / ms->sub_area_rows; - - ms->sub_area_width = sqrt(sub_area_size * 2); - ms->sub_area_columns = ms->area.w / ms->sub_area_width; - if (ms->sub_area_columns == 0) { - ms->sub_area_columns = 1; - } - ms->sub_area_width_off = ms->area.w % ms->sub_area_columns; - ms->sub_area_width = ms->area.w / ms->sub_area_columns; ms->label_symbols = label_symbols_from_str(state->config.mode_tile.label_symbols); @@ -46,9 +65,123 @@ void *tile_mode_enter(struct state *state, struct rect area) { return ms; } - ms->label_selection = label_selection_new( - ms->label_symbols, ms->sub_area_rows * ms->sub_area_columns - ); + if (state->config.general.all_outputs && + !wl_list_empty(&state->overlay_surfaces)) { + // Exclusive-region approach: each output is assigned only the pixels + // that belong exclusively to it — its full bounds minus any area + // already claimed by a previously processed output. This correctly + // handles any overlap topology: side-by-side, corner overlap, + // landscape+portrait, and full mirror (which yields no exclusive area + // for the second output and therefore no labels there). + + // Count monitors and compute average monitor area for a consistent + // cell size across all regions. + int64_t total_area = 0; + int n = 0; + struct overlay_surface *ov; + wl_list_for_each (ov, &state->overlay_surfaces, link) { + if (ov->output != NULL) { + total_area += (int64_t)ov->output->width * ov->output->height; + n++; + } + } + if (n == 0) { + ms->label_selection = NULL; + state->running = false; + return ms; + } + + int32_t avg_area = (int32_t)(total_area / n); + int sub_area_size = max(avg_area / max_num_sub_areas, MIN_SUB_AREA_SIZE); + int cell_h = max((int)sqrt(sub_area_size / 2.), 1); + int cell_w = max((int)sqrt(sub_area_size * 2.), 1); + + // Each output can produce at most MAX_PENDING_RECTS exclusive + // sub-rectangles, so allocate that many slots per output. + ms->regions = malloc((size_t)n * MAX_PENDING_RECTS * sizeof(struct tile_region)); + ms->num_regions = 0; + int label_offset = 0; + + // Full bounds of already-processed outputs, for subtraction. + struct rect *processed = malloc((size_t)n * sizeof(struct rect)); + int n_processed = 0; + + wl_list_for_each (ov, &state->overlay_surfaces, link) { + if (ov->output == NULL) continue; + struct output *o = ov->output; + + // Ping-pong buffers: subtract each prior output's bounds from the + // current pending set to obtain this output's exclusive rectangles. + struct rect ping[MAX_PENDING_RECTS], pong[MAX_PENDING_RECTS]; + struct rect *cur = ping, *nxt = pong; + int n_cur = 1; + cur[0] = (struct rect){.x = o->x, .y = o->y, .w = o->width, .h = o->height}; + + for (int pi = 0; pi < n_processed && n_cur > 0; pi++) { + int n_nxt = 0; + for (int ri = 0; ri < n_cur; ri++) { + struct rect out4[4]; + int cnt = rect_subtract(cur[ri], processed[pi], out4); + for (int k = 0; k < cnt; k++) { + if (n_nxt < MAX_PENDING_RECTS) + nxt[n_nxt++] = out4[k]; + } + } + struct rect *tmp = cur; cur = nxt; nxt = tmp; + n_cur = n_nxt; + } + + // Create one tile region per exclusive sub-rectangle. + for (int ri = 0; ri < n_cur; ri++) { + struct rect r_area = cur[ri]; + if (r_area.w <= 0 || r_area.h <= 0 || + ms->num_regions >= n * MAX_PENDING_RECTS) { + continue; + } + struct tile_region *r = &ms->regions[ms->num_regions++]; + r->area = r_area; + r->rows = max(r_area.h / cell_h, 1); + r->cols = max(r_area.w / cell_w, 1); + r->cell_h = r_area.h / r->rows; + r->cell_h_off = r_area.h % r->rows; + r->cell_w = r_area.w / r->cols; + r->cell_w_off = r_area.w % r->cols; + r->label_offset = label_offset; + r->num_labels = r->rows * r->cols; + label_offset += r->num_labels; + } + + processed[n_processed++] = + (struct rect){.x = o->x, .y = o->y, .w = o->width, .h = o->height}; + } + + free(processed); + ms->label_selection = label_selection_new(ms->label_symbols, label_offset); + } else { + // Single-output path: flat grid over the whole area. + int32_t density_area = ms->area.w * ms->area.h; + int sub_area_size = + max(density_area / max_num_sub_areas, MIN_SUB_AREA_SIZE); + + ms->sub_area_height = sqrt(sub_area_size / 2.); + ms->sub_area_rows = ms->area.h / ms->sub_area_height; + if (ms->sub_area_rows == 0) { + ms->sub_area_rows = 1; + } + ms->sub_area_height_off = ms->area.h % ms->sub_area_rows; + ms->sub_area_height = ms->area.h / ms->sub_area_rows; + + ms->sub_area_width = sqrt(sub_area_size * 2); + ms->sub_area_columns = ms->area.w / ms->sub_area_width; + if (ms->sub_area_columns == 0) { + ms->sub_area_columns = 1; + } + ms->sub_area_width_off = ms->area.w % ms->sub_area_columns; + ms->sub_area_width = ms->area.w / ms->sub_area_columns; + + int total_cells = ms->sub_area_rows * ms->sub_area_columns; + ms->label_selection = label_selection_new(ms->label_symbols, total_cells); + } ms->label_font_face = cairo_toy_font_face_create( state->config.mode_tile.label_font_family, CAIRO_FONT_SLANT_NORMAL, @@ -109,11 +242,40 @@ static bool tile_mode_key( label_selection_append(ms->label_selection, symbol_idx); - int idx = label_selection_to_idx(ms->label_selection); - if (idx >= 0) { - enter_next_mode( - state, idx_to_rect(ms, idx, ms->area.x, ms->area.y) - ); + int label_idx = label_selection_to_idx(ms->label_selection); + if (label_idx >= 0) { + if (ms->regions != NULL) { + // Find which region this label belongs to, then compute the + // cell rect within that region. + for (int ri = 0; ri < ms->num_regions; ri++) { + struct tile_region *r = &ms->regions[ri]; + if (label_idx < r->label_offset || + label_idx >= r->label_offset + r->num_labels) { + continue; + } + int local = label_idx - r->label_offset; + int col = local / r->rows; + int row = local % r->rows; + int x = col * r->cell_w + min(col, r->cell_w_off); + int w = r->cell_w + (col < r->cell_w_off ? 1 : 0); + int y = row * r->cell_h + min(row, r->cell_h_off); + int h = r->cell_h + (row < r->cell_h_off ? 1 : 0); + enter_next_mode( + state, + (struct rect){ + .x = r->area.x + x, + .y = r->area.y + y, + .w = w, + .h = h, + } + ); + break; + } + } else { + enter_next_mode( + state, idx_to_rect(ms, label_idx, ms->area.x, ms->area.y) + ); + } } return true; } @@ -121,94 +283,146 @@ static bool tile_mode_key( return false; } +// Render one selectable cell at position (x, y) with size (w, h). +// curr_label is the label for this cell; selection is the current user input. +static void render_cell( + struct mode_tile_config *config, cairo_t *cairo, + label_selection_t *curr_label, label_selection_t *selection, + int x, int y, int w, int h, + char *label_selected_str, char *label_unselected_str +) { + const bool selectable = label_selection_is_included(curr_label, selection); + cairo_set_operator(cairo, CAIRO_OPERATOR_SOURCE); + if (!selectable) { + return; + } + + cairo_set_source_u32(cairo, config->selectable_bg_color); + cairo_rectangle(cairo, x, y, w, h); + cairo_fill(cairo); + + cairo_set_source_u32(cairo, config->selectable_border_color); + cairo_rectangle(cairo, x + .5, y + .5, w - 1, h - 1); + cairo_set_line_width(cairo, 1); + cairo_stroke(cairo); + + cairo_text_extents_t te_all; + label_selection_str(curr_label, label_selected_str); + cairo_text_extents(cairo, label_selected_str, &te_all); + + label_selection_str_split( + curr_label, label_selected_str, label_unselected_str, selection->next + ); + + cairo_text_extents_t te_selected, te_unselected; + cairo_text_extents(cairo, label_selected_str, &te_selected); + cairo_text_extents(cairo, label_unselected_str, &te_unselected); + + cairo_move_to( + cairo, + x + (w - te_selected.x_advance - te_unselected.x_advance) / 2, + y + (int)((h + te_all.height) / 2) + ); + cairo_set_source_u32(cairo, config->label_select_color); + cairo_show_text(cairo, label_selected_str); + cairo_set_source_u32(cairo, config->label_color); + cairo_show_text(cairo, label_unselected_str); +} + void tile_mode_render(struct state *state, void *mode_state, cairo_t *cairo) { struct mode_tile_config *config = &state->config.mode_tile; struct tile_mode_state *ms = mode_state; + // Font size: for regions use the first region's cell height, otherwise + // the single-output cell height. + int ref_cell_h = (ms->regions != NULL && ms->num_regions > 0) + ? ms->regions[0].cell_h + : ms->sub_area_height; cairo_set_font_face(cairo, ms->label_font_face); cairo_set_font_size( - cairo, compute_relative_font_size( - &config->label_font_size, ms->sub_area_height - ) + cairo, + compute_relative_font_size(&config->label_font_size, ref_cell_h) ); + // Paint background over the whole surface. cairo_set_operator(cairo, CAIRO_OPERATOR_SOURCE); cairo_set_source_u32(cairo, config->unselectable_bg_color); cairo_paint(cairo); - cairo_translate(cairo, ms->area.x, ms->area.y); - - cairo_set_source_u32(cairo, config->unselectable_bg_color); - cairo_rectangle(cairo, .5, .5, ms->area.w - 1, ms->area.h - 1); - cairo_set_line_width(cairo, 1); - cairo_stroke(cairo); - - label_selection_t *curr_label = label_selection_new( - ms->label_symbols, ms->sub_area_columns * ms->sub_area_rows - ); - label_selection_set_from_idx(curr_label, 0); + int num_labels = ms->label_selection->num_labels; + label_selection_t *curr_label = + label_selection_new(ms->label_symbols, num_labels); int label_str_max_len = label_selection_str_max_len(curr_label) + 1; char label_selected_str[label_str_max_len]; char label_unselected_str[label_str_max_len]; - for (int i = 0; i < ms->sub_area_columns; i++) { - for (int j = 0; j < ms->sub_area_rows; j++) { - const int x = - i * ms->sub_area_width + min(i, ms->sub_area_width_off); - const int w = - ms->sub_area_width + (i < ms->sub_area_width_off ? 1 : 0); - const int y = - j * ms->sub_area_height + min(j, ms->sub_area_height_off); - const int h = - ms->sub_area_height + (j < ms->sub_area_height_off ? 1 : 0); - - const bool selectable = - label_selection_is_included(curr_label, ms->label_selection); - - cairo_set_operator(cairo, CAIRO_OPERATOR_SOURCE); - if (selectable) { - cairo_set_source_u32(cairo, config->selectable_bg_color); - cairo_rectangle(cairo, x, y, w, h); - cairo_fill(cairo); - - cairo_set_source_u32(cairo, config->selectable_border_color); - cairo_rectangle(cairo, x + .5, y + .5, w - 1, h - 1); - cairo_set_line_width(cairo, 1); - cairo_stroke(cairo); - - cairo_text_extents_t te_all; - label_selection_str(curr_label, label_selected_str); - cairo_text_extents(cairo, label_selected_str, &te_all); - - label_selection_str_split( - curr_label, label_selected_str, label_unselected_str, - ms->label_selection->next - ); - - cairo_text_extents_t te_selected, te_unselected; - cairo_text_extents(cairo, label_selected_str, &te_selected); - cairo_text_extents(cairo, label_unselected_str, &te_unselected); + if (ms->regions != NULL) { + // Render cells in each exclusive sub-region. + for (int ri = 0; ri < ms->num_regions; ri++) { + struct tile_region *r = &ms->regions[ri]; - // Centers the label. - cairo_move_to( - cairo, - x + (w - te_selected.x_advance - te_unselected.x_advance) / - 2, - y + (int)((h + te_all.height) / 2) + // Draw region outline. + cairo_set_source_u32(cairo, config->unselectable_bg_color); + cairo_rectangle( + cairo, r->area.x + .5, r->area.y + .5, + r->area.w - 1, r->area.h - 1 + ); + cairo_set_line_width(cairo, 1); + cairo_stroke(cairo); + + label_selection_set_from_idx(curr_label, r->label_offset); + + for (int li = 0; li < r->num_labels; li++) { + int col = li / r->rows; + int row = li % r->rows; + int x = r->area.x + col * r->cell_w + min(col, r->cell_w_off); + int w = r->cell_w + (col < r->cell_w_off ? 1 : 0); + int y = r->area.y + row * r->cell_h + min(row, r->cell_h_off); + int h = r->cell_h + (row < r->cell_h_off ? 1 : 0); + + render_cell( + config, cairo, curr_label, ms->label_selection, + x, y, w, h, label_selected_str, label_unselected_str ); - cairo_set_source_u32(cairo, config->label_select_color); - cairo_show_text(cairo, label_selected_str); - cairo_set_source_u32(cairo, config->label_color); - cairo_show_text(cairo, label_unselected_str); + label_selection_incr(curr_label); } - + } + } else { + // Single-output flat grid. + cairo_translate(cairo, ms->area.x, ms->area.y); + + cairo_set_source_u32(cairo, config->unselectable_bg_color); + cairo_rectangle(cairo, .5, .5, ms->area.w - 1, ms->area.h - 1); + cairo_set_line_width(cairo, 1); + cairo_stroke(cairo); + + label_selection_set_from_idx(curr_label, 0); + + for (int li = 0; li < num_labels; li++) { + int column = li / ms->sub_area_rows; + int row = li % ms->sub_area_rows; + + int x = column * ms->sub_area_width + + min(column, ms->sub_area_width_off); + int w = ms->sub_area_width + + (column < ms->sub_area_width_off ? 1 : 0); + int y = + row * ms->sub_area_height + min(row, ms->sub_area_height_off); + int h = ms->sub_area_height + + (row < ms->sub_area_height_off ? 1 : 0); + + render_cell( + config, cairo, curr_label, ms->label_selection, + x, y, w, h, label_selected_str, label_unselected_str + ); label_selection_incr(curr_label); } + + cairo_translate(cairo, -ms->area.x, -ms->area.y); } label_selection_free(curr_label); - cairo_translate(cairo, -ms->area.x, -ms->area.y); } void tile_mode_state_free(void *mode_state) { @@ -216,6 +430,7 @@ void tile_mode_state_free(void *mode_state) { cairo_font_face_destroy(ms->label_font_face); label_selection_free(ms->label_selection); label_symbols_free(ms->label_symbols); + free(ms->regions); free(ms); } diff --git a/src/screencopy.c b/src/screencopy.c index 35399a3..9e3629d 100644 --- a/src/screencopy.c +++ b/src/screencopy.c @@ -121,7 +121,7 @@ const struct zwlr_screencopy_frame_v1_listener screencopy_frame_listener = { }; struct scrcpy_buffer * -query_screenshot(struct state *state, struct rect region) { +query_screenshot(struct state *state, struct wl_output *wl_output, struct rect region) { struct scrcpy_state scrcpy_state; scrcpy_state.wl_shm = state->wl_shm; @@ -137,8 +137,7 @@ query_screenshot(struct state *state, struct rect region) { scrcpy_state.wl_screencopy_frame = zwlr_screencopy_manager_v1_capture_output_region( state->wl_screencopy_manager, false, - state->current_output->wl_output, region.x, region.y, region.w, - region.h + wl_output, region.x, region.y, region.w, region.h ); zwlr_screencopy_frame_v1_add_listener( scrcpy_state.wl_screencopy_frame, &screencopy_frame_listener, diff --git a/src/screencopy.h b/src/screencopy.h index 9136f60..e844dba 100644 --- a/src/screencopy.h +++ b/src/screencopy.h @@ -16,7 +16,9 @@ struct scrcpy_buffer { struct state; struct rect; -struct scrcpy_buffer *query_screenshot(struct state *state, struct rect region); +struct scrcpy_buffer *query_screenshot( + struct state *state, struct wl_output *wl_output, struct rect region +); void destroy_scrcpy_buffer(struct scrcpy_buffer *buf); diff --git a/src/state.h b/src/state.h index 9bbafd4..1c1acb0 100644 --- a/src/state.h +++ b/src/state.h @@ -39,13 +39,32 @@ struct mode_interface; +// One exclusive sub-rectangle of an output in all-outputs mode. +// A single output may produce multiple tile_region structs if its bounds +// partially overlap with a previously processed output. +struct tile_region { + struct rect area; // position and size in global coordinates + int rows; + int cols; + int cell_w; // base cell width + int cell_w_off; // columns that get 1 extra px (remainder distribution) + int cell_h; // base cell height + int cell_h_off; // rows that get 1 extra px + int label_offset; // index of first label in this region + int num_labels; // rows * cols +}; + struct tile_mode_state { - struct rect area; + // Region-based multi-output fields (set when all_outputs is true). + // NULL in single-output mode. + struct tile_region *regions; + int num_regions; + // Single-output fields (used when regions == NULL). + struct rect area; int sub_area_rows; int sub_area_width; int sub_area_width_off; - int sub_area_columns; int sub_area_height; int sub_area_height_off; @@ -91,6 +110,32 @@ struct output { enum wl_output_transform transform; }; +struct state; + +/** + * Per-output overlay surface. In single-output mode there is exactly one of + * these; in all-outputs mode there is one per connected output. + */ +struct overlay_surface { + struct wl_list link; // type: struct overlay_surface + + struct wl_surface *wl_surface; + struct wl_callback *wl_surface_callback; + struct zwlr_layer_surface_v1 *wl_layer_surface; + struct wp_viewport *wp_viewport; + struct wp_fractional_scale_v1 *fractional_scale; + struct surface_buffer_pool surface_buffer_pool; + + uint32_t width; + uint32_t height; + uint32_t fractional_scale_val; // preferred scale * 120 + + bool configured; + + struct output *output; // NULL until surface.enter fires (single-output, no -O) + struct state *state; +}; + struct seat { struct wl_list link; // type: struct seat struct wl_seat *wl_seat; @@ -110,23 +155,16 @@ struct state { struct zwlr_layer_shell_v1 *wl_layer_shell; struct zwlr_virtual_pointer_manager_v1 *wl_virtual_pointer_mgr; struct wp_viewporter *wp_viewporter; - struct wp_viewport *wp_viewport; struct wp_fractional_scale_manager_v1 *fractional_scale_mgr; - struct surface_buffer_pool surface_buffer_pool; - struct wl_surface *wl_surface; - struct wl_callback *wl_surface_callback; - struct zwlr_layer_surface_v1 *wl_layer_surface; - bool surface_configured; #if OPENCV_ENABLED struct zwlr_screencopy_manager_v1 *wl_screencopy_manager; #endif struct zxdg_output_manager_v1 *xdg_output_manager; struct wl_list outputs; struct wl_list seats; - struct output *current_output; - uint32_t surface_height; - uint32_t surface_width; - uint32_t fractional_scale; // scale / 120 + struct wl_list overlay_surfaces; // type: struct overlay_surface + struct output *current_output; // set from -O/-r or surface.enter (single-output); + // set from result coords (all-outputs) before move_pointer bool running; struct rect initial_area; char home_row_buffer[HOME_ROW_BUFFER_LEN];