8000 Add support for CSS-based theming on the canvas by mattpap · Pull Request #13828 · bokeh/bokeh · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
366d1c5
Separate rendering from painting
mattpap Mar 29, 2024
4b74d9a
Make RendererView inherit from DOMComponentView
mattpap Mar 29, 2024
717a6c3
Don't use append(), remove() and replaceWith()
mattpap Apr 11, 2024
18f3e08
Move RendererGroup to a separate module
mattpap Apr 13, 2024
8edd1a1
Introduce StyledElement base class
mattpap Apr 14, 2024
346ad21
Attach renderers' elements to the canvas
mattpap Apr 16, 2024
2783f4d
Use CSS variables in canvas visuals
mattpap Apr 16, 2024
ef6b1f0
Add styling/visuals/css_variables.py
mattpap Apr 16, 2024
ecb85a7
Restore functionality of HTML label annotation
mattpap Apr 17, 2024
084a5ec
Update bokehjs' unit tests
mattpap Apr 17, 2024
6f4193d
Use stylesheet based styling in HTML labels
mattpap Apr 18, 2024
0273ed1
Update cross tests baselines
mattpap Apr 18, 2024
85edc1b
Correctly render the Toolbar in ToolbarPanel
mattpap Apr 21, 2024
fc318fc
Add CSS support to Text and Hatch visuals
mattpap Apr 21, 2024
cad5ad0
Style text in css_variables example
mattpap Apr 21, 2024
9827484
Migrate styling/visuals/css_variables.py to bokehjs
mattpap Apr 21, 2024
75eee1b
Allow to specific rendering target for canvas renderers
mattpap Apr 22, 2024
80aac1f
Reposition menu after toolbar resize if open
mattpap Apr 22, 2024
3dabe74
Correctly update canvas renderers' elements
mattpap Apr 22, 2024
f2d9a79
Allow to recover from invalid gesture state
mattpap Apr 22, 2024
be4ad84
Safeguard against disconnected elements
mattpap Apr 22, 2024
67cba1f
Use render_to() to render toolbar's tool buttons
mattpap Apr 22, 2024
d4c9030
Add integration tests for HTMLLabel
mattpap Apr 22, 2024
431040a
Add docstrings to models/ui/ui_element.py
mattpap Apr 23, 2024
15d4f4a
Remove deprecated APIs from core/dom.ts
mattpap Apr 23, 2024
22bcd1d
Compute CSS prefix of visual properties once
mattpap Apr 23, 2024
0c4c892
Robustify render() and after_render() logic
mattpap Apr 26, 2024
8a28c2a
Call r_after_render() after updating children
mattpap May 1, 2024
980a149
Use computed_renderer_views to avoid race conditions
mattpap May 1, 2024
c9cd2dc
Display duration in devtools' progress bar
mattpap May 1, 2024
5b20522
Robustify is_paused and hold_render logic
mattpap May 1, 2024
2d8bfb0
Recompute toolbar buttons after layout
mattpap May 1, 2024
3bbb63e
Update integration baseline images
mattpap May 1, 2024
77a1459
Robustify ready state in TileRenderer
mattpap May 2, 2024
500f55f
Mark _was_build in after_render()
mattpap May 2, 2024
f22de60
Correctly render contents in Dialog
mattpap May 2, 2024
1e4544a
Add a regression test for issue #13787
mattpap May 2, 2024
11804cd
Update *.blf baseline files
mattpap May 2, 2024
cf8dc3e
Update bokeh's examples
mattpap May 2, 2024
55d9deb
Add release notes
mattpap May 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Introduce StyledElement base class
  • Loading branch information
mattpap committed May 1, 2024
commit 8edd1a1093d08da0e313768cfb8a8c9f3ce16782
3 changes: 2 additions & 1 deletion bokehjs/src/lib/models/dom/dom_element.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {DOMNode, DOMNodeView} from "./dom_node"
import {UIElement, StylesLike} from "../ui/ui_element"
import {StylesLike} from "../ui/styled_element"
import {UIElement} from "../ui/ui_element"
import type {ViewStorage, IterViews} from "core/build_views"
import {build_views, remove_views} from "core/build_views"
import {isString} from "core/util/types"
Expand Down
9 changes: 4 additions & 5 deletions bokehjs/src/lib/models/renderers/renderer.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type {ViewOf, View} from "core/view"
import {DOMComponentView} from "core/dom_view"
import {StyledElement, StyledElementView} from "../ui/styled_element"
import {build_view} from "core/build_views"
import * as visuals from "core/visuals"
import {RenderLevel} from "core/enums"
import type * as p from "core/properties"
import {isNumber} from "core/util/types"
import {Model} from "../../model"
import type {CanvasLayer} from "core/util/canvas"
import {assert} from "core/util/assert"
import type {Plot, PlotView} from "../plots/plot"
Expand All @@ -18,7 +17,7 @@ import {Menu} from "../ui/menus/menu"
import type {HTML} from "../dom/html"
import {RendererGroup} from "./renderer_group"

export abstract class RendererView extends DOMComponentView implements visuals.Paintable {
export abstract class RendererView extends StyledElementView implements visuals.Paintable {
declare model: Renderer
visuals: Renderer.Visuals

Expand Down Expand Up @@ -217,7 +216,7 @@ export abstract class RendererView extends DOMComponentView implements visuals.P
export namespace Renderer {
export type Attrs = p.AttrsOf<Props>

export type Props = Model.Props & {
export type Props = StyledElement.Props & {
group: p.Property<RendererGroup | null>
level: p.Property<RenderLevel>
visible: p.Property<boolean>
Expand All @@ -233,7 +232,7 @@ export namespace Renderer {

export interface Renderer extends Renderer.Attrs {}

export abstract class Renderer extends Model {
export abstract class Renderer extends StyledElement {
declare properties: Renderer.Props
declare __view_type__: RendererView

Expand Down
113 changes: 113 additions & 0 deletions bokehjs/src/lib/models/ui/styled_element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import {Model} from "../../model"
import {Node} from "../coordinates/node"
import {Styles} from "../dom/styles"
import {StyleSheet as BaseStyleSheet} from "../dom/stylesheets"
import {DOMComponentView} from "core/dom_view"
import type {StyleSheet} from "core/dom"
import {apply_styles} from "core/css"
import {InlineStyleSheet} from "core/dom"
import {entries} from "core/util/object"
import {isNumber} from "core/util/types"
import type * as p from "core/properties"
import {List, Or, Ref, Str, Dict, Nullable} from "core/kinds"

export const StylesLike = Or(Dict(Nullable(Str)), Ref(Styles)) // TODO: add validation for CSSStyles
export type StylesLike = typeof StylesLike["__type__"]

export const StyleSheets = List(Or(Ref(BaseStyleSheet), Str, Dict(StylesLike)))
export type StyleSheets = typeof StyleSheets["__type__"]

export const CSSVariables = Dict(Ref(Node))
export type CSSVariables = typeof CSSVariables["__type__"]

export abstract class StyledElementView extends DOMComponentView {
declare model: StyledElement

readonly style = new InlineStyleSheet()

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

const {styles, css_classes, css_variables, stylesheets} = this.model.properties
this.on_change(styles, () => this._update_styles())
this.on_change(css_classes, () => this._update_css_classes())
this.on_transitive_change(css_variables, () => this._update_css_variables())
this.on_change(stylesheets, () => this._update_stylesheets())
}

override render(): void {
super.render()
this._apply_styles()
}

protected override *_css_classes(): Iterable<string> {
yield* super._css_classes()
yield* this.model.css_classes
}

protected override *_css_variables(): Iterable<[string, string]> {
yield* super._css_variables()
for (const [name, node] of entries(this.model.css_variables)) {
const value = this.resolve_coordinate(node)
if (isNumber(value)) {
yield [name, `${value}px`]
}
}
}

protected override *_stylesheets(): Iterable<StyleSheet> {
yield* super._stylesheets()
yield this.style
yield* this._computed_stylesheets()
}

protected *_computed_stylesheets(): Iterable<StyleSheet> {
for (const stylesheet of this.model.stylesheets) {
if (stylesheet instanceof BaseStyleSheet) {
yield stylesheet.underlying()
} else {
yield new InlineStyleSheet(stylesheet)
}
}
}

protected _apply_styles(): void {
apply_styles(this.el.style, this.model.styles)
}

protected _update_styles(): void {
this.el.removeAttribute("style") // TODO: maintain _applied_styles
this._apply_styles()
}
}

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

export type Props = Model.Props & {
css_classes: p.Property<string[]>
css_variables: p.Property<CSSVariables>
styles: p.Property<StylesLike>
stylesheets: p.Property<StyleSheets>
}
}

export interface StyledElement extends StyledElement.Attrs {}

export abstract class StyledElement extends Model {
declare properties: StyledElement.Props
declare __view_type__: StyledElementView

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

static {
this.define<StyledElement.Props>(({List, Str}) => ({
css_classes: [ List(Str), [] ],
css_variables: [ CSSVariables, {} ],
styles: [ StylesLike, {} ],
stylesheets: [ StyleSheets, [] ],
}))
}
}
79 changes: 7 additions & 72 deletions bokehjs/src/lib/models/ui/ui_element.ts
< AA32 td id="diff-cbc189a201ed6689ae2b294b82183fde66548d149b9d94570f4a2510a6e1f9e1L218" data-line-number="218" class="blob-num blob-num-context js-linkable-line-number">
Original file line number Diff line number Diff line change
@@ -1,34 +1,19 @@
import {Model} from "../../model"
import {Node} from "../coordinates/node"
import {Styles} from "../dom/styles"
import {StyledElement, StyledElementView} from "./styled_element"
import type {Node} from "../coordinates/node"
import type {Menu} from "./menus/menu"
import {StyleSheet as BaseStyleSheet} from "../dom/stylesheets"
import type {Align} from "core/enums"
import type {SizingPolicy} from "core/layout"
import type {ViewOf} from "core/view"
import {DOMComponentView} from "core/dom_view"
import type {SerializableState} from "core/view"
import type {StyleSheet, StyleSheetLike} from "core/dom"
import {build_view} from "core/build_views"
import {apply_styles} from "core/css"
import {InlineStyleSheet} from "core/dom"
import {CanvasLayer} from "core/util/canvas"
import type {XY} from "core/util/bbox"
import {BBox} from "core/util/bbox"
import {entries} from "core/util/object"
import {isNumber} from "core/util/types"
import type * as p from "core/properties"
import ui_css from "styles/ui.css"
import {List, Or, Ref, Str, Dict, Nullable} from "core/kinds"

export const StylesLike = Or(Dict(Nullable(Str)), Ref(Styles)) // TODO: add validation for CSSStyles
export type StylesLike = typeof StylesLike["__type__"]

export const StyleSheets = List(Or(Ref(BaseStyleSheet), Str, Dict(StylesLike)))
export type StyleSheets = typeof StyleSheets["__type__"]

export const CSSVariables = Dict(Ref(Node))
export type CSSVariables = typeof CSSVariables["__type__"]

export type DOMBoxSizing = {
width_policy: SizingPolicy | "auto"
Expand All @@ -42,42 +27,14 @@ export type DOMBoxSizing = {

const {round, floor} = Math

export abstract class UIElementView extends DOMComponentView {
export abstract class UIElementView extends StyledElementView {
declare model: UIElement

protected readonly _display = new InlineStyleSheet()
readonly style = new InlineStyleSheet()

protected override *_css_classes(): Iterable<string> {
yield* super._css_classes()
yield* this.model.css_classes
}

protected override *_css_variables(): Iterable<[string, string]> {
yield* super._css_variables()
for (const [name, node] of entries(this.model.css_variables)) {
const value = this.resolve_coordinate(node)
if (isNumber(value)) {
yield [name, `${value}px`]
}
}
}

protected override *_stylesheets(): Iterable<StyleSheet> {
yield* super._stylesheets()
yield this.style
yield this._display
yield* this._computed_stylesheets()
}

protected *_computed_stylesheets(): Iterable<StyleSheet> {
for (const stylesheet of this.model.stylesheets) {
if (stylesheet instanceof BaseStyleSheet) {
yield stylesheet.underlying()
} else {
yield new InlineStyleSheet(stylesheet)
}
}
}

override stylesheets(): StyleSheetLike[] {
Expand Down Expand Up @@ -170,12 +127,8 @@ export abstract class UIElementView extends DOMComponentView {
override connect_signals(): void {
super.connect_signals()

const {visible, styles, css_classes, css_variables, stylesheets} = this.model.properties
const {visible} = this.model.properties
this.on_change(visible, () => this._update_visible())
this.on_change(styles, () => this._update_styles())
this.on_change(css_classes, () => this._update_css_classes())
this.on_transitive_change(css_variables, () => this._update_css_variables())
this.on_change(stylesheets, () => this._update_stylesheets())

this.el.addEventListener("contextmenu", (event) => this.show_context_menu(event))
}
Expand Down Expand Up @@ -216,7 +169,6 @@ export abstract class UIElementView extends DOMComponentView {

override render(): void {
super.render()
this._apply_styles()
this._apply_visible()
}

Expand Down Expand Up @@ -250,19 +202,10 @@ export abstract class UIElementView extends DOMComponentView {
}
}

protected _apply_styles(): void {
apply_styles(this.el.style, this.model.styles)
}

protected _update_visible(): void {
this._apply_visible()
}

protected _update_styles(): void {
this.el.removeAttribute("style") // TODO: maintain _applied_styles
this._apply_styles()
}

export(type: "auto" | "png" | "svg" = "auto", hidpi: boolean = true): CanvasLayer {
const output_backend = type == "auto" || type == "png" ? "canvas" : "svg"
const canvas = new CanvasLayer(output_backend, hidpi)
Expand Down Expand Up @@ -290,19 +233,15 @@ export abstract class UIElementView extends DOMComponentView {
export namespace UIElement {
export type Attrs = p.AttrsOf<Props>

export type Props = Model.Props & {
export type Props = StyledElement.Props & {
visible: p.Property<boolean>
css_classes: p.Property<string[]>
css_variables: p.Property<CSSVariables>
styles: p.Property<StylesLike>
stylesheets: p.Property<StyleSheets>
context_menu: p.Property<Menu | null>
}
}

export interface UIElement extends UIElement.Attrs {}

export abstract class UIElement extends Model {
export abstract class UIElement extends StyledElement {
declare properties: UIElement.Props
declare __view_type__: UIElementView

Expand All @@ -311,12 +250,8 @@ export abstract class UIElement extends Model {
}

static {
this.define<UIElement.Props>(({Bool, List, Str, AnyRef}) => ({
this.define<UIElement.Props>(({Bool, AnyRef, Nullable}) => ({
visible: [ Bool, true ],
css_classes: [ List(Str), [] ],
css_variables: [ CSSVariables, {} ],
styles: [ StylesLike, {} ],
stylesheets: [ StyleSheets, [] ],
context_menu: [ Nullable(AnyRef<Menu>()), null ],
}))
}
Expand Down
3 changes: 2 additions & 1 deletion src/bokeh/models/renderers/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
)
from ...model import Model
from ..coordinates import CoordinateMapping
from ..ui.ui_element import StyledElement

#-----------------------------------------------------------------------------
# Globals and constants
Expand Down Expand Up @@ -72,7 +73,7 @@ def __init__(self, *args, **kwargs) -> None:
#-----------------------------------------------------------------------------

@abstract
class Renderer(Model):
class Renderer(StyledElement):
"""An abstract base class for renderer types.

"""
Expand Down
24 changes: 17 additions & 7 deletions src/bokeh/models/ui/ui_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
#-----------------------------------------------------------------------------

__all__ = (
"StyledElement",
"UIElement",
)

Expand All @@ -52,19 +53,15 @@
# Dev API
#-----------------------------------------------------------------------------

@abstract
class UIElement(Model):
""" Base class for user interface elements.
class StyledElement(Model):
"""

"""

# explicit __init__ to support Init signatures< 618E /td>
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)

visible = Bool(default=True, help="""
Whether the component should be displayed on screen.
""")

css_classes = List(String, default=[], help="""
A list of additional CSS classes to add to the underlying DOM element.
""").accepts(Seq(String), lambda x: list(x))
Expand Down Expand Up @@ -100,6 +97,19 @@ def __init__(self, *args, **kwargs) -> None:
the root DOM element.
""")

@abstract
class UIElement(StyledElement):
""" Base class for user interface elements.
"""

# explicit __init__ to support Init signatures
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)

visible = Bool(default=True, help="""
Whether the component should be displayed on screen.
""")

context_menu = Nullable(Instance(".models.ui.Menu"), default=None, help="""
A menu to display when user right clicks on the component.

Expand Down
Loading
0