Skip to content

Commit

Permalink
Merge branch 'main' into add_message_css_class
Browse files Browse the repository at this point in the history
  • Loading branch information
ahuang11 committed Mar 4, 2024
2 parents b030c4b + 611d373 commit 2e51962
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 62 deletions.
17 changes: 17 additions & 0 deletions examples/reference/widgets/ButtonIcon.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,23 @@
"button"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You can also replace the `description` with `name` to have it shown on the side."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"button = pn.widgets.ButtonIcon(icon=\"heart\", size=\"4em\", name=\"favorite\")\n",
"button"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down
17 changes: 17 additions & 0 deletions examples/reference/widgets/ToggleIcon.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,23 @@
"toggle"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You can also replace the `description` with `name` to have it shown on the side."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"toggle = pn.widgets.ToggleIcon(icon=\"heart\", size=\"4em\", name=\"favorite\")\n",
"toggle"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down
10 changes: 10 additions & 0 deletions panel/dist/css/icon.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,13 @@
opacity: 1;
transform: scale(1.1);
}

.bk-IconLabel {
margin-left: 5px;
}

.bk-IconRow {
display: flex;
align-items: center;
justify-content: center;
}
3 changes: 3 additions & 0 deletions panel/models/icon.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ class _ClickableIcon(Widget):
value = Bool(default=False, help="""
Whether the icon is toggled on or off.""")

title = String(default="", help="""
The title of the icon.""")

tooltip = Nullable(Instance(Tooltip), help="""
A tooltip with plain text or rich HTML contents, providing general help or
description of a widget's or component's function.
Expand Down
109 changes: 61 additions & 48 deletions panel/models/icon.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import type {TooltipView} from "@bokehjs/models/ui/tooltip"
import {Tooltip} from "@bokehjs/models/ui/tooltip"
import type {TablerIconView} from "@bokehjs/models/ui/icons/tabler_icon"
import {TablerIcon} from "@bokehjs/models/ui/icons/tabler_icon"
import type {SVGIconView} from "@bokehjs/models/ui/icons/svg_icon"
import {SVGIcon} from "@bokehjs/models/ui/icons/svg_icon"
import {Control, ControlView} from "@bokehjs/models/widgets/control"
import type {IterViews} from "@bokehjs/core/build_views"
import type * as p from "@bokehjs/core/properties"
import {build_view} from "@bokehjs/core/build_views"
import {ButtonClick} from "@bokehjs/core/bokeh_events"
import type {EventCallback} from "@bokehjs/model"
import { Tooltip, TooltipView } from "@bokehjs/models/ui/tooltip"
import { TablerIcon, TablerIconView } from "@bokehjs/models/ui/icons/tabler_icon";
import { SVGIcon, SVGIconView } from "@bokehjs/models/ui/icons/svg_icon";
import { Control, ControlView } from '@bokehjs/models/widgets/control';
import type { IterViews } from '@bokehjs/core/build_views';
import * as p from "@bokehjs/core/properties";
import { div } from "@bokehjs/core/dom";
import { build_view } from '@bokehjs/core/build_views';
import { ButtonClick } from "@bokehjs/core/bokeh_events"
import type { EventCallback } from "@bokehjs/model"

export class ClickableIconView extends ControlView {
declare model: ClickableIcon

icon_view: TablerIconView | SVGIconView
declare model: ClickableIcon;
icon_view: TablerIconView | SVGIconView;
label_el: HTMLDivElement;
was_svg_icon: boolean
row_div: HTMLDivElement

protected tooltip: TooltipView | null

Expand All @@ -31,11 +30,12 @@ export class ClickableIconView extends ControlView {
await super.lazy_initialize()

this.was_svg_icon = this.is_svg_icon(this.model.icon)
this.icon_view = await this.build_icon_model(this.model.icon, this.was_svg_icon)
const {tooltip} = this.model
if (tooltip != null) {
this.tooltip = await build_view(tooltip, {parent: this})
}
this.label_el = div({ class: 'bk-IconLabel' }, this.model.title);
this.label_el.style.fontSize = this.calculate_size(0.6);
this.icon_view = await this.build_icon_model(this.model.icon, this.was_svg_icon);
const { tooltip } = this.model
if (tooltip != null)
this.tooltip = await build_view(tooltip, { parent: this })
}

override *children(): IterViews {
Expand All @@ -51,19 +51,24 @@ export class ClickableIconView extends ControlView {
}

override connect_signals(): void {
super.connect_signals()
const {icon, active_icon, disabled, value, size} = this.model.properties
this.on_change([active_icon, icon, value], () => this.update_icon())
this.on_change(disabled, () => this.update_cursor())
this.on_change(size, () => this.update_size())
super.connect_signals();
const { icon, active_icon, disabled, value, size } = this.model.properties;
this.on_change([active_icon, icon, value], () => this.update_icon());
this.on_change(this.model.properties.title, () => this.update_label());
this.on_change(disabled, () => this.update_cursor());
this.on_change(size, () => this.update_size());
}

override render(): void {
super.render()
this.icon_view.render()
this.update_icon()
this.update_label()
this.update_cursor()
this.shadow_el.appendChild(this.icon_view.el)
this.row_div = div({
class: 'bk-IconRow'
}, this.icon_view.el, this.label_el)
this.shadow_el.appendChild(this.row_div);

const toggle_tooltip = (visible: boolean) => {
this.tooltip?.model.setv({
Expand All @@ -80,12 +85,17 @@ export class ClickableIconView extends ControlView {
})
}

update_label(): void {
this.label_el.innerText = this.model.title;
}

update_cursor(): void {
this.icon_view.el.style.cursor = this.model.disabled ? "not-allowed" : "pointer"
}

update_size(): void {
this.icon_view.model.size = this.calculate_size()
this.icon_view.model.size = this.calculate_size();
this.label_el.style.fontSize = this.calculate_size(0.6);
}

async build_icon_model(icon: string, is_svg_icon: boolean): Promise<TablerIconView | SVGIconView> {
Expand All @@ -109,15 +119,17 @@ export class ClickableIconView extends ControlView {
if (this.was_svg_icon !== is_svg_icon) {
// If the icon type has changed, we need to rebuild the icon view
// and invalidate the old one.
const icon_view = await this.build_icon_model(icon, is_svg_icon)
icon_view.render()
this.icon_view.remove()
this.icon_view = icon_view
this.was_svg_icon = is_svg_icon
this.update_cursor()
this.shadow_el.appendChild(this.icon_view.el)
} else if (is_svg_icon) {
(this.icon_view as SVGIconView).model.svg = icon
const icon_view = await this.build_icon_model(icon, is_svg_icon);
icon_view.render();
this.icon_view.remove();
this.icon_view = icon_view;
this.was_svg_icon = is_svg_icon;
this.update_cursor();
// We need to re-append the new icon view to the DOM
this.row_div.insertBefore(this.icon_view.el, this.label_el);
}
else if (is_svg_icon) {
(this.icon_view as SVGIconView).model.svg = icon;
} else {
(this.icon_view as TablerIconView).model.icon_name = icon
}
Expand All @@ -128,14 +140,13 @@ export class ClickableIconView extends ControlView {
return this.model.active_icon !== "" ? this.model.active_icon : `${this.model.icon}-filled`
}

calculate_size(): string {
if (this.model.size !== null) {
return this.model.size
}
const maxWidth = this.model.width ?? 15
const maxHeight = this.model.height ?? 15
const size = Math.max(maxWidth, maxHeight)
return `${size}px`
calculate_size(factor: number = 1): string {
if (this.model.size !== null)
return `calc(${this.model.size} * ${factor})`;
const maxWidth = this.model.width ?? 15;
const maxHeight = this.model.height ?? 15;
const size = Math.max(maxWidth, maxHeight) * factor;
return `${size}px`;
}

click(): void {
Expand All @@ -146,10 +157,11 @@ export class ClickableIconView extends ControlView {
export namespace ClickableIcon {
export type Attrs = p.AttrsOf<Props>
export type Props = Control.Props & {
active_icon: p.Property<string>
icon: p.Property<string>
size: p.Property<string | null>
value: p.Property<boolean>
active_icon: p.Property<string>;
icon: p.Property<string>;
size: p.Property<string | null>;
value: p.Property<boolean>;
title: p.Property<string>;
tooltip: p.Property<Tooltip | null>
tooltip_delay: p.Property<number>
}
Expand All @@ -174,6 +186,7 @@ export class ClickableIcon extends Control {
icon: [Str, "heart"],
size: [Nullable(Str), null],
value: [Bool, false],
title: [Str, ""],
tooltip: [Nullable(Ref(Tooltip)), null],
tooltip_delay: [Float, 500],
}))
Expand Down
36 changes: 31 additions & 5 deletions panel/param.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,16 @@

import param

try:
from param import Skip
except Exception:
class Skip(Exception):
"""
Exception that allows skipping an update for function-level updates.
"""
from param.parameterized import (
classlist, discard_events, eval_function_with_deps, get_method_owner,
iscoroutinefunction, resolve_ref, resolve_value,
Undefined, classlist, discard_events, eval_function_with_deps,
get_method_owner, iscoroutinefunction, resolve_ref, resolve_value,
)
from param.reactive import rx

Expand Down Expand Up @@ -829,9 +836,20 @@ async def _eval_async(self, awaitable):
self._inner_layout.append(new_obj)
self._pane = self._inner_layout[-1]
else:
self._update_inner(new_obj)
try:
self._update_inner(new_obj)
except Skip:
pass
else:
self._update_inner(await awaitable)
try:
new = await awaitable
if new is Skip or new is Undefined:
raise Skip
self._update_inner(new)
except Skip:
self.param.log(
param.DEBUG, 'Skip event was raised, skipping update.'
)
except Exception as e:
if not curdoc or (has_context and curdoc.session_context):
raise e
Expand All @@ -850,7 +868,15 @@ def _replace_pane(self, *args, force=False):
if self.object is None:
new_object = Spacer()
else:
new_object = self.eval(self.object)
try:
new_object = self.eval(self.object)
if new_object is Skip and new_object is Undefined:
raise Skip
except Skip:
self.param.log(
param.DEBUG, 'Skip event was raised, skipping update.'
)
return
if inspect.isawaitable(new_object) or isinstance(new_object, types.AsyncGeneratorType):
param.parameterized.async_executor(partial(self._eval_async, new_object))
return
Expand Down
59 changes: 52 additions & 7 deletions panel/tests/test_param.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
import pytest

from bokeh.models import (
AutocompleteInput as BkAutocompleteInput, Button, Checkbox as BkCheckbox,
Column as BkColumn, Div, MultiSelect, RangeSlider as BkRangeSlider,
Row as BkRow, Select, Slider, Tabs as BkTabs, TextInput,
TextInput as BkTextInput, Toggle,
AutocompleteInput as BkAutocompleteInput, Button as BkButton,
Checkbox as BkCheckbox, Column as BkColumn, Div, MultiSelect,
RangeSlider as BkRangeSlider, Row as BkRow, Select, Slider, Tabs as BkTabs,
TextInput, TextInput as BkTextInput, Toggle,
)
from packaging.version import Version

Expand All @@ -22,11 +22,11 @@
HTML, Bokeh, Markdown, Matplotlib, PaneBase, Str, panel,
)
from panel.param import (
JSONInit, Param, ParamFunction, ParamMethod,
JSONInit, Param, ParamFunction, ParamMethod, Skip,
)
from panel.tests.util import mpl_available, mpl_figure
from panel.widgets import (
AutocompleteInput, Checkbox, DatePicker, DatetimeInput,
AutocompleteInput, Button, Checkbox, DatePicker, DatetimeInput,
EditableFloatSlider, EditableRangeSlider, LiteralInput, NumberInput,
RangeSlider,
)
Expand Down Expand Up @@ -392,7 +392,7 @@ class Test(param.Parameterized):
model = test_pane.get_root(document, comm=comm)

button = model.children[1]
assert isinstance(button, Button)
assert isinstance(button, BkButton)

# Check that the action is actually executed
pn_button = test_pane.layout[1]
Expand Down Expand Up @@ -1920,3 +1920,48 @@ async def function(value):
assert root.children[0].text == '&lt;p&gt;5&lt;/p&gt;\n'
await asyncio.sleep(0.1)
assert root.children[0].text == '&lt;p&gt;6&lt;/p&gt;\n'


def test_skip_param(document, comm):
checkbox = Checkbox(value=False)
button = Button()

def layout(value, click):
if not click:
raise Skip()
return Markdown(f"{value}")

layout = ParamFunction(bind(layout, checkbox, button))

root = layout.get_root(document, comm)

div = root.children[0]
assert div.text == '&lt;pre&gt; &lt;/pre&gt;'
checkbox.value = True
assert div.text == '&lt;pre&gt; &lt;/pre&gt;'
button.param.trigger('value')
assert div.text == '&lt;pre&gt; &lt;/pre&gt;'

@pytest.mark.asyncio
async def test_async_skip_param(document, comm):
checkbox = Checkbox(value=False)
button = Button()

async def layout(value, click):
if not click:
raise Skip()
return Markdown(f"{value}")

layout = ParamFunction(bind(layout, checkbox, button))

root = layout.get_root(document, comm)

div = root.children[0]
await asyncio.sleep(0.01)
assert div.text == '&lt;pre&gt; &lt;/pre&gt;'
checkbox.value = True
await asyncio.sleep(0.01)
assert div.text == '&lt;pre&gt; &lt;/pre&gt;'
button.param.trigger('value')
await asyncio.sleep(0.01)
assert div.text == '&lt;pre&gt; &lt;/pre&gt;'
Loading

0 comments on commit 2e51962

Please sign in to comment.