blob: d61fd9226c22273b953c43504f8966f9240f97dd [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/gfx/x/window_cache.h"
#include <algorithm>
#include <vector>
#include "base/containers/adapters.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/notreached.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/gfx/x/atom_cache.h"
#include "ui/gfx/x/connection.h"
#include "ui/gfx/x/event.h"
#include "ui/gfx/x/xproto.h"
namespace x11 {
const base::TimeDelta kDestroyTimerInterval = base::Seconds(3);
Window GetWindowAtPoint(const gfx::Point& point_px,
const base::flat_set<Window>* ignore) {
auto* connection = Connection::Get();
Window root = connection->default_root();
if (!WindowCache::instance()) {
auto instance =
std::make_unique<WindowCache>(connection, connection->default_root());
auto* cache = instance.get();
cache->BeginDestroyTimer(std::move(instance));
}
auto* instance = WindowCache::instance();
instance->WaitUntilReady();
return instance->GetWindowAtPoint(point_px, root, ignore);
}
ScopedShapeEventSelector::ScopedShapeEventSelector(Connection* connection,
Window window)
: connection_(connection), window_(window) {
connection_->shape()
.SelectInput({.destination_window = window_, .enable = true})
.IgnoreError();
}
ScopedShapeEventSelector::~ScopedShapeEventSelector() {
connection_->shape()
.SelectInput({.destination_window = window_, .enable = false})
.IgnoreError();
}
WindowCache::WindowInfo::WindowInfo() = default;
WindowCache::WindowInfo::~WindowInfo() = default;
// static
WindowCache* WindowCache::instance_ = nullptr;
WindowCache::WindowCache(Connection* connection, Window root)
: connection_(connection),
root_(root),
gtk_frame_extents_(GetAtom("_GTK_FRAME_EXTENTS")) {
CHECK(!instance_) << "Only one WindowCache should be active at a time";
instance_ = this;
connection_->AddEventObserver(this);
// We select for SubstructureNotify events on all windows (to receive
// CreateNotify events), which will cause events to be sent for all child
// windows. This means we need to additionally select for StructureNotify
// changes for the root window.
root_events_ =
connection_->ScopedSelectEvent(root_, EventMask::StructureNotify);
AddWindow(root_, Window::None);
}
WindowCache::~WindowCache() {
connection_->RemoveEventObserver(this);
CHECK_EQ(instance_, this);
instance_ = nullptr;
}
void WindowCache::WaitUntilReady() {
auto& events = connection_->events();
size_t event = 0;
while (!pending_requests_.empty()) {
connection_->Flush();
for (size_t pending = pending_requests_.size(); pending;) {
if (event < events.size() &&
pending_requests_.front().AfterEvent(events[event])) {
OnEvent(events[event++]);
} else {
pending_requests_.front().DispatchNow();
--pending;
}
}
}
if (event) {
last_processed_event_ = events[event - 1].sequence();
}
}
void WindowCache::BeginDestroyTimer(std::unique_ptr<WindowCache> self) {
CHECK_EQ(this, self.get());
delete_when_destroy_timer_fires_ = false;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&WindowCache::OnDestroyTimerExpired,
base::Unretained(this), std::move(self)),
kDestroyTimerInterval);
}
void WindowCache::SyncForTest() {
do {
// Perform a blocking sync to prevent spinning while waiting for replies.
connection_->Sync();
connection_->DispatchAll();
} while (!pending_requests_.empty());
}
Window WindowCache::GetWindowAtPoint(gfx::Point point_px,
Window window,
const base::flat_set<Window>* ignore) {
delete_when_destroy_timer_fires_ = true;
if (ignore && ignore->contains(window)) {
return Window::None;
}
auto* info = GetInfo(window);
if (!info || !info->mapped) {
return Window::None;
}
gfx::Rect rect(info->x_px, info->y_px, info->width_px, info->height_px);
rect.Outset(info->border_width_px);
rect.Inset(info->gtk_frame_extents_px);
if (!rect.Contains(point_px)) {
return Window::None;
}
point_px -= gfx::Vector2d(info->x_px, info->y_px);
if (info->bounding_rects_px && info->input_rects_px) {
for (const auto& rects : {info->bounding_rects_px, info->input_rects_px}) {
if (!std::ranges::any_of(*rects, [&point_px](const Rectangle& x_rect) {
gfx::Rect rect{x_rect.x, x_rect.y, x_rect.width, x_rect.height};
return rect.Contains(point_px);
})) {
return Window::None;
}
}
}
for (Window child : base::Reversed(info->children)) {
Window ret = GetWindowAtPoint(point_px, child, ignore);
if (ret != Window::None) {
return ret;
}
}
if (info->has_wm_name) {
return window;
}
return Window::None;
}
void WindowCache::OnEvent(const Event& event) {
// Ignore events that we've already processed.
if (last_processed_event_ &&
CompareSequenceIds(event.sequence(), *last_processed_event_) <= 0) {
return;
}
last_processed_event_ = std::nullopt;
// Ignore events sent by clients since the server will send everything
// we need and client events may have different semantics (eg.
// ConfigureNotifyEvents are parent-relative if sent by the server but
// root-relative when sent by the WM).
if (event.send_event()) {
return;
}
if (auto* configure = event.As<ConfigureNotifyEvent>()) {
if (auto* info = GetInfo(configure->window)) {
info->x_px = configure->x;
info->y_px = configure->y;
info->width_px = configure->width;
info->height_px = configure->height;
info->border_width_px = configure->border_width;
if (auto* siblings = GetChildren(info->parent)) {
Window window = configure->window;
Window above = configure->above_sibling;
auto src = std::ranges::find(*siblings, window);
auto dst = std::ranges::find(*siblings, above);
auto end = siblings->end();
if (src != end && (dst != end || above == Window::None)) {
dst = above == Window::None ? siblings->begin() : ++dst;
if (src < dst) {
std::rotate(src, src + 1, dst);
} else if (src > dst) {
std::rotate(dst, src, src + 1);
}
}
}
}
} else if (auto* property = event.As<PropertyNotifyEvent>()) {
if (auto* info = GetInfo(property->window)) {
if (property->atom == Atom::WM_NAME) {
info->has_wm_name = property->state != Property::Delete;
} else if (property->atom == gtk_frame_extents_) {
if (property->state == Property::Delete) {
info->gtk_frame_extents_px = gfx::Insets();
} else {
GetProperty(property->window, gtk_frame_extents_, 4);
}
}
}
} else if (auto* create = event.As<CreateNotifyEvent>()) {
if (auto* info = GetInfo(create->parent)) {
info->children.push_back(create->window);
AddWindow(create->window, create->parent);
}
} else if (auto* destroy = event.As<DestroyNotifyEvent>()) {
if (auto* info = GetInfo(destroy->window)) {
if (auto* siblings = GetChildren(info->parent)) {
std::erase(*siblings, destroy->window);
}
windows_.erase(destroy->window);
}
} else if (auto* map = event.As<MapNotifyEvent>()) {
if (auto* info = GetInfo(map->window)) {
info->mapped = true;
}
} else if (auto* unmap = event.As<UnmapNotifyEvent>()) {
if (auto* info = GetInfo(unmap->window)) {
info->mapped = false;
}
} else if (auto* reparent = event.As<ReparentNotifyEvent>()) {
if (auto* info = GetInfo(reparent->window)) {
if (auto* old_siblings = GetChildren(info->parent)) {
std::erase(*old_siblings, reparent->window);
}
if (auto* new_siblings = GetChildren(reparent->parent)) {
new_siblings->push_back(reparent->window);
}
info->parent = reparent->parent;
}
} else if (auto* gravity = event.As<GravityNotifyEvent>()) {
if (auto* info = GetInfo(gravity->window)) {
info->x_px = gravity->x;
info->y_px = gravity->y;
}
} else if (auto* circulate = event.As<CirculateEvent>()) {
if (auto* info = GetInfo(circulate->window)) {
if (auto* siblings = GetChildren(info->parent)) {
std::erase(*siblings, circulate->window);
if (circulate->place == Place::OnTop) {
siblings->push_back(circulate->window);
} else {
siblings->insert(siblings->begin(), circulate->window);
}
}
}
} else if (auto* shape = event.As<Shape::NotifyEvent>()) {
Window window = shape->affected_window;
Shape::Sk kind = shape->shape_kind;
if (kind != Shape::Sk::Clip && base::Contains(windows_, window)) {
AddRequest(connection_->shape().GetRectangles(window, kind),
&WindowCache::OnGetRectanglesResponse, window, kind);
}
}
}
void WindowCache::AddWindow(Window window, Window parent) {
if (base::Contains(windows_, window)) {
return;
}
WindowInfo& info = windows_[window];
info.parent = parent;
// Events must be selected before getting the initial window info to
// prevent race conditions.
info.events = connection_->ScopedSelectEvent(
window, EventMask::SubstructureNotify | EventMask::PropertyChange);
AddRequest(connection_->GetWindowAttributes(window),
&WindowCache::OnGetWindowAttributesResponse, window);
AddRequest(connection_->GetGeometry(window),
&WindowCache::OnGetGeometryResponse, window);
AddRequest(connection_->QueryTree(window), &WindowCache::OnQueryTreeResponse,
window);
GetProperty(window, Atom::WM_NAME, 1);
GetProperty(window, gtk_frame_extents_, 4);
auto& shape = connection_->shape();
if (shape.present()) {
info.shape_events =
std::make_unique<ScopedShapeEventSelector>(connection_, window);
for (auto kind : {Shape::Sk::Bounding, Shape::Sk::Input}) {
AddRequest(shape.GetRectangles(window, kind),
&WindowCache::OnGetRectanglesResponse, window, kind);
}
}
}
WindowCache::WindowInfo* WindowCache::GetInfo(Window window) {
auto it = windows_.find(window);
if (it == windows_.end()) {
return nullptr;
}
return &it->second;
}
std::vector<Window>* WindowCache::GetChildren(Window window) {
if (auto* info = GetInfo(window)) {
return &info->children;
}
return nullptr;
}
void WindowCache::GetProperty(Window window, Atom property, uint32_t length) {
AddRequest(
connection_->GetProperty(
{.window = window, .property = property, .long_length = length}),
&WindowCache::OnGetPropertyResponse, window, property);
}
WindowCache::WindowInfo* WindowCache::OnResponse(Window window,
bool has_reply) {
pending_requests_.pop_front();
if (!has_reply) {
windows_.erase(window);
return nullptr;
}
auto it = windows_.find(window);
if (it == windows_.end()) {
return nullptr;
}
return &it->second;
}
void WindowCache::OnGetWindowAttributesResponse(
Window window,
GetWindowAttributesResponse response) {
if (auto* info = OnResponse(window, response.reply.get())) {
info->mapped = response->map_state != MapState::Unmapped;
}
}
void WindowCache::OnGetGeometryResponse(Window window,
GetGeometryResponse response) {
if (auto* info = OnResponse(window, response.reply.get())) {
info->x_px = response->x;
info->y_px = response->y;
info->width_px = response->width;
info->height_px = response->height;
}
}
void WindowCache::OnQueryTreeResponse(Window window,
QueryTreeResponse response) {
if (auto* info = OnResponse(window, response.reply.get())) {
info->parent = response->parent;
info->children = std::move(response->children);
for (auto child : info->children) {
AddWindow(child, window);
}
}
}
void WindowCache::OnGetPropertyResponse(Window window,
Atom atom,
GetPropertyResponse response) {
if (auto* info = OnResponse(window, response.reply.get())) {
if (atom == Atom::WM_NAME) {
info->has_wm_name = response->format;
} else if (atom == gtk_frame_extents_) {
if (response->format == CHAR_BIT * sizeof(int32_t) &&
response->value_len == 4) {
const int32_t* frame_extents = response->value->cast_to<int32_t>();
// This is safe: we've checked (in the condition above) that the
// response contains four int32_ts. It would be nice if instead
// GetPropertyResponse had a way to convert its value safely into a
// span<T> for some T.
UNSAFE_BUFFERS(info->gtk_frame_extents_px = gfx::Insets::TLBR(
frame_extents[2], frame_extents[0], frame_extents[3],
frame_extents[1]));
} else {
info->gtk_frame_extents_px = gfx::Insets();
}
}
}
}
void WindowCache::OnGetRectanglesResponse(
Window window,
Shape::Sk kind,
Shape::GetRectanglesResponse response) {
if (auto* info = OnResponse(window, response.reply.get())) {
switch (kind) {
case Shape::Sk::Bounding:
info->bounding_rects_px = std::move(response->rectangles);
break;
case Shape::Sk::Clip:
NOTREACHED();
case Shape::Sk::Input:
info->input_rects_px = std::move(response->rectangles);
break;
}
}
}
void WindowCache::OnDestroyTimerExpired(std::unique_ptr<WindowCache> self) {
if (!delete_when_destroy_timer_fires_) {
return; // destroy `this`
}
BeginDestroyTimer(std::move(self));
}
} // namespace x11