From f0b71215d0509f84fb6ac2d79b647daad7ea6efe Mon Sep 17 00:00:00 2001 From: Aran-Fey Date: Wed, 19 Feb 2025 18:54:47 +0100 Subject: [PATCH] copy TextStyle parameters into Text --- frontend/code/components/text.ts | 92 ++++++++++++++++++++++++++++--- frontend/code/cssUtils.ts | 93 +++++++++++++++++++------------- frontend/code/dataModels.ts | 8 ++- frontend/css/style.scss | 8 +-- rio/components/text.py | 40 +++++++++++++- rio/session.py | 2 +- 6 files changed, 192 insertions(+), 51 deletions(-) diff --git a/frontend/code/components/text.ts b/frontend/code/components/text.ts index 787a81b3..a07bd9d7 100644 --- a/frontend/code/components/text.ts +++ b/frontend/code/components/text.ts @@ -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

,

,

or 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; } diff --git a/frontend/code/cssUtils.ts b/frontend/code/cssUtils.ts index 9e786d6f..b2f7dabf 100644 --- a/frontend/code/cssUtils.ts +++ b/frontend/code/cssUtils.ts @@ -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 { diff --git a/frontend/code/dataModels.ts b/frontend/code/dataModels.ts index 5ae713dc..4e57f9ab 100644 --- a/frontend/code/dataModels.ts +++ b/frontend/code/dataModels.ts @@ -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"; diff --git a/frontend/css/style.scss b/frontend/css/style.scss index fc4517b8..b7ef9bc7 100644 --- a/frontend/css/style.scss +++ b/frontend/css/style.scss @@ -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); diff --git a/rio/components/text.py b/rio/components/text.py index bc2be7a2..7e84a6a6 100644 --- a/rio/components/text.py +++ b/rio/components/text.py @@ -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: diff --git a/rio/session.py b/rio/session.py index 056f45ae..e0844671 100644 --- a/rio/session.py +++ b/rio/session.py @@ -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