| OLD | NEW |
| (Empty) | |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #include "ui/base/x/x11_window_cache.h" |
| 6 |
| 7 #include <poll.h> |
| 8 #include <X11/Xlib.h> |
| 9 #include <xcb/xcbext.h> |
| 10 |
| 11 #include <algorithm> |
| 12 #include <cstdio> |
| 13 #include <cstdlib> |
| 14 #include <cstring> |
| 15 |
| 16 #include "base/logging.h" |
| 17 |
| 18 namespace ui { |
| 19 |
| 20 namespace { |
| 21 |
| 22 const char kNetWmIcon[] = "_NET_WM_ICON"; |
| 23 |
| 24 template <typename T> |
| 25 void CacheWindowGeometryFromEvent(XWindowCache::Window* window, T* event) { |
| 26 window->cached_geometry = true; |
| 27 window->x = event->x; |
| 28 window->y = event->y; |
| 29 window->width = event->width; |
| 30 window->height = event->height; |
| 31 window->border_width = event->border_width; |
| 32 } |
| 33 |
| 34 auto FindChild(XWindowCache::Window* parent, xcb_window_t child_id) |
| 35 -> decltype(parent->children.begin()) { |
| 36 return std::find_if(parent->children.begin(), parent->children.end(), |
| 37 [child_id](XWindowCache::Window* child) { |
| 38 return child->id == child_id; |
| 39 }); |
| 40 } |
| 41 |
| 42 } // anonymous namespace |
| 43 |
| 44 XWindowCache::Property::Property(xcb_atom_t name) |
| 45 : name(name), cached_property(false) { |
| 46 data.bits_8 = nullptr; |
| 47 } |
| 48 |
| 49 XWindowCache::Property::~Property() { |
| 50 delete[] data.bits_8; |
| 51 } |
| 52 |
| 53 const XWindowCache::Property* XWindowCache::Window::GetProperty( |
| 54 XID atom) const { |
| 55 auto it = properties.find(atom); |
| 56 return it == properties.end() ? nullptr : it->second.get(); |
| 57 } |
| 58 |
| 59 XWindowCache::Window::Window(xcb_window_t id, XWindowCache::Window* parent) |
| 60 : id(id), |
| 61 parent(parent), |
| 62 cached_attributes(false), |
| 63 cached_geometry(false), |
| 64 cached_properties(false), |
| 65 cached_children(false) {} |
| 66 |
| 67 XWindowCache::Window::~Window() {} |
| 68 |
| 69 XWindowCache::Request::Request(xcb_window_t id) : id_(id) {} |
| 70 |
| 71 XWindowCache::GetWindowAttributesRequest::GetWindowAttributesRequest( |
| 72 xcb_window_t window, |
| 73 XWindowCache* cache) |
| 74 : Request(window) { |
| 75 sequence_ = xcb_get_window_attributes(cache->connection_, window).sequence; |
| 76 } |
| 77 |
| 78 void XWindowCache::GetWindowAttributesRequest::OnReply(void* r, |
| 79 XWindowCache* cache) { |
| 80 auto* reply = reinterpret_cast<xcb_get_window_attributes_reply_t*>(r); |
| 81 |
| 82 // Don't bother getting stale attributes if the window was destroyed. |
| 83 Window* window = cache->GetWindow(id_); |
| 84 if (!window) |
| 85 return; |
| 86 |
| 87 window->cached_attributes = true; |
| 88 window->override_redirect = reply->override_redirect; |
| 89 window->is_mapped = reply->map_state != XCB_MAP_STATE_UNMAPPED; |
| 90 } |
| 91 |
| 92 XWindowCache::GetGeometryRequest::GetGeometryRequest(xcb_window_t window, |
| 93 XWindowCache* cache) |
| 94 : Request(window) { |
| 95 sequence_ = xcb_get_geometry(cache->connection_, window).sequence; |
| 96 } |
| 97 |
| 98 void XWindowCache::GetGeometryRequest::OnReply(void* r, XWindowCache* cache) { |
| 99 auto* reply = reinterpret_cast<xcb_get_geometry_reply_t*>(r); |
| 100 |
| 101 // Don't bother getting stale attributes if the window was destroyed. |
| 102 Window* window = cache->GetWindow(id_); |
| 103 if (!window) |
| 104 return; |
| 105 |
| 106 window->cached_geometry = true; |
| 107 window->x = reply->x; |
| 108 window->y = reply->y; |
| 109 window->width = reply->width; |
| 110 window->height = reply->height; |
| 111 window->border_width = reply->border_width; |
| 112 } |
| 113 |
| 114 XWindowCache::ListPropertiesRequest::ListPropertiesRequest(xcb_window_t window, |
| 115 XWindowCache* cache) |
| 116 : Request(window) { |
| 117 sequence_ = xcb_list_properties(cache->connection_, window).sequence; |
| 118 } |
| 119 |
| 120 void XWindowCache::ListPropertiesRequest::OnReply(void* r, |
| 121 XWindowCache* cache) { |
| 122 auto* reply = reinterpret_cast<xcb_list_properties_reply_t*>(r); |
| 123 |
| 124 // Don't bother getting stale properties if the window was destroyed. |
| 125 Window* window = cache->GetWindow(id_); |
| 126 if (!window) |
| 127 return; |
| 128 |
| 129 window->cached_properties = true; |
| 130 for (int i = 0; i < xcb_list_properties_atoms_length(reply); i++) { |
| 131 xcb_atom_t atom = xcb_list_properties_atoms(reply)[i]; |
| 132 cache->CacheProperty(window, atom); |
| 133 } |
| 134 } |
| 135 |
| 136 XWindowCache::QueryTreeRequest::QueryTreeRequest(xcb_window_t window, |
| 137 XWindowCache* cache) |
| 138 : Request(window) { |
| 139 sequence_ = xcb_query_tree(cache->connection_, window).sequence; |
| 140 } |
| 141 |
| 142 void XWindowCache::QueryTreeRequest::OnReply(void* r, XWindowCache* cache) { |
| 143 auto* reply = reinterpret_cast<xcb_query_tree_reply_t*>(r); |
| 144 |
| 145 // Don't bother getting stale children if the parent was destroyed. |
| 146 Window* p = cache->GetWindow(id_); |
| 147 if (!p) |
| 148 return; |
| 149 |
| 150 DCHECK(p->children.empty()); |
| 151 |
| 152 xcb_window_t* children = xcb_query_tree_children(reply); |
| 153 int n_children = xcb_query_tree_children_length(reply); |
| 154 |
| 155 p->cached_children = true; |
| 156 |
| 157 // Iterate over children from top-to-bottom. |
| 158 for (int i = n_children - 1; i >= 0; i--) { |
| 159 Window* child = new Window(children[i], p); |
| 160 p->children.push_back(child); |
| 161 cache->CacheWindow(child); |
| 162 } |
| 163 } |
| 164 |
| 165 XWindowCache::GetPropertyRequest::GetPropertyRequest(xcb_window_t window, |
| 166 xcb_atom_t atom, |
| 167 XWindowCache* cache) |
| 168 : Request(window), atom_(atom) { |
| 169 // In case a property's value is huge, only cache the first 64KiB, and |
| 170 // indicate in Property that the cached value is not the entire value. |
| 171 static constexpr auto max_property_size = 0xffff; |
| 172 sequence_ = xcb_get_property(cache->connection_, false, window, atom, |
| 173 XCB_ATOM_ANY, 0, max_property_size) |
| 174 .sequence; |
| 175 } |
| 176 |
| 177 void XWindowCache::GetPropertyRequest::OnReply(void* r, XWindowCache* cache) { |
| 178 auto* reply = reinterpret_cast<xcb_get_property_reply_t*>(r); |
| 179 |
| 180 Window* window = cache->GetWindow(id_); |
| 181 if (!window) |
| 182 return; |
| 183 |
| 184 if (window->properties.find(atom_) == window->properties.end()) |
| 185 return; |
| 186 if (reply->format == 0) { |
| 187 // According to Xlib, a format of anything other than 8, 16, or 32 is a |
| 188 // BadImplementation error. However, this occurs when creating a new xterm |
| 189 // window, so just forget about the property in question. |
| 190 window->properties.erase(atom_); |
| 191 return; |
| 192 } |
| 193 |
| 194 Property* property = window->properties[atom_].get(); |
| 195 |
| 196 property->cached_property = true; |
| 197 property->type = reply->type; |
| 198 DCHECK(reply->format == 8 || reply->format == 16 || reply->format == 32); |
| 199 property->data_format = reply->format; |
| 200 property->data_length = xcb_get_property_value_length(reply); |
| 201 // TODO(thomasanderson): verify data_bytes is correct |
| 202 uint32_t data_bytes = property->data_length * (property->data_format / 8); |
| 203 property->data.bits_8 = new uint8_t[data_bytes]; |
| 204 std::memcpy(property->data.bits_8, xcb_get_property_value(reply), data_bytes); |
| 205 } |
| 206 |
| 207 XWindowCache::XWindowCache(XDisplay* display, XID root) |
| 208 : root_(nullptr), sequence_(0), net_wm_icon_(0) { |
| 209 connection_ = xcb_connect(DisplayString(display), nullptr); |
| 210 if (xcb_connection_has_error(connection_)) |
| 211 return; |
| 212 |
| 213 net_wm_icon_cookie_ = |
| 214 xcb_intern_atom(connection_, false, sizeof(kNetWmIcon) - 1, kNetWmIcon); |
| 215 |
| 216 root_ = new Window(root, nullptr); |
| 217 CacheWindow(root_); |
| 218 |
| 219 xcb_flush(connection_); |
| 220 } |
| 221 |
| 222 XWindowCache::~XWindowCache() { |
| 223 xcb_disconnect(connection_); |
| 224 } |
| 225 |
| 226 bool XWindowCache::ConnectionHasError() const { |
| 227 return xcb_connection_has_error(connection_); |
| 228 } |
| 229 |
| 230 int XWindowCache::ConnectionFd() const { |
| 231 return xcb_get_file_descriptor(connection_); |
| 232 } |
| 233 |
| 234 bool XWindowCache::BlockUntilConnectionIsReadable() { |
| 235 if (ConnectionHasError()) |
| 236 return false; |
| 237 |
| 238 int conn_fd = ConnectionFd(); |
| 239 |
| 240 struct pollfd rfd; |
| 241 rfd.fd = conn_fd; |
| 242 rfd.events = POLLIN; |
| 243 rfd.revents = 0; |
| 244 // TODO(thomasanderson): should we handle EAGAIN? |
| 245 poll(&rfd, 1, 3000); // Use a timeout of 3s. |
| 246 return rfd.revents & POLLIN; |
| 247 } |
| 248 |
| 249 const XWindowCache::Window* XWindowCache::GetWindow(XID id) const { |
| 250 return GetWindow(static_cast<xcb_window_t>(id)); |
| 251 } |
| 252 |
| 253 XWindowCache::Window* XWindowCache::GetWindow(xcb_window_t id) const { |
| 254 auto it = windows_.find(id); |
| 255 return it == windows_.end() ? nullptr : it->second.get(); |
| 256 } |
| 257 |
| 258 void XWindowCache::OnConnectionReadable() { |
| 259 // xcb serially reads events and replies and then stores them in two queues. |
| 260 // We want to merge these queues into one to make sure we process events and |
| 261 // replies in order. |
| 262 // |
| 263 // xcb_poll_for_reply does not read any new data, while xcb_poll_for_event |
| 264 // does, so get all available events first, followed by all replies. |
| 265 // |
| 266 // xcb_poll_for_event reads all available data, so we cannot have an outer |
| 267 // loop reading events and an inner loop reading replies. That would be too |
| 268 // easy. The only way to guarantee ordering is to read all events at once, |
| 269 // followed by all replies. |
| 270 |
| 271 // Get all events. |
| 272 std::queue<xcb_generic_event_t*> events; |
| 273 xcb_generic_event_t* event; |
| 274 while ((event = xcb_poll_for_event(connection_))) |
| 275 events.emplace(event); |
| 276 |
| 277 // Get all replies for which we have a queued request. Some requests, such as |
| 278 // xcb_change_window_attributes, do not have a corresponding reply, so skip |
| 279 // over those sequence numbers. We may also want to poll for replies |
| 280 // ourselves, such as when getting the _NET_WM_ICON atom, or when |
| 281 // synchronizing. Skip over these too. |
| 282 std::queue<std::unique_ptr<Request>> requests; |
| 283 std::queue<xcb_generic_reply_t*> replies; |
| 284 while (!requests_.empty()) { |
| 285 uint16_t front_sequence = requests_.front()->sequence(); |
| 286 void* reply; |
| 287 if (!xcb_poll_for_reply(connection_, front_sequence, &reply, nullptr)) |
| 288 break; |
| 289 if (reply) { |
| 290 requests.emplace(std::move(requests_.front())); |
| 291 replies.emplace(reinterpret_cast<xcb_generic_reply_t*>(reply)); |
| 292 } |
| 293 // Remove the request from the queue even if there was an error. |
| 294 requests_.pop(); |
| 295 } |
| 296 |
| 297 // Process events and replies in order. |
| 298 while (!events.empty() || !replies.empty()) { |
| 299 // Give precedence to replies. |
| 300 if (!replies.empty() && sequence_ == replies.front()->sequence) { |
| 301 xcb_generic_reply_t* reply = replies.front(); |
| 302 requests.front()->OnReply(reply, this); |
| 303 free(reply); |
| 304 requests.pop(); |
| 305 replies.pop(); |
| 306 } else if (!events.empty() && sequence_ == events.front()->sequence) { |
| 307 xcb_generic_event_t* event = events.front(); |
| 308 ProcessEvent(event); |
| 309 free(event); |
| 310 events.pop(); |
| 311 } else { |
| 312 sequence_++; |
| 313 } |
| 314 } |
| 315 |
| 316 // Write out any requests we may have made. |
| 317 xcb_flush(connection_); |
| 318 } |
| 319 |
| 320 bool XWindowCache::BlockUntilTreeIsCached() { |
| 321 while (!requests_.empty()) { |
| 322 if (!BlockUntilConnectionIsReadable()) |
| 323 return false; |
| 324 OnConnectionReadable(); |
| 325 } |
| 326 return true; |
| 327 } |
| 328 |
| 329 bool XWindowCache::Synchronize() { |
| 330 if (!root_) |
| 331 return true; // There's nothing to synchronize if the root was destroyed. |
| 332 |
| 333 // Try getting some info about the root window and wait until we get a |
| 334 // response or an error. |
| 335 auto sequence = xcb_get_window_attributes(connection_, root_->id).sequence; |
| 336 xcb_flush(connection_); |
| 337 |
| 338 while (BlockUntilConnectionIsReadable()) { |
| 339 OnConnectionReadable(); |
| 340 void* reply = nullptr; |
| 341 if (!xcb_poll_for_reply(connection_, sequence, &reply, nullptr)) |
| 342 continue; |
| 343 if (reply) { |
| 344 free(reply); |
| 345 return true; |
| 346 } |
| 347 // There was some sort of error. This may just be because the root was |
| 348 // destroyed. Only return false if there was a connection error. |
| 349 return !ConnectionHasError(); |
| 350 } |
| 351 xcb_discard_reply(connection_, sequence); |
| 352 return false; |
| 353 } |
| 354 |
| 355 bool XWindowCache::SynchronizeWith(XDisplay* display) { |
| 356 XSync(display, False); |
| 357 return Synchronize(); |
| 358 } |
| 359 |
| 360 void XWindowCache::ProcessEvent(xcb_generic_event_t* g) { |
| 361 switch (g->response_type & ~0x80) { |
| 362 case XCB_PROPERTY_NOTIFY: { |
| 363 auto* event = reinterpret_cast<xcb_property_notify_event_t*>(g); |
| 364 |
| 365 Window* window = GetWindow(event->window); |
| 366 if (!window) |
| 367 break; |
| 368 |
| 369 switch (event->state) { |
| 370 case XCB_PROPERTY_DELETE: |
| 371 window->properties.erase(event->atom); |
| 372 break; |
| 373 case XCB_PROPERTY_NEW_VALUE: |
| 374 CacheProperty(window, event->atom); |
| 375 break; |
| 376 default: |
| 377 NOTREACHED(); |
| 378 } |
| 379 break; |
| 380 } |
| 381 case XCB_CIRCULATE_NOTIFY: { |
| 382 auto* event = reinterpret_cast<xcb_circulate_notify_event_t*>(g); |
| 383 |
| 384 Window* window = GetWindow(event->window); |
| 385 if (!window) |
| 386 break; |
| 387 |
| 388 if (event->event == event->window) |
| 389 break; // This is our root window |
| 390 |
| 391 Window* parent = window->parent; |
| 392 DCHECK_EQ(parent->id, event->event); |
| 393 |
| 394 parent->children.remove(window); |
| 395 switch (event->place) { |
| 396 case XCB_PLACE_ON_TOP: |
| 397 parent->children.push_front(window); |
| 398 break; |
| 399 case XCB_PLACE_ON_BOTTOM: |
| 400 parent->children.push_back(window); |
| 401 break; |
| 402 default: |
| 403 NOTREACHED(); |
| 404 } |
| 405 break; |
| 406 } |
| 407 case XCB_CONFIGURE_NOTIFY: { |
| 408 auto* event = reinterpret_cast<xcb_configure_notify_event_t*>(g); |
| 409 |
| 410 Window* window = GetWindow(event->window); |
| 411 if (!window) |
| 412 break; |
| 413 |
| 414 CacheWindowGeometryFromEvent(window, event); |
| 415 |
| 416 if (event->event == event->window) |
| 417 break; |
| 418 |
| 419 Window* parent = window->parent; |
| 420 DCHECK_EQ(parent->id, event->event); |
| 421 |
| 422 parent->children.remove(window); |
| 423 if (event->above_sibling) { |
| 424 auto it = FindChild(parent, event->above_sibling); |
| 425 if (it == parent->children.end()) { |
| 426 NOTREACHED(); |
| 427 // We got into a bad state. Recache |parent|. |
| 428 Window* grandparent = parent->parent; |
| 429 xcb_window_t id = parent->id; |
| 430 DeleteSubtree(parent); |
| 431 CacheWindow(new Window(id, grandparent)); |
| 432 } else { |
| 433 parent->children.insert(it, window); |
| 434 } |
| 435 } else { |
| 436 // |window| is not above any other sibling window |
| 437 parent->children.push_back(window); |
| 438 } |
| 439 break; |
| 440 } |
| 441 case XCB_CREATE_NOTIFY: { |
| 442 auto* event = reinterpret_cast<xcb_create_notify_event_t*>(g); |
| 443 |
| 444 Window* parent = GetWindow(event->parent); |
| 445 if (!parent) |
| 446 break; |
| 447 |
| 448 if (!parent->cached_children) { |
| 449 // |parent| is in the process of being cached, so we will pick up this |
| 450 // window in the near future. |
| 451 break; |
| 452 } |
| 453 |
| 454 Window* window = new Window(event->window, parent); |
| 455 |
| 456 window->cached_attributes = true; |
| 457 window->override_redirect = event->override_redirect; |
| 458 window->is_mapped = false; |
| 459 |
| 460 CacheWindowGeometryFromEvent(window, event); |
| 461 |
| 462 CacheWindow(window); |
| 463 parent->children.push_front(window); |
| 464 break; |
| 465 } |
| 466 case XCB_DESTROY_NOTIFY: { |
| 467 auto* event = reinterpret_cast<xcb_destroy_notify_event_t*>(g); |
| 468 |
| 469 Window* window = GetWindow(event->window); |
| 470 if (!window) |
| 471 break; |
| 472 |
| 473 if (window->parent) { |
| 474 auto& children = window->parent->children; |
| 475 auto it = std::find(children.begin(), children.end(), window); |
| 476 if (it != children.end()) |
| 477 window->parent->children.erase(it); |
| 478 } else { |
| 479 // Window has no parent, so this must be the root that's going away. |
| 480 root_ = nullptr; |
| 481 } |
| 482 |
| 483 DeleteSubtree(window); |
| 484 break; |
| 485 } |
| 486 case XCB_GRAVITY_NOTIFY: { |
| 487 auto* event = reinterpret_cast<xcb_gravity_notify_event_t*>(g); |
| 488 |
| 489 Window* window = GetWindow(event->window); |
| 490 if (!window) |
| 491 break; |
| 492 |
| 493 window->x = event->x; |
| 494 window->y = event->y; |
| 495 break; |
| 496 } |
| 497 case XCB_MAP_NOTIFY: { |
| 498 auto* event = reinterpret_cast<xcb_map_notify_event_t*>(g); |
| 499 |
| 500 Window* window = GetWindow(event->window); |
| 501 if (!window) |
| 502 break; |
| 503 |
| 504 window->cached_attributes = true; |
| 505 window->override_redirect = event->override_redirect; |
| 506 window->is_mapped = true; |
| 507 break; |
| 508 } |
| 509 case XCB_REPARENT_NOTIFY: { |
| 510 auto* event = reinterpret_cast<xcb_reparent_notify_event_t*>(g); |
| 511 |
| 512 Window* window = GetWindow(event->window); |
| 513 if (!window) |
| 514 break; |
| 515 |
| 516 window->x = event->x; |
| 517 window->y = event->y; |
| 518 |
| 519 window->cached_attributes = true; |
| 520 window->override_redirect = event->override_redirect; |
| 521 window->is_mapped = false; // Reparenting a window unmaps it |
| 522 |
| 523 Window* parent = window->parent; |
| 524 if (parent) |
| 525 parent->children.remove(window); |
| 526 else |
| 527 break; // Don't worry about caching windows above our root. |
| 528 |
| 529 parent = GetWindow(event->parent); |
| 530 if (!parent || !parent->cached_children) { |
| 531 // |window| is either no longer in our tree, or we are already waiting |
| 532 // to receive a list of |parent|'s children. |
| 533 DeleteSubtree(window); |
| 534 break; |
| 535 } |
| 536 window->parent = parent; |
| 537 |
| 538 parent->children.push_front(window); |
| 539 break; |
| 540 } |
| 541 case XCB_UNMAP_NOTIFY: { |
| 542 auto* event = reinterpret_cast<xcb_unmap_notify_event_t*>(g); |
| 543 |
| 544 Window* window = GetWindow(event->window); |
| 545 if (!window) |
| 546 break; |
| 547 |
| 548 window->is_mapped = false; |
| 549 break; |
| 550 } |
| 551 default: |
| 552 // We can't opt-out of receiving events like MappingNotify or |
| 553 // ClientMessage. |
| 554 break; |
| 555 } |
| 556 } |
| 557 |
| 558 void XWindowCache::CacheProperty(XWindowCache::Window* window, |
| 559 xcb_atom_t atom) { |
| 560 if (!net_wm_icon_ && net_wm_icon_cookie_.sequence) { |
| 561 auto reply = |
| 562 xcb_intern_atom_reply(connection_, net_wm_icon_cookie_, nullptr); |
| 563 if (reply) { |
| 564 net_wm_icon_ = reply->atom; |
| 565 free(reply); |
| 566 } |
| 567 net_wm_icon_cookie_.sequence = 0; |
| 568 } |
| 569 if (atom == net_wm_icon_) |
| 570 return; |
| 571 xcb_window_t id = window->id; |
| 572 Property* property = new Property(atom); |
| 573 window->properties[atom].reset(property); |
| 574 if (!property->cached_property) |
| 575 requests_.emplace(new GetPropertyRequest(id, atom, this)); |
| 576 } |
| 577 |
| 578 void XWindowCache::CacheWindow(XWindowCache::Window* window) { |
| 579 DCHECK(window); |
| 580 xcb_window_t id = window->id; |
| 581 DCHECK(windows_.find(id) == windows_.end()); |
| 582 |
| 583 uint32_t event_mask = |
| 584 XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | XCB_EVENT_MASK_PROPERTY_CHANGE; |
| 585 if (id == root_->id) { |
| 586 event_mask |= XCB_EVENT_MASK_STRUCTURE_NOTIFY; |
| 587 } |
| 588 auto cookie = xcb_change_window_attributes(connection_, id, XCB_CW_EVENT_MASK, |
| 589 &event_mask); |
| 590 xcb_discard_reply(connection_, cookie.sequence); |
| 591 |
| 592 // Get the desired window state AFTER requesting state change events to avoid |
| 593 // race conditions. |
| 594 |
| 595 if (!window->cached_children) |
| 596 requests_.emplace(new QueryTreeRequest(id, this)); |
| 597 |
| 598 if (!window->cached_properties) |
| 599 requests_.emplace(new ListPropertiesRequest(id, this)); |
| 600 |
| 601 if (!window->cached_attributes) |
| 602 requests_.emplace(new GetWindowAttributesRequest(id, this)); |
| 603 |
| 604 if (!window->cached_geometry) |
| 605 requests_.emplace(new GetGeometryRequest(id, this)); |
| 606 |
| 607 windows_[id].reset(window); |
| 608 } |
| 609 |
| 610 void XWindowCache::DeleteSubtree(XWindowCache::Window* window) { |
| 611 DCHECK(window); |
| 612 for (auto* child : window->children) |
| 613 DeleteSubtree(child); |
| 614 windows_.erase(window->id); |
| 615 } |
| 616 |
| 617 } // namespace ui |
| OLD | NEW |