--- /dev/null +++ b/src/api/wayfire/window-scope.hpp @@ -0,0 +1,32 @@ +#pragma once +#include +#include +#include +#include + +namespace wf +{ +/** Window policy is independent of the outputs advertised to clients. */ +std::string window_scope_policy(wayfire_toplevel_view view); +std::optional window_scope_zone(wayfire_toplevel_view view); +wf::geometry_t window_scope_workarea(wayfire_toplevel_view view); +wf::geometry_t window_scope_fullscreen(wayfire_toplevel_view view); +wf::geometry_t physical_zone_workarea(wf::output_t *output, const output_stitch_zone_t& zone); +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 = {}); +void set_window_scope(wayfire_toplevel_view view, const std::string& scope); +void select_window_scope_monitor(wayfire_toplevel_view view, const std::string& name); +/** Throws for malformed rule data. Disconnected connector names are retained. */ +void validate_window_scope_rules(const std::string& rules); +bool valid_window_scope(const std::string& scope, bool allow_auto = false); + +/** Relayout keeps explicit tiling choices such as Shift zone maximize. */ +struct window_scope_reflow_t : public wf::custom_data_t {}; + +/** Physical fullscreen hides only the covered part of the output's TOP layer. */ +struct physical_fullscreen_regions_t : public wf::custom_data_t +{ + wf::regionf_t hidden; +}; +} --- a/src/core/output-layout.cpp +++ b/src/core/output-layout.cpp @@ -1,3 +1,5 @@ +#include +#include #include "wayfire/output.hpp" #include "wayfire/core.hpp" #include "wayfire/output-layout.hpp" @@ -1977,6 +1979,24 @@ /** Load config from file, test and apply. Exclude a connector being removed. */ void reconfigure_from_config(wlr_output *removing = nullptr) { + // Keep workspace identity across asynchronous view transactions while + // physical outputs are removed/recreated. Inferring a workspace from + // old committed geometry after a canvas resize can move a physical + // fullscreen window to a different workspace. + std::vector> scoped_views; + if (stitch_output || wf::option_wrapper_t("core/stitch_enabled")) + { + for (auto view : wf::get_core().get_all_views()) + if (auto top = wf::toplevel_cast(view); top && top->is_mapped() && top->get_output() && top->get_wset()) + { + auto size = top->get_output()->get_screen_size(); + auto g = top->get_pending_geometry(); + auto ws = top->get_wset()->get_current_workspace(); + ws.x += std::floor((g.x + g.width / 2) / size.width); + ws.y += std::floor((g.y + g.height / 2) / size.height); + scoped_views.emplace_back(top, ws); + } + } // Load desired configuration from config file output_configuration_t configuration; for (auto& [output, layout_output] : this->outputs) @@ -2031,6 +2051,18 @@ } destroy_stitch_output(); + } + for (auto& [view, workspace] : scoped_views) + { + if (!view->is_mapped() || !view->get_output()) continue; + wf::prepare_window_scope(view); + if (auto wset = view->get_wset()) + { + auto size = wset->get_workspace_grid_size(); + workspace.x = std::clamp(workspace.x, 0, size.width - 1); + workspace.y = std::clamp(workspace.y, 0, size.height - 1); + } + wf::apply_window_scope(view, workspace); } } --- a/src/core/scene.cpp +++ b/src/core/scene.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -393,6 +394,10 @@ return {}; } + auto output = get_output(); + if (output && output->node_for_layer(layer::TOP).get() == this) + if (auto regions = output->get_data()) + if (regions->hidden.contains_pointf(to_local(at))) return {}; return node_t::find_node_at(at); } @@ -470,16 +475,27 @@ wf::render_target_t new_target = target.translated(-offset); damage += -offset; + wf::regionf_t excluded; + if (self->get_output()->node_for_layer(layer::TOP).get() == self) + if (auto regions = self->get_output()->get_data()) + { + excluded = damage & regions->hidden; + damage ^= regions->hidden; + } for (auto& ch : children) { ch->schedule_instructions(instructions, new_target, damage); } + damage |= excluded; damage += offset; } direct_scanout try_scanout(wf::output_t *scanout) override { + if (self->get_output()->node_for_layer(layer::TOP).get() == self) + if (auto regions = self->get_output()->get_data()) + if (!regions->hidden.empty()) return direct_scanout::OCCLUSION; if ((scanout != this->self->get_output()) && this->self->limit_region) { // Can't scanout on a different output because it is outside @@ -502,7 +518,16 @@ void compute_visibility(wf::output_t *output, wf::regionf_t& visible) override { auto offset = wf::origin(output->get_layout_geometry()); + wf::regionf_t excluded; + if (self->get_output()->node_for_layer(layer::TOP).get() == self) + if (auto regions = self->get_output()->get_data()) + { + auto mask = regions->hidden + wf::origin(self->get_output()->get_layout_geometry()); + excluded = visible & mask; + visible ^= mask; + } compute_visibility_from_list(children, output, visible, offset); + visible |= excluded; } }; --- a/src/core/window-manager.cpp +++ b/src/core/window-manager.cpp @@ -1,3 +1,4 @@ +#include #include "wayfire/core.hpp" #include "wayfire/geometry.hpp" #include "wayfire/object.hpp" @@ -217,7 +218,7 @@ } const wf::point_t workspace = ws.value_or(view->get_output()->wset()->get_current_workspace()); - auto wa_geometry = view->get_output()->workarea->get_workarea(); + auto wa_geometry = window_scope_workarea(view); view_tile_request_signal data; data.view = view; @@ -269,6 +270,7 @@ } wf::output_t *wo = output ?: (view->get_output() ?: wf::get_core().seat->get_active_output()); + if (window_scope_policy(view) != "physical") wo = view->get_output() ?: wo; const wf::point_t workspace = ws.value_or(wo->wset()->get_current_workspace()); wf::dassert(wo != nullptr, "Fullscreening should not happen with null output!"); @@ -281,11 +283,11 @@ data.view = view; data.state = state; data.workspace = workspace; - data.desired_size = wo->get_relative_geometry(); + data.desired_size = window_scope_fullscreen(view); if (!state) { - data.desired_size = view->pending_tiled_edges() ? wo->workarea->get_workarea() : + data.desired_size = view->pending_tiled_edges() ? window_scope_workarea(view) : get_last_windowed_geometry(view).value_or(wf::geometry_t{0.0, 0.0, -1.0, -1.0}); } else { --- /dev/null +++ b/src/core/window-scope.cpp @@ -0,0 +1,246 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wf +{ +namespace +{ +struct scope_data_t : public custom_data_t +{ + std::string override_scope, policy, monitor; +}; +std::string setting(const std::string& key) +{ + auto opt = get_core().config->get_option(key); + return opt ? opt->get_value_str() : ""; +} +using rules_t = std::vector>; +rules_t parse_rules(const std::string& text) +{ + json_t value; + if (text.size() > 65536 || json_t::parse_string(text, value) || !value.is_array() || value.size() > 256) + throw std::runtime_error("Window scope rules must be a JSON array of at most 256 rules"); + rules_t rules; + for (size_t i = 0; i < value.size(); ++i) + { + json_t row = value[i]; + if (!row.is_object() || row.get_member_names().size() != 2 || + !row.has_member("app_id") || !row["app_id"].is_string() || + !row.has_member("scope") || !row["scope"].is_string()) + throw std::runtime_error("Each window scope rule needs app_id and scope strings"); + auto app = row["app_id"].as_string(), scope = row["scope"].as_string(); + if (app.empty() || app.size() > 512 || app.find_first_of("\r\n") != app.npos || + app.find('\0') != app.npos || !valid_window_scope(scope)) + throw std::runtime_error("Invalid application pattern or window scope"); + rules.emplace_back(app, scope); + } + return rules; +} +std::string rule_policy(wayfire_toplevel_view view) +{ + static std::string previous; + static rules_t rules; + auto text = setting("core/window_scope_rules"); + if (text != previous) + { + previous = text; + try { rules = parse_rules(text); } + catch (const std::exception& e) { rules.clear(); LOGE(e.what()); } + } + for (auto& [pattern, scope] : rules) + if (!fnmatch(pattern.c_str(), view->get_app_id().c_str(), 0)) return scope; + auto scope = setting("core/window_scope"); + return scope == "virtual" ? "virtual" : "physical"; +} +std::string nearest_monitor(wayfire_toplevel_view view, bool pointer) +{ + auto output = view->get_output(); + auto layout = output ? output->get_data() : nullptr; + if (!layout || layout->zones.empty()) return ""; + auto g = view->get_pending_geometry(); + auto size = output->get_screen_size(); + auto p = pointer ? output->get_cursor_position() : pointf_t{g.x + g.width / 2, g.y + g.height / 2}; + if (!pointer) + { + p.x -= std::floor(p.x / size.width) * size.width; + p.y -= std::floor(p.y / size.height) * size.height; + } + double best = INFINITY; + std::string name; + for (auto& zone : layout->zones) + { + auto& area = zone.geometry; + double x = std::clamp(p.x, area.x, area.x + area.width); + double y = std::clamp(p.y, area.y, area.y + area.height); + double distance = (p.x - x) * (p.x - x) + (p.y - y) * (p.y - y); + if (distance < best) { best = distance; name = zone.name; } + } + return name; +} +} + +bool valid_window_scope(const std::string& scope, bool allow_auto) +{ + if (scope == "virtual" || scope == "physical" || (allow_auto && scope == "auto")) return true; + // Connector names are retained across unplug/replug. Reject reserved tokens, + // whitespace and INI/JSON control characters, including embedded NULs. + return scope != "auto" && !scope.empty() && scope.size() <= 128 && + scope.find('-') != scope.npos && + scope.find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.:") == scope.npos; +} +void validate_window_scope_rules(const std::string& rules) { parse_rules(rules); } + +std::string window_scope_policy(wayfire_toplevel_view view) +{ + if (!view) return "virtual"; + auto data = view->get_data_safe(); + if (!data->override_scope.empty()) return data->override_scope == "VIRTUAL-1" ? "virtual" : data->override_scope; + if (view->parent) return window_scope_policy(view->parent); + auto policy = rule_policy(view); + return policy == "VIRTUAL-1" ? "virtual" : policy; +} + +std::optional window_scope_zone(wayfire_toplevel_view view) +{ + auto output = view->get_output(); + auto layout = output ? output->get_data() : nullptr; + if (!layout || layout->zones.empty()) return {}; + auto data = view->get_data_safe(); + auto policy = window_scope_policy(view); + if (policy == "virtual") return {}; + if (data->policy != policy) { data->policy = policy; data->monitor.clear(); } + if (policy != "physical") data->monitor = policy; + else if (view->parent && data->override_scope.empty()) + { + if (auto zone = window_scope_zone(view->parent)) data->monitor = zone->name; + } + if (data->monitor.empty()) data->monitor = nearest_monitor(view, !view->is_mapped()); + for (auto& zone : layout->zones) if (zone.name == data->monitor) return zone; + // The selected head disappeared; use a remaining region without rewriting + // an explicit application rule, so reconnecting the head restores it. + data->monitor = nearest_monitor(view, !view->is_mapped()); + for (auto& zone : layout->zones) if (zone.name == data->monitor) return zone; + return {}; +} + +wf::geometry_t physical_zone_workarea(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; + int gap = 0; + if (auto option = get_core().config->get_option("grid/zone_gap")) gap = std::stoi(option->get_value_str()); + for (auto& [head, state] : get_core().output_layout->get_current_configuration()) + { + if (zone.name != head->name) continue; + auto section = get_core().config_backend->get_output_section(head); + if (auto option = section->get_option_or("zone_gap")) + { + int local = std::stoi(option->get_value_str()); + if (local >= 0) gap = local; + } + break; + } + 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 window_scope_workarea(wayfire_toplevel_view view) +{ + if (auto zone = window_scope_zone(view)) return physical_zone_workarea(view->get_output(), *zone); + return view->get_output()->workarea->get_workarea(); +} +wf::geometry_t window_scope_fullscreen(wayfire_toplevel_view view) +{ + if (auto zone = window_scope_zone(view)) return zone->geometry; + return view->get_output()->get_relative_geometry(); +} + +void prepare_window_scope(wayfire_toplevel_view view) +{ + if (view->parent) return; + auto policy = window_scope_policy(view); + for (auto output : get_core().output_layout->get_outputs()) + { + auto layout = output->get_data(); + bool match = policy == "virtual" ? bool(layout) : policy == output->handle->name; + if (layout) for (auto& zone : layout->zones) match |= policy == zone.name; + if (match) + { + if (view->get_output() != output) + { + if (view->is_mapped()) move_view_to_output(view, output, false); + else view->set_output(output); + } + break; + } + } + window_scope_zone(view); +} + +void apply_window_scope(wayfire_toplevel_view view, std::optional workspace) +{ + auto old_output = view->get_output(); + auto ws = old_output ? old_output->wset()->get_current_workspace() : wf::point_t{0, 0}; + if (old_output) + { + auto size = old_output->get_screen_size(); + auto geometry = view->get_pending_geometry(); + ws.x += std::floor((geometry.x + geometry.width / 2) / size.width); + ws.y += std::floor((geometry.y + geometry.height / 2) / size.height); + } + prepare_window_scope(view); + auto output = view->get_output(); + if (!output) return; + if (workspace) ws = *workspace; + auto grid = output->wset()->get_workspace_grid_size(); + ws.x = std::clamp(ws.x, 0, grid.width - 1); + ws.y = std::clamp(ws.y, 0, grid.height - 1); + if (view->pending_fullscreen()) get_core().default_wm->fullscreen_request(view, output, true, ws); + else if (view->pending_tiled_edges()) + { + if (workspace) view->get_data_safe(); + get_core().default_wm->tile_request(view, view->pending_tiled_edges(), ws); + if (workspace) view->erase_data(); + } + else if (window_scope_zone(view)) + { + auto area = window_scope_workarea(view); + auto delta = ws - output->wset()->get_current_workspace(); + auto size = output->get_screen_size(); + area.x += delta.x * size.width; area.y += delta.y * size.height; + view->set_geometry(wf::clamp(view->get_pending_geometry(), area)); + } + for (auto child : view->children) if (child->is_mapped()) apply_window_scope(child, ws); +} +void update_window_scope_after_move(wayfire_toplevel_view view) +{ + if (window_scope_policy(view) == "physical") + view->get_data_safe()->monitor = nearest_monitor(view, false); + apply_window_scope(view); +} +void set_window_scope(wayfire_toplevel_view view, const std::string& scope) +{ + if (!valid_window_scope(scope, true)) throw std::runtime_error("Invalid window scope"); + auto data = view->get_data_safe(); + data->override_scope = scope == "auto" ? "" : scope; + data->monitor.clear(); + apply_window_scope(view); +} +void select_window_scope_monitor(wayfire_toplevel_view view, const std::string& name) +{ + auto data = view->get_data_safe(); + data->policy = window_scope_policy(view); + data->monitor = name; +} +} --- a/src/meson.build +++ b/src/meson.build @@ -5,6 +5,7 @@ 'render.cpp', 'core/window-manager.cpp', + 'core/window-scope.cpp', 'core/output-layout.cpp', 'core/plugin-loader.cpp', 'core/matcher.cpp', --- a/src/output/promotion-manager.hpp +++ b/src/output/promotion-manager.hpp @@ -1,3 +1,5 @@ +#include +#include #pragma once #include "wayfire/toplevel-view.hpp" @@ -46,7 +48,7 @@ update_promotion_state(); }; - wayfire_toplevel_view find_top_visible_view(wf::scene::node_ptr root) + wayfire_toplevel_view find_top_visible_view(wf::scene::node_ptr root, std::optional region = {}) { if (auto view = wf::node_to_view(root)) { @@ -57,7 +59,9 @@ if (output->wset()->view_visible_on(toplevel_cast(view), output->wset()->get_current_workspace())) { - return toplevel_cast(view); + auto top = toplevel_cast(view); + auto overlap = region ? wf::geometry_intersection(top->get_geometry(), *region) : top->get_geometry(); + if (overlap.width > 0 && overlap.height > 0) return top; } } @@ -65,7 +69,7 @@ { if (ch->is_enabled()) { - if (auto result = find_top_visible_view(ch)) + if (auto result = find_top_visible_view(ch, region)) { return result; } @@ -77,15 +81,46 @@ void update_promotion_state() { - wayfire_toplevel_view candidate = find_top_visible_view(output->wset()->get_node()); - if (candidate && candidate->toplevel()->current().fullscreen) + // Each physical region has its own top visible window. A fullscreen + // window on one monitor must not hide bars on the other monitors. + if (updating) return; + updating = true; + wf::regionf_t hidden; + bool entire_output = false; + auto layout = output->get_data(); + if (layout) { - start_promotion(); + for (auto& zone : layout->zones) + { + auto candidate = find_top_visible_view(output->wset()->get_node(), zone.geometry); + if (candidate && candidate->toplevel()->current().fullscreen) + { + auto g = candidate->get_geometry(); + hidden |= wf::geometry_intersection(g, zone.geometry); + // Only the real full-output state is announced to shells. + auto full = output->get_relative_geometry(); + entire_output |= g.x <= full.x && g.y <= full.y && + g.x + g.width >= full.x + full.width && g.y + g.height >= full.y + full.height; + } + } } else { - stop_promotion(); + auto candidate = find_top_visible_view(output->wset()->get_node()); + entire_output = candidate && candidate->toplevel()->current().fullscreen; } + auto data = output->get_data_safe(); + if (!(hidden ^ data->hidden).empty() || !(data->hidden ^ hidden).empty()) + { + data->hidden = hidden; + output->render->damage_whole(); + scene::update(output->node_for_layer(scene::layer::TOP), scene::update_flag::INPUT_STATE); + } + if (entire_output) start_promotion(); + else stop_promotion(); + updating = false; } + + bool updating = false; bool promotion_active = false; --- a/src/view/toplevel-view.cpp +++ b/src/view/toplevel-view.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -43,7 +44,7 @@ (int)std::floor(center.y / scr_size.height), }; - auto workarea = view->get_output()->workarea->get_workarea(); + auto workarea = wf::window_scope_workarea(view); workarea.x += scr_size.width * parent_ws.x; workarea.y += scr_size.height * parent_ws.y; --- a/src/view/view-impl.cpp +++ b/src/view/view-impl.cpp @@ -1,3 +1,4 @@ +#include #include "wayfire/core.hpp" #include "view-impl.hpp" #include "wayfire/scene-input.hpp" @@ -456,6 +457,7 @@ void wf::adjust_view_pending_geometry_on_start_map(wf::toplevel_view_interface_t *self, wf::geometry_t map_geometry_client, bool map_fs, bool map_maximized) { + prepare_window_scope(self); if (map_fs) { self->toplevel()->pending().fullscreen = true; @@ -465,7 +467,7 @@ self->toplevel()->pending().tiled_edges = wf::TILED_EDGES_ALL; if (self->get_output()) { - self->toplevel()->pending().geometry = self->get_output()->workarea->get_workarea(); + self->toplevel()->pending().geometry = window_scope_workarea(self); } } else { @@ -474,7 +476,7 @@ if (self->get_output()) { - map_geometry = wf::clamp(map_geometry, self->get_output()->workarea->get_workarea()); + map_geometry = wf::clamp(map_geometry, window_scope_workarea(self)); } self->toplevel()->pending().geometry = map_geometry; --- a/src/view/xdg-shell/xdg-toplevel-view.cpp +++ b/src/view/xdg-shell/xdg-toplevel-view.cpp @@ -1,3 +1,4 @@ +#include #include "xdg-toplevel-view.hpp" #include #include "view/toplevel-node.hpp" @@ -111,6 +112,7 @@ { this->app_id = new_app_id; wf::view_implementation::emit_app_id_changed_signal(self()); + if (auto top = wf::toplevel_cast(self()); top && top->is_mapped()) wf::apply_window_scope(top); } void wf::xdg_toplevel_view_base_t::close() @@ -227,6 +229,7 @@ }); on_request_maximize.set_callback([&] (void *data) { + wf::prepare_window_scope({this}); wf::get_core().default_wm->tile_request({this}, xdg_toplevel->requested.maximized ? wf::TILED_EDGES_ALL : 0); }); @@ -234,6 +237,7 @@ { wlr_xdg_toplevel_requested *req = &xdg_toplevel->requested; auto wo = wf::get_core().output_layout->find_output(req->fullscreen_output); + wf::prepare_window_scope({this}); wf::get_core().default_wm->fullscreen_request({this}, wo, req->fullscreen); }); @@ -268,6 +272,7 @@ // set initial parent self->on_set_parent.emit(nullptr); + wf::prepare_window_scope(self); if (toplevel->requested.fullscreen) { wf::get_core().default_wm->fullscreen_request(self, self->get_output(), true); --- a/plugins/grid/grid.cpp +++ b/plugins/grid/grid.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -62,6 +63,13 @@ wf::ipc_activator_t bindings[10]; wf::ipc_activator_t restore{"grid/restore"}; wf::ipc_activator_t zone_maximize{"grid/zone_maximize"}; + wf::ipc_activator_t use_virtual{"grid/use_virtual"}, use_physical{"grid/use_physical"}, + reset_scope{"grid/reset_scope"}, next_monitor{"grid/next_monitor"}; + wf::option_wrapper_t default_scope{"core/window_scope"}, scope_rules{"core/window_scope_rules"}; + wf::geometry_t slot_dimensions(wayfire_toplevel_view view, int n) + { + return wf::grid::get_slot_dimensions(wf::window_scope_workarea(view), n); + } wf::option_wrapper_t zone_shift{"grid/zone_shift"}; wf::option_wrapper_t zone_gap{"grid/zone_gap"}; wf::option_wrapper_t snap_threshold{"move/snap_threshold"}; @@ -114,36 +122,8 @@ { if (zone.name == name) { - zone.geometry = wf::geometry_intersection(zone.geometry, output->workarea->get_workarea()); - if ((zone.geometry.width > 0) && (zone.geometry.height > 0)) - { - // Inset each physical region, including the internal seams. - // Clamp independently so even a small region stays usable. - auto& g = zone.geometry; - int gap = zone_gap; - for (auto& [head, state] : wf::get_core().output_layout->get_current_configuration()) - { - if (name == head->name) - { - auto section = wf::get_core().config_backend->get_output_section(head); - if (auto opt = section->get_option_or("zone_gap")) - { - int local_gap = std::stoi(opt->get_value_str()); - if (local_gap >= 0) gap = local_gap; - } - break; - } - } - double gx = std::clamp(gap, 0, - std::max(0.0, std::floor((g.width - 1) / 2))); - double gy = std::clamp(gap, 0, - std::max(0.0, std::floor((g.height - 1) / 2))); - g.x += gx; - g.y += gy; - g.width -= 2 * gx; - g.height -= 2 * gy; - return zone; - } + zone.geometry = wf::physical_zone_workarea(output, zone); + return zone; } } } @@ -266,6 +246,36 @@ void init() override { init_output_tracking(); + default_scope.set_callback([this] { reset_policy_zones(); }); + scope_rules.set_callback([this] { reset_policy_zones(); }); + auto scope_handler = [this] (std::string scope) + { + return [this, scope] (wf::output_t *wo, wayfire_view view) + { + auto top = wf::toplevel_cast(view); + if (!top || !can_adjust_view(top) || !wo->can_activate_plugin(&grab_interface)) return false; + top->get_data_safe()->zone.clear(); + wf::set_window_scope(top, scope); + return true; + }; + }; + use_virtual.set_handler(scope_handler("virtual")); + use_physical.set_handler(scope_handler("physical")); + reset_scope.set_handler(scope_handler("auto")); + next_monitor.set_handler([this] (wf::output_t *wo, wayfire_view view) + { + auto top = wf::toplevel_cast(view); + auto layout = wo->get_data(); + if (!top || !layout || layout->zones.empty() || !can_adjust_view(top) || + !wo->can_activate_plugin(&grab_interface)) return false; + auto current = zone_for_window(top); + size_t index = 0; + for (size_t i = 0; i < layout->zones.size(); ++i) + if (current && layout->zones[i].name == current->name) index = (i + 1) % layout->zones.size(); + top->get_data_safe()->zone.clear(); + wf::set_window_scope(top, layout->zones[index].name); + return true; + }); zone_gap.set_callback([this] () { reflow_zones(); }); restore.set_handler(handle_restore); zone_maximize.set_handler([=] (wf::output_t *wo, wayfire_view view) @@ -326,14 +336,22 @@ { return; } + auto drag_area = ev->output->workarea->get_workarea(); + if (wf::window_scope_policy(ev->view) != "virtual") + { + if (auto layout = ev->output->get_data()) + for (auto& zone : layout->zones) + if (zone.geometry & wf::pointf_t{double(ev->input.x), double(ev->input.y)}) + drag_area = wf::physical_zone_workarea(ev->output, zone); + } wf::grid::slot_t new_slot_id = ev->operation == wf::grid::MOVE_OP_CLEAR_PREVIEW ? wf::grid::slot_t::SLOT_NONE : - wf::grid::calc_slot(ev->output, ev->input, snap_threshold, quarter_snap_threshold); + wf::grid::calc_slot(drag_area, ev->input, snap_threshold, quarter_snap_threshold); if ((ev->operation == wf::grid::MOVE_OP_DROP) && new_slot_id) { ev->view->toplevel()->pending().tiled_edges = wf::grid::get_tiled_edges_for_slot(new_slot_id); - auto desired_size = wf::grid::get_slot_dimensions(ev->view->get_output(), new_slot_id); + auto desired_size = slot_dimensions(ev->view, new_slot_id); ev->view->get_data_safe()->slot = new_slot_id; ev->view->get_data_safe()->zone.clear(); ensure_grid_view(ev->view)->adjust_target_geometry( @@ -363,7 +381,7 @@ /* Show a preview overlay */ if (new_slot_id) { - wf::geometry_t slot_geometry = wf::grid::get_slot_dimensions(ev->output, new_slot_id); + wf::geometry_t slot_geometry = wf::grid::get_slot_dimensions(drag_area, new_slot_id); /* Unknown slot geometry, can't show a preview */ if ((slot_geometry.width <= 0) || (slot_geometry.height <= 0)) { @@ -418,7 +436,7 @@ view->get_data_safe()->slot = slot; view->get_data_safe()->zone.clear(); - auto slot_geometry = wf::grid::get_slot_dimensions(view->get_output(), slot) + delta; + auto slot_geometry = slot_dimensions(view, slot) + delta; ensure_grid_view(view)->adjust_target_geometry( slot_geometry, wf::grid::get_tiled_edges_for_slot(slot)); } @@ -448,6 +466,14 @@ return false; } + void reset_policy_zones() + { + for (auto view : wf::get_core().get_all_views()) + if (auto top = wf::toplevel_cast(view)) + if (auto data = top->get_data()) data->zone.clear(); + reflow_zones(); + } + void reflow_zones() { // Member order can change without changing the combined workarea. @@ -455,7 +481,7 @@ { for (auto view : output->wset()->get_views(wf::WSET_MAPPED_ONLY)) { - reflow_zone(view); + if (!reflow_zone(view)) wf::apply_window_scope(view); } } @@ -531,6 +557,15 @@ } data->carried_out = true; + if (data->view->has_data()) + { + if (auto previous = data->view->get_data(); previous && !previous->zone.empty()) + if (auto zone = find_zone(data->view->get_output(), previous->zone)) + { + place_in_zone(data->view, *zone, data->workspace); + return; + } + } if ((data->edges == wf::TILED_EDGES_ALL) && shift_zones_active()) { if (auto zone = zone_for_window(data->view)) @@ -543,7 +578,7 @@ uint32_t slot = wf::grid::get_slot_from_tiled_edges(data->edges); if (slot > 0) { - data->desired_size = wf::grid::get_slot_dimensions(data->view->get_output(), slot); + data->desired_size = slot_dimensions(data->view, slot); } data->view->get_data_safe()->slot = slot; @@ -571,7 +606,7 @@ uint32_t slot = data->view->get_data_safe()->slot; if (slot > 0) { - geom = wf::grid::get_slot_dimensions(data->view->get_output(), slot); + geom = slot_dimensions(data->view, slot); edges = wf::grid::get_tiled_edges_for_slot(slot); if (auto zone = find_zone(data->view->get_output(), data->view->get_data()->zone)) --- a/plugins/grid/wayfire/plugins/grid.hpp +++ b/plugins/grid/wayfire/plugins/grid.hpp @@ -104,9 +104,8 @@ * 4 5 6 * 1 2 3 * */ -inline wf::geometry_t get_slot_dimensions(wf::output_t *output, int n) +inline wf::geometry_t get_slot_dimensions(wf::geometry_t area, int n) { - auto area = output->workarea->get_workarea(); int w2 = area.width / 2; int h2 = area.height / 2; @@ -131,12 +130,16 @@ return area; } +inline wf::geometry_t get_slot_dimensions(wf::output_t *output, int n) +{ + return get_slot_dimensions(output->workarea->get_workarea(), n); +} + /* Calculate the slot to which the view would be snapped if the input * is released at output-local coordinates (x, y) */ -inline wf::grid::slot_t calc_slot(wf::output_t *output, wf::point_t point, int snap_threshold, +inline wf::grid::slot_t calc_slot(wf::geometry_t g, wf::point_t point, int snap_threshold, int quarter_snap_threshold) { - auto g = output->workarea->get_workarea(); int threshold = snap_threshold; @@ -180,5 +183,11 @@ return slot; } +inline wf::grid::slot_t calc_slot(wf::output_t *output, wf::point_t point, int snap_threshold, + int quarter_snap_threshold) +{ + return calc_slot(output->workarea->get_workarea(), point, snap_threshold, quarter_snap_threshold); +} + } } --- a/plugins/ipc-rules/ipc-rules-common.hpp +++ b/plugins/ipc-rules/ipc-rules-common.hpp @@ -1,3 +1,4 @@ +#include #pragma once #include "wayfire/plugins/common/shared-core-data.hpp" #include "wayfire/plugins/ipc/ipc-helpers.hpp" @@ -211,6 +212,12 @@ description["mapped"] = view->is_mapped(); description["layer"] = layer_to_string(get_view_layer(view)); description["tiled-edges"] = toplevel ? toplevel->pending_tiled_edges() : 0; + if (toplevel && output) + { + description["window-scope"] = wf::window_scope_policy(toplevel); + auto region = wf::window_scope_zone(toplevel); + description["physical-monitor"] = region ? region->name : ""; + } description["fullscreen"] = toplevel ? toplevel->pending_fullscreen() : false; description["minimized"] = toplevel ? toplevel->minimized : false; description["activated"] = toplevel ? toplevel->activated : false; --- a/plugins/ipc-rules/ipc-rules.cpp +++ b/plugins/ipc-rules/ipc-rules.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -25,6 +26,15 @@ public: void init() override { + method_repository->register_method("window-rules/set-window-scope", [] (wf::json_t data) + { + auto view = wf::toplevel_cast(wf::ipc::json_find_view_or_throw(data)); + if (!view) return wf::ipc::json_error("view is not a toplevel"); + auto scope = wf::ipc::json_get_string(data, "scope"); + if (!wf::valid_window_scope(scope, true)) return wf::ipc::json_error("Invalid window scope"); + wf::set_window_scope(view, scope); + return wf::ipc::json_ok(); + }); method_repository->register_method("window-rules/list-views", list_views); method_repository->register_method("window-rules/list-outputs", list_outputs); method_repository->register_method("window-rules/set-output-power", [] (wf::json_t data) @@ -54,6 +64,7 @@ void fini() override { + method_repository->unregister_method("window-rules/set-window-scope"); method_repository->unregister_method("window-rules/list-views"); method_repository->unregister_method("window-rules/list-outputs"); method_repository->unregister_method("window-rules/set-output-power"); @@ -366,7 +377,7 @@ if (named_output) { wf::move_view_to_output(toplevel, named_output, false); - toplevel->toplevel()->pending().geometry = *target_region; + if (target_region) toplevel->toplevel()->pending().geometry = *target_region; } if (requested_geometry) @@ -390,6 +401,8 @@ toplevel->toplevel()->pending().fullscreen = *fullscreen; } + if (requested_geometry || target_region) wf::update_window_scope_after_move(toplevel); + if (fullscreen.value_or(false) || tiled_edges.value_or(0)) wf::apply_window_scope(toplevel); // Schedule one transaction with all the state changes. if (data.has_member("geometry") || target_region || tiled_edges.has_value() || fullscreen.has_value()) { --- a/plugins/single_plugins/move.cpp +++ b/plugins/single_plugins/move.cpp @@ -1,3 +1,4 @@ +#include #include "wayfire/debug.hpp" #include "wayfire/geometry.hpp" #include "wayfire/plugins/common/input-grab.hpp" @@ -115,6 +116,7 @@ wf::get_core().default_wm->set_view_grabbed(ev->main_view, true); wf::move_drag::adjust_view_on_output(ev); + wf::update_window_scope_after_move(ev->main_view); if (enable_snap) { --- a/plugins/single_plugins/output-control.cpp +++ b/plugins/single_plugins/output-control.cpp @@ -1,3 +1,4 @@ +#include // Display settings transactions for WCM. The compositor owns the timeout; // unconfirmed settings are never written to disk. #include @@ -130,7 +131,7 @@ std::set original_heads; std::chrono::steady_clock::time_point deadline; const std::vector globals = {"core/stitch_enabled", "core/stitch_outputs", - "core/default_output", "grid/zone_gap", "grid/zone_shift", "grid/zone_maximize", "move/activate_zone"}; + "core/default_output", "core/window_scope", "core/window_scope_rules", "grid/zone_gap", "grid/zone_shift", "grid/zone_maximize", "move/activate_zone", "grid/use_virtual", "grid/use_physical", "grid/reset_scope", "grid/next_monitor"}; const std::vector head_options = {"mode", "position", "scale", "transform", "masked", "vrr", "hdr", "depth", "zone_gap"}; auto option(const std::string& name) { return wf::get_core().config->get_option(name); } @@ -271,6 +272,9 @@ auto copy = p->clone_option(); if (!copy->set_value_str(v)) throw std::runtime_error("Invalid value for " + key); } + auto scope = value(changes, "core/window_scope"); + if (scope != "physical" && scope != "virtual") throw std::runtime_error("Default scope must be physical or virtual"); + wf::validate_window_scope_rules(value(changes, "core/window_scope_rules")); auto config = wf::get_core().output_layout->get_current_configuration(); auto connected = heads(); auto group = members(changes); --- a/plugins/single_plugins/place.cpp +++ b/plugins/single_plugins/place.cpp @@ -1,3 +1,4 @@ +#include #include "wayfire/signal-provider.hpp" #include "wayfire/toplevel-view.hpp" #include @@ -70,8 +71,8 @@ void do_place(wayfire_toplevel_view view) { - auto output = view->get_output(); - auto workarea = output->workarea->get_workarea(); + wf::prepare_window_scope(view); + auto workarea = wf::window_scope_workarea(view); std::string mode = placement_mode; if (mode == "cascade") @@ -90,6 +91,7 @@ { center(view, workarea); } + if (wf::window_scope_zone(view)) view->toplevel()->pending().geometry = wf::clamp(view->get_pending_geometry(), workarea); } void adjust_cascade_for_workarea(nonstd::observer_ptr cascade, @@ -174,8 +176,8 @@ wf::pointf_t pos = output->get_cursor_position(); wf::geometry_t window = view->get_pending_geometry(); - window.x = workarea.x + std::clamp(pos.x - window.width / 2, 0.0, workarea.width - window.width); - window.y = workarea.y + std::clamp(pos.y - window.height / 2, 0.0, workarea.height - window.height); + window.x = workarea.x + std::clamp(pos.x - window.width / 2, 0.0, std::max(0.0, workarea.width - window.width)); + window.y = workarea.y + std::clamp(pos.y - window.height / 2, 0.0, std::max(0.0, workarea.height - window.height)); view->toplevel()->pending().geometry.x = window.x; view->toplevel()->pending().geometry.y = window.y; } --- a/plugins/single_plugins/resize.cpp +++ b/plugins/single_plugins/resize.cpp @@ -1,3 +1,4 @@ +#include #include "wayfire/geometry.hpp" #include "wayfire/plugins/common/input-grab.hpp" #include "wayfire/scene-input.hpp" @@ -267,6 +268,7 @@ if (view) { end_wobbly(view); + wf::apply_window_scope(view); wf::view_change_workspace_signal workspace_may_changed; workspace_may_changed.view = this->view; --- a/plugins/window-rules/view-action-interface.cpp +++ b/plugins/window-rules/view-action-interface.cpp @@ -1,3 +1,4 @@ +#include #include "view-action-interface.hpp" #include "wayfire/core.hpp" @@ -554,7 +555,8 @@ move_view_to_output(_view, combined, true); } - // True fullscreen still belongs to the complete logical output. + wf::select_window_scope_monitor(_view, member.name); + // Scope rules govern subsequent maximize and fullscreen requests. if (!_view->pending_fullscreen()) { auto area = wf::geometry_intersection(member.geometry, --- a/metadata/core.xml +++ b/metadata/core.xml @@ -14,6 +14,18 @@ <_long>Space-separated connector names, in left-to-right order. The combined output is named VIRTUAL-1. + + + + + +