Skip to content

Commit

Permalink
Attempt to finalize implementation of HTMLText
Browse files Browse the repository at this point in the history
  • Loading branch information
mattpap committed May 15, 2024
1 parent f507a45 commit 7b8ca2c
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 139 deletions.
1 change: 1 addition & 0 deletions bokehjs/src/lib/core/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type ElementOurAttrs = {
}

type ElementCommonAttrs = {
id: Element["id"]
title: HTMLElement["title"]
tabIndex: HTMLOrSVGElement["tabIndex"]
}
Expand Down
4 changes: 4 additions & 0 deletions bokehjs/src/lib/models/common/kinds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from "core/kinds"
import type {HasProps} from "core/has_props"
import * as enums from "core/enums"
import * as p from "core/properties"

export const Length = NonNegative(Int)
export type Length = typeof Length["__type__"]
Expand Down Expand Up @@ -88,3 +89,6 @@ export type TrackSizingLike = typeof TrackSizingLike["__type__"]

export const TracksSizing = Or(TrackSizingLike, List(TrackSizingLike), Mapping(Int, TrackSizingLike))
export type TracksSizing = typeof TracksSizing["__type__"]

export class TextAnchorSpec extends p.DataSpec<TextAnchor> {}
export class OutlineShapeSpec extends p.DataSpec<enums.OutlineShapeName> {}
167 changes: 111 additions & 56 deletions bokehjs/src/lib/models/glyphs/html_text.ts
Original file line number Diff line number Diff line change
@@ -1,125 +1,180 @@
import {Text, TextView} from "./text"
import {px, div} from "core/dom"
import type * as p from "core/properties"
import {isString, non_null} from "core/util/types"
import {TextBase, TextBaseView} from "./text_base"
import {div, InlineStyleSheet} from "core/dom"
import {non_null} from "core/util/types"
import type {Context2d} from "core/util/canvas"
import type {XY, LRTB, Corners} from "core/util/bbox"
import type * as p from "core/properties"

export class HTMLTextView extends TextView {
export interface HTMLTextView extends HTMLText.Data {}

export class HTMLTextView extends TextBaseView {
declare model: HTMLText
declare visuals: HTMLText.Visuals

protected _elements: (HTMLElement | null)[] = []
protected readonly _style = new InlineStyleSheet()

protected override async _build_labels(): Promise<void> {
for (const el of this._elements) {
protected _id_for(i: number): string {
return `${this.model.id}-${i}`
}

protected _build_elements(text: p.Uniform<string | null>): (Element | null)[] {
for (const el of this.elements) {
el?.remove()
}

const {text} = this.base_glyph ?? this
this._elements = Array.from(text, (text_i, i) => {
const elements = Array.from(text, (text_i, i) => {
if (text_i == null) {
return null
} else {
return div({id: `${this.model.id}-${i}`}, text_i)
}
})

this.el.append(...this._elements.filter(non_null))
this.el.append(...elements.filter(non_null))
return elements
}

override _set_data(): void {
if (this.inherited_text) {
this._inherit_attr<HTMLText.Data>("elements")
} else {
this._define_attr<HTMLText.Data>("elements", this._build_elements(this.text))
}
}

override remove(): void {
for (const el of this._elements) {
for (const el of this.elements) {
el?.remove()
}
this._elements = []
super.remove()
}

protected _paint_label(ctx: Context2d, i: number, el: HTMLElement, text: string, sx: number, sy: number, angle: number): void {
// TODO append to frame element for clipping
el.textContent = text
protected _paint(ctx: Context2d, indices: number[], data?: Partial<HTMLText.Data>): void {
const {sx, sy, x_offset, y_offset, angle} = {...this, ...data}
//const {anchor_: anchor, border_radius, padding} = this
const {elements} = this

for (const i of indices) {
const sx_i = sx[i] + x_offset.get(i)
const sy_i = sy[i] + y_offset.get(i)
const angle_i = angle.get(i)
const el_i = elements[i]

if (!isFinite(sx_i + sy_i + angle_i) || el_i == null) {
continue
}

//const anchor_i = anchor.get(i)
this._paint_text(ctx, i, el_i, "" /*text_i*/, sx_i, sy_i, angle_i)
}
}

protected _paint_text(ctx: Context2d, i: number, el: Element, text: string, sx: number, sy: number, angle: number): void {
el.textContent = text
this.visuals.text.set_vectorize(ctx, i)
el.style.display = ""
el.style.position = "absolute"
el.style.left = `${sx}px`
el.style.top = `${sy}px`
if (isString(ctx.fillStyle)) {
el.style.color = ctx.fillStyle

const {padding, border_radius} = this
const id = this._id_for(i)

this._style.append(`
#${id} {
left: ${sx}px;
top: ${sy}px;
}
`)

this._style.append(`
#${id} {
color: ${ctx.fillStyle};
-webkit-text-stroke: 1px ${ctx.strokeStyle};
font: ${ctx.font};
white-space: pre;
padding-left: ${padding.left}px;
padding-right: ${padding.right}px;
padding-top: ${padding.top}px;
padding-bottom: ${padding.bottom}px;
border-top-left-radius: ${border_radius.top_left}px;
border-top-right-radius: ${border_radius.top_right}px;
border-bottom-right-radius: ${border_radius.bottom_right}px;
border-bottom-left-radius: ${border_radius.bottom_left}px;
}
el.style.webkitTextStroke = `1px ${ctx.strokeStyle}`
el.style.font = ctx.font
el.style.whiteSpace = "pre"
`)

const [x_anchor, x_t] = (() => {
switch (this.visuals.text.text_align.get(i)) {
case "left": return ["left", "0%"]
case "left": return ["left", "0%"]
case "center": return ["center", "-50%"]
case "right": return ["right", "-100%"]
case "right": return ["right", "-100%"]
}
})()
const [y_anchor, y_t] = (() => {
switch (this.visuals.text.text_baseline.get(i)) {
case "top": return ["top", "0%"]
case "top": return ["top", "0%"]
case "middle": return ["center", "-50%"]
case "bottom": return ["bottom", "-100%"]
default: return ["center", "-50%"] // "baseline"
default: return ["center", "-50%"] // "baseline"
}
})()

let transform = `translate(${x_t}, ${y_t})`
if (angle != 0) {
transform += `rotate(${angle}rad)`
transform += ` rotate(${angle}rad)`
}

el.style.transformOrigin = `${x_anchor} ${y_anchor}`
el.style.transform = transform
this._style.append(`
#${id} {
transform-origin: ${x_anchor} ${y_anchor};
transform: ${transform};
}
`)

if (this.visuals.background_fill.doit) {
if (this.visuals.background_fill.v_doit(i)) {
this.visuals.background_fill.set_vectorize(ctx, i)
if (isString(ctx.fillStyle)) {
el.style.backgroundColor = ctx.fillStyle
this._style.append(`
#${id} {
background-color: ${ctx.fillStyle};
}
`)
}

const {padding} = this
el.style.paddingLeft = px(padding.left)
el.style.paddingRight = px(padding.right)
el.style.paddingTop = px(padding.top)
el.style.paddingBottom = px(padding.bottom)

const {border_radius} = this
el.style.borderTopLeftRadius = px(border_radius.top_left)
el.style.borderTopRightRadius = px(border_radius.top_right)
el.style.borderBottomLeftRadius = px(border_radius.bottom_left)
el.style.borderBottomRightRadius = px(border_radius.bottom_right)

if (this.visuals.border_line.doit) {
if (this.visuals.border_line.v_doit(i)) {
this.visuals.border_line.set_vectorize(ctx, i)

// attempt to support vector-style ("8 4 8") line dashing for css mode
el.style.borderStyle = ctx.getLineDash().length < 2 ? "solid" : "dashed"
el.style.borderWidth = `${ctx.lineWidth}px`
if (isString(ctx.strokeStyle)) {
el.style.borderColor = ctx.strokeStyle
this._style.append(`
#${id} {
border-style: ${ctx.getLineDash().length < 2 ? "solid" : "dashed"};
border-width: ${ctx.lineWidth}px;
border-color: ${ctx.strokeStyle};
}
`)
}
}
}

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

export type Props = Text.Props & Mixins
export type Props = TextBase.Props & Mixins

export type Mixins = Text.Mixins
export type Mixins = TextBase.Mixins

export type Visuals = Text.Visuals
export type Visuals = TextBase.Visuals

export type Data = p.GlyphDataOf<Props> & {
readonly elements: (Element | null)[]

anchor_: p.Uniform<XY<number>> // can't resolve in v_materialize() due to dependency on other properties
padding: LRTB<number>
border_radius: Corners<number>
}
}

export interface HTMLText extends HTMLText.Attrs {}

export class HTMLText extends Text {
export class HTMLText extends TextBase {
declare properties: HTMLText.Props
declare __view_type__: HTMLTextView

Expand Down
60 changes: 9 additions & 51 deletions bokehjs/src/lib/models/glyphs/text.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import {XYGlyph, XYGlyphView} from "./xy_glyph"
import {TextBase, TextBaseView} from "./text_base"
import type {PointGeometry} from "core/geometry"
import * as mixins from "core/property_mixins"
import type * as visuals from "core/visuals"
import * as p from "core/properties"
import type * as p from "core/properties"
import {UniformScalar, UniformVector} from "core/uniforms"
import type {Context2d} from "core/util/canvas"
import {Selection} from "../selections/selection"
Expand All @@ -13,20 +11,16 @@ import type {Rect} from "core/util/affine"
import {rotate_around, AffineTransform} from "core/util/affine"
import type {GraphicsBox} from "core/graphics"
import {TextBox} from "core/graphics"
import type {TextAnchor} from "../common/kinds"
import {BorderRadius, Padding} from "../common/kinds"
import {OutlineShapeSpec} from "../common/kinds"
import * as resolve from "../common/resolve"
import {round_rect} from "../common/painting"
import type {VectorVisuals} from "./defs"
import {sqrt, PI} from "core/util/math"
import type {OutlineShapeName} from "core/enums"

class TextAnchorSpec extends p.DataSpec<TextAnchor> {}
class OutlineShapeSpec extends p.DataSpec<OutlineShapeName> {}

export interface TextView extends Text.Data {}

export class TextView extends XYGlyphView {
export class TextView extends TextBaseView {
declare model: Text
declare visuals: Text.Visuals

Expand Down Expand Up @@ -340,29 +334,13 @@ export class TextView extends XYGlyphView {
export namespace Text {
export type Attrs = p.AttrsOf<Props>

export type Props = XYGlyph.Props & {
text: p.NullStringSpec
angle: p.AngleSpec
x_offset: p.NumberSpec
y_offset: p.NumberSpec
anchor: TextAnchorSpec
padding: p.Property<Padding>
border_radius: p.Property<BorderRadius>
export type Props = TextBase.Props & {
outline_shape: OutlineShapeSpec
} & Mixins

export type Mixins =
mixins.TextVector &
mixins.BorderLineVector &
mixins.BackgroundFillVector &
mixins.BackgroundHatchVector

export type Visuals = XYGlyph.Visuals & {
text: visuals.TextVector
border_line: visuals.LineVector
background_fill: visuals.FillVector
background_hatch: visuals.HatchVector
}
export type Mixins = TextBase.Mixins

export type Visuals = TextBase.Visuals

export type Data = p.GlyphDataOf<Props> & {
readonly labels: (GraphicsBox | null)[]
Expand All @@ -378,7 +356,7 @@ export namespace Text {

export interface Text extends Text.Attrs {}

export class Text extends XYGlyph {
export class Text extends TextBase {
declare properties: Text.Props
declare __view_type__: TextView

Expand All @@ -389,28 +367,8 @@ export class Text extends XYGlyph {
static {
this.prototype.default_view = TextView

this.mixins<Text.Mixins>([
mixins.TextVector,
["border_", mixins.LineVector],
["background_", mixins.FillVector],
["background_", mixins.HatchVector],
])

this.define<Text.Props>(() => ({
text: [ p.NullStringSpec, {field: "text"} ],
angle: [ p.AngleSpec, 0 ],
x_offset: [ p.NumberSpec, 0 ],
y_offset: [ p.NumberSpec, 0 ],
anchor: [ TextAnchorSpec, {value: "auto"} ],
padding: [ Padding, 0 ],
border_radius: [ BorderRadius, 0 ],
outline_shape: [ OutlineShapeSpec, "box" ],
}))

this.override<Text.Props>({
border_line_color: null,
background_fill_color: null,
background_hatch_color: null,
})
}
}

0 comments on commit 7b8ca2c

Please sign in to comment.