From 1a58beafab5ff1bb7effb2d6d3546102327ab687 Mon Sep 17 00:00:00 2001 From: trigg Date: Mon, 9 Mar 2026 15:25:55 +0000 Subject: [PATCH 01/12] xdpw: added a screencast chooser --- data/meson.build | 2 + data/xdpw/wayfire | 3 + proto/ext-foreign-toplevel-list-v1.xml | 219 ++++++++++++++++++++++++ proto/meson.build | 7 +- src/meson.build | 1 + src/stream-chooser/meson.build | 13 ++ src/stream-chooser/outputwidget.cpp | 29 ++++ src/stream-chooser/outputwidget.hpp | 15 ++ src/stream-chooser/stream-chooser.cpp | 228 +++++++++++++++++++++++++ src/stream-chooser/stream-chooser.hpp | 49 ++++++ src/stream-chooser/toplevelwidget.cpp | 115 +++++++++++++ src/stream-chooser/toplevelwidget.hpp | 26 +++ 12 files changed, 704 insertions(+), 3 deletions(-) create mode 100644 data/xdpw/wayfire create mode 100644 proto/ext-foreign-toplevel-list-v1.xml create mode 100644 src/stream-chooser/meson.build create mode 100644 src/stream-chooser/outputwidget.cpp create mode 100644 src/stream-chooser/outputwidget.hpp create mode 100644 src/stream-chooser/stream-chooser.cpp create mode 100644 src/stream-chooser/stream-chooser.hpp create mode 100644 src/stream-chooser/toplevelwidget.cpp create mode 100644 src/stream-chooser/toplevelwidget.hpp diff --git a/data/meson.build b/data/meson.build index e35455ef..bc26276a 100644 --- a/data/meson.build +++ b/data/meson.build @@ -15,4 +15,6 @@ install_data(join_paths('icons', 'scalable', 'wayfire.svg'), install_dir: join_p install_data('wf-locker-password', install_dir:'/etc/pam.d/') +install_data('xdpw/wayfire', install_dir: '/etc/xdg/xdg-desktop-portal-wlr/') + subdir('css') \ No newline at end of file diff --git a/data/xdpw/wayfire b/data/xdpw/wayfire new file mode 100644 index 00000000..8b0ac616 --- /dev/null +++ b/data/xdpw/wayfire @@ -0,0 +1,3 @@ +[screencast] +chooser_cmd=wf-stream-chooser +chooser_type=simple \ No newline at end of file diff --git a/proto/ext-foreign-toplevel-list-v1.xml b/proto/ext-foreign-toplevel-list-v1.xml new file mode 100644 index 00000000..11b0113a --- /dev/null +++ b/proto/ext-foreign-toplevel-list-v1.xml @@ -0,0 +1,219 @@ + + + + Copyright © 2018 Ilia Bozhinov + Copyright © 2020 Isaac Freund + Copyright © 2022 wb9688 + Copyright © 2023 i509VCB + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + The purpose of this protocol is to provide protocol object handles for + toplevels, possibly originating from another client. + + This protocol is intentionally minimalistic and expects additional + functionality (e.g. creating a screencopy source from a toplevel handle, + getting information about the state of the toplevel) to be implemented + in extension protocols. + + The compositor may choose to restrict this protocol to a special client + launched by the compositor itself or expose it to all clients, + this is compositor policy. + + The key words "must", "must not", "required", "shall", "shall not", + "should", "should not", "recommended", "may", and "optional" in this + document are to be interpreted as described in IETF RFC 2119. + + Warning! The protocol described in this file is currently in the testing + phase. Backward compatible changes may be added together with the + corresponding interface version bump. Backward incompatible changes can + only be done by creating a new major version of the extension. + + + + + A toplevel is defined as a surface with a role similar to xdg_toplevel. + XWayland surfaces may be treated like toplevels in this protocol. + + After a client binds the ext_foreign_toplevel_list_v1, each mapped + toplevel window will be sent using the ext_foreign_toplevel_list_v1.toplevel + event. + + Clients which only care about the current state can perform a roundtrip after + binding this global. + + For each instance of ext_foreign_toplevel_list_v1, the compositor must + create a new ext_foreign_toplevel_handle_v1 object for each mapped toplevel. + + If a compositor implementation sends the ext_foreign_toplevel_list_v1.finished + event after the global is bound, the compositor must not send any + ext_foreign_toplevel_list_v1.toplevel events. + + + + + This event is emitted whenever a new toplevel window is created. It is + emitted for all toplevels, regardless of the app that has created them. + + All initial properties of the toplevel (identifier, title, app_id) will be sent + immediately after this event using the corresponding events for + ext_foreign_toplevel_handle_v1. The compositor will use the + ext_foreign_toplevel_handle_v1.done event to indicate when all data has + been sent. + + + + + + + This event indicates that the compositor is done sending events + to this object. The client should destroy the object. + See ext_foreign_toplevel_list_v1.destroy for more information. + + The compositor must not send any more toplevel events after this event. + + + + + + This request indicates that the client no longer wishes to receive + events for new toplevels. + + The Wayland protocol is asynchronous, meaning the compositor may send + further toplevel events until the stop request is processed. + The client should wait for a ext_foreign_toplevel_list_v1.finished + event before destroying this object. + + + + + + This request should be called either when the client will no longer + use the ext_foreign_toplevel_list_v1 or after the finished event + has been received to allow destruction of the object. + + If a client wishes to destroy this object it should send a + ext_foreign_toplevel_list_v1.stop request and wait for a ext_foreign_toplevel_list_v1.finished + event, then destroy the handles and then this object. + + + + + + + A ext_foreign_toplevel_handle_v1 object represents a mapped toplevel + window. A single app may have multiple mapped toplevels. + + + + + This request should be used when the client will no longer use the handle + or after the closed event has been received to allow destruction of the + object. + + When a handle is destroyed, a new handle may not be created by the server + until the toplevel is unmapped and then remapped. Destroying a toplevel handle + is not recommended unless the client is cleaning up child objects + before destroying the ext_foreign_toplevel_list_v1 object, the toplevel + was closed or the toplevel handle will not be used in the future. + + Other protocols which extend the ext_foreign_toplevel_handle_v1 + interface should require destructors for extension interfaces be + called before allowing the toplevel handle to be destroyed. + + + + + + The server will emit no further events on the ext_foreign_toplevel_handle_v1 + after this event. Any requests received aside from the destroy request must + be ignored. Upon receiving this event, the client should destroy the handle. + + Other protocols which extend the ext_foreign_toplevel_handle_v1 + interface must also ignore requests other than destructors. + + + + + + This event is sent after all changes in the toplevel state have + been sent. + + This allows changes to the ext_foreign_toplevel_handle_v1 properties + to be atomically applied. Other protocols which extend the + ext_foreign_toplevel_handle_v1 interface may use this event to also + atomically apply any pending state. + + This event must not be sent after the ext_foreign_toplevel_handle_v1.closed + event. + + + + + + The title of the toplevel has changed. + + The configured state must not be applied immediately. See + ext_foreign_toplevel_handle_v1.done for details. + + + + + + + The app id of the toplevel has changed. + + The configured state must not be applied immediately. See + ext_foreign_toplevel_handle_v1.done for details. + + + + + + + This identifier is used to check if two or more toplevel handles belong + to the same toplevel. + + The identifier is useful for command line tools or privileged clients + which may need to reference an exact toplevel across processes or + instances of the ext_foreign_toplevel_list_v1 global. + + The compositor must only send this event when the handle is created. + + The identifier must be unique per toplevel and it's handles. Two different + toplevels must not have the same identifier. The identifier is only valid + as long as the toplevel is mapped. If the toplevel is unmapped the identifier + must not be reused. An identifier must not be reused by the compositor to + ensure there are no races when sharing identifiers between processes. + + An identifier is a string that contains up to 32 printable ASCII bytes. + An identifier must not be an empty string. It is recommended that a + compositor includes an opaque generation value in identifiers. How the + generation value is used when generating the identifier is implementation + dependent. + + + + + diff --git a/proto/meson.build b/proto/meson.build index 831308a0..ebcaebfa 100644 --- a/proto/meson.build +++ b/proto/meson.build @@ -15,9 +15,10 @@ wayland_scanner_client = generator( ) client_protocols = [ - 'wlr-foreign-toplevel-management-unstable-v1.xml', - 'wlr-screencopy.xml', - wayfire.get_pkgconfig_variable('pkgdatadir') / 'unstable' / 'wayfire-shell-unstable-v2.xml', + 'ext-foreign-toplevel-list-v1.xml', + 'wlr-foreign-toplevel-management-unstable-v1.xml', + 'wlr-screencopy.xml', + wayfire.get_pkgconfig_variable('pkgdatadir') / 'unstable' / 'wayfire-shell-unstable-v2.xml', ] if gbm.found() and drm.found() and not get_option('live-previews-dmabuf').disabled() diff --git a/src/meson.build b/src/meson.build index a2fe8f19..78774b7c 100644 --- a/src/meson.build +++ b/src/meson.build @@ -4,6 +4,7 @@ subdir('background') subdir('dock') subdir('locker') subdir('locker-pin') +subdir('stream-chooser') pkgconfig = import('pkgconfig') pkgconfig.generate( diff --git a/src/stream-chooser/meson.build b/src/stream-chooser/meson.build new file mode 100644 index 00000000..90343525 --- /dev/null +++ b/src/stream-chooser/meson.build @@ -0,0 +1,13 @@ +deps = [ + gtkmm, + wf_protos, + libutil, + gtklayershell, +] + +executable( + 'wf-stream-chooser', + ['stream-chooser.cpp', 'toplevelwidget.cpp', 'outputwidget.cpp'], + dependencies: deps, + install: true, +) \ No newline at end of file diff --git a/src/stream-chooser/outputwidget.cpp b/src/stream-chooser/outputwidget.cpp new file mode 100644 index 00000000..6220b28d --- /dev/null +++ b/src/stream-chooser/outputwidget.cpp @@ -0,0 +1,29 @@ +#include + +#include "outputwidget.hpp" +#include "stream-chooser.hpp" + +WayfireChooserOutput::WayfireChooserOutput(std::shared_ptr output) : output(output) +{ + append(contents); + append(model); + append(connector); + + /* TODO Contents. We should probably grab screenshots of each output and display them */ + + model.set_label(output->get_model()); + connector.set_label(output->get_connector()); + + set_orientation(Gtk::Orientation::VERTICAL); + + output->signal_invalidate().connect([=] + { + WayfireStreamChooserApp::getInstance().remove_output(output->get_connector()); + }); +} + +void WayfireChooserOutput::print() +{ + std::cout << "Monitor: " << output->get_connector() << std::endl; + exit(0); +} diff --git a/src/stream-chooser/outputwidget.hpp b/src/stream-chooser/outputwidget.hpp new file mode 100644 index 00000000..e96b6386 --- /dev/null +++ b/src/stream-chooser/outputwidget.hpp @@ -0,0 +1,15 @@ +#pragma once +#include +#include + +class WayfireChooserOutput : public Gtk::Box +{ + Gtk::Label connector, model; + Gtk::Image contents; + + std::shared_ptr output; + + public: + void print(); + WayfireChooserOutput(std::shared_ptr output); +}; diff --git a/src/stream-chooser/stream-chooser.cpp b/src/stream-chooser/stream-chooser.cpp new file mode 100644 index 00000000..a0a1965c --- /dev/null +++ b/src/stream-chooser/stream-chooser.cpp @@ -0,0 +1,228 @@ +#include +#include +#include +#include + +#include "stream-chooser.hpp" +#include "outputwidget.hpp" +#include "toplevelwidget.hpp" + +#define EXT_IMAGE_COPY_CAPTURE_V1 "ext-image-copy-capture-v1" + +/* Static callbacks for toplevel list object */ +static void handle_toplevel(void *data, + struct ext_foreign_toplevel_list_v1 *list, + struct ext_foreign_toplevel_handle_v1 *toplevel) +{ + WayfireStreamChooserApp::getInstance().add_toplevel(toplevel); +} + +static void handle_finished(void *data, + struct ext_foreign_toplevel_list_v1 *list) +{ + ext_foreign_toplevel_list_v1_stop(list); + ext_foreign_toplevel_list_v1_destroy(list); +} + +ext_foreign_toplevel_list_v1_listener toplevel_list_v1_impl = { + .toplevel = handle_toplevel, + .finished = handle_finished, +}; + +/* Static callbacks for wayland registry */ +static void registry_add_object(void *data, wl_registry *registry, uint32_t name, + const char *interface, uint32_t version) +{ + if (strcmp(interface, ext_foreign_toplevel_list_v1_interface.name) == 0) + { + auto list = (ext_foreign_toplevel_list_v1*) + wl_registry_bind(registry, name, + &ext_foreign_toplevel_list_v1_interface, + version); + + WayfireStreamChooserApp::getInstance().set_toplevel_list(list); + ext_foreign_toplevel_list_v1_add_listener(list, + &toplevel_list_v1_impl, NULL); + } else if (strcmp(interface, EXT_IMAGE_COPY_CAPTURE_V1) == 0) + { + /* We need this to exist, but we're not using it directly */ + WayfireStreamChooserApp::getInstance().has_image_copy_capture = true; + } +} + +static void registry_remove_object(void *data, struct wl_registry *registry, uint32_t name) +{} + +static struct wl_registry_listener registry_listener = +{ + ®istry_add_object, + ®istry_remove_object +}; + +WayfireStreamChooserApp::WayfireStreamChooserApp() : Gtk::Application("org.wayfire.screen-chooser", + Gio::Application::Flags::NONE) +{ + signal_activate().connect(sigc::mem_fun(*this, &WayfireStreamChooserApp::activate)); +} + +void WayfireStreamChooserApp::activate() +{ + window.set_size_request(300, 300); + add_window(window); + window.set_child(main); + main.append(header); + main.append(notebook); + main.append(buttons); + + notebook.set_expand(true); + notebook.append_page(window_list, window_label); + notebook.append_page(screen_list, screen_label); + + main.set_orientation(Gtk::Orientation::VERTICAL); + + buttons.set_hexpand(true); + buttons.append(cancel); + cancel.set_halign(Gtk::Align::START); + buttons.append(done); + done.set_halign(Gtk::Align::END); + + window_label.set_label("Window"); + screen_label.set_label("Screen"); + header.set_label("Choose a view to share"); + + cancel.set_label("Cancel"); + done.set_label("Done"); + buttons.set_homogeneous(true); + + done.signal_clicked().connect([this] () + { + if (notebook.get_current_page() == 0) + { + auto children = window_list.get_selected_children(); + if (children.size() == 1) + { + WayfireChooserTopLevel *cast_child = (WayfireChooserTopLevel*)(children[0]->get_child()); + cast_child->print(); + } + + /* TODO Consider an error to let user know the selection was invalid */ + exit(0); + } else + { + auto children = screen_list.get_selected_children(); + if (children.size() == 1) + { + WayfireChooserOutput *cast_child = (WayfireChooserOutput*)(children[0]->get_child()); + cast_child->print(); + } + + /* TODO Consider an error to let user know the selection was invalid */ + exit(0); + } + }); + + cancel.signal_clicked().connect([] () + { + exit(0); + }); + + /* Attempt to get Window list */ + auto gdk_display = gdk_display_get_default(); + auto display = gdk_wayland_display_get_wl_display(gdk_display); + + this->display = display; + + wl_registry *registry = wl_display_get_registry(display); + wl_registry_add_listener(registry, ®istry_listener, this); + this->registry = registry; + wl_display_roundtrip(display); + + if (this->list && has_image_copy_capture) + {} else + { + std::cerr << "Compositor doesn't support" << + " ext-foreign-toplevel-list and/or ext-image-copy-capture-v1." << + " Only screens can be cast currently." << std::endl; + window_label.set_sensitive(false); + window_label.set_tooltip_text("This compositor does not currently support sharing individual windows"); + notebook.set_current_page(1); + } + + /* Get output list */ + auto gtkdisplay = Gdk::Display::get_default(); + auto monitors = gtkdisplay->get_monitors(); + monitors->signal_items_changed().connect( + [this] (const int pos, const int rem, const int add) + { + auto display = Gdk::Display::get_default(); + auto monitors = display->get_monitors(); + int num_monitors = monitors->get_n_items(); + for (int i = 0; i < num_monitors; i++) + { + auto obj = std::dynamic_pointer_cast(monitors->get_object(i)); + add_output(obj); + } + }); + + // Initial monitors + int num_monitors = monitors->get_n_items(); + for (int i = 0; i < num_monitors; i++) + { + auto obj = std::dynamic_pointer_cast(monitors->get_object(i)); + add_output(obj); + } + + gtk_layer_init_for_window(window.gobj()); + gtk_layer_set_namespace(window.gobj(), "chooser"); + gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_TOP, true); + gtk_layer_set_keyboard_mode(window.gobj(), GTK_LAYER_SHELL_KEYBOARD_MODE_ON_DEMAND); + gtk_layer_set_exclusive_zone(window.gobj(), 0); + window.present(); +} + +void WayfireStreamChooserApp::add_toplevel(ext_foreign_toplevel_handle_v1 *handle) +{ + toplevels.emplace(handle, new WayfireChooserTopLevel(handle)); + window_list.append(*toplevels[handle]); + if (window_list.get_selected_children().size() == 0) + { + auto child = window_list.get_child_at_index(0); + window_list.select_child(*child); + } +} + +void WayfireStreamChooserApp::remove_toplevel(WayfireChooserTopLevel *toplevel) +{ + window_list.remove(*toplevel); + toplevels.erase(toplevel->handle); +} + +void WayfireStreamChooserApp::add_output(std::shared_ptr monitor) +{ + std::string connector = monitor->get_connector(); + outputs.emplace(connector, new WayfireChooserOutput(monitor)); + screen_list.append(*outputs[connector]); + if (screen_list.get_selected_children().size() == 0) + { + auto child = screen_list.get_child_at_index(0); + screen_list.select_child(*child); + } +} + +void WayfireStreamChooserApp::remove_output(std::string connector) +{ + screen_list.remove(*outputs[connector]); + outputs.erase(connector); +} + +void WayfireStreamChooserApp::set_toplevel_list(ext_foreign_toplevel_list_v1 *list) +{ + this->list = list; +} + +/* Starting point */ +int main(int argc, char **argv) +{ + WayfireStreamChooserApp::getInstance().run(); + exit(0); +} diff --git a/src/stream-chooser/stream-chooser.hpp b/src/stream-chooser/stream-chooser.hpp new file mode 100644 index 00000000..6c400e0c --- /dev/null +++ b/src/stream-chooser/stream-chooser.hpp @@ -0,0 +1,49 @@ +#pragma once +#include +#include +#include + +#include "outputwidget.hpp" +#include "toplevelwidget.hpp" + + +class WayfireStreamChooserApp : public Gtk::Application +{ + private: + Gtk::Window window; + Gtk::Notebook notebook; + + Gtk::Box main, buttons; + + Gtk::Label window_label, screen_label, header; + + Gtk::FlowBox window_list, screen_list; + Gtk::Button done, cancel; + WayfireStreamChooserApp(); + + wl_display *display; + wl_registry *registry; + ext_foreign_toplevel_list_v1 *list; + + public: + bool has_image_copy_capture = false; + + std::map> toplevels; + std::map> outputs; + + static WayfireStreamChooserApp& getInstance() + { + static WayfireStreamChooserApp instance; + return instance; + } + + void set_toplevel_list(ext_foreign_toplevel_list_v1 *list); + void add_toplevel(ext_foreign_toplevel_handle_v1 *handle); + void remove_toplevel(WayfireChooserTopLevel *widget); + + void add_output(std::shared_ptr monitor); + void remove_output(std::string connector); + void activate(); + WayfireStreamChooserApp(WayfireStreamChooserApp const&) = delete; + void operator =(WayfireStreamChooserApp const&) = delete; +}; diff --git a/src/stream-chooser/toplevelwidget.cpp b/src/stream-chooser/toplevelwidget.cpp new file mode 100644 index 00000000..26dc182d --- /dev/null +++ b/src/stream-chooser/toplevelwidget.cpp @@ -0,0 +1,115 @@ +#include + +#include "stream-chooser.hpp" +#include "toplevelwidget.hpp" + +static void handle_closed(void *data, + struct ext_foreign_toplevel_handle_v1 *handle) +{ + WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; + WayfireStreamChooserApp::getInstance().remove_toplevel(toplevel); + + /* TODO Clean up */ +} + +static void handle_done(void *data, + struct ext_foreign_toplevel_handle_v1 *handle) +{ + WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; + toplevel->commit(); +} + +static void handle_title(void *data, + struct ext_foreign_toplevel_handle_v1 *ext_foreign_toplevel_handle_v1, + const char *title) +{ + WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; + toplevel->set_title(title); +} + +static void handle_app_id(void *data, + struct ext_foreign_toplevel_handle_v1 *handle1, + const char *app_id) +{ + WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; + toplevel->set_app_id(app_id); +} + +static void handle_identifier(void *data, + struct ext_foreign_toplevel_handle_v1 *ext_foreign_toplevel_handle_v1, + const char *identifier) +{ + WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; + toplevel->set_identifier(identifier); +} + +ext_foreign_toplevel_handle_v1_listener listener = +{ + .closed = handle_closed, + .done = handle_done, + .title = handle_title, + .app_id = handle_app_id, + .identifier = handle_identifier, +}; + +/* Gtk Overlay showing information about a window */ +WayfireChooserTopLevel::WayfireChooserTopLevel(ext_foreign_toplevel_handle_v1 *handle) +{ + append(overlay); + append(label); + overlay.set_child(screenshot); + overlay.add_overlay(icon); + icon.set_halign(Gtk::Align::START); + icon.set_valign(Gtk::Align::END); + label.set_ellipsize(Pango::EllipsizeMode::MIDDLE); + label.set_max_width_chars(40); + + ext_foreign_toplevel_handle_v1_add_listener(handle, &listener, this); +} + +void WayfireChooserTopLevel::set_title(std::string title) +{ + buffered_title = title; +} + +void WayfireChooserTopLevel::set_app_id(std::string app_id) +{ + buffered_app_id = app_id; +} + +void WayfireChooserTopLevel::set_identifier(std::string identifier) +{ + buffered_identifier = identifier; +} + +void WayfireChooserTopLevel::commit() +{ + if (buffered_app_id != "") + { + app_id = buffered_app_id; + icon.set_from_icon_name(app_id); + buffered_app_id = ""; + } + + if (buffered_title != "") + { + title = buffered_title; + label.set_label(title); + buffered_title = ""; + } + + if (buffered_identifier != "") + { + identifier = buffered_identifier; + buffered_identifier = ""; + } +} + +WayfireChooserTopLevel::~WayfireChooserTopLevel() +{} + +void WayfireChooserTopLevel::print() +{ + std::cout << "Window: " << identifier << std::endl; + exit(0); +} diff --git a/src/stream-chooser/toplevelwidget.hpp b/src/stream-chooser/toplevelwidget.hpp new file mode 100644 index 00000000..ef0bd1b3 --- /dev/null +++ b/src/stream-chooser/toplevelwidget.hpp @@ -0,0 +1,26 @@ +#pragma once +#include +#include "ext-foreign-toplevel-list-v1-client-protocol.h" + +class WayfireChooserTopLevel : public Gtk::Box +{ + private: + Gtk::Overlay overlay; + Gtk::Image icon; + Gtk::Image screenshot; + Gtk::Label label; + + std::string buffered_title = "", title = ""; + std::string buffered_app_id = "", app_id = ""; + std::string buffered_identifier = "", identifier = ""; + + public: + ext_foreign_toplevel_handle_v1 *handle; + WayfireChooserTopLevel(ext_foreign_toplevel_handle_v1 *handle); + ~WayfireChooserTopLevel(); + void commit(); + void set_app_id(std::string app_id); + void set_title(std::string title); + void set_identifier(std::string identifier); + void print(); +}; From e1f5e0bc9b0d2960f25990fb6d18a4062ae1d45d Mon Sep 17 00:00:00 2001 From: trigg Date: Sun, 26 Apr 2026 11:37:00 +0100 Subject: [PATCH 02/12] xdpw: theme chooser to match panel logout --- src/stream-chooser/stream-chooser.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/stream-chooser/stream-chooser.cpp b/src/stream-chooser/stream-chooser.cpp index a0a1965c..e202634f 100644 --- a/src/stream-chooser/stream-chooser.cpp +++ b/src/stream-chooser/stream-chooser.cpp @@ -4,6 +4,7 @@ #include #include "stream-chooser.hpp" +#include "gtkmm/enums.h" #include "outputwidget.hpp" #include "toplevelwidget.hpp" @@ -67,12 +68,22 @@ WayfireStreamChooserApp::WayfireStreamChooserApp() : Gtk::Application("org.wayfi void WayfireStreamChooserApp::activate() { - window.set_size_request(300, 300); + window.add_css_class("stream-chooser"); + main.set_size_request(300, 300); add_window(window); window.set_child(main); + main.set_valign(Gtk::Align::CENTER); + main.set_halign(Gtk::Align::CENTER); + main.set_vexpand(false); + main.set_hexpand(false); main.append(header); main.append(notebook); main.append(buttons); + auto window_display = window.get_display(); + auto css_provider = Gtk::CssProvider::create(); + css_provider->load_from_data("window.stream-chooser { background-color: rgba(0, 0, 0, 0.5); }"); + Gtk::StyleContext::add_provider_for_display(window_display, + css_provider, GTK_STYLE_PROVIDER_PRIORITY_USER); notebook.set_expand(true); notebook.append_page(window_list, window_label); @@ -175,7 +186,11 @@ void WayfireStreamChooserApp::activate() gtk_layer_init_for_window(window.gobj()); gtk_layer_set_namespace(window.gobj(), "chooser"); gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_TOP, true); - gtk_layer_set_keyboard_mode(window.gobj(), GTK_LAYER_SHELL_KEYBOARD_MODE_ON_DEMAND); + gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_BOTTOM, true); + gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_LEFT, true); + gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_RIGHT, true); + + gtk_layer_set_keyboard_mode(window.gobj(), GTK_LAYER_SHELL_KEYBOARD_MODE_EXCLUSIVE); gtk_layer_set_exclusive_zone(window.gobj(), 0); window.present(); } From 399624deb9fa26ea6e06892a35110e65f2fd8189 Mon Sep 17 00:00:00 2001 From: trigg Date: Sun, 26 Apr 2026 19:54:28 +0100 Subject: [PATCH 03/12] xdpw: more specific error messages --- src/stream-chooser/stream-chooser.cpp | 21 +++++++++++++++------ src/stream-chooser/stream-chooser.hpp | 3 ++- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/stream-chooser/stream-chooser.cpp b/src/stream-chooser/stream-chooser.cpp index e202634f..996294b1 100644 --- a/src/stream-chooser/stream-chooser.cpp +++ b/src/stream-chooser/stream-chooser.cpp @@ -40,7 +40,7 @@ static void registry_add_object(void *data, wl_registry *registry, uint32_t name wl_registry_bind(registry, name, &ext_foreign_toplevel_list_v1_interface, version); - + WayfireStreamChooserApp::getInstance().has_foreign_toplevel_list = true; WayfireStreamChooserApp::getInstance().set_toplevel_list(list); ext_foreign_toplevel_list_v1_add_listener(list, &toplevel_list_v1_impl, NULL); @@ -148,12 +148,21 @@ void WayfireStreamChooserApp::activate() this->registry = registry; wl_display_roundtrip(display); - if (this->list && has_image_copy_capture) - {} else + bool failed = false; + if (!has_image_copy_capture) + { + failed = true; + std::cerr << "Compositor has not advertised ext-image-copy-capture-v1" << std::endl; + } + + if (!has_foreign_toplevel_list) + { + failed = true; + std::cerr << "Compositor has not advertised ext-foreign-toplevel-list-v1" << std::endl; + } + + if (failed) { - std::cerr << "Compositor doesn't support" << - " ext-foreign-toplevel-list and/or ext-image-copy-capture-v1." << - " Only screens can be cast currently." << std::endl; window_label.set_sensitive(false); window_label.set_tooltip_text("This compositor does not currently support sharing individual windows"); notebook.set_current_page(1); diff --git a/src/stream-chooser/stream-chooser.hpp b/src/stream-chooser/stream-chooser.hpp index 6c400e0c..41757924 100644 --- a/src/stream-chooser/stream-chooser.hpp +++ b/src/stream-chooser/stream-chooser.hpp @@ -26,7 +26,8 @@ class WayfireStreamChooserApp : public Gtk::Application ext_foreign_toplevel_list_v1 *list; public: - bool has_image_copy_capture = false; + bool has_foreign_toplevel_list = false; + bool has_image_copy_capture = false; std::map> toplevels; std::map> outputs; From a06b011117ec9c4f03108a1adbc22e571a760a4e Mon Sep 17 00:00:00 2001 From: trigg Date: Fri, 1 May 2026 11:36:45 +0100 Subject: [PATCH 04/12] xdpw: stylistic changes to layout xdpw: add layout manager for window xdpw: add layout manager for toplevels --- proto/ext-image-capture-source-v1.xml | 109 ++++++ proto/ext-image-copy-capture-v1.xml | 455 ++++++++++++++++++++++++++ proto/meson.build | 2 + src/stream-chooser/mainlayout.cpp | 29 ++ src/stream-chooser/mainlayout.hpp | 19 ++ src/stream-chooser/meson.build | 8 +- src/stream-chooser/outputwidget.cpp | 3 + src/stream-chooser/outputwidget.hpp | 2 +- src/stream-chooser/stream-chooser.cpp | 67 +++- src/stream-chooser/stream-chooser.hpp | 4 + src/stream-chooser/toplevellayout.cpp | 41 +++ src/stream-chooser/toplevellayout.hpp | 19 ++ src/stream-chooser/toplevelwidget.cpp | 9 + src/stream-chooser/toplevelwidget.hpp | 5 +- 14 files changed, 751 insertions(+), 21 deletions(-) create mode 100644 proto/ext-image-capture-source-v1.xml create mode 100644 proto/ext-image-copy-capture-v1.xml create mode 100644 src/stream-chooser/mainlayout.cpp create mode 100644 src/stream-chooser/mainlayout.hpp create mode 100644 src/stream-chooser/toplevellayout.cpp create mode 100644 src/stream-chooser/toplevellayout.hpp diff --git a/proto/ext-image-capture-source-v1.xml b/proto/ext-image-capture-source-v1.xml new file mode 100644 index 00000000..becab095 --- /dev/null +++ b/proto/ext-image-capture-source-v1.xml @@ -0,0 +1,109 @@ + + + + Copyright © 2022 Andri Yngvason + Copyright © 2024 Simon Ser + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol serves as an intermediary between capturing protocols and + potential image capture sources such as outputs and toplevels. + + This protocol may be extended to support more image capture sources in the + future, thereby adding those image capture sources to other protocols that + use the image capture source object without having to modify those + protocols. + + Warning! The protocol described in this file is currently in the testing + phase. Backward compatible changes may be added together with the + corresponding interface version bump. Backward incompatible changes can + only be done by creating a new major version of the extension. + + + + + The image capture source object is an opaque descriptor for a capturable + resource. This resource may be any sort of entity from which an image + may be derived. + + Note, because ext_image_capture_source_v1 objects are created from multiple + independent factory interfaces, the ext_image_capture_source_v1 interface is + frozen at version 1. + + + + + Destroys the image capture source. This request may be sent at any time + by the client. + + + + + + + A manager for creating image capture source objects for wl_output objects. + + + + + Creates a source object for an output. Images captured from this source + will show the same content as the output. Some elements may be omitted, + such as cursors and overlays that have been marked as transparent to + capturing. + + + + + + + + Destroys the manager. This request may be sent at any time by the client + and objects created by the manager will remain valid after its + destruction. + + + + + + + A manager for creating image capture source objects for + ext_foreign_toplevel_handle_v1 objects. + + + + + Creates a source object for a foreign toplevel handle. Images captured + from this source will show the same content as the toplevel. + + + + + + + + Destroys the manager. This request may be sent at any time by the client + and objects created by the manager will remain valid after its + destruction. + + + + \ No newline at end of file diff --git a/proto/ext-image-copy-capture-v1.xml b/proto/ext-image-copy-capture-v1.xml new file mode 100644 index 00000000..50ee1f6d --- /dev/null +++ b/proto/ext-image-copy-capture-v1.xml @@ -0,0 +1,455 @@ + + + + Copyright © 2021-2023 Andri Yngvason + Copyright © 2024 Simon Ser + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol allows clients to ask the compositor to capture image sources + such as outputs and toplevels into user submitted buffers. + + Warning! The protocol described in this file is currently in the testing + phase. Backward compatible changes may be added together with the + corresponding interface version bump. Backward incompatible changes can + only be done by creating a new major version of the extension. + + + + + This object is a manager which offers requests to start capturing from a + source. + + + + + + + + + + + + + Create a capturing session for an image capture source. + + If the paint_cursors option is set, cursors shall be composited onto + the captured frame. The cursor must not be composited onto the frame + if this flag is not set. + + If the options bitfield is invalid, the invalid_option protocol error + is sent. + + + + + + + + + Create a cursor capturing session for the pointer of an image capture + source. + + + + + + + + + Destroy the manager object. + + Other objects created via this interface are unaffected. + + + + + + + This object represents an active image copy capture session. + + After a capture session is created, buffer constraint events will be + emitted from the compositor to tell the client which buffer types and + formats are supported for reading from the session. The compositor may + re-send buffer constraint events whenever they change. + + To advertise buffer constraints, the compositor must send in no + particular order: zero or more shm_format and dmabuf_format events, zero + or one dmabuf_device event, and exactly one buffer_size event. Then the + compositor must send a done event. + + When the client has received all the buffer constraints, it can create a + buffer accordingly, attach it to the capture session using the + attach_buffer request, set the buffer damage using the damage_buffer + request and then send the capture request. + + + + + + + + + Provides the dimensions of the source image in buffer pixel coordinates. + + The client must attach buffers that match this size. + + + + + + + + Provides the format that must be used for shared-memory buffers. + + This event may be emitted multiple times, in which case the client may + choose any given format. + + + + + + + This event advertises the device buffers must be allocated on for + dma-buf buffers. + + In general the device is a DRM node. The DRM node type (primary vs. + render) is unspecified. Clients must not rely on the compositor sending + a particular node type. Clients cannot check two devices for equality + by comparing the dev_t value. + + + + + + + Provides the format that must be used for dma-buf buffers. + + The client may choose any of the modifiers advertised in the array of + 64-bit unsigned integers. + + This event may be emitted multiple times, in which case the client may + choose any given format. + + + + + + + + This event is sent once when all buffer constraint events have been + sent. + + The compositor must always end a batch of buffer constraint events with + this event, regardless of whether it sends the initial constraints or + an update. + + + + + + This event indicates that the capture session has stopped and is no + longer available. This can happen in a number of cases, e.g. when the + underlying source is destroyed, if the user decides to end the image + capture, or if an unrecoverable runtime error has occurred. + + The client should destroy the session after receiving this event. + + + + + + Create a capture frame for this session. + + At most one frame object can exist for a given session at any time. If + a client sends a create_frame request before a previous frame object + has been destroyed, the duplicate_frame protocol error is raised. + + + + + + + Destroys the session. This request can be sent at any time by the + client. + + This request doesn't affect ext_image_copy_capture_frame_v1 objects created by + this object. + + + + + + + This object represents an image capture frame. + + The client should attach a buffer, damage the buffer, and then send a + capture request. + + If the capture is successful, the compositor must send the frame metadata + (transform, damage, presentation_time in any order) followed by the ready + event. + + If the capture fails, the compositor must send the failed event. + + + + + + + + + + + Destroys the frame. This request can be sent at any time by the + client. + + + + + + Attach a buffer to the session. + + The wl_buffer.release request is unused. + + The new buffer replaces any previously attached buffer. + + This request must not be sent after capture, or else the + already_captured protocol error is raised. + + + + + + + Apply damage to the buffer which is to be captured next. This request + may be sent multiple times to describe a region. + + The client indicates the accumulated damage since this wl_buffer was + last captured. During capture, the compositor will update the buffer + with at least the union of the region passed by the client and the + region advertised by ext_image_copy_capture_frame_v1.damage. + + When a wl_buffer is captured for the first time, or when the client + doesn't track damage, the client must damage the whole buffer. + + This is for optimisation purposes. The compositor may use this + information to reduce copying. + + These coordinates originate from the upper left corner of the buffer. + + If x or y are strictly negative, or if width or height are negative or + zero, the invalid_buffer_damage protocol error is raised. + + This request must not be sent after capture, or else the + already_captured protocol error is raised. + + + + + + + + + + Capture a frame. + + Unless this is the first successful captured frame performed in this + session, the compositor may wait an indefinite amount of time for the + source content to change before performing the copy. + + This request may only be sent once, or else the already_captured + protocol error is raised. A buffer must be attached before this request + is sent, or else the no_buffer protocol error is raised. + + + + + + This event is sent before the ready event and holds the transform that + the compositor has applied to the buffer contents. + + + + + + + This event is sent before the ready event. It may be generated multiple + times to describe a region. + + The first captured frame in a session will always carry full damage. + Subsequent frames' damaged regions describe which parts of the buffer + have changed since the last ready event. + + These coordinates originate in the upper left corner of the buffer. + + + + + + + + + + This event indicates the time at which the frame is presented to the + output in system monotonic time. This event is sent before the ready + event. + + The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples, + each component being an unsigned 32-bit value. Whole seconds are in + tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo, + and the additional fractional part in tv_nsec as nanoseconds. Hence, + for valid timestamps tv_nsec must be in [0, 999999999]. + + + + + + + + + Called as soon as the frame is copied, indicating it is available + for reading. + + The buffer may be re-used by the client after this event. + + After receiving this event, the client must destroy the object. + + + + + + + An unspecified runtime error has occurred. The client may retry. + + + + + The buffer submitted by the client doesn't match the latest session + constraints. The client should re-allocate its buffers and retry. + + + + + The session has stopped. See ext_image_copy_capture_session_v1.stopped. + + + + + + + This event indicates that the attempted frame copy has failed. + + After receiving this event, the client must destroy the object. + + + + + + + + This object represents a cursor capture session. It extends the base + capture session with cursor-specific metadata. + + + + + + + + + Destroys the session. This request can be sent at any time by the + client. + + This request doesn't affect ext_image_copy_capture_frame_v1 objects created by + this object. + + + + + + Gets the image copy capture session for this cursor session. + + The session will produce frames of the cursor image. The compositor may + pause the session when the cursor leaves the captured area. + + This request must not be sent more than once, or else the + duplicate_session protocol error is raised. + + + + + + + Sent when a cursor enters the captured area. It shall be generated + before the "position" and "hotspot" events when and only when a cursor + enters the area. + + The cursor enters the captured area when the cursor image intersects + with the captured area. Note, this is different from e.g. + wl_pointer.enter. + + + + + + Sent when a cursor leaves the captured area. No "position" or "hotspot" + event is generated for the cursor until the cursor enters the captured + area again. + + + + + + Cursors outside the image capture source do not get captured and no + event will be generated for them. + + The given position is the position of the cursor's hotspot and it is + relative to the main buffer's top left corner in transformed buffer + pixel coordinates. The coordinates may be negative or greater than the + main buffer size. + + + + + + + + The hotspot describes the offset between the cursor image and the + position of the input device. + + The given coordinates are the hotspot's offset from the origin in + buffer coordinates. + + Clients should not apply the hotspot immediately: the hotspot becomes + effective when the next ext_image_copy_capture_frame_v1.ready event is received. + Compositors may delay this event until the client captures a new frame. + + + + + + \ No newline at end of file diff --git a/proto/meson.build b/proto/meson.build index ebcaebfa..fc0eea5f 100644 --- a/proto/meson.build +++ b/proto/meson.build @@ -16,6 +16,8 @@ wayland_scanner_client = generator( client_protocols = [ 'ext-foreign-toplevel-list-v1.xml', + 'ext-image-capture-source-v1.xml', + 'ext-image-copy-capture-v1.xml', 'wlr-foreign-toplevel-management-unstable-v1.xml', 'wlr-screencopy.xml', wayfire.get_pkgconfig_variable('pkgdatadir') / 'unstable' / 'wayfire-shell-unstable-v2.xml', diff --git a/src/stream-chooser/mainlayout.cpp b/src/stream-chooser/mainlayout.cpp new file mode 100644 index 00000000..9e62ca32 --- /dev/null +++ b/src/stream-chooser/mainlayout.cpp @@ -0,0 +1,29 @@ +#include "mainlayout.hpp" +void MainLayout::allocate_vfunc(const Gtk::Widget& widget, int width, int height, int baseline) +{ + Gtk::Widget& widget_not_const = const_cast(widget); + + auto inner = widget_not_const.get_children()[0]; + auto alloc = Gtk::Allocation(); + alloc.set_height(height / 2); + alloc.set_width(width / 2); + alloc.set_y(height / 4); + alloc.set_x(width / 4); + inner->size_allocate(alloc, -1); +} + +void MainLayout::measure_vfunc(const Gtk::Widget& widget, Gtk::Orientation orientation, + int for_size, int& minimum, int& natural, int& minimum_baseline, + int& natural_baseline) const +{ + // Answer 1:1 aspect ratio + minimum = std::min(for_size, 300); + natural = std::min(for_size, 2000); + minimum_baseline = -1; + natural_baseline = -1; +} + +Gtk::SizeRequestMode MainLayout::get_request_mode_vfunc(const Gtk::Widget& widget) const +{ + return Gtk::SizeRequestMode::WIDTH_FOR_HEIGHT; +} diff --git a/src/stream-chooser/mainlayout.hpp b/src/stream-chooser/mainlayout.hpp new file mode 100644 index 00000000..6696a8ea --- /dev/null +++ b/src/stream-chooser/mainlayout.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + + +class MainLayout : public Gtk::LayoutManager +{ + protected: + void allocate_vfunc(const Gtk::Widget& widget, int width, int height, int baseline) override; + void measure_vfunc(const Gtk::Widget& widget, Gtk::Orientation orientation, int for_size, int& minimum, + int& natural, int& minimum_baseline, int& natural_baseline) const override; + Gtk::SizeRequestMode get_request_mode_vfunc(const Gtk::Widget& widget) const override; + std::shared_ptr layout; + + public: + MainLayout() + {} +}; diff --git a/src/stream-chooser/meson.build b/src/stream-chooser/meson.build index 90343525..1bec648c 100644 --- a/src/stream-chooser/meson.build +++ b/src/stream-chooser/meson.build @@ -7,7 +7,13 @@ deps = [ executable( 'wf-stream-chooser', - ['stream-chooser.cpp', 'toplevelwidget.cpp', 'outputwidget.cpp'], + [ + 'stream-chooser.cpp', + 'toplevelwidget.cpp', + 'outputwidget.cpp', + 'toplevellayout.cpp', + 'mainlayout.cpp', + ], dependencies: deps, install: true, ) \ No newline at end of file diff --git a/src/stream-chooser/outputwidget.cpp b/src/stream-chooser/outputwidget.cpp index 6220b28d..1e078e54 100644 --- a/src/stream-chooser/outputwidget.cpp +++ b/src/stream-chooser/outputwidget.cpp @@ -8,6 +8,9 @@ WayfireChooserOutput::WayfireChooserOutput(std::shared_ptr output) append(contents); append(model); append(connector); + set_size_request(150, 150); + set_valign(Gtk::Align::FILL); + set_halign(Gtk::Align::FILL); /* TODO Contents. We should probably grab screenshots of each output and display them */ diff --git a/src/stream-chooser/outputwidget.hpp b/src/stream-chooser/outputwidget.hpp index e96b6386..5a050ffe 100644 --- a/src/stream-chooser/outputwidget.hpp +++ b/src/stream-chooser/outputwidget.hpp @@ -5,7 +5,7 @@ class WayfireChooserOutput : public Gtk::Box { Gtk::Label connector, model; - Gtk::Image contents; + Gtk::Picture contents; std::shared_ptr output; diff --git a/src/stream-chooser/stream-chooser.cpp b/src/stream-chooser/stream-chooser.cpp index 996294b1..2e2e767b 100644 --- a/src/stream-chooser/stream-chooser.cpp +++ b/src/stream-chooser/stream-chooser.cpp @@ -2,13 +2,14 @@ #include #include #include +#include +#include #include "stream-chooser.hpp" #include "gtkmm/enums.h" #include "outputwidget.hpp" #include "toplevelwidget.hpp" -#define EXT_IMAGE_COPY_CAPTURE_V1 "ext-image-copy-capture-v1" /* Static callbacks for toplevel list object */ static void handle_toplevel(void *data, @@ -44,10 +45,13 @@ static void registry_add_object(void *data, wl_registry *registry, uint32_t name WayfireStreamChooserApp::getInstance().set_toplevel_list(list); ext_foreign_toplevel_list_v1_add_listener(list, &toplevel_list_v1_impl, NULL); - } else if (strcmp(interface, EXT_IMAGE_COPY_CAPTURE_V1) == 0) + } else if (strcmp(interface, ext_image_copy_capture_manager_v1_interface.name) == 0) { /* We need this to exist, but we're not using it directly */ WayfireStreamChooserApp::getInstance().has_image_copy_capture = true; + } else if (strcmp(interface, ext_image_capture_source_v1_interface.name) == 0) + { + WayfireStreamChooserApp::getInstance().has_image_capture_source = true; } } @@ -69,25 +73,41 @@ WayfireStreamChooserApp::WayfireStreamChooserApp() : Gtk::Application("org.wayfi void WayfireStreamChooserApp::activate() { window.add_css_class("stream-chooser"); - main.set_size_request(300, 300); + window.set_size_request(300, 300); add_window(window); window.set_child(main); - main.set_valign(Gtk::Align::CENTER); - main.set_halign(Gtk::Align::CENTER); - main.set_vexpand(false); - main.set_hexpand(false); + layout = std::make_shared(); + window.set_layout_manager(layout); + main.add_css_class("main-chooser"); + main.set_valign(Gtk::Align::FILL); + main.set_halign(Gtk::Align::FILL); main.append(header); main.append(notebook); main.append(buttons); auto window_display = window.get_display(); auto css_provider = Gtk::CssProvider::create(); - css_provider->load_from_data("window.stream-chooser { background-color: rgba(0, 0, 0, 0.5); }"); + css_provider->load_from_data( + "window.stream-chooser { background-color: rgba(0, 0, 0, 0.5); } .main-chooser { background-color: unset; }"); Gtk::StyleContext::add_provider_for_display(window_display, css_provider, GTK_STYLE_PROVIDER_PRIORITY_USER); + scroll_window.set_child(window_list); + scroll_screen.set_child(screen_list); + + scroll_window.set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC); + scroll_screen.set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC); + + window_list.set_homogeneous(true); + screen_list.set_homogeneous(true); + + window_list.set_halign(Gtk::Align::START); + window_list.set_valign(Gtk::Align::START); + screen_list.set_halign(Gtk::Align::START); + screen_list.set_valign(Gtk::Align::START); + notebook.set_expand(true); - notebook.append_page(window_list, window_label); - notebook.append_page(screen_list, screen_label); + notebook.append_page(scroll_window, window_label); + notebook.append_page(scroll_screen, screen_label); main.set_orientation(Gtk::Orientation::VERTICAL); @@ -161,6 +181,12 @@ void WayfireStreamChooserApp::activate() std::cerr << "Compositor has not advertised ext-foreign-toplevel-list-v1" << std::endl; } + if (!has_image_capture_source) + { + failed = true; + std::cerr << "Compositor has not advertised ext-image-capture-source-v1" << std::endl; + } + if (failed) { window_label.set_sensitive(false); @@ -192,15 +218,20 @@ void WayfireStreamChooserApp::activate() add_output(obj); } - gtk_layer_init_for_window(window.gobj()); - gtk_layer_set_namespace(window.gobj(), "chooser"); - gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_TOP, true); - gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_BOTTOM, true); - gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_LEFT, true); - gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_RIGHT, true); + auto debug = Glib::getenv("WF_CHOOSER_DEBUG"); + if (debug != "1") + { + gtk_layer_init_for_window(window.gobj()); + gtk_layer_set_namespace(window.gobj(), "chooser"); + gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_TOP, true); + gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_BOTTOM, true); + gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_LEFT, true); + gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_RIGHT, true); + + gtk_layer_set_keyboard_mode(window.gobj(), GTK_LAYER_SHELL_KEYBOARD_MODE_EXCLUSIVE); + gtk_layer_set_exclusive_zone(window.gobj(), 0); + } - gtk_layer_set_keyboard_mode(window.gobj(), GTK_LAYER_SHELL_KEYBOARD_MODE_EXCLUSIVE); - gtk_layer_set_exclusive_zone(window.gobj(), 0); window.present(); } diff --git a/src/stream-chooser/stream-chooser.hpp b/src/stream-chooser/stream-chooser.hpp index 41757924..e06b06d5 100644 --- a/src/stream-chooser/stream-chooser.hpp +++ b/src/stream-chooser/stream-chooser.hpp @@ -3,6 +3,7 @@ #include #include +#include "mainlayout.hpp" #include "outputwidget.hpp" #include "toplevelwidget.hpp" @@ -19,15 +20,18 @@ class WayfireStreamChooserApp : public Gtk::Application Gtk::FlowBox window_list, screen_list; Gtk::Button done, cancel; + Gtk::ScrolledWindow scroll_window, scroll_screen; WayfireStreamChooserApp(); wl_display *display; wl_registry *registry; ext_foreign_toplevel_list_v1 *list; + Glib::RefPtr layout; public: bool has_foreign_toplevel_list = false; bool has_image_copy_capture = false; + bool has_image_capture_source = false; std::map> toplevels; std::map> outputs; diff --git a/src/stream-chooser/toplevellayout.cpp b/src/stream-chooser/toplevellayout.cpp new file mode 100644 index 00000000..41162c26 --- /dev/null +++ b/src/stream-chooser/toplevellayout.cpp @@ -0,0 +1,41 @@ +#include "toplevellayout.hpp" +void ToplevelLayout::allocate_vfunc(const Gtk::Widget& widget, int width, int height, int baseline) +{ + Gtk::Widget& widget_not_const = const_cast(widget); + + auto overlay = widget_not_const.get_children()[0]; + auto label = widget_not_const.get_children()[1]; + + /* Label */ + auto label_height = std::max(16, label->get_height()); + auto l_alloc = Gtk::Allocation(); + l_alloc.set_height(label_height); + l_alloc.set_width(width); + l_alloc.set_y(0); + l_alloc.set_x(0); + label->size_allocate(l_alloc, -1); + + /* Overlay */ + auto o_alloc = Gtk::Allocation(); + o_alloc.set_height(height - label_height); + o_alloc.set_width(width); + o_alloc.set_y(label_height); + o_alloc.set_x(0); + overlay->size_allocate(o_alloc, -1); +} + +void ToplevelLayout::measure_vfunc(const Gtk::Widget& widget, Gtk::Orientation orientation, + int for_size, int& minimum, int& natural, int& minimum_baseline, + int& natural_baseline) const +{ + // Answer 1:1 aspect ratio + minimum = for_size; + natural = for_size; + minimum_baseline = -1; + natural_baseline = -1; +} + +Gtk::SizeRequestMode ToplevelLayout::get_request_mode_vfunc(const Gtk::Widget& widget) const +{ + return Gtk::SizeRequestMode::WIDTH_FOR_HEIGHT; +} diff --git a/src/stream-chooser/toplevellayout.hpp b/src/stream-chooser/toplevellayout.hpp new file mode 100644 index 00000000..871c602b --- /dev/null +++ b/src/stream-chooser/toplevellayout.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + + +class ToplevelLayout : public Gtk::LayoutManager +{ + protected: + void allocate_vfunc(const Gtk::Widget& widget, int width, int height, int baseline) override; + void measure_vfunc(const Gtk::Widget& widget, Gtk::Orientation orientation, int for_size, int& minimum, + int& natural, int& minimum_baseline, int& natural_baseline) const override; + Gtk::SizeRequestMode get_request_mode_vfunc(const Gtk::Widget& widget) const override; + std::shared_ptr layout; + + public: + ToplevelLayout() + {} +}; diff --git a/src/stream-chooser/toplevelwidget.cpp b/src/stream-chooser/toplevelwidget.cpp index 26dc182d..ed75320e 100644 --- a/src/stream-chooser/toplevelwidget.cpp +++ b/src/stream-chooser/toplevelwidget.cpp @@ -1,6 +1,8 @@ #include +#include "gtkmm/enums.h" #include "stream-chooser.hpp" +#include "toplevellayout.hpp" #include "toplevelwidget.hpp" static void handle_closed(void *data, @@ -55,12 +57,19 @@ ext_foreign_toplevel_handle_v1_listener listener = /* Gtk Overlay showing information about a window */ WayfireChooserTopLevel::WayfireChooserTopLevel(ext_foreign_toplevel_handle_v1 *handle) { + set_size_request(150, 150); + set_valign(Gtk::Align::FILL); + set_halign(Gtk::Align::FILL); + layout = std::make_shared(); + set_layout_manager(layout); append(overlay); append(label); overlay.set_child(screenshot); overlay.add_overlay(icon); icon.set_halign(Gtk::Align::START); icon.set_valign(Gtk::Align::END); + screenshot.set_halign(Gtk::Align::FILL); + screenshot.set_valign(Gtk::Align::FILL); label.set_ellipsize(Pango::EllipsizeMode::MIDDLE); label.set_max_width_chars(40); diff --git a/src/stream-chooser/toplevelwidget.hpp b/src/stream-chooser/toplevelwidget.hpp index ef0bd1b3..95144aa5 100644 --- a/src/stream-chooser/toplevelwidget.hpp +++ b/src/stream-chooser/toplevelwidget.hpp @@ -1,18 +1,21 @@ #pragma once #include #include "ext-foreign-toplevel-list-v1-client-protocol.h" +#include "glibmm/refptr.h" +#include "toplevellayout.hpp" class WayfireChooserTopLevel : public Gtk::Box { private: Gtk::Overlay overlay; Gtk::Image icon; - Gtk::Image screenshot; + Gtk::Picture screenshot; Gtk::Label label; std::string buffered_title = "", title = ""; std::string buffered_app_id = "", app_id = ""; std::string buffered_identifier = "", identifier = ""; + Glib::RefPtr layout; public: ext_foreign_toplevel_handle_v1 *handle; From d43b03116d091c49388ae141298b4e9d7fddfeaf Mon Sep 17 00:00:00 2001 From: Scott Moreau Date: Fri, 1 May 2026 14:14:48 -0600 Subject: [PATCH 05/12] Use snapshots of toplevels for chooser list --- proto/ext-foreign-toplevel-list-v1.xml | 219 ---------------------- proto/meson.build | 19 +- src/stream-chooser/stream-chooser.cpp | 29 ++- src/stream-chooser/stream-chooser.hpp | 8 + src/stream-chooser/toplevelwidget.cpp | 242 +++++++++++++++++++++++++ src/stream-chooser/toplevelwidget.hpp | 3 +- 6 files changed, 289 insertions(+), 231 deletions(-) delete mode 100644 proto/ext-foreign-toplevel-list-v1.xml diff --git a/proto/ext-foreign-toplevel-list-v1.xml b/proto/ext-foreign-toplevel-list-v1.xml deleted file mode 100644 index 11b0113a..00000000 --- a/proto/ext-foreign-toplevel-list-v1.xml +++ /dev/null @@ -1,219 +0,0 @@ - - - - Copyright © 2018 Ilia Bozhinov - Copyright © 2020 Isaac Freund - Copyright © 2022 wb9688 - Copyright © 2023 i509VCB - - Permission to use, copy, modify, distribute, and sell this - software and its documentation for any purpose is hereby granted - without fee, provided that the above copyright notice appear in - all copies and that both that copyright notice and this permission - notice appear in supporting documentation, and that the name of - the copyright holders not be used in advertising or publicity - pertaining to distribution of the software without specific, - written prior permission. The copyright holders make no - representations about the suitability of this software for any - purpose. It is provided "as is" without express or implied - warranty. - - THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS - SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND - FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY - SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN - AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, - ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF - THIS SOFTWARE. - - - - The purpose of this protocol is to provide protocol object handles for - toplevels, possibly originating from another client. - - This protocol is intentionally minimalistic and expects additional - functionality (e.g. creating a screencopy source from a toplevel handle, - getting information about the state of the toplevel) to be implemented - in extension protocols. - - The compositor may choose to restrict this protocol to a special client - launched by the compositor itself or expose it to all clients, - this is compositor policy. - - The key words "must", "must not", "required", "shall", "shall not", - "should", "should not", "recommended", "may", and "optional" in this - document are to be interpreted as described in IETF RFC 2119. - - Warning! The protocol described in this file is currently in the testing - phase. Backward compatible changes may be added together with the - corresponding interface version bump. Backward incompatible changes can - only be done by creating a new major version of the extension. - - - - - A toplevel is defined as a surface with a role similar to xdg_toplevel. - XWayland surfaces may be treated like toplevels in this protocol. - - After a client binds the ext_foreign_toplevel_list_v1, each mapped - toplevel window will be sent using the ext_foreign_toplevel_list_v1.toplevel - event. - - Clients which only care about the current state can perform a roundtrip after - binding this global. - - For each instance of ext_foreign_toplevel_list_v1, the compositor must - create a new ext_foreign_toplevel_handle_v1 object for each mapped toplevel. - - If a compositor implementation sends the ext_foreign_toplevel_list_v1.finished - event after the global is bound, the compositor must not send any - ext_foreign_toplevel_list_v1.toplevel events. - - - - - This event is emitted whenever a new toplevel window is created. It is - emitted for all toplevels, regardless of the app that has created them. - - All initial properties of the toplevel (identifier, title, app_id) will be sent - immediately after this event using the corresponding events for - ext_foreign_toplevel_handle_v1. The compositor will use the - ext_foreign_toplevel_handle_v1.done event to indicate when all data has - been sent. - - - - - - - This event indicates that the compositor is done sending events - to this object. The client should destroy the object. - See ext_foreign_toplevel_list_v1.destroy for more information. - - The compositor must not send any more toplevel events after this event. - - - - - - This request indicates that the client no longer wishes to receive - events for new toplevels. - - The Wayland protocol is asynchronous, meaning the compositor may send - further toplevel events until the stop request is processed. - The client should wait for a ext_foreign_toplevel_list_v1.finished - event before destroying this object. - - - - - - This request should be called either when the client will no longer - use the ext_foreign_toplevel_list_v1 or after the finished event - has been received to allow destruction of the object. - - If a client wishes to destroy this object it should send a - ext_foreign_toplevel_list_v1.stop request and wait for a ext_foreign_toplevel_list_v1.finished - event, then destroy the handles and then this object. - - - - - - - A ext_foreign_toplevel_handle_v1 object represents a mapped toplevel - window. A single app may have multiple mapped toplevels. - - - - - This request should be used when the client will no longer use the handle - or after the closed event has been received to allow destruction of the - object. - - When a handle is destroyed, a new handle may not be created by the server - until the toplevel is unmapped and then remapped. Destroying a toplevel handle - is not recommended unless the client is cleaning up child objects - before destroying the ext_foreign_toplevel_list_v1 object, the toplevel - was closed or the toplevel handle will not be used in the future. - - Other protocols which extend the ext_foreign_toplevel_handle_v1 - interface should require destructors for extension interfaces be - called before allowing the toplevel handle to be destroyed. - - - - - - The server will emit no further events on the ext_foreign_toplevel_handle_v1 - after this event. Any requests received aside from the destroy request must - be ignored. Upon receiving this event, the client should destroy the handle. - - Other protocols which extend the ext_foreign_toplevel_handle_v1 - interface must also ignore requests other than destructors. - - - - - - This event is sent after all changes in the toplevel state have - been sent. - - This allows changes to the ext_foreign_toplevel_handle_v1 properties - to be atomically applied. Other protocols which extend the - ext_foreign_toplevel_handle_v1 interface may use this event to also - atomically apply any pending state. - - This event must not be sent after the ext_foreign_toplevel_handle_v1.closed - event. - - - - - - The title of the toplevel has changed. - - The configured state must not be applied immediately. See - ext_foreign_toplevel_handle_v1.done for details. - - - - - - - The app id of the toplevel has changed. - - The configured state must not be applied immediately. See - ext_foreign_toplevel_handle_v1.done for details. - - - - - - - This identifier is used to check if two or more toplevel handles belong - to the same toplevel. - - The identifier is useful for command line tools or privileged clients - which may need to reference an exact toplevel across processes or - instances of the ext_foreign_toplevel_list_v1 global. - - The compositor must only send this event when the handle is created. - - The identifier must be unique per toplevel and it's handles. Two different - toplevels must not have the same identifier. The identifier is only valid - as long as the toplevel is mapped. If the toplevel is unmapped the identifier - must not be reused. An identifier must not be reused by the compositor to - ensure there are no races when sharing identifiers between processes. - - An identifier is a string that contains up to 32 printable ASCII bytes. - An identifier must not be an empty string. It is recommended that a - compositor includes an opaque generation value in identifiers. How the - generation value is used when generating the identifier is implementation - dependent. - - - - - diff --git a/proto/meson.build b/proto/meson.build index ebcaebfa..28d4037a 100644 --- a/proto/meson.build +++ b/proto/meson.build @@ -1,5 +1,4 @@ -wl_protocol_dir = wayland_protos.get_pkgconfig_variable('pkgdatadir') - +wl_protocol_dir = wayland_protos.get_variable(pkgconfig: 'pkgdatadir', internal: 'pkgdatadir') wayland_scanner = find_program('wayland-scanner') wayland_scanner_code = generator( @@ -15,26 +14,26 @@ wayland_scanner_client = generator( ) client_protocols = [ - 'ext-foreign-toplevel-list-v1.xml', + [wl_protocol_dir, 'unstable/xdg-output/xdg-output-unstable-v1.xml'], + [wl_protocol_dir, 'unstable/linux-dmabuf/linux-dmabuf-unstable-v1.xml'], + [wl_protocol_dir, 'staging/ext-foreign-toplevel-list/ext-foreign-toplevel-list-v1.xml'], + [wl_protocol_dir, 'staging/ext-image-capture-source/ext-image-capture-source-v1.xml'], + [wl_protocol_dir, 'staging/ext-image-copy-capture/ext-image-copy-capture-v1.xml'], 'wlr-foreign-toplevel-management-unstable-v1.xml', 'wlr-screencopy.xml', wayfire.get_pkgconfig_variable('pkgdatadir') / 'unstable' / 'wayfire-shell-unstable-v2.xml', ] -if gbm.found() and drm.found() and not get_option('live-previews-dmabuf').disabled() - client_protocols += [join_paths(wl_protocol_dir, 'unstable/linux-dmabuf/linux-dmabuf-unstable-v1.xml')] -endif - -wl_protos_src = [] +wl_protos_client_src = [] wl_protos_headers = [] foreach p : client_protocols xml = join_paths(p) + wl_protos_client_src += wayland_scanner_code.process(xml) wl_protos_headers += wayland_scanner_client.process(xml) - wl_protos_src += wayland_scanner_code.process(xml) endforeach -lib_wl_protos = static_library('wl_protos', wl_protos_src + wl_protos_headers, +lib_wl_protos = static_library('wl_protos', wl_protos_client_src + wl_protos_headers, dependencies: [wayland_client]) # for the include directory wf_protos = declare_dependency( diff --git a/src/stream-chooser/stream-chooser.cpp b/src/stream-chooser/stream-chooser.cpp index 996294b1..ce5e4a78 100644 --- a/src/stream-chooser/stream-chooser.cpp +++ b/src/stream-chooser/stream-chooser.cpp @@ -44,10 +44,22 @@ static void registry_add_object(void *data, wl_registry *registry, uint32_t name WayfireStreamChooserApp::getInstance().set_toplevel_list(list); ext_foreign_toplevel_list_v1_add_listener(list, &toplevel_list_v1_impl, NULL); - } else if (strcmp(interface, EXT_IMAGE_COPY_CAPTURE_V1) == 0) + } else if (strcmp(interface, ext_image_copy_capture_manager_v1_interface.name) == 0) { /* We need this to exist, but we're not using it directly */ + auto manager = (ext_image_copy_capture_manager_v1*) wl_registry_bind(registry, name, + &ext_image_copy_capture_manager_v1_interface, version); WayfireStreamChooserApp::getInstance().has_image_copy_capture = true; + WayfireStreamChooserApp::getInstance().set_copy_capture_manager(manager); + } else if (strcmp(interface, wl_shm_interface.name) == 0) + { + auto shm = (wl_shm*) wl_registry_bind(registry, name, &wl_shm_interface, 1); + WayfireStreamChooserApp::getInstance().set_shm(shm); + } else if (strcmp(interface, ext_foreign_toplevel_image_capture_source_manager_v1_interface.name) == 0) + { + auto toplevel_capture_manager = (ext_foreign_toplevel_image_capture_source_manager_v1*) wl_registry_bind(registry, name, + &ext_foreign_toplevel_image_capture_source_manager_v1_interface, version); + WayfireStreamChooserApp::getInstance().set_toplevel_capture_manager(toplevel_capture_manager); } } @@ -233,6 +245,11 @@ void WayfireStreamChooserApp::add_output(std::shared_ptr monitor) } } +void WayfireStreamChooserApp::set_shm(wl_shm *shm) +{ + this->shm = shm; +} + void WayfireStreamChooserApp::remove_output(std::string connector) { screen_list.remove(*outputs[connector]); @@ -244,6 +261,16 @@ void WayfireStreamChooserApp::set_toplevel_list(ext_foreign_toplevel_list_v1 *li this->list = list; } +void WayfireStreamChooserApp::set_copy_capture_manager(ext_image_copy_capture_manager_v1 *manager) +{ + this->manager = manager; +} + +void WayfireStreamChooserApp::set_toplevel_capture_manager(ext_foreign_toplevel_image_capture_source_manager_v1 *toplevel_capture_manager) +{ + this->toplevel_capture_manager = toplevel_capture_manager; +} + /* Starting point */ int main(int argc, char **argv) { diff --git a/src/stream-chooser/stream-chooser.hpp b/src/stream-chooser/stream-chooser.hpp index 41757924..d7d9fa8e 100644 --- a/src/stream-chooser/stream-chooser.hpp +++ b/src/stream-chooser/stream-chooser.hpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include "outputwidget.hpp" #include "toplevelwidget.hpp" @@ -28,6 +30,9 @@ class WayfireStreamChooserApp : public Gtk::Application public: bool has_foreign_toplevel_list = false; bool has_image_copy_capture = false; + wl_shm *shm; + ext_image_copy_capture_manager_v1 *manager; + ext_foreign_toplevel_image_capture_source_manager_v1 *toplevel_capture_manager; std::map> toplevels; std::map> outputs; @@ -38,7 +43,10 @@ class WayfireStreamChooserApp : public Gtk::Application return instance; } + void set_shm(wl_shm *shm); void set_toplevel_list(ext_foreign_toplevel_list_v1 *list); + void set_copy_capture_manager(ext_image_copy_capture_manager_v1 *manager); + void set_toplevel_capture_manager(ext_foreign_toplevel_image_capture_source_manager_v1 *toplevel_capture_manager); void add_toplevel(ext_foreign_toplevel_handle_v1 *handle); void remove_toplevel(WayfireChooserTopLevel *widget); diff --git a/src/stream-chooser/toplevelwidget.cpp b/src/stream-chooser/toplevelwidget.cpp index 26dc182d..86b39f78 100644 --- a/src/stream-chooser/toplevelwidget.cpp +++ b/src/stream-chooser/toplevelwidget.cpp @@ -1,8 +1,20 @@ #include +#include +#include #include "stream-chooser.hpp" #include "toplevelwidget.hpp" +struct toplevel_buffer +{ + int width; + int height; + void *data; + wl_buffer *buffer; + size_t size = 0; + ext_image_copy_capture_frame_v1 *frame = NULL; +}; + static void handle_closed(void *data, struct ext_foreign_toplevel_handle_v1 *handle) { @@ -52,6 +64,234 @@ ext_foreign_toplevel_handle_v1_listener listener = .identifier = handle_identifier, }; +static int backingfile(off_t size) +{ + static int count; + char name[128]; + sprintf(name, "/tmp/wf-stream-chooser-%d-XXXXXX", count++); + int fd = mkstemp(name); + if (fd < 0) + { + perror("mkstemp"); + return -1; + } + + int ret; + while ((ret = ftruncate(fd, size)) == EINTR); + if (ret < 0) + { + perror("ret < 0"); + close(fd); + return -1; + } + + unlink(name); + return fd; +} + +static struct wl_buffer *create_shm_buffer(int width, int height, void **data_out, size_t *size) +{ + *size = width * 4 * height; + printf("size: %ld\n", *size); + + int fd = backingfile(*size); + if (fd < 0) + { + perror("Creating a buffer file failed"); + return NULL; + } + + void *data = mmap(NULL, *size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (data == MAP_FAILED) + { + perror("mmap failed"); + close(fd); + return NULL; + } + + wl_shm_pool *pool = wl_shm_create_pool(WayfireStreamChooserApp::getInstance().shm, fd, *size); + close(fd); + wl_buffer *buffer = wl_shm_pool_create_buffer(pool, 0, width, height, + width * 4, WL_SHM_FORMAT_ABGR8888); + wl_shm_pool_destroy(pool); + + *data_out = data; + return buffer; +} + +void free_shm_buffer(std::shared_ptr& buffer) +{ + if (buffer->buffer == NULL) + { + return; + } + + munmap(buffer->data, buffer->size); + wl_buffer_destroy(buffer->buffer); + buffer->buffer = NULL; +} + +static int current_buffer_width, current_buffer_height, current_buffer_format; + +static void handle_buffer_size(void *, + struct ext_image_copy_capture_session_v1 *, + uint32_t width, uint32_t height) +{ + current_buffer_width = width; + current_buffer_height = height; +} + +static void handle_shm_format(void *, + struct ext_image_copy_capture_session_v1 *, + uint32_t format) +{ + current_buffer_format = format; +} + +static void handle_dmabuf_device(void *, + struct ext_image_copy_capture_session_v1 *, + struct wl_array *) +{ +} + +static void handle_dmabuf_format(void *, + struct ext_image_copy_capture_session_v1 *, + uint32_t, + struct wl_array *) +{ +} + +static void handle_done(void *, + struct ext_image_copy_capture_session_v1 *) +{ +} + +static void handle_stopped(void *, + struct ext_image_copy_capture_session_v1 *) +{ +} + +static const struct ext_image_copy_capture_session_v1_listener recording_session_listener = { + .buffer_size = handle_buffer_size, + .shm_format = handle_shm_format, + .dmabuf_device = handle_dmabuf_device, + .dmabuf_format = handle_dmabuf_format, + .done = handle_done, + .stopped = handle_stopped, +}; + +static void frame_handle_transform(void *, + struct ext_image_copy_capture_frame_v1 *, + uint32_t) +{ +} + +static void frame_handle_damage(void *, + struct ext_image_copy_capture_frame_v1 *, + int32_t, int32_t, int32_t, int32_t) +{} + +static void frame_handle_presentation_time(void *, + struct ext_image_copy_capture_frame_v1 *, + uint32_t, uint32_t, uint32_t) +{} + +static bool buffer_copy_done = false; +static void frame_handle_ready(void *, + struct ext_image_copy_capture_frame_v1 *) +{ + buffer_copy_done = true; +} + +static void frame_handle_failed(void *, + struct ext_image_copy_capture_frame_v1 *, + uint32_t reason) +{ + std::cerr << "Failed to copy frame because reason: " << reason << std::endl; +} + +static const struct ext_image_copy_capture_frame_v1_listener frame_listener = { + .transform = frame_handle_transform, + .damage = frame_handle_damage, + .presentation_time = frame_handle_presentation_time, + .ready = frame_handle_ready, + .failed = frame_handle_failed, +}; + +static std::vector> toplevel_buffers; +static ext_image_copy_capture_frame_v1 *frame = NULL; +static ext_image_copy_capture_session_v1 *recording_session = NULL; + +static std::shared_ptr request_frame() +{ + auto buffer = std::make_shared(); + + if (frame) + { + ext_image_copy_capture_frame_v1_destroy(frame); + } + + buffer->width = current_buffer_width; + buffer->height = current_buffer_height; + + frame = ext_image_copy_capture_session_v1_create_frame(recording_session); + buffer->frame = frame; + ext_image_copy_capture_frame_v1_add_listener(buffer->frame, &frame_listener, &buffer); + + free_shm_buffer(buffer); + buffer->buffer = + create_shm_buffer(buffer->width, buffer->height, &buffer->data, &buffer->size); + + if (buffer->buffer == NULL) + { + std::cerr << "Failed to create buffer" << std::endl; + exit(EXIT_FAILURE); + } + + ext_image_copy_capture_frame_v1_attach_buffer(buffer->frame, buffer->buffer); + ext_image_copy_capture_frame_v1_damage_buffer(buffer->frame, 0, 0, buffer->width, buffer->height); + ext_image_copy_capture_frame_v1_capture(buffer->frame); + + return buffer; +} + +void WayfireChooserTopLevel::grab_toplevel_screenshot(ext_foreign_toplevel_handle_v1 *toplevel) +{printf("%s: %p\n", __func__, toplevel); + auto copy_capture_source = ext_foreign_toplevel_image_capture_source_manager_v1_create_source(WayfireStreamChooserApp::getInstance().toplevel_capture_manager, toplevel); + recording_session = ext_image_copy_capture_manager_v1_create_session(WayfireStreamChooserApp::getInstance().manager, copy_capture_source, EXT_IMAGE_COPY_CAPTURE_MANAGER_V1_OPTIONS_PAINT_CURSORS); + ext_image_copy_capture_session_v1_add_listener(recording_session, &recording_session_listener, NULL); + while ((current_buffer_width <= 0 || current_buffer_height <= 0) && wl_display_dispatch(gdk_wayland_display_get_wl_display(Gdk::Display::get_default()->gobj())) != -1); + auto buffer = request_frame(); + buffer_copy_done = false; + while (!buffer_copy_done && wl_display_dispatch(gdk_wayland_display_get_wl_display(Gdk::Display::get_default()->gobj())) != -1); + buffer->width = current_buffer_width; + buffer->height = current_buffer_height; + /* buffer->data is now valid */ + std::shared_ptr bytes = 0; + size_t size = buffer->size; + if (buffer->data) + { + bytes = Glib::Bytes::create(buffer->data, size); + } + + if (!bytes) + { + return; + } + + auto builder = Gdk::MemoryTextureBuilder::create(); + builder->set_bytes(bytes); + builder->set_width(buffer->width); + builder->set_height(buffer->height); + builder->set_stride(buffer->width * 4); + builder->set_format(Gdk::MemoryFormat::R8G8B8A8); + + auto texture = builder->build(); + + screenshot.set_paintable(texture); + ext_image_copy_capture_session_v1_destroy(recording_session); +} + /* Gtk Overlay showing information about a window */ WayfireChooserTopLevel::WayfireChooserTopLevel(ext_foreign_toplevel_handle_v1 *handle) { @@ -65,6 +305,8 @@ WayfireChooserTopLevel::WayfireChooserTopLevel(ext_foreign_toplevel_handle_v1 *h label.set_max_width_chars(40); ext_foreign_toplevel_handle_v1_add_listener(handle, &listener, this); + grab_toplevel_screenshot(handle); + WayfireStreamChooserApp::getInstance().toplevel_done = true; } void WayfireChooserTopLevel::set_title(std::string title) diff --git a/src/stream-chooser/toplevelwidget.hpp b/src/stream-chooser/toplevelwidget.hpp index ef0bd1b3..09201bbd 100644 --- a/src/stream-chooser/toplevelwidget.hpp +++ b/src/stream-chooser/toplevelwidget.hpp @@ -7,7 +7,7 @@ class WayfireChooserTopLevel : public Gtk::Box private: Gtk::Overlay overlay; Gtk::Image icon; - Gtk::Image screenshot; + Gtk::Picture screenshot; Gtk::Label label; std::string buffered_title = "", title = ""; @@ -22,5 +22,6 @@ class WayfireChooserTopLevel : public Gtk::Box void set_app_id(std::string app_id); void set_title(std::string title); void set_identifier(std::string identifier); + void grab_toplevel_screenshot(ext_foreign_toplevel_handle_v1 *toplevel); void print(); }; From 214d023c6044c96f3dd9fd5cbe61e28286b43593 Mon Sep 17 00:00:00 2001 From: trigg Date: Sat, 2 May 2026 00:07:14 +0100 Subject: [PATCH 06/12] xdpw: rearrange loops and merge upstream changes --- src/stream-chooser/mainlayout.cpp | 29 ++++ src/stream-chooser/mainlayout.hpp | 19 +++ src/stream-chooser/meson.build | 8 +- src/stream-chooser/outputwidget.cpp | 3 + src/stream-chooser/outputwidget.hpp | 3 +- src/stream-chooser/stream-chooser.cpp | 76 ++++++--- src/stream-chooser/stream-chooser.hpp | 8 +- src/stream-chooser/toplevellayout.cpp | 41 +++++ src/stream-chooser/toplevellayout.hpp | 19 +++ src/stream-chooser/toplevelwidget.cpp | 217 +++++++++++++++----------- src/stream-chooser/toplevelwidget.hpp | 26 ++- 11 files changed, 330 insertions(+), 119 deletions(-) create mode 100644 src/stream-chooser/mainlayout.cpp create mode 100644 src/stream-chooser/mainlayout.hpp create mode 100644 src/stream-chooser/toplevellayout.cpp create mode 100644 src/stream-chooser/toplevellayout.hpp diff --git a/src/stream-chooser/mainlayout.cpp b/src/stream-chooser/mainlayout.cpp new file mode 100644 index 00000000..9e62ca32 --- /dev/null +++ b/src/stream-chooser/mainlayout.cpp @@ -0,0 +1,29 @@ +#include "mainlayout.hpp" +void MainLayout::allocate_vfunc(const Gtk::Widget& widget, int width, int height, int baseline) +{ + Gtk::Widget& widget_not_const = const_cast(widget); + + auto inner = widget_not_const.get_children()[0]; + auto alloc = Gtk::Allocation(); + alloc.set_height(height / 2); + alloc.set_width(width / 2); + alloc.set_y(height / 4); + alloc.set_x(width / 4); + inner->size_allocate(alloc, -1); +} + +void MainLayout::measure_vfunc(const Gtk::Widget& widget, Gtk::Orientation orientation, + int for_size, int& minimum, int& natural, int& minimum_baseline, + int& natural_baseline) const +{ + // Answer 1:1 aspect ratio + minimum = std::min(for_size, 300); + natural = std::min(for_size, 2000); + minimum_baseline = -1; + natural_baseline = -1; +} + +Gtk::SizeRequestMode MainLayout::get_request_mode_vfunc(const Gtk::Widget& widget) const +{ + return Gtk::SizeRequestMode::WIDTH_FOR_HEIGHT; +} diff --git a/src/stream-chooser/mainlayout.hpp b/src/stream-chooser/mainlayout.hpp new file mode 100644 index 00000000..6696a8ea --- /dev/null +++ b/src/stream-chooser/mainlayout.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + + +class MainLayout : public Gtk::LayoutManager +{ + protected: + void allocate_vfunc(const Gtk::Widget& widget, int width, int height, int baseline) override; + void measure_vfunc(const Gtk::Widget& widget, Gtk::Orientation orientation, int for_size, int& minimum, + int& natural, int& minimum_baseline, int& natural_baseline) const override; + Gtk::SizeRequestMode get_request_mode_vfunc(const Gtk::Widget& widget) const override; + std::shared_ptr layout; + + public: + MainLayout() + {} +}; diff --git a/src/stream-chooser/meson.build b/src/stream-chooser/meson.build index 90343525..1bec648c 100644 --- a/src/stream-chooser/meson.build +++ b/src/stream-chooser/meson.build @@ -7,7 +7,13 @@ deps = [ executable( 'wf-stream-chooser', - ['stream-chooser.cpp', 'toplevelwidget.cpp', 'outputwidget.cpp'], + [ + 'stream-chooser.cpp', + 'toplevelwidget.cpp', + 'outputwidget.cpp', + 'toplevellayout.cpp', + 'mainlayout.cpp', + ], dependencies: deps, install: true, ) \ No newline at end of file diff --git a/src/stream-chooser/outputwidget.cpp b/src/stream-chooser/outputwidget.cpp index 6220b28d..1e078e54 100644 --- a/src/stream-chooser/outputwidget.cpp +++ b/src/stream-chooser/outputwidget.cpp @@ -8,6 +8,9 @@ WayfireChooserOutput::WayfireChooserOutput(std::shared_ptr output) append(contents); append(model); append(connector); + set_size_request(150, 150); + set_valign(Gtk::Align::FILL); + set_halign(Gtk::Align::FILL); /* TODO Contents. We should probably grab screenshots of each output and display them */ diff --git a/src/stream-chooser/outputwidget.hpp b/src/stream-chooser/outputwidget.hpp index e96b6386..8c489bed 100644 --- a/src/stream-chooser/outputwidget.hpp +++ b/src/stream-chooser/outputwidget.hpp @@ -1,11 +1,12 @@ #pragma once +#include "gtkmm/picture.h" #include #include class WayfireChooserOutput : public Gtk::Box { Gtk::Label connector, model; - Gtk::Image contents; + Gtk::Picture contents; std::shared_ptr output; diff --git a/src/stream-chooser/stream-chooser.cpp b/src/stream-chooser/stream-chooser.cpp index ce5e4a78..93145c5b 100644 --- a/src/stream-chooser/stream-chooser.cpp +++ b/src/stream-chooser/stream-chooser.cpp @@ -5,11 +5,10 @@ #include "stream-chooser.hpp" #include "gtkmm/enums.h" +#include "mainlayout.hpp" #include "outputwidget.hpp" #include "toplevelwidget.hpp" -#define EXT_IMAGE_COPY_CAPTURE_V1 "ext-image-copy-capture-v1" - /* Static callbacks for toplevel list object */ static void handle_toplevel(void *data, struct ext_foreign_toplevel_list_v1 *list, @@ -36,6 +35,7 @@ static void registry_add_object(void *data, wl_registry *registry, uint32_t name { if (strcmp(interface, ext_foreign_toplevel_list_v1_interface.name) == 0) { + std::cout << "Got Toplevel list" << std::endl; auto list = (ext_foreign_toplevel_list_v1*) wl_registry_bind(registry, name, &ext_foreign_toplevel_list_v1_interface, @@ -46,19 +46,22 @@ static void registry_add_object(void *data, wl_registry *registry, uint32_t name &toplevel_list_v1_impl, NULL); } else if (strcmp(interface, ext_image_copy_capture_manager_v1_interface.name) == 0) { - /* We need this to exist, but we're not using it directly */ - auto manager = (ext_image_copy_capture_manager_v1*) wl_registry_bind(registry, name, + std::cout << "Got Copy Capture Manager" << std::endl; + auto manager = (ext_image_copy_capture_manager_v1*)wl_registry_bind(registry, name, &ext_image_copy_capture_manager_v1_interface, version); WayfireStreamChooserApp::getInstance().has_image_copy_capture = true; WayfireStreamChooserApp::getInstance().set_copy_capture_manager(manager); } else if (strcmp(interface, wl_shm_interface.name) == 0) { - auto shm = (wl_shm*) wl_registry_bind(registry, name, &wl_shm_interface, 1); + std::cout << "Got SHM" << std::endl; + auto shm = (wl_shm*)wl_registry_bind(registry, name, &wl_shm_interface, 1); WayfireStreamChooserApp::getInstance().set_shm(shm); } else if (strcmp(interface, ext_foreign_toplevel_image_capture_source_manager_v1_interface.name) == 0) { - auto toplevel_capture_manager = (ext_foreign_toplevel_image_capture_source_manager_v1*) wl_registry_bind(registry, name, - &ext_foreign_toplevel_image_capture_source_manager_v1_interface, version); + std::cout << "Got Capture Source Manager" << std::endl; + auto toplevel_capture_manager = + (ext_foreign_toplevel_image_capture_source_manager_v1*)wl_registry_bind(registry, name, + &ext_foreign_toplevel_image_capture_source_manager_v1_interface, version); WayfireStreamChooserApp::getInstance().set_toplevel_capture_manager(toplevel_capture_manager); } } @@ -81,13 +84,14 @@ WayfireStreamChooserApp::WayfireStreamChooserApp() : Gtk::Application("org.wayfi void WayfireStreamChooserApp::activate() { window.add_css_class("stream-chooser"); - main.set_size_request(300, 300); + window.set_size_request(300, 300); add_window(window); window.set_child(main); - main.set_valign(Gtk::Align::CENTER); - main.set_halign(Gtk::Align::CENTER); - main.set_vexpand(false); - main.set_hexpand(false); + layout = std::make_shared(); + window.set_layout_manager(layout); + main.add_css_class("main-chooser"); + main.set_valign(Gtk::Align::FILL); + main.set_halign(Gtk::Align::FILL); main.append(header); main.append(notebook); main.append(buttons); @@ -97,9 +101,23 @@ void WayfireStreamChooserApp::activate() Gtk::StyleContext::add_provider_for_display(window_display, css_provider, GTK_STYLE_PROVIDER_PRIORITY_USER); + scroll_window.set_child(window_list); + scroll_screen.set_child(screen_list); + + scroll_window.set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC); + scroll_screen.set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC); + + window_list.set_homogeneous(true); + screen_list.set_homogeneous(true); + + window_list.set_halign(Gtk::Align::START); + window_list.set_valign(Gtk::Align::START); + screen_list.set_halign(Gtk::Align::START); + screen_list.set_valign(Gtk::Align::START); + notebook.set_expand(true); - notebook.append_page(window_list, window_label); - notebook.append_page(screen_list, screen_label); + notebook.append_page(scroll_window, window_label); + notebook.append_page(scroll_screen, screen_label); main.set_orientation(Gtk::Orientation::VERTICAL); @@ -173,6 +191,12 @@ void WayfireStreamChooserApp::activate() std::cerr << "Compositor has not advertised ext-foreign-toplevel-list-v1" << std::endl; } + if (!has_image_capture_source) + { + failed = true; + std::cerr << "Compositor has not advertised ext-image-capture-source-v1" << std::endl; + } + if (failed) { window_label.set_sensitive(false); @@ -204,15 +228,20 @@ void WayfireStreamChooserApp::activate() add_output(obj); } - gtk_layer_init_for_window(window.gobj()); - gtk_layer_set_namespace(window.gobj(), "chooser"); - gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_TOP, true); - gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_BOTTOM, true); - gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_LEFT, true); - gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_RIGHT, true); + auto debug = Glib::getenv("WF_CHOOSER_DEBUG"); + if (debug != "1") + { + gtk_layer_init_for_window(window.gobj()); + gtk_layer_set_namespace(window.gobj(), "chooser"); + gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_TOP, true); + gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_BOTTOM, true); + gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_LEFT, true); + gtk_layer_set_anchor(window.gobj(), GTK_LAYER_SHELL_EDGE_RIGHT, true); + + gtk_layer_set_keyboard_mode(window.gobj(), GTK_LAYER_SHELL_KEYBOARD_MODE_EXCLUSIVE); + gtk_layer_set_exclusive_zone(window.gobj(), 0); + } - gtk_layer_set_keyboard_mode(window.gobj(), GTK_LAYER_SHELL_KEYBOARD_MODE_EXCLUSIVE); - gtk_layer_set_exclusive_zone(window.gobj(), 0); window.present(); } @@ -266,7 +295,8 @@ void WayfireStreamChooserApp::set_copy_capture_manager(ext_image_copy_capture_ma this->manager = manager; } -void WayfireStreamChooserApp::set_toplevel_capture_manager(ext_foreign_toplevel_image_capture_source_manager_v1 *toplevel_capture_manager) +void WayfireStreamChooserApp::set_toplevel_capture_manager( + ext_foreign_toplevel_image_capture_source_manager_v1 *toplevel_capture_manager) { this->toplevel_capture_manager = toplevel_capture_manager; } diff --git a/src/stream-chooser/stream-chooser.hpp b/src/stream-chooser/stream-chooser.hpp index d7d9fa8e..82e4b1d4 100644 --- a/src/stream-chooser/stream-chooser.hpp +++ b/src/stream-chooser/stream-chooser.hpp @@ -5,6 +5,7 @@ #include #include +#include "mainlayout.hpp" #include "outputwidget.hpp" #include "toplevelwidget.hpp" @@ -21,15 +22,19 @@ class WayfireStreamChooserApp : public Gtk::Application Gtk::FlowBox window_list, screen_list; Gtk::Button done, cancel; + Gtk::ScrolledWindow scroll_window, scroll_screen; WayfireStreamChooserApp(); wl_display *display; wl_registry *registry; ext_foreign_toplevel_list_v1 *list; + Glib::RefPtr layout; public: bool has_foreign_toplevel_list = false; bool has_image_copy_capture = false; + bool has_image_capture_source = false; + wl_shm *shm; ext_image_copy_capture_manager_v1 *manager; ext_foreign_toplevel_image_capture_source_manager_v1 *toplevel_capture_manager; @@ -46,7 +51,8 @@ class WayfireStreamChooserApp : public Gtk::Application void set_shm(wl_shm *shm); void set_toplevel_list(ext_foreign_toplevel_list_v1 *list); void set_copy_capture_manager(ext_image_copy_capture_manager_v1 *manager); - void set_toplevel_capture_manager(ext_foreign_toplevel_image_capture_source_manager_v1 *toplevel_capture_manager); + void set_toplevel_capture_manager( + ext_foreign_toplevel_image_capture_source_manager_v1 *toplevel_capture_manager); void add_toplevel(ext_foreign_toplevel_handle_v1 *handle); void remove_toplevel(WayfireChooserTopLevel *widget); diff --git a/src/stream-chooser/toplevellayout.cpp b/src/stream-chooser/toplevellayout.cpp new file mode 100644 index 00000000..41162c26 --- /dev/null +++ b/src/stream-chooser/toplevellayout.cpp @@ -0,0 +1,41 @@ +#include "toplevellayout.hpp" +void ToplevelLayout::allocate_vfunc(const Gtk::Widget& widget, int width, int height, int baseline) +{ + Gtk::Widget& widget_not_const = const_cast(widget); + + auto overlay = widget_not_const.get_children()[0]; + auto label = widget_not_const.get_children()[1]; + + /* Label */ + auto label_height = std::max(16, label->get_height()); + auto l_alloc = Gtk::Allocation(); + l_alloc.set_height(label_height); + l_alloc.set_width(width); + l_alloc.set_y(0); + l_alloc.set_x(0); + label->size_allocate(l_alloc, -1); + + /* Overlay */ + auto o_alloc = Gtk::Allocation(); + o_alloc.set_height(height - label_height); + o_alloc.set_width(width); + o_alloc.set_y(label_height); + o_alloc.set_x(0); + overlay->size_allocate(o_alloc, -1); +} + +void ToplevelLayout::measure_vfunc(const Gtk::Widget& widget, Gtk::Orientation orientation, + int for_size, int& minimum, int& natural, int& minimum_baseline, + int& natural_baseline) const +{ + // Answer 1:1 aspect ratio + minimum = for_size; + natural = for_size; + minimum_baseline = -1; + natural_baseline = -1; +} + +Gtk::SizeRequestMode ToplevelLayout::get_request_mode_vfunc(const Gtk::Widget& widget) const +{ + return Gtk::SizeRequestMode::WIDTH_FOR_HEIGHT; +} diff --git a/src/stream-chooser/toplevellayout.hpp b/src/stream-chooser/toplevellayout.hpp new file mode 100644 index 00000000..871c602b --- /dev/null +++ b/src/stream-chooser/toplevellayout.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + + +class ToplevelLayout : public Gtk::LayoutManager +{ + protected: + void allocate_vfunc(const Gtk::Widget& widget, int width, int height, int baseline) override; + void measure_vfunc(const Gtk::Widget& widget, Gtk::Orientation orientation, int for_size, int& minimum, + int& natural, int& minimum_baseline, int& natural_baseline) const override; + Gtk::SizeRequestMode get_request_mode_vfunc(const Gtk::Widget& widget) const override; + std::shared_ptr layout; + + public: + ToplevelLayout() + {} +}; diff --git a/src/stream-chooser/toplevelwidget.cpp b/src/stream-chooser/toplevelwidget.cpp index 86b39f78..e227554b 100644 --- a/src/stream-chooser/toplevelwidget.cpp +++ b/src/stream-chooser/toplevelwidget.cpp @@ -2,36 +2,34 @@ #include #include +#include "ext-foreign-toplevel-list-v1-client-protocol.h" +#include "ext-image-copy-capture-v1-client-protocol.h" +#include "glib.h" +#include "glibmm/main.h" #include "stream-chooser.hpp" +#include "toplevellayout.hpp" #include "toplevelwidget.hpp" -struct toplevel_buffer -{ - int width; - int height; - void *data; - wl_buffer *buffer; - size_t size = 0; - ext_image_copy_capture_frame_v1 *frame = NULL; -}; +/* Toplevel Callbacks */ -static void handle_closed(void *data, +static void toplevel_handle_closed(void *data, struct ext_foreign_toplevel_handle_v1 *handle) { WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; WayfireStreamChooserApp::getInstance().remove_toplevel(toplevel); - - /* TODO Clean up */ + ext_foreign_toplevel_handle_v1_destroy(handle); + printf("%s\n", __func__); } -static void handle_done(void *data, +static void toplevel_handle_done(void *data, struct ext_foreign_toplevel_handle_v1 *handle) { WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; toplevel->commit(); + printf("%s\n", __func__); } -static void handle_title(void *data, +static void toplevel_handle_title(void *data, struct ext_foreign_toplevel_handle_v1 *ext_foreign_toplevel_handle_v1, const char *title) { @@ -39,7 +37,7 @@ static void handle_title(void *data, toplevel->set_title(title); } -static void handle_app_id(void *data, +static void toplevel_handle_app_id(void *data, struct ext_foreign_toplevel_handle_v1 *handle1, const char *app_id) { @@ -47,7 +45,7 @@ static void handle_app_id(void *data, toplevel->set_app_id(app_id); } -static void handle_identifier(void *data, +static void toplevel_handle_identifier(void *data, struct ext_foreign_toplevel_handle_v1 *ext_foreign_toplevel_handle_v1, const char *identifier) { @@ -57,13 +55,14 @@ static void handle_identifier(void *data, ext_foreign_toplevel_handle_v1_listener listener = { - .closed = handle_closed, - .done = handle_done, - .title = handle_title, - .app_id = handle_app_id, - .identifier = handle_identifier, + .closed = toplevel_handle_closed, + .done = toplevel_handle_done, + .title = toplevel_handle_title, + .app_id = toplevel_handle_app_id, + .identifier = toplevel_handle_identifier, }; +/* SHM Callbacks*/ static int backingfile(off_t size) { static int count; @@ -77,7 +76,9 @@ static int backingfile(off_t size) } int ret; - while ((ret = ftruncate(fd, size)) == EINTR); + while ((ret = ftruncate(fd, size)) == EINTR) + {} + if (ret < 0) { perror("ret < 0"); @@ -131,112 +132,138 @@ void free_shm_buffer(std::shared_ptr& buffer) buffer->buffer = NULL; } -static int current_buffer_width, current_buffer_height, current_buffer_format; - -static void handle_buffer_size(void *, - struct ext_image_copy_capture_session_v1 *, +static void session_handle_buffer_size(void *data, + struct ext_image_copy_capture_session_v1*, uint32_t width, uint32_t height) { - current_buffer_width = width; - current_buffer_height = height; + WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; + + printf("%s : %d %d\n", __func__, width, height); + + toplevel->current_buffer_width = width; + toplevel->current_buffer_height = height; } -static void handle_shm_format(void *, - struct ext_image_copy_capture_session_v1 *, +static void session_handle_shm_format(void *data, + struct ext_image_copy_capture_session_v1*, uint32_t format) { - current_buffer_format = format; -} + WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; -static void handle_dmabuf_device(void *, - struct ext_image_copy_capture_session_v1 *, - struct wl_array *) -{ + printf("%s : %d\n", __func__, format); + + toplevel->current_buffer_format = format; } -static void handle_dmabuf_format(void *, - struct ext_image_copy_capture_session_v1 *, +static void session_handle_dmabuf_device(void*, + struct ext_image_copy_capture_session_v1*, + struct wl_array*) +{} + +static void session_handle_dmabuf_format(void*, + struct ext_image_copy_capture_session_v1*, uint32_t, - struct wl_array *) -{ -} + struct wl_array*) +{} -static void handle_done(void *, - struct ext_image_copy_capture_session_v1 *) +static void session_handle_done(void *data, + struct ext_image_copy_capture_session_v1*) { + WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; + toplevel->size(); + + printf("%s\n", __func__); } -static void handle_stopped(void *, - struct ext_image_copy_capture_session_v1 *) +static void session_handle_stopped(void*, + struct ext_image_copy_capture_session_v1 *session) { + printf("%s\n", __func__); + ext_image_copy_capture_session_v1_destroy(session); } static const struct ext_image_copy_capture_session_v1_listener recording_session_listener = { - .buffer_size = handle_buffer_size, - .shm_format = handle_shm_format, - .dmabuf_device = handle_dmabuf_device, - .dmabuf_format = handle_dmabuf_format, - .done = handle_done, - .stopped = handle_stopped, + .buffer_size = session_handle_buffer_size, + .shm_format = session_handle_shm_format, + .dmabuf_device = session_handle_dmabuf_device, + .dmabuf_format = session_handle_dmabuf_format, + .done = session_handle_done, + .stopped = session_handle_stopped, }; -static void frame_handle_transform(void *, - struct ext_image_copy_capture_frame_v1 *, +/* Copy Capture Callbacks */ + +static void frame_handle_transform(void*, + struct ext_image_copy_capture_frame_v1*, uint32_t) -{ -} +{} -static void frame_handle_damage(void *, - struct ext_image_copy_capture_frame_v1 *, +static void frame_handle_damage(void*, + struct ext_image_copy_capture_frame_v1*, int32_t, int32_t, int32_t, int32_t) {} -static void frame_handle_presentation_time(void *, - struct ext_image_copy_capture_frame_v1 *, +static void frame_handle_presentation_time(void*, + struct ext_image_copy_capture_frame_v1*, uint32_t, uint32_t, uint32_t) {} -static bool buffer_copy_done = false; -static void frame_handle_ready(void *, - struct ext_image_copy_capture_frame_v1 *) +static void frame_handle_ready(void *data, + struct ext_image_copy_capture_frame_v1*) { - buffer_copy_done = true; + WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; + toplevel->buffer_ready(); } -static void frame_handle_failed(void *, - struct ext_image_copy_capture_frame_v1 *, +static void frame_handle_failed(void*, + struct ext_image_copy_capture_frame_v1 *handle, uint32_t reason) { std::cerr << "Failed to copy frame because reason: " << reason << std::endl; + ext_image_copy_capture_frame_v1_destroy(handle); } static const struct ext_image_copy_capture_frame_v1_listener frame_listener = { .transform = frame_handle_transform, - .damage = frame_handle_damage, + .damage = frame_handle_damage, .presentation_time = frame_handle_presentation_time, - .ready = frame_handle_ready, + .ready = frame_handle_ready, .failed = frame_handle_failed, }; -static std::vector> toplevel_buffers; -static ext_image_copy_capture_frame_v1 *frame = NULL; -static ext_image_copy_capture_session_v1 *recording_session = NULL; +void WayfireChooserTopLevel::grab_toplevel_screenshot() +{ + printf("%s: %p : %p\n", __func__, handle, + WayfireStreamChooserApp::getInstance().toplevel_capture_manager); + auto copy_capture_source = ext_foreign_toplevel_image_capture_source_manager_v1_create_source( + WayfireStreamChooserApp::getInstance().toplevel_capture_manager, handle); + recording_session = ext_image_copy_capture_manager_v1_create_session( + WayfireStreamChooserApp::getInstance().manager, copy_capture_source, + EXT_IMAGE_COPY_CAPTURE_MANAGER_V1_OPTIONS_PAINT_CURSORS); + ext_image_copy_capture_session_v1_add_listener(recording_session, &recording_session_listener, this); +} -static std::shared_ptr request_frame() +void WayfireChooserTopLevel::size() { - auto buffer = std::make_shared(); + if ((current_buffer_width <= 0) || (current_buffer_height <= 0)) + { + printf("%s invalid size\n", __func__); + return; + } + + buffer = std::make_shared(); if (frame) { ext_image_copy_capture_frame_v1_destroy(frame); } - buffer->width = current_buffer_width; + buffer->width = current_buffer_width; buffer->height = current_buffer_height; frame = ext_image_copy_capture_session_v1_create_frame(recording_session); buffer->frame = frame; - ext_image_copy_capture_frame_v1_add_listener(buffer->frame, &frame_listener, &buffer); + ext_image_copy_capture_frame_v1_add_listener(buffer->frame, &frame_listener, this); free_shm_buffer(buffer); buffer->buffer = @@ -244,28 +271,24 @@ static std::shared_ptr request_frame() if (buffer->buffer == NULL) { - std::cerr << "Failed to create buffer" << std::endl; + printf("%s failed to create buffer\n", __func__); exit(EXIT_FAILURE); } ext_image_copy_capture_frame_v1_attach_buffer(buffer->frame, buffer->buffer); ext_image_copy_capture_frame_v1_damage_buffer(buffer->frame, 0, 0, buffer->width, buffer->height); ext_image_copy_capture_frame_v1_capture(buffer->frame); - - return buffer; } -void WayfireChooserTopLevel::grab_toplevel_screenshot(ext_foreign_toplevel_handle_v1 *toplevel) -{printf("%s: %p\n", __func__, toplevel); - auto copy_capture_source = ext_foreign_toplevel_image_capture_source_manager_v1_create_source(WayfireStreamChooserApp::getInstance().toplevel_capture_manager, toplevel); - recording_session = ext_image_copy_capture_manager_v1_create_session(WayfireStreamChooserApp::getInstance().manager, copy_capture_source, EXT_IMAGE_COPY_CAPTURE_MANAGER_V1_OPTIONS_PAINT_CURSORS); - ext_image_copy_capture_session_v1_add_listener(recording_session, &recording_session_listener, NULL); - while ((current_buffer_width <= 0 || current_buffer_height <= 0) && wl_display_dispatch(gdk_wayland_display_get_wl_display(Gdk::Display::get_default()->gobj())) != -1); - auto buffer = request_frame(); - buffer_copy_done = false; - while (!buffer_copy_done && wl_display_dispatch(gdk_wayland_display_get_wl_display(Gdk::Display::get_default()->gobj())) != -1); - buffer->width = current_buffer_width; - buffer->height = current_buffer_height; +void WayfireChooserTopLevel::buffer_ready() +{ + if ((buffer == nullptr) || (buffer->buffer == nullptr)) + { + printf("%s buffer null\n", __func__); + + return; + } + /* buffer->data is now valid */ std::shared_ptr bytes = 0; size_t size = buffer->size; @@ -289,24 +312,30 @@ void WayfireChooserTopLevel::grab_toplevel_screenshot(ext_foreign_toplevel_handl auto texture = builder->build(); screenshot.set_paintable(texture); + ext_image_copy_capture_frame_v1_destroy(frame); ext_image_copy_capture_session_v1_destroy(recording_session); } /* Gtk Overlay showing information about a window */ -WayfireChooserTopLevel::WayfireChooserTopLevel(ext_foreign_toplevel_handle_v1 *handle) +WayfireChooserTopLevel::WayfireChooserTopLevel(ext_foreign_toplevel_handle_v1 *handle) : handle(handle) { + set_size_request(150, 150); + set_valign(Gtk::Align::FILL); + set_halign(Gtk::Align::FILL); + layout = std::make_shared(); + set_layout_manager(layout); append(overlay); append(label); overlay.set_child(screenshot); overlay.add_overlay(icon); icon.set_halign(Gtk::Align::START); icon.set_valign(Gtk::Align::END); + screenshot.set_halign(Gtk::Align::FILL); + screenshot.set_valign(Gtk::Align::FILL); label.set_ellipsize(Pango::EllipsizeMode::MIDDLE); label.set_max_width_chars(40); ext_foreign_toplevel_handle_v1_add_listener(handle, &listener, this); - grab_toplevel_screenshot(handle); - WayfireStreamChooserApp::getInstance().toplevel_done = true; } void WayfireChooserTopLevel::set_title(std::string title) @@ -345,6 +374,12 @@ void WayfireChooserTopLevel::commit() identifier = buffered_identifier; buffered_identifier = ""; } + + /* If we have the protocols, grab a screenshot */ + if (WayfireStreamChooserApp::getInstance().has_image_copy_capture) + { + grab_toplevel_screenshot(); + } } WayfireChooserTopLevel::~WayfireChooserTopLevel() diff --git a/src/stream-chooser/toplevelwidget.hpp b/src/stream-chooser/toplevelwidget.hpp index 09201bbd..647b3e34 100644 --- a/src/stream-chooser/toplevelwidget.hpp +++ b/src/stream-chooser/toplevelwidget.hpp @@ -1,6 +1,19 @@ #pragma once #include +#include #include "ext-foreign-toplevel-list-v1-client-protocol.h" +#include "ext-image-copy-capture-v1-client-protocol.h" +#include "toplevellayout.hpp" + +struct toplevel_buffer +{ + int width = 0; + int height = 0; + void *data; + wl_buffer *buffer; + size_t size = 0; + ext_image_copy_capture_frame_v1 *frame = NULL; +}; class WayfireChooserTopLevel : public Gtk::Box { @@ -14,14 +27,23 @@ class WayfireChooserTopLevel : public Gtk::Box std::string buffered_app_id = "", app_id = ""; std::string buffered_identifier = "", identifier = ""; + std::shared_ptr buffer = nullptr; + void request_frame(); + ext_image_copy_capture_frame_v1 *frame = NULL; + ext_image_copy_capture_session_v1 *recording_session = NULL; + Glib::RefPtr layout; + public: - ext_foreign_toplevel_handle_v1 *handle; + int current_buffer_width = 0, current_buffer_height = 0, current_buffer_format = 0; + ext_foreign_toplevel_handle_v1 *handle = nullptr; WayfireChooserTopLevel(ext_foreign_toplevel_handle_v1 *handle); ~WayfireChooserTopLevel(); void commit(); void set_app_id(std::string app_id); void set_title(std::string title); void set_identifier(std::string identifier); - void grab_toplevel_screenshot(ext_foreign_toplevel_handle_v1 *toplevel); + void grab_toplevel_screenshot(); + void size(); + void buffer_ready(); void print(); }; From cd297887a14d40a869a726bd64f7ac131de88113 Mon Sep 17 00:00:00 2001 From: trigg Date: Sat, 2 May 2026 01:06:38 +0100 Subject: [PATCH 07/12] xdpw: remove cursor xdpw: fix some double frees --- src/stream-chooser/toplevelwidget.cpp | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/stream-chooser/toplevelwidget.cpp b/src/stream-chooser/toplevelwidget.cpp index e227554b..8958a4e0 100644 --- a/src/stream-chooser/toplevelwidget.cpp +++ b/src/stream-chooser/toplevelwidget.cpp @@ -3,6 +3,7 @@ #include #include #include "ext-foreign-toplevel-list-v1-client-protocol.h" +#include "ext-image-capture-source-v1-client-protocol.h" #include "ext-image-copy-capture-v1-client-protocol.h" #include "glib.h" #include "glibmm/main.h" @@ -137,9 +138,6 @@ static void session_handle_buffer_size(void *data, uint32_t width, uint32_t height) { WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; - - printf("%s : %d %d\n", __func__, width, height); - toplevel->current_buffer_width = width; toplevel->current_buffer_height = height; } @@ -149,9 +147,6 @@ static void session_handle_shm_format(void *data, uint32_t format) { WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; - - printf("%s : %d\n", __func__, format); - toplevel->current_buffer_format = format; } @@ -171,8 +166,6 @@ static void session_handle_done(void *data, { WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; toplevel->size(); - - printf("%s\n", __func__); } static void session_handle_stopped(void*, @@ -236,11 +229,14 @@ void WayfireChooserTopLevel::grab_toplevel_screenshot() printf("%s: %p : %p\n", __func__, handle, WayfireStreamChooserApp::getInstance().toplevel_capture_manager); auto copy_capture_source = ext_foreign_toplevel_image_capture_source_manager_v1_create_source( - WayfireStreamChooserApp::getInstance().toplevel_capture_manager, handle); + WayfireStreamChooserApp::getInstance().toplevel_capture_manager, + handle); recording_session = ext_image_copy_capture_manager_v1_create_session( - WayfireStreamChooserApp::getInstance().manager, copy_capture_source, - EXT_IMAGE_COPY_CAPTURE_MANAGER_V1_OPTIONS_PAINT_CURSORS); + WayfireStreamChooserApp::getInstance().manager, + copy_capture_source, + 0); ext_image_copy_capture_session_v1_add_listener(recording_session, &recording_session_listener, this); + ext_image_capture_source_v1_destroy(copy_capture_source); } void WayfireChooserTopLevel::size() @@ -256,6 +252,7 @@ void WayfireChooserTopLevel::size() if (frame) { ext_image_copy_capture_frame_v1_destroy(frame); + frame = NULL; } buffer->width = current_buffer_width; @@ -313,7 +310,9 @@ void WayfireChooserTopLevel::buffer_ready() screenshot.set_paintable(texture); ext_image_copy_capture_frame_v1_destroy(frame); + frame = NULL; ext_image_copy_capture_session_v1_destroy(recording_session); + recording_session = NULL; } /* Gtk Overlay showing information about a window */ From 61b3f874e5a3acc65ca6049ac5fb64cf2090ed11 Mon Sep 17 00:00:00 2001 From: trigg Date: Sat, 2 May 2026 01:16:59 +0100 Subject: [PATCH 08/12] xdpw: get one image at a time --- src/stream-chooser/stream-chooser.hpp | 1 + src/stream-chooser/toplevelwidget.cpp | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/stream-chooser/stream-chooser.hpp b/src/stream-chooser/stream-chooser.hpp index 82e4b1d4..e0c3bd71 100644 --- a/src/stream-chooser/stream-chooser.hpp +++ b/src/stream-chooser/stream-chooser.hpp @@ -31,6 +31,7 @@ class WayfireStreamChooserApp : public Gtk::Application Glib::RefPtr layout; public: + bool is_in_use = false; bool has_foreign_toplevel_list = false; bool has_image_copy_capture = false; bool has_image_capture_source = false; diff --git a/src/stream-chooser/toplevelwidget.cpp b/src/stream-chooser/toplevelwidget.cpp index 8958a4e0..d5d481a4 100644 --- a/src/stream-chooser/toplevelwidget.cpp +++ b/src/stream-chooser/toplevelwidget.cpp @@ -173,6 +173,7 @@ static void session_handle_stopped(void*, { printf("%s\n", __func__); ext_image_copy_capture_session_v1_destroy(session); + session = NULL; } static const struct ext_image_copy_capture_session_v1_listener recording_session_listener = { @@ -226,6 +227,19 @@ static const struct ext_image_copy_capture_frame_v1_listener frame_listener = { void WayfireChooserTopLevel::grab_toplevel_screenshot() { + if (WayfireStreamChooserApp::getInstance().is_in_use) + { + Glib::signal_timeout().connect( + [this] () + { + grab_toplevel_screenshot(); + return G_SOURCE_REMOVE; + }, 100); + return; + } + + WayfireStreamChooserApp::getInstance().is_in_use = true; + printf("%s: %p : %p\n", __func__, handle, WayfireStreamChooserApp::getInstance().toplevel_capture_manager); auto copy_capture_source = ext_foreign_toplevel_image_capture_source_manager_v1_create_source( @@ -313,6 +327,7 @@ void WayfireChooserTopLevel::buffer_ready() frame = NULL; ext_image_copy_capture_session_v1_destroy(recording_session); recording_session = NULL; + WayfireStreamChooserApp::getInstance().is_in_use = false; } /* Gtk Overlay showing information about a window */ From 3f7c91804300ad77256df6baeebced885ac8d829 Mon Sep 17 00:00:00 2001 From: Scott Moreau Date: Fri, 1 May 2026 18:41:56 -0600 Subject: [PATCH 09/12] stream-chooser: Fix build Depend on wf-config. --- src/stream-chooser/meson.build | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stream-chooser/meson.build b/src/stream-chooser/meson.build index 1bec648c..d5b7ee1f 100644 --- a/src/stream-chooser/meson.build +++ b/src/stream-chooser/meson.build @@ -1,6 +1,7 @@ deps = [ gtkmm, wf_protos, + wfconfig, libutil, gtklayershell, ] @@ -16,4 +17,4 @@ executable( ], dependencies: deps, install: true, -) \ No newline at end of file +) From 7a95f4d962a7d1e87fecc320246889f2fe8a27a3 Mon Sep 17 00:00:00 2001 From: Scott Moreau Date: Fri, 1 May 2026 22:24:27 -0600 Subject: [PATCH 10/12] stream-chooser: Remove some debug statements --- src/stream-chooser/stream-chooser.cpp | 5 +---- src/stream-chooser/toplevelwidget.cpp | 6 ------ 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/stream-chooser/stream-chooser.cpp b/src/stream-chooser/stream-chooser.cpp index 93145c5b..5d438357 100644 --- a/src/stream-chooser/stream-chooser.cpp +++ b/src/stream-chooser/stream-chooser.cpp @@ -35,7 +35,6 @@ static void registry_add_object(void *data, wl_registry *registry, uint32_t name { if (strcmp(interface, ext_foreign_toplevel_list_v1_interface.name) == 0) { - std::cout << "Got Toplevel list" << std::endl; auto list = (ext_foreign_toplevel_list_v1*) wl_registry_bind(registry, name, &ext_foreign_toplevel_list_v1_interface, @@ -46,22 +45,20 @@ static void registry_add_object(void *data, wl_registry *registry, uint32_t name &toplevel_list_v1_impl, NULL); } else if (strcmp(interface, ext_image_copy_capture_manager_v1_interface.name) == 0) { - std::cout << "Got Copy Capture Manager" << std::endl; auto manager = (ext_image_copy_capture_manager_v1*)wl_registry_bind(registry, name, &ext_image_copy_capture_manager_v1_interface, version); WayfireStreamChooserApp::getInstance().has_image_copy_capture = true; WayfireStreamChooserApp::getInstance().set_copy_capture_manager(manager); } else if (strcmp(interface, wl_shm_interface.name) == 0) { - std::cout << "Got SHM" << std::endl; auto shm = (wl_shm*)wl_registry_bind(registry, name, &wl_shm_interface, 1); WayfireStreamChooserApp::getInstance().set_shm(shm); } else if (strcmp(interface, ext_foreign_toplevel_image_capture_source_manager_v1_interface.name) == 0) { - std::cout << "Got Capture Source Manager" << std::endl; auto toplevel_capture_manager = (ext_foreign_toplevel_image_capture_source_manager_v1*)wl_registry_bind(registry, name, &ext_foreign_toplevel_image_capture_source_manager_v1_interface, version); + WayfireStreamChooserApp::getInstance().has_image_capture_source = true; WayfireStreamChooserApp::getInstance().set_toplevel_capture_manager(toplevel_capture_manager); } } diff --git a/src/stream-chooser/toplevelwidget.cpp b/src/stream-chooser/toplevelwidget.cpp index d5d481a4..afc0b218 100644 --- a/src/stream-chooser/toplevelwidget.cpp +++ b/src/stream-chooser/toplevelwidget.cpp @@ -19,7 +19,6 @@ static void toplevel_handle_closed(void *data, WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; WayfireStreamChooserApp::getInstance().remove_toplevel(toplevel); ext_foreign_toplevel_handle_v1_destroy(handle); - printf("%s\n", __func__); } static void toplevel_handle_done(void *data, @@ -27,7 +26,6 @@ static void toplevel_handle_done(void *data, { WayfireChooserTopLevel *toplevel = (WayfireChooserTopLevel*)data; toplevel->commit(); - printf("%s\n", __func__); } static void toplevel_handle_title(void *data, @@ -94,7 +92,6 @@ static int backingfile(off_t size) static struct wl_buffer *create_shm_buffer(int width, int height, void **data_out, size_t *size) { *size = width * 4 * height; - printf("size: %ld\n", *size); int fd = backingfile(*size); if (fd < 0) @@ -171,7 +168,6 @@ static void session_handle_done(void *data, static void session_handle_stopped(void*, struct ext_image_copy_capture_session_v1 *session) { - printf("%s\n", __func__); ext_image_copy_capture_session_v1_destroy(session); session = NULL; } @@ -240,8 +236,6 @@ void WayfireChooserTopLevel::grab_toplevel_screenshot() WayfireStreamChooserApp::getInstance().is_in_use = true; - printf("%s: %p : %p\n", __func__, handle, - WayfireStreamChooserApp::getInstance().toplevel_capture_manager); auto copy_capture_source = ext_foreign_toplevel_image_capture_source_manager_v1_create_source( WayfireStreamChooserApp::getInstance().toplevel_capture_manager, handle); From 7fe2fa76c8844d728e3e1142917ccb5872d9dba3 Mon Sep 17 00:00:00 2001 From: trigg Date: Sat, 2 May 2026 15:02:53 +0100 Subject: [PATCH 11/12] xdpw: output screenshots --- src/stream-chooser/outputwidget.cpp | 269 ++++++++++++++++++++++++++ src/stream-chooser/outputwidget.hpp | 20 ++ src/stream-chooser/stream-chooser.cpp | 14 ++ src/stream-chooser/stream-chooser.hpp | 2 + src/stream-chooser/toplevelwidget.cpp | 8 +- src/stream-chooser/toplevelwidget.hpp | 1 - 6 files changed, 309 insertions(+), 5 deletions(-) diff --git a/src/stream-chooser/outputwidget.cpp b/src/stream-chooser/outputwidget.cpp index 1e078e54..de611176 100644 --- a/src/stream-chooser/outputwidget.cpp +++ b/src/stream-chooser/outputwidget.cpp @@ -1,8 +1,196 @@ #include +#include #include "outputwidget.hpp" +#include "gdk/wayland/gdkwayland.h" #include "stream-chooser.hpp" +/* Copy Capture Callbacks */ + +static void frame_handle_transform(void*, + struct ext_image_copy_capture_frame_v1*, + uint32_t) +{} + +static void frame_handle_damage(void*, + struct ext_image_copy_capture_frame_v1*, + int32_t, int32_t, int32_t, int32_t) +{} + +static void frame_handle_presentation_time(void*, + struct ext_image_copy_capture_frame_v1*, + uint32_t, uint32_t, uint32_t) +{} + +static void frame_handle_ready(void *data, + struct ext_image_copy_capture_frame_v1*) +{ + WayfireChooserOutput *output = (WayfireChooserOutput*)data; + output->buffer_ready(); +} + +static void frame_handle_failed(void*, + struct ext_image_copy_capture_frame_v1 *handle, + uint32_t reason) +{ + std::cerr << "Failed to copy frame because reason: " << reason << std::endl; + ext_image_copy_capture_frame_v1_destroy(handle); +} + +static const struct ext_image_copy_capture_frame_v1_listener frame_listener = { + .transform = frame_handle_transform, + .damage = frame_handle_damage, + .presentation_time = frame_handle_presentation_time, + .ready = frame_handle_ready, + .failed = frame_handle_failed, +}; + +/* SHM Callbacks*/ +static int backingfile(off_t size) +{ + static int count; + char name[128]; + sprintf(name, "/tmp/wf-stream-chooser-%d-XXXXXX", count++); + int fd = mkstemp(name); + if (fd < 0) + { + perror("mkstemp"); + return -1; + } + + int ret; + while ((ret = ftruncate(fd, size)) == EINTR) + {} + + if (ret < 0) + { + perror("ret < 0"); + close(fd); + return -1; + } + + unlink(name); + return fd; +} + +static struct wl_buffer *output_create_shm_buffer(int width, int height, void **data_out, size_t *size) +{ + *size = width * 4 * height; + + int fd = backingfile(*size); + if (fd < 0) + { + perror("Creating a buffer file failed"); + return NULL; + } + + void *data = mmap(NULL, *size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (data == MAP_FAILED) + { + perror("mmap failed"); + close(fd); + return NULL; + } + + wl_shm_pool *pool = wl_shm_create_pool(WayfireStreamChooserApp::getInstance().shm, fd, *size); + close(fd); + wl_buffer *buffer = wl_shm_pool_create_buffer(pool, 0, width, height, + width * 4, WL_SHM_FORMAT_ABGR8888); + wl_shm_pool_destroy(pool); + + *data_out = data; + return buffer; +} + +void output_free_shm_buffer(std::shared_ptr& buffer) +{ + if (buffer->buffer == NULL) + { + return; + } + + munmap(buffer->data, buffer->size); + wl_buffer_destroy(buffer->buffer); + buffer->buffer = NULL; +} + +static void session_handle_buffer_size(void *data, + struct ext_image_copy_capture_session_v1*, + uint32_t width, uint32_t height) +{ + WayfireChooserOutput *output = (WayfireChooserOutput*)data; + output->current_buffer_width = width; + output->current_buffer_height = height; +} + +static void session_handle_shm_format(void *data, + struct ext_image_copy_capture_session_v1*, + uint32_t format) +{ + WayfireChooserOutput *output = (WayfireChooserOutput*)data; + output->current_buffer_format = format; +} + +static void session_handle_dmabuf_device(void*, + struct ext_image_copy_capture_session_v1*, + struct wl_array*) +{} + +static void session_handle_dmabuf_format(void*, + struct ext_image_copy_capture_session_v1*, + uint32_t, + struct wl_array*) +{} + +static void session_handle_done(void *data, + struct ext_image_copy_capture_session_v1*) +{ + WayfireChooserOutput *output = (WayfireChooserOutput*)data; + output->size(); +} + +static void session_handle_stopped(void*, + struct ext_image_copy_capture_session_v1 *session) +{ + ext_image_copy_capture_session_v1_destroy(session); + session = NULL; +} + +static const struct ext_image_copy_capture_session_v1_listener recording_session_listener = { + .buffer_size = session_handle_buffer_size, + .shm_format = session_handle_shm_format, + .dmabuf_device = session_handle_dmabuf_device, + .dmabuf_format = session_handle_dmabuf_format, + .done = session_handle_done, + .stopped = session_handle_stopped, +}; + +void WayfireChooserOutput::grab_output_screenshot() +{ + if (WayfireStreamChooserApp::getInstance().is_in_use) + { + Glib::signal_timeout().connect( + [this] () + { + grab_output_screenshot(); + return G_SOURCE_REMOVE; + }, 100); + return; + } + + WayfireStreamChooserApp::getInstance().is_in_use = true; + + auto copy_capture_source = ext_output_image_capture_source_manager_v1_create_source( + WayfireStreamChooserApp::getInstance().output_capture_manager, + output_handle); + recording_session = ext_image_copy_capture_manager_v1_create_session( + WayfireStreamChooserApp::getInstance().manager, + copy_capture_source, + 0); + ext_image_copy_capture_session_v1_add_listener(recording_session, &recording_session_listener, this); + ext_image_capture_source_v1_destroy(copy_capture_source); +} + WayfireChooserOutput::WayfireChooserOutput(std::shared_ptr output) : output(output) { append(contents); @@ -12,6 +200,8 @@ WayfireChooserOutput::WayfireChooserOutput(std::shared_ptr output) set_valign(Gtk::Align::FILL); set_halign(Gtk::Align::FILL); + output_handle = gdk_wayland_monitor_get_wl_output(output->gobj()); + /* TODO Contents. We should probably grab screenshots of each output and display them */ model.set_label(output->get_model()); @@ -23,6 +213,8 @@ WayfireChooserOutput::WayfireChooserOutput(std::shared_ptr output) { WayfireStreamChooserApp::getInstance().remove_output(output->get_connector()); }); + + grab_output_screenshot(); } void WayfireChooserOutput::print() @@ -30,3 +222,80 @@ void WayfireChooserOutput::print() std::cout << "Monitor: " << output->get_connector() << std::endl; exit(0); } + +void WayfireChooserOutput::size() +{ + if ((current_buffer_width <= 0) || (current_buffer_height <= 0)) + { + printf("%s invalid size\n", __func__); + return; + } + + buffer = std::make_shared(); + + if (frame) + { + ext_image_copy_capture_frame_v1_destroy(frame); + frame = NULL; + } + + buffer->width = current_buffer_width; + buffer->height = current_buffer_height; + + frame = ext_image_copy_capture_session_v1_create_frame(recording_session); + buffer->frame = frame; + ext_image_copy_capture_frame_v1_add_listener(buffer->frame, &frame_listener, this); + + output_free_shm_buffer(buffer); + buffer->buffer = + output_create_shm_buffer(buffer->width, buffer->height, &buffer->data, &buffer->size); + + if (buffer->buffer == NULL) + { + printf("%s failed to create buffer\n", __func__); + exit(EXIT_FAILURE); + } + + ext_image_copy_capture_frame_v1_attach_buffer(buffer->frame, buffer->buffer); + ext_image_copy_capture_frame_v1_damage_buffer(buffer->frame, 0, 0, buffer->width, buffer->height); + ext_image_copy_capture_frame_v1_capture(buffer->frame); +} + +void WayfireChooserOutput::buffer_ready() +{ + if ((buffer == nullptr) || (buffer->buffer == nullptr)) + { + printf("%s buffer null\n", __func__); + + return; + } + + /* buffer->data is now valid */ + std::shared_ptr bytes = 0; + size_t size = buffer->size; + if (buffer->data) + { + bytes = Glib::Bytes::create(buffer->data, size); + } + + if (!bytes) + { + return; + } + + auto builder = Gdk::MemoryTextureBuilder::create(); + builder->set_bytes(bytes); + builder->set_width(buffer->width); + builder->set_height(buffer->height); + builder->set_stride(buffer->width * 4); + builder->set_format(Gdk::MemoryFormat::R8G8B8A8); + + auto texture = builder->build(); + + contents.set_paintable(texture); + ext_image_copy_capture_frame_v1_destroy(frame); + frame = NULL; + ext_image_copy_capture_session_v1_destroy(recording_session); + recording_session = NULL; + WayfireStreamChooserApp::getInstance().is_in_use = false; +} diff --git a/src/stream-chooser/outputwidget.hpp b/src/stream-chooser/outputwidget.hpp index 8c489bed..b53ee086 100644 --- a/src/stream-chooser/outputwidget.hpp +++ b/src/stream-chooser/outputwidget.hpp @@ -2,15 +2,35 @@ #include "gtkmm/picture.h" #include #include +#include +#include "ext-image-copy-capture-v1-client-protocol.h" +struct output_buffer +{ + int width = 0; + int height = 0; + void *data; + wl_buffer *buffer; + size_t size = 0; + ext_image_copy_capture_frame_v1 *frame = NULL; +}; class WayfireChooserOutput : public Gtk::Box { Gtk::Label connector, model; Gtk::Picture contents; + wl_output *output_handle; std::shared_ptr output; + std::shared_ptr buffer = nullptr; + ext_image_copy_capture_frame_v1 *frame = NULL; + ext_image_copy_capture_session_v1 *recording_session = NULL; + void grab_output_screenshot(); public: void print(); + void size(); + void buffer_ready(); + WayfireChooserOutput(std::shared_ptr output); + int current_buffer_width = 0, current_buffer_height = 0, current_buffer_format = 0; }; diff --git a/src/stream-chooser/stream-chooser.cpp b/src/stream-chooser/stream-chooser.cpp index a7dcc6f7..5f7703d9 100644 --- a/src/stream-chooser/stream-chooser.cpp +++ b/src/stream-chooser/stream-chooser.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "stream-chooser.hpp" #include "gtkmm/enums.h" @@ -62,6 +63,13 @@ static void registry_add_object(void *data, wl_registry *registry, uint32_t name &ext_foreign_toplevel_image_capture_source_manager_v1_interface, version); WayfireStreamChooserApp::getInstance().has_image_capture_source = true; WayfireStreamChooserApp::getInstance().set_toplevel_capture_manager(toplevel_capture_manager); + } else if (strcmp(interface, ext_output_image_capture_source_manager_v1_interface.name) == 0) + { + auto output_capture_manager = + (ext_output_image_capture_source_manager_v1*)wl_registry_bind(registry, name, + &ext_output_image_capture_source_manager_v1_interface, + version); + WayfireStreamChooserApp::getInstance().set_output_capture_manager(output_capture_manager); } } @@ -301,6 +309,12 @@ void WayfireStreamChooserApp::set_toplevel_capture_manager( this->toplevel_capture_manager = toplevel_capture_manager; } +void WayfireStreamChooserApp::set_output_capture_manager( + ext_output_image_capture_source_manager_v1 *output_capture_manager) +{ + this->output_capture_manager = output_capture_manager; +} + /* Starting point */ int main(int argc, char **argv) { diff --git a/src/stream-chooser/stream-chooser.hpp b/src/stream-chooser/stream-chooser.hpp index e0c3bd71..9aa1496f 100644 --- a/src/stream-chooser/stream-chooser.hpp +++ b/src/stream-chooser/stream-chooser.hpp @@ -39,6 +39,7 @@ class WayfireStreamChooserApp : public Gtk::Application wl_shm *shm; ext_image_copy_capture_manager_v1 *manager; ext_foreign_toplevel_image_capture_source_manager_v1 *toplevel_capture_manager; + ext_output_image_capture_source_manager_v1 *output_capture_manager; std::map> toplevels; std::map> outputs; @@ -54,6 +55,7 @@ class WayfireStreamChooserApp : public Gtk::Application void set_copy_capture_manager(ext_image_copy_capture_manager_v1 *manager); void set_toplevel_capture_manager( ext_foreign_toplevel_image_capture_source_manager_v1 *toplevel_capture_manager); + void set_output_capture_manager(ext_output_image_capture_source_manager_v1 *output_capture_manager); void add_toplevel(ext_foreign_toplevel_handle_v1 *handle); void remove_toplevel(WayfireChooserTopLevel *widget); diff --git a/src/stream-chooser/toplevelwidget.cpp b/src/stream-chooser/toplevelwidget.cpp index afc0b218..7eac76ab 100644 --- a/src/stream-chooser/toplevelwidget.cpp +++ b/src/stream-chooser/toplevelwidget.cpp @@ -89,7 +89,7 @@ static int backingfile(off_t size) return fd; } -static struct wl_buffer *create_shm_buffer(int width, int height, void **data_out, size_t *size) +static struct wl_buffer *toplevel_create_shm_buffer(int width, int height, void **data_out, size_t *size) { *size = width * 4 * height; @@ -118,7 +118,7 @@ static struct wl_buffer *create_shm_buffer(int width, int height, void **data_ou return buffer; } -void free_shm_buffer(std::shared_ptr& buffer) +void toplevel_free_shm_buffer(std::shared_ptr& buffer) { if (buffer->buffer == NULL) { @@ -270,9 +270,9 @@ void WayfireChooserTopLevel::size() buffer->frame = frame; ext_image_copy_capture_frame_v1_add_listener(buffer->frame, &frame_listener, this); - free_shm_buffer(buffer); + toplevel_free_shm_buffer(buffer); buffer->buffer = - create_shm_buffer(buffer->width, buffer->height, &buffer->data, &buffer->size); + toplevel_create_shm_buffer(buffer->width, buffer->height, &buffer->data, &buffer->size); if (buffer->buffer == NULL) { diff --git a/src/stream-chooser/toplevelwidget.hpp b/src/stream-chooser/toplevelwidget.hpp index 0def7e5f..77c72580 100644 --- a/src/stream-chooser/toplevelwidget.hpp +++ b/src/stream-chooser/toplevelwidget.hpp @@ -29,7 +29,6 @@ class WayfireChooserTopLevel : public Gtk::Box Glib::RefPtr layout; std::shared_ptr buffer = nullptr; - void request_frame(); ext_image_copy_capture_frame_v1 *frame = NULL; ext_image_copy_capture_session_v1 *recording_session = NULL; From 74061eeabb7418b6217dcfe23d7dc305409db231 Mon Sep 17 00:00:00 2001 From: trigg Date: Sat, 2 May 2026 16:50:14 +0100 Subject: [PATCH 12/12] xdpw: Use IconProvider --- src/stream-chooser/toplevelwidget.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stream-chooser/toplevelwidget.cpp b/src/stream-chooser/toplevelwidget.cpp index 7eac76ab..7d2b9587 100644 --- a/src/stream-chooser/toplevelwidget.cpp +++ b/src/stream-chooser/toplevelwidget.cpp @@ -7,6 +7,7 @@ #include "ext-image-copy-capture-v1-client-protocol.h" #include "glib.h" #include "glibmm/main.h" +#include "gtk-utils.hpp" #include "stream-chooser.hpp" #include "toplevellayout.hpp" #include "toplevelwidget.hpp" @@ -366,7 +367,7 @@ void WayfireChooserTopLevel::commit() if (buffered_app_id != "") { app_id = buffered_app_id; - icon.set_from_icon_name(app_id); + IconProvider::image_set_icon(icon, app_id); buffered_app_id = ""; }