Bug 1442531 - Part 1. Make chevron button of devtool to be exclusive and apply the photon design. r=jdescottes

This patch will:
 * change detecting overflow to using resize event.
 * make chevron menu and tab menu to be exclusive.
 * use photon design chevron menu.
 * modify the related tests.

In this patch, the toolbox will create the cache of the displayed tool tab width
after rendering an element since each width of toolbox tab is not fixed size.
(i.e. each toolbox tab size is different from another size)

MozReview-Commit-ID: EQ0nU6WzCg1

--HG--
extra : rebase_source : ee01fbb5789769c4a75056e7eba41ad8ba232798
This commit is contained in:
Mantaroh Yoshinaga 2018-04-16 16:48:11 +09:00
Родитель 9b466fa2d1
Коммит 793eeff896
10 изменённых файлов: 370 добавлений и 156 удалений

Просмотреть файл

@ -13,6 +13,9 @@ const Menu = require("devtools/client/framework/menu");
const MenuItem = require("devtools/client/framework/menu-item");
const ToolboxTab = createFactory(require("devtools/client/framework/components/toolbox-tab"));
// 26px is chevron devtools button width.(i.e. tools-chevronmenu)
const CHEVRON_BUTTON_WIDTH = 26;
class ToolboxTabs extends Component {
// See toolbox-toolbar propTypes for details on the props used here.
static get propTypes() {
@ -32,55 +35,190 @@ class ToolboxTabs extends Component {
super(props);
this.state = {
overflow: false,
// Array of overflowed tool id.
overflowedTabIds: [],
};
this.addFlowEvents = this.addFlowEvents.bind(this);
this.removeFlowEvents = this.removeFlowEvents.bind(this);
this.onOverflow = this.onOverflow.bind(this);
this.onUnderflow = this.onUnderflow.bind(this);
// Map with tool Id and its width size. This lifecycle is out of React's
// lifecycle. If a tool is registered, ToolboxTabs will add target tool id
// to this map. ToolboxTabs will never remove tool id from this cache.
this._cachedToolTabsWidthMap = new Map();
this._resizeTimerId = null;
this.resizeHandler = this.resizeHandler.bind(this);
}
componentDidUpdate() {
this.addFlowEvents();
componentDidMount() {
window.addEventListener("resize", this.resizeHandler);
this.updateCachedToolTabsWidthMap();
this.updateOverflowedTabs();
}
componentWillUnmount() {
this.removeFlowEvents();
}
addFlowEvents() {
this.removeFlowEvents();
let node = findDOMNode(this);
if (node) {
node.addEventListener("overflow", this.onOverflow);
node.addEventListener("underflow", this.onUnderflow);
componentWillUpdate(nextProps, nextState) {
if (this.shouldUpdateToolboxTabs(this.props, nextProps)) {
// Force recalculate and render in this cycle if panel definition has
// changed or selected tool has changed.
nextState.overflowedTabIds = [];
}
}
removeFlowEvents() {
let node = findDOMNode(this);
if (node) {
node.removeEventListener("overflow", this.onOverflow);
node.removeEventListener("underflow", this.onUnderflow);
componentDidUpdate(prevProps, prevState) {
if (this.shouldUpdateToolboxTabs(prevProps, this.props)) {
this.updateCachedToolTabsWidthMap();
this.updateOverflowedTabs();
}
}
onOverflow() {
this.setState({
overflow: true
});
/**
* Check if two array of ids are the same or not.
*/
equalToolIdArray(prevPanels, nextPanels) {
if (prevPanels.length !== nextPanels.length) {
return false;
}
// Check panel definitions even if both of array size is same.
// For example, the case of changing the tab's order.
return prevPanels.join("-") === nextPanels.join("-");
}
onUnderflow() {
this.setState({
overflow: false
/**
* Return true if we should update the overflowed tabs.
*/
shouldUpdateToolboxTabs(prevProps, nextProps) {
if (prevProps.currentToolId !== nextProps.currentToolId) {
return true;
}
let prevPanels = prevProps.panelDefinitions.map(def => def.id);
let nextPanels = nextProps.panelDefinitions.map(def => def.id);
return !this.equalToolIdArray(prevPanels, nextPanels);
}
/**
* Update the Map of tool id and tool tab width.
*/
updateCachedToolTabsWidthMap() {
let thisNode = findDOMNode(this);
for (let tab of thisNode.querySelectorAll(".devtools-tab")) {
let tabId = tab.id.replace("toolbox-tab-", "");
if (!this._cachedToolTabsWidthMap.has(tabId)) {
let cs = getComputedStyle(tab);
this._cachedToolTabsWidthMap.set(tabId, parseInt(cs.width, 10));
}
}
}
/**
* Update the overflowed tab array from currently displayed tool tab.
* If calculated result is the same as the current overflowed tab array, this
* function will not update state.
*/
updateOverflowedTabs() {
let node = findDOMNode(this);
const toolboxWidth = parseInt(getComputedStyle(node).width, 10);
let { currentToolId } = this.props;
let enabledTabs = this.props.panelDefinitions.map(def => def.id);
let sumWidth = 0;
let visibleTabs = [];
for (const id of enabledTabs) {
let width = this._cachedToolTabsWidthMap.get(id);
sumWidth += width;
if (sumWidth <= toolboxWidth) {
visibleTabs.push(id);
} else {
sumWidth = sumWidth - width + CHEVRON_BUTTON_WIDTH;
// If toolbox can't display the Chevron, remove the last tool tab.
if (sumWidth > toolboxWidth) {
let removeTabId = visibleTabs.pop();
sumWidth -= this._cachedToolTabsWidthMap.get(removeTabId);
}
break;
}
}
// If the selected tab is in overflowed tabs, insert it into a visible
// toolbox.
if (!visibleTabs.includes(currentToolId) &&
enabledTabs.includes(currentToolId)) {
let selectedToolWidth = this._cachedToolTabsWidthMap.get(currentToolId);
while ((sumWidth + selectedToolWidth) > toolboxWidth &&
visibleTabs.length > 0) {
let removingToolId = visibleTabs.pop();
let removingToolWidth = this._cachedToolTabsWidthMap.get(removingToolId);
sumWidth -= removingToolWidth;
}
visibleTabs.push(currentToolId);
}
if (visibleTabs.length === 0) {
visibleTabs = [enabledTabs[0]];
}
let willOverflowTabs = enabledTabs.filter(id => !visibleTabs.includes(id));
if (!this.equalToolIdArray(this.state.overflowedTabIds, willOverflowTabs)) {
this.setState({ overflowedTabIds: willOverflowTabs });
}
}
resizeHandler(evt) {
window.cancelIdleCallback(this._resizeTimerId);
this._resizeTimerId = window.requestIdleCallback(() => {
this.updateOverflowedTabs();
}, { timeout: 300 });
}
/**
* Render a button to access overflowed tools, displayed only when the toolbar
* presents an overflow.
*/
renderToolsChevronButton() {
let {
panelDefinitions,
selectTool,
toolbox,
L10N,
} = this.props;
return button({
className: "devtools-button tools-chevron-menu",
tabIndex: -1,
title: L10N.getStr("toolbox.allToolsButton.tooltip"),
id: "tools-chevron-menu-button",
onClick: ({ target }) => {
let menu = new Menu({
id: "tools-chevron-menupopup"
});
panelDefinitions.forEach(({id, label}) => {
if (this.state.overflowedTabIds.includes(id)) {
menu.append(new MenuItem({
click: () => {
selectTool(id);
},
id: "tools-chevron-menupopup-" + id,
label,
type: "checkbox",
}));
}
});
let rect = target.getBoundingClientRect();
let screenX = target.ownerDocument.defaultView.mozInnerScreenX;
let screenY = target.ownerDocument.defaultView.mozInnerScreenY;
// Display the popup below the button.
menu.popup(rect.left + screenX, rect.bottom + screenY, toolbox);
return menu;
},
});
}
/**
* Render all of the tabs, based on the panel definitions and builds out
* a toolbox tab for each of them. Will render an all-tabs button if the
* a toolbox tab for each of them. Will render the chevron button if the
* container has an overflow.
*/
render() {
@ -93,17 +231,22 @@ class ToolboxTabs extends Component {
selectTool,
} = this.props;
let tabs = panelDefinitions.map(panelDefinition => ToolboxTab({
key: panelDefinition.id,
currentToolId,
focusButton,
focusedButton,
highlightedTools,
panelDefinition,
selectTool,
}));
let tabs = panelDefinitions.map(panelDefinition => {
// Don't display overflowed tab.
if (!this.state.overflowedTabIds.includes(panelDefinition.id)) {
return ToolboxTab({
key: panelDefinition.id,
currentToolId,
focusButton,
focusedButton,
highlightedTools,
panelDefinition,
selectTool,
});
}
return null;
});
// A wrapper is needed to get flex sizing correct in XUL.
return div(
{
className: "toolbox-tabs-wrapper"
@ -112,55 +255,12 @@ class ToolboxTabs extends Component {
{
className: "toolbox-tabs"
},
tabs
),
this.state.overflow ? renderAllToolsButton(this.props) : null
tabs,
(this.state.overflowedTabIds.length > 0)
? this.renderToolsChevronButton() : null
)
);
}
}
module.exports = ToolboxTabs;
/**
* Render a button to access all tools, displayed only when the toolbar presents an
* overflow.
*/
function renderAllToolsButton(props) {
let {
currentToolId,
panelDefinitions,
selectTool,
toolbox,
L10N,
} = props;
return button({
className: "all-tools-menu all-tabs-menu",
tabIndex: -1,
title: L10N.getStr("toolbox.allToolsButton.tooltip"),
onClick: ({ target }) => {
let menu = new Menu({
id: "all-tools-menupopup"
});
panelDefinitions.forEach(({id, label}) => {
menu.append(new MenuItem({
checked: currentToolId === id,
click: () => {
selectTool(id);
},
id: "all-tools-menupopup-" + id,
label,
type: "checkbox",
}));
});
let rect = target.getBoundingClientRect();
let screenX = target.ownerDocument.defaultView.mozInnerScreenX;
let screenY = target.ownerDocument.defaultView.mozInnerScreenY;
// Display the popup below the button.
menu.popup(rect.left + screenX, rect.bottom + screenY, toolbox);
return menu;
},
});
}

Просмотреть файл

@ -113,6 +113,7 @@ skip-if = e10s # Bug 1069044 - destroyInspector may hang during shutdown
[browser_toolbox_tool_ready.js]
[browser_toolbox_tool_remote_reopen.js]
[browser_toolbox_toolbar_overflow.js]
[browser_toolbox_toolbar_reorder_by_width.js]
[browser_toolbox_tools_per_toolbox_registration.js]
[browser_toolbox_transport_events.js]
[browser_toolbox_view_source_01.js]

Просмотреть файл

@ -20,6 +20,15 @@ add_task(async function() {
let tab = await addTab(URL);
let target = TargetFactory.forTab(tab);
toolbox = await gDevTools.showToolbox(target);
info("In order to ensure display the chevron menu, increase the width of " +
"the toolbox");
let hostWindow = toolbox.win.parent;
let originalWidth = hostWindow.outerWidth;
let onResize = once(hostWindow, "resize");
hostWindow.resizeTo(1350, hostWindow.outerHeight);
await onResize;
doc = toolbox.doc;
await registerNewPerToolboxTool();
await testSelectTool();
@ -33,7 +42,7 @@ add_task(async function() {
await registerNewWebExtensions();
await testToggleWebExtensions();
await cleanup();
await cleanup(hostWindow, originalWidth);
});
function registerNewTool() {
@ -455,7 +464,7 @@ function GetPref(name) {
}
}
async function cleanup() {
async function cleanup(win, winWidth) {
gDevTools.unregisterTool("test-tool");
await toolbox.destroy();
gBrowser.removeCurrentTab();
@ -463,4 +472,5 @@ async function cleanup() {
Services.prefs.clearUserPref(pref);
}
toolbox = doc = panelWin = modifiedPrefs = null;
win.resizeTo(winWidth, win.outerHeight);
}

Просмотреть файл

@ -12,8 +12,8 @@ let { Toolbox } = require("devtools/client/framework/toolbox");
add_task(async function() {
let tab = await addTab("about:blank");
info("Open devtools on the Inspector in a separate window");
let toolbox = await openToolboxForTab(tab, "inspector", Toolbox.HostType.WINDOW);
info("Open devtools on the Inspector in a bottom dock");
let toolbox = await openToolboxForTab(tab, "inspector", Toolbox.HostType.BOTTOM);
let hostWindow = toolbox.win.parent;
let originalWidth = hostWindow.outerWidth;
@ -21,66 +21,53 @@ add_task(async function() {
info("Resize devtools window to a width that should not trigger any overflow");
let onResize = once(hostWindow, "resize");
hostWindow.resizeTo(640, 300);
hostWindow.resizeTo(1350, 300);
await onResize;
waitUntil(() => {
// Wait for all buttons are displayed.
return toolbox.panelDefinitions.length !==
toolbox.doc.querySelectorAll(".devtools-tab").length;
});
let allToolsButton = toolbox.doc.querySelector(".all-tools-menu");
ok(!allToolsButton, "The all tools button is not displayed");
let chevronMenuButton = toolbox.doc.querySelector(".tools-chevron-menu");
ok(!chevronMenuButton, "The chevron menu button is not displayed");
info("Resize devtools window to a width that should trigger an overflow");
onResize = once(hostWindow, "resize");
hostWindow.resizeTo(300, 300);
hostWindow.resizeTo(800, 300);
await onResize;
waitUntil(() => !toolbox.doc.querySelector(".tools-chevron-menu"));
info("Wait until the all tools button is available");
await waitUntil(() => toolbox.doc.querySelector(".all-tools-menu"));
info("Wait until the chevron menu button is available");
await waitUntil(() => toolbox.doc.querySelector(".tools-chevron-menu"));
allToolsButton = toolbox.doc.querySelector(".all-tools-menu");
ok(allToolsButton, "The all tools button is displayed");
chevronMenuButton = toolbox.doc.querySelector(".tools-chevron-menu");
ok(chevronMenuButton, "The chevron menu button is displayed");
info("Open the all-tools-menupopup and verify that the inspector button is checked");
let menuPopup = await openAllToolsMenu(toolbox);
info("Open the tools-chevron-menupopup and verify that the inspector button is checked");
let menuPopup = await openChevronMenu(toolbox);
let inspectorButton = toolbox.doc.querySelector("#all-tools-menupopup-inspector");
ok(inspectorButton, "The inspector button is available");
ok(inspectorButton.getAttribute("checked"), "The inspector button is checked");
let inspectorButton = toolbox.doc.querySelector("#tools-chevron-menupopup-inspector");
ok(!inspectorButton, "The chevron menu doesn't have the inspector button.");
let consoleButton = toolbox.doc.querySelector("#all-tools-menupopup-webconsole");
ok(consoleButton, "The console button is available");
ok(!consoleButton.getAttribute("checked"), "The console button is not checked");
let consoleButton = toolbox.doc.querySelector("#tools-chevron-menupopup-webconsole");
ok(!consoleButton, "The chevron menu doesn't have the console button.");
info("Switch to the webconsole using the all-tools-menupopup popup");
let onSelected = toolbox.once("webconsole-selected");
consoleButton.click();
let storageButton = toolbox.doc.querySelector("#tools-chevron-menupopup-storage");
ok(storageButton, "The chevron menu has the storage button.");
info("Switch to the performance using the tools-chevron-menupopup popup");
let onSelected = toolbox.once("storage-selected");
storageButton.click();
await onSelected;
info("Closing the all-tools-menupopup popup");
info("Closing the tools-chevron-menupopup popup");
let onPopupHidden = once(menuPopup, "popuphidden");
menuPopup.hidePopup();
await onPopupHidden;
info("Re-open the all-tools-menupopup and verify that the console button is checked");
menuPopup = await openAllToolsMenu(toolbox);
inspectorButton = toolbox.doc.querySelector("#all-tools-menupopup-inspector");
ok(!inspectorButton.getAttribute("checked"), "The inspector button is not checked");
consoleButton = toolbox.doc.querySelector("#all-tools-menupopup-webconsole");
ok(consoleButton.getAttribute("checked"), "The console button is checked");
info("Restore the original window size");
onResize = once(hostWindow, "resize");
hostWindow.resizeTo(originalWidth, originalHeight);
await onResize;
});
async function openAllToolsMenu(toolbox) {
let allToolsButton = toolbox.doc.querySelector(".all-tools-menu");
EventUtils.synthesizeMouseAtCenter(allToolsButton, {}, toolbox.win);
let menuPopup = toolbox.doc.querySelector("#all-tools-menupopup");
ok(menuPopup, "all-tools-menupopup is available");
info("Waiting for the menu popup to be displayed");
await waitUntil(() => menuPopup && menuPopup.state === "open");
return menuPopup;
}

Просмотреть файл

@ -0,0 +1,106 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// This test will:
//
// * Confirm that currently selected button to access tools will not hide due to overflow.
// In this case, a button which is located on the left of a currently selected will hide.
// * Confirm that a button to access tool will hide when registering a new panel.
//
// Note that this test is based on the tab ordinal is fixed.
// i.e. After changed by Bug 1226272, this test might fail.
let { Toolbox } = require("devtools/client/framework/toolbox");
add_task(async function() {
let tab = await addTab("about:blank");
info("Open devtools on the Storage in a sidebar.");
let toolbox = await openToolboxForTab(tab, "storage", Toolbox.HostType.BOTTOM);
info("Waiting for the window to be resized");
let {hostWin, originalWidth, originalHeight} = await resizeWindow(toolbox, 800);
info("Wait until the tools menu button is available");
await waitUntil(() => toolbox.doc.querySelector(".tools-chevron-menu"));
let toolsMenuButton = toolbox.doc.querySelector(".tools-chevron-menu");
ok(toolsMenuButton, "The tools menu button is displayed");
info("Confirm that selected tab is not hidden.");
let storageButton = toolbox.doc.querySelector("#toolbox-tab-storage");
ok(storageButton, "The storage tab is on toolbox.");
await resizeWindow(toolbox, originalWidth, originalHeight);
});
add_task(async function() {
let tab = await addTab("about:blank");
info("Open devtools on the Storage in a sidebar.");
let toolbox = await openToolboxForTab(tab, "storage", Toolbox.HostType.BOTTOM);
info("Resize devtools window to a width that should trigger an overflow");
let {hostWin, originalWidth, originalHeight} = await resizeWindow(toolbox, 800);
info("Regist a new tab");
let onRegistered = toolbox.once("tool-registered");
gDevTools.registerTool({
id: "test-tools",
label: "Test Tools",
isMenu: true,
isTargetSupported: () => true,
build: function() {},
});
await onRegistered;
info("Open the tools menu button.");
let popup = await openChevronMenu(toolbox);
info("The registered new tool tab should be in the tools menu.");
let testToolsButton = toolbox.doc.querySelector("#tools-chevron-menupopup-test-tools");
ok(testToolsButton, "The tools menu has a registered new tool button.");
info("Closing the tools-chevron-menupopup popup");
let onPopupHidden = once(popup, "popuphidden");
popup.hidePopup();
await onPopupHidden;
info("Unregistering test-tools");
let onUnregistered = toolbox.once("tool-unregistered");
gDevTools.unregisterTool("test-tools");
await onUnregistered;
info("Open the tools menu button.");
popup = await openChevronMenu(toolbox);
info("An unregistered new tool tab should not be in the tools menu.");
testToolsButton = toolbox.doc.querySelector("#tools-chevron-menupopup-test-tools");
ok(!testToolsButton, "The tools menu doesn't have a unregistered new tool button.");
info("Closing the tools-chevron-menupopup popup");
onPopupHidden = once(popup, "popuphidden");
popup.hidePopup();
await onPopupHidden;
await resizeWindow(toolbox, originalWidth, originalHeight);
});
async function resizeWindow(toolbox, width, height) {
let hostWindow = toolbox.win.parent;
let originalWidth = hostWindow.outerWidth;
let originalHeight = hostWindow.outerHeight;
let toWidth = width || originalWidth;
let toHeight = height || originalHeight;
info("Resize devtools window to a width that should trigger an overflow");
let onResize = once(hostWindow, "resize");
hostWindow.resizeTo(toWidth, toHeight);
await onResize;
return {hostWindow, originalWidth, originalHeight};
}

Просмотреть файл

@ -247,3 +247,16 @@ DevToolPanel.prototype = {
function createTestPanel(iframeWindow, toolbox) {
return new DevToolPanel(iframeWindow, toolbox);
}
async function openChevronMenu(toolbox) {
let chevronMenuButton = toolbox.doc.querySelector(".tools-chevron-menu");
EventUtils.synthesizeMouseAtCenter(chevronMenuButton, {}, toolbox.win);
let menuPopup = toolbox.doc.querySelector("#tools-chevron-menupopup");
ok(menuPopup, "tools-chevron-menupopup is available");
info("Waiting for the menu popup to be displayed");
await waitUntil(() => menuPopup && menuPopup.state === "open");
return menuPopup;
}

Просмотреть файл

@ -150,6 +150,7 @@ devtools.jar:
skin/images/command-eyedropper.svg (themes/images/command-eyedropper.svg)
skin/images/command-rulers.svg (themes/images/command-rulers.svg)
skin/images/command-measure.svg (themes/images/command-measure.svg)
skin/images/command-chevron.svg (themes/images/command-chevron.svg)
skin/markup.css (themes/markup.css)
skin/images/editor-error.png (themes/images/editor-error.png)
skin/images/breakpoint.svg (themes/images/breakpoint.svg)

Просмотреть файл

@ -263,7 +263,6 @@ checkbox:-moz-focusring {
.devtools-button:empty::before {
content: "";
display: inline-block;
background-size: cover;
background-repeat: no-repeat;
vertical-align: middle;
}

Просмотреть файл

@ -0,0 +1,6 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 16 16">
<path fill="context-fill" d="M8.707 7.293l-5-5a1 1 0 0 0-1.414 1.414L6.586 8l-4.293 4.293a1 1 0 1 0 1.414 1.414l5-5a1 1 0 0 0 0-1.414zm6 0l-5-5a1 1 0 0 0-1.414 1.414L12.586 8l-4.293 4.293a1 1 0 1 0 1.414 1.414l5-5a1 1 0 0 0 0-1.414z"></path>
</svg>

После

Ширина:  |  Высота:  |  Размер: 547 B

Просмотреть файл

@ -16,6 +16,7 @@
--command-frames-image: url(images/command-frames.svg);
--command-rulers-image: url(images/command-rulers.svg);
--command-measure-image: url(images/command-measure.svg);
--command-chevron-image: url(images/command-chevron.svg);
}
/* Toolbox tabbar */
@ -35,8 +36,7 @@
flex: 1;
}
.toolbox-tabs-wrapper .all-tools-menu {
border-inline-end: 1px solid var(--theme-splitter-color);
.toolbox-tabs-wrapper .tools-chevron-menu {
border-top-width: 0;
border-bottom-width: 0;
}
@ -68,7 +68,6 @@
position: relative;
display: flex;
align-items: center;
min-width: 32px;
min-height: 24px;
margin: 0;
padding: 0;
@ -77,6 +76,7 @@
overflow: hidden;
text-overflow: ellipsis;
background-color: transparent;
flex-shrink: 0;
}
.devtools-tab-label {
@ -91,23 +91,6 @@
mask-image: linear-gradient(to right, transparent 0, black 6px);
}
/* Hide tab icons when the viewport width is limited */
@media (max-width: 700px) {
.devtools-tab-label {
/* Set the end padding on the label to make sure the label gets faded out properly */
padding-inline-end: 5px;
}
.devtools-tab:not(.devtools-tab-icon-only) {
padding-inline-start: 5px !important;
}
/* Hide the icons */
.devtools-tab:not(.devtools-tab-icon-only) > img {
display: none;
}
}
.devtools-tab-icon-only {
min-width: 24px;
}
@ -144,9 +127,10 @@
margin: 0 4px;
}
.devtools-tab > img {
.devtools-tab > img,
.tools-chevron-menu > img {
-moz-context-properties: fill;
fill: var(--theme-toolbar-color);
fill: var(--theme-toolbar-photon-icon-color);
}
.devtools-tab.selected > img {
@ -159,6 +143,13 @@
/* Toolbox controls */
#tools-chevron-menu-button::before {
top: 0;
offset-inline-end: 0;
background-image: var(--command-chevron-image);
background-position: center;
}
#toolbox-controls {
margin-right: 3px;
}