Bug 1449885 - Read font variation axis data and setup UI with any axis values defined on rule. r=gl

MozReview-Commit-ID: 6tWRyjYcdDH
This commit is contained in:
Razvan Caliman 2018-04-02 17:32:51 +02:00
Родитель 3680684f70
Коммит 0a3e54885d
10 изменённых файлов: 343 добавлений и 15 удалений

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

@ -5,11 +5,20 @@
"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,
@ -18,4 +27,20 @@ module.exports = {
};
},
updateAxis(axis, value) {
return {
type: UPDATE_AXIS_VALUE,
axis,
value,
};
},
updateFontEditor(fonts, properties = {}) {
return {
type: UPDATE_EDITOR_STATE,
fonts,
properties,
};
},
};

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

@ -8,6 +8,15 @@ 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",

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

@ -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;

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

@ -4,27 +4,96 @@
"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()
);
}
}

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

@ -20,6 +20,7 @@ class FontsApp extends PureComponent {
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,
};
}
@ -29,7 +30,8 @@ class FontsApp extends PureComponent {
fontData,
fontEditor,
fontOptions,
onPreviewFonts
onAxisUpdate,
onPreviewFonts,
} = this.props;
return dom.div(
@ -40,6 +42,7 @@ class FontsApp extends PureComponent {
fontEditor.isVisible ?
FontEditor({
fontEditor,
onAxisUpdate,
})
:
FontOverview({

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

@ -6,6 +6,7 @@
DevToolsModules(
'Font.js',
'FontAxis.js',
'FontEditor.js',
'FontList.js',
'FontOverview.js',

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

@ -19,9 +19,18 @@ const INSPECTOR_L10N =
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) {
@ -33,6 +42,7 @@ class FontInspector {
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);
@ -49,6 +59,7 @@ class FontInspector {
let fontsApp = FontsApp({
onPreviewFonts: this.onPreviewFonts,
onAxisUpdate: this.onAxisUpdate,
});
let provider = createElement(Provider, {
@ -145,6 +156,19 @@ class FontInspector {
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.
*/
@ -173,11 +197,13 @@ class FontInspector {
* - {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));
}
}
@ -198,6 +224,7 @@ class FontInspector {
if (editorId === FONT_EDITOR_ID && rule == this.selectedRule) {
this.selectedRule = null;
this.store.dispatch(toggleFontEditor(false));
this.store.dispatch(resetFontEditor());
}
}
@ -210,6 +237,46 @@ class FontInspector {
}
}
/**
* 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) {

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

@ -5,18 +5,58 @@
"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 };
},

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

@ -80,12 +80,18 @@ exports.fontOptions = {
};
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,
};

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

@ -8,14 +8,10 @@
flex-direction: column;
width: 100%;
height: 100%;
}
#sidebar-panel-fonteditor {
padding: 1em;
overflow: auto;
}
#font-container {
overflow: auto;
flex: auto;
}
@ -113,6 +109,40 @@
vertical-align: middle;
}
.font-axis {
display: flex;
flex-direction: row;
flex-wrap: 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);