Pyr r85: monitor reservations. A plugin (drawers) can keep space free along the edges of one monitor of a stitched output (a stitch zone) or of a plain output; maximize, slots, zones, placement and Move snap previews then use the rest, and grid re-fits that monitor's slotted windows (only those that move) with its crossfade. A window marked monitor_reservation_exempt_t (a drawer window) ignores reservations for itself. API additions only (wayfire/window-scope.hpp); WAYFIRE_ABI unchanged. Without reservations nothing changes. diff --git a/plugins/grid/grid.cpp b/plugins/grid/grid.cpp index 46bf79e..4653bc0 100644 --- a/plugins/grid/grid.cpp +++ b/plugins/grid/grid.cpp @@ -457,6 +457,7 @@ class wayfire_grid : public wf::plugin_interface_t, public wf::per_output_tracke void handle_new_output(wf::output_t *output) override { output->connect(&on_workarea_changed); + output->connect(&on_reservation_changed); output->connect(&on_maximize_signal); output->connect(&on_fullscreen_signal); output->connect(&on_tiled); @@ -598,6 +599,69 @@ class wayfire_grid : public wf::plugin_interface_t, public wf::per_output_tracke } }; + /* A drawer (or another plugin) reserved or released space on one monitor: re-fit the slotted and + * zone-placed windows of that monitor. Windows whose slot does not move are left alone, so they do + * not crossfade. */ + wf::signal::connection_t on_reservation_changed = + [=] (wf::monitor_reservation_changed_signal *ev) + { + auto output = ev->output; + auto layout = output->get_data(); + const bool zoned = layout && !layout->zones.empty(); + auto screen = output->get_relative_geometry(); + for (auto& view : output->wset()->get_views(wf::WSET_MAPPED_ONLY)) + { + if (view->pending_fullscreen() || !can_adjust_view(view) || !get_view_wset(view) || + view->has_data()) + { + continue; + } + + auto data = view->get_data(); + auto wm = view->get_pending_geometry(); + if (data && !data->zone.empty()) + { + if ((data->zone != ev->monitor) || !view->pending_tiled_edges()) continue; + auto zone = find_zone(output, data->zone); + if (!zone) continue; + auto ws = get_view_wset(view)->get_current_workspace(); + ws.x += std::floor((wm.x + wm.width / 2) / screen.width); + ws.y += std::floor((wm.y + wm.height / 2) / screen.height); + if (adjust_for_workspace(get_view_wset(view), zone->geometry, ws) != wm) + { + place_in_zone(view, *zone, ws); + } + + continue; + } + + if (zoned) + { + auto zone = wf::window_scope_zone(view); + if (!zone || (zone->name != ev->monitor)) continue; + } else if (ev->monitor != output->to_string()) + { + continue; + } + + int slot = data ? data->slot : 0; + /* Maximized outside the grid plugin (for example when it opened maximized). */ + if (!slot && (view->pending_tiled_edges() == wf::TILED_EDGES_ALL) && + (wm.width == ev->old_workarea.width) && (wm.height == ev->old_workarea.height)) + { + slot = wf::grid::SLOT_CENTER; + } + + if (!slot) continue; + wf::pointf_t delta{std::floor(1.0 * wm.x / screen.width) * screen.width, + std::floor(1.0 * wm.y / screen.height) * screen.height}; + if (slot_dimensions(view, slot) + delta != wm) + { + handle_slot(view, slot, delta); + } + } + }; + wf::geometry_t adjust_for_workspace(std::shared_ptr wset, wf::geometry_t geometry, wf::point_t workspace) { diff --git a/plugins/single_plugins/move-snap-map.hpp b/plugins/single_plugins/move-snap-map.hpp index a55249d..9bf0cc5 100644 --- a/plugins/single_plugins/move-snap-map.hpp +++ b/plugins/single_plugins/move-snap-map.hpp @@ -185,8 +185,14 @@ class move_snap_map_t // Workarea changes are emitted on each output, not on core. wf::signal::connection_t workarea_changed = [this] (auto) { if (!owner.empty()) show(snapshot(scope)); }; + wf::signal::connection_t reservation_changed = + [this] (auto) { if (!owner.empty()) show(snapshot(scope)); }; wf::signal::connection_t output_added = - [this] (wf::output_added_signal *ev) { ev->output->connect(&workarea_changed); }; + [this] (wf::output_added_signal *ev) + { + ev->output->connect(&workarea_changed); + ev->output->connect(&reservation_changed); + }; public: void init() { @@ -226,6 +232,7 @@ class move_snap_map_t for (auto output : wf::get_core().output_layout->get_outputs()) { output->connect(&workarea_changed); + output->connect(&reservation_changed); } } void fini() { hide(); repository->unregister_method("move/snap-regions"); } diff --git a/plugins/single_plugins/move-snap.hpp b/plugins/single_plugins/move-snap.hpp index 18bc85d..32d276d 100644 --- a/plugins/single_plugins/move-snap.hpp +++ b/plugins/single_plugins/move-snap.hpp @@ -334,11 +334,12 @@ class move_snap_t : public wf::per_output_tracker_mixin_t<> }; wf::signal::connection_t on_layout = [this] (auto) { clear(); }; wf::signal::connection_t on_workarea = [this] (auto) { clear(); }; + wf::signal::connection_t on_reservation = [this] (auto) { clear(); }; wf::signal::connection_t on_config = [this] (auto) { clear(); }; public: void handle_new_output(wf::output_t *output) override { - output->connect(&on_tile); output->connect(&on_workarea); + output->connect(&on_tile); output->connect(&on_workarea); output->connect(&on_reservation); } void handle_output_removed(wf::output_t*) override { clear(); } void init() diff --git a/src/api/wayfire/window-scope.hpp b/src/api/wayfire/window-scope.hpp index 73f8d3e..7ee5015 100644 --- a/src/api/wayfire/window-scope.hpp +++ b/src/api/wayfire/window-scope.hpp @@ -17,6 +17,43 @@ wf::geometry_t physical_zone_workarea(wf::output_t *output, const output_stitch_ /** The workarea outside physical zones. A plain output is one monitor, so it gets the same grid/zone_gap * inset as a physical zone; the virtual scope of a combined output stays edge to edge. */ wf::geometry_t unzoned_workarea(wf::output_t *output); + +/** Space kept free along a monitor's edges, in logical pixels (for example by a drawer plugin). */ +struct monitor_reservation_t +{ + int top = 0, bottom = 0, left = 0, right = 0; + bool operator ==(const monitor_reservation_t& o) const + { + return top == o.top && bottom == o.bottom && left == o.left && right == o.right; + } +}; +/** A monitor is a stitch zone (by name) of a combined output, or a plain output (output->to_string()). + * Sets @owner's reservation on @monitor; all zeros removes it. The reservations of all owners add up and shrink + * physical_zone_workarea() / unzoned_workarea() (and so maximize, slots, zones and placement) before the zone gap + * is applied; together they never leave less than a third of the monitor in either direction. Emits + * monitor_reservation_changed_signal on @output when the sum changes. */ +void set_monitor_reservation(wf::output_t *output, const std::string& monitor, const std::string& owner, + monitor_reservation_t reservation); +/** The sum of all owners' reservations on @monitor. */ +monitor_reservation_t get_monitor_reservation(wf::output_t *output, const std::string& monitor); +/** The monitor's area before gaps and reservations: its stitch zone within the output workarea, or the workarea + * of a plain output. Empty if @monitor is not on @output. */ +wf::geometry_t monitor_area(wf::output_t *output, const std::string& monitor); +/** The area windows tile into on @monitor: monitor_area() minus reservations, inset by the zone gap. */ +wf::geometry_t monitor_workarea(wf::output_t *output, const std::string& monitor); +/** grid/zone_gap, or the monitor's [output:NAME] zone_gap when that is not negative. */ +int monitor_gap(const std::string& monitor); +/** Emitted on the output after the reservations of one of its monitors changed. */ +struct monitor_reservation_changed_signal +{ + wf::output_t *output; + std::string monitor; + /** monitor_workarea() before the change. */ + wf::geometry_t old_workarea; +}; +/** A window that lives in reserved space (a drawer): its own workarea ignores monitor reservations. */ +struct monitor_reservation_exempt_t : public wf::custom_data_t {}; + void prepare_window_scope(wayfire_toplevel_view view); void update_window_scope_after_move(wayfire_toplevel_view view); void apply_window_scope(wayfire_toplevel_view view, std::optional workspace = {}); diff --git a/src/core/window-scope.cpp b/src/core/window-scope.cpp index c7be4b6..bc1b55f 100644 --- a/src/core/window-scope.cpp +++ b/src/core/window-scope.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -155,8 +156,7 @@ std::optional window_scope_zone_peek(wayfire_toplevel_view return {}; } -/** Inset a monitor's workarea by grid/zone_gap, or by its [output:NAME] zone_gap when that is not negative. */ -static wf::geometry_t inset_by_zone_gap(wf::geometry_t area, const std::string& monitor) +int monitor_gap(const std::string& monitor) { int gap = 0; if (auto option = get_core().config->get_option("grid/zone_gap")) gap = std::stoi(option->get_value_str()); @@ -168,27 +168,136 @@ static wf::geometry_t inset_by_zone_gap(wf::geometry_t area, const std::string& if (local >= 0) gap = local; } } + return gap; +} +/** Inset a monitor's workarea by grid/zone_gap, or by its [output:NAME] zone_gap when that is not negative. */ +static wf::geometry_t inset_by_zone_gap(wf::geometry_t area, const std::string& monitor) +{ + int gap = monitor_gap(monitor); double gx = std::clamp(gap, 0, std::max(0.0, std::floor((area.width - 1) / 2))); double gy = std::clamp(gap, 0, std::max(0.0, std::floor((area.height - 1) / 2))); return {area.x + gx, area.y + gy, area.width - 2 * gx, area.height - 2 * gy}; } -wf::geometry_t physical_zone_workarea(wf::output_t *output, const output_stitch_zone_t& zone) + +namespace +{ +/** Per output: monitor -> owner -> reservation. */ +struct monitor_reservations_t : public custom_data_t +{ + std::map> monitors; +}; +monitor_reservation_t sum_reservations(wf::output_t *output, const std::string& monitor) +{ + monitor_reservation_t sum; + auto data = output ? output->get_data() : nullptr; + if (!data || !data->monitors.count(monitor)) return sum; + for (auto& [owner, r] : data->monitors[monitor]) + { + sum.top += std::max(0, r.top); sum.bottom += std::max(0, r.bottom); + sum.left += std::max(0, r.left); sum.right += std::max(0, r.right); + } + return sum; +} +/** Shrink a pair of reservations proportionally so at least a third of @length stays free. */ +void cap_pair(int& a, int& b, int length) +{ + const int most = length - (length + 2) / 3; + if (a + b <= most || a + b <= 0) return; + const double k = 1.0 * std::max(0, most) / (a + b); + a = std::floor(a * k); b = std::floor(b * k); +} +wf::geometry_t reserve(wf::geometry_t area, wf::output_t *output, const std::string& monitor) +{ + auto r = sum_reservations(output, monitor); + cap_pair(r.left, r.right, area.width); + cap_pair(r.top, r.bottom, area.height); + return {area.x + r.left, area.y + r.top, area.width - r.left - r.right, area.height - r.top - r.bottom}; +} +wf::geometry_t zone_area(wf::output_t *output, const output_stitch_zone_t& zone) { auto area = geometry_intersection(zone.geometry, output->workarea->get_workarea()); if (area.width <= 0 || area.height <= 0) area = zone.geometry; - return inset_by_zone_gap(area, zone.name); + return area; } -wf::geometry_t unzoned_workarea(wf::output_t *output) +bool combined(wf::output_t *output) { - auto area = output->workarea->get_workarea(); auto layout = output->get_data(); - if (layout && !layout->zones.empty()) return area; - return inset_by_zone_gap(area, output->to_string()); + return layout && !layout->zones.empty(); +} +wf::geometry_t zone_workarea(wf::output_t *output, const output_stitch_zone_t& zone, bool reserved) +{ + auto area = zone_area(output, zone); + return inset_by_zone_gap(reserved ? reserve(area, output, zone.name) : area, zone.name); +} +wf::geometry_t plain_workarea(wf::output_t *output, bool reserved) +{ + auto area = output->workarea->get_workarea(); + if (combined(output)) return area; + auto name = output->to_string(); + return inset_by_zone_gap(reserved ? reserve(area, output, name) : area, name); +} +} + +wf::geometry_t physical_zone_workarea(wf::output_t *output, const output_stitch_zone_t& zone) +{ + return zone_workarea(output, zone, true); +} +wf::geometry_t unzoned_workarea(wf::output_t *output) +{ + return plain_workarea(output, true); } wf::geometry_t window_scope_workarea(wayfire_toplevel_view view) { - if (auto zone = window_scope_zone(view)) return physical_zone_workarea(view->get_output(), *zone); - return unzoned_workarea(view->get_output()); + const bool reserved = !view->has_data(); + if (auto zone = window_scope_zone(view)) return zone_workarea(view->get_output(), *zone, reserved); + return plain_workarea(view->get_output(), reserved); +} + +wf::geometry_t monitor_area(wf::output_t *output, const std::string& monitor) +{ + if (!output) return {0, 0, 0, 0}; + if (auto layout = output->get_data(); layout && !layout->zones.empty()) + { + for (auto& zone : layout->zones) if (zone.name == monitor) return zone_area(output, zone); + return {0, 0, 0, 0}; + } + return monitor == output->to_string() ? output->workarea->get_workarea() : wf::geometry_t{0, 0, 0, 0}; +} +wf::geometry_t monitor_workarea(wf::output_t *output, const std::string& monitor) +{ + if (!output) return {0, 0, 0, 0}; + if (auto layout = output->get_data(); layout && !layout->zones.empty()) + { + for (auto& zone : layout->zones) if (zone.name == monitor) return zone_workarea(output, zone, true); + return {0, 0, 0, 0}; + } + return monitor == output->to_string() ? plain_workarea(output, true) : wf::geometry_t{0, 0, 0, 0}; +} +monitor_reservation_t get_monitor_reservation(wf::output_t *output, const std::string& monitor) +{ + return sum_reservations(output, monitor); +} +void set_monitor_reservation(wf::output_t *output, const std::string& monitor, const std::string& owner, + monitor_reservation_t reservation) +{ + if (!output) return; + auto before = sum_reservations(output, monitor); + auto old_workarea = monitor_workarea(output, monitor); + auto data = output->get_data_safe(); + if (reservation == monitor_reservation_t{}) + { + if (auto it = data->monitors.find(monitor); it != data->monitors.end()) + { + it->second.erase(owner); + if (it->second.empty()) data->monitors.erase(it); + } + } else + { + data->monitors[monitor][owner] = reservation; + } + if (sum_reservations(output, monitor) == before) return; + monitor_reservation_changed_signal ev{output, monitor, old_workarea}; + output->emit(&ev); } wf::geometry_t window_scope_fullscreen(wayfire_toplevel_view view) {