Untitled
unknown
plain_text
10 months ago
23 kB
7
Indexable
#include <memory>
#include <wayfire/plugin.hpp>
#include <wayfire/per-output-plugin.hpp>
#include <wayfire/output.hpp>
#include <wayfire/core.hpp>
#include <wayfire/view.hpp>
#include <wayfire/toplevel-view.hpp>
#include <wayfire/seat.hpp>
#include <wayfire/signal-definitions.hpp>
#include <wayfire/util/log.hpp>
#include <wayfire/view-transform.hpp>
#include <wayfire/render-manager.hpp>
#include <wayfire/scene-operations.hpp>
#include <wayfire/opengl.hpp>
// Helper to check for intersection
bool boxes_intersect(const wlr_box& a, const wlr_box& b)
{
return !(b.x >= a.x + a.width || a.x >= b.x + b.width ||
b.y >= a.y + a.height || a.y >= b.y + b.height);
}
// Helper to check if point is inside geometry
bool point_inside(const wf::geometry_t& geom, int x, int y)
{
return x >= geom.x && x < geom.x + geom.width &&
y >= geom.y && y < geom.y + geom.height;
}
// Preview overlay data
struct preview_overlay_t
{
wayfire_view target_view;
wf::geometry_t position;
wf::dimensions_t size;
};
// Represents a single grouped window
struct group_member_t
{
wayfire_view view;
std::shared_ptr<wf::scene::view_2d_transformer_t> transformer;
std::unique_ptr<preview_overlay_t> preview_overlay;
bool is_hidden;
group_member_t() : is_hidden(false) {}
group_member_t(const group_member_t&) = delete;
group_member_t& operator=(const group_member_t&) = delete;
group_member_t(group_member_t&&) = default;
group_member_t& operator=(group_member_t&&) = default;
};
// Represents a full group of windows
struct window_group_t
{
std::vector<group_member_t> members;
};
class wayfire_group : public wf::per_output_plugin_instance_t
{
private:
// Configuration options
wf::option_wrapper_t<int> preview_size{"group/preview_size"};
wf::option_wrapper_t<int> preview_margin{"group/preview_margin"};
// A map to quickly find which group a view belongs to
std::map<wayfire_view, std::shared_ptr<window_group_t>> view_to_group_map;
// Track the last known positions for detecting movement
std::map<wayfire_view, wf::geometry_t> last_positions;
// Keybinding to create/destroy groups
wf::option_wrapper_t<wf::keybinding_t> group_key{"group/toggle"};
// Button binding for click detection
wf::option_wrapper_t<wf::buttonbinding_t> click_binding{"group/click"};
// Transformer name for group movements
std::string group_transformer_name = "group-transform";
// Effect hook for rendering overlays on top
wf::effect_hook_t overlay_effect = [=] ()
{
render_preview_overlays();
};
// Key callback for the keybinding
wf::key_callback group_key_callback = [=] (const wf::keybinding_t& key) -> bool
{
auto focused_view = wf::get_core().seat->get_active_view();
if (!focused_view)
{
return false;
}
if (view_to_group_map.count(focused_view))
{
ungroup_view(focused_view);
} else
{
create_group_from_focused(focused_view);
}
return true;
};
// Button callback for click detection
wf::button_callback click_callback = [=] (const wf::buttonbinding_t& button) -> bool
{
auto cursor = wf::get_core().get_cursor_position();
return handle_preview_click(cursor.x, cursor.y);
};
// Handle clicks on preview overlays
bool handle_preview_click(int x, int y)
{
for (const auto& [view, group] : view_to_group_map)
{
for (auto& member : group->members)
{
if (member.is_hidden && member.preview_overlay)
{
auto& overlay = *member.preview_overlay;
wf::geometry_t click_area{
overlay.position.x,
overlay.position.y,
overlay.size.width,
overlay.size.height
};
if (point_inside(click_area, x, y))
{
switch_to_window(member, group);
return true; // Consume the click
}
}
}
}
return false; // Let the click pass through
}
// Switch to a specific window in the group
void switch_to_window(group_member_t& target_member, std::shared_ptr<window_group_t> group)
{
if (!target_member.view || !target_member.is_hidden) return;
LOGD("GROUP: Switching to window ", target_member.view->get_title());
// Find current visible member (anchor)
group_member_t* current_anchor = nullptr;
for (auto& member : group->members)
{
if (!member.is_hidden)
{
current_anchor = &member;
break;
}
}
if (!current_anchor) return;
// Hide current anchor
if (!current_anchor->transformer)
{
// Create transformer for the current anchor
current_anchor->transformer = std::make_shared<wf::scene::view_2d_transformer_t>(current_anchor->view);
current_anchor->view->get_transformed_node()->add_transformer(current_anchor->transformer,
wf::TRANSFORMER_2D, group_transformer_name);
}
hide_window(*current_anchor);
create_preview_overlay(*current_anchor, target_member.view);
// Show target member
show_window(target_member);
destroy_preview_overlay(target_member);
// Focus the newly visible window
wf::get_core().seat->focus_view(target_member.view);
// Update preview positions
update_preview_positions(group);
LOGD("GROUP: Window switch complete");
}
// Create a preview overlay
void create_preview_overlay(group_member_t& member, wayfire_view anchor_view)
{
if (!member.view || member.preview_overlay) return;
auto toplevel = wf::toplevel_cast(member.view);
auto anchor_toplevel = wf::toplevel_cast(anchor_view);
if (!toplevel || !anchor_toplevel) return;
member.preview_overlay = std::make_unique<preview_overlay_t>();
auto& overlay = *member.preview_overlay;
overlay.target_view = member.view;
// Calculate preview size maintaining aspect ratio
auto view_geom = toplevel->get_geometry();
int target_size = preview_size;
if (view_geom.width > view_geom.height)
{
overlay.size.width = target_size;
overlay.size.height = (view_geom.height * target_size) / view_geom.width;
}
else
{
overlay.size.height = target_size;
overlay.size.width = (view_geom.width * target_size) / view_geom.height;
}
// Position in top-right of anchor window
auto anchor_geom = anchor_toplevel->get_geometry();
overlay.position.x = anchor_geom.x + anchor_geom.width - overlay.size.width - preview_margin;
overlay.position.y = anchor_geom.y + preview_margin;
LOGD("GROUP: Created preview overlay for ", member.view->get_title());
}
// Destroy preview overlay
void destroy_preview_overlay(group_member_t& member)
{
if (!member.preview_overlay) return;
member.preview_overlay.reset();
LOGD("GROUP: Destroyed preview overlay for ", member.view->get_title());
}
// Hide a window by scaling it to zero
void hide_window(group_member_t& member)
{
if (!member.transformer || member.is_hidden) return;
// --- START OF FIX ---
// Damage the view's current area BEFORE hiding it by scaling to zero.
// This is crucial to notify the compositor that the area previously occupied
// by this window needs to be redrawn, thus clearing its old contents and
// preventing visual artifacts (ghost windows).
member.view->damage();
// --- END OF FIX ---
// Scale to zero to hide the window
member.transformer->scale_x = 0.0f;
member.transformer->scale_y = 0.0f;
member.is_hidden = true;
member.view->damage(); // Damage after is also good practice.
LOGD("GROUP: Hidden window ", member.view->get_title());
}
// Show a window by restoring its scale
void show_window(group_member_t& member)
{
if (!member.transformer || !member.is_hidden) return;
// Restore normal scale
member.transformer->scale_x = 1.0f;
member.transformer->scale_y = 1.0f;
member.is_hidden = false;
member.view->damage();
LOGD("GROUP: Shown window ", member.view->get_title());
}
// Update preview positions
void update_preview_positions(std::shared_ptr<window_group_t> group)
{
if (group->members.empty()) return;
// Find the anchor (the single visible member in the group)
group_member_t* anchor = nullptr;
for (auto& member : group->members)
{
if (!member.is_hidden)
{
anchor = &member;
break;
}
}
if (!anchor) return;
auto anchor_toplevel = wf::toplevel_cast(anchor->view);
if (!anchor_toplevel) return;
// Calculate the actual on-screen geometry of the anchor window.
// get_geometry() returns the base geometry. If a transformer is active,
// we must account for its translation to find the correct visual position.
wf::geometry_t anchor_geom = anchor_toplevel->get_geometry();
if (anchor->transformer)
{
anchor_geom.x += anchor->transformer->translation_x;
anchor_geom.y += anchor->transformer->translation_y;
}
int current_y = anchor_geom.y + preview_margin;
// Update positions for all preview overlays
for (auto& member : group->members)
{
// Only update positions for hidden members that have a preview
if (member.is_hidden && member.preview_overlay)
{
auto& overlay = *member.preview_overlay;
overlay.position.x = anchor_geom.x + anchor_geom.width - overlay.size.width - preview_margin;
overlay.position.y = current_y;
current_y += overlay.size.height + preview_margin;
}
}
// Schedule a redraw to show updated overlays
this->output->render->schedule_redraw();
}
// Render all preview overlays on top of everything
void render_preview_overlays()
{
for (const auto& [view, group] : view_to_group_map)
{
render_group_overlays(group);
}
}
// Render overlays for a specific group
void render_group_overlays(std::shared_ptr<window_group_t> group)
{
if (!group || group->members.empty()) return;
// Find anchor to check if we should render on this output
group_member_t* anchor = nullptr;
for (auto& member : group->members)
{
if (!member.transformer || !member.is_hidden)
{
anchor = &member;
break;
}
}
if (!anchor || anchor->view->get_output() != this->output) return;
// Render previews for hidden group members
for (auto& member : group->members)
{
if (&member != anchor && member.is_hidden && member.preview_overlay)
{
render_single_overlay(member);
}
}
}
// Render a single preview overlay
void render_single_overlay(const group_member_t& member)
{
if (!member.preview_overlay || !member.view) return;
auto& overlay = *member.preview_overlay;
// Take snapshot of the view
wf::auxilliary_buffer_t snapshot;
member.view->take_snapshot(snapshot);
auto snapshot_size = snapshot.get_size();
if (snapshot_size.width > 0 && snapshot_size.height > 0)
{
// Calculate scaling to fit preview size
double scale_x = double(overlay.size.width) / snapshot_size.width;
double scale_y = double(overlay.size.height) / snapshot_size.height;
double scale = std::min(scale_x, scale_y);
int scaled_width = snapshot_size.width * scale;
int scaled_height = snapshot_size.height * scale;
// Center the scaled preview
int offset_x = (overlay.size.width - scaled_width) / 2;
int offset_y = (overlay.size.height - scaled_height) / 2;
wf::geometry_t dest_geometry{
overlay.position.x + offset_x,
overlay.position.y + offset_y,
scaled_width,
scaled_height
};
wlr_fbox src_box{0, 0, float(snapshot_size.width), float(snapshot_size.height)};
// Get the current render target and render the preview
auto target_fb = this->output->render->get_target_framebuffer();
target_fb.blit(snapshot, src_box, dest_geometry);
snapshot.free();
}
}
public:
void init() override
{
// Set up keybinding and overlay effect
this->output->add_key(group_key, &group_key_callback);
this->output->add_button(click_binding, &click_callback);
this->output->render->add_effect(&overlay_effect, wf::OUTPUT_EFFECT_OVERLAY);
// Connect to view signals
wf::get_core().connect(&on_view_mapped);
wf::get_core().connect(&on_view_unmapped);
// Connect to geometry changes for existing views
for (auto& view : wf::get_core().get_all_views())
{
if (wf::toplevel_cast(view))
{
view->connect(&on_view_geometry_changed);
if (auto toplevel = wf::toplevel_cast(view))
{
last_positions[view] = toplevel->get_geometry();
}
}
}
}
void create_group_from_focused(wayfire_view anchor_view)
{
auto toplevel_anchor = wf::toplevel_cast(anchor_view);
if (!toplevel_anchor) return;
LOGD("GROUP: Creating new group with anchor ", anchor_view->get_title());
auto new_group = std::make_shared<window_group_t>();
auto anchor_geometry = toplevel_anchor->get_geometry();
// Add the anchor view to the group (not hidden)
group_member_t anchor_member;
anchor_member.view = anchor_view;
anchor_member.transformer = nullptr;
anchor_member.is_hidden = false;
new_group->members.emplace_back(std::move(anchor_member));
view_to_group_map[anchor_view] = new_group;
// Find overlapping windows and group them
for (auto& view : wf::get_core().get_all_views())
{
auto toplevel_other = wf::toplevel_cast(view);
if (!toplevel_other || view == anchor_view || !view->is_mapped())
{
continue;
}
auto view_geometry = toplevel_other->get_geometry();
if (boxes_intersect(anchor_geometry, view_geometry))
{
group_member_t member;
member.view = view;
member.is_hidden = false;
// Create transformer for movement
member.transformer = std::make_shared<wf::scene::view_2d_transformer_t>(view);
view->get_transformed_node()->add_transformer(member.transformer,
wf::TRANSFORMER_2D, group_transformer_name);
// Move to anchor position
int diff_x = anchor_geometry.x - view_geometry.x;
int diff_y = anchor_geometry.y - view_geometry.y;
member.transformer->translation_x = diff_x;
member.transformer->translation_y = diff_y;
// Hide the grouped window and create preview
hide_window(member);
create_preview_overlay(member, anchor_view);
new_group->members.emplace_back(std::move(member));
view_to_group_map[view] = new_group;
LOGD("GROUP: Added ", view->get_title(), " to group.");
}
}
update_preview_positions(new_group);
}
void ungroup_view(wayfire_view view)
{
if (!view_to_group_map.count(view)) return;
LOGD("GROUP: Dissolving group for view ", view->get_title());
auto group = view_to_group_map[view];
for (auto& member : group->members)
{
if (member.transformer)
{
show_window(member);
member.view->get_transformed_node()->rem_transformer(group_transformer_name);
member.view->damage();
}
destroy_preview_overlay(member);
view_to_group_map.erase(member.view);
}
// Schedule redraw to clear overlays
this->output->render->schedule_redraw();
}
void update_group_transformers(wayfire_view moved_view)
{
auto group_it = view_to_group_map.find(moved_view);
if (group_it == view_to_group_map.end()) return;
auto group = group_it->second;
auto toplevel_moved = wf::toplevel_cast(moved_view);
if (!toplevel_moved) return;
auto current_geometry = toplevel_moved->get_geometry();
auto last_geometry = last_positions[moved_view];
int delta_x = current_geometry.x - last_geometry.x;
int delta_y = current_geometry.y - last_geometry.y;
for (auto& member : group->members)
{
if (member.view == moved_view || !member.transformer) continue;
member.transformer->translation_x += delta_x;
member.transformer->translation_y += delta_y;
member.view->damage();
}
last_positions[moved_view] = current_geometry;
update_preview_positions(group);
}
// Signal handlers
wf::signal::connection_t<wf::view_geometry_changed_signal> on_view_geometry_changed =
[=] (wf::view_geometry_changed_signal *ev)
{
wayfire_view moved_view = ev->view;
if (auto toplevel = wf::toplevel_cast(moved_view))
{
auto current_geometry = toplevel->get_geometry();
if (view_to_group_map.count(moved_view))
{
if (last_positions.count(moved_view))
{
auto last_geometry = last_positions[moved_view];
if (current_geometry.x != last_geometry.x || current_geometry.y != last_geometry.y)
{
update_group_transformers(moved_view);
}
}
else
{
last_positions[moved_view] = current_geometry;
}
}
else
{
last_positions[moved_view] = current_geometry;
}
}
};
wf::signal::connection_t<wf::view_mapped_signal> on_view_mapped =
[=] (wf::view_mapped_signal *ev)
{
if (wf::toplevel_cast(ev->view))
{
ev->view->connect(&on_view_geometry_changed);
if (auto toplevel = wf::toplevel_cast(ev->view))
{
last_positions[ev->view] = toplevel->get_geometry();
}
}
};
wf::signal::connection_t<wf::view_unmapped_signal> on_view_unmapped =
[=] (wf::view_unmapped_signal *ev)
{
last_positions.erase(ev->view);
if (view_to_group_map.count(ev->view))
{
auto group = view_to_group_map[ev->view];
// Destroy preview overlay for this view
for (auto& member : group->members)
{
if (member.view == ev->view)
{
destroy_preview_overlay(member);
break;
}
}
// Remove from group
group->members.erase(
std::remove_if(group->members.begin(), group->members.end(),
[=](const group_member_t& member) { return member.view == ev->view; }),
group->members.end()
);
view_to_group_map.erase(ev->view);
// Dissolve group if too few members
if (group->members.size() <= 1)
{
if (!group->members.empty())
{
auto& last_member = group->members.front();
if (last_member.transformer)
{
show_window(last_member);
last_member.view->get_transformed_node()->rem_transformer(group_transformer_name);
last_member.view->damage();
}
destroy_preview_overlay(last_member);
view_to_group_map.erase(last_member.view);
}
}
else
{
update_preview_positions(group);
}
}
};
void fini() override
{
// Clean up
this->output->rem_binding(&group_key_callback);
this->output->rem_binding(&click_callback);
this->output->render->rem_effect(&overlay_effect);
// Remove all transformers and preview overlays
for (const auto& [view, group] : view_to_group_map)
{
for (auto& member : group->members)
{
if (member.transformer)
{
show_window(member);
member.view->get_transformed_node()->rem_transformer(group_transformer_name);
}
destroy_preview_overlay(member);
}
}
// Disconnect signals
on_view_mapped.disconnect();
on_view_unmapped.disconnect();
on_view_geometry_changed.disconnect();
// Clear tracking
last_positions.clear();
view_to_group_map.clear();
}
};
class wayfire_group_global : public wf::plugin_interface_t,
public wf::per_output_tracker_mixin_t<wayfire_group>
{
public:
void init() override
{
this->init_output_tracking();
}
void fini() override
{
this->fini_output_tracking();
}
};
DECLARE_WAYFIRE_PLUGIN(wayfire_group_global);Editor is loading...
Leave a Comment