diff --git a/examples/reference/widgets/DiscretePlayer.ipynb b/examples/reference/widgets/DiscretePlayer.ipynb index 9323014579..e2559ab4ff 100644 --- a/examples/reference/widgets/DiscretePlayer.ipynb +++ b/examples/reference/widgets/DiscretePlayer.ipynb @@ -38,6 +38,8 @@ "* **``disabled``** (boolean): Whether the widget is editable\n", "* **``name``** (str): The title of the widget\n", "* **``show_loop_controls``** (boolean): Whether radio buttons allowing to switch between loop policies options are shown\n", + "* **``show_value``** (boolean): Whether to display the value of the player\n", + "* **``value_location``** (str): Where to display the value; must be one of 'top_left', 'top_center', 'top_right'\n", "\n", "___" ] @@ -55,7 +57,8 @@ "metadata": {}, "outputs": [], "source": [ - "discrete_player = pn.widgets.DiscretePlayer(name='Discrete Player', options=[2, 4, 8, 16, 32, 64, 128], value=8, loop_policy='loop')\n", + "discrete_player = pn.widgets.DiscretePlayer(name='Discrete Player', options=[2, 4, 8, 16, 32, 64, 128],\n", + " value=8, loop_policy='loop', show_value=True, value_location='top_left')\n", "\n", "discrete_player" ] diff --git a/examples/reference/widgets/Player.ipynb b/examples/reference/widgets/Player.ipynb index 238b852a96..6245d20a9f 100644 --- a/examples/reference/widgets/Player.ipynb +++ b/examples/reference/widgets/Player.ipynb @@ -40,6 +40,8 @@ "* **``disabled``** (boolean): Whether the widget is editable\n", "* **``name``** (str): The title of the widget\n", "* **``show_loop_controls``** (boolean): Whether radio buttons allowing to switch between loop policies options are shown\n", + "* **``show_value``** (boolean): Whether to display the value of the player\n", + "* **``value_location``** (str): Where to display the value; must be one of 'top_left', 'top_center', 'top_right'\n", "\n", "___" ] @@ -57,7 +59,8 @@ "metadata": {}, "outputs": [], "source": [ - "player = pn.widgets.Player(name='Player', start=0, end=100, value=32, loop_policy='loop')\n", + "player = pn.widgets.Player(name='Player', start=0, end=100, value=32, loop_policy='loop',\n", + " show_value=True, value_location='top_center')\n", "\n", "player" ] diff --git a/panel/dist/css/player.css b/panel/dist/css/player.css new file mode 100644 index 0000000000..1b0bb61ad5 --- /dev/null +++ b/panel/dist/css/player.css @@ -0,0 +1,3 @@ +.pn-player-value { + font-weight: bold; +} diff --git a/panel/models/player.ts b/panel/models/player.ts index cd8b70eacc..f52d86b5e7 100644 --- a/panel/models/player.ts +++ b/panel/models/player.ts @@ -1,7 +1,8 @@ import { Enum } from "@bokehjs/core/kinds" import * as p from "@bokehjs/core/properties" -import { div } from "@bokehjs/core/dom" +import { div, empty, span} from "@bokehjs/core/dom" import { Widget, WidgetView } from "@bokehjs/models/widgets/widget" +import {to_string} from "@bokehjs/core/util/pretty" const SVG_STRINGS = { slower: ' this.update_title()) + this.connect(this.model.properties.value_location.change, () => this.set_value_location()) this.connect(this.model.properties.direction.change, () => this.set_direction()) this.connect(this.model.properties.value.change, () => this.render()) this.connect(this.model.properties.loop_policy.change, () => this.set_loop_state(this.model.loop_policy)) @@ -89,6 +93,8 @@ export class PlayerView extends WidgetView { else if (!this.model.show_loop_controls && this.loop_state.parentNode == this.groupEl) this.groupEl.removeChild(this.loop_state) }) + this.connect(this.model.properties.show_value.change, () => this.update_title()) + } toggle_disable() { @@ -124,7 +130,12 @@ export class PlayerView extends WidgetView { this.groupEl = div() this.groupEl.style.display = "flex" this.groupEl.style.flexDirection = "column" - this.groupEl.style.alignItems = "center" + + // Display Value + this.titleEl = document.createElement('label'); + this.update_title() + this.titleEl.style.cssText = "padding: 0 5px 0 5px; user-select:none;" + this.set_value_location() // Slider this.sliderEl = document.createElement('input') @@ -259,6 +270,7 @@ export class PlayerView extends WidgetView { this.loop_state.appendChild(reflect) this.loop_state.appendChild(reflect_label) + this.groupEl.appendChild(this.titleEl) this.groupEl.appendChild(this.sliderEl) this.groupEl.appendChild(button_div) if (this.model.show_loop_controls) @@ -270,6 +282,7 @@ export class PlayerView extends WidgetView { set_frame(frame: number, throttled: boolean = true): void { this.model.value = frame + this.update_title() if (throttled) this.model.value_throttled = frame if (this.sliderEl.value != String(frame)) @@ -286,6 +299,47 @@ export class PlayerView extends WidgetView { return "once" } + update_title(): void { + empty(this.titleEl) + + const hide_header = this.model.title == null || (this.model.title.length == 0 && !this.model.show_value) + this.titleEl.style.display = hide_header ? "none" : "" + + if (!hide_header) { + this.titleEl.style.visibility = 'visible'; + const {title} = this.model + if (title != null && title.length > 0) { + if (this.contains_tex_string(title)) { + this.titleEl.innerHTML = `${this.process_tex(title)}: ` + } else { + this.titleEl.textContent = `${title}: ` + } + } + + if (this.model.show_value) { + + this.titleEl.appendChild(span({class: 'pn-player-value'}, to_string(this.model.value))) + } + } + else{ + this.titleEl.style.visibility = 'hidden'; + } + } + + set_value_location(): void { + switch (this.model.value_location){ + case 'top_left': + this.titleEl.style.textAlign = "left"; + break; + case 'top_center': + this.titleEl.style.textAlign = "center"; + break; + case 'top_right': + this.titleEl.style.textAlign = "right"; + break; + } + } + set_loop_state(state: string): void { var button_group = this.loop_state.state; for (var i = 0; i < button_group.length; i++) { @@ -415,9 +469,12 @@ export namespace Player { end: p.Property step: p.Property loop_policy: p.Property + title: p.Property value: p.Property + value_location: p.Property value_throttled: p.Property show_loop_controls: p.Property + show_value: p.Property } } @@ -437,16 +494,19 @@ export class Player extends Widget { static { this.prototype.default_view = PlayerView - this.define(({ Boolean, Int }) => ({ + this.define(({ Boolean, Int, String }) => ({ direction: [Int, 0], interval: [Int, 500], start: [Int, 0], end: [Int, 10], step: [Int, 1], loop_policy: [LoopPolicy, "once"], + title: [String,""], value: [Int, 0], + value_location: [String, "top_center"], value_throttled: [Int, 0], show_loop_controls: [Boolean, true], + show_value: [Boolean, true] })) this.override({ width: 400 }) diff --git a/panel/models/widgets.py b/panel/models/widgets.py index ae58077dec..cf222f34ea 100644 --- a/panel/models/widgets.py +++ b/panel/models/widgets.py @@ -21,6 +21,9 @@ class Player(Widget): """ The Player widget provides controls to play through a number of frames. """ + title = Nullable(String, default="", help=""" + The slider's label (supports :ref:`math text `). + """) start = Int(0, help="Lower bound of the Player slider") @@ -30,6 +33,9 @@ class Player(Widget): value_throttled = Int(0, help="Current throttled value of the player app") + value_location = String("top_left", help="""Location to display + the value of the slider ("top_left" "top_center", "top_right")""") + step = Int(1, help="Number of steps to advance the player by.") interval = Int(500, help="Interval between updates") @@ -43,6 +49,9 @@ class Player(Widget): show_loop_controls = Bool(True, help="""Whether the loop controls radio buttons are shown""") + show_value = Bool(True, help=""" + Whether to show the widget value""") + width = Override(default=400) height = Override(default=250) diff --git a/panel/tests/ui/widgets/test_player.py b/panel/tests/ui/widgets/test_player.py new file mode 100644 index 0000000000..9137b1e2d8 --- /dev/null +++ b/panel/tests/ui/widgets/test_player.py @@ -0,0 +1,54 @@ +import pytest + +pytest.importorskip("playwright") + +from playwright.sync_api import expect + +from panel.tests.util import serve_component +from panel.widgets import Player + +pytestmark = pytest.mark.ui + + +def test_init(page): + player = Player() + serve_component(page, player) + + assert not page.is_visible('label') + assert page.query_selector('.pn-player-value') is None + +def test_show_value(page): + player = Player(show_value=True) + serve_component(page, player) + + assert page.is_visible('label') + assert page.query_selector('.pn-player-value') is not None + + +def test_name(page): + player = Player(name='test') + serve_component(page, player) + + assert page.is_visible('label') + assert page.query_selector('.pn-player-value') is None + + name = page.locator('label:has-text("test")') + expect(name).to_have_count(1) + + +def test_value_location(page): + player = Player(name='test', value_location='top_right') + serve_component(page, player) + + name = page.locator('label:has-text("test")') + expect(name).to_have_css("text-align", "right") + +def test_name_and_show_value(page): + player = Player(name='test', show_value=True) + serve_component(page, player) + + assert page.is_visible('label') + assert page.query_selector('.pn-player-value') is not None + + name = page.locator('label:has-text("test")') + expect(name).to_have_count(1) diff --git a/panel/widgets/player.py b/panel/widgets/player.py index f2049ee68b..c1a1d9524a 100644 --- a/panel/widgets/player.py +++ b/panel/widgets/player.py @@ -4,12 +4,13 @@ from __future__ import annotations from typing import ( - TYPE_CHECKING, ClassVar, Mapping, Type, + TYPE_CHECKING, ClassVar, List, Mapping, Type, ) import param from ..config import config +from ..io.resources import CDN_DIST from ..models.widgets import Player as _BkPlayer from ..util import indexOf, isIn from .base import Widget @@ -36,19 +37,29 @@ class PlayerBase(Widget): show_loop_controls = param.Boolean(default=True, doc=""" Whether the loop controls radio buttons are shown""") + show_value = param.Boolean(default=False, doc=""" + Whether to show the widget value""") + step = param.Integer(default=1, doc=""" Number of frames to step forward and back by on each event.""") height = param.Integer(default=80) + value_location = param.ObjectSelector( + objects=["top_left", "top_center", "top_right"], doc=""" + Location to display the value of the slider + ("top_left", "top_center", "top_right")""") + width = param.Integer(default=510, allow_None=True, doc=""" Width of this component. If sizing_mode is set to stretch or scale mode this will merely be used as a suggestion.""") - _rename: ClassVar[Mapping[str, str | None]] = {'name': None} + _rename: ClassVar[Mapping[str, str | None]] = {'name': 'title'} _widget_type: ClassVar[Type[Model]] = _BkPlayer + _stylesheets: ClassVar[List[str]] = [f"{CDN_DIST}css/player.css"] + __abstract = True def __init__(self, **params): @@ -78,7 +89,7 @@ class Player(PlayerBase): :Example: - >>> Player(name='Player', start=0, end=100, value=32, loop_policy='loop') + >>> Player(name='Player', start=0, end=100, value=32, loop_policy='loop', value_location='top_center') """ start = param.Integer(default=0, doc="Lower bound on the slider value") @@ -132,7 +143,8 @@ class DiscretePlayer(PlayerBase, SelectBase): >>> DiscretePlayer( ... name='Discrete Player', ... options=[2, 4, 8, 16, 32, 64, 128], value=32, - ... loop_policy='loop' + ... loop_policy='loop', + ... value_location='top_left' ... ) """ @@ -142,7 +154,7 @@ class DiscretePlayer(PlayerBase, SelectBase): value_throttled = param.Parameter(constant=True, doc="Current player value") - _rename: ClassVar[Mapping[str, str | None]] = {'name': None, 'options': None} + _rename: ClassVar[Mapping[str, str | None]] = {'name': 'title', 'options': None} _source_transforms: ClassVar[Mapping[str, str | None]] = {'value': None, 'value_throttled': None}