Skip to content

Commit

Permalink
Add support for CSS-based theming on the canvas (#13828)
Browse files Browse the repository at this point in the history
* Separate rendering from painting

* rendering is for constructing DOM nodes
* painting is for drawing to the canvas

* Make RendererView inherit from DOMComponentView

* Don't use append(), remove() and replaceWith()

* Move RendererGroup to a separate module

* Introduce StyledElement base class

* Attach renderers' elements to the canvas

* Use CSS variables in canvas visuals

* Add styling/visuals/css_variables.py

* Restore functionality of HTML label annotation

* Update bokehjs' unit tests

* Use stylesheet based styling in HTML labels

* Update cross tests baselines

* Correctly render the Toolbar in ToolbarPanel

* Add CSS support to Text and Hatch visuals

* Style text in css_variables example

* Migrate styling/visuals/css_variables.py to bokehjs

* Allow to specific rendering target for canvas renderers

* Reposition menu after toolbar resize if open

* Correctly update canvas renderers' elements

* Allow to recover from invalid gesture state

* Safeguard against disconnected elements

* Use render_to() to render toolbar's tool buttons

* Add integration tests for HTMLLabel

* Add docstrings to models/ui/ui_element.py

* Remove deprecated APIs from core/dom.ts

* Compute CSS prefix of visual properties once

* Robustify render() and after_render() logic

* Call r_after_render() after updating children

* Use computed_renderer_views to avoid race conditions

* Display duration in devtools' progress bar

* Robustify is_paused and hold_render logic

* Recompute toolbar buttons after layout

* Update integration baseline images

* Robustify ready state in TileRenderer

* Mark _was_build in after_render()

* Correctly render contents in Dialog

* Add a regression test for issue #13787

* Update *.blf baseline files

* Update bokeh's examples

* Add release notes
  • Loading branch information
mattpap committed May 3, 2024
1 parent 74a7d42 commit 9aaa141
Show file tree
Hide file tree
Showing 131 changed files with 1,325 additions and 759 deletions.
3 changes: 3 additions & 0 deletions bokehjs/src/less/renderer.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:host {
position: absolute;
}
24 changes: 2 additions & 22 deletions bokehjs/src/lib/core/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,26 +156,6 @@ export function nbsp(): Text {
return text("\u00a0")
}

export function append(element: Node, ...children: Node[]): void {
for (const child of children) {
element.appendChild(child)
}
}

export function remove(element: Node): void {
const parent = element.parentNode
if (parent != null) {
parent.removeChild(element)
}
}

export function replaceWith(element: Node, replacement: Node): void {
const parent = element.parentNode
if (parent != null) {
parent.replaceChild(replacement, element)
}
}

export function prepend(element: Node, ...nodes: Node[]): void {
const first = element.firstChild
for (const node of nodes) {
Expand Down Expand Up @@ -495,7 +475,7 @@ export class InlineStyleSheet extends StyleSheet {
}

remove(): void {
remove(this.el)
this.el.remove()
}
}

Expand All @@ -520,7 +500,7 @@ export class ImportedStyleSheet extends StyleSheet {
}

remove(): void {
remove(this.el)
this.el.remove()
}
}

Expand Down
37 changes: 27 additions & 10 deletions bokehjs/src/lib/core/dom_view.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {View} from "./view"
import type {StyleSheet, StyleSheetLike} from "./dom"
import {createElement, remove, empty, InlineStyleSheet, ClassList} from "./dom"
import {createElement, empty, InlineStyleSheet, ClassList} from "./dom"
import {isString} from "./util/types"
import {assert} from "./util/assert"
import base_css from "styles/base.css"

export interface DOMView extends View {
Expand All @@ -22,11 +23,11 @@ export abstract class DOMView extends View {

override initialize(): void {
super.initialize()
this.el = this._createElement()
this.el = this._create_element()
}

override remove(): void {
remove(this.el)
this.el.remove()
super.remove()
}

Expand All @@ -40,26 +41,42 @@ export abstract class DOMView extends View {

abstract render(): void

render_to(target: Node | null): void {
render_to(target: Node): void {
this.render()
target?.appendChild(this.el)
this.after_render()
target.appendChild(this.el)
}

after_render(): void {
this.reposition()
}

finish(): void {
this._has_finished = true
this.notify_finished()
r_after_render(): void {
for (const child_view of this.children()) {
if (child_view instanceof DOMView) {
child_view.r_after_render()
}
}
this.after_render()
this._was_built = true
}

protected _createElement(): this["el"] {
protected _create_element(): this["el"] {
return createElement(this.constructor.tag_name, {class: this.css_classes()})
}

reposition(_displayed?: boolean): void {}

protected _was_built: boolean = false

/**
* Build a top-level DOM view (e.g. during embedding).
*/
build(target: Node): void {
assert(this.is_root)
this.render_to(target)
this.r_after_render()
this.notify_finished()
}
}

export abstract class DOMElementView extends DOMView {
Expand Down
5 changes: 1 addition & 4 deletions bokehjs/src/lib/core/ui_events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,14 +202,11 @@ export class UIEventBus {
}

hit_test_renderers(plot_view: PlotView, sx: number, sy: number): RendererView | null {
const views = plot_view.get_renderer_views()

for (const view of reversed(views)) {
for (const view of reversed(plot_view.computed_renderer_views)) {
if (view.interactive_hit?.(sx, sy) ?? false) {
return view
}
}

return null
}

Expand Down
20 changes: 19 additions & 1 deletion bokehjs/src/lib/core/ui_gestures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,25 @@ export class UIGestures {
}

protected phase: GesturePhase = "idle"
protected pointers: Map<PointerId, Pointer> = new Map()
protected readonly pointers: Map<PointerId, Pointer> = new Map()
protected press_timer: number | null = null
protected tap_timestamp: number = -Infinity

protected last_scale: number | null = null
protected last_rotation: number | null = null

reset(): void {
this._cancel_timeout()

this.phase = "idle"
this.pointers.clear()
this.press_timer = null
this.tap_timestamp = -Infinity

this.last_scale = null
this.last_rotation = null
}

static readonly move_threshold: number = 5/*px*/
static readonly press_threshold: number = 300/*ms*/
static readonly doubletap_threshold: number = 300/*ms*/
Expand Down Expand Up @@ -219,6 +231,9 @@ export class UIGestures {
if (event.isPrimary && event.pointerType == "mouse" && event.buttons != MouseButton.Left) {
return
}
if (!this.hit_area.isConnected) {
return
}
this.pointers.set(event.pointerId, {init: event, last: event})
this.hit_area.setPointerCapture(event.pointerId)
switch (this.phase) {
Expand Down Expand Up @@ -254,6 +269,7 @@ export class UIGestures {
pointer.last = event
switch (this.phase) {
case "idle": {
this.reset()
unreachable()
}
case "started":
Expand Down Expand Up @@ -328,6 +344,7 @@ export class UIGestures {
this._cancel_timeout()
switch (this.phase) {
case "idle": {
this.reset()
unreachable()
}
case "started": {
Expand Down Expand Up @@ -391,6 +408,7 @@ export class UIGestures {
this._cancel_timeout()
switch (this.phase) {
case "idle": {
this.reset()
unreachable()
}
case "started":
Expand Down
23 changes: 10 additions & 13 deletions bokehjs/src/lib/core/util/menus.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {StyleSheetLike} from "../dom"
import {div, empty, remove, InlineStyleSheet, ClassList} from "../dom"
import {div, empty, InlineStyleSheet, ClassList} from "../dom"
import type {Orientation} from "../enums"
import {reversed} from "./array"
import {isString} from "./types"
Expand Down Expand Up @@ -106,7 +106,7 @@ export class ContextMenu { //extends DOMComponentView {

remove(): void {
this._unlisten()
remove(this.el)
this.el.remove()
}

protected _listen(): void {
Expand Down Expand Up @@ -211,24 +211,21 @@ export class ContextMenu { //extends DOMComponentView {
if (this.items.length == 0) {
return
}

if (!this._open) {
this.render()
if (this.shadow_el.children.length == 0) {
return
}
(this.target.shadowRoot ?? this.target).appendChild(this.el)
this._position(at ?? {left: 0, top: 0})
this._listen()
this._open = true
this.render()
if (this.shadow_el.children.length == 0) {
return
}
(this.target.shadowRoot ?? this.target).appendChild(this.el)
this._position(at ?? {left: 0, top: 0})
this._listen()
this._open = true
}

hide(): void {
if (this._open) {
this._open = false
this._unlisten()
remove(this.el)
this.el.remove()
}
}

Expand Down
6 changes: 3 additions & 3 deletions bokehjs/src/lib/core/util/panes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {StyleSheetLike, Keys} from "../dom"
import {div, empty, remove, InlineStyleSheet, ClassList} from "../dom"
import {div, empty, InlineStyleSheet, ClassList} from "../dom"
import type {Orientation} from "../enums"
import {isString} from "./types"

Expand Down Expand Up @@ -75,7 +75,7 @@ export class DropPane { //extends DOMComponentView {

remove(): void {
this._unlisten()
remove(this.el)
this.el.remove()
}

protected _listen(): void {
Expand Down Expand Up @@ -124,7 +124,7 @@ export class DropPane { //extends DOMComponentView {
if (this._open) {
this._open = false
this._unlisten()
remove(this.el)
this.el.remove()
}
}

Expand Down
61 changes: 36 additions & 25 deletions bokehjs/src/lib/core/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,6 @@ export class View implements ISignalable {
return this._ready
}

public *children(): IterViews {}

protected _has_finished: boolean

mark_finished(): void {
this._has_finished = true
}

/** @internal */
protected _slots = new WeakMap<Slot<any, any>, Slot<any, any>>()

Expand Down Expand Up @@ -90,9 +82,7 @@ export class View implements ISignalable {
}
}

initialize(): void {
this._has_finished = false
}
initialize(): void {}

async lazy_initialize(): Promise<void> {}

Expand All @@ -112,6 +102,41 @@ export class View implements ISignalable {
return `${this.model.type}View(${this.model.id})`
}

public *children(): IterViews {}

protected _has_finished: boolean = false

mark_finished(): void {
this._has_finished = true
}

/**
* Mark as finished even if e.g. external resources were not loaded yet.
*/
force_finished(): void {
this.mark_finished()
}

finish(): void {
this.mark_finished()
this.notify_finished()
}

private _idle_notified: boolean = false
notify_finished(): void {
if (!this.is_root) {
this.root.notify_finished()
} else {
if (!this._idle_notified && this.has_finished()) {
const {document} = this.model
if (document != null) {
this._idle_notified = true
document.notify_idle(this.model)
}
}
}
}

serializable_state(): SerializableState {
return {type: this.model.type}
}
Expand Down Expand Up @@ -175,20 +200,6 @@ export class View implements ISignalable {

on_hit?(sx: number, sy: number): boolean

private _idle_notified: boolean = false
notify_finished(): void {
if (!this.is_root) {
this.root.notify_finished()
} else {
if (!this._idle_notified && this.has_finished()) {
if (this.model.document != null) {
this._idle_notified = true
this.model.document.notify_idle(this.model)
}
}
}
}

resolve_frame(): View | null {
return null
}
Expand Down

0 comments on commit 9aaa141

Please sign in to comment.