Skip to content

Commit

Permalink
Display value for player (#7060)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahuang11 authored Aug 8, 2024
1 parent 86c7690 commit 811d794
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 11 deletions.
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

0 comments on commit 811d794

Please sign in to comment.