diff --git a/metadata/ambient-background.xml b/metadata/ambient-background.xml deleted file mode 100644 index 7655aadd4a42a824b97478ecc0d17743a270b642..0000000000000000000000000000000000000000 --- a/metadata/ambient-background.xml +++ /dev/null @@ -1,78 +0,0 @@ - - - - <_short>Ambient Background - <_long>Continuous Hyperspace or Digital Matrix, with independent desktop fading on each physical monitor. - Desktop - - <_short>Ambient background - - - - - - - - - - - - - - diff --git a/metadata/vswitch.xml b/metadata/vswitch.xml index ab3d5e0f7a714f4f0ca66c2a6fed8ca235ef94f2..d4e29fc6d92b3976e8d8fd31e75524498b947ca8 100644 --- a/metadata/vswitch.xml +++ b/metadata/vswitch.xml @@ -63,7 +63,7 @@ <_short>Window move alpha <_long>Opacity used for the focused window while it is being held or moved between workspaces. 0.65 - 0.0 + 0.1 1.0 0.01 diff --git a/plugins/ambient-background/ambient-background.cpp b/plugins/ambient-background/ambient-background.cpp deleted file mode 100644 index e70b80823d0a1064e9535fc7c0e20a72d56d2747..0000000000000000000000000000000000000000 --- a/plugins/ambient-background/ambient-background.cpp +++ /dev/null @@ -1,452 +0,0 @@ -#include "../cube/ambient-api.hpp" -#include "../cube/hyperspace-background.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -extern "C" { -#include -#include -} - -namespace { -using clock_t = std::chrono::steady_clock; -double now() { return std::chrono::duration(clock_t::now().time_since_epoch()).count(); } -struct zone_t { - std::string name; - wf::geometry_t box; - float opacity = 1, target = 1, black = 0, effect = 1; -}; -class ambient_fade_t : public wf::scene::view_2d_transformer_t { - public: - using view_2d_transformer_t::view_2d_transformer_t; - std::vector> regions; - class instance_t : public wf::scene::transformer_render_instance_t { - public: - using transformer_render_instance_t::transformer_render_instance_t; - void render(const wf::scene::render_instruction_t& data) override { - auto normal = data.damage; - auto texture = get_texture(data.target.scale); - if (!texture) return; - auto box = self->get_bounding_box(); - for (const auto& [region, alpha] : self->regions) { - auto part = normal & region; - normal ^= part; - if (!part.empty() && alpha > 0) - data.pass->add_texture(texture, data.target, box, part, alpha); - } - if (!normal.empty()) data.pass->add_texture(texture, data.target, box, normal); - } - }; - void gen_render_instances(std::vector& list, - wf::scene::damage_callback damage, wf::output_t *output) override { - list.push_back(std::make_unique(this, damage, output)); - } -}; -class ambient_node_t : public wf::scene::node_t { - wf::output_t *output; - std::function paint; - public: - ambient_node_t(wf::output_t *o, decltype(paint) p) : node_t(false), output(o), paint(p) {} - wf::geometry_t get_bounding_box() override { return output->get_layout_geometry(); } - class instance_t : public wf::scene::render_instance_t { - ambient_node_t *self; - public: - explicit instance_t(ambient_node_t *s) : self(s) {} - void schedule_instructions(std::vector& list, - const wf::render_target_t& target, wf::regionf_t& damage) override { - auto clipped = damage & self->get_bounding_box(); - if (clipped.empty()) return; - list.push_back({.instance=this, .target=target, .damage=clipped}); - damage ^= clipped; - } - void render(const wf::scene::render_instruction_t& data) override { - auto box = self->get_bounding_box(); - auto damage = data.damage; - damage += wf::pointf_t{-double(box.x), -double(box.y)}; - auto target = data.target.translated({-box.x, -box.y}); - data.pass->custom_gles_subpass([&] { self->paint(target, damage); }); - } - }; - void gen_render_instances(std::vector& list, - wf::scene::damage_callback, wf::output_t*) override { - list.push_back(std::make_unique(this)); - } -}; -class shield_t : public wf::scene::node_t { - std::function()> regions; - public: - explicit shield_t(decltype(regions) fn) : node_t(false), regions(fn) {} - std::optional find_node_at(const wf::pointf_t& point) override { - for (auto b : regions()) - if (point.x >= b.x && point.y >= b.y && point.x < b.x+b.width && point.y < b.y+b.height) - return wf::scene::input_node_t{this, point}; - return {}; - } -}; -class ambient_plugin_t : public wf::plugin_interface_t { - wf::option_wrapper_t enabled, black_enabled; - wf::option_wrapper_t backend, controller; - wf::option_wrapper_t focused_delay, inactive_delay, fade_seconds, wake_seconds, - min_opacity, manual_seconds; - wf::option_wrapper_t toggle_binding, restore_binding; - wf::shared_data::ref_ptr_t repo; - struct output_state_t { - std::vector zones; - std::unique_ptr hyperspace; - wf::scene::node_ptr node; - }; - std::map outputs; - std::map> fades; - wf::scene::node_ptr shield; - wf::wl_timer timer; - bool manual = false, daemon_started = false, cursor_hidden = false; - unsigned reset_generation = 0; - double last_tick = now(), heartbeat = 0, restore_until = 0, last_launch = 0; - std::weak_ptr previous_focus; - - template void option(wf::option_wrapper_t& wrapper, const char *name, T value) { - auto section = wf::get_core().config->get_section("ambient-background"); - if (!section) { - section = std::make_shared("ambient-background"); - wf::get_core().config->merge_section(section); - } - auto old = section->get_option_or(name); - if (!std::dynamic_pointer_cast>(old)) { - auto typed = std::make_shared>(name, value); - if (old) typed->set_value_str(old->get_value_str()); - section->register_new_option(typed); - } - wrapper.load_option(std::string("ambient-background/") + name); - } - static void insert_before(wf::scene::node_ptr node, wf::scene::layer layer) { - auto root = wf::get_core().scene(); - auto children = root->get_children(); - auto anchor = root->layers[(int)layer]; - auto it = std::find(children.begin(), children.end(), anchor); - children.insert(it, node); - root->set_children_list(children); - } - bool locked() const { - for (auto& [o, s] : outputs) if (o->is_inhibited()) return true; - return false; - } - static std::vector targets(wf::output_t *o) { - if (auto stitch = o->get_data()) - if (!stitch->zones.empty()) return stitch->zones; - return {{o->to_string(), o->get_relative_geometry()}}; - } - void update_zones(wf::output_t *o, output_state_t& state) { - auto old = state.zones; - state.zones.clear(); - for (auto z : targets(o)) { - zone_t zone{z.name, z.geometry}; - for (auto& p : old) if (p.name == zone.name) { - zone.opacity=p.opacity; zone.target=p.target; zone.black=p.black; zone.effect=p.effect; - } - state.zones.push_back(zone); - } - } - void add_output(wf::output_t *o) { - if (outputs.count(o)) return; - auto& state = outputs[o]; - update_zones(o, state); - state.hyperspace = std::make_unique([o] { o->render->damage_whole(); }); - state.hyperspace->set_active(enabled && std::string(backend) == "hyperspace"); - o->add_activator(toggle_binding, &toggle); - o->add_activator(restore_binding, &restore); - if (enabled) attach(o, state); - } - void attach(wf::output_t *o, output_state_t& state) { - if (state.node) return; - state.node = std::make_shared(o, [this,o] (auto& t,auto& d) { paint(o,t,d); }); - insert_before(state.node, wf::scene::layer::BOTTOM); - o->render->damage_whole(); - } - static bool belongs(wayfire_view view, wf::output_t *output, bool wallpaper = false) { - if (!view->is_mapped()) return false; - using layer = wf::scene::layer; - auto layers = wallpaper ? std::vector{layer::BACKGROUND} : - std::vector{layer::BOTTOM,layer::WORKSPACE,layer::TOP,layer::UNMANAGED,layer::OVERLAY,layer::DWIDGET}; - for (wf::scene::node_t *n=view->get_root_node().get(); n; n=n->parent()) - for (auto l:layers) if (n == output->node_for_layer(l).get()) return true; - return false; - } - void clear_fades() { - for (auto& [v,f]:fades) - if (auto alive=f->view.lock()) alive->get_transformed_node()->rem_transformer(f); - fades.clear(); - } - void update_fades() { - if (!enabled) { clear_fades(); return; } - std::map>> desired; - for (auto view:wf::get_core().get_all_views()) { - auto output=view->get_output(); - if (!outputs.count(output) || !belongs(view,output)) continue; - auto box=view->get_bounding_box(), screen=output->get_relative_geometry(); - for (auto& z:outputs.at(output).zones) { - if (z.opacity >= .999f) continue; - // Workspace faces keep their monitor's alpha, including off-screen rows. - int xmin=std::floor(double(box.x-z.box.x-z.box.width)/screen.width)+1; - int xmax=std::floor(double(box.x+box.width-1-z.box.x)/screen.width); - int ymin=std::floor(double(box.y-z.box.y-z.box.height)/screen.height)+1; - int ymax=std::floor(double(box.y+box.height-1-z.box.y)/screen.height); - for (int x=xmin;x<=xmax && xfirst)) { - if (auto alive=it->second->view.lock()) alive->get_transformed_node()->rem_transformer(it->second); - it=fades.erase(it); - } else ++it; - } - for (auto& [view,regions]:desired) { - auto& fade=fades[view]; - if (!fade) { - fade=std::make_shared(view); - view->get_transformed_node()->add_transformer(fade,wf::TRANSFORMER_BLUR+2,"ambient-background-fade"); - } - fade->regions=std::move(regions); - wf::scene::damage_node(fade,fade->get_bounding_box()); - } - } - void update_shield() { - auto cursor=wf::get_core().get_cursor_position(); - bool hide=manual; - for (auto& [o,s]:outputs) { - auto layout=o->get_layout_geometry(); - for (auto& z:s.zones) { - auto b=z.box;b.x+=layout.x;b.y+=layout.y; - if ((b & cursor) && z.opacity<.02f) hide=true; - } - } - hide &= bool(enabled) && !locked(); - if (hide != cursor_hidden) { - if (hide) wf::get_core().hide_cursor(); else wf::get_core().unhide_cursor(); - cursor_hidden=hide; - } - bool needed = manual; - for (auto& [o,s]:outputs) for (auto& z:s.zones) needed |= z.opacity < .02f; - needed &= bool(enabled) && !locked(); - if (needed && !shield) { - shield=std::make_shared([this] { - std::vector result; - for (auto& [o,s]:outputs) { - auto layout=o->get_layout_geometry(); - for (auto& z:s.zones) if (manual || z.opacity < .02f) { - auto b=z.box; b.x+=layout.x; b.y+=layout.y; result.push_back(b); - } - } - return result; - }); - insert_before(shield,wf::scene::layer::SCREENSAVER); - } else if (!needed && shield) { - wf::scene::remove_child(shield); shield.reset(); - } - if (shield) wf::scene::update(shield,wf::scene::update_flag::INPUT_STATE); - } - void set_manual(bool state) { - if (state == manual) return; - if (state && (!enabled || locked())) return; - manual=state; - if (state) { if (auto v=wf::get_core().seat->get_active_view()) previous_focus=v->weak_from_this(); } - else { - ++reset_generation; restore_until=now()+1; - for (auto& [o,s]:outputs) for (auto& z:s.zones) { z.target=1; z.black=0; z.effect=1; } - if (auto view=previous_focus.lock()) if (view->is_mapped()) wf::get_core().seat->focus_view(view->self()); - previous_focus.reset(); - } - update_shield(); - } - wf::activator_callback toggle = [this] (auto) { if (!enabled || locked()) return false; set_manual(!manual); return true; }; - wf::activator_callback restore = [this] (auto) { if (!manual) return false; set_manual(false); return true; }; - wf::signal::connection_t> on_key = [this] (auto ev) { - if (manual && !locked()) ev->mode=wf::input_event_processing_mode_t::NO_CLIENT; - }; - void paint(wf::output_t *o,const wf::render_target_t& target,const wf::regionf_t& damage) { - auto it=outputs.find(o); if (it==outputs.end() || !enabled) return; - auto& state=it->second; - GL_CALL(glDisable(GL_DEPTH_TEST)); GL_CALL(glDisable(GL_CULL_FACE)); - wf::gles::for_each_scissor_rect(target,damage,[&] { - OpenGL::render_rectangle(o->get_relative_geometry(),{0,0,0,1},wf::gles::render_target_orthographic_projection(target)); - }); - if (std::string(backend) == "hyperspace") - state.hyperspace->render(target,damage,wf::dimensions(o->get_relative_geometry())); - else { - // Import only background layer clients: wallpaper and Matrix's own GPU buffers. - for (bool matrix:{false,true}) { - if (matrix) for (auto& z:state.zones) { - float black=manual ? 0 : z.black; - if (black>0) wf::gles::for_each_scissor_rect(target,damage & z.box,[&] { - OpenGL::render_rectangle(z.box,{0,0,0,black},wf::gles::render_target_orthographic_projection(target)); - }); - } - for (auto view:wf::get_core().get_all_views()) { - if (!belongs(view,o,true) || !view->get_wlr_surface()) continue; - bool ambient=view->get_app_id() == "ambient-matrix"; - if (ambient != matrix) continue; - auto texture=wlr_surface_get_texture(view->get_wlr_surface()); - if (!texture) continue; - auto box=view->get_bounding_box(); - wf::gles::for_each_scissor_rect(target,damage & box,[&] { - OpenGL::render_texture(wf::gles_texture_t(texture),target,box); - }); - } - } - } - for (auto& z:state.zones) { - auto d=damage & z.box; - float alpha=manual ? 0 : 1-z.effect; - if (alpha > 0) wf::gles::for_each_scissor_rect(target,d,[&] { - OpenGL::render_rectangle(z.box,{0,0,0,alpha},wf::gles::render_target_orthographic_projection(target)); - }); - } - GL_CALL(glDisable(GL_BLEND)); - } - bool tick() { - double t=now(), dt=std::clamp(t-last_tick,0.0,.1); last_tick=t; - if (locked() && manual) set_manual(false); - bool stale=(heartbeat ? t-heartbeat>3 : daemon_started && t-last_launch>3); - if (stale && manual) set_manual(false); - for (auto& [o,s]:outputs) { - update_zones(o,s); - for (auto& z:s.zones) { - float target = manual ? 0 : (stale || trender->damage_whole(); - } - update_fades(); update_shield(); - if (enabled && stale && !std::string(controller).empty() && t-last_launch>10) { - last_launch=t;wf::get_core().run(std::string(controller)); - } - return true; - } - wf::json_t status() { - wf::json_t j=wf::ipc::json_ok(); - j["enabled"]=bool(enabled);j["backend"]=std::string(backend);j["manual"]=manual; - j["reset_generation"]=int(reset_generation);j["black_enabled"]=bool(black_enabled); - j["focused_delay"]=double(focused_delay);j["inactive_delay"]=double(inactive_delay); - j["fade_seconds"]=double(fade_seconds);j["wake_seconds"]=double(wake_seconds); - j["min_opacity"]=double(min_opacity);j["manual_seconds"]=double(manual_seconds); - j["outputs"]=wf::json_t::array(); - for (auto& [o,s]:outputs) for (auto& z:s.zones) { - wf::json_t item;item["name"]=z.name;item["desktop"]=o->to_string(); - item["opacity"]=double(z.opacity);item["target"]=double(z.target); - j["outputs"].append(item); - } - return j; - } - void changed() { - if (!enabled) { set_manual(false);clear_fades(); } - for (auto& [o,s]:outputs) { - s.hyperspace->set_active(enabled && std::string(backend)=="hyperspace"); - if (enabled) attach(o,s); - else if (s.node) { wf::scene::remove_child(s.node);s.node.reset(); } - o->render->damage_whole(); - } - update_shield(); - wf::ambient_changed_signal signal;wf::get_core().emit(&signal); - if (enabled && !daemon_started && !std::string(controller).empty()) { - daemon_started=true;last_launch=now();wf::get_core().run(std::string(controller)); - } - } - wf::signal::connection_t query = [this] (auto ev) { - ev->enabled=enabled && outputs.count(ev->output); - }; - wf::signal::connection_t render = [this] (auto ev) { - if (enabled && outputs.count(ev->output)) { paint(ev->output,*ev->target,*ev->damage);ev->handled=true; } - }; - wf::signal::connection_t transition = [this] (auto ev) { - if (outputs.count(ev->output)) outputs.at(ev->output).hyperspace->set_transition(ev->moving); - }; - wf::signal::connection_t added = [this] (auto ev) { add_output(ev->output); }; - wf::signal::connection_t removed = [this] (auto ev) { - clear_fades(); - if (outputs.count(ev->output)) { - ev->output->rem_binding(&toggle);ev->output->rem_binding(&restore); - auto& s=outputs.at(ev->output);if(s.node)wf::scene::remove_child(s.node); - outputs.erase(ev->output); - } - }; - public: - void init() override { - option(enabled,"enabled",false);option(black_enabled,"fade_to_black",true); - option(backend,"backend",std::string("hyperspace")); - option(controller,"controller",std::string("ambient-background --daemon")); - option(focused_delay,"focused_delay",240.0);option(inactive_delay,"inactive_delay",120.0); - option(fade_seconds,"fade_duration",20.0);option(wake_seconds,"restore_duration",.25); - option(min_opacity,"minimum_opacity",0.0);option(manual_seconds,"manual_duration",.7); - option(toggle_binding,"toggle",wf::activatorbinding_t{}); - option(restore_binding,"restore",wf::option_type::from_string("KEY_ESC").value()); - for (auto o:wf::get_core().output_layout->get_outputs()) add_output(o); - wf::get_core().output_layout->connect(&added);wf::get_core().output_layout->connect(&removed); - wf::get_core().connect(&query);wf::get_core().connect(&render);wf::get_core().connect(&transition); - wf::get_core().connect(&on_key); - enabled.set_callback([this]{changed();});backend.set_callback([this]{changed();}); - repo->register_method("ambient/status",[this](wf::json_t){return status();}); - repo->register_method("ambient/toggle",[this](wf::json_t){set_manual(!manual);return status();}); - repo->register_method("ambient/restore",[this](wf::json_t){set_manual(false);return status();}); - repo->register_method("ambient/sync",[this](wf::json_t data){ - if (!data.has_member("outputs") || !data["outputs"].is_array())return wf::ipc::json_error("outputs array required"); - for (size_t i=0;i1) - return wf::ipc::json_error("opacity, black and effect must be in [0,1]"); - } - } - for (size_t i=0;irem_binding(&toggle);o->rem_binding(&restore); - if(s.node)wf::scene::remove_child(s.node); - o->render->damage_whole(); - } - outputs.clear(); - for(auto m:{"ambient/status","ambient/toggle","ambient/restore","ambient/sync"})repo->unregister_method(m); - wf::ambient_changed_signal signal;wf::get_core().emit(&signal); - } -}; -} -DECLARE_WAYFIRE_PLUGIN(ambient_plugin_t); diff --git a/plugins/ambient-background/ambient-background.py b/plugins/ambient-background/ambient-background.py deleted file mode 100644 index 58034e2a67bb5648e2e6f0688d794453fc74f490..0000000000000000000000000000000000000000 --- a/plugins/ambient-background/ambient-background.py +++ /dev/null @@ -1,287 +0,0 @@ -#!/usr/bin/env python3 -"""Per-physical-monitor ambient controller; no credentials or desktop capture. - -The compositor owns rendering, bindings and reversible alpha transforms. This -ordinary-user process reads Digital Rain's proven activity/inhibition clocks. -A connection-bound ownership lease leaves those clocks running and restores -legacy saver behavior automatically if this process disappears. -""" -import argparse -import fcntl -import json -import os -from pathlib import Path -import signal -import socket -import struct -import subprocess -import sys -import time -import tomllib - -MAX_MESSAGE = 1024 * 1024 -STOP = False - - -def peer(stream): - pid, uid, _ = struct.unpack('3i', stream.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12)) - if uid != os.geteuid(): - raise PermissionError('Ambient IPC requires the same desktop user') - return pid - - -def connect(path, kind=socket.SOCK_STREAM): - stream = socket.socket(socket.AF_UNIX, kind) - stream.settimeout(2) - try: - stream.connect(str(path)) - peer(stream) - return stream - except BaseException: - stream.close() - raise - - -def read(stream, count): - result = bytearray() - while len(result) < count: - part = stream.recv(count - len(result)) - if not part: - raise ConnectionError('Ambient IPC disconnected') - result.extend(part) - return result - - -def ipc(method, data=None): - with connect(os.environ['WAYFIRE_SOCKET']) as stream: - payload = json.dumps({'method': method, 'data': data or {}}).encode() - stream.sendall(struct.pack('=I', len(payload)) + payload) - count, = struct.unpack('=I', read(stream, 4)) - if not 0 < count <= MAX_MESSAGE: - raise ValueError('Oversized compositor response') - result = json.loads(read(stream, count)) - if isinstance(result, dict) and result.get('error'): - raise RuntimeError(result['error']) - return result - - -def dr_request(stream, method, **data): - stream.send(json.dumps({'version': 1, 'method': method, **data}).encode()) - payload, _, flags, _ = stream.recvmsg(MAX_MESSAGE) - if flags & socket.MSG_TRUNC or not payload: - raise ConnectionError('Invalid Digital Rain response') - result = json.loads(payload) - if result.get('error'): - raise RuntimeError(result['error']) - return result - - -def clamp(value): - return min(1., max(0., value)) - - -def phase(options, output, config): - """Pure timeline: UI fades, backdrop blackens, then the animation fades. - - Legacy black delays begin at idle activation, never at daemon startup. - Zero delays retain DR's meaning of a disabled stage. - """ - if output.get('application_inhibited') or output.get('timed_inhibited'): - return {'opacity': 1., 'black': 0., 'effect': 1.} - delay = options['focused_delay'] if output['focused'] else options['inactive_delay'] - override = config.get('output_timeouts', {}).get(output['name'], 0) - if not output['focused'] and override > 0: - delay = override - age = max(0., output['idle_seconds'] - delay) - fade = max(.05, options['fade_seconds']) - alpha = 1. - clamp(age / fade) * (1. - clamp(options['min_opacity'])) - matrix = config.get('matrix', {}) - black = 0. if matrix.get('TRANSPARENT', True) else 1. - effect = 1. - if options['black_enabled'] and output['idle_seconds'] >= delay: - seconds = max(1, matrix.get('FADE_FRAMES', 20)) / 30. - black_delay = matrix.get('TRANSPARENT_TIMEOUT', 600) - if black_delay > 0: - black = clamp((age - black_delay) / seconds) - rain_delay = matrix.get('BLACK_RAIN_TIMEOUT', 600) - black_start = black_delay + seconds if matrix.get('TRANSPARENT', True) else 0 - if rain_delay > 0 and (not matrix.get('TRANSPARENT', True) or black_delay > 0): - effect = 1. - clamp((age - black_start - rain_delay) / seconds) - return {'opacity': alpha, 'black': black, 'effect': effect} - - -class Matrix: - def __init__(self, name, config, binary): - m = config.get('matrix', {}) - args = [binary, '--render', '--ambient', '--output', name, '--transparent', - '--transparent-timeout', '0', '--black-rain-timeout', '0', '--fade-frames', '1'] - for key in ('FONT_SIZE', 'DEPTH_LAYERS', 'MIN_SPEED', 'MAX_SPEED', 'AVG_SPEED', - 'MIN_TRAIL', 'MAX_TRAIL', 'MIN_DENSITY', 'MAX_DENSITY', 'HEAD_HOLD_FRAMES', - 'HEAD_MUTATION_CHANCE', 'HEAD_WHITE_MIN', 'HEAD_WHITE_MAX', - 'LAYER_NEAR_DENSITY', 'LAYER_FAR_DENSITY'): - if key in m: - args += ['--' + key.lower().replace('_', '-'), str(m[key])] - args += ['--color', config.get('output_colors', {}).get(name, m.get('COLOR', 'green'))] - self.channel, child = socket.socketpair(socket.AF_UNIX, socket.SOCK_SEQPACKET) - self.channel.setblocking(False) - args += ['--report-fd', str(child.fileno())] - try: - self.process = subprocess.Popen(args, pass_fds=(child.fileno(),), - stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - except BaseException: - self.channel.close() - raise - finally: - child.close() - - def alive(self): - # Drain bounded renderer reports so their channel never blocks rendering. - for _ in range(8): - try: - if not self.channel.recv(8192): - break - except BlockingIOError: - break - return self.process.poll() is None - - def close(self): - self.channel.close() # EOF is the renderer's explicit lifetime boundary. - try: - self.process.wait(timeout=1) - except subprocess.TimeoutExpired: - self.process.terminate() - try: - self.process.wait(timeout=1) - except subprocess.TimeoutExpired: - self.process.kill() - self.process.wait(timeout=1) - - -class Controller: - def __init__(self, binary): - self.binary = binary - self.dr = None - self.matrix = {} - self.config = {} - self.config_key = None - self.fingerprint = None - self.generation = None - self.error = None - - def release(self): - for child in self.matrix.values(): - child.close() - self.matrix.clear() - if self.dr: - self.dr.close() - self.dr = None - - def tick(self, options): - if not options['enabled']: - self.release() - return - if not self.dr: - self.dr = connect(Path(os.environ['XDG_RUNTIME_DIR']) / 'digitalrain/control.sock', socket.SOCK_SEQPACKET) - dr_request(self.dr, 'ambient', enabled=True) - generation = options['reset_generation'] - if self.generation is not None and generation != self.generation: - dr_request(self.dr, 'wake') - self.generation = generation - status = dr_request(self.dr, 'status') - path = status.get('config_file', '') - key = (path, status.get('settings_revision')) - if key != self.config_key: - # DR validates edits before changing this revision. Retain the last - # valid settings if an editor is midway through a replacement. - try: - with open(path, 'rb') as handle: - candidate = tomllib.load(handle) - # Scalars come from the manager's validated current settings, - # including runtime API changes and defaults absent from TOML. - for qualified, entry in dr_request(self.dr, 'schema').items(): - group, name = qualified.split('.', 1) - candidate.setdefault(group, {})[name] = entry['value'] - self.config = candidate - self.config_key = key - except (OSError, ValueError): - pass - targets = {item['name'] for item in options['outputs']} - wanted = targets if options['backend'] == 'matrix' else set() - fingerprint = json.dumps([self.config.get('matrix', {}), self.config.get('output_colors', {})], sort_keys=True) - for name in list(self.matrix): - if name not in wanted or fingerprint != self.fingerprint or not self.matrix[name].alive(): - self.matrix.pop(name).close() - for name in sorted(wanted): - if name not in self.matrix: - self.matrix[name] = Matrix(name, self.config, self.binary) - self.fingerprint = fingerprint - activity = {item['name']: item for item in status['outputs']} - updates = [] - for name in sorted(targets): - output = activity.get(name) - state = phase(options, output, self.config) if output and status['lock'] == 'unlocked' else { - 'opacity': 1., 'black': 0., 'effect': 1.} - updates.append({'name': name, **state}) - ipc('ambient/sync', {'outputs': updates}) - self.error = None - - -def main(): - parser = argparse.ArgumentParser(description='Continuous ambient background and per-monitor desktop fading') - parser.add_argument('--daemon', action='store_true', help='run the compositor helper') - parser.add_argument('--toggle', action='store_true', help='toggle ambient-only mode on all monitors') - parser.add_argument('--restore', action='store_true', help='restore desktop elements and keyboard focus') - parser.add_argument('--status', action='store_true') - parser.add_argument('--digitalrain', default='/usr/bin/digitalrain', help=argparse.SUPPRESS) - args = parser.parse_args() - if args.toggle or args.restore or args.status: - print(json.dumps(ipc('ambient/toggle' if args.toggle else 'ambient/restore' if args.restore else 'ambient/status'), indent=2)) - return - if not args.daemon: - parser.print_help() - return - if os.geteuid() == 0: - parser.error('Run the ambient controller as the desktop user') - # One daemon per compositor, including independent test sessions. - runtime = Path(os.environ['XDG_RUNTIME_DIR']) - with connect(os.environ['WAYFIRE_SOCKET']) as stream: - pid = peer(stream) - handle = open(runtime / f'wayfire-ambient-{pid}.lock', 'w') - try: - fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB) - except BlockingIOError: - return - def stop(*_): - global STOP - STOP = True - signal.signal(signal.SIGTERM, stop) - signal.signal(signal.SIGINT, stop) - controller = Controller(args.digitalrain) - try: - while not STOP: - try: - options = ipc('ambient/status') - except (OSError, RuntimeError, ValueError): - break # compositor exit or plugin unload - try: - controller.tick(options) - except (OSError, RuntimeError, ValueError) as error: - controller.release() - message = str(error) - if message != controller.error: - print('Ambient controller:', message, file=sys.stderr, flush=True) - controller.error = message - # Fail open immediately; the plugin also has an independent watchdog. - try: - ipc('ambient/sync', {'outputs': [{'name': z['name'], 'opacity': 1., 'black': 0., 'effect': 1.} - for z in options['outputs']]}) - except (OSError, RuntimeError, ValueError): - break - time.sleep(.25) - finally: - controller.release() - handle.close() - - -if __name__ == '__main__': - main() diff --git a/plugins/cube/ambient-api.hpp b/plugins/cube/ambient-api.hpp deleted file mode 100644 index 17f912921c808245440d000e6d48e66aa3cd43e5..0000000000000000000000000000000000000000 --- a/plugins/cube/ambient-api.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once -#include -#include -namespace wf { -struct ambient_query_signal { wf::output_t *output; bool enabled = false; }; -struct ambient_render_signal { - wf::output_t *output; - const wf::render_target_t *target; - const wf::regionf_t *damage; - bool handled = false; -}; -struct ambient_transition_signal { wf::output_t *output; bool moving; }; -struct ambient_changed_signal {}; -} diff --git a/plugins/cube/cube.cpp b/plugins/cube/cube.cpp index c9b8f77addb1e5d344edbb019a30d5b817ff7b17..5d80a6f9474ecb0a3e296fad3c212ef5dfc292f0 100644 --- a/plugins/cube/cube.cpp +++ b/plugins/cube/cube.cpp @@ -1,4 +1,4 @@ -#include "ambient-api.hpp" +#include "live-wallpaper-api.hpp" #include #include #include @@ -467,13 +467,13 @@ class wayfire_cube : public wf::per_output_plugin_instance_t, public wf::pointer (std::string(background_mode) == "warp" || std::string(background_mode) == "hyperspace"); } - bool ambient_active() const + bool live_wallpaper_active() const { - wf::ambient_query_signal query{output}; + wf::live_wallpaper_query_signal query{output}; wf::get_core().emit(&query); return query.enabled; } - wf::signal::connection_t ambient_changed = [this] (auto) + wf::signal::connection_t live_wallpaper_changed = [this] (auto) { sync_background_animation(); rebuild_cube_render_node(); @@ -482,13 +482,13 @@ class wayfire_cube : public wf::per_output_plugin_instance_t, public wf::pointer bool show_backdrop_image() const { - return !ambient_active() && std::string(background_mode) != "none" && + return !live_wallpaper_active() && std::string(background_mode) != "none" && double(backdrop_image_opacity) > 0 && !procedural_background_active(); } bool procedural_should_animate() const { - if (ambient_active()) return false; + if (live_wallpaper_active()) return false; const auto mode = std::string(background_mode); const bool animate_at_rest = mode == "warp" ? bool(warp_animate_at_rest) : mode == "hyperspace" && bool(hyperspace_animate_at_rest); @@ -506,7 +506,7 @@ class wayfire_cube : public wf::per_output_plugin_instance_t, public wf::pointer void sync_background_animation() { if (!warp_background || !hyperspace_background) return; - wf::ambient_transition_signal transition{output, + wf::live_wallpaper_transition_signal transition{output, !resting_renderer && !input_is_grabbed && !animation.in_exit && animation.cube_animation.running()}; wf::get_core().emit(&transition); const auto mode = std::string(background_mode); @@ -684,7 +684,7 @@ class wayfire_cube : public wf::per_output_plugin_instance_t, public wf::pointer void init() override { - wf::get_core().connect(&ambient_changed); + wf::get_core().connect(&live_wallpaper_changed); // Promote ini values read with older metadata when hot-loading Cube. auto section = wf::get_core().config->get_section("cube"); for (const auto& name : {"mouse_overview", "render_at_rest"}) @@ -1705,10 +1705,10 @@ class wayfire_cube : public wf::per_output_plugin_instance_t, public wf::pointer // Fill the pixels withheld from the wallpaper pass before drawing // the faces. Never fade windows or foreground desktop surfaces. - wf::ambient_render_signal ambient{output, &data.target, &data.damage}; - wf::get_core().emit(&ambient); + wf::live_wallpaper_render_signal wallpaper{output, &data.target, &data.damage}; + wf::get_core().emit(&wallpaper); const bool no_background = std::string(background_mode) == "none"; - const float opacity = ambient.handled ? 0.0f : ((no_background || procedural_background_active()) ? 1.0f : + const float opacity = wallpaper.handled ? 0.0f : ((no_background || procedural_background_active()) ? 1.0f : 1.0f - std::clamp(double(backdrop_image_opacity), 0.0, 1.0)); if (opacity > 0 && std::string(background_mode) == "warp") { diff --git a/plugins/cube/live-wallpaper-api.hpp b/plugins/cube/live-wallpaper-api.hpp new file mode 100644 index 0000000000000000000000000000000000000000..104aa3202aa3c1020a10f1110b89232ab609074f --- /dev/null +++ b/plugins/cube/live-wallpaper-api.hpp @@ -0,0 +1,70 @@ +#pragma once +/* + * Live wallpaper compositor API, version 1. + * + * Every consumer vendors an identical copy of this header. Wayfire dispatches + * signals by std::type_index, so the types must stay out of anonymous + * namespaces and must not change layout without bumping the version. + * All signals are emitted on wf::get_core(). + * + * Coordinates: render targets and damage are output-local logical + * coordinates (0,0 is the output's top-left corner, as in + * output->get_relative_geometry()). Zone names are the physical member names + * from wf::output_stitch_layout_t::zones, or output->to_string() for an + * output that is not stitched. + */ +#include +#include +#include +#include + +#define LIVE_WALLPAPER_API_VERSION 1 + +namespace wf +{ +/* Ask whether live-wallpaper serves this output. The provider sets enabled + * and api_version = LIVE_WALLPAPER_API_VERSION. */ +struct live_wallpaper_query_signal +{ + wf::output_t *output; + bool enabled = false; + int api_version = 0; +}; + +/* Draw the wallpaper (effect, or the static backdrop while paused or + * disabled) into target, limited to damage. Emitted from inside a GLES + * context with target's framebuffer bound (custom_gles_subpass or after + * wf::gles::bind_render_buffer). The handler must draw opaque pixels and + * set handled = true. If nobody handles it, the emitter draws black. Used by + * cube, switcher and the screensaver fade. */ +struct live_wallpaper_render_signal +{ + wf::output_t *output; + const wf::render_target_t *target; + const wf::regionf_t *damage; + bool handled = false; +}; + +/* Cube/switcher motion state for an output (lets the effect adapt). */ +struct live_wallpaper_transition_signal +{ + wf::output_t *output; + bool moving; +}; + +/* live-wallpaper loaded, unloaded or changed effect/settings. Covering + * plugins re-emit their full cover state when they receive it. */ +struct live_wallpaper_changed_signal +{}; + +/* Full cover state of one output, from the screensaver: the zones listed are + * hidden under opaque black (or powered off). live-wallpaper skips them and + * stops frames for an output whose zones are all covered. Emitted on every + * change and again on live_wallpaper_changed_signal; an empty list uncovers + * everything. */ +struct live_wallpaper_cover_signal +{ + wf::output_t *output; + std::vector covered_zones; +}; +} diff --git a/plugins/decor/deco-subsurface.cpp b/plugins/decor/deco-subsurface.cpp index 5e9a75d879d99c20af7e37d2d8b42ebe9d1d453e..32bfa5f9011654298d4567dfe30feb1a12f58d32 100644 --- a/plugins/decor/deco-subsurface.cpp +++ b/plugins/decor/deco-subsurface.cpp @@ -6,6 +6,7 @@ #include "wayfire/signal-provider.hpp" #include "wayfire/toplevel.hpp" #include +#include #define GLM_FORCE_RADIANS #include @@ -100,6 +101,9 @@ class simple_decoration_node_t : public wf::scene::node_t, public wf::pointer_in update_decoration_size(); } + /* Last pointer or touch position, in the view's surface coordinates. */ + wf::pointf_t last_input = {0, 0}; + wf::pointf_t get_offset() { return {(double)-current_thickness, (double)-current_titlebar}; @@ -221,6 +225,7 @@ class simple_decoration_node_t : public wf::scene::node_t, public wf::pointer_in /* wf::compositor_surface_t implementation */ void handle_pointer_enter(wf::pointf_t point) override { + last_input = point; point -= get_offset(); layout.handle_motion(point.x, point.y); } @@ -232,6 +237,7 @@ class simple_decoration_node_t : public wf::scene::node_t, public wf::pointer_in void handle_pointer_motion(wf::pointf_t to, uint32_t) override { + last_input = to; to -= get_offset(); handle_action(layout.handle_motion(to.x, to.y)); } @@ -296,9 +302,16 @@ class simple_decoration_node_t : public wf::scene::node_t, public wf::pointer_in case wf::decor::DECORATION_ACTION_SHOW_MENU: { + // Like a client's request: on the output and core, at the + // pointer in surface coordinates. wf::view_show_window_menu_signal signal; signal.view = view; - signal.relative_position = {0, 0}; + signal.relative_position = {(int)std::floor(last_input.x), (int)std::floor(last_input.y)}; + if (auto output = view->get_output()) + { + output->emit(&signal); + } + wf::get_core().emit(&signal); return; } @@ -323,6 +336,7 @@ class simple_decoration_node_t : public wf::scene::node_t, public wf::pointer_in void handle_touch_motion(uint32_t time_ms, int finger_id, wf::pointf_t position) override { + last_input = position; position -= get_offset(); handle_action(layout.handle_motion(position.x, position.y)); } diff --git a/plugins/ipc-rules/ipc-rules-common.hpp b/plugins/ipc-rules/ipc-rules-common.hpp index 3c30bde0bb4363942ef2473b35d9c3f070d7a1f9..0acada4141ed89b024799e958527a815e933d640 100644 --- a/plugins/ipc-rules/ipc-rules-common.hpp +++ b/plugins/ipc-rules/ipc-rules-common.hpp @@ -215,7 +215,7 @@ static inline wf::json_t view_to_json(wayfire_view view) if (toplevel && output) { description["window-scope"] = wf::window_scope_policy(toplevel); - auto region = wf::window_scope_zone(toplevel); + auto region = wf::window_scope_zone_peek(toplevel); description["physical-monitor"] = region ? region->name : ""; } description["fullscreen"] = toplevel ? toplevel->pending_fullscreen() : false; diff --git a/plugins/ipc-rules/ipc-rules.cpp b/plugins/ipc-rules/ipc-rules.cpp index d6357def3db046852dae7d6f6e9a780a9a7d55d2..c6218e159623675aab89f3d41845590527c973ff 100644 --- a/plugins/ipc-rules/ipc-rules.cpp +++ b/plugins/ipc-rules/ipc-rules.cpp @@ -315,12 +315,23 @@ class ipc_rules_t : public wf::plugin_interface_t, if (member.name == *output_name) { named_output = output; + // Panels may cover a whole member; keep a usable size. target_region = wf::geometry_intersection(member.geometry, output->workarea->get_workarea()); + if ((target_region->width <= 0) || (target_region->height <= 0)) + { + target_region = member.geometry; + } + break; } } } + + if (named_output) + { + break; + } } if (!named_output) diff --git a/plugins/single_plugins/move-snap-map.hpp b/plugins/single_plugins/move-snap-map.hpp index decce89f54ae11b389dc8e3e26f4964d3e617dd7..56acac40d30023b87e5e9321dff86e6d7cbdd03c 100644 --- a/plugins/single_plugins/move-snap-map.hpp +++ b/plugins/single_plugins/move-snap-map.hpp @@ -174,15 +174,19 @@ class move_snap_map_t overlay.reset(); displayed = content; if (render_regions.empty()) return; overlay = std::make_shared(render_regions, render_bounds); - wf::scene::add_front(wf::get_core().scene(), overlay); + // Above windows and panels, below a session lock. + wf::scene::add_front(wf::get_core().scene()->layers[(size_t)wf::scene::layer::OVERLAY], overlay); } wf::signal::connection_t layout_changed = [this] (auto) { if (!owner.empty()) show(snapshot(scope)); }; wf::signal::connection_t config_changed = [this] (auto) { if (!owner.empty()) show(snapshot(scope)); }; + // 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 output_added = + [this] (wf::output_added_signal *ev) { ev->output->connect(&workarea_changed); }; public: void init() { @@ -217,8 +221,12 @@ class move_snap_map_t return reply; }); wf::get_core().output_layout->connect(&layout_changed); + wf::get_core().output_layout->connect(&output_added); wf::get_core().connect(&config_changed); - wf::get_core().connect(&workarea_changed); + for (auto output : wf::get_core().output_layout->get_outputs()) + { + output->connect(&workarea_changed); + } } void fini() { hide(); repository->unregister_method("move/snap-regions"); } }; diff --git a/plugins/single_plugins/oswitch.cpp b/plugins/single_plugins/oswitch.cpp index 376a29cc59b9eef0a650372567d6e38eadbf4478..6c795589eb47f1b840339c51d8017c204299ff41 100644 --- a/plugins/single_plugins/oswitch.cpp +++ b/plugins/single_plugins/oswitch.cpp @@ -23,6 +23,39 @@ class wayfire_oswitch : public wf::plugin_interface_t { wf::wl_idle_call idle_switch_output; + static bool contains(wf::geometry_t g, double x, double y) + { + return x >= g.x && x < g.x + g.width && y >= g.y && y < g.y + g.height; + } + + /** The member under the pointer, else the one holding the focused window. */ + static std::string current_zone(wf::output_t *output, const wf::output_stitch_layout_t& layout) + { + auto pointer = output->get_cursor_position(); + for (const auto& zone : layout.zones) + { + if (contains(zone.geometry, pointer.x, pointer.y)) + { + return zone.name; + } + } + + auto focus = wf::toplevel_cast(wf::get_core().seat->get_active_view()); + if (focus && (focus->get_output() == output)) + { + auto box = focus->get_geometry(); + for (const auto& zone : layout.zones) + { + if (contains(zone.geometry, box.x + box.width / 2.0, box.y + box.height / 2.0)) + { + return zone.name; + } + } + } + + return ""; + } + bool switch_stitched(int direction) { auto output = wf::get_core().seat->get_active_output(); @@ -32,22 +65,24 @@ class wayfire_oswitch : public wf::plugin_interface_t return false; } + // Without a current member, let the ordinary output switch handle it. + auto from = current_zone(output, *layout); + if (from.empty()) + { + return false; + } + // Resolve the target when the binding has finished dispatching. A // physical member is a focus region, not a separate workspace/output. - idle_switch_output.run_once([direction] () + idle_switch_output.run_once([direction, from] () { auto& core = wf::get_core(); auto output = core.seat->get_active_output(); auto layout = output ? output->get_data() : nullptr; if (!layout || layout->zones.size() < 2) return; - auto pointer = output->get_cursor_position(); - auto contains = [] (wf::geometry_t g, double x, double y) - { - return x >= g.x && x < g.x + g.width && y >= g.y && y < g.y + g.height; - }; const wf::output_stitch_zone_t *current = nullptr; for (const auto& zone : layout->zones) - if (contains(zone.geometry, pointer.x, pointer.y)) current = &zone; + if (zone.name == from) current = &zone; if (!current) return; const wf::output_stitch_zone_t *next = nullptr; const wf::output_stitch_zone_t *wrap = current; diff --git a/plugins/single_plugins/output-control.cpp b/plugins/single_plugins/output-control.cpp index 527925673f13412980c691cda126423370a3b684..98f606dbbd3bb655c05458afe164a2be4d22f84c 100644 --- a/plugins/single_plugins/output-control.cpp +++ b/plugins/single_plugins/output-control.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,33 @@ std::string read_file(const std::string& path) return {std::istreambuf_iterator(f), {}}; } +// Escape a value the way wf-config writes it: '#' would start a comment and a +// trailing '\' would join the next line. +std::string escape_value(const std::string& value) +{ + std::string result; + for (char c : value) + { + if (c == '#') result += '\\'; + result += c; + } + if (!result.empty() && result.back() == '\\') result += '\\'; + return result; +} + +// Whether wf-config joins the next line to this one: after removing the +// comment and trailing whitespace, it ends in a '\' that is not escaped. +bool continues(const std::string& line) +{ + size_t end = line.size(); + for (size_t i = 0; i < line.size(); ++i) + { + if (line[i] == '#' && (i == 0 || line[i - 1] != '\\')) { end = i; break; } + } + while (end > 0 && std::isspace((unsigned char)line[end - 1])) --end; + return end > 0 && line[end - 1] == '\\' && (end < 2 || line[end - 2] != '\\'); +} + // Preserve comments, unrelated sections, and options owned by other tools. std::string patch_ini(const std::string& text, values_t pending) { @@ -54,14 +82,25 @@ std::string patch_ini(const std::string& text, values_t pending) auto slash = it->first.find('/'); if (it->first.substr(0, slash) == section) { - result += it->first.substr(slash + 1) + " = " + it->second + "\n"; + result += it->first.substr(slash + 1) + " = " + escape_value(it->second) + "\n"; it = pending.erase(it); } else ++it; } }; std::set seen; + // A continued line belongs to the option above it; drop it when that + // option is being replaced. + bool continued = false, replacing = false; while (std::getline(input, line)) { + if (continued) + { + continued = continues(line); + if (!replacing) result += line + "\n"; + continue; + } + replacing = false; + continued = continues(line); auto stripped = trim(line); if (!stripped.empty() && stripped.front() == '[' && stripped.back() == ']') { @@ -77,8 +116,9 @@ std::string patch_ini(const std::string& text, values_t pending) auto it = pending.find(key); if (it != pending.end()) { - line = trim(line.substr(0, eq)) + " = " + it->second; + line = trim(line.substr(0, eq)) + " = " + escape_value(it->second); pending.erase(it); + replacing = true; } } } @@ -94,8 +134,14 @@ std::string patch_ini(const std::string& text, values_t pending) return result; } -void save_atomic(const std::string& path, const std::string& original, const std::string& text) +void save_atomic(const std::string& link, const std::string& original, const std::string& text) { + // A dotfile manager may link wayfire.ini elsewhere; replace the target so + // the link survives. + std::error_code ec; + auto resolved = std::filesystem::canonical(link, ec); + if (ec) throw std::runtime_error("Cannot resolve configuration path: " + link); + std::string path = resolved.string(); struct stat st; if (lstat(path.c_str(), &st) || !S_ISREG(st.st_mode) || st.st_uid != getuid()) throw std::runtime_error("Configuration must be a regular file owned by this user"); @@ -117,9 +163,14 @@ void save_atomic(const std::string& path, const std::string& original, const std if (read_file(path) != original) throw std::runtime_error("Configuration changed in another application; refresh and try again"); if (rename(tmp.c_str(), path.c_str())) throw std::runtime_error("Cannot replace configuration"); close(fd); + fd = -1; int dir = open(std::filesystem::path(path).parent_path().c_str(), O_DIRECTORY | O_RDONLY | O_CLOEXEC); if (dir >= 0) { fsync(dir); close(dir); } - } catch (...) { close(fd); unlink(tmp.c_str()); throw; } + } catch (...) + { + if (fd >= 0) { close(fd); unlink(tmp.c_str()); } + throw; + } } } diff --git a/plugins/tile/tree-controller.cpp b/plugins/tile/tree-controller.cpp index 9789b61a865ce65ebb76cd08fabd626aee8099a8..8afa8cd45907cb85a3c6ecc9956514c25d097cc3 100644 --- a/plugins/tile/tree-controller.cpp +++ b/plugins/tile/tree-controller.cpp @@ -146,6 +146,11 @@ resize_view_controller_t::resize_view_controller_t(wf::workspace_set_t *wset, wa this->last_point = get_global_input_coordinates(wset->get_attached_output()); this->grabbed_view = view_node_t::get_node(view); this->output = wset->get_attached_output(); + // calculate_resizing_edges() requires the grab point inside the tile. + if (this->grabbed_view && !(this->grabbed_view->geometry & last_point)) + { + this->grabbed_view = nullptr; + } if (this->grabbed_view) { diff --git a/plugins/vswitch/vswitch.cpp b/plugins/vswitch/vswitch.cpp index 0cfd20110ea6db461b24eb922e9e310addbae152..a7771e3926bf9694e190b17ba03ae5ac6bbc7b0f 100644 --- a/plugins/vswitch/vswitch.cpp +++ b/plugins/vswitch/vswitch.cpp @@ -428,8 +428,7 @@ class vswitch : public wf::per_output_plugin_instance_t uint32_t held_move_modifiers = 0; const std::string held_dim_transformer_name = "workspace-move-window-dim"; wf::option_wrapper_t window_move_alpha{"vswitch/window_move_alpha"}; - static constexpr uint32_t held_window_modifiers = - WLR_MODIFIER_CTRL | WLR_MODIFIER_ALT | WLR_MODIFIER_LOGO; + wf::option_wrapper_t with_win_modifier{"vswitch/with_win_modifier"}; // Capabilities which are always required for vswitch, for now wall needs // a custom renderer. @@ -614,7 +613,8 @@ class vswitch : public wf::per_output_plugin_instance_t clear_held_window_dim(); auto transformer = std::make_shared(view); - transformer->alpha = wf::clamp((double)window_move_alpha, 0.0, 1.0); + // Feedback only: never make the window disappear. + transformer->alpha = wf::clamp((double)window_move_alpha, 0.1, 1.0); remove_held_dim_transformers(view); view->get_transformed_node()->add_transformer(transformer, wf::TRANSFORMER_2D, held_dim_transformer_name); @@ -778,7 +778,10 @@ class vswitch : public wf::per_output_plugin_instance_t clear_held_window_target(); } - if ((modifiers & held_window_modifiers) == held_window_modifiers) + // Dim only for the configured hold modifier, and only while vswitch + // could act on it. + if (with_win_modifier.value().has_match(wf::keybinding_t{modifiers, 0}) && + (is_active() || output->can_activate_plugin(&grab_interface))) { show_held_window_dim(get_held_window_target_view()); } else if (held_dim_view) diff --git a/plugins/window-rules/view-action-interface.cpp b/plugins/window-rules/view-action-interface.cpp index 14b717985e4e84c91d6fd52b697dfee85386160f..9f0e56a20f1237587f83134d0971ecc70f121c2f 100644 --- a/plugins/window-rules/view-action-interface.cpp +++ b/plugins/window-rules/view-action-interface.cpp @@ -561,6 +561,11 @@ void view_action_interface_t::_start_on_output(std::string name) { auto area = wf::geometry_intersection(member.geometry, combined->workarea->get_workarea()); + if ((area.width <= 0) || (area.height <= 0)) + { + area = member.geometry; + } + auto geometry = _view->get_pending_geometry(); geometry.width = std::min(geometry.width, area.width); geometry.height = std::min(geometry.height, area.height); diff --git a/plugins/wm-actions/wm-actions.cpp b/plugins/wm-actions/wm-actions.cpp index 60cb56a3e7c3df9dfae49e9cc222c3996187789a..ec0e23e3f3ef52c06b659b19253a0446eb23a30d 100644 --- a/plugins/wm-actions/wm-actions.cpp +++ b/plugins/wm-actions/wm-actions.cpp @@ -458,6 +458,19 @@ class wayfire_wm_actions_output_t : public wf::per_output_plugin_instance_t view->erase_data("wm-actions-floating-manual"); } + // set_keep_above_state() refuses while another plugin is active. + // Never remove windows from the scene along with always_above. + auto remaining = always_above->get_children(); + for (auto& node : remaining) + { + wf::scene::readd_front(output->wset()->get_node(), node); + } + + for (auto view : output->wset()->get_views()) + { + view->erase_data("wm-actions-above"); + } + wf::scene::remove_child(always_above); output->rem_binding(&on_minimize); output->rem_binding(&on_toggle_maximize); diff --git a/src/api/wayfire/render-manager.hpp b/src/api/wayfire/render-manager.hpp index bbcf250411d36cdde8d77e7363e89e4df54c2305..664e808def6c884605054a65dfcdca6e2d383a18 100644 --- a/src/api/wayfire/render-manager.hpp +++ b/src/api/wayfire/render-manager.hpp @@ -77,7 +77,8 @@ class render_manager * such as cube), without modifying live visibility, damage or frame callbacks. * Excludes screensavers, locks, software cursors and final output postprocessing. * Call from the compositor event loop, outside an active output render pass. - * Returns false for inhibited outputs, invalid targets or a failed submit. + * Returns false for inhibited outputs, invalid targets, while any output is + * rendering, or for a failed submit. * No work is performed between calls. The caller owns the target buffer. */ bool render_desktop(const render_target_t& target); diff --git a/src/api/wayfire/window-scope.hpp b/src/api/wayfire/window-scope.hpp index 80d602f15c64dbd5dd43eba02f346ffea726a6ea..7bc952b3e88fb90c922e499f082caa5bc4609b0a 100644 --- a/src/api/wayfire/window-scope.hpp +++ b/src/api/wayfire/window-scope.hpp @@ -9,6 +9,8 @@ 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); +/** The zone window_scope_zone() would pick, without remembering it. For IPC queries. */ +std::optional window_scope_zone_peek(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); diff --git a/src/api/wayfire/workspace-stream.hpp b/src/api/wayfire/workspace-stream.hpp index f8703d64abdc93d989deb78d859bcd5155a0cf3c..2e86873d85e381e03de59e83fb6485d43ba5fc0c 100644 --- a/src/api/wayfire/workspace-stream.hpp +++ b/src/api/wayfire/workspace-stream.hpp @@ -63,8 +63,9 @@ class workspace_stream_node_t : public scene::node_t void gen_render_instances(std::vector& instances, scene::damage_callback push_damage, wf::output_t *output) override; // Cube's fixed background omits TOP surfaces, which have their own - // foreground pass. A non-virtual overload preserves the existing node - // layout and plugin ABI. + // foreground pass. The overload is non-virtual, so it adds no vtable slot; + // the members above changed the node layout, which WAYFIRE_API_ABI_VERSION + // records. void gen_render_instances(std::vector& instances, scene::damage_callback push_damage, wf::output_t *output, bool include_top_layer); // The bounding box of a workspace stream is diff --git a/src/core/output-layout.cpp b/src/core/output-layout.cpp index a211b6e3b512744db077672a4f8b9943523543d1..c964ab1597af60a13632b4c4d861da6ba460eba8 100644 --- a/src/core/output-layout.cpp +++ b/src/core/output-layout.cpp @@ -479,7 +479,7 @@ struct output_layout_output_t std::optional stitch_restore_state; std::vector stitch_zones; /* A stitched member has two independent power owners: plugins through - * output_layout_t::set_output_power() (digitalrain-power), and clients of + * output_layout_t::set_output_power() (the screensaver), and clients of * wlr-output-power-management. Each only changes its own flag; the slice * is off while either asks for it. */ bool stitch_power_off_plugin = false; @@ -1948,8 +1948,8 @@ class output_layout_t::impl lo->stitch_restore_state = configured; lo->stitch_source_box = member.slice; // Power requests are not carried over: like any new connector, the - // member starts powered on (digitalrain-power drops its lease when - // the old wlr_output is destroyed). + // member starts powered on (the screensaver drops its power-off + // when the old wlr_output is destroyed). // Back to full rate before the member starts mirroring. update_stitch_idle_refresh(); @@ -2211,7 +2211,7 @@ class output_layout_t::impl // 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; + std::vector, wf::point_t>> scoped_views; if (stitch_output || wf::option_wrapper_t("core/stitch_enabled")) { for (auto view : wf::get_core().get_all_views()) @@ -2222,7 +2222,7 @@ class output_layout_t::impl 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); + scoped_views.emplace_back(top->weak_from_this(), ws); } } if (!missing_members.empty() && (!stitch_enabled || (stitch_outputs.value() != missing_members_group))) @@ -2323,11 +2323,13 @@ class output_layout_t::impl rescope_views(scoped_views); } - void rescope_views(std::vector>& scoped_views) + // Views may be destroyed while outputs are reconfigured. + void rescope_views(std::vector, wf::point_t>>& scoped_views) { - for (auto& [view, workspace] : scoped_views) + for (auto& [weak, workspace] : scoped_views) { - if (!view->is_mapped() || !view->get_output()) continue; + auto view = weak.lock(); + if (!view || !view->is_mapped() || !view->get_output()) continue; wf::prepare_window_scope(view); if (auto wset = view->get_wset()) { diff --git a/src/core/window-manager.cpp b/src/core/window-manager.cpp index 0c58c8e249dbcdb319239aaa4c11beab17dab365..3d0fc6666c512ea54e0019e1f90fdf1a8fa63709 100644 --- a/src/core/window-manager.cpp +++ b/src/core/window-manager.cpp @@ -270,9 +270,18 @@ void window_manager_t::fullscreen_request(wayfire_toplevel_view view, } 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()); + // A combined desktop and an explicit connector rule keep the window where + // its scope put it. Otherwise honour the output the client asked for. + auto policy = window_scope_policy(view); + auto current = view->get_output(); + if (current && (policy != "physical") && + ((policy != "virtual") || current->get_data())) + { + wo = current; + } + wf::dassert(wo != nullptr, "Fullscreening should not happen with null output!"); + const wf::point_t workspace = ws.value_or(wo->wset()->get_current_workspace()); /* TODO: what happens if the view is moved to the other output, but not * fullscreened? We should make sure that it stays visible there */ diff --git a/src/core/window-scope.cpp b/src/core/window-scope.cpp index 8ddd27deaa5a1a7216b2175c1ab0b357fc75dfc2..c5b42f8ff6c059e14320728b0ffd574ca2c99f7f 100644 --- a/src/core/window-scope.cpp +++ b/src/core/window-scope.cpp @@ -5,7 +5,7 @@ #include #include #include -#include +#include #include #include #include @@ -134,22 +134,40 @@ std::optional window_scope_zone(wayfire_toplevel_view view return {}; } +std::optional window_scope_zone_peek(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(); + auto policy = window_scope_policy(view); + if (policy == "virtual") return {}; + std::string monitor = (data && data->policy == policy) ? data->monitor : ""; + if (policy != "physical") monitor = policy; + else if (view->parent && (!data || data->override_scope.empty())) + { + if (auto zone = window_scope_zone_peek(view->parent)) monitor = zone->name; + } + if (monitor.empty()) monitor = nearest_monitor(view, !view->is_mapped()); + for (auto& zone : layout->zones) if (zone.name == monitor) return zone; + monitor = nearest_monitor(view, !view->is_mapped()); + for (auto& zone : layout->zones) if (zone.name == 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 (auto section = get_core().config->get_section("output:" + zone.name)) { - 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))); @@ -170,15 +188,20 @@ void prepare_window_scope(wayfire_toplevel_view view) { if (view->parent) return; auto policy = window_scope_policy(view); - auto remembered_monitor = view->get_data_safe()->monitor; + auto data = view->get_data_safe(); + auto remembered_monitor = data->monitor; for (auto output : get_core().output_layout->get_outputs()) { auto layout = output->get_data(); bool match = policy == "virtual" ? bool(layout) : policy == output->handle->name; // When a combined desktop is separated, the remembered physical - // region becomes a real output. Keep the window bound to that head. + // region becomes a real output. Move the window to that head once; + // later moves to other outputs must not send it back. if (policy == "physical" && !layout && !remembered_monitor.empty()) + { match = remembered_monitor == output->handle->name; + if (match) data->monitor.clear(); + } if (layout) for (auto& zone : layout->zones) match |= policy == zone.name; if (match) { diff --git a/src/output/promotion-manager.hpp b/src/output/promotion-manager.hpp index 4f6ed2c6b3516d596df31e7c7455e8ea0f657ec8..5306d45aac000d2ff893682ffe29132c18acc311 100644 --- a/src/output/promotion-manager.hpp +++ b/src/output/promotion-manager.hpp @@ -1,6 +1,6 @@ +#pragma once #include #include -#pragma once #include "wayfire/toplevel-view.hpp" #include diff --git a/src/output/render-manager.cpp b/src/output/render-manager.cpp index 5c178db5d416474ae15cc54dbe123f16bc352b09..90790c4013ed478e83a6488ea2370cc9622562b9 100644 --- a/src/output/render-manager.cpp +++ b/src/output/render-manager.cpp @@ -1577,6 +1577,16 @@ bool render_manager::render_desktop(const render_target_t& target) return false; } + // The pass rebinds GL framebuffers, which would corrupt another output's + // frame if called from inside its render pass. + for (auto wo : wf::get_core().output_layout->get_outputs()) + { + if (wo->render->pimpl->current_pass) + { + return false; + } + } + auto root = wf::get_core().scene(); std::vector instances; bool below_screensaver = false; diff --git a/src/output/workspace-stream.cpp b/src/output/workspace-stream.cpp index c2b583b57cfef716c816b3925584ccd06c56532a..5321cf60e3306d637c0d0ba4470e9028ecbdc460 100644 --- a/src/output/workspace-stream.cpp +++ b/src/output/workspace-stream.cpp @@ -5,11 +5,13 @@ #include "wayfire/scene.hpp" #include "wayfire/view.hpp" #include +#include #include #include #include #include #include "fullscreen-regions.hpp" +#include namespace wf { @@ -92,9 +94,9 @@ class workspace_stream_node_t::workspace_stream_instance_t : public scene:: auto view = node_to_view(ch); const bool is_de = (view && (view->role == wf::VIEW_ROLE_DESKTOP_ENVIRONMENT)); - static wf::option_wrapper_t rotate_layer_shell{ - "cube/rotate_layer_shell"}; - const std::string rotate_namespace = rotate_layer_shell; + // Cube's option, which exists only while its metadata is installed. + auto rotate_option = wf::get_core().config->get_option("cube/rotate_layer_shell"); + const std::string rotate_namespace = rotate_option ? rotate_option->get_value_str() : ""; const bool rotate_this_de = is_de && view && !rotate_namespace.empty() && (rotate_namespace == "*" || @@ -156,7 +158,8 @@ class workspace_stream_node_t::workspace_stream_instance_t : public scene:: wf::render_target_t subtarget = target.translated(offset); our_damage += offset; - auto fullscreen = workspace_fullscreen_regions(self->output, self->ws); + bool has_top = std::find(is_top_layer.begin(), is_top_layer.end(), true) != is_top_layer.end(); + auto fullscreen = has_top ? workspace_fullscreen_regions(self->output, self->ws) : wf::regionf_t{}; for (size_t i = 0; i < instances.size(); i++) { if (output_local_instances[i]) @@ -209,7 +212,8 @@ class workspace_stream_node_t::workspace_stream_instance_t : public scene:: void compute_visibility(wf::output_t *output, wf::regionf_t& visible) override { auto offset = get_offset(); - auto fullscreen = workspace_fullscreen_regions(self->output, self->ws); + bool has_top = std::find(is_top_layer.begin(), is_top_layer.end(), true) != is_top_layer.end(); + auto fullscreen = has_top ? workspace_fullscreen_regions(self->output, self->ws) : wf::regionf_t{}; for (size_t i = 0; i < instances.size(); ++i) { if (!output_local_instances[i]) visible += offset; diff --git a/src/view/layer-shell/layer-shell.cpp b/src/view/layer-shell/layer-shell.cpp index 6655a29e683b58ce549af5461240fd366301d668..f1114bb1ead4449e6e94bfb4bb13cbd45ab546a9 100644 --- a/src/view/layer-shell/layer-shell.cpp +++ b/src/view/layer-shell/layer-shell.cpp @@ -476,12 +476,6 @@ void wayfire_layer_shell_view::handle_destroy() wf::scene::layer wayfire_layer_shell_view::get_layer() { - /* Keep the OLED screensaver above every normal overlay. */ - if (std::string{nonull(lsurface->namespace_t)} == "screensaver") - { - return wf::scene::layer::SCREENSAVER; - } - static const std::vector desktop_widget_ids = { "keyboard", "de-widget", diff --git a/src/view/xdg-shell/xdg-toplevel-view.cpp b/src/view/xdg-shell/xdg-toplevel-view.cpp index d1999777dfdd3f75c2d1fc5b3a4961df770a3299..3bb155c39f548f12a21f42cfb4b9256fec2d406d 100644 --- a/src/view/xdg-shell/xdg-toplevel-view.cpp +++ b/src/view/xdg-shell/xdg-toplevel-view.cpp @@ -110,9 +110,13 @@ void wf::xdg_toplevel_view_base_t::handle_title_changed(std::string new_title) void wf::xdg_toplevel_view_base_t::handle_app_id_changed(std::string new_app_id) { + // Only a rule match that changes the window's scope moves it; anything else + // would fight an interactive move or resize in progress. + auto top = wf::toplevel_cast(self()); + auto old_policy = (top && top->is_mapped()) ? wf::window_scope_policy(top) : std::string{}; 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); + if (top && top->is_mapped() && (wf::window_scope_policy(top) != old_policy)) wf::apply_window_scope(top); } void wf::xdg_toplevel_view_base_t::close()