mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-13 18:46:37 +00:00
feat(element-style): adopt hand-written customization blocks, and widen the font list
Nineteen plugins spell their style elements out longhand instead of declaring them -- football's block is 701 lines for seven elements -- and predate this system entirely. Core now recognises that shape, so they pick up the row-per-element editor and the real font picker on a core update rather than on a plugin release. Checked against every published schema: 21 plugins adopt, and the defaults of each still validate against the schema generated for it. Detection requires *every* field in a block to be one this system understands. A looser "has at least one style field" rule sweeps in baseball's `count`, which carries a text_color beside geometry that means nothing here. That distinction took three attempts to test: the first two assertions passed under both rules, because an over-eager rule leaves a fontless block looking untouched and only surfaces as an extra row in the editor. The hardcoded font enum is replaced rather than extended. Football lists five of the thirty-five installed fonts, which is why a font a user uploads can never appear in one. It is not a curated safe set -- it omits some twenty other faces that fit the declared size cap just as well -- it is the fonts that happened to exist when it was written. Widening it does need a guard, though, and not the one the schema already has: a bitmap font ignores font_size and renders at its size baked into the file, so `maximum: 16` cannot stop a 27px face. The picker now filters out fixed-size fonts taller than the element's own declared ceiling, which drops exactly the four that would overflow a 32px panel and keeps the other thirty. Per-mode overrides stay opt-in: core cannot invent a plugin's display modes, so `x-style-modes` remains the one line that unlocks them. Their layout half covers every positionable element rather than only those with a style block -- the two namespaces do not line up in a hand-written schema, and football positions six things (logos, timeouts, possession) that have no style block at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+193
-2
@@ -307,7 +307,9 @@ def expand_style_elements(schema: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return schema
|
||||
declaration = customization.get('x-style-elements')
|
||||
if not isinstance(declaration, dict) or not declaration:
|
||||
return schema
|
||||
# No compact declaration: the plugin may still have hand-written
|
||||
# its style blocks longhand, which nineteen of them do.
|
||||
return _adopt_handwritten_block(schema, customization)
|
||||
|
||||
expanded = copy.deepcopy(schema)
|
||||
customization = expanded['properties']['customization']
|
||||
@@ -370,6 +372,7 @@ def _element_block_from_spec(element_key: str,
|
||||
properties: Dict[str, Any] = {}
|
||||
order = []
|
||||
|
||||
size_spec = spec.get('size') if isinstance(spec.get('size'), dict) else None
|
||||
font_spec = spec.get('font')
|
||||
if isinstance(font_spec, dict):
|
||||
font_prop: Dict[str, Any] = {
|
||||
@@ -381,6 +384,13 @@ def _element_block_from_spec(element_key: str,
|
||||
# text box the user had to type a filename into.
|
||||
'x-widget': 'font-selector',
|
||||
}
|
||||
# A bitmap font ignores font_size and renders at its own baked-in
|
||||
# size, so the size ceiling has to be enforced when picking the
|
||||
# font, not when setting the size.
|
||||
max_size = (size_spec or {}).get('max') if isinstance(
|
||||
spec.get('size'), dict) else None
|
||||
if isinstance(max_size, (int, float)):
|
||||
font_prop['x-options'] = {'maxFixedSize': max_size}
|
||||
if 'default' in font_spec:
|
||||
font_prop['default'] = font_spec['default']
|
||||
if isinstance(font_spec.get('enum'), list):
|
||||
@@ -388,7 +398,6 @@ def _element_block_from_spec(element_key: str,
|
||||
properties['font'] = font_prop
|
||||
order.append('font')
|
||||
|
||||
size_spec = spec.get('size')
|
||||
if isinstance(size_spec, dict):
|
||||
size_prop: Dict[str, Any] = {
|
||||
'type': 'integer',
|
||||
@@ -587,6 +596,188 @@ def _modes_block(declaration: Dict[str, Any],
|
||||
}
|
||||
|
||||
|
||||
#: The sub-fields that make a customization sub-object a style element.
|
||||
#: Checked against every published schema: 68 blocks across 19 plugins match
|
||||
#: exactly, and nothing else does -- favorite_result_colors, baseball's
|
||||
#: bases/outs/player_card, jellyfin's progress_bar and the stocks blocks all
|
||||
#: carry other fields and are correctly left alone.
|
||||
_STYLE_BLOCK_FIELDS = frozenset(_STYLE_KEYS)
|
||||
|
||||
|
||||
def _looks_like_style_block(block: Any) -> bool:
|
||||
"""Whether a hand-written customization sub-object is a style element.
|
||||
|
||||
Deliberately strict: every field must be one this system understands.
|
||||
A looser rule ("has at least one style field") would sweep in blocks
|
||||
like baseball's ``count``, which happens to carry a text_color next to
|
||||
geometry that means nothing here.
|
||||
"""
|
||||
if not isinstance(block, dict):
|
||||
return False
|
||||
props = block.get('properties')
|
||||
if not isinstance(props, dict) or not props:
|
||||
return False
|
||||
return set(props) <= _STYLE_BLOCK_FIELDS
|
||||
|
||||
|
||||
def _detect_style_blocks(customization: Dict[str, Any]) -> list:
|
||||
"""Element keys in a hand-written customization block, in declared order."""
|
||||
props = customization.get('properties')
|
||||
if not isinstance(props, dict):
|
||||
return []
|
||||
return [key for key, block in props.items()
|
||||
if key not in ('layout', 'modes') and _looks_like_style_block(block)]
|
||||
|
||||
|
||||
def _upgrade_font_property(block: Dict[str, Any]) -> None:
|
||||
"""Point a hand-written font field at the font picker, in place.
|
||||
|
||||
These fields ship a hardcoded ``enum`` -- football lists five of the
|
||||
thirty-five installed fonts -- which is why a font a user uploads can
|
||||
never appear in one. The enum is replaced rather than extended: it is
|
||||
not a curated safe set (it omits some twenty other faces that fit
|
||||
just as well), it is the fonts that happened to exist when it was
|
||||
written.
|
||||
|
||||
The size ceiling the block already declares is carried across as
|
||||
``maxFixedSize``, because a bitmap font ignores font_size and renders
|
||||
at its own baked-in size -- so widening the list without that would
|
||||
offer faces that overflow the panel no matter what size is set.
|
||||
"""
|
||||
props = block.get('properties')
|
||||
if not isinstance(props, dict):
|
||||
return
|
||||
font_prop = props.get('font')
|
||||
if not isinstance(font_prop, dict):
|
||||
return
|
||||
|
||||
font_prop.pop('enum', None)
|
||||
font_prop['x-widget'] = 'font-selector'
|
||||
|
||||
size_prop = props.get('font_size')
|
||||
maximum = size_prop.get('maximum') if isinstance(size_prop, dict) else None
|
||||
if isinstance(maximum, (int, float)):
|
||||
options = font_prop.setdefault('x-options', {})
|
||||
if isinstance(options, dict):
|
||||
options.setdefault('maxFixedSize', maximum)
|
||||
|
||||
|
||||
def _nullable_block(block: Dict[str, Any], title: Optional[str] = None,
|
||||
description: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""A copy of an element block with every field optional.
|
||||
|
||||
The per-mode counterpart of a hand-written block: same fields, all
|
||||
nullable and defaulting to null, which is this system's "inherit".
|
||||
"""
|
||||
out = copy.deepcopy(block)
|
||||
out['properties'] = {k: _nullable(v)
|
||||
for k, v in (out.get('properties') or {}).items()}
|
||||
out['x-style-managed'] = True
|
||||
if title:
|
||||
out['title'] = title
|
||||
if description is not None:
|
||||
out['description'] = description
|
||||
return out
|
||||
|
||||
|
||||
def _modes_block_from_properties(props: Dict[str, Any], element_keys: list,
|
||||
modes: Any) -> Dict[str, Any]:
|
||||
"""``customization.modes`` built from already-expanded element blocks.
|
||||
|
||||
The compact declaration has ``_modes_block``; this is the same thing for
|
||||
a plugin that hand-wrote its blocks, so both forms get per-mode overrides
|
||||
from one declaration line.
|
||||
"""
|
||||
layout_source = (props.get('layout') or {}).get('properties') or {}
|
||||
mode_props: Dict[str, Any] = {}
|
||||
for mode in modes:
|
||||
if not isinstance(mode, str) or not mode:
|
||||
continue
|
||||
element_props: Dict[str, Any] = {}
|
||||
layout_props: Dict[str, Any] = {}
|
||||
for key in element_keys:
|
||||
block = props.get(key)
|
||||
if isinstance(block, dict):
|
||||
element_props[key] = _nullable_block(
|
||||
block,
|
||||
description='Leave blank to use the settings above for '
|
||||
'this mode.')
|
||||
# Every layout element, not just those with a style block. The two
|
||||
# namespaces do not line up in a hand-written schema -- football
|
||||
# styles 'score_text' but positions 'score', and positions logos,
|
||||
# timeouts and possession that have no style block at all. Keying
|
||||
# this off the style elements would have given six of its eleven
|
||||
# positionable things no per-mode offset.
|
||||
for key, layout_block in layout_source.items():
|
||||
if isinstance(layout_block, dict):
|
||||
layout_props[key] = _nullable_block(layout_block)
|
||||
if layout_props:
|
||||
element_props['layout'] = {
|
||||
'type': 'object',
|
||||
'title': 'Layout Offsets',
|
||||
'x-advanced': True,
|
||||
'additionalProperties': False,
|
||||
'properties': layout_props,
|
||||
}
|
||||
mode_props[mode] = {
|
||||
'type': 'object',
|
||||
'title': mode.replace('_', ' ').title(),
|
||||
'x-style-managed': True,
|
||||
'additionalProperties': False,
|
||||
'properties': element_props,
|
||||
}
|
||||
return {
|
||||
'type': 'object',
|
||||
'title': 'Per-Mode Overrides',
|
||||
'description': 'Override the settings above for one display mode. '
|
||||
'Anything left blank follows the settings above.',
|
||||
'x-advanced': True,
|
||||
'additionalProperties': False,
|
||||
'properties': mode_props,
|
||||
}
|
||||
|
||||
|
||||
def _adopt_handwritten_block(schema: Dict[str, Any],
|
||||
customization: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Give a hand-written customization block the same treatment as a
|
||||
declared one, without the plugin rewriting its schema.
|
||||
|
||||
Nineteen plugins spell their style elements out longhand -- football's
|
||||
block is 701 lines for seven elements -- and predate every part of this
|
||||
system. Recognising that shape lets them pick up the row-per-element
|
||||
editor and the real font picker on a core update, with no plugin
|
||||
release. What they do not get for free is per-mode overrides and the
|
||||
visible/align/scale fields, because core cannot invent a plugin's list
|
||||
of display modes: adding ``x-style-modes`` is the one line that unlocks
|
||||
the rest.
|
||||
"""
|
||||
element_keys = _detect_style_blocks(customization)
|
||||
if not element_keys:
|
||||
return schema
|
||||
|
||||
expanded = copy.deepcopy(schema)
|
||||
customization = expanded['properties']['customization']
|
||||
customization.setdefault('x-widget', 'style-editor')
|
||||
props = customization['properties']
|
||||
|
||||
for key in element_keys:
|
||||
_upgrade_font_property(props[key])
|
||||
|
||||
modes = customization.get('x-style-modes')
|
||||
if isinstance(modes, list) and modes:
|
||||
props.setdefault('modes',
|
||||
_modes_block_from_properties(props, element_keys,
|
||||
modes))
|
||||
|
||||
# Stated explicitly because the config form serialises the schema with
|
||||
# Flask's JSON provider, which sorts keys -- without this the elements
|
||||
# reach the browser alphabetised.
|
||||
order = list(element_keys)
|
||||
order += [k for k in props if k not in order]
|
||||
customization.setdefault('x-propertyOrder', order)
|
||||
return expanded
|
||||
|
||||
|
||||
def defaults_from_schema(schema: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Extract per-element style defaults from a config schema dict.
|
||||
|
||||
|
||||
+186
-2
@@ -177,10 +177,25 @@ class TestExpandStyleElements:
|
||||
expand_style_elements(STYLE_ELEMENTS_SCHEMA)
|
||||
assert json.dumps(STYLE_ELEMENTS_SCHEMA, sort_keys=True) == before
|
||||
|
||||
def test_no_declaration_returns_same_object(self):
|
||||
assert expand_style_elements(MANUAL_SCHEMA) is MANUAL_SCHEMA
|
||||
def test_a_schema_with_nothing_to_expand_is_returned_as_is(self):
|
||||
"""No needless deep copy when there is nothing to do."""
|
||||
empty = {"properties": {}}
|
||||
assert expand_style_elements(empty) is empty
|
||||
no_style = {"properties": {"customization": {"type": "object",
|
||||
"properties": {"favorite_result_colors": {"type": "object",
|
||||
"properties": {"win_color": {"type": "array"}}}}}}}
|
||||
assert expand_style_elements(no_style) is no_style
|
||||
|
||||
def test_a_hand_written_block_is_adopted(self):
|
||||
"""Nineteen plugins spell their style elements out longhand rather
|
||||
than declaring them, and predate this system entirely. They pick up
|
||||
the editor and the font picker on a core update rather than on a
|
||||
plugin release."""
|
||||
expanded = expand_style_elements(MANUAL_SCHEMA)
|
||||
assert expanded is not MANUAL_SCHEMA
|
||||
customization = expanded["properties"]["customization"]
|
||||
assert customization["x-widget"] == "style-editor"
|
||||
assert "x-style-elements" not in MANUAL_SCHEMA["properties"]["customization"]
|
||||
|
||||
def test_garbage_input_never_raises(self):
|
||||
bad = {"properties": {"customization": {"x-style-elements": "nope"}}}
|
||||
@@ -1073,3 +1088,172 @@ class TestAliasedLookup:
|
||||
config, defaults_from_schema_file(extra_schema_path)
|
||||
).style("score_text")
|
||||
assert st.scale == 2.0
|
||||
|
||||
HANDWRITTEN = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"customization": {
|
||||
"type": "object",
|
||||
"title": "Display Customization",
|
||||
"properties": {
|
||||
"score_text": {
|
||||
"type": "object",
|
||||
"title": "Game Score",
|
||||
"properties": {
|
||||
"font": {
|
||||
"type": "string",
|
||||
"enum": ["PressStart2P-Regular.ttf", "4x6-font.ttf",
|
||||
"5by7.regular.ttf", "5x7.bdf", "4x6.bdf"],
|
||||
"default": "PressStart2P-Regular.ttf",
|
||||
},
|
||||
"font_size": {"type": "integer", "minimum": 4,
|
||||
"maximum": 16, "default": 10},
|
||||
"text_color": {"type": "array", "minItems": 3,
|
||||
"maxItems": 3, "default": [255, 255, 255]},
|
||||
},
|
||||
},
|
||||
"favorite_result_colors": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean", "default": False},
|
||||
"win_color": {"type": "array", "default": [0, 255, 0]},
|
||||
},
|
||||
},
|
||||
# baseball's 'count' shape: one field this system knows,
|
||||
# next to geometry that means nothing to it.
|
||||
"count": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text_color": {"type": "array", "default": [0, 255, 0]},
|
||||
"y_offset": {"type": "integer", "default": 2},
|
||||
},
|
||||
},
|
||||
"layout": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"score": {"type": "object", "properties": {
|
||||
"x_offset": {"type": "integer", "default": 0},
|
||||
"y_offset": {"type": "integer", "default": 0}}},
|
||||
"home_logo": {"type": "object", "properties": {
|
||||
"x_offset": {"type": "integer", "default": 0}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class TestHandWrittenAdoption:
|
||||
"""Nineteen plugins spell their style elements out longhand -- football's
|
||||
block is 701 lines for seven elements -- and predate this system. Core
|
||||
recognises that shape so they pick up the editor and the real font picker
|
||||
on a core update rather than on a plugin release.
|
||||
"""
|
||||
|
||||
def _customization(self, schema=None):
|
||||
return expand_style_elements(
|
||||
schema or HANDWRITTEN)["properties"]["customization"]
|
||||
|
||||
def test_the_block_gets_the_composite_editor(self):
|
||||
assert self._customization()["x-widget"] == "style-editor"
|
||||
|
||||
def test_the_declared_order_is_stated_explicitly(self):
|
||||
"""Flask's JSON provider sorts keys, so without this the elements
|
||||
reach the browser alphabetised."""
|
||||
order = self._customization()["x-propertyOrder"]
|
||||
assert order[0] == "score_text"
|
||||
|
||||
def test_a_block_that_is_not_styling_is_left_alone(self):
|
||||
"""favorite_result_colors, baseball's bases/outs/player_card and the
|
||||
stocks blocks all carry fields this system knows nothing about."""
|
||||
block = self._customization()["properties"]["favorite_result_colors"]
|
||||
assert "x-style-managed" not in block
|
||||
assert block["properties"]["win_color"]["default"] == [0, 255, 0]
|
||||
|
||||
def test_a_block_that_merely_shares_a_field_is_left_alone(self):
|
||||
"""The reason detection requires *every* field to be one this system
|
||||
understands. baseball's 'count' carries a text_color beside a
|
||||
y_offset that means nothing here.
|
||||
|
||||
Asserted on the element list rather than on the block itself: an
|
||||
over-eager rule leaves a fontless block looking untouched, and only
|
||||
shows up as an extra row in the editor and an extra per-mode
|
||||
override group.
|
||||
"""
|
||||
schema = copy.deepcopy(HANDWRITTEN)
|
||||
schema["properties"]["customization"]["x-style-modes"] = ["live"]
|
||||
live = expand_style_elements(schema)["properties"]["customization"][
|
||||
"properties"]["modes"]["properties"]["live"]["properties"]
|
||||
assert "score_text" in live
|
||||
assert "count" not in live, (
|
||||
"'count' is not a style element -- it shares one field and "
|
||||
"carries geometry this system does not understand")
|
||||
assert "favorite_result_colors" not in live
|
||||
|
||||
def test_the_hardcoded_font_list_is_replaced_by_the_picker(self):
|
||||
"""The reason an uploaded font could never appear in one of these."""
|
||||
font = self._customization()["properties"]["score_text"]["properties"]["font"]
|
||||
assert "enum" not in font
|
||||
assert font["x-widget"] == "font-selector"
|
||||
|
||||
def test_the_picker_inherits_the_declared_size_ceiling(self):
|
||||
"""A bitmap font ignores font_size and renders at its own baked-in
|
||||
size, so a field capped at 16 must not offer a 27px face."""
|
||||
font = self._customization()["properties"]["score_text"]["properties"]["font"]
|
||||
assert font["x-options"]["maxFixedSize"] == 16
|
||||
|
||||
def test_the_users_existing_font_choice_stays_valid(self):
|
||||
font = self._customization()["properties"]["score_text"]["properties"]["font"]
|
||||
assert font["default"] == "PressStart2P-Regular.ttf"
|
||||
assert font["type"] == "string"
|
||||
|
||||
def test_the_input_schema_is_not_mutated(self):
|
||||
expand_style_elements(HANDWRITTEN)
|
||||
original = HANDWRITTEN["properties"]["customization"]
|
||||
assert "x-widget" not in original
|
||||
assert "enum" in original["properties"]["score_text"]["properties"]["font"]
|
||||
|
||||
def test_no_style_blocks_means_no_expansion(self):
|
||||
schema = {"properties": {"customization": {"type": "object",
|
||||
"properties": {"favorite_result_colors": {"type": "object",
|
||||
"properties": {"win_color": {"type": "array"}}}}}}}
|
||||
assert expand_style_elements(schema) is schema
|
||||
|
||||
|
||||
class TestAdoptedModes:
|
||||
"""Per-mode overrides stay opt-in: core cannot invent a plugin's list of
|
||||
display modes. Declaring x-style-modes is the one line that unlocks them
|
||||
for a hand-written block."""
|
||||
|
||||
def _live(self):
|
||||
schema = copy.deepcopy(HANDWRITTEN)
|
||||
schema["properties"]["customization"]["x-style-modes"] = ["live", "recent"]
|
||||
return expand_style_elements(schema)["properties"]["customization"][
|
||||
"properties"]["modes"]["properties"]["live"]["properties"]
|
||||
|
||||
def test_modes_are_not_invented(self):
|
||||
assert "modes" not in self._customization_props()
|
||||
|
||||
def _customization_props(self):
|
||||
return expand_style_elements(HANDWRITTEN)["properties"]["customization"]["properties"]
|
||||
|
||||
def test_declaring_modes_generates_them(self):
|
||||
assert "score_text" in self._live()
|
||||
|
||||
def test_mode_fields_are_nullable(self):
|
||||
size = self._live()["score_text"]["properties"]["font_size"]
|
||||
assert size["default"] is None and "null" in size["type"]
|
||||
|
||||
def test_every_positionable_element_gets_per_mode_offsets(self):
|
||||
"""The two namespaces do not line up in a hand-written schema: this
|
||||
one styles 'score_text' but positions 'score', and positions a logo
|
||||
that has no style block at all. Keying the layout off the style
|
||||
elements would have left both without a per-mode offset."""
|
||||
layout = self._live()["layout"]["properties"]
|
||||
assert set(layout) == {"score", "home_logo"}
|
||||
|
||||
def test_the_mode_font_picker_keeps_the_ceiling(self):
|
||||
font = self._live()["score_text"]["properties"]["font"]
|
||||
assert font["x-options"]["maxFixedSize"] == 16
|
||||
assert "null" in font["type"]
|
||||
|
||||
@@ -174,6 +174,12 @@
|
||||
const xOptions = config['x-options'] || config['x_options'] || {};
|
||||
const placeholder = xOptions.placeholder || 'Select a font...';
|
||||
const filterTypes = xOptions.filterTypes || null; // e.g., ['ttf', 'bdf']
|
||||
// Bitmap (BDF) fonts render at the one size baked into the file
|
||||
// and ignore the size setting entirely, so a field that caps size
|
||||
// at 16 can still be handed a 27px face. maxFixedSize hides the
|
||||
// ones that cannot honour the cap, rather than offering a choice
|
||||
// that silently overflows the panel.
|
||||
const maxFixedSize = Number(xOptions.maxFixedSize) || null;
|
||||
const showPreview = xOptions.showPreview === true;
|
||||
const disabled = xOptions.disabled === true;
|
||||
const required = xOptions.required === true;
|
||||
@@ -204,6 +210,16 @@
|
||||
return filterTypes.some(t => t.toLowerCase() === fontType);
|
||||
});
|
||||
}
|
||||
if (maxFixedSize) {
|
||||
filteredFonts = filteredFonts.filter(font => {
|
||||
// A scalable face can always meet the cap.
|
||||
if (font.scalable !== false) { return true; }
|
||||
// An unknown native size is not evidence it is too
|
||||
// big; keep it rather than hiding a usable font.
|
||||
if (!font.native_size) { return true; }
|
||||
return font.native_size <= maxFixedSize;
|
||||
});
|
||||
}
|
||||
|
||||
// Build select HTML
|
||||
let html = `<div id="${fieldId}_widget" class="font-selector-widget" data-field-id="${fieldId}">`;
|
||||
|
||||
@@ -127,6 +127,27 @@
|
||||
|
||||
// ---- individual controls -------------------------------------------
|
||||
|
||||
/**
|
||||
* Fonts this field can actually honour.
|
||||
*
|
||||
* A bitmap (BDF) face renders at the one size baked into the file and
|
||||
* ignores the size setting, so an element that caps size at 16 can
|
||||
* still be handed a 27px face and overflow a 32px panel. The cap comes
|
||||
* from the element's own font_size.maximum, so this hides exactly the
|
||||
* faces that cannot meet the limit the plugin already declared --
|
||||
* rather than a list curated by hand, which is what these fields used
|
||||
* to carry and why an uploaded font could never appear in one.
|
||||
*/
|
||||
function usableFonts(fonts, maxFixedSize) {
|
||||
if (!maxFixedSize) { return fonts; }
|
||||
return fonts.filter(function (f) {
|
||||
if (f.scalable !== false) { return true; }
|
||||
// An unknown native size is not evidence it is too big.
|
||||
if (!f.nativeSize) { return true; }
|
||||
return f.nativeSize <= maxFixedSize;
|
||||
});
|
||||
}
|
||||
|
||||
function fontControl(name, current, fonts, optional, onChange) {
|
||||
var select = el('select', {
|
||||
name: name,
|
||||
@@ -334,7 +355,10 @@
|
||||
var types = Array.isArray(declared) ? declared : [declared];
|
||||
|
||||
if (opts.key === 'font' || prop['x-widget'] === 'font-selector') {
|
||||
return fontControl(opts.name, opts.current, opts.fonts,
|
||||
var xOptions = prop['x-options'] || prop['x_options'] || {};
|
||||
var cap = Number(xOptions.maxFixedSize) || opts.maxFixedSize || null;
|
||||
return fontControl(opts.name, opts.current,
|
||||
usableFonts(opts.fonts, cap),
|
||||
opts.optional, opts.onFontChange);
|
||||
}
|
||||
if (types.indexOf('boolean') !== -1) {
|
||||
@@ -411,6 +435,9 @@
|
||||
current: effective(value, path, prop, optional),
|
||||
optional: optional,
|
||||
fonts: opts.fonts,
|
||||
// The element's declared size ceiling is what a fixed-size
|
||||
// font has to fit under.
|
||||
maxFixedSize: (props.font_size || {}).maximum || null,
|
||||
onFontChange: function () { syncSize(fontSelect); }
|
||||
});
|
||||
if (col.key === 'font') { fontSelect = node; }
|
||||
|
||||
Reference in New Issue
Block a user