copy TextStyle parameters into Text

This commit is contained in:
Aran-Fey
2025-02-19 18:54:47 +01:00
parent 0340a0cd21
commit f0b71215d0
6 changed files with 192 additions and 51 deletions

View File

@@ -1,5 +1,11 @@
import { TextStyle } from "../dataModels";
import { textStyleToCss } from "../cssUtils";
import {
Color,
SolidFill,
LinearGradientFill,
ImageFill,
TextStyle,
} from "../dataModels";
import { textfillToCss, textStyleToCss } from "../cssUtils";
import { ComponentBase, ComponentState } from "./componentBase";
export type TextState = ComponentState & {
@@ -9,6 +15,21 @@ export type TextState = ComponentState & {
style?: "heading1" | "heading2" | "heading3" | "text" | "dim" | TextStyle;
justify?: "left" | "right" | "center" | "justify";
overflow?: "nowrap" | "wrap" | "ellipsize";
font?: string | null;
fill?:
| Color
| SolidFill
| LinearGradientFill
| ImageFill
| null
| "not given";
font_size?: number | null;
italic?: boolean | null;
font_weight?: "normal" | "bold" | null;
underlined?: boolean | null;
strikethrough?: boolean | null;
all_caps?: boolean | null;
};
export class TextComponent extends ComponentBase {
@@ -32,7 +53,7 @@ export class TextComponent extends ComponentBase {
): void {
super.updateElement(deltaState, latentComponents);
// BEFORE WE DO ANYTHING ELSE, update the text style
// BEFORE WE DO ANYTHING ELSE, replace the inner HTML element
if (deltaState.style !== undefined) {
// Change the element to <h1>, <h2>, <h3> or <span> as necessary
let tagName: string = "SPAN";
@@ -61,14 +82,71 @@ export class TextComponent extends ComponentBase {
return;
}
}
}
// Now apply the style
Object.assign(this.inner.style, textStyleToCss(deltaState.style));
// Styling
if (
deltaState.style !== undefined ||
deltaState.font !== undefined ||
deltaState.fill !== undefined ||
deltaState.font_size !== undefined ||
deltaState.italic !== undefined ||
deltaState.font_weight !== undefined ||
deltaState.underlined !== undefined ||
deltaState.strikethrough !== undefined ||
deltaState.all_caps !== undefined
) {
let textStyleCss = textStyleToCss(
deltaState.style ?? this.state.style
);
if (deltaState.font) {
textStyleCss["font-family"] = deltaState.font;
}
if (
deltaState.fill !== undefined &&
deltaState.fill !== "not given"
) {
Object.assign(textStyleCss, textfillToCss(deltaState.fill));
}
if (deltaState.font_size) {
textStyleCss["font-size"] = `${deltaState.font_size}rem`;
}
if (deltaState.italic) {
textStyleCss["font-style"] = deltaState.italic
? "italic"
: "normal";
}
if (deltaState.font_weight) {
textStyleCss["font-weight"] = deltaState.font_weight;
}
let textDecorations: string[] = [];
if (deltaState.underlined) {
textDecorations.push("underline");
}
if (deltaState.strikethrough) {
textDecorations.push("line-through");
}
textStyleCss["text-decoration"] = textDecorations.join(" ");
if (deltaState.all_caps) {
textStyleCss["text-transform"] = deltaState.all_caps
? "uppercase"
: "none";
}
Object.assign(this.inner.style, textStyleCss);
}
// Text content
//
// Make sure not to allow any linebreaks if the text is not multiline.
if (deltaState.text !== undefined) {
this.inner.textContent = deltaState.text;
}

View File

@@ -1,4 +1,4 @@
import { Color, AnyFill, TextStyle } from "./dataModels";
import { Color, AnyFill, TextStyle, TextCompatibleFill } from "./dataModels";
export function colorToCssString(color: Color): string {
const [r, g, b, a] = color;
@@ -85,6 +85,57 @@ export function fillToCss(fill: AnyFill): {
};
}
export function textfillToCss(fill: TextCompatibleFill): {
color: string;
background: string;
backgroundClip: string;
textFillColor: string;
} {
// If no fill is provided, stick to the local text color. This allows
// the user to have their text automatically adapt to different
// themes/contexts.
if (fill === null) {
return {
color: "var(--rio-local-text-color)",
background: "var(--rio-local-text-background)",
backgroundClip: "var(--rio-local-text-background-clip)",
textFillColor: "var(--rio-local-text-fill-color)",
};
}
// Color?
if (Array.isArray(fill)) {
return {
color: colorToCssString(fill),
background: "none",
backgroundClip: "unset",
textFillColor: "unset",
};
}
// Solid fill, i.e. also a color
if (fill.type === "solid") {
return {
color: colorToCssString(fill.color),
background: "none",
backgroundClip: "unset",
textFillColor: "unset",
};
}
// Anything else
return {
color: "unset",
background: fillToCss(fill).background,
// TODO: The `backdrop-filter` in `cssProps` is ignored because it
// doesn't do what we want. (It isn't clipped to the text, it blurs
// everything behind the element.) This means FrostedGlassFill
// doesn't blur the background when used on text.
backgroundClip: "text",
textFillColor: "transparent",
};
}
export function textStyleToCss(
style: "heading1" | "heading2" | "heading3" | "text" | "dim" | TextStyle
): {
@@ -138,7 +189,7 @@ export function textStyleToCss(
// Others
fontFamily = globalPrefix + "font-name)";
fontSize = globalPrefix + "font-size)";
fontStyle = globalPrefix + "font-italic)";
fontStyle = globalPrefix + "font-style)";
textDecorations.push(globalPrefix + "text-decoration)");
textTransform = globalPrefix + "all-caps)";
}
@@ -166,41 +217,9 @@ export function textStyleToCss(
fontFamily = style.fontName;
}
// If no fill is provided, stick to the local text color. This allows
// the user to have their text automatically adapt to different
// themes/contexts.
if (style.fill === null) {
color = "var(--rio-local-text-color)";
background = "var(--rio-local-text-background)";
backgroundClip = "var(--rio-local-text-background-clip)";
textFillColor = "var(--rio-local-text-fill-color)";
}
// Color?
else if (Array.isArray(style.fill)) {
color = colorToCssString(style.fill);
background = "none";
backgroundClip = "unset";
textFillColor = "unset";
}
// Solid fill, i.e. also a color
else if (style.fill.type === "solid") {
color = colorToCssString(style.fill.color);
background = "none";
backgroundClip = "unset";
textFillColor = "unset";
}
// Anything else
else {
color = "unset";
const cssProps = fillToCss(style.fill);
background = cssProps.background;
// TODO: The `backdrop-filter` in `cssProps` is ignored because it
// doesn't do what we want. (It isn't clipped to the text, it blurs
// everything behind the element.) This means FrostedGlassFill
// doesn't blur the background when used on text.
backgroundClip = "text";
textFillColor = "transparent";
}
({ color, background, backgroundClip, textFillColor } = textfillToCss(
style.fill
));
}
return {

View File

@@ -54,9 +54,15 @@ export type AnyFill =
| ImageFill
| FrostedGlassFill;
export type TextCompatibleFill =
| Color
| SolidFill
| LinearGradientFill
| ImageFill
| null;
export type TextStyle = {
fontName: string | null;
fill: Color | SolidFill | LinearGradientFill | ImageFill | null;
fill: TextCompatibleFill;
fontSize: number;
italic: boolean;
fontWeight: "normal" | "bold";

View File

@@ -2155,7 +2155,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
font-family: var(--rio-global-heading1-font-name);
color: var(--rio-local-heading1-color);
font-size: var(--rio-global-heading1-font-size);
font-style: var(--rio-global-heading1-italic);
font-style: var(--rio-global-heading1-font-style);
font-weight: var(--rio-global-heading1-font-weight);
text-decoration: var(--rio-global-heading1-text-decoration);
text-transform: var(--rio-global-heading1-all-caps);
@@ -2173,7 +2173,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
font-family: var(--rio-global-heading2-font-name);
color: var(--rio-local-heading2-color);
font-size: var(--rio-global-heading2-font-size);
font-style: var(--rio-global-heading2-italic);
font-style: var(--rio-global-heading2-font-style);
font-weight: var(--rio-global-heading2-font-weight);
text-decoration: var(--rio-global-heading2-text-decoration);
text-transform: var(--rio-global-heading2-all-caps);
@@ -2193,7 +2193,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
font-family: var(--rio-global-heading3-font-name);
color: var(--rio-local-heading3-color);
font-size: var(--rio-global-heading3-font-size);
font-style: var(--rio-global-heading3-italic);
font-style: var(--rio-global-heading3-font-style);
font-weight: var(--rio-global-heading3-font-weight);
text-decoration: var(--rio-global-heading3-text-decoration);
text-transform: var(--rio-global-heading3-all-caps);
@@ -2214,7 +2214,7 @@ $rio-input-box-small-label-spacing-top: 0.5rem;
color: var(--rio-local-text-color);
font-size: var(--rio-global-text-font-size);
line-height: 1.35em; // Purposely uses em
font-style: var(--rio-global-text-italic);
font-style: var(--rio-global-text-font-style);
font-weight: var(--rio-global-text-font-weight);
text-decoration: var(--rio-global-text-text-decoration);
text-transform: var(--rio-global-text-all-caps);

View File

@@ -7,7 +7,7 @@ from uniserde import JsonDoc
import rio
from .. import deprecations
from .. import deprecations, text_style, utils
from .fundamental_component import FundamentalComponent
__all__ = [
@@ -48,6 +48,29 @@ class Text(FundamentalComponent):
Finally, if `"ellipsize"`, the text will be truncated when there isn't
enough space and an ellipsis (`...`) will be added.
`font`: The `Font` to use for the text. When set to `None`, the default font
for the current context (heading or regular text, etc) will be used.
`fill`: The fill (color, gradient, etc.) for the text. Overrides the `fill`
from the `style`.
`font_size`: The font size. Overrides the `font_size` from the `style`.
`italic`: Whether the text is *italic* or not. Overrides the `italic` from
the `style`.
`font_weight`: Whether the text is normal or **bold**. Overrides the
`font_weight` from the `style`.
`underlined`: Whether the text is underlined or not. Overrides the
`underlined` from the `style`.
`strikethrough`: Whether the text should have a line through it. Overrides
the `strikethrough` from the `style`.
`all_caps`: Whether the text is transformed to ALL CAPS or not. Overrides
the `all_caps` from the `style`.
## Examples
@@ -84,6 +107,15 @@ class Text(FundamentalComponent):
wrap: bool | t.Literal["ellipsize"] = False
overflow: t.Literal["nowrap", "wrap", "ellipsize"] = "nowrap"
font: rio.Font | None = None
fill: text_style._TextFill | None | utils.NotGiven = utils.NOT_GIVEN
font_size: float | None = None
italic: bool | None = None
font_weight: t.Literal["normal", "bold"] | None = None
underlined: bool | None = None
strikethrough: bool | None = None
all_caps: bool | None = None
def _custom_serialize_(self) -> JsonDoc:
# Serialization doesn't handle unions. Hence the custom serialization
# here
@@ -109,10 +141,16 @@ class Text(FundamentalComponent):
else:
overflow = self.overflow
if isinstance(self.fill, utils.NotGiven):
fill = "not given"
else:
fill = self.session._serialize_fill(self.fill)
# Build the result
return {
"style": style,
"overflow": overflow,
"fill": fill,
}
def __repr__(self) -> str:

View File

@@ -2601,7 +2601,7 @@ a.remove();
"inherit" if style.font is None else style.font._serialize(self)
)
result[f"{css_prefix}-font-size"] = f"{style.font_size}rem"
result[f"{css_prefix}-italic"] = (
result[f"{css_prefix}-font-style"] = (
"italic" if style.italic else "normal"
)
result[f"{css_prefix}-font-weight"] = style.font_weight