diff --git a/plugins/protocols/wayfire-shell.cpp b/plugins/protocols/wayfire-shell.cpp index ee8f38710..a4dd54b69 100644 --- a/plugins/protocols/wayfire-shell.cpp +++ b/plugins/protocols/wayfire-shell.cpp @@ -26,8 +26,10 @@ static void handle_hotspot_destroy(wl_resource *resource); class wfs_hotspot { private: + wf::geometry_t trigger; wf::geometry_t hotspot_geometry; + uint32_t threshold; bool hotspot_triggered = false; wf::wl_idle_call idle_check_input; wf::wl_timer timer; @@ -71,92 +73,144 @@ class wfs_hotspot void process_input_motion(wf::point_t gc) { - if (!(hotspot_geometry & gc)) + if (is_cursor_in_hotspot(gc)) + { + if (hotspot_triggered) + { + send_proximity_if_needed(gc); + } else if (!timer.is_connected()) + { + timer.set_timeout(timeout_ms, [=] () + { + hotspot_triggered = true; + zwf_hotspot_v2_send_enter(hotspot_resource); + send_proximity_if_needed(gc); + }); + } + } else { if (hotspot_triggered) { zwf_hotspot_v2_send_leave(hotspot_resource); + hotspot_triggered = false; } - /* Cursor outside of the hotspot */ - hotspot_triggered = false; timer.disconnect(); + } + } - return; + wf::geometry_t calculate_hotspot_geometry(wf::output_t *output, const wf::geometry_t& trigger_rect, + uint32_t threshold) const + { + // Get output geometry in global coordinates + wf::geometry_t output_geom = output->get_layout_geometry(); + + // Expand core rectangle by threshold in all directions + wf::geometry_t expanded = { + trigger_rect.x - (int)threshold, + trigger_rect.y - (int)threshold, + trigger_rect.width + (int)threshold * 2, + trigger_rect.height + (int)threshold * 2 + }; + + // Clamp to output boundaries + wf::geometry_t result = wf::geometry_intersection(expanded, output_geom); + + return result; + } + + uint32_t calculate_proximity(const wf::point_t& cursor) const + { + const auto& rect = trigger; + int dx = 0, dy = 0; + + if (cursor.x < rect.x) + { + dx = rect.x - cursor.x; + } else if ((rect.width > 0) && (cursor.x > rect.x + rect.width)) + { + dx = cursor.x - (rect.x + rect.width); } - if (hotspot_triggered) + if (cursor.y < rect.y) { - /* Hotspot was already triggered, wait for the next time the cursor - * enters the hotspot area to trigger again */ - return; + dy = rect.y - cursor.y; + } else if ((rect.height > 0) && (cursor.y > rect.y + rect.height)) + { + dy = cursor.y - (rect.y + rect.height); } - if (!timer.is_connected()) + // Inside the core rectangle + if ((dx == 0) && (dy == 0)) { - timer.set_timeout(timeout_ms, [=] () - { - hotspot_triggered = true; - zwf_hotspot_v2_send_enter(hotspot_resource); - }); + return 0; } - } - wf::geometry_t calculate_hotspot_geometry(wf::output_t *output, - uint32_t edge_mask, uint32_t distance) const - { - wf::geometry_t slot = output->get_layout_geometry(); - if (edge_mask & ZWF_OUTPUT_V2_HOTSPOT_EDGE_TOP) + // Euclidean distance to corner + if ((dx > 0) && (dy > 0)) { - slot.height = distance; - } else if (edge_mask & ZWF_OUTPUT_V2_HOTSPOT_EDGE_BOTTOM) + return std::min((uint32_t)std::sqrt(dx * dx + dy * dy), threshold); + } else { - slot.y += slot.height - distance; - slot.height = distance; + return std::min((uint32_t)(dx + dy), threshold); } + } - if (edge_mask & ZWF_OUTPUT_V2_HOTSPOT_EDGE_LEFT) + bool is_cursor_in_hotspot(const wf::point_t& cursor) const + { + // Quick rejection using bounding box + if (!(hotspot_geometry & cursor)) { - slot.width = distance; - } else if (edge_mask & ZWF_OUTPUT_V2_HOTSPOT_EDGE_RIGHT) + return false; + } + + // Exact distance check + return calculate_proximity(cursor) <= threshold; + } + + void send_proximity_if_needed(const wf::point_t& cursor) + { + if (wl_resource_get_version(hotspot_resource) < 2) { - slot.x += slot.width - distance; - slot.width = distance; + return; } - return slot; + if (hotspot_triggered) + { + uint32_t proximity = calculate_proximity(cursor); + zwf_hotspot_v2_send_proximity_changed(hotspot_resource, proximity); + } } - wfs_hotspot(const wfs_hotspot &) = delete; - wfs_hotspot(wfs_hotspot &&) = delete; + wfs_hotspot(const wfs_hotspot&) = delete; + wfs_hotspot(wfs_hotspot&&) = delete; wfs_hotspot& operator =(const wfs_hotspot&) = delete; wfs_hotspot& operator =(wfs_hotspot&&) = delete; public: + /** * Create a new hotspot. * It is guaranteedd that edge_mask contains at most 2 non-opposing edges. */ - wfs_hotspot(wf::output_t *output, uint32_t edge_mask, - uint32_t distance, uint32_t timeout, wl_client *client, uint32_t id) + wfs_hotspot(wf::output_t *output, wf::geometry_t trigger, uint32_t threshold, uint32_t timeout, + wl_client *client, uint32_t id) : trigger(trigger), threshold(threshold), timeout_ms(timeout) { - this->timeout_ms = timeout; - this->hotspot_geometry = - calculate_hotspot_geometry(output, edge_mask, distance); + // Calculate expanded bounding box + this->hotspot_geometry = calculate_hotspot_geometry(output, trigger, threshold); - hotspot_resource = - wl_resource_create(client, &zwf_hotspot_v2_interface, 1, id); - wl_resource_set_implementation(hotspot_resource, NULL, this, - handle_hotspot_destroy); + // Create resource and setup signals (same as before) + hotspot_resource = wl_resource_create(client, &zwf_hotspot_v2_interface, 3, id); + wl_resource_set_implementation(hotspot_resource, NULL, this, handle_hotspot_destroy); - // setup output destroy listener + // output destroy handler on_output_removed.set_callback([this, output] (wf::output_removed_signal *ev) { if (ev->output == output) { - /* Make hotspot inactive by setting the region to empty */ - hotspot_geometry = {0, 0, 0, 0}; - process_input_motion({0, 0}); + this->hotspot_geometry = {0, 0, 0, 0}; + this->trigger = {0, 0, 0, 0}; + process_input_motion({-1, -1}); } }); @@ -172,6 +226,7 @@ class wfs_hotspot static void handle_hotspot_destroy(wl_resource *resource) { auto *hotspot = (wfs_hotspot*)wl_resource_get_user_data(resource); + delete hotspot; wl_resource_set_user_data(resource, nullptr); @@ -180,15 +235,18 @@ static void handle_hotspot_destroy(wl_resource *resource) /* ------------------------------ wfs_output -------------------------------- */ static void handle_output_destroy(wl_resource *resource); static void handle_zwf_output_inhibit_output(wl_client*, wl_resource *resource); -static void handle_zwf_output_inhibit_output_done(wl_client*, - wl_resource *resource); -static void handle_zwf_output_create_hotspot(wl_client*, wl_resource *resource, - uint32_t hotspot, uint32_t threshold, uint32_t timeout, uint32_t id); +static void handle_zwf_output_inhibit_output_done(wl_client*, wl_resource *resource); +static void handle_zwf_output_create_hotspot(wl_client*, wl_resource *resource, uint32_t hotspot, + uint32_t threshold, uint32_t timeout, uint32_t id); +static void handle_zwf_output_create_custom_hotspot(wl_client*, wl_resource *resource, uint32_t x, uint32_t y, + uint32_t w, uint32_t h, uint32_t threshold, + uint32_t timeout, uint32_t id); static struct zwf_output_v2_interface zwf_output_impl = { .inhibit_output = handle_zwf_output_inhibit_output, .inhibit_output_done = handle_zwf_output_inhibit_output_done, .create_hotspot = handle_zwf_output_create_hotspot, + .create_custom_hotspot = handle_zwf_output_create_custom_hotspot, }; /** @@ -252,8 +310,7 @@ class wfs_output this->shell_resource = shell_resource; resource = - wl_resource_create(client, &zwf_output_v2_interface, - std::min(wl_resource_get_version(shell_resource), 2), id); + wl_resource_create(client, &zwf_output_v2_interface, 3, id); wl_resource_set_implementation(resource, &zwf_output_impl, this, handle_output_destroy); output->connect(&on_fullscreen_layer_focused); output->connect(&on_toggle_menu); @@ -271,8 +328,8 @@ class wfs_output disconnect_from_output(); } - wfs_output(const wfs_output &) = delete; - wfs_output(wfs_output &&) = delete; + wfs_output(const wfs_output&) = delete; + wfs_output(wfs_output&&) = delete; wfs_output& operator =(const wfs_output&) = delete; wfs_output& operator =(wfs_output&&) = delete; @@ -296,50 +353,136 @@ class wfs_output } } - void create_hotspot(uint32_t hotspot, uint32_t threshold, uint32_t timeout, - uint32_t id) + wf::geometry_t calculate_trigger_geometry(uint32_t edge_mask) + { + wf::geometry_t output_geom = this->output->get_layout_geometry(); + int edge_count = 0; + + if (edge_mask & ZWF_OUTPUT_V2_HOTSPOT_EDGE_TOP) + { + edge_count++; + } + + if (edge_mask & ZWF_OUTPUT_V2_HOTSPOT_EDGE_BOTTOM) + { + edge_count++; + } + + if (edge_mask & ZWF_OUTPUT_V2_HOTSPOT_EDGE_LEFT) + { + edge_count++; + } + + if (edge_mask & ZWF_OUTPUT_V2_HOTSPOT_EDGE_RIGHT) + { + edge_count++; + } + + if (edge_count == 1) + { + // Single edge: rectangle with zero thickness + if (edge_mask & ZWF_OUTPUT_V2_HOTSPOT_EDGE_TOP) + { + return {output_geom.x, output_geom.y, output_geom.width, 0}; + } else if (edge_mask & ZWF_OUTPUT_V2_HOTSPOT_EDGE_BOTTOM) + { + return {output_geom.x, output_geom.y + output_geom.height - 1, output_geom.width, 0}; + } else if (edge_mask & ZWF_OUTPUT_V2_HOTSPOT_EDGE_LEFT) + { + return {output_geom.x, output_geom.y, 0, output_geom.height}; + } else // RIGHT + { + return {output_geom.x + output_geom.width - 1, output_geom.y, 0, output_geom.height}; + } + } else if (edge_count == 2) + { + // Corner: zero-area point at the corner + int x = output_geom.x; + int y = output_geom.y; + + if (edge_mask & ZWF_OUTPUT_V2_HOTSPOT_EDGE_RIGHT) + { + x = output_geom.x + output_geom.width - 1; + } + + if (edge_mask & ZWF_OUTPUT_V2_HOTSPOT_EDGE_BOTTOM) + { + y = output_geom.y + output_geom.height - 1; + } + + return {x, y, 0, 0}; + } + + // Invalid + return {0, 0, 0, 0}; + } + + wf::geometry_t calculate_trigger_geometry(wf::geometry_t rect) + { + wf::geometry_t output_geom = this->output->get_layout_geometry(); + + wf::geometry_t trigger = { + output_geom.x + rect.x, + output_geom.y + rect.y, + rect.width, + rect.height + }; + + return trigger; + } + + void create_hotspot(const wf::geometry_t& trigger_rect, uint32_t threshold, uint32_t timeout, uint32_t id) { if (!this->output) { - // It can happen that the client requests a hotspot immediately after an output is destroyed - - // this is an inherent race condition because the compositor and client are not in sync. - // - // In this case, we create a dummy hotspot resource to avoid Wayland protocol errors. - auto resource = wl_resource_create( - wl_resource_get_client(this->resource), &zwf_hotspot_v2_interface, 1, id); + auto resource = wl_resource_create(wl_resource_get_client( + this->resource), &zwf_hotspot_v2_interface, 3, id); wl_resource_set_implementation(resource, NULL, NULL, NULL); return; } - // will be auto-deleted when the resource is destroyed by the client - new wfs_hotspot(this->output, hotspot, threshold, timeout, - wl_resource_get_client(this->resource), id); + new wfs_hotspot(this->output, trigger_rect, threshold, timeout, wl_resource_get_client( + this->resource), id); } }; static void handle_zwf_output_inhibit_output(wl_client*, wl_resource *resource) { auto output = (wfs_output*)wl_resource_get_user_data(resource); + output->inhibit_output(); } -static void handle_zwf_output_inhibit_output_done( - wl_client*, wl_resource *resource) +static void handle_zwf_output_inhibit_output_done(wl_client*, wl_resource *resource) { auto output = (wfs_output*)wl_resource_get_user_data(resource); + output->inhibit_output_done(); } static void handle_zwf_output_create_hotspot(wl_client*, wl_resource *resource, - uint32_t hotspot, uint32_t threshold, uint32_t timeout, uint32_t id) + uint32_t edge_mask, uint32_t threshold, uint32_t timeout, uint32_t id) { auto output = (wfs_output*)wl_resource_get_user_data(resource); - output->create_hotspot(hotspot, threshold, timeout, id); + wf::geometry_t trigger = output->calculate_trigger_geometry(edge_mask); + + output->create_hotspot(trigger, threshold, timeout, id); +} + +static void handle_zwf_output_create_custom_hotspot(wl_client*, wl_resource *resource, + uint32_t x, uint32_t y, uint32_t width, uint32_t height, + uint32_t threshold, uint32_t timeout, uint32_t id) +{ + auto output = (wfs_output*)wl_resource_get_user_data(resource); + wf::geometry_t trigger = output->calculate_trigger_geometry({(int)x, (int)y, (int)width, (int)height}); + + output->create_hotspot(trigger, threshold, timeout, id); } static void handle_output_destroy(wl_resource *resource) { auto *output = (wfs_output*)wl_resource_get_user_data(resource); + delete output; wl_resource_set_user_data(resource, nullptr); @@ -347,8 +490,7 @@ static void handle_output_destroy(wl_resource *resource) /* ------------------------------ wfs_surface ------------------------------- */ static void handle_surface_destroy(wl_resource *resource); -static void handle_zwf_surface_interactive_move(wl_client*, - wl_resource *resource); +static void handle_zwf_surface_interactive_move(wl_client*, wl_resource *resource); static struct zwf_surface_v2_interface zwf_surface_impl = { .interactive_move = handle_zwf_surface_interactive_move, @@ -372,7 +514,7 @@ class wfs_surface wfs_surface(wayfire_view view, wl_client *client, int id) { this->view = view; - resource = wl_resource_create(client, &zwf_surface_v2_interface, 1, id); + resource = wl_resource_create(client, &zwf_surface_v2_interface, 3, id); wl_resource_set_implementation(resource, &zwf_surface_impl, this, handle_surface_destroy); view->connect(&on_unmap); } @@ -387,18 +529,20 @@ class wfs_surface static void handle_zwf_surface_interactive_move(wl_client*, wl_resource *resource) { auto surface = (wfs_surface*)wl_resource_get_user_data(resource); + surface->interactive_move(); } static void handle_surface_destroy(wl_resource *resource) { auto surface = (wfs_surface*)wl_resource_get_user_data(resource); + delete surface; wl_resource_set_user_data(resource, nullptr); } -static void zwf_shell_manager_get_wf_output(wl_client *client, - wl_resource *resource, wl_resource *output, uint32_t id) +static void zwf_shell_manager_get_wf_output(wl_client *client, wl_resource *resource, wl_resource *output, + uint32_t id) { auto wlr_out = (wlr_output*)wl_resource_get_user_data(output); auto wo = wf::get_core().output_layout->find_output(wlr_out); @@ -410,10 +554,11 @@ static void zwf_shell_manager_get_wf_output(wl_client *client, } } -static void zwf_shell_manager_get_wf_surface(wl_client *client, - wl_resource *resource, wl_resource *surface, uint32_t id) +static void zwf_shell_manager_get_wf_surface(wl_client *client, wl_resource *resource, wl_resource *surface, + uint32_t id) { auto view = wf::wl_surface_to_wayfire_view(surface); + if (view) { /* Will be freed when the resource is destroyed */ @@ -430,10 +575,9 @@ const struct zwf_shell_manager_v2_interface zwf_shell_manager_v2_impl = void bind_zwf_shell_manager(wl_client *client, void *data, uint32_t version, uint32_t id) { - auto resource = - wl_resource_create(client, &zwf_shell_manager_v2_interface, version, id); - wl_resource_set_implementation(resource, - &zwf_shell_manager_v2_impl, NULL, NULL); + auto resource = wl_resource_create(client, &zwf_shell_manager_v2_interface, version, id); + + wl_resource_set_implementation(resource, &zwf_shell_manager_v2_impl, NULL, NULL); } struct wayfire_shell @@ -445,8 +589,8 @@ wayfire_shell *wayfire_shell_create(wl_display *display) { wayfire_shell *ws = new wayfire_shell; - ws->shell_manager = wl_global_create(display, - &zwf_shell_manager_v2_interface, 2, NULL, bind_zwf_shell_manager); + ws->shell_manager = wl_global_create(display, &zwf_shell_manager_v2_interface, 3, NULL, + bind_zwf_shell_manager); if (ws->shell_manager == NULL) { diff --git a/proto/wayfire-shell-unstable-v2.xml b/proto/wayfire-shell-unstable-v2.xml index a6d1d9d93..b9ad0fc31 100644 --- a/proto/wayfire-shell-unstable-v2.xml +++ b/proto/wayfire-shell-unstable-v2.xml @@ -1,6 +1,6 @@ - + This protocol provides additional events and requests for special DE clients like panels, docks, etc. @@ -21,7 +21,7 @@ - + Represents a single output. Each output is managed independently from the others. @@ -65,14 +65,14 @@ - - - - + + + + - + A hotspot on the output is an edge or a corner region of the output where the mouse or touch point has been residing for a given amount of time. @@ -80,11 +80,17 @@ The hotspot can be used for example for autohiding panels, where the panel is shown when the input hovers on the edge of the output for a specific amount of time. + + For edge hotspots (single edge), the entire edge region of 'threshold' + pixels depth triggers the hotspot. + + For corner hotspots (two adjacent edges), the corner region of + 'threshold' pixels by 'threshold' pixels triggers the hotspot. - - - + + + @@ -96,15 +102,67 @@ Emitted using an activator binding. + + + + + Creates a hotspot for an arbitrary axis-aligned rectangle on the output. + The rectangle is defined in output-local coordinates with (x, y) as the + top-left corner, and (w, h) as width and height. + + The hotspot activates when the input pointer comes within 'threshold' + pixels of any point on the rectangle's boundary. The proximity event + reports the exact distance (0 to threshold) from the pointer to the + nearest point on the rectangle boundary. + + This is a generalization of edge and corner hotspots: + - A top edge hotspot is a rectangle covering the entire top edge + with w = output.width, h = threshold, positioned at (0, 0) + - A corner hotspot is a square of size threshold x threshold + positioned at the output corner + + Use cases include: + - Trigger zones that aren't aligned to output edges + - Complex panel layouts where activation regions are around specific UI elements + - Desktop widgets that expand/collapse based on pointer approach + + Note: The rectangle should be within the output bounds. The compositor + may clamp it to output geometry or ignore rectangles entirely outside. + + + + + + + + + - - + + + Represents a region on an output that triggers when the input pointer + remains within it for a specified timeout period. + + Hotspots can be either edge-based (full edge or corner) or positioned + (a specific point on an edge). + + The lifecycle of a hotspot interaction is: + 1. Pointer enters hotspot area -> timeout timer starts + 2. After timeout, enter event is sent + 3. While pointer remains inside hotspot area, proximity_changed events + are sent as the pointer moves + 4. Pointer leaves hotspot area -> leave event is sent + - + Means that the mouse and/or touch finger was inside the indicated - hotspot for the given amount of time. + hotspot for the given amount of timeout time. + + After receiving this event, the client can expect proximity_changed + events as the pointer moves within the hotspot, and eventually a + leave event when the pointer exits. Emitted at most once for each entry of the input inside the hotspot. @@ -115,12 +173,45 @@ This event indicates that the mouse or touch point has left the hotspot area. + After this event, no further proximity_changed events will be sent + until a new enter event. + Emitted only once after each enter. + + + + Sent after an enter event and before the corresponding leave event, + whenever the input pointer moves within the hotspot area. + + Proximity indicates the current distance in pixels from the input pointer + to the trigger point of the hotspot: + - 0: Input pointer is exactly at the trigger point + - threshold: Input pointer is at the outer edge of the hotspot radius + (just entered the hotspot area) + - Values in between: Linear distance from trigger point + + This allows clients to implement pixel-perfect smooth feedback without + needing to know the threshold value used to create the hotspot. + + For standard edge/corner hotspots (created via create_hotspot), the + "threshold" is the distance from the edge parameter. The trigger point + is defined as the closest point on the edge to the input pointer. + + For positioned hotspots (created via create_positioned_hotspot), the + threshold is the radius parameter, and the trigger point is the exact + point on the edge specified by position. + + The compositor should send this event at the same frequency as pointer + motion events to ensure smooth animations. + + + + - +