mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-10 13:18:04 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cad279a7d2 |
@@ -149,7 +149,7 @@
|
|||||||
"min_plugin_width": 8,
|
"min_plugin_width": 8,
|
||||||
"lead_in_width": 0,
|
"lead_in_width": 0,
|
||||||
"plugins_per_cycle": 6,
|
"plugins_per_cycle": 6,
|
||||||
"max_plugin_width_ratio": 0.0,
|
"max_plugin_width_ratio": 3.0,
|
||||||
"overflow_mode": "rotate",
|
"overflow_mode": "rotate",
|
||||||
"dynamic_duration_enabled": true,
|
"dynamic_duration_enabled": true,
|
||||||
"min_cycle_duration": 60,
|
"min_cycle_duration": 60,
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ Read by `src/vegas_mode/config.py` (`VegasScrollConfig.from_config`). See
|
|||||||
| `min_plugin_width` | int, `8` |
|
| `min_plugin_width` | int, `8` |
|
||||||
| `lead_in_width` | int, `0` |
|
| `lead_in_width` | int, `0` |
|
||||||
| `plugins_per_cycle` | int, `6` |
|
| `plugins_per_cycle` | int, `6` |
|
||||||
| `max_plugin_width_ratio` | float, `0.0` |
|
| `max_plugin_width_ratio` | float, `3.0` |
|
||||||
| `overflow_mode` | string, `"rotate"` |
|
| `overflow_mode` | string, `"rotate"` |
|
||||||
| `dynamic_duration_enabled` | bool, `true` |
|
| `dynamic_duration_enabled` | bool, `true` |
|
||||||
| `min_cycle_duration` | int, `60` |
|
| `min_cycle_duration` | int, `60` |
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ from src.plugin_system.testing.loading import ( # noqa: E402
|
|||||||
)
|
)
|
||||||
from src.plugin_system.testing.harness import ( # noqa: E402
|
from src.plugin_system.testing.harness import ( # noqa: E402
|
||||||
RenderResult, render_plugin_matrix, compare_to_goldens, write_goldens,
|
RenderResult, render_plugin_matrix, compare_to_goldens, write_goldens,
|
||||||
|
check_empty_claimed,
|
||||||
check_scale_up,
|
check_scale_up,
|
||||||
)
|
)
|
||||||
from src.plugin_system.testing.sizes import ( # noqa: E402
|
from src.plugin_system.testing.sizes import ( # noqa: E402
|
||||||
@@ -115,6 +116,11 @@ def check_one(plugin_id: str, search_dirs: List[str], sizes, mock_data: Dict,
|
|||||||
declared = load_manifest(plugin_dir).get("display", {}).get("design_size", {})
|
declared = load_manifest(plugin_dir).get("display", {}).get("design_size", {})
|
||||||
design_size = (int(declared.get("width", 128)), int(declared.get("height", 32)))
|
design_size = (int(declared.get("width", 128)), int(declared.get("height", 32)))
|
||||||
fill_strict = spec.get("fill_check") == "strict"
|
fill_strict = spec.get("fill_check") == "strict"
|
||||||
|
# A mode that renders nothing without returning False is never skipped by
|
||||||
|
# the display controller, so it holds a blank panel for its whole duration.
|
||||||
|
# Warn-only by default: a scroll mode's first frame is legitimately its
|
||||||
|
# blank scroll-in buffer.
|
||||||
|
empty_strict = spec.get("empty_check") == "strict"
|
||||||
|
|
||||||
# Every run: the base config, plus one per harness.json "variant" —
|
# Every run: the base config, plus one per harness.json "variant" —
|
||||||
# a config overlay with its own golden dir (e.g. adaptive layout mode
|
# a config overlay with its own golden dir (e.g. adaptive layout mode
|
||||||
@@ -142,6 +148,7 @@ def check_one(plugin_id: str, search_dirs: List[str], sizes, mock_data: Dict,
|
|||||||
compare_to_goldens(results, golden_dir)
|
compare_to_goldens(results, golden_dir)
|
||||||
|
|
||||||
check_scale_up(results, design_size=design_size, strict=fill_strict)
|
check_scale_up(results, design_size=design_size, strict=fill_strict)
|
||||||
|
check_empty_claimed(results, strict=empty_strict)
|
||||||
|
|
||||||
# Tag variant runs so the report and PNG dumps stay distinguishable.
|
# Tag variant runs so the report and PNG dumps stay distinguishable.
|
||||||
if variant_name:
|
if variant_name:
|
||||||
@@ -178,6 +185,9 @@ def print_report(all_results: Dict[str, List[RenderResult]]) -> bool:
|
|||||||
# warn-only underfill: big panel left mostly empty
|
# warn-only underfill: big panel left mostly empty
|
||||||
ex, ey = r.fill_extent
|
ex, ey = r.fill_extent
|
||||||
detail += f" (fill warn: extent {ex:.0%}x{ey:.0%})"
|
detail += f" (fill warn: extent {ex:.0%}x{ey:.0%})"
|
||||||
|
if r.empty_claimed and r.empty_ok is None:
|
||||||
|
detail += (f" (empty warn: drew nothing but display() returned"
|
||||||
|
f" {r.display_returned!r}, so the mode is not skipped)")
|
||||||
else:
|
else:
|
||||||
everything_ok = False
|
everything_ok = False
|
||||||
if r.error is not None:
|
if r.error is not None:
|
||||||
@@ -191,6 +201,11 @@ def print_report(all_results: Dict[str, List[RenderResult]]) -> bool:
|
|||||||
ex, ey = r.fill_extent or (0.0, 0.0)
|
ex, ey = r.fill_extent or (0.0, 0.0)
|
||||||
status = "FAIL"
|
status = "FAIL"
|
||||||
detail = f" fill: extent {ex:.0%}x{ey:.0%} below required coverage"
|
detail = f" fill: extent {ex:.0%}x{ey:.0%} below required coverage"
|
||||||
|
elif r.empty_ok is False:
|
||||||
|
status = "FAIL"
|
||||||
|
detail = (f" drew nothing but display() returned"
|
||||||
|
f" {r.display_returned!r}; return False so the"
|
||||||
|
f" controller skips the mode")
|
||||||
else:
|
else:
|
||||||
status, detail = "FAIL", ""
|
status, detail = "FAIL", ""
|
||||||
print(f" [{status}] {r.size_label:>7} {r.mode}{detail}")
|
print(f" [{status}] {r.size_label:>7} {r.mode}{detail}")
|
||||||
|
|||||||
@@ -73,6 +73,11 @@ class RenderResult:
|
|||||||
golden_ok: Optional[bool] = None
|
golden_ok: Optional[bool] = None
|
||||||
golden_diff_pixels: int = 0
|
golden_diff_pixels: int = 0
|
||||||
golden_max_delta: int = 0
|
golden_max_delta: int = 0
|
||||||
|
# what display() handed back; the controller skips a mode only on False
|
||||||
|
display_returned: Any = None
|
||||||
|
# empty-frame check: rendered nothing while not reporting "no content"
|
||||||
|
empty_claimed: Optional[bool] = None # True when that happened
|
||||||
|
empty_ok: Optional[bool] = None # False only in strict mode
|
||||||
# fill / scale-up check (populated only for sizes >= 2x the design size)
|
# fill / scale-up check (populated only for sizes >= 2x the design size)
|
||||||
fill_checked: bool = False
|
fill_checked: bool = False
|
||||||
fill_ok: Optional[bool] = None # False only in strict mode
|
fill_ok: Optional[bool] = None # False only in strict mode
|
||||||
@@ -92,6 +97,8 @@ class RenderResult:
|
|||||||
return False
|
return False
|
||||||
if self.fill_ok is False:
|
if self.fill_ok is False:
|
||||||
return False
|
return False
|
||||||
|
if self.empty_ok is False:
|
||||||
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -132,21 +139,25 @@ def _instantiate(plugin_id: str, manifest: Dict[str, Any], plugin_dir: Path,
|
|||||||
return plugin_instance
|
return plugin_instance
|
||||||
|
|
||||||
|
|
||||||
def _render_mode(plugin_instance: Any, mode: str) -> None:
|
def _render_mode(plugin_instance: Any, mode: str) -> Any:
|
||||||
"""Render a specific screen. Prefer an explicit display_mode kwarg; otherwise
|
"""Render a specific screen. Prefer an explicit display_mode kwarg; otherwise
|
||||||
drive the plugin's internal mode state machine (first display() call renders
|
drive the plugin's internal mode state machine (first display() call renders
|
||||||
modes[current_mode_index] when current_display_mode is None)."""
|
modes[current_mode_index] when current_display_mode is None).
|
||||||
|
|
||||||
|
Returns whatever display() returned. The display controller skips a mode
|
||||||
|
whose display() returns False, so that value decides whether an empty mode
|
||||||
|
is rotated past or sat on -- which makes it worth reporting rather than
|
||||||
|
discarding."""
|
||||||
sig = inspect.signature(plugin_instance.display)
|
sig = inspect.signature(plugin_instance.display)
|
||||||
if "display_mode" in sig.parameters:
|
if "display_mode" in sig.parameters:
|
||||||
plugin_instance.display(force_clear=True, display_mode=mode)
|
return plugin_instance.display(force_clear=True, display_mode=mode)
|
||||||
return
|
|
||||||
|
|
||||||
modes = getattr(plugin_instance, "modes", None)
|
modes = getattr(plugin_instance, "modes", None)
|
||||||
if modes and mode in modes:
|
if modes and mode in modes:
|
||||||
plugin_instance.current_mode_index = list(modes).index(mode)
|
plugin_instance.current_mode_index = list(modes).index(mode)
|
||||||
if hasattr(plugin_instance, "current_display_mode"):
|
if hasattr(plugin_instance, "current_display_mode"):
|
||||||
plugin_instance.current_display_mode = None
|
plugin_instance.current_display_mode = None
|
||||||
plugin_instance.display(force_clear=False)
|
return plugin_instance.display(force_clear=False)
|
||||||
|
|
||||||
|
|
||||||
def _freeze(freeze_time: Optional[str]):
|
def _freeze(freeze_time: Optional[str]):
|
||||||
@@ -234,7 +245,7 @@ def _render_size(plugin_id, manifest, plugin_dir, config, mock_data,
|
|||||||
logger.warning("update() raised a non-connectivity error for %s [%s]: %s",
|
logger.warning("update() raised a non-connectivity error for %s [%s]: %s",
|
||||||
plugin_id, mode, e)
|
plugin_id, mode, e)
|
||||||
if result.error is None:
|
if result.error is None:
|
||||||
_render_mode(inst, mode)
|
result.display_returned = _render_mode(inst, mode)
|
||||||
result.image = dm.get_image()
|
result.image = dm.get_image()
|
||||||
result.overflow = dm.check_overflow()
|
result.overflow = dm.check_overflow()
|
||||||
except Exception as e: # noqa: BLE001 — a display crash is a real failure
|
except Exception as e: # noqa: BLE001 — a display crash is a real failure
|
||||||
@@ -341,6 +352,44 @@ def fill_metrics(image: Image.Image) -> Tuple[float, float, float]:
|
|||||||
return (extent_x, extent_y, ink)
|
return (extent_x, extent_y, ink)
|
||||||
|
|
||||||
|
|
||||||
|
def check_empty_claimed(results: List[RenderResult],
|
||||||
|
strict: bool = False) -> List[RenderResult]:
|
||||||
|
"""Flag a mode that rendered nothing without reporting "no content".
|
||||||
|
|
||||||
|
The display controller skips a mode whose ``display()`` returns False, and
|
||||||
|
treats anything else -- including None -- as "content was shown". A mode
|
||||||
|
that draws nothing and does not return False therefore holds whatever is on
|
||||||
|
the panel for its whole display duration. Since a mode switch clears first,
|
||||||
|
that is a blank screen. Two sports plugins shipped exactly this: their
|
||||||
|
``display()`` returned None on every path, so an out-of-season league sat
|
||||||
|
blank for its full duration rather than being rotated past.
|
||||||
|
|
||||||
|
Warn-only by default, because a blank frame is not automatically wrong: a
|
||||||
|
scroll mode whose first frame is its blank scroll-in buffer renders empty
|
||||||
|
and is behaving correctly. ``strict=True`` sets ``empty_claimed`` such that
|
||||||
|
``RenderResult.ok`` fails -- opt in per plugin via harness.json
|
||||||
|
``{"empty_check": "strict"}`` once its modes are known to draw on the
|
||||||
|
fixture data.
|
||||||
|
|
||||||
|
Note this can only catch what the fixtures actually render. A plugin whose
|
||||||
|
harness fixture seeds content never exercises its empty path here; the
|
||||||
|
source-level gate in the plugins repo covers that case.
|
||||||
|
"""
|
||||||
|
for r in results:
|
||||||
|
if r.image is None or r.error is not None:
|
||||||
|
continue
|
||||||
|
# An explicit False is the plugin correctly saying "nothing to show".
|
||||||
|
if r.display_returned is False:
|
||||||
|
continue
|
||||||
|
if r.image.convert("L").point(
|
||||||
|
lambda p: 255 if p > _LIT_THRESHOLD else 0).getbbox() is not None:
|
||||||
|
continue
|
||||||
|
r.empty_claimed = True
|
||||||
|
if strict:
|
||||||
|
r.empty_ok = False
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
def check_scale_up(results: List[RenderResult],
|
def check_scale_up(results: List[RenderResult],
|
||||||
design_size: Tuple[int, int] = (128, 32),
|
design_size: Tuple[int, int] = (128, 32),
|
||||||
min_extent: float = _MIN_FILL_EXTENT,
|
min_extent: float = _MIN_FILL_EXTENT,
|
||||||
|
|||||||
@@ -104,22 +104,10 @@ class VegasModeConfig:
|
|||||||
overflow_mode: str = "rotate"
|
overflow_mode: str = "rotate"
|
||||||
|
|
||||||
# Cap on one plugin's share of a cycle, as a multiple of display width.
|
# Cap on one plugin's share of a cycle, as a multiple of display width.
|
||||||
# 0 (the default) disables the cap, so every plugin contributes all of its
|
# A single ticker returning 7,000px would otherwise hold the panel for over
|
||||||
# content and is always entered at its beginning.
|
# two minutes. Overflow is deferred to later cycles rather than discarded.
|
||||||
#
|
# 0 disables the cap.
|
||||||
# Capping was the default until it proved to cost more than it bought.
|
max_plugin_width_ratio: float = 3.0
|
||||||
# Measured over a 17-plugin fleet on a 512px panel, only four plugins were
|
|
||||||
# ever wide enough to hit a 3.0 cap; for those four it produced two visible
|
|
||||||
# faults. Content resumed mid-item on each appearance (a news ticker entered
|
|
||||||
# at column 6027 of its own strip), and the final window of a rotation was
|
|
||||||
# whatever happened to be left — 348px of a 1840px stocks ticker, seven
|
|
||||||
# seconds of panel time. Both read as the display being broken rather than
|
|
||||||
# as deferral working.
|
|
||||||
#
|
|
||||||
# A wide plugin does hold the panel for a long time uncapped: set the cap
|
|
||||||
# per plugin with vegas_max_width_screens where that matters, rather than
|
|
||||||
# globally where it mostly hurts plugins that were never the problem.
|
|
||||||
max_plugin_width_ratio: float = 0.0
|
|
||||||
|
|
||||||
# Plugin management
|
# Plugin management
|
||||||
plugin_order: List[str] = field(default_factory=list)
|
plugin_order: List[str] = field(default_factory=list)
|
||||||
@@ -171,7 +159,7 @@ class VegasModeConfig:
|
|||||||
lead_in_width=int(vegas_config.get('lead_in_width', 0)),
|
lead_in_width=int(vegas_config.get('lead_in_width', 0)),
|
||||||
plugins_per_cycle=int(vegas_config.get('plugins_per_cycle', 6)),
|
plugins_per_cycle=int(vegas_config.get('plugins_per_cycle', 6)),
|
||||||
max_plugin_width_ratio=float(
|
max_plugin_width_ratio=float(
|
||||||
vegas_config.get('max_plugin_width_ratio', 0.0)),
|
vegas_config.get('max_plugin_width_ratio', 3.0)),
|
||||||
overflow_mode=str(vegas_config.get('overflow_mode', 'rotate')),
|
overflow_mode=str(vegas_config.get('overflow_mode', 'rotate')),
|
||||||
plugin_order=list(vegas_config.get('plugin_order', [])),
|
plugin_order=list(vegas_config.get('plugin_order', [])),
|
||||||
excluded_plugins=set(vegas_config.get('excluded_plugins', [])),
|
excluded_plugins=set(vegas_config.get('excluded_plugins', [])),
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
"""Tests for the harness empty-frame check (src/plugin_system/testing/harness.py).
|
||||||
|
|
||||||
|
The display controller skips a mode whose display() returns False and treats
|
||||||
|
anything else -- including None -- as "content was shown". A mode that draws
|
||||||
|
nothing without returning False is therefore never skipped, and since a mode
|
||||||
|
switch clears the panel first, it sits on a blank screen for its whole display
|
||||||
|
duration.
|
||||||
|
|
||||||
|
Two sports plugins shipped exactly that: their display() returned None on every
|
||||||
|
path, so an out-of-season league held a blank panel instead of being rotated
|
||||||
|
past. The harness rendered those modes and passed them, because it discarded
|
||||||
|
the return value entirely.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from src.plugin_system.testing.harness import RenderResult, check_empty_claimed
|
||||||
|
|
||||||
|
|
||||||
|
def _blank(w=64, h=32):
|
||||||
|
return Image.new("RGB", (w, h), (0, 0, 0))
|
||||||
|
|
||||||
|
|
||||||
|
def _drawn(w=64, h=32):
|
||||||
|
img = _blank(w, h)
|
||||||
|
img.paste(Image.new("RGB", (10, 10), (255, 255, 255)), (5, 5))
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def _result(image, returned=None, **kw):
|
||||||
|
return RenderResult("p", 64, 32, "mode", image=image,
|
||||||
|
display_returned=returned, **kw)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCheckEmptyClaimed:
|
||||||
|
def test_blank_frame_returning_none_is_flagged(self):
|
||||||
|
# The shape that shipped: nothing drawn, nothing reported.
|
||||||
|
r = _result(_blank(), returned=None)
|
||||||
|
check_empty_claimed([r])
|
||||||
|
assert r.empty_claimed is True
|
||||||
|
|
||||||
|
def test_blank_frame_returning_true_is_flagged(self):
|
||||||
|
# Just as broken, and more explicit about it.
|
||||||
|
r = _result(_blank(), returned=True)
|
||||||
|
check_empty_claimed([r])
|
||||||
|
assert r.empty_claimed is True
|
||||||
|
|
||||||
|
def test_blank_frame_returning_false_is_fine(self):
|
||||||
|
# The plugin correctly said "no content"; the controller will skip it.
|
||||||
|
r = _result(_blank(), returned=False)
|
||||||
|
check_empty_claimed([r])
|
||||||
|
assert r.empty_claimed is None
|
||||||
|
|
||||||
|
def test_a_drawn_frame_is_fine_whatever_it_returns(self):
|
||||||
|
for returned in (None, True, False):
|
||||||
|
r = _result(_drawn(), returned=returned)
|
||||||
|
check_empty_claimed([r])
|
||||||
|
assert r.empty_claimed is None, returned
|
||||||
|
|
||||||
|
def test_near_black_still_counts_as_drawn(self):
|
||||||
|
# Guard the threshold: content dim enough to look black to the eye is
|
||||||
|
# still content, and flagging it would train people to ignore this.
|
||||||
|
img = _blank()
|
||||||
|
img.paste(Image.new("RGB", (4, 4), (60, 60, 60)), (2, 2))
|
||||||
|
r = _result(img, returned=None)
|
||||||
|
check_empty_claimed([r])
|
||||||
|
assert r.empty_claimed is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestWarnVersusStrict:
|
||||||
|
def test_warn_only_by_default(self):
|
||||||
|
# A scroll mode's first frame is legitimately its blank scroll-in
|
||||||
|
# buffer, so this must not fail a run unless opted in.
|
||||||
|
r = _result(_blank(), returned=None)
|
||||||
|
check_empty_claimed([r])
|
||||||
|
assert r.empty_ok is None
|
||||||
|
assert r.ok is True
|
||||||
|
|
||||||
|
def test_strict_fails_the_result(self):
|
||||||
|
r = _result(_blank(), returned=None)
|
||||||
|
check_empty_claimed([r], strict=True)
|
||||||
|
assert r.empty_ok is False
|
||||||
|
assert r.ok is False
|
||||||
|
|
||||||
|
def test_strict_still_allows_an_honest_false(self):
|
||||||
|
r = _result(_blank(), returned=False)
|
||||||
|
check_empty_claimed([r], strict=True)
|
||||||
|
assert r.empty_ok is None
|
||||||
|
assert r.ok is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestSkippedResults:
|
||||||
|
def test_a_crashed_render_is_left_alone(self):
|
||||||
|
# error already fails the result; adding a second reason just muddies
|
||||||
|
# the report.
|
||||||
|
r = _result(None, returned=None, error="boom")
|
||||||
|
check_empty_claimed([r], strict=True)
|
||||||
|
assert r.empty_claimed is None
|
||||||
|
|
||||||
|
def test_a_result_with_no_image_is_left_alone(self):
|
||||||
|
r = _result(None, returned=None)
|
||||||
|
check_empty_claimed([r], strict=True)
|
||||||
|
assert r.empty_claimed is None
|
||||||
@@ -781,20 +781,6 @@ class TestNewConfigKeys:
|
|||||||
assert cfg.render_width_pct == 100
|
assert cfg.render_width_pct == 100
|
||||||
assert cfg.min_content_separation == 24
|
assert cfg.min_content_separation == 24
|
||||||
|
|
||||||
def test_width_cap_is_off_by_default(self):
|
|
||||||
# Capping made wide plugins resume mid-content on every appearance and
|
|
||||||
# emit runt final windows; it is now opt-in per plugin instead.
|
|
||||||
assert VegasModeConfig().max_plugin_width_ratio == 0.0
|
|
||||||
assert VegasModeConfig.from_config({}).max_plugin_width_ratio == 0.0
|
|
||||||
|
|
||||||
def test_width_cap_is_still_available_when_asked_for(self):
|
|
||||||
# Defaulting the cap off must not remove it: a user who sets a ratio
|
|
||||||
# still gets one, and 0 still means uncapped.
|
|
||||||
cfg = VegasModeConfig.from_config(
|
|
||||||
{'display': {'vegas_scroll': {'max_plugin_width_ratio': 3.0}}})
|
|
||||||
assert cfg.max_plugin_width_ratio == 3.0
|
|
||||||
assert cfg.validate() == []
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('overrides,bad_key', [
|
@pytest.mark.parametrize('overrides,bad_key', [
|
||||||
({'render_width_pct': 5}, 'render_width_pct'),
|
({'render_width_pct': 5}, 'render_width_pct'),
|
||||||
({'render_width_pct': 101}, 'render_width_pct'),
|
({'render_width_pct': 101}, 'render_width_pct'),
|
||||||
|
|||||||
@@ -556,11 +556,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="setting-display-vegas_max_plugin_width_ratio" data-setting-key="display.vegas_scroll.max_plugin_width_ratio">
|
<div class="form-group" id="setting-display-vegas_max_plugin_width_ratio" data-setting-key="display.vegas_scroll.max_plugin_width_ratio">
|
||||||
<label for="vegas_max_plugin_width_ratio" class="block text-sm font-medium text-gray-700">Max Plugin Width (screens){{ ui.help_tip('Caps how much of one cycle a single plugin may occupy, measured in screen widths (0–20).\nDefault: 0 (no limit) — every plugin shows all of its content and always starts at the beginning.\nSet a limit to stop one long ticker holding the display for minutes: it is cut to this width and the remainder shown on later cycles. The trade-off is that such a plugin then resumes mid-content on each appearance instead of starting fresh.', 'Max Plugin Width') }}</label>
|
<label for="vegas_max_plugin_width_ratio" class="block text-sm font-medium text-gray-700">Max Plugin Width (screens){{ ui.help_tip('Caps how much of one cycle a single plugin may occupy, measured in screen widths (0–20).\nDefault: 3. A long ticker such as a news feed or leaderboard is trimmed to this and the remainder shown on later cycles, so one plugin cannot hold the display for minutes. Set 0 for no limit.', 'Max Plugin Width') }}</label>
|
||||||
<input type="number"
|
<input type="number"
|
||||||
id="vegas_max_plugin_width_ratio"
|
id="vegas_max_plugin_width_ratio"
|
||||||
name="vegas_max_plugin_width_ratio"
|
name="vegas_max_plugin_width_ratio"
|
||||||
value="{{ main_config.display.get('vegas_scroll', {}).get('max_plugin_width_ratio', 0.0) }}"
|
value="{{ main_config.display.get('vegas_scroll', {}).get('max_plugin_width_ratio', 3.0) }}"
|
||||||
min="0"
|
min="0"
|
||||||
max="20"
|
max="20"
|
||||||
step="0.5"
|
step="0.5"
|
||||||
|
|||||||
Reference in New Issue
Block a user