| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/base/test/view_tree_validator.h" |
| |
| #include <Cocoa/Cocoa.h> |
| #include "base/mac/mac_util.h" |
| #include "base/strings/sys_string_conversions.h" |
| |
| namespace { |
| |
| NSArray* CollectSubviews(NSView* root) { |
| NSMutableArray* subviews = [NSMutableArray arrayWithObject:root]; |
| for (NSView* child in root.subviews) { |
| [subviews addObjectsFromArray:CollectSubviews(child)]; |
| } |
| return subviews; |
| } |
| |
| bool ViewsOverlap(NSView* a, NSView* b) { |
| NSRect a_frame = [a convertRect:a.bounds toView:nil]; |
| NSRect b_frame = [b convertRect:b.bounds toView:nil]; |
| return NSIntersectsRect(a_frame, b_frame); |
| } |
| |
| bool IsLocalizable(NSView* view) { |
| return [view isKindOfClass:[NSControl class]] || |
| [view isKindOfClass:[NSText class]]; |
| } |
| |
| // Returns whether to expect children of |view| to perhaps not fit within its |
| // bounds. |
| bool IgnoreChildBoundsChecks(NSView* view) { |
| // On macOS 10.14+, NSButton has a subview of a private helper class whose |
| // bounds extend a bit outside the NSButton itself. We don't care about this |
| // helper class's bounds being outside the button. |
| return base::mac::IsAtLeastOS10_14() && [view isKindOfClass:[NSButton class]]; |
| } |
| |
| } // namespace |
| |
| namespace ui { |
| |
| base::Optional<ViewTreeProblemDetails> ValidateViewTree(NSView* root) { |
| NSArray* allViews = CollectSubviews(root); |
| |
| for (NSView* view in allViews) { |
| // 1: Check that every subview's frame lies entirely inside this view's |
| // bounds. |
| for (NSView* child in view.subviews) { |
| if (!NSContainsRect(view.bounds, child.frame) && |
| !IgnoreChildBoundsChecks(view)) { |
| return base::Optional<ViewTreeProblemDetails>( |
| {ViewTreeProblemDetails::VIEW_OUTSIDE_PARENT, child, view}); |
| } |
| } |
| |
| // If |view| isn't localizable, skip the rest of the checks. |
| if (!IsLocalizable(view)) |
| continue; |
| |
| // 2: Check that every other subview either: |
| // a: doesn't overlap this view |
| // b: is a descendant of this view |
| // c: has this view as a descendant |
| // note that a view is its own descendant. |
| for (NSView* other in allViews) { |
| if (!ViewsOverlap(view, other)) |
| continue; |
| if ([view isDescendantOf:other] || [other isDescendantOf:view]) |
| continue; |
| return base::Optional<ViewTreeProblemDetails>( |
| {ViewTreeProblemDetails::VIEWS_OVERLAP, view, other}); |
| } |
| } |
| |
| return base::nullopt; |
| } |
| |
| std::string ViewTreeProblemDetails::ToString() { |
| NSString* s; |
| switch (type) { |
| case VIEW_OUTSIDE_PARENT: |
| s = [NSString stringWithFormat:@"View %@ [%@] outside parent %@ [%@]", |
| view_a, NSStringFromRect(view_a.frame), |
| view_b, NSStringFromRect(view_b.frame)]; |
| break; |
| case VIEWS_OVERLAP: |
| s = [NSString stringWithFormat:@"Views %@ [%@] and %@ [%@] overlap", |
| view_a, NSStringFromRect(view_a.frame), |
| view_b, NSStringFromRect(view_b.frame)]; |
| break; |
| } |
| |
| return base::SysNSStringToUTF8(s); |
| } |
| |
| } // namespace ui |