view_tree_validator.mm 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. // Copyright 2017 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. #include "ui/base/test/view_tree_validator.h"
  5. #include <Cocoa/Cocoa.h>
  6. #include "base/mac/mac_util.h"
  7. #include "base/strings/sys_string_conversions.h"
  8. namespace {
  9. NSArray* CollectSubviews(NSView* root) {
  10. NSMutableArray* subviews = [NSMutableArray arrayWithObject:root];
  11. for (NSView* child in root.subviews) {
  12. [subviews addObjectsFromArray:CollectSubviews(child)];
  13. }
  14. return subviews;
  15. }
  16. bool ViewsOverlap(NSView* a, NSView* b) {
  17. NSRect a_frame = [a convertRect:a.bounds toView:nil];
  18. NSRect b_frame = [b convertRect:b.bounds toView:nil];
  19. return NSIntersectsRect(a_frame, b_frame);
  20. }
  21. bool IsLocalizable(NSView* view) {
  22. return [view isKindOfClass:[NSControl class]] ||
  23. [view isKindOfClass:[NSText class]];
  24. }
  25. // Returns whether to expect children of |view| to perhaps not fit within its
  26. // bounds.
  27. bool IgnoreChildBoundsChecks(NSView* view) {
  28. // On macOS 10.14+, NSButton has a subview of a private helper class whose
  29. // bounds extend a bit outside the NSButton itself. We don't care about this
  30. // helper class's bounds being outside the button.
  31. return base::mac::IsAtLeastOS10_14() && [view isKindOfClass:[NSButton class]];
  32. }
  33. } // namespace
  34. namespace ui {
  35. absl::optional<ViewTreeProblemDetails> ValidateViewTree(NSView* root) {
  36. NSArray* allViews = CollectSubviews(root);
  37. for (NSView* view in allViews) {
  38. // 1: Check that every subview's frame lies entirely inside this view's
  39. // bounds.
  40. for (NSView* child in view.subviews) {
  41. if (!NSContainsRect(view.bounds, child.frame) &&
  42. !IgnoreChildBoundsChecks(view)) {
  43. return absl::optional<ViewTreeProblemDetails>(
  44. {ViewTreeProblemDetails::VIEW_OUTSIDE_PARENT, child, view});
  45. }
  46. }
  47. // If |view| isn't localizable, skip the rest of the checks.
  48. if (!IsLocalizable(view))
  49. continue;
  50. // 2: Check that every other subview either:
  51. // a: doesn't overlap this view
  52. // b: is a descendant of this view
  53. // c: has this view as a descendant
  54. // note that a view is its own descendant.
  55. for (NSView* other in allViews) {
  56. if (!ViewsOverlap(view, other))
  57. continue;
  58. if ([view isDescendantOf:other] || [other isDescendantOf:view])
  59. continue;
  60. return absl::optional<ViewTreeProblemDetails>(
  61. {ViewTreeProblemDetails::VIEWS_OVERLAP, view, other});
  62. }
  63. }
  64. return absl::nullopt;
  65. }
  66. std::string ViewTreeProblemDetails::ToString() {
  67. NSString* s;
  68. switch (type) {
  69. case VIEW_OUTSIDE_PARENT:
  70. s = [NSString stringWithFormat:@"View %@ [%@] outside parent %@ [%@]",
  71. view_a, NSStringFromRect(view_a.frame),
  72. view_b, NSStringFromRect(view_b.frame)];
  73. break;
  74. case VIEWS_OVERLAP:
  75. s = [NSString stringWithFormat:@"Views %@ [%@] and %@ [%@] overlap",
  76. view_a, NSStringFromRect(view_a.frame),
  77. view_b, NSStringFromRect(view_b.frame)];
  78. break;
  79. }
  80. return base::SysNSStringToUTF8(s);
  81. }
  82. } // namespace ui