Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display value for player #6059

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
5 changes: 4 additions & 1 deletion examples/reference/widgets/DiscretePlayer.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
"___"
]
Expand All @@ -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"
]
Expand Down
5 changes: 4 additions & 1 deletion examples/reference/widgets/Player.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
"___"
]
Expand All @@ -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"
]
Expand Down
3 changes: 3 additions & 0 deletions panel/dist/css/player.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.pn-player-value {
font-weight: bold;
}
66 changes: 63 additions & 3 deletions panel/models/player.ts
Original file line number Diff line number Diff line change
@@ -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: '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-minus" width="24" \
Expand Down Expand Up @@ -68,6 +69,7 @@ export class PlayerView extends WidgetView {
model: Player

protected buttonEl: HTMLDivElement
protected titleEl: HTMLLabelElement
protected groupEl: HTMLDivElement
protected sliderEl: HTMLInputElement
protected loop_state: HTMLFormElement
Expand All @@ -79,6 +81,8 @@ export class PlayerView extends WidgetView {

connect_signals(): void {
super.connect_signals()
this.connect(this.model.properties.title.change, () => 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))
Expand All @@ -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() {
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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)
Expand All @@ -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))
Expand All @@ -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++) {
Expand Down Expand Up @@ -415,9 +469,12 @@ export namespace Player {
end: p.Property<number>
step: p.Property<number>
loop_policy: p.Property<typeof LoopPolicy["__type__"]>
title: p.Property<string>
value: p.Property<any>
value_location: p.Property <string>
value_throttled: p.Property<any>
show_loop_controls: p.Property<boolean>
show_value: p.Property<boolean>
}
}

Expand All @@ -437,16 +494,19 @@ export class Player extends Widget {
static {
this.prototype.default_view = PlayerView

this.define<Player.Props>(({ Boolean, Int }) => ({
this.define<Player.Props>(({ 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<Player.Props>({ width: 400 })
Expand Down
9 changes: 9 additions & 0 deletions panel/models/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ug_styling_mathtext>`).
""")

start = Int(0, help="Lower bound of the Player slider")

Expand All @@ -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")
Expand All @@ -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)
Expand Down
54 changes: 54 additions & 0 deletions panel/tests/ui/widgets/test_player.py
Original file line number Diff line number Diff line change
@@ -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)
22 changes: 17 additions & 5 deletions panel/widgets/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")""")

ahuang11 marked this conversation as resolved.
Show resolved Hide resolved
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):
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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'
... )
"""

Expand All @@ -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}

Expand Down