0

personalization: add color selector to customization dialog

Displays the color options for each zone in the pop up view. When a user
selects a zone, corresponding color will be highlighted. If another
color is selected for a zone, updates the zone color for that zone.
http://go/scrcast/NDg2OTQ4MzI0MDg4MjE3NnxmNmE5NzY3Zi1jOQ

BUG=b:265855838, b:269137429, b:265855854
TEST=browser_tests --gtest_filter='*KeyboardBacklight*'
TEST=browser_tests --gtest_filter='*ZoneCustomization*'
TEST=unit_tests --gtest_filter='*KeyboardBacklight*'

Change-Id: Idf168ce7bd7d2d8cf2c6121e68ef84d318875355
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4307552
Reviewed-by: Jason Thai <jasontt@chromium.org>
Reviewed-by: David Padlipsky <dpad@google.com>
Commit-Queue: Thuong Phan <thuongphan@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1118949}
This commit is contained in:
Thuong Phan
2023-03-17 23:31:09 +00:00
committed by Chromium LUCI CQ
parent 4f8154b2a9
commit dcde19a752
12 changed files with 297 additions and 62 deletions

@ -39,6 +39,11 @@ class ASH_EXPORT RgbKeyboardManager : public ImeControllerImpl::Observer,
void SetRainbowMode();
void SetAnimationMode(rgbkbd::RgbAnimationMode mode);
// RgbkbdClient::Observer:
// Also used in tests to override the keyboard capability.
void OnCapabilityUpdatedForTesting(
rgbkbd::RgbKeyboardCapabilities capability) override;
// Returns the global instance if initialized. May return null.
static RgbKeyboardManager* Get();
@ -65,11 +70,6 @@ class ASH_EXPORT RgbKeyboardManager : public ImeControllerImpl::Observer,
void OnCapsLockChanged(bool enabled) override;
void OnKeyboardLayoutNameChanged(const std::string&) override {}
// RgbkbdClient::Observer:
// Also used in tests to override the keyboard capability.
void OnCapabilityUpdatedForTesting(
rgbkbd::RgbKeyboardCapabilities capability) override;
void FetchRgbKeyboardSupport();
void OnGetRgbKeyboardCapabilities(

@ -13,10 +13,8 @@
align-items: center;
border-radius: 50%;
display: flex;
height: 28px;
justify-content: center;
position: relative;
width: 28px;
}
:host-context([aria-checked='true']) .color-inner-container {
@ -24,6 +22,12 @@
width: 36px;
}
:host-context([aria-checked='false']) .color-inner-container,
:host-context(.zone-title-container) .color-inner-container {
height: 28px;
width: 28px;
}
.dark-icon {
fill: var(--cros-icon-color-primary-dark);
}

@ -2,6 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview
* The color icon indicates wallpaper or preset colors in keyboard backlight and
* zone customization section.
*/
import {SkColor} from 'chrome://resources/mojo/skia/public/mojom/skcolor.mojom-webui.js';
import {WithPersonalizationStore} from '../personalization_store.js';
@ -9,12 +15,6 @@ import {convertToRgbHexStr, getPresetColors, GREEN, INDIGO, RAINBOW, RED, WALLPA
import {getTemplate} from './color_icon_element.html.js';
/**
* @fileoverview
* The color icon indicates wallpaper or preset colors in keyboard backlight and
* zone customization section.
*/
/**
Based on this algorithm suggested by the W3:
https://www.w3.org/TR/AERT/#color-contrast

@ -87,12 +87,12 @@
role="radiogroup" aria-label="[[getColorSelectorAriaLabel_()]]">
<template is="dom-if" if="[[!isMultiZoneRgbKeyboardSupported_]]">
<div id$="[[wallpaperColorId_]]"
class$="[[getWallpaperColorContainerClass_(backlightColor_)]]"
class$="[[getWallpaperColorContainerClass_(selectedColor)]]"
tabindex="0"
on-click="onWallpaperColorSelected_"
on-keypress="onWallpaperColorSelected_"
aria-label="$i18n{wallpaperColor}"
aria-checked$="[[getWallpaperColorAriaChecked_(backlightColor_)]]"
aria-checked$="[[getWallpaperColorAriaChecked_(selectedColor)]]"
title$="[[getWallpaperColorTitle_()]]"
role="radio">
<color-icon color-id="[[wallpaperColorId_]]"></color-icon>
@ -126,10 +126,10 @@
</template>
<template is="dom-if" if="[[!isCustomizedDialog]]">
<div id$="[[rainbowColorId_]]"
class$="[[getRainbowColorContainerClass_(backlightColor_)]]"
class$="[[getRainbowColorContainerClass_(selectedColor)]]"
on-click="onRainbowColorSelected_" on-keypress="onRainbowColorSelected_"
aria-label="$i18n{rainbowColor}" role="radio"
aria-checked$="[[getRainbowColorAriaChecked_(backlightColor_)]]"
aria-checked$="[[getRainbowColorAriaChecked_(selectedColor)]]"
title="$i18n{rainbowColor}">
<color-icon color-id="[[rainbowColorId_]]"></color-icon>
</div>

@ -103,13 +103,7 @@ export class ColorSelector extends WithPersonalizationStore {
currentBacklightState_: Object,
/** The selected backlight color if single color is selected. */
backlightColor_: {
type: Object,
computed: 'computeBacklightColor_(currentBacklightState_)',
},
selectedColor: String,
selectedColor: BacklightColor,
/** The current wallpaper extracted color. */
wallpaperColor_: Object,
@ -129,8 +123,7 @@ export class ColorSelector extends WithPersonalizationStore {
private rainbowColorId_: string;
private wallpaperColorId_: string;
private currentBacklightState_: CurrentBacklightState|null;
private backlightColor_: BacklightColor|null;
private selectedColor: string;
private selectedColor: BacklightColor;
private wallpaperColor_: SkColor|null;
private shouldShowNudge_: boolean;
@ -205,11 +198,6 @@ export class ColorSelector extends WithPersonalizationStore {
return !this.isCustomizedDialog;
}
private computeBacklightColor_(currentBacklightState: CurrentBacklightState):
BacklightColor|null|undefined {
return currentBacklightState ? currentBacklightState.color : null;
}
private computePresetColorIds_(presetColors: Record<string, string>):
string[] {
// ES2020 maintains ordering of Object.keys.

@ -7,7 +7,6 @@ import {SkColor} from 'chrome://resources/mojo/skia/public/mojom/skcolor.mojom-w
import {CurrentBacklightState} from '../../personalization_app.mojom-webui.js';
/**
* @fileoverview Defines the actions to update keyboard backlight settings.
*/

@ -19,15 +19,21 @@ export function setBacklightColor(
provider: KeyboardBacklightProviderInterface, store: PersonalizationStore) {
provider.setBacklightColor(backlightColor);
// Dispatch action to set the current backlight state.
// Dispatch action to set the current backlight state - color.
store.dispatch(setCurrentBacklightStateAction({color: backlightColor}));
}
// Set the keyboard backlight color for the given zone.
export function setBacklightZoneColor(
zone: number, backlightColor: BacklightColor,
provider: KeyboardBacklightProviderInterface) {
zone: number, backlightColor: BacklightColor, zoneColors: BacklightColor[],
provider: KeyboardBacklightProviderInterface, store: PersonalizationStore) {
provider.setBacklightZoneColor(zone, backlightColor);
// Dispatch action to set the current backlight state - zone colors.
const updatedZoneColors = [...zoneColors];
updatedZoneColors[zone] = backlightColor;
store.dispatch(
setCurrentBacklightStateAction({zoneColors: updatedZoneColors}));
}
// Set the should show nudge boolean.

@ -67,7 +67,6 @@
</cr-button>
</div>
</color-selector>
<cr-lazy-render id="zoneCustomizationRender">
<template>
<zone-customization></zone-customization>

@ -1,28 +1,41 @@
<style include="common cros-button-style">
cr-dialog::part(dialog) {
padding-bottom: 20px;
width: 600px;
}
#zoneSelector {
background-color: var(--cros-sys-system_on_base);
border-radius: 18px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(0,1fr));
grid-template-rows: minmax(0,1fr);
margin-block-end: 20px;
width: 100%;
}
.zone-button[aria-checked='true'] {
background-color: var(--cros-sys-system_primary_container);
}
.zone-button {
border-style: none;
height: 100%;
width: 100%;
}
.zone-title-container {
align-items: center;
display: flex;
width: 100%;
}
color-icon {
.zone-title-container > color-icon {
height: auto;
margin-inline-end: 10px;
width: auto;
}
#zoneTitle {
max-width: calc(100% - 40px);
word-wrap: break-word;
@ -30,6 +43,8 @@
</style>
<cr-dialog id="dialog" on-close="closeZoneCustomizationDialog_">
<div slot="body">
<iron-a11y-keys id="zoneKeys" keys="left right" on-keys-pressed="onZoneKeysPress_">
</iron-a11y-keys>
<iron-selector
id="zoneSelector"
selected="0"
@ -37,8 +52,10 @@
<template is="dom-repeat" items="[[zoneIdxs_]]" as="zoneIdx">
<cr-button
class="zone-button tab-slider"
tabindex$="[[getTabIndex_(zoneIdx)]]"
on-click="onClickZoneButton_">
tabindex$="[[getZoneTabIndex_(zoneIdx)]]"
data-zone-idx$="[[zoneIdx]]"
on-click="onClickZoneButton_"
aria-checked$="[[getZoneAriaChecked_(zoneIdx, zoneSelected_)]]">
<div class="zone-title-container">
<color-icon color-id="[[getColorId_(zoneIdx, zoneColors_)]]"></color-icon>
<div id="zoneTitle">[[getZoneTitle_(zoneIdx)]]</div>
@ -46,12 +63,16 @@
</cr-button>
</template>
</iron-selector>
<!-- TODO(b/265855838): Add color selector -->
<cr-button on-click="setZoneOneToRed_">[temp] Red</cr-button>
</div>
<div slot="button-container">
<cr-button class="primary" id="dialogCloseButton" on-click="closeZoneCustomizationDialog_">
$i18n{dismissButtonText}
</cr-button>
<color-selector
is-customized-dialog
selected-color="[[getSelectedColor_(zoneSelected_, zoneColors_)]]"
on-wallpaper-color-selected="onWallpaperColorSelected_"
on-preset-color-selected="onPresetColorSelected_">
<div slot="button-container" class="customization-button-container">
<cr-button class="primary" id="dialogCloseButton" on-click="closeZoneCustomizationDialog_">
$i18n{dismissButtonText}
</cr-button>
</div>
</color-selector>
</div>
</cr-dialog>

@ -8,16 +8,21 @@
* zone color.
*/
import 'chrome://resources/polymer/v3_0/iron-a11y-keys/iron-a11y-keys.js';
import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
import './color_icon_element.js';
import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
import {CrDialogElement} from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
import {assert} from 'chrome://resources/js/assert_ts.js';
import {IronA11yKeysElement} from 'chrome://resources/polymer/v3_0/iron-a11y-keys/iron-a11y-keys.js';
import {IronSelectorElement} from 'chrome://resources/polymer/v3_0/iron-selector/iron-selector.js';
import {BacklightColor, CurrentBacklightState} from '../../personalization_app.mojom-webui.js';
import {WithPersonalizationStore} from '../personalization_store.js';
import {RAINBOW, staticColorIds} from '../utils.js';
import {getPresetColors, isSelectionEvent, RAINBOW, staticColorIds} from '../utils.js';
import {PresetColorSelectedEvent} from './color_selector_element.js';
import {setBacklightZoneColor} from './keyboard_backlight_controller.js';
import {getKeyboardBacklightProvider} from './keyboard_backlight_interface_provider.js';
import {getTemplate} from './zone_customization_element.html.js';
@ -25,6 +30,8 @@ import {getTemplate} from './zone_customization_element.html.js';
export interface ZoneCustomizationElement {
$: {
dialog: CrDialogElement,
zoneKeys: IronA11yKeysElement,
zoneSelector: IronSelectorElement,
};
}
@ -39,6 +46,11 @@ export class ZoneCustomizationElement extends WithPersonalizationStore {
static get properties() {
return {
zoneSelected_: {
type: Number,
value: 0,
},
/** The currently selected zone index. */
ironSelectedZone_: Object,
@ -67,11 +79,17 @@ export class ZoneCustomizationElement extends WithPersonalizationStore {
};
}
private ironSelectedZone_: number;
private zoneSelected_: number;
private ironSelectedZone_: HTMLElement;
private currentBacklightState_: CurrentBacklightState|null;
private zoneColors_: BacklightColor[]|null;
private zoneCount_: number;
private zoneIds_: number[];
private zoneIdxs_: number[];
override ready() {
super.ready();
this.$.zoneKeys.target = this.$.zoneSelector;
}
override connectedCallback() {
super.connectedCallback();
@ -85,15 +103,6 @@ export class ZoneCustomizationElement extends WithPersonalizationStore {
this.$.dialog.showModal();
}
/**
* Sets zone one color to red. TODO(b/265855838): Remove after the color
* selector is implemented.
*/
private setZoneOneToRed_() {
setBacklightZoneColor(
0, BacklightColor.kRed, getKeyboardBacklightProvider());
}
private computeZoneIdxs_(): number[] {
return [...Array(this.zoneCount_).keys()];
}
@ -109,14 +118,89 @@ export class ZoneCustomizationElement extends WithPersonalizationStore {
return null;
}
private getTabIndex_(zoneIdx: number): string {
return zoneIdx === 0 ? '0' : '-1';
/** Handle keyboard navigation. */
private onZoneKeysPress_(
e: CustomEvent<{key: string, keyboardEvent: KeyboardEvent}>) {
const selector = this.$.zoneSelector;
const prevButton = this.ironSelectedZone_;
switch (e.detail.key) {
case 'left':
selector.selectPrevious();
break;
case 'right':
selector.selectNext();
break;
default:
return;
}
// Remove focus state of previous button.
if (prevButton) {
prevButton.removeAttribute('tabindex');
}
// Add focus state for new button.
if (this.ironSelectedZone_) {
this.ironSelectedZone_.setAttribute('tabindex', '0');
this.ironSelectedZone_.focus();
}
e.detail.keyboardEvent.preventDefault();
}
private getZoneTitle_(zoneIdx: number): string {
private onClickZoneButton_(event: Event) {
if (!isSelectionEvent(event)) {
return;
}
const eventTarget = event.currentTarget as HTMLElement;
this.zoneSelected_ = Number(eventTarget.dataset['zoneIdx']);
}
private onWallpaperColorSelected_() {
if (!this.zoneColors_) {
return;
}
const currentColor =
this.getSelectedColor_(this.zoneSelected_, this.zoneColors_);
if (currentColor !== BacklightColor.kWallpaper) {
setBacklightZoneColor(
this.zoneSelected_, BacklightColor.kWallpaper, this.zoneColors_,
getKeyboardBacklightProvider(), this.getStore());
}
}
private onPresetColorSelected_(e: PresetColorSelectedEvent) {
if (!this.zoneColors_) {
return;
}
const currentColor =
this.getSelectedColor_(this.zoneSelected_, this.zoneColors_);
const colorId = e.detail.colorId;
assert(colorId !== undefined, 'colorId not found');
const newColor = getPresetColors()[colorId].enumVal;
if (currentColor !== newColor) {
setBacklightZoneColor(
this.zoneSelected_, newColor, this.zoneColors_,
getKeyboardBacklightProvider(), this.getStore());
}
}
private getZoneTabIndex_(zoneIdx: number): string {
return zoneIdx === 0 ? '0' : '1';
}
private getZoneAriaChecked_(zoneIdx: number, zoneSelected: number) {
return (zoneIdx === zoneSelected).toString();
}
private getZoneTitle_(zoneIdx: number) {
return loadTimeData.getStringF('zoneTitle', zoneIdx + 1);
}
private getSelectedColor_(zoneSelected: number, zoneColors: BacklightColor[]):
BacklightColor|null {
return zoneColors ? zoneColors[zoneSelected] : null;
}
// Returns the matching colorId for each zone based on its zone color.
private getColorId_(zoneIdx: number, zoneColors: BacklightColor[]): string
|null {

@ -8,6 +8,8 @@
#include <vector>
#include "ash/constants/ash_features.h"
#include "ash/rgb_keyboard/rgb_keyboard_manager.h"
#include "ash/shell.h"
#include "ash/system/keyboard_brightness/keyboard_backlight_color_controller.h"
#include "ash/webui/personalization_app/mojom/personalization_app.mojom.h"
#include "base/test/metrics/histogram_tester.h"
@ -154,6 +156,12 @@ class PersonalizationAppKeyboardBacklightProviderImplTest
return test_keyboard_backlight_observer_.wallpaper_color();
}
void set_rgb_capability(rgbkbd::RgbKeyboardCapabilities capability) {
RgbKeyboardManager* rgb_keyboard_manager =
Shell::Get()->rgb_keyboard_manager();
rgb_keyboard_manager->OnCapabilityUpdatedForTesting(capability);
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
user_manager::ScopedUserManager scoped_user_manager_;
@ -187,6 +195,28 @@ TEST_F(PersonalizationAppKeyboardBacklightProviderImplTest, SetBacklightColor) {
mojom::BacklightColor::kBlue, 1);
}
TEST_F(PersonalizationAppKeyboardBacklightProviderImplTest,
SetBacklightZoneColor) {
SimulateUserLogin(account_id);
SetKeyboardBacklightObserver();
set_rgb_capability(rgbkbd::RgbKeyboardCapabilities::kFourZoneFortyLed);
keyboard_backlight_provider_remote()->FlushForTesting();
keyboard_backlight_provider()->SetBacklightColor(
mojom::BacklightColor::kBlue);
EXPECT_TRUE(ObservedBacklightColor()->is_color());
keyboard_backlight_provider()->SetBacklightZoneColor(
1, mojom::BacklightColor::kRed);
// Verify JS side is notified.
EXPECT_TRUE(ObservedBacklightColor()->is_zone_colors());
const auto zone_colors = ObservedBacklightColor()->get_zone_colors();
for (size_t i = 0; i < zone_colors.size(); i++) {
EXPECT_EQ(zone_colors[i], i == 1 ? mojom::BacklightColor::kRed
: mojom::BacklightColor::kBlue);
}
}
TEST_F(PersonalizationAppKeyboardBacklightProviderImplTest,
ObserveWallpaperColor) {
SimulateUserLogin(account_id);

@ -5,7 +5,8 @@
import 'chrome://personalization/strings.m.js';
import 'chrome://webui-test/mojo_webui_test_support.js';
import {CurrentBacklightState, KeyboardBacklightActionName, KeyboardBacklightObserver, SetCurrentBacklightStateAction, staticColorIds, ZoneCustomizationElement} from 'chrome://personalization/js/personalization_app.js';
import {BacklightColor, CurrentBacklightState, KeyboardBacklightActionName, KeyboardBacklightObserver, SetCurrentBacklightStateAction, staticColorIds, ZoneCustomizationElement} from 'chrome://personalization/js/personalization_app.js';
import {CrButtonElement} from 'chrome://resources/cr_elements/cr_button/cr_button.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {assertDeepEquals, assertEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {waitAfterNextRender} from 'chrome://webui-test/polymer_test_util.js';
@ -42,6 +43,23 @@ suite('ZoneCustomizationElementTest', function() {
await waitAfterNextRender(zoneCustomizationElement);
}
function verifyColorContainerAriaChecked(
expectedColor: string, colorContainers: NodeListOf<Element>) {
for (let i = 0; i < colorContainers!.length; i++) {
const colorContainer = colorContainers[i] as HTMLElement;
const colorId = colorContainer.id;
if (colorId === expectedColor) {
assertEquals(
'true', colorContainer.ariaChecked,
`${expectedColor} should be highlighted.`);
} else {
assertEquals(
'false', colorContainer.ariaChecked,
`${colorId} should not be highlighted.`);
}
}
}
test(
'displays content with current backlight state as a static color',
async () => {
@ -56,6 +74,15 @@ suite('ZoneCustomizationElementTest', function() {
assertEquals(
5, zoneButtons!.length,
'5 zones should display in customization dialog');
const colorSelectorElement =
zoneCustomizationElement!.shadowRoot!.querySelector(
'color-selector');
assertTrue(!!colorSelectorElement);
const colorContainers =
colorSelectorElement.shadowRoot!.querySelectorAll('.selectable');
assertEquals(
8, colorContainers!.length,
'8 color options should display in customization dialog');
const dialogCloseButton =
zoneCustomizationElement!.shadowRoot!.getElementById(
'dialogCloseButton');
@ -113,4 +140,81 @@ suite('ZoneCustomizationElementTest', function() {
SetCurrentBacklightStateAction;
assertDeepEquals(currentBacklightState, action.currentBacklightState);
});
test('displays correct zone color when a zone is selected', async () => {
keyboardBacklightProvider.setZoneCount(4);
keyboardBacklightProvider.setCurrentBacklightState(
{zoneColors: keyboardBacklightProvider.zoneColors});
await initZoneCustomizationElement();
const zoneSelector =
zoneCustomizationElement!.shadowRoot!.getElementById('zoneSelector');
assertTrue(!!zoneSelector, 'zone selector should display');
const zoneButtons =
zoneCustomizationElement!.shadowRoot!.querySelectorAll('.zone-button');
assertEquals(
4, zoneButtons!.length,
'4 zones should display in customization dialog');
// Zone 2 has zone color as red, expect red color button to be highlighted.
(zoneButtons[1] as CrButtonElement).click();
const colorSelectorElement =
zoneCustomizationElement!.shadowRoot!.querySelector('color-selector');
assertTrue(!!colorSelectorElement, 'color-selector should display.');
const colorContainers =
colorSelectorElement.shadowRoot!.querySelectorAll('.selectable');
assertEquals(8, colorContainers!.length);
verifyColorContainerAriaChecked('redColor', colorContainers);
// Zone 4 has zone color as yellow, expect yellow color button to be
// highlighted.
(zoneButtons[3] as CrButtonElement).click();
await waitAfterNextRender(zoneCustomizationElement!);
verifyColorContainerAriaChecked('yellowColor', colorContainers);
});
test('sets color for a zone', async () => {
keyboardBacklightProvider.setZoneCount(4);
keyboardBacklightProvider.setCurrentBacklightState(
{zoneColors: keyboardBacklightProvider.zoneColors});
await initZoneCustomizationElement();
const zoneSelector =
zoneCustomizationElement!.shadowRoot!.getElementById('zoneSelector');
assertTrue(!!zoneSelector, 'zone selector should display');
const zoneButtons =
zoneCustomizationElement!.shadowRoot!.querySelectorAll('.zone-button');
assertEquals(
4, zoneButtons!.length,
'4 zones should display in customization dialog');
// Click on zone 2, expect red color icon to be highlighted.
(zoneButtons[1] as CrButtonElement).click();
const colorSelectorElement =
zoneCustomizationElement!.shadowRoot!.querySelector('color-selector') as
HTMLElement;
assertTrue(!!colorSelectorElement, 'color-selector should display.');
const colorContainers =
colorSelectorElement.shadowRoot!.querySelectorAll('.selectable');
assertEquals(
8, colorContainers.length!, 'there should be 8 color containers');
verifyColorContainerAriaChecked('redColor', colorContainers);
personalizationStore.setReducersEnabled(true);
personalizationStore.expectAction(
KeyboardBacklightActionName.SET_CURRENT_BACKLIGHT_STATE);
// Selects wallpaper color, color of zone 2 should change to wallpaper.
(colorContainers[7] as HTMLElement).click();
await keyboardBacklightProvider.whenCalled('setBacklightZoneColor');
const action =
await personalizationStore.waitForAction(
KeyboardBacklightActionName.SET_CURRENT_BACKLIGHT_STATE) as
SetCurrentBacklightStateAction;
assertTrue(!!action.currentBacklightState);
const expectedZoneColors = [...keyboardBacklightProvider.zoneColors];
expectedZoneColors[1] = BacklightColor.kWallpaper;
assertDeepEquals(
expectedZoneColors, action.currentBacklightState.zoneColors);
await waitAfterNextRender(zoneCustomizationElement!);
verifyColorContainerAriaChecked('wallpaper', colorContainers);
});
});