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 #7060

Merged
merged 14 commits into from
Aug 8, 2024
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_align``** (str): Where to display the value; must be one of 'start', 'center', 'end'\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_align='start')\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_align``** (str): Where to display the value; must be one of 'start', 'center', 'end'\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_align='start')\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;
}
46 changes: 46 additions & 0 deletions panel/models/discrete_player.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type * as p from "@bokehjs/core/properties"
import {PlayerView, Player} from "./player"
import {span} from "@bokehjs/core/dom"
import {to_string} from "@bokehjs/core/util/pretty"

export class DiscretePlayerView extends PlayerView {
declare model: DiscretePlayer

override append_value_to_title_el(): void {
let label = this.model.options[this.model.value]
if (typeof label !== "string") {
label = to_string(label)
}
this.titleEl.appendChild(span({class: "pn-player-value"}, label))
}
}

export namespace DiscretePlayer {
export type Attrs = p.AttrsOf<Props>
export type Props = Player.Props & {
options: p.Property<any>
}
}

export interface DiscretePlayer extends DiscretePlayer.Attrs { }

export class DiscretePlayer extends Player {

declare properties: DiscretePlayer.Props

constructor(attrs?: Partial<DiscretePlayer.Attrs>) {
super(attrs)
}

static override __module__ = "panel.models.widgets"

static {
this.prototype.default_view = DiscretePlayerView

this.define<DiscretePlayer.Props>(({List, Any}) => ({
options: [List(Any), []],
}))

this.override<DiscretePlayer.Props>({width: 400})
}
}
1 change: 1 addition & 0 deletions panel/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export {CustomMultiSelect} from "./multiselect"
export {DataTabulator} from "./tabulator"
export {DatetimePicker} from "./datetime_picker"
export {DeckGLPlot} from "./deckgl"
export {DiscretePlayer} from "./discrete_player"
export {ECharts} from "./echarts"
export {Feed} from "./feed"
export {FileDownload} from "./file_download"
Expand Down
78 changes: 74 additions & 4 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 type * 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 {
declare model: Player

protected buttonEl: HTMLDivElement
protected titleEl: HTMLDivElement
protected groupEl: HTMLDivElement
protected sliderEl: HTMLInputElement
protected loop_state: HTMLFormElement
Expand All @@ -80,7 +82,9 @@ export class PlayerView extends WidgetView {
override connect_signals(): void {
super.connect_signals()

const {direction, value, loop_policy, disabled, show_loop_controls} = this.model.properties
const {title, value_align, direction, value, loop_policy, disabled, show_loop_controls, show_value} = this.model.properties
this.on_change(title, () => this.update_title_and_value())
this.on_change(value_align, () => this.set_value_align())
this.on_change(direction, () => this.set_direction())
this.on_change(value, () => this.render())
this.on_change(loop_policy, () => this.set_loop_state(this.model.loop_policy))
Expand All @@ -92,6 +96,8 @@ export class PlayerView extends WidgetView {
this.groupEl.removeChild(this.loop_state)
}
})
this.on_change(show_value, () => this.update_title_and_value())

}

toggle_disable() {
Expand Down Expand Up @@ -127,7 +133,13 @@ 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 = div()
this.titleEl.classList.add("pn-player-title")
this.titleEl.style.padding = "0 5px 0 5px"
this.update_title_and_value()
this.set_value_align()

// Slider
this.sliderEl = document.createElement("input")
Expand Down Expand Up @@ -263,6 +275,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 @@ -275,6 +288,7 @@ export class PlayerView extends WidgetView {

set_frame(frame: number, throttled: boolean = true): void {
this.model.value = frame
this.update_title_and_value()
if (throttled) {
this.model.value_throttled = frame
}
Expand All @@ -294,6 +308,56 @@ export class PlayerView extends WidgetView {
return "once"
}

update_title_and_value(): 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)}`
if (this.model.show_value) {
this.titleEl.innerHTML += ": "
}
} else {
this.titleEl.textContent = `${title}`
if (this.model.show_value) {
this.titleEl.textContent += ": "
}
}
}

if (this.model.show_value) {
this.append_value_to_title_el()
}
} else {
this.titleEl.style.visibility = "hidden"
}
}

append_value_to_title_el(): void {
this.titleEl.appendChild(span({class: "pn-player-value"}, to_string(this.model.value)))
}

set_value_align(): void {
switch (this.model.value_align) {
case "start":
this.titleEl.style.textAlign = "left"
break
case "center":
this.titleEl.style.textAlign = "center"
break
case "end":
this.titleEl.style.textAlign = "right"
console.log(this.titleEl)
break
}
}

set_loop_state(state: string): void {
const button_group = this.loop_state.state
for (let i = 0; i < button_group.length; i++) {
Expand Down Expand Up @@ -429,9 +493,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_align: p.Property<string>
value_throttled: p.Property<any>
show_loop_controls: p.Property<boolean>
show_value: p.Property<boolean>
}
}

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

this.define<Player.Props>(({Bool, Int}) => ({
this.define<Player.Props>(({Bool, Int, Str}) => ({
direction: [Int, 0],
interval: [Int, 500],
start: [Int, 0],
end: [Int, 10],
step: [Int, 1],
loop_policy: [LoopPolicy, "once"],
title: [Str, ""],
value: [Int, 0],
value_align: [Str, "start"],
value_throttled: [Int, 0],
show_loop_controls: [Bool, true],
show_value: [Bool, true],
}))

this.override<Player.Props>({width: 400})
Expand Down
15 changes: 15 additions & 0 deletions panel/models/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,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 @@ -40,6 +43,9 @@ class Player(Widget):

value_throttled = Int(0, help="Current throttled value of the player app")

value_align = String("start", help="""Location to display
the value of the slider ("start" "center", "end")""")

step = Int(1, help="Number of steps to advance the player by.")

interval = Int(500, help="Interval between updates")
Expand All @@ -53,11 +59,20 @@ 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)


class DiscretePlayer(Player):

options = List(Any, help="""
List of discrete options.""")


class SingleSelect(InputWidget):
''' Single-select widget.

Expand Down
55 changes: 55 additions & 0 deletions panel/tests/ui/widgets/test_player.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import pytest

pytest.importorskip("playwright")

from playwright.sync_api import expect

from panel.tests.util import serve_component, wait_until
from panel.widgets import Player

pytestmark = pytest.mark.ui


def test_init(page):
player = Player()
serve_component(page, player)

assert not page.is_visible('pn-player-value')
assert page.query_selector('.pn-player-value') is None

def test_show_value(page):
player = Player(show_value=True)
serve_component(page, player)

wait_until(lambda: page.query_selector('.pn-player-value') is not None)
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('.pn-player-title:has-text("test")')
expect(name).to_have_count(1)


def test_value_align(page):
player = Player(name='test', value_align='end')
serve_component(page, player)

name = page.locator('.pn-player-title: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('.pn-player-title:has-text("test")')
expect(name).to_have_count(1)
Loading
Loading