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