diff --git a/docs/labwc-config.5.scd b/docs/labwc-config.5.scd index c7bfaf4c..3bf8f91f 100644 --- a/docs/labwc-config.5.scd +++ b/docs/labwc-config.5.scd @@ -299,6 +299,16 @@ extending outward from the snapped edge. SnapToEdge action for that edge. A *range* of 0 disables snapping via interactive moves. Default is 1. +** [yes|no] + Show a preview when snaping to a window to an edge. Default is yes. + +**++ +** + Sets the delay to show a preview when snapping a window to each type of edges. + Defaults are 500 ms. + *inner* edges are edges with an adjacent output and *outer* edges are edges + without an adjacent output. + ** [yes|no] If *yes*, an interactive move that snaps a window to the top edge will maximize the window. If *no*, snapping will behave as it does with other diff --git a/docs/rc.xml.all b/docs/rc.xml.all index 4de36af1..9d9f0188 100644 --- a/docs/rc.xml.all +++ b/docs/rc.xml.all @@ -99,6 +99,9 @@ 1 + + + yes always diff --git a/include/config/rcxml.h b/include/config/rcxml.h index ac4d1795..0a4f10a7 100644 --- a/include/config/rcxml.h +++ b/include/config/rcxml.h @@ -125,6 +125,9 @@ struct rcxml { /* window snapping */ int snap_edge_range; + bool snap_preview_enabled; + int snap_preview_delay_inner; + int snap_preview_delay_outer; bool snap_top_maximize; enum tiling_events_mode snap_tiling_events_mode; diff --git a/include/labwc.h b/include/labwc.h index 83f37dca..b60d7647 100644 --- a/include/labwc.h +++ b/include/labwc.h @@ -47,6 +47,7 @@ #include "config/rcxml.h" #include "input/cursor.h" #include "input/ime.h" +#include "overlay.h" #include "regions.h" #include "session-lock.h" #if HAVE_NLS @@ -159,9 +160,7 @@ struct seat { struct wlr_scene_tree *icons; } drag; - /* Private use by regions.c */ - struct region *region_active; - struct region_overlay region_overlay; + struct overlay overlay; /* Used to prevent region snapping when starting a move with A-Left */ bool region_prevent_snap; @@ -479,6 +478,8 @@ void seat_output_layout_changed(struct seat *seat); void interactive_begin(struct view *view, enum input_mode mode, uint32_t edges); void interactive_finish(struct view *view); void interactive_cancel(struct view *view); +/* Possibly returns VIEW_EDGE_CENTER if is yes */ +enum view_edge edge_from_cursor(struct seat *seat, struct output **dest_output); void output_init(struct server *server); void output_manager_init(struct server *server); diff --git a/include/overlay.h b/include/overlay.h new file mode 100644 index 00000000..3d56f8d7 --- /dev/null +++ b/include/overlay.h @@ -0,0 +1,36 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef LABWC_OVERLAY_H +#define LABWC_OVERLAY_H + +#include +#include "common/graphic-helpers.h" +#include "regions.h" +#include "view.h" + +struct overlay { + struct wlr_scene_tree *tree; + union { + struct wlr_scene_rect *rect; + struct multi_rect *pixman_rect; + }; + + /* Represents currently shown or delayed overlay */ + struct { + /* Region overlay */ + struct region *region; + + /* Snap-to-edge overlay */ + enum view_edge edge; + struct output *output; + } active; + + /* For delayed snap-to-edge overlay */ + struct wl_event_source *timer; +}; + +/* Calls overlay_hide() internally if there's no overlay to show */ +void overlay_update(struct seat *seat); +/* This function must be called when server->grabbed_view is destroyed */ +void overlay_hide(struct seat *seat); + +#endif diff --git a/include/regions.h b/include/regions.h index 6638f9f3..abe472be 100644 --- a/include/regions.h +++ b/include/regions.h @@ -25,14 +25,6 @@ struct region { } center; }; -struct region_overlay { - struct wlr_scene_tree *tree; - union { - struct wlr_scene_rect *overlay; - struct multi_rect *pixman_overlay; - }; -}; - /* Returns true if we should show the region overlay or snap to region */ bool regions_should_snap(struct server *server); @@ -73,7 +65,4 @@ void regions_destroy(struct seat *seat, struct wl_list *regions); struct region *regions_from_cursor(struct server *server); struct region *regions_from_name(const char *region_name, struct output *output); -void regions_show_overlay(struct view *view, struct seat *seat, struct region *region); -void regions_hide_overlay(struct seat *seat); - #endif /* LABWC_REGIONS_H */ diff --git a/src/config/rcxml.c b/src/config/rcxml.c index 62afdc6c..5e5ce68b 100644 --- a/src/config/rcxml.c +++ b/src/config/rcxml.c @@ -907,6 +907,12 @@ entry(xmlNode *node, char *nodename, char *content) rc.window_edge_strength = atoi(content); } else if (!strcasecmp(nodename, "range.snapping")) { rc.snap_edge_range = atoi(content); + } else if (!strcasecmp(nodename, "enabled.preview.snapping")) { + set_bool(content, &rc.snap_preview_enabled); + } else if (!strcasecmp(nodename, "inner.delay.preview.snapping")) { + rc.snap_preview_delay_inner = atoi(content); + } else if (!strcasecmp(nodename, "outer.delay.preview.snapping")) { + rc.snap_preview_delay_outer = atoi(content); } else if (!strcasecmp(nodename, "topMaximize.snapping")) { set_bool(content, &rc.snap_top_maximize); } else if (!strcasecmp(nodename, "notifyClient.snapping")) { @@ -1184,6 +1190,9 @@ rcxml_init(void) rc.window_edge_strength = 20; rc.snap_edge_range = 1; + rc.snap_preview_enabled = true; + rc.snap_preview_delay_inner = 500; + rc.snap_preview_delay_outer = 500; rc.snap_top_maximize = true; rc.snap_tiling_events_mode = LAB_TILING_EVENTS_ALWAYS; diff --git a/src/debug.c b/src/debug.c index d8b9c179..3b79f7ac 100644 --- a/src/debug.c +++ b/src/debug.c @@ -136,10 +136,10 @@ get_special(struct server *server, struct wlr_scene_node *node) if (node == &server->seat.drag.icons->node) { return "seat->drag.icons"; } - if (server->seat.region_overlay.tree - && node == &server->seat.region_overlay.tree->node) { + if (server->seat.overlay.tree + && node == &server->seat.overlay.tree->node) { /* Created on-demand */ - return "seat->region_overlay"; + return "seat->overlay"; } if (server->seat.input_method_relay->popup_tree && node == &server->seat.input_method_relay->popup_tree->node) { diff --git a/src/input/cursor.c b/src/input/cursor.c index eb359e71..11b71b82 100644 --- a/src/input/cursor.c +++ b/src/input/cursor.c @@ -225,16 +225,7 @@ process_cursor_move(struct server *server, uint32_t time) resistance_move_apply(view, &dx, &dy); view_move(view, dx, dy); - /* Region overlay */ - if (!regions_should_snap(server)) { - return; - } - struct region *region = regions_from_cursor(server); - if (region) { - regions_show_overlay(view, &server->seat, region); - } else { - regions_hide_overlay(&server->seat); - } + overlay_update(&server->seat); } static void diff --git a/src/input/keyboard.c b/src/input/keyboard.c index 0ca6d390..415edffd 100644 --- a/src/input/keyboard.c +++ b/src/input/keyboard.c @@ -80,11 +80,13 @@ keyboard_modifiers_notify(struct wl_listener *listener, void *data) if (server->input_mode == LAB_INPUT_STATE_MOVE) { /* Any change to the modifier state re-enable region snap */ seat->region_prevent_snap = false; + /* Pressing/releasing modifier key may show/hide region overlay */ + overlay_update(seat); } - if (server->osd_state.cycle_view || server->grabbed_view + if (server->osd_state.cycle_view || seat->workspace_osd_shown_by_modifier) { - if (!keyboard_any_modifiers_pressed(wlr_keyboard)) { + if (!keyboard_any_modifiers_pressed(wlr_keyboard)) { if (server->osd_state.cycle_view) { if (key_state_nr_bound_keys()) { should_cancel_cycling_on_next_key_release = true; @@ -95,9 +97,6 @@ keyboard_modifiers_notify(struct wl_listener *listener, void *data) if (seat->workspace_osd_shown_by_modifier) { workspaces_osd_hide(seat); } - if (server->grabbed_view) { - regions_hide_overlay(seat); - } } } diff --git a/src/interactive.c b/src/interactive.c index a0b4f255..243aaa6a 100644 --- a/src/interactive.c +++ b/src/interactive.c @@ -129,39 +129,53 @@ interactive_begin(struct view *view, enum input_mode mode, uint32_t edges) } } +enum view_edge +edge_from_cursor(struct seat *seat, struct output **dest_output) +{ + int snap_range = rc.snap_edge_range; + if (!snap_range) { + return VIEW_EDGE_INVALID; + } + + struct output *output = output_nearest_to_cursor(seat->server); + if (!output_is_usable(output)) { + wlr_log(WLR_ERROR, "output at cursor is unusable"); + return VIEW_EDGE_INVALID; + } + *dest_output = output; + + /* Translate into output local coordinates */ + double cursor_x = seat->cursor->x; + double cursor_y = seat->cursor->y; + wlr_output_layout_output_coords(seat->server->output_layout, + output->wlr_output, &cursor_x, &cursor_y); + + struct wlr_box *area = &output->usable_area; + if (cursor_x <= area->x + snap_range) { + return VIEW_EDGE_LEFT; + } else if (cursor_x >= area->x + area->width - snap_range) { + return VIEW_EDGE_RIGHT; + } else if (cursor_y <= area->y + snap_range) { + if (rc.snap_top_maximize) { + return VIEW_EDGE_CENTER; + } else { + return VIEW_EDGE_UP; + } + } else if (cursor_y >= area->y + area->height - snap_range) { + return VIEW_EDGE_DOWN; + } else { + /* Not close to any edge */ + return VIEW_EDGE_INVALID; + } +} + /* Returns true if view was snapped to any edge */ static bool snap_to_edge(struct view *view) { - int snap_range = rc.snap_edge_range; - if (!snap_range) { - return false; - } - - struct output *output = output_nearest_to_cursor(view->server); - if (!output_is_usable(output)) { - wlr_log(WLR_ERROR, "output at cursor is unusable"); - return false; - } - - /* Translate into output local coordinates */ - double cursor_x = view->server->seat.cursor->x; - double cursor_y = view->server->seat.cursor->y; - wlr_output_layout_output_coords(view->server->output_layout, - output->wlr_output, &cursor_x, &cursor_y); - - struct wlr_box *area = &output->usable_area; - enum view_edge edge; - if (cursor_x <= area->x + snap_range) { - edge = VIEW_EDGE_LEFT; - } else if (cursor_x >= area->x + area->width - snap_range) { - edge = VIEW_EDGE_RIGHT; - } else if (cursor_y <= area->y + snap_range) { - edge = VIEW_EDGE_UP; - } else if (cursor_y >= area->y + area->height - snap_range) { - edge = VIEW_EDGE_DOWN; - } else { - /* Not close to any edge */ + struct output *output; + enum view_edge edge = edge_from_cursor(&view->server->seat, &output); + if (edge == VIEW_EDGE_INVALID) { return false; } @@ -170,7 +184,8 @@ snap_to_edge(struct view *view) * Don't store natural geometry here (it was * stored already in interactive_begin()) */ - if (edge == VIEW_EDGE_UP && rc.snap_top_maximize) { + if (edge == VIEW_EDGE_CENTER) { + /* */ view_maximize(view, VIEW_AXIS_BOTH, /*store_natural_geometry*/ false); } else { @@ -226,7 +241,7 @@ interactive_cancel(struct view *view) return; } - regions_hide_overlay(&view->server->seat); + overlay_hide(&view->server->seat); resize_indicator_hide(view); diff --git a/src/meson.build b/src/meson.build index a71eb06b..e08afb82 100644 --- a/src/meson.build +++ b/src/meson.build @@ -14,6 +14,7 @@ labwc_sources = files( 'osd.c', 'output.c', 'output-virtual.c', + 'overlay.c', 'placement.c', 'regions.c', 'resistance.c', diff --git a/src/output.c b/src/output.c index 605b3acb..e81abaee 100644 --- a/src/output.c +++ b/src/output.c @@ -102,13 +102,17 @@ static void output_destroy_notify(struct wl_listener *listener, void *data) { struct output *output = wl_container_of(listener, output, destroy); + struct seat *seat = &output->server->seat; regions_evacuate_output(output); - regions_destroy(&output->server->seat, &output->regions); + regions_destroy(seat, &output->regions); + if (seat->overlay.active.output == output) { + overlay_hide(seat); + } wl_list_remove(&output->link); wl_list_remove(&output->frame.link); wl_list_remove(&output->destroy.link); wl_list_remove(&output->request_state.link); - seat_output_layout_changed(&output->server->seat); + seat_output_layout_changed(seat); for (size_t i = 0; i < ARRAY_SIZE(output->layer_tree); i++) { wlr_scene_node_destroy(&output->layer_tree[i]->node); diff --git a/src/overlay.c b/src/overlay.c new file mode 100644 index 00000000..04471b50 --- /dev/null +++ b/src/overlay.c @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include +#include +#include "labwc.h" +#include "overlay.h" +#include "view.h" + +static void +create_overlay(struct seat *seat) +{ + assert(!seat->overlay.tree); + + struct server *server = seat->server; + struct wlr_scene_tree *parent = wlr_scene_tree_create(&server->scene->tree); + + seat->overlay.tree = parent; + wlr_scene_node_set_enabled(&parent->node, false); + if (!wlr_renderer_is_pixman(server->renderer)) { + /* Hardware assisted rendering: Half transparent overlay */ + float color[4] = { 0.25, 0.25, 0.35, 0.5 }; + seat->overlay.rect = wlr_scene_rect_create(parent, 0, 0, color); + } else { + /* Software rendering: Outlines */ + int line_width = server->theme->osd_border_width; + float *colors[3] = { + server->theme->osd_bg_color, + server->theme->osd_label_text_color, + server->theme->osd_bg_color + }; + seat->overlay.pixman_rect = multi_rect_create(parent, colors, line_width); + } +} + +static void +show_overlay(struct seat *seat, struct wlr_box *box) +{ + struct server *server = seat->server; + struct view *view = server->grabbed_view; + assert(view); + + if (!seat->overlay.tree) { + create_overlay(seat); + } + + struct wlr_scene_node *node = &seat->overlay.tree->node; + if (!wlr_renderer_is_pixman(server->renderer)) { + /* Hardware assisted rendering: Half transparent overlay */ + wlr_scene_rect_set_size(seat->overlay.rect, + box->width, box->height); + } else { + /* Software rendering: Outlines */ + multi_rect_set_size(seat->overlay.pixman_rect, + box->width, box->height); + } + if (node->parent != view->scene_tree->node.parent) { + wlr_scene_node_reparent(node, view->scene_tree->node.parent); + wlr_scene_node_place_below(node, &view->scene_tree->node); + } + wlr_scene_node_set_position(node, box->x, box->y); + wlr_scene_node_set_enabled(node, true); + + if (seat->overlay.timer) { + wl_event_source_timer_update(seat->overlay.timer, 0); + } +} + +static void +show_region_overlay(struct seat *seat, struct region *region) +{ + if (region == seat->overlay.active.region) { + return; + } + seat->overlay.active.region = region; + seat->overlay.active.edge = VIEW_EDGE_INVALID; + seat->overlay.active.output = NULL; + + show_overlay(seat, ®ion->geo); +} + +/* TODO: share logic with view_get_edge_snap_box() */ +static struct wlr_box get_edge_snap_box(enum view_edge edge, struct output *output) +{ + struct wlr_box box = output_usable_area_in_layout_coords(output); + switch (edge) { + case VIEW_EDGE_RIGHT: + box.x += box.width / 2; + /* fallthrough */ + case VIEW_EDGE_LEFT: + box.width /= 2; + break; + case VIEW_EDGE_DOWN: + box.y += box.height / 2; + /* fallthrough */ + case VIEW_EDGE_UP: + box.height /= 2; + break; + case VIEW_EDGE_CENTER: + /* */ + break; + default: + /* not reached */ + assert(false); + } + return box; +} + +static int +handle_edge_overlay_timeout(void *data) +{ + struct seat *seat = data; + assert(seat->overlay.active.edge != VIEW_EDGE_INVALID + && seat->overlay.active.output); + struct wlr_box box = get_edge_snap_box(seat->overlay.active.edge, + seat->overlay.active.output); + show_overlay(seat, &box); + return 0; +} + +static enum wlr_direction +get_wlr_direction(enum view_edge edge) +{ + switch (edge) { + case VIEW_EDGE_LEFT: + return WLR_DIRECTION_LEFT; + case VIEW_EDGE_RIGHT: + return WLR_DIRECTION_RIGHT; + case VIEW_EDGE_UP: + case VIEW_EDGE_CENTER: + return WLR_DIRECTION_UP; + case VIEW_EDGE_DOWN: + return WLR_DIRECTION_DOWN; + default: + /* not reached */ + assert(false); + return 0; + } +} + +static bool +edge_has_adjacent_output_from_cursor(struct seat *seat, struct output *output, + enum view_edge edge) +{ + return wlr_output_layout_adjacent_output( + seat->server->output_layout, get_wlr_direction(edge), + output->wlr_output, seat->cursor->x, seat->cursor->y); +} + +static void +show_edge_overlay(struct seat *seat, enum view_edge edge, + struct output *output) +{ + if (!rc.snap_preview_enabled) { + return; + } + if (seat->overlay.active.edge == edge + && seat->overlay.active.output == output) { + return; + } + seat->overlay.active.region = NULL; + seat->overlay.active.edge = edge; + seat->overlay.active.output = output; + + int delay; + if (edge_has_adjacent_output_from_cursor(seat, output, edge)) { + delay = rc.snap_preview_delay_inner; + } else { + delay = rc.snap_preview_delay_outer; + } + + if (delay > 0) { + if (!seat->overlay.timer) { + seat->overlay.timer = wl_event_loop_add_timer( + seat->server->wl_event_loop, + handle_edge_overlay_timeout, seat); + } + /* Show overlay ms later */ + wl_event_source_timer_update(seat->overlay.timer, delay); + } else { + /* Show overlay now */ + struct wlr_box box = get_edge_snap_box(seat->overlay.active.edge, + seat->overlay.active.output); + show_overlay(seat, &box); + } +} + +void +overlay_update(struct seat *seat) +{ + struct server *server = seat->server; + + /* Region-snapping overlay */ + if (regions_should_snap(server)) { + struct region *region = regions_from_cursor(server); + if (region) { + show_region_overlay(seat, region); + return; + } + } + + /* Edge-snapping overlay */ + struct output *output; + enum view_edge edge = edge_from_cursor(seat, &output); + if (edge != VIEW_EDGE_INVALID) { + show_edge_overlay(seat, edge, output); + return; + } + + overlay_hide(seat); +} + +void +overlay_hide(struct seat *seat) +{ + seat->overlay.active.region = NULL; + seat->overlay.active.edge = VIEW_EDGE_INVALID; + seat->overlay.active.output = NULL; + if (seat->overlay.timer) { + wl_event_source_timer_update(seat->overlay.timer, 0); + } + + if (!seat->overlay.tree) { + return; + } + struct server *server = seat->server; + struct wlr_scene_node *node = &seat->overlay.tree->node; + + wlr_scene_node_set_enabled(node, false); + if (node->parent != &server->scene->tree) { + wlr_scene_node_reparent(node, &server->scene->tree); + } +} diff --git a/src/regions.c b/src/regions.c index f6eb6174..1b265fe3 100644 --- a/src/regions.c +++ b/src/regions.c @@ -5,11 +5,9 @@ #include #include #include -#include #include #include #include -#include "common/graphic-helpers.h" #include "common/list.h" #include "common/mem.h" #include "input/keyboard.h" @@ -30,32 +28,6 @@ regions_should_snap(struct server *server) return keyboard_any_modifiers_pressed(keyboard); } -static void -overlay_create(struct seat *seat) -{ - assert(!seat->region_overlay.tree); - - struct server *server = seat->server; - struct wlr_scene_tree *parent = wlr_scene_tree_create(&server->scene->tree); - - seat->region_overlay.tree = parent; - wlr_scene_node_set_enabled(&parent->node, false); - if (!wlr_renderer_is_pixman(server->renderer)) { - /* Hardware assisted rendering: Half transparent overlay */ - float color[4] = { 0.25, 0.25, 0.35, 0.5 }; - seat->region_overlay.overlay = wlr_scene_rect_create(parent, 0, 0, color); - } else { - /* Software rendering: Outlines */ - int line_width = server->theme->osd_border_width; - float *colors[3] = { - server->theme->osd_bg_color, - server->theme->osd_label_text_color, - server->theme->osd_bg_color - }; - seat->region_overlay.pixman_overlay = multi_rect_create(parent, colors, line_width); - } -} - struct region * regions_from_name(const char *region_name, struct output *output) { @@ -101,61 +73,6 @@ regions_from_cursor(struct server *server) return closest_region; } -void -regions_show_overlay(struct view *view, struct seat *seat, struct region *region) -{ - assert(view); - assert(seat); - assert(region); - - /* Don't show active region */ - if (seat->region_active == region) { - return; - } - - if (!seat->region_overlay.tree) { - overlay_create(seat); - } - - /* Update overlay */ - struct server *server = seat->server; - struct wlr_scene_node *node = &seat->region_overlay.tree->node; - if (!wlr_renderer_is_pixman(server->renderer)) { - /* Hardware assisted rendering: Half transparent overlay */ - wlr_scene_rect_set_size(seat->region_overlay.overlay, - region->geo.width, region->geo.height); - } else { - /* Software rendering: Outlines */ - multi_rect_set_size(seat->region_overlay.pixman_overlay, - region->geo.width, region->geo.height); - } - if (node->parent != view->scene_tree->node.parent) { - wlr_scene_node_reparent(node, view->scene_tree->node.parent); - wlr_scene_node_place_below(node, &view->scene_tree->node); - } - wlr_scene_node_set_position(node, region->geo.x, region->geo.y); - wlr_scene_node_set_enabled(node, true); - seat->region_active = region; -} - -void -regions_hide_overlay(struct seat *seat) -{ - assert(seat); - if (!seat->region_active) { - return; - } - - struct server *server = seat->server; - struct wlr_scene_node *node = &seat->region_overlay.tree->node; - - wlr_scene_node_set_enabled(node, false); - if (node->parent != &server->scene->tree) { - wlr_scene_node_reparent(node, &server->scene->tree); - } - seat->region_active = NULL; -} - void regions_reconfigure_output(struct output *output) { @@ -246,10 +163,10 @@ regions_destroy(struct seat *seat, struct wl_list *regions) struct region *region, *region_tmp; wl_list_for_each_safe(region, region_tmp, regions, link) { wl_list_remove(®ion->link); - zfree(region->name); - if (seat && seat->region_active == region) { - seat->region_active = NULL; + if (seat && seat->overlay.active.region == region) { + overlay_hide(seat); } + zfree(region->name); zfree(region); } } diff --git a/src/view.c b/src/view.c index 066b8427..6e8a7769 100644 --- a/src/view.c +++ b/src/view.c @@ -2223,7 +2223,7 @@ view_destroy(struct view *view) /* Application got killed while moving around */ server->input_mode = LAB_INPUT_STATE_PASSTHROUGH; server->grabbed_view = NULL; - regions_hide_overlay(&server->seat); + overlay_hide(&server->seat); } if (server->active_view == view) {