Commit 2692eeb5 authored by shend's avatar shend Committed by Commit bot

Generate subgroup StyleSurroundData in ComputedStyle.

StyleSurroundData is a class that contains the values of the offset,
margin, padding and border properties. This patch generates this class
as a nested class inside ComputedStyle. We add an extra key in
CSSProperties.json5 called 'field_group' that specifies the name of the
group that the field is in. Fields with the same field_group are grouped
together into a nested class.

A few caveats:
- Causes perf regression because it doesn't exploit sharing of groups.
  The fix is in a dependent patch (to avoid bloating this CL). We
  will land the fix right after this patch is landed.
  https://codereview.chromium.org/2826633002
- We generate 'border' as 'storage_only', because it is a shorthand
  and not a CSS property.
- Subgroups are currently generated as nested classes of ComputedStyle,
  but this makes the code difficult to read. This may change in the
  future (such as putting subgroups in a ::detail namespace).
- We don't ASSERT_SIZE on subgroups yet.

Diff of generated files:
https://gist.github.com/darrnshn/876c8171fd134b373f28ca23e821d28f/revisions

BUG=628043

Review-Url: https://codereview.chromium.org/2786883002
Cr-Commit-Position: refs/heads/master@{#467239}
parent b546eb97
......@@ -12,9 +12,50 @@ import make_style_builder
from name_utilities import (
enum_for_css_keyword, enum_type_name, enum_value_name, class_member_name, method_name,
join_name
class_name, join_name
)
from collections import OrderedDict
from collections import defaultdict, OrderedDict
from itertools import chain
# TODO(shend): Improve documentation and add docstrings.
def _flatten_list(x):
"""Flattens a list of lists into a single list."""
return list(chain.from_iterable(x))
def _num_32_bit_words_for_bit_fields(bit_fields):
"""Gets the number of 32 bit unsigned integers needed store a list of bit fields."""
num_buckets, cur_bucket = 0, 0
for field in bit_fields:
if field.size + cur_bucket > 32:
num_buckets += 1
cur_bucket = 0
cur_bucket += field.size
return num_buckets + (cur_bucket > 0)
class Group(object):
"""Represents a group of fields stored together in a class.
Attributes:
name: The name of the group as a string.
subgroups: List of Group instances that are stored as subgroups under this group.
fields: List of Field instances stored directly under this group.
"""
def __init__(self, name, subgroups, fields):
self.name = name
self.subgroups = subgroups
self.fields = fields
self.type_name = class_name(join_name('style', name, 'data'))
self.member_name = class_member_name(join_name(name, 'data'))
self.num_32_bit_words_for_bit_fields = _num_32_bit_words_for_bit_fields(
field for field in fields if field.is_bit_field
)
# Recursively get all the fields in the subgroups as well
self.all_fields = _flatten_list(subgroup.all_fields for subgroup in subgroups) + fields
class Field(object):
......@@ -42,18 +83,21 @@ class Field(object):
type_name: Name of the C++ type exposed by the generated interface (e.g. EClear, int).
field_template: Determines the interface generated for the field. Can be one of:
keyword, flag, or monotonic_flag.
field_group: The name of the group that this field is inside.
size: Number of bits needed for storage.
default_value: Default value for this field when it is first initialized.
"""
def __init__(self, field_role, name_for_methods, property_name, type_name,
field_template, size, default_value, getter_method_name, setter_method_name,
initial_method_name, **kwargs):
field_template, field_group, size, default_value,
getter_method_name, setter_method_name, initial_method_name, **kwargs):
"""Creates a new field."""
self.name = class_member_name(name_for_methods)
self.property_name = property_name
self.type_name = type_name
self.field_template = field_template
self.group_name = field_group
self.group_member_name = class_member_name(join_name(field_group, 'data')) if field_group else None
self.size = size
self.default_value = default_value
......@@ -98,6 +142,17 @@ def _get_include_paths(properties):
return list(sorted(include_paths))
def _group_fields(fields):
"""Groups a list of fields by their group_name and returns the root group."""
groups = defaultdict(list)
for field in fields:
groups[field.group_name].append(field)
no_group = groups.pop(None)
subgroups = [Group(group_name, [], _reorder_fields(fields)) for group_name, fields in groups.items()]
return Group('', subgroups=subgroups, fields=_reorder_fields(no_group))
def _create_enums(properties):
"""
Returns an OrderedDict of enums to be generated, enum name -> [list of enum values]
......@@ -165,6 +220,7 @@ def _create_field(field_role, property_):
independent=property_['independent'],
type_name=type_name,
field_template=property_['field_template'],
field_group=property_['field_group'],
size=size,
default_value=default_value,
getter_method_name=property_['getter'],
......@@ -185,6 +241,7 @@ def _create_inherited_flag_field(property_):
property_name=property_['name'],
type_name='bool',
field_template='primitive',
field_group=property_['field_group'],
size=1,
default_value='true',
getter_method_name=method_name(name_for_methods),
......@@ -207,17 +264,22 @@ def _create_fields(properties):
fields.append(_create_inherited_flag_field(property_))
# TODO(shend): Get rid of the property/nonproperty field roles.
# If the field has_custom_compare_and_copy, then it does not appear in
# ComputedStyle::operator== and ComputedStyle::CopyNonInheritedFromCached.
field_role = 'nonproperty' if property_['has_custom_compare_and_copy'] else 'property'
fields.append(_create_field(field_role, property_))
return fields
def _pack_fields(fields):
def _reorder_fields(fields):
"""
Group a list of fields into buckets to minimise padding.
Returns a list of buckets, where each bucket is a list of Field objects.
Returns a list of fields ordered to minimise padding.
"""
# Separate out bit fields from non bit fields
bit_fields = [field for field in fields if field.is_bit_field]
non_bit_fields = [field for field in fields if not field.is_bit_field]
# Since fields cannot cross word boundaries, in order to minimize
# padding, group fields into buckets so that as many buckets as possible
# are exactly 32 bits. Although this greedy approach may not always
......@@ -230,7 +292,7 @@ def _pack_fields(fields):
field_buckets = []
# Consider fields in descending order of size to reduce fragmentation
# when they are selected. Ties broken in alphabetical order by name.
for field in sorted(fields, key=lambda f: (-f.size, f.name)):
for field in sorted(bit_fields, key=lambda f: (-f.size, f.name)):
added_to_bucket = False
# Go through each bucket and add this field if it will not increase
# the bucket's size to larger than 32 bits. Otherwise, make a new
......@@ -243,7 +305,8 @@ def _pack_fields(fields):
if not added_to_bucket:
field_buckets.append([field])
return field_buckets
# Non bit fields go first, then the bit fields.
return list(non_bit_fields) + _flatten_list(field_buckets)
class ComputedStyleBaseWriter(make_style_builder.StyleBuilderWriter):
......@@ -285,40 +348,9 @@ class ComputedStyleBaseWriter(make_style_builder.StyleBuilderWriter):
all_fields = _create_fields(all_properties)
# Separate the normal fields from the bit fields
bit_fields = [field for field in all_fields if field.is_bit_field]
normal_fields = [field for field in all_fields if not field.is_bit_field]
# Pack bit fields into buckets
field_buckets = _pack_fields(bit_fields)
# The expected size of ComputedStyleBase is equivalent to as many words
# as the total number of buckets.
self._expected_bit_field_bytes = len(field_buckets)
# The most optimal size of ComputedStyleBase is the total sum of all the
# field sizes, rounded up to the nearest word. If this produces the
# incorrect value, either the packing algorithm is not optimal or there
# is no way to pack the fields such that excess padding space is not
# added.
# If this fails, increase extra_padding_bytes by 1, but be aware that
# this also increases ComputedStyleBase by 1 word.
# We should be able to bring extra_padding_bytes back to 0 from time to
# time.
extra_padding_bytes = 0
optimal_bit_field_bytes = int(math.ceil(sum(f.size for f in bit_fields) / 32.0))
real_bit_field_bytes = optimal_bit_field_bytes + extra_padding_bytes
assert self._expected_bit_field_bytes == real_bit_field_bytes, \
('The field packing algorithm produced %s bytes, optimal is %s bytes' %
(self._expected_bit_field_bytes, real_bit_field_bytes))
# Normal fields go first, then the bit fields.
self._fields = list(normal_fields)
# Order the fields so fields in each bucket are adjacent.
for bucket in field_buckets:
for field in bucket:
self._fields.append(field)
# Organise fields into a tree structure where the root group
# is ComputedStyleBase.
self._root_group = _group_fields(all_fields)
self._include_paths = _get_include_paths(all_properties)
self._outputs = {
......@@ -333,7 +365,7 @@ class ComputedStyleBaseWriter(make_style_builder.StyleBuilderWriter):
'properties': self._properties,
'enums': self._generated_enums,
'include_paths': self._include_paths,
'fields': self._fields,
'computed_style': self._root_group,
}
@template_expander.use_jinja('ComputedStyleBase.cpp.tmpl')
......@@ -341,8 +373,7 @@ class ComputedStyleBaseWriter(make_style_builder.StyleBuilderWriter):
return {
'properties': self._properties,
'enums': self._generated_enums,
'fields': self._fields,
'expected_bit_field_bytes': self._expected_bit_field_bytes,
'computed_style': self._root_group,
}
@template_expander.use_jinja('ComputedStyleBaseConstants.h.tmpl')
......@@ -350,7 +381,6 @@ class ComputedStyleBaseWriter(make_style_builder.StyleBuilderWriter):
return {
'properties': self._properties,
'enums': self._generated_enums,
'fields': self._fields,
}
if __name__ == '__main__':
......
......@@ -152,6 +152,10 @@ def enum_value_name(name):
return 'k' + upper_camel_case(name)
def class_name(name):
return upper_camel_case(name)
def class_member_name(name):
return snake_case(name) + "_"
......
{% from 'macros.tmpl' import license %}
{% from 'fields/field.tmpl' import getter_expression, setter_expression %}
{{license()}}
#include "core/ComputedStyleBase.h"
......@@ -7,11 +8,15 @@
namespace blink {
struct SameSizeAsComputedStyleBase {
{% for field in fields|rejectattr("is_bit_field") %}
{% if computed_style.subgroups is defined %}
void* dataRefs[{{computed_style.subgroups|length}}];
{% endif %}
{% for field in computed_style.fields|rejectattr("is_bit_field") %}
{{field.type_name}} {{field.name}}};
{% endfor %}
unsigned m_bit_fields[{{expected_bit_field_bytes}}];
unsigned m_bit_fields[{{computed_style.num_32_bit_words_for_bit_fields}}];
};
// If this fails, the packing algorithm in make_computed_style_base.py has
// failed to produce the optimal packed size. To fix, update the algorithm to
// ensure that the buckets are placed so that each takes up at most 1 word.
......@@ -19,23 +24,23 @@ ASSERT_SIZE(ComputedStyleBase, SameSizeAsComputedStyleBase);
void ComputedStyleBase::InheritFrom(const ComputedStyleBase& inheritParent,
IsAtShadowBoundary isAtShadowBoundary) {
{% for field in fields if field.is_inherited %}
{{field.name}} = inheritParent.{{field.name}};
{% for field in computed_style.all_fields if field.is_inherited %}
{{setter_expression(field)}} = inheritParent.{{getter_expression(field)}};
{% endfor %}
}
void ComputedStyleBase::CopyNonInheritedFromCached(
const ComputedStyleBase& other) {
{% for field in fields if (field.is_property and not field.is_inherited) or field.is_inherited_flag %}
{{field.name}} = other.{{field.name}};
{% for field in computed_style.all_fields if (field.is_property and not field.is_inherited) or field.is_inherited_flag %}
{{setter_expression(field)}} = other.{{getter_expression(field)}};
{% endfor %}
}
void ComputedStyleBase::PropagateIndependentInheritedProperties(
const ComputedStyleBase& parentStyle) {
{% for field in fields if field.is_property and field.is_independent %}
{% for field in computed_style.all_fields if field.is_property and field.is_independent %}
if ({{field.is_inherited_method_name}}())
{{field.setter_method_name}}(parentStyle.{{field.getter_method_name}}());
{{setter_expression(field)}} = parentStyle.{{getter_expression(field)}};
{% endfor %}
}
......
{% from 'macros.tmpl' import license, print_if %}
{% from 'fields/field.tmpl' import encode, getter_expression, declare_storage %}
{% from 'fields/group.tmpl' import define_field_group_class %}
{{license()}}
#ifndef ComputedStyleBase_h
......@@ -6,6 +8,7 @@
#include "core/style/ComputedStyleConstants.h"
#include "core/CoreExport.h"
#include "core/style/DataRef.h"
{% for path in include_paths %}
#include "{{path}}"
{% endfor %}
......@@ -35,16 +38,16 @@ class CORE_EXPORT ComputedStyleBase {
public:
inline bool IndependentInheritedEqual(const ComputedStyleBase& o) const {
return (
{% for field in fields if field.is_inherited and field.is_independent %}
{{field.name}} == o.{{field.name}}{{print_if(not loop.last, ' &&')}}
{% for field in computed_style.all_fields if field.is_inherited and field.is_independent %}
{{getter_expression(field)}} == o.{{getter_expression(field)}}{{print_if(not loop.last, ' &&')}}
{% endfor %}
);
}
inline bool NonIndependentInheritedEqual(const ComputedStyleBase& o) const {
return (
{% for field in fields if field.is_inherited and not field.is_independent %}
{{field.name}} == o.{{field.name}}{{print_if(not loop.last, ' &&')}}
{% for field in computed_style.all_fields if field.is_inherited and not field.is_independent %}
{{getter_expression(field)}} == o.{{getter_expression(field)}}{{print_if(not loop.last, ' &&')}}
{% endfor %}
);
}
......@@ -55,8 +58,8 @@ class CORE_EXPORT ComputedStyleBase {
inline bool NonInheritedEqual(const ComputedStyleBase& o) const {
return (
{% for field in fields if field.is_property and not field.is_inherited %}
{{field.name}} == o.{{field.name}}{{print_if(not loop.last, ' &&')}}
{% for field in computed_style.all_fields if field.is_property and not field.is_inherited %}
{{getter_expression(field)}} == o.{{getter_expression(field)}}{{print_if(not loop.last, ' &&')}}
{% endfor %}
);
}
......@@ -79,21 +82,31 @@ class CORE_EXPORT ComputedStyleBase {
// TODO(sashab): Remove initialFoo() static methods and update callers to
// use resetFoo(), which can be more efficient.
{% for field in fields %}
{% for field in computed_style.all_fields %}
// {{field.property_name}}
{{field_templates[field.field_template].decl_public_methods(field)|indent(2)}}
{% endfor %}
private:
{% for subgroup in computed_style.subgroups %}
{{define_field_group_class(subgroup)|indent(2)}}
{% endfor %}
protected:
// Constructor and destructor are protected so that only the parent class ComputedStyle
// can instantiate this class.
ALWAYS_INLINE ComputedStyleBase() :
{% for field in fields %}
{% for field in computed_style.fields %}
{{field.name}}({{encode(field, field.default_value)}}){{print_if(not loop.last, ',')}}
{% endfor %}
{}
{
{% for subgroup in computed_style.subgroups %}
{{subgroup.member_name}}.Init();
{% endfor %}
}
{% for field in fields %}
{% for field in computed_style.fields %}
{% if field.field_template in ('storage_only', 'monotonic_flag', 'external') %}
// {{field.property_name}}
{{field_templates[field.field_template].decl_protected_methods(field)|indent(2)}}
......@@ -103,14 +116,14 @@ class CORE_EXPORT ComputedStyleBase {
~ComputedStyleBase() = default;
private:
// Storage.
{% for field in fields %}
{% if field.is_bit_field %}
unsigned {{field.name}} : {{field.size}}; // {{field.type_name}}
{% else %}
{{field.type_name}} {{field.name}};
{% endif %}
{% for subgroup in computed_style.subgroups %}
DataRef<{{subgroup.type_name}}> {{subgroup.member_name}};
{% endfor %}
private:
{% for field in computed_style.fields %}
{{declare_storage(field)}}
{% endfor %}
};
......
{% from 'fields/field.tmpl' import encode, decode, const_ref, nonconst_ref %}
{% from 'fields/field.tmpl' import encode, decode, const_ref, nonconst_ref, getter_expression, setter_expression %}
{% macro decl_initial_method(field) -%}
inline static {{field.type_name}} {{field.initial_method_name}}() {
......@@ -8,36 +8,36 @@ inline static {{field.type_name}} {{field.initial_method_name}}() {
{% macro decl_getter_method(field) -%}
{{const_ref(field)}} {{field.getter_method_name}}() const {
return {{decode(field, field.name)}};
return {{decode(field, getter_expression(field))}};
}
{%- endmacro %}
{% macro decl_setter_method(field) -%}
void {{field.setter_method_name}}({{const_ref(field)}} v) {
{{field.name}} = {{encode(field, "v")}};
{{setter_expression(field)}} = {{encode(field, "v")}};
}
{%- endmacro %}
{% macro decl_resetter_method(field) -%}
inline void {{field.resetter_method_name}}() {
{{field.name}} = {{encode(field, field.default_value)}};
{{setter_expression(field)}} = {{encode(field, field.default_value)}};
}
{%- endmacro %}
{% macro decl_mutable_method(field) -%}
{{nonconst_ref(field)}} {{field.mutable_method_name}}() const {
return {{decode(field, field.name)}};
return {{decode(field, getter_expression(field))}};
}
{%- endmacro %}
{% macro decl_internal_getter_method(field) -%}
{{const_ref(field)}} {{field.internal_getter_method_name}}() const {
return {{decode(field, field.name)}};
return {{decode(field, getter_expression(field))}};
}
{%- endmacro %}
{% macro decl_internal_setter_method(field) -%}
void {{field.internal_setter_method_name}}({{const_ref(field)}} v) {
{{field.name}} = {{encode(field, "v")}};
{{setter_expression(field)}} = {{encode(field, "v")}};
}
{%- endmacro %}
{% import 'fields/base.tmpl' as base %}
{% from 'fields/field.tmpl' import setter_expression %}
{% macro decl_public_methods(field) %}
{{base.decl_initial_method(field)}}
{{base.decl_getter_method(field)}}
{{base.decl_setter_method(field)}}
void {{field.setter_method_name}}({{field.type_name}}&& v) {
{{field.name}} = std::move(v);
{{setter_expression(field)}} = std::move(v);
}
{{base.decl_resetter_method(field)}}
{% endmacro %}
......
......@@ -14,6 +14,22 @@ static_cast<{{field.type_name}}>({{value}})
{%- endif %}
{% endmacro %}
{% macro getter_expression(field) %}
{% if field.group_member_name -%}
{{field.group_member_name}}->{{field.name}}
{%- else -%}
{{field.name}}
{%- endif %}
{% endmacro %}
{% macro setter_expression(field) %}
{% if field.group_member_name -%}
{{field.group_member_name}}.Access()->{{field.name}}
{%- else -%}
{{field.name}}
{%- endif %}
{% endmacro %}
{% macro nonconst_ref(field) %}
{% if field.is_bit_field -%}
{{field.type_name}}
......@@ -29,3 +45,11 @@ static_cast<{{field.type_name}}>({{value}})
const {{field.type_name}}&
{%- endif %}
{% endmacro %}
{% macro declare_storage(field) %}
{% if field.is_bit_field %}
unsigned {{field.name}} : {{field.size}}; // {{field.type_name}}
{%- else %}
{{field.type_name}} {{field.name}};
{%- endif %}
{% endmacro %}
{% from 'fields/field.tmpl' import encode, declare_storage %}
{% from 'macros.tmpl' import print_if %}
{% macro define_field_group_class(group): -%}
class {{group.type_name}} : public RefCounted<{{group.type_name}}> {
public:
static PassRefPtr<{{group.type_name}}> Create() {
return AdoptRef(new {{group.type_name}});
}
PassRefPtr<{{group.type_name}}> Copy() const {
return AdoptRef(new {{group.type_name}}(*this));
}
bool operator==(const {{group.type_name}}& other) const {
return (
{% for field in group.fields %}
{{field.name}} == other.{{field.name}}{{print_if(not loop.last, ' &&')}}
{% endfor %}
);
}
bool operator!=(const {{group.type_name}}& other) const { return !(*this == other); }
{% for field in group.fields %}
{{declare_storage(field)}}
{% endfor %}
private:
{{group.type_name}}() :
{% for field in group.fields %}
{{field.name}}({{encode(field, field.default_value)}}){{print_if(not loop.last, ',')}}
{% endfor %}
{}
ALWAYS_INLINE {{group.type_name}}(const {{group.type_name}}& other) :
RefCounted<{{group.type_name}}>(),
{% for field in group.fields %}
{{field.name}}(other.{{field.name}}){{print_if(not loop.last, ',')}}
{% endfor %}
{}
};
{%- endmacro %}
{% import 'fields/base.tmpl' as base %}
{% from 'fields/field.tmpl' import encode %}
{% from 'fields/field.tmpl' import encode, setter_expression %}
{% macro decl_public_methods(field) %}
{{base.decl_getter_method(field)}}
void {{field.setter_method_name}}() {
{{field.name}} = {{encode(field, "true")}};
{{setter_expression(field)}} = {{encode(field, "true")}};
}
{% endmacro %}
......
......@@ -413,6 +413,7 @@ css_properties("make_core_generated_computed_style_base") {
in_files = [ "css/ComputedStyleExtraFields.json5" ]
other_inputs = [
"../build/scripts/templates/fields/field.tmpl",
"../build/scripts/templates/fields/group.tmpl",
"../build/scripts/templates/fields/base.tmpl",
"../build/scripts/templates/fields/keyword.tmpl",
"../build/scripts/templates/fields/primitive.tmpl",
......
......@@ -61,6 +61,14 @@
valid_values: ["parseSingleValue", "parseShorthand"],
},
// - field_group
// Name of the group that this field belongs to. Fields in the same group are stored
// together as a nested class inside ComputedStyle and dynamically allocated on use.
// Leave this out if the field is stored directly on ComputedStyle.
field_group: {
value_type: "str"
},
// - field_template
// Affects how the interface to this field is generated.
// TODO(sashab, meade): Remove this once TypedOM types are specified for
......@@ -796,6 +804,10 @@
keywords: ["auto"],
supports_percentage: true,
typedom_types: ["Length"],
field_template: "external",
field_type_path: "platform/Length",
field_group: "surround",
default_value: "Length()",
},
{
name: "box-shadow",
......@@ -1198,6 +1210,10 @@
keywords: ["auto"],
supports_percentage: true,
typedom_types: ["Length"],
field_template: "external",
field_type_path: "platform/Length",
field_group: "surround",
default_value: "Length()",
},
{
name: "letter-spacing",
......@@ -1265,6 +1281,10 @@
api_methods: ["parseSingleValue"],
converter: "ConvertQuirkyLength",
interpolable: true,
field_template: "external",
field_type_path: "platform/Length",
field_group: "surround",
default_value: "Length(kFixed)",
},
{
name: "margin-left",
......@@ -1272,6 +1292,10 @@
api_methods: ["parseSingleValue"],
converter: "ConvertQuirkyLength",
interpolable: true,
field_template: "external",
field_type_path: "platform/Length",
field_group: "surround",
default_value: "Length(kFixed)",
},
{
name: "margin-right",
......@@ -1279,6 +1303,10 @@
api_methods: ["parseSingleValue"],
converter: "ConvertQuirkyLength",
interpolable: true,
field_template: "external",
field_type_path: "platform/Length",
field_group: "surround",
default_value: "Length(kFixed)",
},
{
name: "margin-top",
......@@ -1286,6 +1314,10 @@
api_methods: ["parseSingleValue"],
converter: "ConvertQuirkyLength",
interpolable: true,
field_template: "external",
field_type_path: "platform/Length",
field_group: "surround",
default_value: "Length(kFixed)",
},
{
name: "marker-end",
......@@ -1508,6 +1540,10 @@
api_methods: ["parseSingleValue"],
converter: "ConvertLength",
interpolable: true,
field_template: "external",
field_type_path: "platform/Length",
field_group: "surround",
default_value: "Length(kFixed)",
},
{
name: "padding-left",
......@@ -1515,6 +1551,10 @@
api_methods: ["parseSingleValue"],
converter: "ConvertLength",
interpolable: true,
field_template: "external",
field_type_path: "platform/Length",
field_group: "surround",
default_value: "Length(kFixed)",
},
{
name: "padding-right",
......@@ -1522,6 +1562,10 @@
api_methods: ["parseSingleValue"],
converter: "ConvertLength",
interpolable: true,
field_template: "external",
field_type_path: "platform/Length",
field_group: "surround",
default_value: "Length(kFixed)",
},
{
name: "padding-top",
......@@ -1529,6 +1573,10 @@
api_methods: ["parseSingleValue"],
converter: "ConvertLength",
interpolable: true,
field_template: "external",
field_type_path: "platform/Length",
field_group: "surround",
default_value: "Length(kFixed)",
},
{
name: "paint-order",
......@@ -1589,6 +1637,10 @@
keywords: ["auto"],
supports_percentage: true,
typedom_types: ["Length"],
field_template: "external",
field_type_path: "platform/Length",
field_group: "surround",
default_value: "Length()",