# 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/+/main: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/+/main:ui/views/layout/layout_provider.h;l=129;drc=f5df5da5753795298349b2dd6325e2c5e6e13e38
[`ChromeLayoutProvider`]: https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/views/chrome_layout_provider.h;drc=ec62c9ac3ef71a7014e27c5d2cf98917a89e3524;l=76
[`Get()`]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/layout/layout_provider.h;drc=f5df5da5753795298349b2dd6325e2c5e6e13e38;l=135
[distances]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/layout/layout_provider.h;drc=f5df5da5753795298349b2dd6325e2c5e6e13e38;l=148
[insets]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/layout/layout_provider.h;drc=f5df5da5753795298349b2dd6325e2c5e6e13e38;l=144
[corner radii]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/layout/layout_provider.h;drc=f5df5da5753795298349b2dd6325e2c5e6e13e38;l=168
[shadow elevations]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/layout/layout_provider.h;drc=f5df5da5753795298349b2dd6325e2c5e6e13e38;l=172
[`Label`]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/controls/label.h;l=30;drc=59135b4042aa469752899e8e4bf2a0a81d3d320c
[`TypographyProvider`]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/style/typography_provider.h;l=22;drc=b5e29e075e814ed41e6727c281b69f797d8a1e10
[`ChromeTypographyProvider`]: https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/views/chrome_typography_provider.h;l=13;drc=a7ee000c95842e2dce6397ca36926924f4cb322b
[global helper functions]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/style/typography.h;l=109;drc=8f7db479018a99e5906876954de93ae6d23bee58
[fonts]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/style/typography_provider.h;l=28;drc=b5e29e075e814ed41e6727c281b69f797d8a1e10
[colors]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/style/typography_provider.h;l=32;drc=b5e29e075e814ed41e6727c281b69f797d8a1e10
[line heights]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/style/typography_provider.h;l=37;drc=b5e29e075e814ed41e6727c281b69f797d8a1e10
[context]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/style/typography.h;l=23;drc=8f7db479018a99e5906876954de93ae6d23bee58
[style]: https://source.chromium.org/chromium/chromium/src/+/main: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/+/main: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(std::u16string());
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>(
std::u16string()));
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/+/main:ui/views/view.h;l=730;drc=f5df5da5753795298349b2dd6325e2c5e6e13e38
[`LayoutManager`]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/layout/layout_manager.h;l=33;drc=f5df5da5753795298349b2dd6325e2c5e6e13e38
[bespoke `LayoutManager`]: https://source.chromium.org/chromium/chromium/src/+/main: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/+/main:chrome/browser/ui/views/relaunch_notification/relaunch_required_dialog_view.cc;l=91;drc=1ec33e7c19e2d63b3f918df115c12f77f419645b
[FillLayout]: https://source.chromium.org/chromium/chromium/src/+/main: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(PassKey) {
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/+/main:ui/base/class_property.h;l=55;drc=f5df5da5753795298349b2dd6325e2c5e6e13e38
[margins]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/view_class_properties.h;l=30;drc=1449b8c60358c4cdea1722e4c1e8079bd1b5f306
[margin collapsing]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/layout/flex_layout.h;l=87;drc=62bf27aca5418212ceadd8daf9188d2aa437bfcc
[internal padding]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/view_class_properties.h;l=40;drc=1449b8c60358c4cdea1722e4c1e8079bd1b5f306
[different padding amounts between different children]: https://source.chromium.org/chromium/chromium/src/+/main: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/+/main: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,
std::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::BindRepeating(
&TabGroupEditorBubbleView::UpdateGroup,
base::Unretained(this))));
...
}
```
#####
``` cpp
TabGroupEditorBubbleView::TabGroupEditorBubbleView(
const Browser* browser,
const tab_groups::TabGroupId& group,
views::View* anchor_view,
std::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::BindRepeating(
&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 TableLayout with caution
[`TableLayout`][] 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 `TableLayout` only when creating a true grid or table, 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.
[`TableLayout`]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/layout/table_layout.h;l=73;drc=f513afe81fca508d22153b192f1fab33e2c444fa
[`BoxLayout`]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/layout/box_layout.h;l=28;drc=5b9e43d976aca377588875fc59c5348ede02a8b5
[`FlexLayout`]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/layout/flex_layout.h;l=73;drc=62bf27aca5418212ceadd8daf9188d2aa437bfcc
|||---|||
#####
**Avoid**
The following old code uses a [`TableLayout`][] 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/+/main:chrome/browser/ui/views/controls/hover_button.cc;l=129;drc=0139ceffb2f8e1f64b7c30834e57d5793e529ed7
|||---|||
|||---|||
#####
``` cpp
// Used to create the following layout
// +-----------+---------------------+----------------+
// | icon_view | title | secondary_view |
// +-----------+---------------------+----------------+
// | | subtitle | |
// +-----------+---------------------+----------------+
HoverButton::HoverButton(
...
std::unique_ptr<views::View> icon_view,
const std::u16string& title,
const std::u16string& subtitle,
std::unique_ptr<views::View> secondary_view,
...) {
...
views::TableLayout* table_layout =
SetLayoutManager(
std::make_unique<views::TableLayout>());
...
table_layout->AddColumn(
views::LayoutAlignment::kCenter,
views::LayoutAlignment::kCenter,
views::TableLayout::kFixedSize,
views::TableLayout::kUsePreferred, 0, 0);
table_layout->AddPaddingColumn(
views::TableLayout::kFixedSize,
icon_label_spacing);
table_layout->AddColumn(
views::LayoutAlignment::kStretch,
views::LayoutAlignment::kStretch, 1.0f,
views::TableLayout::kUsePreferred, 0, 0);
...
table_layout->AddRows(
1, views::TableLayout::kFixedSize,
row_height);
icon_view_ = AddChildView(
std::move(icon_view), 1, num_labels);
...
auto title_wrapper =
std::make_unique<SingleLineStyledLabelWrapper>(
title);
title_ = title_wrapper->label();
AddChildView(std::move(title_wrapper));
if (secondary_view) {
table_layout->AddColumn(
views::LayoutAlignment::kCenter,
views::LayoutAlignment::kCenter,
views::TableLayout::kFixedSize,
views::TableLayout::kUsePreferred, 0, 0);
...
secondary_view_ = AddChildView(
std::move(secondary_view), 1, num_labels);
...
}
if (!subtitle.empty()) {
table_layout->AddRows(
1, views::TableLayout::kFixedSize,
row_height);
auto subtitle_label =
std::make_unique<views::Label>(
subtitle, views::style::CONTEXT_BUTTON,
views::style::STYLE_SECONDARY);
...
AddChildView(std::make_unique<views::View>());
subtitle_ =
AddChildView(std::move(subtitle_label));
}
...
}
```
#####
``` cpp
// Used to create the following layout
// +-----------+---------------------+----------------+
// | | title | |
// | icon_view |---------------------| secondary_view |
// | | subtitle | |
// +-----------+---------------------+----------------+
HoverButton::HoverButton(
...
std::unique_ptr<views::View> icon_view,
const std::u16string& title,
const std::u16string& subtitle,
std::unique_ptr<views::View> secondary_view,
...) {
...
// Set the layout manager to ignore the
// ink_drop_container to ensure the ink drop tracks
// the bounds of its parent.
ink_drop_container()->SetProperty(
views::kViewIgnoredByLayoutKey, true);
SetLayoutManager(
std::make_unique<views::FlexLayout>())
->SetCrossAxisAlignment(
views::LayoutAlignment::kCenter);
...
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/+/main:chrome/browser/ui/views/chrome_layout_provider.h;l=68;drc=ec62c9ac3ef71a7014e27c5d2cf98917a89e3524
[`BubbleFrameView::GetFrameWidthForClientWidth()`]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/bubble/bubble_frame_view.cc;l=688;drc=f5df5da5753795298349b2dd6325e2c5e6e13e38
[conformed to spec]: https://source.chromium.org/chromium/chromium/src/+/main: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/+/main:ui/views/view.h;l=1377;drc=34a8c4215229379ced3586125399c7ad3c65b87f
[hit-testing and event-handling functions]: https://source.chromium.org/chromium/chromium/src/+/main: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(PassKey) {
LayoutSuperclass<views::View>(this);
// 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::kMaximum, 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 DeprecatedLayoutImmediately()
**Avoid calls to `DeprecatedLayoutImmediately()`.**
These are typically used for three purposes:
1. *Calling `DeprecatedLayoutImmediately()` 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 `DeprecatedLayoutImmediately()` 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 `DeprecatedLayoutImmediately()` 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 RunScheduledLayout() 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 `RunScheduledLayout()`
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/+/main:ui/views/view.cc;l=1673;drc=bc9a6d40468646be476c61b6637b51729bec7b6d
[a child needing layout]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/view.cc;l=777;drc=bc9a6d40468646be476c61b6637b51729bec7b6d
[`InvalidateLayout()`]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/view.h;l=735;drc=c06f6b339b47ce2388624aa9a89334ace38a71e4
[`ChildPreferredSizeChanged()`]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/view.h;l=1381;drc=c06f6b339b47ce2388624aa9a89334ace38a71e4
[`ViewObserver`]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/view_observer.h;l=17;drc=eb20fd77330dc4a89eecf17459263e5895e7f177
[requests a LayerTreeHost update]: https://source.chromium.org/chromium/chromium/src/+/main:cc/trees/layer_tree_host.cc;l=304;drc=c06f6b339b47ce2388624aa9a89334ace38a71e4
[`Widget::LayoutRootViewIfNecessary()`]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/widget/widget.h;l=946;drc=b1dcb398c454a576092d38d0d67db3709b2b2a9b
[call RunScheduledLayout() manually]: https://source.chromium.org/chromium/chromium/src/+/main:ui/views/test/views_test_utils.h;l=17;drc=3e1a26c44c024d97dc9a4c09bbc6a2365398ca2c
|||---|||
#####
**Avoid**
[Current code][5] makes an unnecessary call to DeprecatedLayoutImmediately()
[5]: https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/views/media_router/cast_dialog_view.cc;l=283;drc=7db01ff4534c04419f3fa10d75e0b97b0a5a4f99
#####
**Best practice**
A better approach would be to call InvalidateLayout() and update the necessary tests.
|||---|||
|||---|||
#####
``` cpp
void CastDialogView::PopulateScrollView(
const std::vector<UIMediaSink>& sinks) {
...
DeprecatedLayoutImmediately();
}
TEST_F(CastDialogViewTest, PopulateDialog) {
CastDialogModel model =
CreateModelWithSinks({CreateAvailableSink()});
InitializeDialogWithModel(model);
EXPECT_TRUE(dialog_->ShouldShowCloseButton());
EXPECT_EQ(model.dialog_header(),
dialog_->GetWindowTitle());
EXPECT_EQ(static_cast<int>(ui::mojom::DialogButton::kNone),
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(static_cast<int>(ui::mojom::DialogButton::kNone),
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/+/main: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);
}
DeprecatedLayoutImmediately();
}
```
#####
``` 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::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);
SetLayoutManager(
std::make_unique<views::TableLayout>())
->AddPaddingColumn(
views::TableLayout::kFixedSize,
kVerticalClockLeftPadding)
.AddColumn(
views::LayoutAlignment::kEnd,
views::LayoutAlignment::kCenter,
views::TableLayout::kFixedSize,
views::TableLayout::kUsePreferred, 0, 0)
.AddPaddingRow(
views::TableLayout::kFixedSize,
kClockLeadingPadding)
.AddRows(2, views::TableLayout::kFixedSize)
.AddPaddingRow(
views::TableLayout::kFixedSize,
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();
}
```
|||---|||