Skip to content

Commit

Permalink
Add support for progress widgets
Browse files Browse the repository at this point in the history
Adds determinate and indeterminate variants.
  • Loading branch information
mattpap committed Nov 23, 2023
1 parent 2555a4f commit e0a26fd
Show file tree
Hide file tree
Showing 17 changed files with 481 additions and 22 deletions.
82 changes: 82 additions & 0 deletions bokehjs/src/less/widgets/progress.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
.bk-bar {
position: relative;
background-color: #c2d5f7;
border: 1px solid #cccccc;
border-radius: 4px;
overflow: hidden;
}

.bk-value {
position: relative;
background-color: #3b80f0;
}

:host(:not(.bk-vertical)) {
.bk-bar {
width: 100%;
height: max-content;
min-width: 3em;
}

.bk-value {
left: 0;
right: unset;
width: var(--progress);
min-height: 0.5em;
}
}

:host(.bk-vertical) {
.bk-bar {
width: max-content;
height: 100%;
min-height: 3em;
}

.bk-value {
top: 0;
bottom: unset;
height: var(--progress);
min-width: 0.5em;
}
}

:host(:not(.bk-vertical).bk-reversed) {
.bk-value {
left: unset;
right: 0;
}
}

:host(.bk-vertical.bk-reversed) {
.bk-value {
top: unset;
bottom: 0;
}
}

:host(.bk-indeterminate) {
--indeterminate-width: 20%;

.bk-label {
visibility: hidden;
}

.bk-value {
width: var(--indeterminate-width);
animation-duration: 1.5s;
animation-direction: alternate;
animation-iteration-count: infinite;
animation-timing-function: linear;
animation-name: bk-progress-animation;
}
}

@keyframes bk-progress-animation {
from {
left: calc(-1 * var(--indeterminate-width));
}
to {
left: 100%;
}
}
12 changes: 6 additions & 6 deletions bokehjs/src/less/widgets/switch.less
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
cursor: pointer;
}

:host(.disabled) {
:host(.bk-disabled) {
cursor: default;
}

Expand All @@ -11,12 +11,12 @@
--bar-height: 10px;
}

.body {
.bk-body {
width: 100%;
height: var(--switch-size);
}

.bar {
.bk-bar {
position: relative;
top: calc(50% - var(--bar-height)/2);
height: var(--bar-height);
Expand All @@ -25,7 +25,7 @@
transition-property: background-color;
}

.knob {
.bk-knob {
position: absolute;
top: 0;
left: 0;
Expand All @@ -36,11 +36,11 @@
transition-property: left, background-color;
}

:host(.active) .bar {
:host(.bk-active) .bk-bar {
background-color: #c2d5f7;
}

:host(.active) .knob {
:host(.bk-active) .bk-knob {
left: calc(100% - var(--switch-size));
background-color: #3b80f0;
}
11 changes: 11 additions & 0 deletions bokehjs/src/lib/core/kinds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,16 @@ export namespace Kinds {
}
}

export class FiniteNumber extends Number {
override valid(value: unknown): value is number {
return super.valid(value) && isFinite(value)
}

override toString(): string {
return "FiniteNumber"
}
}

export class Int extends Number {
override valid(value: unknown): value is number {
return super.valid(value) && tp.isInteger(value)
Expand Down Expand Up @@ -588,6 +598,7 @@ export const Any = new Kinds.Any()
export const Unknown = new Kinds.Unknown()
export const Boolean = new Kinds.Boolean()
export const Number = new Kinds.Number()
export const FiniteNumber = new Kinds.FiniteNumber()
export const Int = new Kinds.Int()
export const Bytes = new Kinds.Bytes()
export const String = new Kinds.String()
Expand Down
4 changes: 2 additions & 2 deletions bokehjs/src/lib/core/util/templating.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export function replace_placeholders(content: string | {html: string}, data_sour
}
}

export function process_placeholders(text: string, fn: (type: "@" | "$", spec: string, format?: string) => string): string {
export function process_placeholders(text: string, fn: (type: "@" | "$", spec: string, format?: string) => string | null): string {
//
// (?:\$\w+) - special vars: $x
// (?:@\w+) - simple names: @foo
Expand All @@ -181,6 +181,6 @@ export function process_placeholders(text: string, fn: (type: "@" | "$", spec: s
return text.replace(regex, (_match, spec: string, format?: string) => {
const type = spec[0] as "@" | "$"
const name = spec.substring(1).replace(/[{}]/g, "").trim()
return fn(type, name, format)
return fn(type, name, format) ?? "???"
})
}
1 change: 1 addition & 0 deletions bokehjs/src/lib/models/widgets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export {PaletteSelect} from "./palette_select"
export {Paragraph} from "./paragraph"
export {PasswordInput} from "./password_input"
export {PreText} from "./pretext"
export {Progress} from "./progress"
export {RadioButtonGroup} from "./radio_button_group"
export {RadioGroup} from "./radio_group"
export {Select} from "./select"
Expand Down
147 changes: 147 additions & 0 deletions bokehjs/src/lib/models/widgets/progress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import {Widget, WidgetView} from "./widget"
import {HTML} from "../dom/html"
import type {StyleSheetLike} from "core/dom"
import {div} from "core/dom"
import type * as p from "core/properties"
import {Align, Location, Orientation} from "core/enums"
import {Enum, Or} from "../../core/kinds"
import {isString} from "core/util/types"
import {clamp} from "core/util/math"
import {process_placeholders} from "core/util/templating"
import * as progress_css from "styles/widgets/progress.css"

const LabelLocation = Or(Enum("none", "inline"), Location)
type LabelLocation = typeof LabelLocation["__type__"]

export class ProgressView extends WidgetView {
declare model: Progress

protected label_el: HTMLElement
protected value_el: HTMLElement
protected bar_el: HTMLElement

override connect_signals(): void {
super.connect_signals()
const {value, min, max, reversed, orientation} = this.model.properties
this.on_change([value, min, max], () => this._update_value())
this.on_change(reversed, () => this._update_reversed())
this.on_change(orientation, () => this._update_orientation())
}

override stylesheets(): StyleSheetLike[] {
return [...super.stylesheets(), progress_css.default]
}

override render(): void {
super.render()

this.label_el = div({class: progress_css.label})
this.value_el = div({class: progress_css.value})
this.bar_el = div({class: progress_css.bar}, this.value_el)

this._update_value()
this._update_reversed()
this._update_orientation()

this.shadow_el.append(this.bar_el)

const {label_location} = this.model
switch (label_location) {
case "inline": {
this.bar_el.append(this.label_el)
break
}
default:
}
}

protected _update_value(): void {
const {value, min, max, label, disabled} = this.model

const indeterminate = value == null
const progress = (clamp(value ?? 0, min, max) - min)/(max - min)*100

this.class_list.toggle(progress_css.indeterminate, indeterminate && !disabled)
this.value_el.style.setProperty("--progress", !indeterminate ? `${progress}%` : "unset")

if (isString(label)) {
const text = process_placeholders(label, (_, name) => {
if (name == "progress") {
return `${progress}`
} else {
return null
}
})
this.label_el.textContent = text
}
}

protected _update_reversed(): void {
this.class_list.toggle(progress_css.reversed, this.model.reversed)
}

protected _update_orientation(): void {
this.class_list.toggle(progress_css.vertical, this.model.orientation == "vertical")
}
}

export namespace Progress {
export type Attrs = p.AttrsOf<Props>

export type Props = Widget.Props & {
value: p.Property<number | null>
min: p.Property<number>
max: p.Property<number>
reversed: p.Property<boolean>
orientation: p.Property<Orientation>
label: p.Property<HTML | string>
label_location: p.Property<LabelLocation>
label_align: p.Property<Align>
}
}

export interface Progress extends Progress.Attrs {}

export class Progress extends Widget {
declare properties: Progress.Props
declare __view_type__: ProgressView

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

static {
this.prototype.default_view = ProgressView

this.define<Progress.Props>(({Boolean, Int, String, Ref, Or, Nullable}) => ({
value: [ Nullable(Int), 0 ],
min: [ Int, 0 ],
max: [ Int, 100 ],
reversed: [ Boolean, false ],
orientation: [ Orientation, "horizontal" ],
label: [ Or(Ref(HTML), String), "@{progress}%" ],
label_location: [ LabelLocation, "none" ],
label_align: [ Align, "center" ],
}))
}

get finished(): boolean {
return this.value == this.max
}

update(n: number): void {
const {value} = this
if (value != null) {
const {min, max} = this
this.value = clamp(value + n, min, max)
}
}

increment(n: number = 1): void {
this.update(n)
}

decrement(n: number = 1): void {
this.update(-n)
}
}
19 changes: 10 additions & 9 deletions bokehjs/src/lib/models/widgets/switch.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {ToggleInput, ToggleInputView} from "./toggle_input"
import type {StyleSheetLike} from "core/dom"
import type {StyleSheetLike, Keys} from "core/dom"
import {div} from "core/dom"
import type * as p from "core/properties"
import switch_css from "styles/widgets/switch.css"
import * as switch_css from "styles/widgets/switch.css"

export class SwitchView extends ToggleInputView {
declare model: Switch
Expand All @@ -11,41 +11,42 @@ export class SwitchView extends ToggleInputView {
protected bar_el: HTMLElement

override stylesheets(): StyleSheetLike[] {
return [...super.stylesheets(), switch_css]
return [...super.stylesheets(), switch_css.default]
}

override connect_signals(): void {
super.connect_signals()

this.el.addEventListener("keydown", (event) => {
switch (event.key) {
switch (event.key as Keys) {
case "Enter":
case " ": {
event.preventDefault()
this._toggle_active()
break
}
default:
}
})
this.el.addEventListener("click", () => this._toggle_active())
}

override render(): void {
super.render()
this.bar_el = div({class: "bar"})
this.knob_el = div({class: "knob", tabIndex: 0})
const body_el = div({class: "body"}, this.bar_el, this.knob_el)
this.bar_el = div({class: switch_css.bar})
this.knob_el = div({class: switch_css.knob, tabIndex: 0})
const body_el = div({class: switch_css.body}, this.bar_el, this.knob_el)
this._update_active()
this._update_disabled()
this.shadow_el.appendChild(body_el)
}

protected _update_active(): void {
this.el.classList.toggle("active", this.model.active)
this.el.classList.toggle(switch_css.active, this.model.active)
}

protected _update_disabled(): void {
this.el.classList.toggle("disabled", this.model.disabled)
this.el.classList.toggle(switch_css.disabled, this.model.disabled)
}
}

Expand Down

0 comments on commit e0a26fd

Please sign in to comment.