910e62b5创建于 1月15日历史提交
// 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