--- a/devtools/client/inspector/fonts/actions/font-editor.js
+++ b/devtools/client/inspector/fonts/actions/font-editor.js
@@ -1,21 +1,46 @@
/* 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/. */
"use strict";
const {
+ RESET_EDITOR,
+ UPDATE_AXIS_VALUE,
UPDATE_EDITOR_VISIBILITY,
+ UPDATE_EDITOR_STATE,
} = require("./index");
module.exports = {
+ resetFontEditor() {
+ return {
+ type: RESET_EDITOR,
+ };
+ },
+
toggleFontEditor(isVisible, selector = "") {
return {
type: UPDATE_EDITOR_VISIBILITY,
isVisible,
selector,
};
},
+ updateAxis(axis, value) {
+ return {
+ type: UPDATE_AXIS_VALUE,
+ axis,
+ value,
+ };
+ },
+
+ updateFontEditor(fonts, properties = {}) {
+ return {
+ type: UPDATE_EDITOR_STATE,
+ fonts,
+ properties,
+ };
+ },
+
};
--- a/devtools/client/inspector/fonts/actions/index.js
+++ b/devtools/client/inspector/fonts/actions/index.js
@@ -3,16 +3,25 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { createEnum } = require("devtools/client/shared/enum");
createEnum([
+ // Reset font editor to intial state.
+ "RESET_EDITOR",
+
+ // Update the value of a variable font axis.
+ "UPDATE_AXIS_VALUE",
+
+ // Update font editor with applicable fonts and user-defined CSS font properties.
+ "UPDATE_EDITOR_STATE",
+
// Toggle the visibiltiy of the font editor
"UPDATE_EDITOR_VISIBILITY",
// Update the list of fonts.
"UPDATE_FONTS",
// Update the preview text.
"UPDATE_PREVIEW_TEXT",
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/fonts/components/FontAxis.js
@@ -0,0 +1,78 @@
+/* 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/. */
+
+"use strict";
+
+const { PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+class FontAxis extends PureComponent {
+ static get propTypes() {
+ return {
+ defaultValue: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+ label: PropTypes.string.isRequired,
+ min: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+ max: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+ name: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+ showInput: PropTypes.bool,
+ step: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+ value: PropTypes.string,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.onChange = this.onChange.bind(this);
+ }
+
+ onChange(e) {
+ this.props.onChange(this.props.name, e.target.value);
+ }
+
+ render() {
+ const defaults = {
+ min: this.props.min,
+ max: this.props.max,
+ onChange: this.onChange,
+ step: this.props.step || 1,
+ value: this.props.value || this.props.defaultValue,
+ };
+
+ const range = dom.input(
+ {
+ ...defaults,
+ className: "font-axis-slider",
+ title: this.props.label,
+ type: "range",
+ }
+ );
+
+ const input = dom.input(
+ {
+ ...defaults,
+ className: "font-axis-input",
+ type: "number",
+ }
+ );
+
+ return dom.label(
+ {
+ className: "font-axis",
+ },
+ dom.span(
+ {
+ className: "font-axis-label",
+ },
+ this.props.label
+ ),
+ range,
+ this.props.showInput ? input : null
+ );
+ }
+}
+
+module.exports = FontAxis;
--- a/devtools/client/inspector/fonts/components/FontEditor.js
+++ b/devtools/client/inspector/fonts/components/FontEditor.js
@@ -1,32 +1,101 @@
/* 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/. */
"use strict";
-const { PureComponent } = require("devtools/client/shared/vendor/react");
+const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const FontAxis = createFactory(require("./FontAxis"));
+
const Types = require("../types");
class FontEditor extends PureComponent {
static get propTypes() {
return {
fontEditor: PropTypes.shape(Types.fontEditor).isRequired,
+ onAxisUpdate: PropTypes.func.isRequired,
};
}
+ /**
+ * Naive implementation to get increment step for variable font axis that ensures
+ * a wide spectrum of precision based on range of values between min and max.
+ *
+ * @param {Number|String} min
+ * Minumum value for range.
+ * @param {Number|String} max
+ * Maximum value for range.
+ * @return {String}
+ * Step value used in range input for font axis.
+ */
+ getAxisStep(min, max) {
+ let step = 1;
+ let delta = parseInt(max, 10) - parseInt(min, 10);
+
+ if (delta <= 1) {
+ step = 0.001;
+ } else if (delta <= 10) {
+ step = 0.01;
+ } else if (delta <= 100) {
+ step = 0.1;
+ }
+
+ return step.toString();
+ }
+
+ /**
+ * Get an array of FontAxis components for of the given variable font axis instances.
+ * If an axis is defined in the fontEditor store, use its value, else use the default.
+ *
+ * @param {Array} fontAxes
+ * Array of font axis instances
+ * @param {Object} editedAxes
+ * Object with axes and values edited by the user or predefined in the CSS
+ * declaration for font-variation-settings.
+ * @return {Array}
+ * Array of FontAxis components
+ */
+ renderAxes(fontAxes = [], editedAxes) {
+ return fontAxes.map(axis => {
+ return FontAxis({
+ min: axis.minValue,
+ max: axis.maxValue,
+ value: editedAxes[axis.tag] || axis.defaultValue,
+ step: this.getAxisStep(axis.minValue, axis.maxValue),
+ label: axis.name,
+ name: axis.tag,
+ onChange: this.props.onAxisUpdate,
+ showInput: true
+ });
+ });
+ }
+
+ // Placeholder for non-variable font UI.
+ // Bug https://bugzilla.mozilla.org/show_bug.cgi?id=1450695
+ renderPlaceholder() {
+ return dom.div({}, "No fonts with variation axes apply to this element.");
+ }
+
render() {
- const { selector } = this.props.fontEditor;
+ const { fonts, axes } = this.props.fontEditor;
+ // For MVP use ony first font to show axes if available.
+ // Future implementations will allow switching between multiple fonts.
+ const fontAxes = (fonts[0] && fonts[0].variationAxes) ? fonts[0].variationAxes : null;
return dom.div(
{
className: "theme-sidebar inspector-tabpanel",
- id: "sidebar-panel-fonteditor"
- }, `Placeholder for Font Editor panel for selector: ${selector}`
+ id: "sidebar-panel-fontinspector"
+ },
+ fontAxes ?
+ this.renderAxes(fontAxes, axes)
+ :
+ this.renderPlaceholder()
);
}
}
module.exports = FontEditor;
--- a/devtools/client/inspector/fonts/components/FontsApp.js
+++ b/devtools/client/inspector/fonts/components/FontsApp.js
@@ -15,36 +15,39 @@ const FontOverview = createFactory(requi
const Types = require("../types");
class FontsApp extends PureComponent {
static get propTypes() {
return {
fontData: PropTypes.shape(Types.fontData).isRequired,
fontEditor: PropTypes.shape(Types.fontEditor).isRequired,
fontOptions: PropTypes.shape(Types.fontOptions).isRequired,
+ onAxisUpdate: PropTypes.func.isRequired,
onPreviewFonts: PropTypes.func.isRequired,
};
}
render() {
const {
fontData,
fontEditor,
fontOptions,
- onPreviewFonts
+ onAxisUpdate,
+ onPreviewFonts,
} = this.props;
return dom.div(
{
className: "theme-sidebar inspector-tabpanel",
id: "sidebar-panel-fontinspector"
},
fontEditor.isVisible ?
FontEditor({
fontEditor,
+ onAxisUpdate,
})
:
FontOverview({
fontData,
fontOptions,
onPreviewFonts,
})
);
--- a/devtools/client/inspector/fonts/components/moz.build
+++ b/devtools/client/inspector/fonts/components/moz.build
@@ -1,14 +1,15 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
DevToolsModules(
'Font.js',
+ 'FontAxis.js',
'FontEditor.js',
'FontList.js',
'FontOverview.js',
'FontPreview.js',
'FontsApp.js',
)
--- a/devtools/client/inspector/fonts/fonts.js
+++ b/devtools/client/inspector/fonts/fonts.js
@@ -14,46 +14,57 @@ const { Provider } = require("devtools/c
const FontsApp = createFactory(require("./components/FontsApp"));
const { LocalizationHelper } = require("devtools/shared/l10n");
const INSPECTOR_L10N =
new LocalizationHelper("devtools/client/locales/inspector.properties");
const { updateFonts } = require("./actions/fonts");
const { updatePreviewText } = require("./actions/font-options");
-const { toggleFontEditor } = require("./actions/font-editor");
+const { resetFontEditor, toggleFontEditor, updateAxis, updateFontEditor } =
+ require("./actions/font-editor");
const FONT_EDITOR_ID = "fonteditor";
+const FONT_PROPERTIES = [
+ "font-optical-sizing",
+ "font-size",
+ "font-stretch",
+ "font-style",
+ "font-variation-settings",
+ "font-weight",
+];
class FontInspector {
constructor(inspector, window) {
this.document = window.document;
this.inspector = inspector;
this.pageStyle = this.inspector.pageStyle;
this.ruleView = this.inspector.getPanel("ruleview").view;
this.selectedRule = null;
this.store = this.inspector.store;
this.update = this.update.bind(this);
+ this.onAxisUpdate = this.onAxisUpdate.bind(this);
this.onNewNode = this.onNewNode.bind(this);
this.onPreviewFonts = this.onPreviewFonts.bind(this);
this.onRuleSelected = this.onRuleSelected.bind(this);
this.onRuleUnselected = this.onRuleUnselected.bind(this);
this.onThemeChanged = this.onThemeChanged.bind(this);
this.init();
}
init() {
if (!this.inspector) {
return;
}
let fontsApp = FontsApp({
onPreviewFonts: this.onPreviewFonts,
+ onAxisUpdate: this.onAxisUpdate,
});
let provider = createElement(Provider, {
id: "fontinspector",
key: "fontinspector",
store: this.store,
title: INSPECTOR_L10N.getStr("inspector.sidebar.fontInspectorTitle"),
}, fontsApp);
@@ -141,16 +152,29 @@ class FontInspector {
* Returns true if the font inspector panel is visible, and false otherwise.
*/
isPanelVisible() {
return this.inspector.sidebar &&
this.inspector.sidebar.getCurrentTabID() === "fontinspector";
}
/**
+ * Handler for changes of font axis value. Updates the value in the store and previews
+ * the change on the page.
+ *
+ * @param {String} tag
+ * Tag name of the font axis.
+ * @param {String} value
+ * Value of the font axis.
+ */
+ onAxisUpdate(tag, value) {
+ this.store.dispatch(updateAxis(tag, value));
+ }
+
+ /**
* Selection 'new-node' event handler.
*/
onNewNode() {
if (this.isPanelVisible()) {
this.update();
}
}
@@ -168,21 +192,23 @@ class FontInspector {
* If selected for the font editor, hold a reference to the rule so we know where to
* put property changes coming from the font editor and show the font editor panel.
*
* @param {Object} eventData
* Data payload for the event. Contains:
* - {String} editorId - id of the editor for which the rule was selected
* - {Rule} rule - reference to rule that was selected
*/
- onRuleSelected(eventData) {
+ async onRuleSelected(eventData) {
const { editorId, rule } = eventData;
if (editorId === FONT_EDITOR_ID) {
const selector = rule.matchedSelectors[0];
this.selectedRule = rule;
+
+ await this.refreshFontEditor();
this.store.dispatch(toggleFontEditor(true, selector));
}
}
/**
* Handler for "ruleview-rule-unselected" event emitted from the rule view when a rule
* was released from being selected for an editor.
* If previously selected for the font editor, release the reference to the rule and
@@ -193,28 +219,69 @@ class FontInspector {
* - {String} editorId - id of the editor for which the rule was released
* - {Rule} rule - reference to rule that was released
*/
onRuleUnselected(eventData) {
const { editorId, rule } = eventData;
if (editorId === FONT_EDITOR_ID && rule == this.selectedRule) {
this.selectedRule = null;
this.store.dispatch(toggleFontEditor(false));
+ this.store.dispatch(resetFontEditor());
}
}
/**
* Handler for the "theme-switched" event.
*/
onThemeChanged(frame) {
if (frame === this.document.defaultView) {
this.update();
}
}
+ /**
+ * Update the state of the font editor with:
+ * - the fonts which apply to the current node;
+ * - the CSS font properties declared on the selected rule.
+ *
+ * This method is called during initial setup and as a result of any property
+ * values change in the Rule view. For the latter case, we do a deep compare between the
+ * font properties on the selected rule and the ones already store to decide if to
+ * update the font edtior to reflect a new external state.
+ */
+ async refreshFontEditor() {
+ if (!this.selectedRule || !this.inspector || !this.store) {
+ return;
+ }
+
+ const options = {};
+ if (this.pageStyle.supportsFontVariations) {
+ options.includeVariations = true;
+ }
+
+ const node = this.inspector.selection.nodeFront;
+ const fonts = await this.getFontsForNode(node, options);
+ // Collect any expected font properties and their values from the selected rule.
+ const properties = this.selectedRule.textProps.reduce((acc, prop) => {
+ if (FONT_PROPERTIES.includes(prop.name)) {
+ acc[prop.name] = prop.value;
+ }
+
+ return acc;
+ }, {});
+
+ const fontEditor = this.store.getState().fontEditor;
+
+ // Update the font editor state only if property values in rule differ from store.
+ // This can happen when a user makes manual edits to the values in the rule view.
+ if (JSON.stringify(properties) !== JSON.stringify(fontEditor.properties)) {
+ this.store.dispatch(updateFontEditor(fonts, properties));
+ }
+ }
+
async update() {
// Stop refreshing if the inspector or store is already destroyed.
if (!this.inspector || !this.store) {
return;
}
let node = this.inspector.selection.nodeFront;
--- a/devtools/client/inspector/fonts/reducers/font-editor.js
+++ b/devtools/client/inspector/fonts/reducers/font-editor.js
@@ -1,27 +1,67 @@
/* 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/. */
"use strict";
const {
+ RESET_EDITOR,
+ UPDATE_AXIS_VALUE,
+ UPDATE_EDITOR_STATE,
UPDATE_EDITOR_VISIBILITY,
} = require("../actions/index");
const INITIAL_STATE = {
+ // Variable font axes.
+ axes: {},
+ // Fonts applicable to selected element.
+ fonts: [],
// Whether or not the font editor is visible.
isVisible: false,
- // Selector text of the rule where font properties will be written.
+ // CSS font properties defined on the selected rule.
+ properties: {},
+ // Selector text of the selected rule where updated font properties will be written.
selector: "",
};
let reducers = {
+ [RESET_EDITOR](state) {
+ return { ...INITIAL_STATE };
+ },
+
+ [UPDATE_AXIS_VALUE](state, { axis, value }) {
+ let newState = { ...state };
+ newState.axes[axis] = value;
+ return newState;
+ },
+
+ [UPDATE_EDITOR_STATE](state, { fonts, properties }) {
+ let axes = {};
+
+ if (properties["font-variation-settings"]) {
+ // Parse font-variation-settings CSS declaration into an object
+ // with axis tags as keys and axis values as values.
+ axes = properties["font-variation-settings"]
+ .split(",")
+ .reduce((acc, pair) => {
+ // Tags are always in quotes. Split by quote and filter excessive whitespace.
+ pair = pair.split(/["']/).filter(part => part.trim() !== "");
+ const tag = pair[0].trim();
+ const value = pair[1].trim();
+ acc[tag] = value;
+ return acc;
+ }, {});
+ }
+
+ return { ...state, axes, fonts, properties };
+ },
+
[UPDATE_EDITOR_VISIBILITY](state, { isVisible, selector }) {
return { ...state, isVisible, selector };
},
};
module.exports = function(state = INITIAL_STATE, action) {
let reducer = reducers[action.type];
--- a/devtools/client/inspector/fonts/types.js
+++ b/devtools/client/inspector/fonts/types.js
@@ -75,22 +75,28 @@ const font = exports.font = {
};
exports.fontOptions = {
// The current preview text
previewText: PropTypes.string,
};
exports.fontEditor = {
- // Font currently being edited
- font: PropTypes.shape(font),
+ // Variable font axes and their values
+ axes: PropTypes.object,
+
+ // Fonts applicable to selected element
+ fonts: PropTypes.arrayOf(PropTypes.shape(font)),
// Whether or not the font editor is visible
isVisible: PropTypes.bool,
+ // CSS font properties defined on the element
+ properties: PropTypes.object,
+
// Selector text of the rule where font properties will be written
selector: PropTypes.string,
};
/**
* Font data.
*/
exports.fontData = {
--- a/devtools/client/themes/fonts.css
+++ b/devtools/client/themes/fonts.css
@@ -3,24 +3,20 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#sidebar-panel-fontinspector {
margin: 0;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
-}
-
-#sidebar-panel-fonteditor {
- padding: 1em;
+ overflow: auto;
}
#font-container {
- overflow: auto;
flex: auto;
}
.fonts-list {
padding: 0;
margin: 0;
list-style: none;
}
@@ -108,16 +104,49 @@
color: var(--theme-body-color-inactive);
border-radius: 3px;
border-style: solid;
border-width: 1px;
text-align: center;
vertical-align: middle;
}
+.font-axis {
+ display: flex;
+ flex-direction: row nowrap;
+ justify-content: space-between;
+ align-items: center;
+ padding: 5px 20px;
+}
+
+.font-axis-input {
+ width: 60px;
+}
+
+.font-axis-label {
+ width: 70px;
+}
+
+.font-axis-slider {
+ flex: 1;
+}
+
+.font-axis-slider::-moz-focus-outer {
+ border: 0;
+}
+
+.font-axis-slider::-moz-range-thumb {
+ background: var(--grey-50);
+ border: 0;
+}
+
+.font-axis-slider:focus::-moz-range-thumb {
+ background: var(--blue-55);
+}
+
.font-origin {
margin-top: .2em;
color: var(--grey-50);
justify-self: start;
}
.font-origin.system {
text-transform: capitalize;