Wei Li | 098f5271 | 2020-07-10 20:26:12 | [diff] [blame] | 1 | # Example: Theme-Aware UI |
| 2 | |
| 3 | [TOC] |
| 4 | |
| 5 | ## Introduction |
| 6 | |
| 7 | A common pitfall in UI development is failing to handle varying themes and |
| 8 | colors. A perfect looking UI may become illegible after a theme change or on a |
| 9 | different background color, which can happen when changing the Chrome or OS |
| 10 | theme. |
| 11 | |
| 12 | This example shows you sample UI for a common scenario: click an icon to show |
| 13 | a dialog. A checkbox dynamically switches between light and dark themes to |
| 14 | demonstrate how the UI responds to theme changes. In this example, you can |
| 15 | learn a few common techniques to make UI handle theme changes correctly. |
| 16 | |
| 17 | |
| 18 | |
| 19 | | | |
| 20 | | :---: | |
| 21 | |  | |
| 22 | | Figure 1. Sample UI in light theme | |
| 23 | |
| 24 | |
| 25 | |
| 26 | | | |
| 27 | | :---: | |
| 28 | |  | |
| 29 | | Figure 2. Sample UI in dark theme | |
| 30 | |
| 31 | |
| 32 | ## Prerequisites |
| 33 | |
| 34 | This example assumes you are already familiar with Views toolkit fundamentals, |
| 35 | such as how to lay out UI elements and how to customize them. |
| 36 | |
| 37 | |
Wei Li | 4c62d3f | 2020-07-17 00:26:05 | [diff] [blame] | 38 | ## Run the example |
Wei Li | 098f5271 | 2020-07-10 20:26:12 | [diff] [blame] | 39 | |
| 40 | The example code is in the file |
John Palmer | 046f987 | 2021-05-24 01:24:56 | [diff] [blame] | 41 | [`ui/views/examples/colored_dialog_example.cc`](https://source.chromium.org/chromium/chromium/src/+/main:ui/views/examples/colored_dialog_example.cc) |
Wei Li | 098f5271 | 2020-07-10 20:26:12 | [diff] [blame] | 42 | and its corresponding header file. You can run it on Windows or Linux via |
| 43 | the `views_examples` application. Change the path accordingly based on your |
| 44 | platform and building environment: |
| 45 | |
| 46 | |
| 47 | ``` shell |
| 48 | $ autoninja -C out\Default views_examples |
| 49 | $ out\Default\views_examples --enable-examples="Colored Dialog" |
| 50 | ``` |
| 51 | |
| 52 | |
Wei Li | 4c62d3f | 2020-07-17 00:26:05 | [diff] [blame] | 53 | ## Technique 1: use existing Views controls |
Wei Li | 098f5271 | 2020-07-10 20:26:12 | [diff] [blame] | 54 | |
| 55 | The example's dialog consists of a title, a text field, and two buttons. |
| 56 | For all these components, you can use Views’ existing controls. The existing |
| 57 | controls were developed to be theme-aware and use correct colors by default. |
| 58 | When used appropriately, they should require no extra effort to handle dynamic |
| 59 | color changes. |
| 60 | |
| 61 | The following code snippet shows the creation of all the UI elements in the |
| 62 | dialog. Note how no color-specific code or dynamic theme changing handling |
| 63 | is necessary. |
| 64 | |
| 65 | |
| 66 | ``` cpp |
| 67 | SetTitle(l10n_util::GetStringUTF16(IDS_COLORED_DIALOG_TITLE)); |
| 68 | |
| 69 | SetLayoutManager(std::make_unique<views::FillLayout>()); |
| 70 | set_margins(views::LayoutProvider::Get()->GetDialogInsetsForContentType( |
Hwanseung Lee | 6d2d6a8 | 2021-04-15 02:48:51 | [diff] [blame] | 71 | views::DialogContentType::kControl, views::DialogContentType::kControl)); |
Wei Li | 098f5271 | 2020-07-10 20:26:12 | [diff] [blame] | 72 | |
| 73 | textfield_ = AddChildView(std::make_unique<views::Textfield>()); |
| 74 | textfield_->SetPlaceholderText( |
| 75 | l10n_util::GetStringUTF16(IDS_COLORED_DIALOG_TEXTFIELD_PLACEHOLDER)); |
| 76 | textfield_->SetAccessibleName( |
| 77 | l10n_util::GetStringUTF16(IDS_COLORED_DIALOG_TEXTFIELD_AX_LABEL)); |
| 78 | textfield_->set_controller(this); |
| 79 | |
| 80 | SetButtonLabel(ui::DIALOG_BUTTON_OK, |
| 81 | l10n_util::GetStringUTF16(IDS_COLORED_DIALOG_SUBMIT_BUTTON)); |
| 82 | SetButtonEnabled(ui::DIALOG_BUTTON_OK, false); |
| 83 | ``` |
| 84 | |
| 85 | |
Wei Li | 4c62d3f | 2020-07-17 00:26:05 | [diff] [blame] | 86 | ## Technique 2: override `OnThemeChanged()` in custom controls |
Wei Li | 098f5271 | 2020-07-10 20:26:12 | [diff] [blame] | 87 | |
| 88 | The checkbox in the main UI overrides `OnThemeChanged()` to implement |
| 89 | customized behavior (in this case, automatically adjusting its visible state |
| 90 | to externally-triggered theme changes). This method is called every time the |
| 91 | theme changes, including when a `View` is first shown. |
| 92 | |
| 93 | |
| 94 | ``` cpp |
Peter Kasting | 7e3f354 | 2020-11-04 20:37:29 | [diff] [blame] | 95 | class ThemeTrackingCheckbox : public views::Checkbox { |
Wei Li | 098f5271 | 2020-07-10 20:26:12 | [diff] [blame] | 96 | public: |
Jan Wilken Dörrie | 85285b0 | 2021-03-11 23:38:47 | [diff] [blame] | 97 | explicit ThemeTrackingCheckbox(const std::u16string& label) |
Wei Li | 098f5271 | 2020-07-10 20:26:12 | [diff] [blame] | 98 | : Checkbox(label, this) {} |
| 99 | ThemeTrackingCheckbox(const ThemeTrackingCheckbox&) = delete; |
| 100 | ThemeTrackingCheckbox& operator=(const ThemeTrackingCheckbox&) = delete; |
| 101 | ~ThemeTrackingCheckbox() override = default; |
| 102 | |
| 103 | // views::Checkbox: |
| 104 | void OnThemeChanged() override { |
| 105 | views::Checkbox::OnThemeChanged(); |
| 106 | |
| 107 | // Without this, the checkbox would not update for external (e.g. OS-driven) |
| 108 | // theme changes. |
| 109 | SetChecked(GetNativeTheme()->ShouldUseDarkColors()); |
| 110 | } |
| 111 | |
Peter Kasting | 7e3f354 | 2020-11-04 20:37:29 | [diff] [blame] | 112 | void ButtonPressed() { |
Wei Li | 098f5271 | 2020-07-10 20:26:12 | [diff] [blame] | 113 | GetNativeTheme()->set_use_dark_colors(GetChecked()); |
| 114 | |
| 115 | // An OS or Chrome theme change would do this automatically. |
| 116 | GetWidget()->ThemeChanged(); |
| 117 | } |
| 118 | }; |
| 119 | ``` |
| 120 | |
| 121 | |
| 122 | When creating controls using custom colors, setting the colors in |
| 123 | `OnThemeChanged()` (instead of in the constructor or in a call from another |
| 124 | object) ensures they will always be read from an up-to-date source and reset |
| 125 | whenever the theme changes. By contrast, setting them in the constructor will |
| 126 | not handle theme changes while the control is visible, and (depending on how |
| 127 | the colors are calculated) may not even work correctly to begin with. |
| 128 | |
| 129 | |
Wei Li | 4c62d3f | 2020-07-17 00:26:05 | [diff] [blame] | 130 | ## Technique 3: use theme neutral icons and images |
Wei Li | 098f5271 | 2020-07-10 20:26:12 | [diff] [blame] | 131 | |
| 132 | The button in the main UI contains an icon. Using a vector icon (as shown in |
| 133 | the example) makes it easy to re-rasterize to the correct color any time the |
| 134 | theme changes. |
| 135 | |
| 136 | |
| 137 | ``` cpp |
| 138 | AddChildView(std::make_unique<TextVectorImageButton>( |
Peter Kasting | 7e3f354 | 2020-11-04 20:37:29 | [diff] [blame] | 139 | base::BindRepeating(&ColoredDialogChooser::ButtonPressed, |
| 140 | base::Unretained(this)), |
| 141 | l10n_util::GetStringUTF16(IDS_COLORED_DIALOG_CHOOSER_BUTTON), |
Wei Li | 098f5271 | 2020-07-10 20:26:12 | [diff] [blame] | 142 | views::kInfoIcon)); |
| 143 | ``` |
| 144 | |
| 145 | While it's possible to create theme-aware or theme-neutral UI with bitmap |
| 146 | images as well, it's generally more difficult. Since vector icons typically |
| 147 | also provide better support for different scale factors than bitmaps do, |
| 148 | vector imagery is preferable in most cases. |
| 149 | |
| 150 | The following code snippet shows how to make the icon color adapt to the theme |
| 151 | change. |
| 152 | |
| 153 | ``` cpp |
| 154 | class TextVectorImageButton : public views::MdTextButton { |
| 155 | public: |
Peter Kasting | 7e3f354 | 2020-11-04 20:37:29 | [diff] [blame] | 156 | TextVectorImageButton(PressedCallback callback, |
Jan Wilken Dörrie | 85285b0 | 2021-03-11 23:38:47 | [diff] [blame] | 157 | const std::u16string& text, |
Wei Li | 098f5271 | 2020-07-10 20:26:12 | [diff] [blame] | 158 | const gfx::VectorIcon& icon) |
Peter Kasting | 7e3f354 | 2020-11-04 20:37:29 | [diff] [blame] | 159 | : MdTextButton(std::move(callback), text), icon_(icon) {} |
Wei Li | 098f5271 | 2020-07-10 20:26:12 | [diff] [blame] | 160 | TextVectorImageButton(const TextVectorImageButton&) = delete; |
| 161 | TextVectorImageButton& operator=(const TextVectorImageButton&) = delete; |
| 162 | ~TextVectorImageButton() override = default; |
| 163 | |
| 164 | void OnThemeChanged() override { |
| 165 | views::MdTextButton::OnThemeChanged(); |
| 166 | |
| 167 | // Use the text color for the associated vector image. |
| 168 | SetImage(views::Button::ButtonState::STATE_NORMAL, |
| 169 | gfx::CreateVectorIcon(icon_, label()->GetEnabledColor())); |
| 170 | } |
| 171 | ``` |
| 172 | |
| 173 | |
Wei Li | 4c62d3f | 2020-07-17 00:26:05 | [diff] [blame] | 174 | ## Learn more |
Wei Li | 098f5271 | 2020-07-10 20:26:12 | [diff] [blame] | 175 | |
| 176 | To experiment with all Views examples and controls, run the examples app |
| 177 | without any argument, which will show a list of all the examples and controls |
| 178 | you can try out: |
| 179 | |
| 180 | |
| 181 | ``` shell |
| 182 | $ out\Default\views_examples |
| 183 | ``` |
| 184 | |
| 185 | For more in-depth recommendations on working with colors in Views, read |
| 186 | [Best Practice: Colors](https://chromium.googlesource.com/chromium/src/+/HEAD/docs/ui/learn/bestpractices/colors.md). |
| 187 | |