summaryrefslogtreecommitdiff
path: root/chromium/docs/ui/learn/bestpractices/layout.md
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/docs/ui/learn/bestpractices/layout.md')
-rw-r--r--chromium/docs/ui/learn/bestpractices/layout.md1350
1 files changed, 1350 insertions, 0 deletions
diff --git a/chromium/docs/ui/learn/bestpractices/layout.md b/chromium/docs/ui/learn/bestpractices/layout.md
new file mode 100644
index 00000000000..5740c73dd03
--- /dev/null
+++ b/chromium/docs/ui/learn/bestpractices/layout.md
@@ -0,0 +1,1350 @@
+# Best practice: layout
+
+The most important principles when working with Views layout are abstraction
+and encapsulation. The goal of the guidance here is to split the broad work of
+"layout" into many discrete pieces, each of which can be easily understood,
+tested, and changed in isolation. Compliant code should be more readable,
+performant, and maintainable; more adaptable to future modifications; and more
+stylistically consistent with other UI elements both now and in the future.
+
+[TOC]
+
+## Express layout values logically
+
+Both in mocks and code, **layout values should be described in functional terms
+that conform to the relevant specs** (particularly the
+[Material Refresh][] and older [Harmony][] specs). Rather than a mock saying
+a dialog title is "15 pt high", it should say it is the "Title 1" or
+"DIALOG\_TITLE" style; two non-interacting controls on the same line should not
+be separated by "16 dip" but by [`DISTANCE_UNRELATED_CONTROL_HORIZONTAL`][].
+Designers and engineers can reference a [dictionary][] to agree on common
+terminology. Don't simply back-figure the constant to use based on what
+produces the same value as the mock, as future design system changes will
+cause subtle and confusing bugs. Similarly, don't hardcode designer-provided
+values that aren't currently present in Chrome, as the semantics of such
+one-off values are unclear and will cause maintenance problems.
+Work with the Toolkit and UX teams to modify the design and overall spec so
+the desired results can be achieved in a centralized way.
+
+Note: the concept in this section is general, but the linked specs are
+**Googlers Only**.
+
+[Material Refresh]: https://docs.google.com/presentation/d/1EO7TOpIMJ7QHjaTVw9St-q6naKwtXX2TwzMirG5EsKY/edit#slide=id.g3232c09376_6_794
+[Harmony]: https://folio.googleplex.com/chrome-ux-specs-and-sources/Chrome%20browser%20%28MD%29/Secondary%20UI%20Previews%20and%20specs%20%28exports%29/Spec
+[`DISTANCE_UNRELATED_CONTROL_HORIZONTAL`]: https://source.chromium.org/chromium/chromium/src/+/master:chrome/browser/ui/views/chrome_layout_provider.h;l=56;drc=ec62c9ac3ef71a7014e27c5d2cf98917a89e3524
+[dictionary]: http://go/DesktopDictionary
+
+## Obtain layout values from provider objects
+
+Once layout styles and values are expressed functionally, **the exact values
+should be obtained from relevant provider objects, not computed directly.**
+Code should not have any magic numbers for sizes, positions, elevations, and
+the like; this includes transformations like scaling values up or down by a
+fraction, or tweaking a provided value by a few DIPs. The most commonly used
+provider object is the [`LayoutProvider`][] (or its `chrome`/-side extension,
+[`ChromeLayoutProvider`][]); `View`s can obtain the global instance via a
+relevant [`Get()`][] method and ask it for relevant [distances][], [insets][],
+[corner radii][], [shadow elevations][], and the like. For text-setting
+controls like [`Label`][], the [`TypographyProvider`][] (or its
+`chrome`/-side extension, [`ChromeTypographyProvider`]),
+usually accessed via [global helper functions][], can provide appropriate
+[fonts][], [colors][], and [line heights][]. Most `View`s should not use these
+directly, but use `Label` and other such controls, providing the appropriate
+[context][] and [style][].
+
+[`LayoutProvider`]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/layout/layout_provider.h;l=129;drc=f5df5da5753795298349b2dd6325e2c5e6e13e38
+[`ChromeLayoutProvider`]: https://source.chromium.org/chromium/chromium/src/+/master:chrome/browser/ui/views/chrome_layout_provider.h;drc=ec62c9ac3ef71a7014e27c5d2cf98917a89e3524;l=76
+[`Get()`]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/layout/layout_provider.h;drc=f5df5da5753795298349b2dd6325e2c5e6e13e38;l=135
+[distances]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/layout/layout_provider.h;drc=f5df5da5753795298349b2dd6325e2c5e6e13e38;l=148
+[insets]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/layout/layout_provider.h;drc=f5df5da5753795298349b2dd6325e2c5e6e13e38;l=144
+[corner radii]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/layout/layout_provider.h;drc=f5df5da5753795298349b2dd6325e2c5e6e13e38;l=168
+[shadow elevations]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/layout/layout_provider.h;drc=f5df5da5753795298349b2dd6325e2c5e6e13e38;l=172
+[`Label`]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/controls/label.h;l=30;drc=59135b4042aa469752899e8e4bf2a0a81d3d320c
+[`TypographyProvider`]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/style/typography_provider.h;l=22;drc=b5e29e075e814ed41e6727c281b69f797d8a1e10
+[`ChromeTypographyProvider`]: https://source.chromium.org/chromium/chromium/src/+/master:chrome/browser/ui/views/chrome_typography_provider.h;l=13;drc=a7ee000c95842e2dce6397ca36926924f4cb322b
+[global helper functions]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/style/typography.h;l=109;drc=8f7db479018a99e5906876954de93ae6d23bee58
+[fonts]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/style/typography_provider.h;l=28;drc=b5e29e075e814ed41e6727c281b69f797d8a1e10
+[colors]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/style/typography_provider.h;l=32;drc=b5e29e075e814ed41e6727c281b69f797d8a1e10
+[line heights]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/style/typography_provider.h;l=37;drc=b5e29e075e814ed41e6727c281b69f797d8a1e10
+[context]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/style/typography.h;l=23;drc=8f7db479018a99e5906876954de93ae6d23bee58
+[style]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/style/typography.h;l=67;drc=8f7db479018a99e5906876954de93ae6d23bee58
+
+|||---|||
+
+#####
+
+**Avoid**
+
+[Current code][1] uses file scoped hard-coded padding values for its layout
+constants.
+
+[1]: https://source.chromium.org/chromium/chromium/src/+/master:chrome/browser/ui/views/subtle_notification_view.cc;l=142;drc=787d0aacc071674dc83f6059072d15f8cfffbf84
+
+#####
+
+**Best practice**
+
+A better approach would be to use layout constants sourced from the
+[`ChromeLayoutProvider`][].
+
+|||---|||
+
+|||---|||
+
+#####
+
+``` cpp
+namespace {
+// Space between the site info label.
+const int kMiddlePaddingPx = 30;
+
+const int kOuterPaddingHorizPx = 40;
+const int kOuterPaddingVertPx = 8;
+} // namespace
+
+SubtleNotificationView::SubtleNotificationView()
+ : instruction_view_(nullptr) {
+ ...
+ instruction_view_ =
+ new InstructionView(base::string16());
+
+ int outer_padding_horiz = kOuterPaddingHorizPx;
+ int outer_padding_vert = kOuterPaddingVertPx;
+ AddChildView(instruction_view_);
+
+ SetLayoutManager(std::make_unique<views::BoxLayout>(
+ views::BoxLayout::Orientation::kHorizontal,
+ gfx::Insets(outer_padding_vert,
+ outer_padding_horiz),
+ kMiddlePaddingPx));
+}
+```
+
+#####
+
+``` cpp
+
+
+
+
+
+
+
+
+SubtleNotificationView::SubtleNotificationView()
+ : instruction_view_(nullptr) {
+ ...
+ AddChildView(std::make_unique<InstructionView>(
+ base::string16()));
+
+ const gfx::Insets kDialogInsets =
+ ChromeLayoutProvider::Get()->GetInsetsMetric(
+ views::INSETS_DIALOG);
+ const int kHorizontalPadding =
+ ChromeLayoutProvider::Get()->GetDistanceMetric(
+ views::DISTANCE_RELATED_LABEL_HORIZONTAL);
+ SetLayoutManager(std::make_unique<views::BoxLayout>(
+ views::BoxLayout::Orientation::kHorizontal,
+ kDialogInsets, kHorizontalPadding));
+}
+```
+
+|||---|||
+
+
+## Use hierarchy liberally
+
+While not a layout-specific tactic, it simplifies many layout issues
+**to break a high-level UI construct into a hierarchy of `View`s**,
+with as many levels as necessary to make each `View` as simple as possible.
+In such hierarchies, most non-leaf `View`s will be nameless "containers" that
+simply size or group their immediate children, perhaps with padding between
+them or a margin around the outside. Each such `View` is easy to lay out,
+and you can later combine or factor out pieces of the hierarchy as appropriate,
+including adding helpers for common Material Design idioms to the core toolkit.
+
+
+## Use LayoutManagers
+
+**Avoid overriding [`Layout()`][] to programmatically lay out children.**
+In nearly all cases, the built-in [`LayoutManager`][]s can achieve the desired
+layout, and do so in a declarative rather than imperative fashion. The
+resulting code is often simpler and easier to understand. Writing a [bespoke
+`LayoutManager`][] is also possible, but less common.
+
+[`Layout()`]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/view.h;l=730;drc=f5df5da5753795298349b2dd6325e2c5e6e13e38
+[`LayoutManager`]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/layout/layout_manager.h;l=33;drc=f5df5da5753795298349b2dd6325e2c5e6e13e38
+[bespoke `LayoutManager`]: https://source.chromium.org/chromium/chromium/src/+/master:chrome/browser/ui/views/try_chrome_dialog_win/button_layout.h;l=30;drc=f5df5da5753795298349b2dd6325e2c5e6e13e38
+
+|||---|||
+
+#####
+
+**Avoid**
+
+The following old code used Layout() to have its label text fill the dialog.
+
+#####
+
+**Best practice**
+
+[Current code][2] uses a [FillLayout][] to achieve the same result.
+
+[2]: https://source.chromium.org/chromium/chromium/src/+/master:chrome/browser/ui/views/relaunch_notification/relaunch_required_dialog_view.cc;l=91;drc=1ec33e7c19e2d63b3f918df115c12f77f419645b
+[FillLayout]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/layout/fill_layout.h
+
+|||---|||
+
+|||---|||
+
+#####
+
+``` cpp
+int RelaunchRequiredDialogView::GetHeightForWidth(
+ int width) const {
+ const gfx::Insets insets = GetInsets();
+ return body_label_->GetHeightForWidth(
+ width - insets.width()) + insets.height();
+}
+
+void RelaunchRequiredDialogView::Layout() {
+ body_label_->SetBoundsRect(GetContentsBounds());
+}
+```
+
+#####
+
+``` cpp
+RelaunchRequiredDialogView::RelaunchRequiredDialogView(
+ base::Time deadline,
+ base::RepeatingClosure on_accept)
+ : ...{
+ SetLayoutManager(
+ std::make_unique<views::FillLayout>());
+ ...
+}
+
+
+```
+
+|||---|||
+
+
+## Prefer intrinsic constraints to extrinsic computation
+
+Where possible, **express the desired outcome of layout in terms of intrinsic
+constraints for each `View`,** instead of trying to conditionally compute
+the desired output metrics. For example, using a [`ClassProperty`][]
+to set each child's [margins][] is less error-prone than trying to
+conditionally add padding `View`s between children. When coupled with
+[margin collapsing][] and [internal padding][], it's possible to do things
+like use [different padding amounts between different children][]
+or visually align elements without manually computing offsets.
+Such manual computation is prone to bugs if someone changes a size, padding
+value, or child order in one place without also updating related computations
+elsewhere.
+
+[`ClassProperty`]: https://source.chromium.org/chromium/chromium/src/+/master:ui/base/class_property.h;l=55;drc=f5df5da5753795298349b2dd6325e2c5e6e13e38
+[margins]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/view_class_properties.h;l=30;drc=1449b8c60358c4cdea1722e4c1e8079bd1b5f306
+[margin collapsing]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/layout/flex_layout.h;l=87;drc=62bf27aca5418212ceadd8daf9188d2aa437bfcc
+[internal padding]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/view_class_properties.h;l=40;drc=1449b8c60358c4cdea1722e4c1e8079bd1b5f306
+[different padding amounts between different children]: https://source.chromium.org/chromium/chromium/src/+/master:chrome/browser/ui/views/toolbar/toolbar_view.cc;l=974;drc=34a8c4215229379ced3586125399c7ad3c65b87f
+
+|||---|||
+
+#####
+
+**Avoid**
+
+The following is old code that calculated bubble padding through calculations
+involving the control insets.
+
+#####
+
+**Best practice**
+
+[Current code][3] uses a combination of margin and padding on the
+ColorPickerView to ensure proper alignment.
+
+[3]: https://source.chromium.org/chromium/chromium/src/+/master:chrome/browser/ui/views/tabs/tab_group_editor_bubble_view.cc;l=89;drc=542c4c6ac89bc665807351d3fb4aca5ebddc82f8
+
+|||---|||
+
+|||---|||
+
+#####
+
+``` cpp
+TabGroupEditorBubbleView::TabGroupEditorBubbleView(
+ const Browser* browser,
+ const tab_groups::TabGroupId& group,
+ TabGroupHeader* anchor_view,
+ base::Optional<gfx::Rect> anchor_rect,
+ bool stop_context_menu_propagation)
+ : ... {
+
+ ...
+ const auto* layout_provider =
+ ChromeLayoutProvider::Get();
+ const int horizontal_spacing =
+ layout_provider->GetDistanceMetric(
+ views::DISTANCE_RELATED_CONTROL_HORIZONTAL);
+ const int vertical_menu_spacing =
+ layout_provider->GetDistanceMetric(
+ views::DISTANCE_RELATED_CONTROL_VERTICAL);
+
+ // The vertical spacing for the non menu items within
+ // the editor bubble.
+ const int vertical_dialog_content_spacing = 16;
+
+
+
+
+
+
+
+
+
+
+ views::View* group_modifier_container =
+ AddChildView(std::make_unique<views::View>());
+
+ gfx::Insets color_element_insets =
+ ChromeLayoutProvider::Get()->GetInsetsMetric(
+ views::INSETS_VECTOR_IMAGE_BUTTON);
+ group_modifier_container->SetBorder(
+ views::CreateEmptyBorder(
+ gfx::Insets(vertical_dialog_content_spacing,
+ horizontal_spacing -
+ color_element_insets.left(),
+ vertical_dialog_content_spacing,
+ horizontal_spacing -
+ color_element_insets.right())));
+ ...
+ // Add the text field for editing the title.
+ views::View* title_field_container =
+ group_modifier_container->AddChildView(
+ std::make_unique<views::View>());
+ title_field_container->SetBorder(
+ views::CreateEmptyBorder(gfx::Insets(
+ 0, color_element_insets.left(),
+ vertical_dialog_content_spacing,
+ color_element_insets.right()))
+ ...
+ color_selector_ =
+ group_modifier_container->AddChildView(
+ std::make_unique<ColorPickerView>(
+ colors_, background_color(), initial_color,
+ base::Bind(
+ &TabGroupEditorBubbleView::UpdateGroup,
+ base::Unretained(this))));
+ ...
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+#####
+
+``` cpp
+TabGroupEditorBubbleView::TabGroupEditorBubbleView(
+ const Browser* browser,
+ const tab_groups::TabGroupId& group,
+ views::View* anchor_view,
+ base::Optional<gfx::Rect> anchor_rect,
+ TabGroupHeader* header_view,
+ bool stop_context_menu_propagation)
+ : ... {
+ ...
+ const auto* layout_provider =
+ ChromeLayoutProvider::Get();
+ const int horizontal_spacing =
+ layout_provider->GetDistanceMetric(
+ views::DISTANCE_RELATED_CONTROL_HORIZONTAL);
+ const int vertical_spacing =
+ layout_provider->GetDistanceMetric(
+ views::DISTANCE_RELATED_CONTROL_VERTICAL);
+
+ // The padding of the editing controls is adaptive,
+ // to improve the hit target size and screen real
+ // estate usage on touch devices.
+ const int group_modifier_vertical_spacing =
+ ui::TouchUiController::Get()->touch_ui() ?
+ vertical_spacing / 2 : vertical_spacing;
+ const gfx::Insets control_insets =
+ ui::TouchUiController::Get()->touch_ui()
+ ? gfx::Insets(5 * vertical_spacing / 4,
+ horizontal_spacing)
+ : gfx::Insets(vertical_spacing,
+ horizontal_spacing);
+
+ views::View* group_modifier_container =
+ AddChildView(std::make_unique<views::View>());
+ group_modifier_container->SetBorder(
+ views::CreateEmptyBorder(gfx::Insets(
+ group_modifier_vertical_spacing, 0)));
+
+ views::FlexLayout* group_modifier_container_layout =
+ group_modifier_container->SetLayoutManager(
+ std::make_unique<views::FlexLayout>());
+ group_modifier_container_layout
+ ->SetOrientation(
+ views::LayoutOrientation::kVertical)
+ .SetIgnoreDefaultMainAxisMargins(true);
+
+
+ // Add the text field for editing the title.
+ views::View* title_field_container =
+ group_modifier_container->AddChildView(
+ std::make_unique<views::View>());
+ title_field_container->SetBorder(
+ views::CreateEmptyBorder(
+ control_insets.top(), control_insets.left(),
+ group_modifier_vertical_spacing,
+ control_insets.right()));
+
+ ...
+ const tab_groups::TabGroupColorId initial_color_id =
+ InitColorSet();
+ color_selector_ =
+ group_modifier_container->AddChildView(
+ std::make_unique<ColorPickerView>(
+ this, colors_, initial_color_id,
+ base::Bind(
+ &TabGroupEditorBubbleView::UpdateGroup,
+ base::Unretained(this))));
+ color_selector_->SetProperty(
+ views::kMarginsKey,
+ gfx::Insets(0, control_insets.left(), 0,
+ control_insets.right()));
+ ...
+}
+
+ColorPickerView::ColorPickerView(
+ const views::BubbleDialogDelegateView* bubble_view,
+ const TabGroupEditorBubbleView::Colors& colors,
+ tab_groups::TabGroupColorId initial_color_id,
+ ColorSelectedCallback callback)
+ : callback_(std::move(callback)) {
+ ...
+ // Set the internal padding to be equal to the
+ // horizontal insets of a color picker element,
+ // since that is the amount by which the color
+ // picker's margins should be adjusted to make it
+ // visually align with other controls.
+ gfx::Insets child_insets = elements_[0]->GetInsets();
+ SetProperty(views::kInternalPaddingKey,
+ gfx::Insets(0, child_insets.left(), 0,
+ child_insets.right()));
+}
+```
+
+|||---|||
+
+## Use GridLayout with caution
+
+[`GridLayout`][] is a `LayoutManager` used for tabular layouts. Much like
+table-based layout in HTML, it can achieve almost any desired effect, and in
+some scenarios (e.g. creating an actual table) is the best tool. Used
+indiscriminately, it can be cryptic, verbose, and error-prone. Accordingly,
+**use `GridLayout` only when creating a true grid, not simply for selective
+horizontal and vertical alignment.** For simple layouts, [`BoxLayout`][] and
+[`FlexLayout`][] are better choices; for more complex layouts, representing
+sections or groups hierarchically may result in simpler inner layouts that
+can be nested within an overall layout.
+
+[`GridLayout`]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/layout/grid_layout.h;l=79;drc=8cab3382ac9b70b7ecfe29ae03b1b7ee8f4e01fa
+[`BoxLayout`]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/layout/box_layout.h;l=28;drc=5b9e43d976aca377588875fc59c5348ede02a8b5
+[`FlexLayout`]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/layout/flex_layout.h;l=73;drc=62bf27aca5418212ceadd8daf9188d2aa437bfcc
+
+|||---|||
+
+#####
+
+**Avoid**
+
+The following old code uses a [`GridLayout`][] to create a HoverButton with
+a stacked title and subtitle flanked on by views on both sides.
+
+#####
+
+**Best practice**
+
+[Current code][4] uses [`FlexLayout`][] to achieve the desired result, resulting
+in clearer code.
+
+[4]: https://source.chromium.org/chromium/chromium/src/+/master:chrome/browser/ui/views/hover_button.cc;l=106;drc=888af74006ea1c4ee9907d18c8df2a7ca424eab9
+
+|||---|||
+
+|||---|||
+
+#####
+
+``` cpp
+// Used to create the following layout
+// +-----------+---------------------+----------------+
+// | icon_view | title | secondary_view |
+// +-----------+---------------------+----------------+
+// | | subtitle | |
+// +-----------+---------------------+----------------+
+HoverButton::HoverButton(
+ views::ButtonListener* button_listener,
+ std::unique_ptr<views::View> icon_view,
+ const base::string16& title,
+ const base::string16& subtitle,
+ std::unique_ptr<views::View> secondary_view,
+ bool resize_row_for_secondary_view,
+ bool secondary_view_can_process_events) {
+ ...
+ views::GridLayout* grid_layout =
+ SetLayoutManager(
+ std::make_unique<views::GridLayout>());
+ ...
+ constexpr int kColumnSetId = 0;
+ views::ColumnSet* columns =
+ grid_layout->AddColumnSet(kColumnSetId);
+ columns->AddColumn(
+ views::GridLayout::CENTER,
+ views::GridLayout::CENTER,
+ views::GridLayout::kFixedSize,
+ views::GridLayout::USE_PREF, 0, 0);
+ columns->AddPaddingColumn(
+ views::GridLayout::kFixedSize,
+ icon_label_spacing);
+ columns->AddColumn(
+ views::GridLayout::FILL,
+ views::GridLayout::FILL,
+ 1.0, views::GridLayout::USE_PREF, 0, 0);
+ ...
+ grid_layout->StartRow(
+ views::GridLayout::kFixedSize, kColumnSetId,
+ row_height);
+ icon_view_ = grid_layout->AddView(
+ std::move(icon_view), 1, num_labels);
+ ...
+ auto title_wrapper =
+ std::make_unique<SingleLineStyledLabelWrapper>(
+ title);
+ title_ = title_wrapper->label();
+ grid_layout->AddView(std::move(title_wrapper));
+
+ if (secondary_view) {
+ columns->AddColumn(
+ views::GridLayout::CENTER,
+ views::GridLayout::CENTER,
+ views::GridLayout::kFixedSize,
+ views::GridLayout::USE_PREF, 0, 0);
+ ...
+ secondary_view_ =
+ grid_layout->AddView(
+ std::move(secondary_view), 1, num_labels);
+ ...
+ }
+ if (!subtitle.empty()) {
+ grid_layout->StartRow(
+ views::GridLayout::kFixedSize, kColumnSetId,
+ row_height);
+ auto subtitle_label =
+ std::make_unique<views::Label>(
+ subtitle, views::style::CONTEXT_BUTTON,
+ views::style::STYLE_SECONDARY);
+ ...
+ grid_layout->SkipColumns(1);
+ subtitle_ =
+ grid_layout->AddView(std::move(subtitle_label));
+ }
+ ...
+}
+```
+
+#####
+
+``` cpp
+// Used to create the following layout
+// +-----------+---------------------+----------------+
+// | | title | |
+// | icon_view |---------------------| secondary_view |
+// | | subtitle | |
+// +-----------+---------------------+----------------+
+HoverButton::HoverButton(
+ views::ButtonListener* button_listener,
+ std::unique_ptr<views::View> icon_view,
+ const base::string16& title,
+ const base::string16& subtitle,
+ std::unique_ptr<views::View> secondary_view,
+ bool resize_row_for_secondary_view,
+ bool secondary_view_can_process_events) {
+ ...
+ SetLayoutManager(
+ std::make_unique<views::FlexLayout>())
+ ->SetCrossAxisAlignment(
+ views::LayoutAlignment::kCenter)
+ .SetChildViewIgnoredByLayout(
+ ink_drop_container(), true);
+ ...
+ icon_view_ =
+ AddChildView(std::make_unique<IconWrapper>(
+ std::move(icon_view), vertical_spacing))
+ ->icon();
+
+ // |label_wrapper| will hold both the title and
+ // subtitle if it exists.
+ auto label_wrapper = std::make_unique<views::View>();
+ title_ = label_wrapper->AddChildView(
+ std::make_unique<views::StyledLabel>(
+ title, nullptr));
+
+ if (!subtitle.empty()) {
+ auto subtitle_label =
+ std::make_unique<views::Label>(
+ subtitle, views::style::CONTEXT_BUTTON,
+ views::style::STYLE_SECONDARY);
+ ...
+ subtitle_ = label_wrapper->AddChildView(
+ std::move(subtitle_label));
+ }
+
+ label_wrapper->SetLayoutManager(
+ std::make_unique<views::FlexLayout>())
+ ->SetOrientation(
+ views::LayoutOrientation::kVertical)
+ .SetMainAxisAlignment(
+ views::LayoutAlignment::kCenter);
+ label_wrapper->SetProperty(
+ views::kFlexBehaviorKey,
+ views::FlexSpecification(
+ views::MinimumFlexSizeRule::kScaleToZero,
+ views::MaximumFlexSizeRule::kUnbounded));
+ label_wrapper->SetProperty(
+ views::kMarginsKey,
+ gfx::Insets(vertical_spacing, 0));
+ label_wrapper_ =
+ AddChildView(std::move(label_wrapper));
+ ...
+
+ if (secondary_view) {
+ ...
+ secondary_view->SetProperty(
+ views::kMarginsKey,
+ gfx::Insets(secondary_spacing,
+ icon_label_spacing,
+ secondary_spacing, 0));
+ secondary_view_ =
+ AddChildView(std::move(secondary_view));
+ }
+ ...
+}
+```
+
+|||---|||
+
+## Compute preferred/minimum sizes recursively from children
+
+**Avoid hardcoding preferred or minimum sizes,** including via metrics like
+[`DISTANCE_BUBBLE_PREFERRED_WIDTH`][].
+In many cases, `LayoutManager`s will provide reasonable values for these,
+and common codepaths like [`BubbleFrameView::GetFrameWidthForClientWidth()`][]
+can help ensure that the returned values are [conformed to spec][].
+When a `View` does need to calculate these manually, it should do so based on
+the corresponding values returned by its children, not by returning specific
+numbers (e.g. dialog preferred size is 300 by 150). In particular, assuming
+fonts will be in a certain size, or that a given fixed area is sufficient to
+display all necessary information, can cause hard-to-find localization and
+accessibility bugs for users with verbose languages or unusually large fonts.
+
+[`DISTANCE_BUBBLE_PREFERRED_WIDTH`]: https://source.chromium.org/chromium/chromium/src/+/master:chrome/browser/ui/views/chrome_layout_provider.h;l=68;drc=ec62c9ac3ef71a7014e27c5d2cf98917a89e3524
+[`BubbleFrameView::GetFrameWidthForClientWidth()`]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/bubble/bubble_frame_view.cc;l=688;drc=f5df5da5753795298349b2dd6325e2c5e6e13e38
+[conformed to spec]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/bubble/bubble_frame_view.cc;l=698;drc=f5df5da5753795298349b2dd6325e2c5e6e13e38
+
+|||---|||
+
+#####
+
+**Avoid**
+
+Current code overloads CalculatePreferredSize() in the dialog view.
+
+#####
+
+**Best practice**
+
+A better approach would be to omit the overload completely and let leaf views
+size the dialog appropriately, relying on the minimum size fallbacks
+if necessary.
+
+|||---|||
+
+|||---|||
+
+#####
+
+``` cpp
+...
+gfx::Size
+CastDialogView::CalculatePreferredSize() const {
+ const int width =
+ ChromeLayoutProvider::Get()->GetDistanceMetric(
+ DISTANCE_BUBBLE_PREFERRED_WIDTH);
+ return gfx::Size(width, GetHeightForWidth(width));
+}
+```
+
+#####
+
+``` cpp
+...
+```
+
+|||---|||
+
+
+## Handle events directly, not via Layout()
+
+In addition to using `LayoutManager`s in place of manual layout,
+**avoid overriding `Layout()` to perform non-layout actions.** For example,
+instead of updating properties tied to a `View`'s size in `Layout()`,
+do so in [`OnBoundsChanged()`][];
+when the `View` in question is a child, make the child a `View` subclass
+with an `OnBoundsChanged()` override instead of having the parent both lay
+the child out and update its properties. Modify the
+[hit-testing and event-handling functions][] directly instead of laying out
+invisible `View`s to intercept events. Toggle child visibility directly in
+response to external events rather than calculating it inside `Layout()`.
+
+[`OnBoundsChanged()`]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/view.h;l=1377;drc=34a8c4215229379ced3586125399c7ad3c65b87f
+[hit-testing and event-handling functions]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/view.h;l=919;drc=34a8c4215229379ced3586125399c7ad3c65b87f
+
+|||---|||
+
+#####
+
+**Avoid**
+
+Old code updated hit testing and button properties in the Layout() method.
+
+#####
+
+**Best practice**
+
+Current code wraps the buttons in a file scoped class with an
+OnBoundsChanged() method and modifies the hit testing functions directly to
+achieve the same result.
+
+|||---|||
+
+|||---|||
+
+#####
+
+``` cpp
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+FindBarView::FindBarView(FindBarHost* host)
+ : find_bar_host_(host) {
+ auto find_text = std::make_unique<views::Textfield>();
+ find_text_ = AddChildView(std::move(find_text));
+ ...
+ auto find_previous_button =
+ views::CreateVectorImageButton(this);
+ find_previous_button_ =
+ AddChildView(std::move(find_previous_button));
+ ...
+ auto find_next_button =
+ views::CreateVectorImageButton(this);
+ find_next_button_ =
+ AddChildView(std::move(find_next_button));
+ ...
+ auto close_button =
+ views::CreateVectorImageButton(this);
+ close_button_ =
+ AddChildView(std::move(close_button));
+}
+
+void FindBarView::Layout() {
+ views::View::Layout();
+ // The focus forwarder view is a hidden view that
+ // should cover the area between the find text box
+ // and the find button so that when the user clicks
+ // in that area we focus on the find text box.
+ const int find_text_edge =
+ find_text_->x() + find_text_->width();
+ focus_forwarder_view_->SetBounds(
+ find_text_edge, find_previous_button_->y(),
+ find_previous_button_->x() - find_text_edge,
+ find_previous_button_->height());
+
+ for (auto* button :
+ {find_previous_button_, find_next_button_,
+ close_button_}) {
+ constexpr int kCircleDiameterDp = 24;
+ auto highlight_path = std::make_unique<SkPath>();
+ // Use a centered circular shape for inkdrops and
+ // focus rings.
+ gfx::Rect circle_rect(button->GetLocalBounds());
+ circle_rect.ClampToCenteredSize(
+ gfx::Size(kCircleDiameterDp,
+ kCircleDiameterDp));
+ highlight_path->addOval(
+ gfx::RectToSkRect(circle_rect));
+ button->SetProperty(views::kHighlightPathKey,
+ highlight_path.release());
+ }
+}
+```
+
+#####
+
+``` cpp
+// An ImageButton that has a centered circular
+// highlight.
+class FindBarView::FindBarButton
+ : public views::ImageButton {
+ public:
+ using ImageButton::ImageButton;
+ protected:
+ void OnBoundsChanged(
+ const gfx::Rect& previous_bounds) override {
+ const gfx::Rect bounds = GetLocalBounds();
+ auto highlight_path = std::make_unique<SkPath>();
+ const gfx::Point center = bounds.CenterPoint();
+ const int radius = views::LayoutProvider::Get()
+ ->GetCornerRadiusMetric(
+ views::EMPHASIS_MAXIMUM, bounds.size());
+ highlight_path->addCircle(
+ center.x(), center.y(), radius);
+ SetProperty(views::kHighlightPathKey,
+ highlight_path.release());
+ }
+};
+
+bool FindBarView::OnMousePressed(
+ const ui::MouseEvent& event) {
+ // The find text box only extends to the match count
+ // label. However, users expect to be able to click
+ // anywhere inside what looks like the find text
+ // box (including on or around the match_count label)
+ // and have focus brought to the find box. Cause
+ // clicks between the textfield and the find previous
+ // button to focus the textfield.
+ const int find_text_edge =
+ find_text_->bounds().right();
+ const gfx::Rect focus_area(
+ find_text_edge, find_previous_button_->y(),
+ find_previous_button_->x() - find_text_edge,
+ find_previous_button_->height());
+ if (!GetMirroredRect(focus_area).Contains(
+ event.location()))
+ return false;
+ find_text_->RequestFocus();
+ return true;
+
+FindBarView::FindBarView(FindBarHost* host)
+ : find_bar_host_(host) {
+ auto find_text = std::make_unique<views::Textfield>();
+ find_text_ = AddChildView(std::move(find_text));
+ ...
+ auto find_previous_button =
+ std::make_unique<FindBarButton>(this);
+ views::ConfigureVectorImageButton(
+ find_previous_button.get());
+ ...
+ auto find_next_button =
+ std::make_unique<FindBarButton>(this);
+ views::ConfigureVectorImageButton(
+ find_next_button.get());
+ ...
+ auto close_button =
+ std::make_unique<FindBarButton>(this);
+ views::ConfigureVectorImageButton(close_button.get());
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+|||---|||
+
+## Don't invoke Layout() directly
+
+**Avoid direct calls to `Layout()`.**
+These are typically used for three purposes:
+
+1. *Calling `Layout()` on `this`, when something that affects layout has
+changed.* This forces a synchronous layout, which can lead to needless
+work (e.g. if several sequential changes each trigger layout). Use
+asynchronous layout\* instead. In many cases (such as
+[the preferred size changing][] or
+[a child needing layout][],
+a `View` will automatically mark itself as needing layout; when necessary, call
+[`InvalidateLayout()`][] to mark it manually.
+
+1. *Calling `Layout()` or `InvalidateLayout()` on some `View` to notify it
+that something affecting its layout has changed.* Instead, ensure that
+`View` is notified of the underlying change (via specific method overrides or
+plumbing from a model object), and then invalidates its own layout when needed.
+
+1. *Calling `Layout()` on some `View` to "ensure it's up to date" before
+reading some layout-related property off it.* Instead, plumb any relevant
+events to the current object, then handle them directly (e.g. override
+[`ChildPreferredSizeChanged()`][] or use a [`ViewObserver`][]
+to monitor the target `View`; then update local state as necessary and trigger
+handler methods).
+
+\* *How does asynchronous layout work?* In the browser, the compositor
+periodically [requests a LayerTreeHost update][].
+This ultimately calls back to [`Widget::LayoutRootViewIfNecessary()`][],
+recursively laying out invalidated `View`s within the `Widget`. In unittests,
+this compositor-driven sequence never occurs, so it's necessary to
+[call LayoutRootViewIfNecessary() manually][] when a test needs to ensure a
+`View`'s layout is up-to-date. Many tests fail to do this, but currently pass
+because something triggers Layout() directly;
+accordingly, changing existing code from synchronous to asynchronous layout may
+require adding `LayoutRootViewIfNecessary()` calls to (possibly many) tests,
+and this is not a sign that the change is wrong.
+
+[the preferred size changing]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/view.cc;l=1673;drc=bc9a6d40468646be476c61b6637b51729bec7b6d
+[a child needing layout]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/view.cc;l=777;drc=bc9a6d40468646be476c61b6637b51729bec7b6d
+[`InvalidateLayout()`]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/view.h;l=735;drc=c06f6b339b47ce2388624aa9a89334ace38a71e4
+[`ChildPreferredSizeChanged()`]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/view.h;l=1381;drc=c06f6b339b47ce2388624aa9a89334ace38a71e4
+[`ViewObserver`]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/view_observer.h;l=17;drc=eb20fd77330dc4a89eecf17459263e5895e7f177
+[requests a LayerTreeHost update]: https://source.chromium.org/chromium/chromium/src/+/master:cc/trees/layer_tree_host.cc;l=304;drc=c06f6b339b47ce2388624aa9a89334ace38a71e4
+[`Widget::LayoutRootViewIfNecessary()`]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/widget/widget.h;l=946;drc=b1dcb398c454a576092d38d0d67db3709b2b2a9b
+[call LayoutRootViewIfNecessary() manually]: https://source.chromium.org/chromium/chromium/src/+/master:ui/views/widget/widget_unittest.cc;l=3110;drc=c06f6b339b47ce2388624aa9a89334ace38a71e4
+
+|||---|||
+
+#####
+
+**Avoid**
+
+[Current code][5] makes a direct and unnecessary call to Layout()
+
+[5]: https://source.chromium.org/chromium/chromium/src/+/master:chrome/browser/ui/views/media_router/cast_dialog_view.cc;l=349;drc=18ca2de542bfa53802639dc5c85762b5e7b5bef6
+
+#####
+
+**Best practice**
+
+A better approach would be to call InvalidateLayout() and update the necessary tests.
+
+|||---|||
+
+|||---|||
+
+#####
+
+``` cpp
+void CastDialogView::PopulateScrollView(
+ const std::vector<UIMediaSink>& sinks) {
+ ...
+ Layout();
+}
+
+TEST_F(CastDialogViewTest, PopulateDialog) {
+ CastDialogModel model =
+ CreateModelWithSinks({CreateAvailableSink()});
+ InitializeDialogWithModel(model);
+
+ EXPECT_TRUE(dialog_->ShouldShowCloseButton());
+ EXPECT_EQ(model.dialog_header(),
+ dialog_->GetWindowTitle());
+ EXPECT_EQ(ui::DIALOG_BUTTON_NONE,
+ dialog_->GetDialogButtons());
+}
+
+
+```
+
+#####
+
+``` cpp
+void CastDialogView::PopulateScrollView(
+ const std::vector<UIMediaSink>& sinks) {
+ ...
+ InvalidateLayout();
+}
+
+TEST_F(CastDialogViewTest, PopulateDialog) {
+ CastDialogModel model =
+ CreateModelWithSinks({CreateAvailableSink()});
+ InitializeDialogWithModel(model);
+ CastDialogView::GetCurrentDialogWidget()
+ ->LayoutRootViewIfNecessary();
+
+ EXPECT_TRUE(dialog_->ShouldShowCloseButton());
+ EXPECT_EQ(model.dialog_header(),
+ dialog_->GetWindowTitle());
+ EXPECT_EQ(ui::DIALOG_BUTTON_NONE,
+ dialog_->GetDialogButtons());
+}
+```
+
+|||---|||
+
+
+## Consider different objects for different layouts
+
+If a surface needs very different appearances in different states (e.g. a
+dialog whose content changes at each of several steps, or a container whose
+layout toggles between disparate orientations), **use different `View`s to
+contain the distinct states** instead of manually adding and removing
+children and changing layout properties at each step. It's easier to reason
+about several distinct fixed-layout `View`s than a single object whose layout
+and children vary over time, and often more performant as well.
+
+|||---|||
+
+#####
+
+**Avoid**
+
+[Current code][6] holds both horizontal and vertical time views and replaces
+the children and LayoutManager on orientation change.
+
+[6]: https://source.chromium.org/chromium/chromium/src/+/master:ash/system/time/time_view.h;l=35;drc=7d8bc7f807a433e6a127806e991fe780aa27ce77;bpv=1;bpt=0?originalUrl=https:%2F%2Fcs.chromium.org%2F
+
+#####
+
+**Best practice**
+
+A better approach would encapsulate the horizontal and vertical time views
+into separate views.
+
+|||---|||
+
+|||---|||
+
+#####
+
+``` cpp
+class ASH_EXPORT TimeView : public ActionableView,
+ public ClockObserver {
+ ...
+ private:
+ ...
+ std::unique_ptr<views::Label> horizontal_label_;
+ std::unique_ptr<views::Label> vertical_label_hours_;
+ std::unique_ptr<views::Label> vertical_label_minutes_;
+ ...
+};
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+void TimeView::SetupLabels() {
+ horizontal_label_.reset(new views::Label());
+ SetupLabel(horizontal_label_.get());
+ vertical_label_hours_.reset(new views::Label());
+ SetupLabel(vertical_label_hours_.get());
+ vertical_label_minutes_.reset(new views::Label());
+ SetupLabel(vertical_label_minutes_.get());
+ ...
+}
+
+
+
+void TimeView::UpdateClockLayout(
+ ClockLayout clock_layout) {
+ // Do nothing if the layout hasn't changed.
+ if (((clock_layout == ClockLayout::HORIZONTAL_CLOCK) ?
+ horizontal_label_ : vertical_label_hours_)
+ ->parent() == this)
+ return;
+
+ SetBorder(views::NullBorder());
+ if (clock_layout == ClockLayout::HORIZONTAL_CLOCK) {
+ RemoveChildView(vertical_label_hours_.get());
+ RemoveChildView(vertical_label_minutes_.get());
+ SetLayoutManager(
+ std::make_unique<views::FillLayout>());
+ AddChildView(horizontal_label_.get());
+ } else {
+ RemoveChildView(horizontal_label_.get());
+ // Remove the current layout manager since it could
+ // be the FillLayout which only allows one child.
+ SetLayoutManager(nullptr);
+ // Pre-add the children since ownership is being
+ // retained by this.
+ AddChildView(vertical_label_hours_.get());
+ AddChildView(vertical_label_minutes_.get());
+ views::GridLayout* layout =
+ SetLayoutManager(
+ std::make_unique<views::GridLayout>());
+ const int kColumnId = 0;
+ views::ColumnSet* columns =
+ layout->AddColumnSet(kColumnId);
+ columns->AddPaddingColumn(
+ 0, kVerticalClockLeftPadding);
+ columns->AddColumn(views::GridLayout::TRAILING,
+ views::GridLayout::CENTER,
+ 0, views::GridLayout::USE_PREF,
+ 0, 0);
+ layout->AddPaddingRow(0, kClockLeadingPadding);
+ layout->StartRow(0, kColumnId);
+ // Add the views as existing since ownership isn't
+ // being transferred.
+ layout->AddExistingView(
+ vertical_label_hours_.get());
+ layout->StartRow(0, kColumnId);
+ layout->AddExistingView(
+ vertical_label_minutes_.get());
+ layout->AddPaddingRow(
+ 0, kVerticalClockMinutesTopOffset);
+ }
+ Layout();
+}
+```
+
+#####
+
+``` cpp
+class ASH_EXPORT TimeView : public ActionableView,
+ public ClockObserver {
+ ...
+ private:
+ class HorizontalLabelView;
+ class VerticalLabelView;
+ ...
+ HorizontalLabelView* horizontal_label_;
+ VerticalLabelView* vertical_label_;
+ ...
+};
+
+TimeView::HorizontalLabelView::HorizontalLabelView() {
+ SetLayoutManager(
+ std::make_unique<views::FillLayout>());
+ views::Label* time_label =
+ AddChildView(std::make_unique<views::Label>());
+ SetupLabels(time_label);
+ ...
+}
+
+TimeView::VerticalLabelView::VerticalLabelView() {
+ views::GridLayout* layout =
+ SetLayoutManager(
+ std::make_unique<views::GridLayout>());
+ views::Label* label_hours =
+ AddChildView(std::make_unique<views::Label>());
+ views::Label* label_minutes =
+ AddChildView(std::make_unique<views::Label>());
+ SetupLabel(label_hours);
+ SetupLabel(label_minutes);
+ const int kColumnId = 0;
+ views::ColumnSet* columns =
+ layout->AddColumnSet(kColumnId);
+ columns->AddPaddingColumn(
+ 0, kVerticalClockLeftPadding);
+ columns->AddColumn(
+ views::GridLayout::TRAILING,
+ views::GridLayout::CENTER,
+ 0, views::GridLayout::USE_PREF, 0, 0);
+ layout->AddPaddingRow(0, kClockLeadingPadding);
+ layout->StartRow(0, kColumnId);
+ // Add the views as existing since ownership isn't
+ // being transferred.
+ layout->AddExistingView(label_hours);
+ layout->StartRow(0, kColumnId);
+ layout->AddExistingView(label_minutes);
+ layout->AddPaddingRow(
+ 0, kVerticalClockMinutesTopOffset);
+ ...
+}
+
+void TimeView::TimeView(ClockLayout clock_layout,
+ ClockModel* model) {
+ ...
+ horizontal_label_ =
+ AddChildView(
+ std::make_unique<HorizontalLabelView>());
+ vertical_label_ =
+ AddChildView(
+ std::make_unique<VerticalLabelView());
+ ...
+}
+
+void TimeView::UpdateClockLayout(
+ ClockLayout clock_layout) {
+ ...
+ const bool is_horizontal =
+ clock_layout == ClockLayout::HORIZONTAL_CLOCK;
+ horizontal_label_->SetVisible(is_horizontal);
+ vertical_label_->SetVisible(!is_horizontal);
+ InvalidateLayout();
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+|||---|||
+