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