TextStyles no longer have default values, omitted values are left unchanged instead

This commit is contained in:
Aran-Fey
2025-04-13 16:42:45 +02:00
parent 2e0d30d7e0
commit 0f4a3a4cd0
21 changed files with 381 additions and 217 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { textStyleToCss } from "../cssUtils";
import { applyTextStyleCss, textStyleToCss } from "../cssUtils";
export type HeadingListItemState = ComponentState & {
_type_: "HeadingListItem-builtin";
@@ -15,7 +15,7 @@ export class HeadingListItemComponent extends ComponentBase<HeadingListItemState
// Apply a style. This could be done with CSS, instead of doing it
// individually for each component, but these are rare and this preempts
// duplicate code.
Object.assign(element.style, textStyleToCss("heading3"));
applyTextStyleCss(element, textStyleToCss("heading3"));
return element;
}
+25 -12
View File
@@ -9,22 +9,22 @@ import {
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { applyIcon, applyFillToSVG } from "../designApplication";
type IconCompatibleFill =
| SolidFill
| LinearGradientFill
| RadialGradientFill
| ImageFill
| Color
| ColorSet
| "dim";
export type IconState = ComponentState & {
_type_: "Icon-builtin";
icon: string;
fill:
| SolidFill
| LinearGradientFill
| RadialGradientFill
| ImageFill
| Color
| ColorSet
| "dim";
fill: IconCompatibleFill;
};
export class IconComponent extends ComponentBase<IconState> {
private svgElement: SVGSVGElement;
createElement(): HTMLElement {
let element = document.createElement("div");
element.classList.add("rio-icon");
@@ -44,12 +44,25 @@ export class IconComponent extends ComponentBase<IconState> {
// fill.
let fill = deltaState.fill ?? this.state.fill;
applyIcon(this.element, deltaState.icon, fill);
applyIcon(this.element, deltaState.icon, fill).then(() => {
// The fill may have changed while the icon was loading, so
// we'll re-apply it
this.applyFillIfSvgElementExists(this.state.fill);
});
return;
}
if (deltaState.fill !== undefined) {
applyFillToSVG(this.svgElement, deltaState.fill);
this.applyFillIfSvgElementExists(deltaState.fill);
}
}
private applyFillIfSvgElementExists(fill: IconCompatibleFill): void {
let svgRoot = this.element.querySelector("svg");
if (svgRoot === null) {
return;
}
applyFillToSVG(svgRoot, fill);
}
}
+4 -5
View File
@@ -1,7 +1,6 @@
import { textStyleToCss } from "../cssUtils";
import { applyTextStyleCss, textStyleToCss } from "../cssUtils";
import { applyIcon } from "../designApplication";
import { ComponentId, TextStyle } from "../dataModels";
import { commitCss } from "../utils";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
import { RippleEffect } from "../rippleEffect";
import {
@@ -120,8 +119,8 @@ export class RevealerComponent extends ComponentBase<RevealerState> {
// Update the text style
if (deltaState.header_style !== undefined) {
// The text is handled by a helper function
Object.assign(
this.labelElement.style,
applyTextStyleCss(
this.labelElement,
textStyleToCss(deltaState.header_style)
);
@@ -136,7 +135,7 @@ export class RevealerComponent extends ComponentBase<RevealerState> {
} else if (deltaState.header_style === "text") {
headerScale = 1;
} else {
headerScale = deltaState.header_style.fontSize;
headerScale = deltaState.header_style.fontSize ?? 1;
}
// Adapt the header's padding
+4 -13
View File
@@ -5,7 +5,7 @@ import {
ImageFill,
TextStyle,
} from "../dataModels";
import { textfillToCss, textStyleToCss } from "../cssUtils";
import { applyTextStyleCss, textfillToCss, textStyleToCss } from "../cssUtils";
import { ComponentBase, ComponentState, DeltaState } from "./componentBase";
export type TextState = ComponentState & {
@@ -17,13 +17,7 @@ export type TextState = ComponentState & {
overflow: "nowrap" | "wrap" | "ellipsize";
font: string | null;
fill:
| Color
| SolidFill
| LinearGradientFill
| ImageFill
| null
| "not given";
fill: Color | SolidFill | LinearGradientFill | ImageFill;
font_size: number | null;
italic: boolean | null;
font_weight: "normal" | "bold" | null;
@@ -102,10 +96,7 @@ export class TextComponent extends ComponentBase<TextState> {
textStyleCss["font-family"] = deltaState.font;
}
if (
deltaState.fill !== undefined &&
deltaState.fill !== "not given"
) {
if (deltaState.fill !== undefined) {
Object.assign(textStyleCss, textfillToCss(deltaState.fill));
}
@@ -141,7 +132,7 @@ export class TextComponent extends ComponentBase<TextState> {
: "none";
}
Object.assign(this.inner.style, textStyleCss);
applyTextStyleCss(this.inner, textStyleCss);
}
// Text content
+102 -81
View File
@@ -116,12 +116,14 @@ export function fillToCss(fill: AnyFill): {
};
}
export function textfillToCss(fill: TextCompatibleFill): {
type TextFillCss = {
color: string;
background: string;
backgroundClip: string;
textFillColor: string;
} {
"-webkit-background-clip": string;
"-webkit-text-fill-color": string;
};
export function textfillToCss(fill: TextCompatibleFill): TextFillCss {
// 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.
@@ -129,8 +131,8 @@ export function textfillToCss(fill: TextCompatibleFill): {
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)",
"-webkit-background-clip": "var(--rio-local-text-background-clip)",
"-webkit-text-fill-color": "var(--rio-local-text-fill-color)",
};
}
@@ -139,8 +141,8 @@ export function textfillToCss(fill: TextCompatibleFill): {
return {
color: colorToCssString(fill),
background: "none",
backgroundClip: "unset",
textFillColor: "unset",
"-webkit-background-clip": "unset",
"-webkit-text-fill-color": "unset",
};
}
@@ -149,8 +151,8 @@ export function textfillToCss(fill: TextCompatibleFill): {
return {
color: colorToCssString(fill.color),
background: "none",
backgroundClip: "unset",
textFillColor: "unset",
"-webkit-background-clip": "unset",
"-webkit-text-fill-color": "unset",
};
}
@@ -162,44 +164,30 @@ export function textfillToCss(fill: TextCompatibleFill): {
// 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",
"-webkit-background-clip": "text",
"-webkit-text-fill-color": "transparent",
};
}
type TextStyleCss = {
opacity?: string;
"font-family"?: string;
"font-size"?: string;
"font-weight"?: string;
"font-style"?: string;
"text-decoration"?: string;
"text-transform"?: string;
} & Partial<TextFillCss>;
export function textStyleToCss(
style: "heading1" | "heading2" | "heading3" | "text" | "dim" | TextStyle
): {
"font-family": string;
"font-size": string;
"font-weight": string;
"font-style": string;
"text-decoration": string;
"text-transform": string;
color: string;
background: string;
"-webkit-background-clip": string;
"-webkit-text-fill-color": string;
opacity: string;
} {
let fontFamily: string;
let fontSize: string;
let fontWeight: string;
let fontStyle: string;
let textDecorations: string[] = [];
let textTransform: string;
let color: string;
let background: string;
let backgroundClip: string;
let textFillColor: string;
let opacity: string;
): TextStyleCss {
let result: TextStyleCss = {};
// `Dim` is the same as `text`, just with some opacity
if (style === "dim") {
style = "text";
opacity = "0.4";
} else {
opacity = "1";
result.opacity = "0.4";
}
// Predefined style from theme
@@ -208,63 +196,96 @@ export function textStyleToCss(
let localPrefix = `var(--rio-local-${style}-`;
// Text fill
color = localPrefix + "color)";
background = localPrefix + "background)";
backgroundClip = localPrefix + "background-clip)";
textFillColor = localPrefix + "fill-color)";
result.color = localPrefix + "color)";
result.background = localPrefix + "background)";
result["-webkit-background-clip"] = localPrefix + "background-clip)";
result["-webkit-text-fill-color"] = localPrefix + "fill-color)";
// Font weight. This is local so that buttons can make their label text
// bold.
fontWeight = localPrefix + "font-weight)";
result["font-weight"] = localPrefix + "font-weight)";
// Others
fontFamily = globalPrefix + "font-name)";
fontSize = globalPrefix + "font-size)";
fontStyle = globalPrefix + "font-style)";
textDecorations.push(globalPrefix + "text-decoration)");
textTransform = globalPrefix + "all-caps)";
result["font-family"] = globalPrefix + "font-name)";
result["font-size"] = globalPrefix + "font-size)";
result["font-style"] = globalPrefix + "font-style)";
result["text-decoration"] = globalPrefix + "text-decoration)";
result["text-transform"] = globalPrefix + "all-caps)";
}
// Explicitly defined style
else {
fontSize = style.fontSize + "em";
fontStyle = style.italic ? "italic" : "normal";
fontWeight = style.fontWeight;
if (style.underlined) {
textDecorations.push("underline");
if (style.fontName !== null) {
result["font-family"] = style.fontName;
}
if (style.strikethrough) {
textDecorations.push("line-through");
if (style.fontSize !== null) {
result["font-size"] = style.fontSize + "rem";
}
textTransform = style.allCaps ? "uppercase" : "none";
// If no font family is provided, stick to the theme's.
if (style.fontName === null) {
fontFamily = "inherit";
} else {
fontFamily = style.fontName;
if (style.fontWeight !== null) {
result["font-weight"] = style.fontWeight;
}
({ color, background, backgroundClip, textFillColor } = textfillToCss(
style.fill
));
if (style.italic !== null) {
result["font-style"] = style.italic ? "italic" : "normal";
}
// TODO: `underlined` and `strikethrough` both map to the
// `text-decoration` CSS attribute. Which means that if only one of them
// is defined, the other one will be disabled instead of inherited. I
// don't think we can do anything about that, though.
if (style.underlined !== null || style.strikethrough !== null) {
let textDecorations: string[] = [];
if (style.underlined) {
textDecorations.push("underline");
}
if (style.strikethrough) {
textDecorations.push("line-through");
}
result["text-decoration"] = textDecorations.join(" ");
}
if (style.allCaps !== null) {
result["text-transform"] = style.allCaps ? "uppercase" : "none";
}
if (style.fill !== null) {
Object.assign(result, textfillToCss(style.fill));
}
}
return {
"font-family": fontFamily,
"font-size": fontSize,
"font-weight": fontWeight,
"font-style": fontStyle,
"text-decoration":
textDecorations.length > 0 ? textDecorations.join(" ") : "none",
"text-transform": textTransform,
color: color,
background: background,
"-webkit-background-clip": backgroundClip,
"-webkit-text-fill-color": textFillColor,
opacity: opacity,
};
return result;
}
export function applyTextStyleCss(
element: HTMLElement,
cssProps: TextStyleCss
): void {
// Since TextStyles don't have to include all of the styling parameters, we
// have to clear the old style before applying the new one to ensure that no
// undesired settings remain.
removeTextStyle(element);
Object.assign(element, cssProps);
}
export function removeTextStyle(element: HTMLElement): void {
for (let attribute of [
"opacity",
"font-family",
"font-size",
"font-weight",
"font-style",
"text-decoration",
"text-transform",
"color",
"background",
"-webkit-background-clip",
"-webkit-text-fill-color",
]) {
element.style.removeProperty(attribute);
}
}
+8 -9
View File
@@ -66,17 +66,16 @@ export type TextCompatibleFill =
| SolidFill
| LinearGradientFill
| RadialGradientFill
| ImageFill
| null;
| ImageFill;
export type TextStyle = {
fontName: string | null;
fill: TextCompatibleFill;
fontSize: number;
italic: boolean;
fontWeight: "normal" | "bold";
underlined: boolean;
strikethrough: boolean;
allCaps: boolean;
fill: TextCompatibleFill | null;
fontSize: number | null;
italic: boolean | null;
fontWeight: "normal" | "bold" | null;
underlined: boolean | null;
strikethrough: boolean | null;
allCaps: boolean | null;
};
export type Theme = {
+3 -1
View File
@@ -55,7 +55,9 @@ class Link(FundamentalComponent):
`accessibility_relationship`: Describes the linked page's relationship to
the current page. For example, a link to the next page of search results
should use `accessibility_relationship="next"`.
should use `accessibility_relationship="next"`. [MDN describes the
options in more
detail.](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel)
## Examples
+3 -8
View File
@@ -7,7 +7,7 @@ from uniserde import JsonDoc
import rio
from .. import deprecations, text_style, utils
from .. import deprecations, text_style
from .fundamental_component import FundamentalComponent
__all__ = [
@@ -110,7 +110,7 @@ class Text(FundamentalComponent):
overflow: t.Literal["nowrap", "wrap", "ellipsize"] = "nowrap"
font: rio.Font | None = None
fill: text_style._TextFill | None | utils.NotGiven = utils.NOT_GIVEN
fill: text_style._TextFill | None = None
font_size: float | None = None
italic: bool | None = None
font_weight: t.Literal["normal", "bold"] | None = None
@@ -143,16 +143,11 @@ 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,
"fill": self.session._serialize_fill(self.fill),
}
def __repr__(self) -> str:
+31 -14
View File
@@ -59,6 +59,27 @@ def _prepare_docs():
url_docs.owner = RIO_MODULE_DOCS
RIO_MODULE_DOCS.members["URL"] = url_docs
# There's some trickery in the `Theme` for static typing purposes, which
# involves some classes that we don't really want users to know about. Fix
# up the affected docs.
theme_docs = t.cast(
imy.docstrings.ClassDocs, RIO_MODULE_DOCS.members["Theme"]
)
for attr in (
"heading1_style",
"heading2_style",
"heading3_style",
"text_style",
):
# Ideally we would delete the PropertyDocs and replace them with
# AttributeDocs, but I'd rather not have to instantiate one of imy's
# classes here. They have a lot of parameters, and are quite likely to
# change in the future.
property_docs = t.cast(
imy.docstrings.PropertyDocs, theme_docs.members[attr]
)
property_docs.getter.return_type = rio.TextStyle
# Apply rio-specific post-processing
postprocess_docs(RIO_MODULE_DOCS)
@@ -149,14 +170,12 @@ def get_rio_module_docs() -> imy.docstrings.ModuleDocs:
@functools.cache
def get_all_documented_objects() -> (
dict[
type | t.Callable | property,
imy.docstrings.ClassDocs
| imy.docstrings.FunctionDocs
| imy.docstrings.PropertyDocs,
]
):
def get_all_documented_objects() -> dict[
type | t.Callable | property,
imy.docstrings.ClassDocs
| imy.docstrings.FunctionDocs
| imy.docstrings.PropertyDocs,
]:
all_docs = get_rio_module_docs().iter_children(
include_self=True, recursive=True
)
@@ -175,12 +194,10 @@ def get_all_documented_objects() -> (
@functools.cache
def get_toplevel_documented_objects() -> (
dict[
type | t.Callable,
imy.docstrings.ClassDocs | imy.docstrings.FunctionDocs,
]
):
def get_toplevel_documented_objects() -> dict[
type | t.Callable,
imy.docstrings.ClassDocs | imy.docstrings.FunctionDocs,
]:
"""
Returns only objects that have their own page in our docs. (That means no
methods.)
+15 -5
View File
@@ -2812,7 +2812,7 @@ a.remove();
)
for style_name in style_names:
style = getattr(thm, f"{style_name}_style")
style: theme.TextStyle = getattr(thm, f"{style_name}_style")
assert isinstance(style, rio.TextStyle), style
css_prefix = f"--rio-global-{style_name}"
@@ -3376,13 +3376,23 @@ a.remove();
if icon is not None:
icon_size = self.theme.heading2_style.font_size * 1.1
# Icons don't support the same fills as headings. Pick out a
# valid option.
for fill in (
self.theme.heading2_style.fill,
self.theme.heading3_style.fill,
self.theme.heading1_style.fill,
self.theme.text_style.fill,
):
if fill is not None:
break
else:
fill = rio.Color.BLACK
title_components.append(
rio.Icon(
icon,
# FIXME: This is technically wrong, since the heading
# style could be filled with something other than a
# valid icon color. What to do?
fill=self.theme.heading2_style.fill, # type: ignore
fill=fill,
min_width=icon_size,
min_height=icon_size,
)
@@ -62,7 +62,7 @@ class CardSection(rio.Component):
# Add Section Title
rio.Text(
"Why choose our service?",
style=theme.BOLD_SECTION_TITEL_DESKTOP,
style=theme.BOLD_SECTION_TITLE_DESKTOP,
justify="center",
),
# Add Section Description
@@ -114,7 +114,7 @@ class CardSection(rio.Component):
# Add section title
rio.Text(
"Why choose our service?",
style=theme.BOLD_SECTION_TITEL_MOBILE,
style=theme.BOLD_SECTION_TITLE_MOBILE,
),
# Add section description
rio.Text(
@@ -28,7 +28,7 @@ class FaqSection(rio.Component):
rio.Text(
"Frequently asked questions",
justify="center",
style=theme.BOLD_BIGGER_SECTION_TITEL_DESKTOP,
style=theme.BOLD_BIGGER_SECTION_TITLE_DESKTOP,
),
# Add Section Subtitle
rio.Text(
@@ -90,7 +90,7 @@ class FaqSection(rio.Component):
"Frequently asked questions",
justify="center",
overflow="wrap",
style=theme.BOLD_SECTION_TITEL_MOBILE,
style=theme.BOLD_SECTION_TITLE_MOBILE,
),
# Add Section Subtitle
rio.Text(
@@ -33,13 +33,13 @@ class GetStarted(rio.Component):
# Set styles and layout behavior based on the device type
if device == "desktop":
header_text_style: rio.TextStyle = (
theme.BOLD_SMALLER_SECTION_TITEL_DESKTOP
theme.BOLD_SMALLER_SECTION_TITLE_DESKTOP
)
sub_header_text_size = 1.1
overflow = "nowrap"
else:
header_text_style: rio.TextStyle = theme.BOLD_SECTION_TITEL_MOBILE
header_text_style: rio.TextStyle = theme.BOLD_SECTION_TITLE_MOBILE
sub_header_text_size = 1
overflow = "wrap"
@@ -37,7 +37,7 @@ class HeroSection(rio.Component):
rio.Text(
"Build your SaaS in seconds",
justify="center",
style=theme.BOLD_SECTION_TITEL_DESKTOP,
style=theme.BOLD_SECTION_TITLE_DESKTOP,
),
# Add subtitle
rio.Text(
@@ -52,7 +52,7 @@ class MajorColumn(rio.Component):
# Add header
rio.Text(
self.header,
style=theme.BOLD_SECTION_TITEL_DESKTOP,
style=theme.BOLD_SECTION_TITLE_DESKTOP,
),
# Add sub-header
rio.Text(
@@ -89,7 +89,7 @@ class MajorColumn(rio.Component):
# Add header
rio.Text(
self.header,
style=theme.BOLD_SECTION_TITEL_MOBILE,
style=theme.BOLD_SECTION_TITLE_MOBILE,
),
# Add sub-header
rio.Text(
@@ -40,7 +40,7 @@ class PricingSection(rio.Component):
rio.Text(
"A plan for every need",
justify="center",
style=theme.BOLD_BIGGER_SECTION_TITEL_DESKTOP,
style=theme.BOLD_BIGGER_SECTION_TITLE_DESKTOP,
),
# Sub-header providing additional context
rio.Text(
@@ -106,7 +106,7 @@ class PricingSection(rio.Component):
"A plan for every need",
justify="center", # Center-align the text horizontally
overflow="wrap", # Allow text to wrap to the next line if necessary
style=theme.BOLD_BIGGER_SECTION_TITEL_MOBILE,
style=theme.BOLD_BIGGER_SECTION_TITLE_MOBILE,
),
# Sub-header providing additional context
rio.Text(
@@ -109,7 +109,7 @@ class Testimonials(rio.Component):
# Section Title
rio.Text(
"What our customers are saying.",
style=theme.BOLD_SECTION_TITEL_DESKTOP,
style=theme.BOLD_SECTION_TITLE_DESKTOP,
justify="center",
),
# Brief description
@@ -162,7 +162,7 @@ class Testimonials(rio.Component):
rio.Text(
"What our customers are saying.",
overflow="wrap",
style=theme.BOLD_SECTION_TITEL_MOBILE,
style=theme.BOLD_SECTION_TITLE_MOBILE,
justify="center",
),
# Brief Description
@@ -39,7 +39,7 @@ class BlogPage(rio.Component):
return rio.Column(
rio.Text(
"Blog",
style=theme.BOLD_BIGGER_SECTION_TITEL_DESKTOP,
style=theme.BOLD_BIGGER_SECTION_TITLE_DESKTOP,
),
rio.Text(
"Welcome to our blog. Here you can find all the latest news and updates.",
@@ -64,7 +64,7 @@ class BlogPage(rio.Component):
rio.Text(
"Blog",
overflow="wrap",
style=theme.BOLD_BIGGER_SECTION_TITEL_MOBILE,
style=theme.BOLD_BIGGER_SECTION_TITLE_MOBILE,
),
rio.Text(
"Welcome to our blog. Here you can find all the latest news and updates.",
@@ -54,20 +54,20 @@ DARK_TEXT_SMALLER = rio.TextStyle(
# Text style for desktop
BOLD_BIGGER_SECTION_TITEL_DESKTOP = rio.TextStyle(
BOLD_BIGGER_SECTION_TITLE_DESKTOP = rio.TextStyle(
fill=TEXT_FILL_BRIGHTER,
font_size=SUB_TITLE_HEIGHT * 1.1,
font_weight="bold",
)
BOLD_SECTION_TITEL_DESKTOP = rio.TextStyle(
BOLD_SECTION_TITLE_DESKTOP = rio.TextStyle(
fill=TEXT_FILL_BRIGHTER,
font_size=SUB_TITLE_HEIGHT,
font_weight="bold",
)
BOLD_SMALLER_SECTION_TITEL_DESKTOP = rio.TextStyle(
BOLD_SMALLER_SECTION_TITLE_DESKTOP = rio.TextStyle(
fill=TEXT_FILL_BRIGHTER,
font_size=SUB_TITLE_HEIGHT * 0.8,
font_weight="bold",
@@ -75,13 +75,13 @@ BOLD_SMALLER_SECTION_TITEL_DESKTOP = rio.TextStyle(
# Text style for mobile
BOLD_BIGGER_SECTION_TITEL_MOBILE = rio.TextStyle(
BOLD_BIGGER_SECTION_TITLE_MOBILE = rio.TextStyle(
fill=TEXT_FILL_BRIGHTER,
font_size=SUB_TITLE_HEIGHT * 1.1 * MOBILE_TEXT_SCALING,
font_weight="bold",
)
BOLD_SECTION_TITEL_MOBILE = rio.TextStyle(
BOLD_SECTION_TITLE_MOBILE = rio.TextStyle(
fill=TEXT_FILL_BRIGHTER,
font_size=SUB_TITLE_HEIGHT * MOBILE_TEXT_SCALING,
font_weight="bold",
+85 -26
View File
@@ -88,11 +88,24 @@ class TextStyle(SelfSerializing):
be styled. You can use it to specify the font, fill, size, and other
properties of text in your rio app.
All parameters are optional. Omitting a parameter will leave that setting
unchanged (i.e. as if you hadn't applied the style at all). For example:
```py
theme = rio.Theme.from_colors(text_color=rio.Color.PURPLE)
highlighted_style = rio.TextStyle(font_weight="bold", italic=True)
... # Somewhere later
# This text will be purple, as defined in the theme, but also bold and
# italic.
rio.Text("Hello, World!", style=highlighted_style)
```
## Attributes
`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.
`font`: The `Font` to use for the text.
`fill`: The fill (color, gradient, etc.) for the text.
@@ -112,24 +125,26 @@ class TextStyle(SelfSerializing):
_: dataclasses.KW_ONLY
font: Font | None = None
fill: _TextFill | None = None
font_size: float = 1.0
italic: bool = False
font_weight: t.Literal["normal", "bold"] = "normal"
underlined: bool = False
strikethrough: bool = False
all_caps: bool = False
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 replace(
self,
*,
font: Font | None = None,
font: Font | None | utils.NotGiven = utils.NOT_GIVEN,
fill: _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,
font_size: float | None | utils.NotGiven = utils.NOT_GIVEN,
italic: bool | None | utils.NotGiven = utils.NOT_GIVEN,
font_weight: t.Literal["normal", "bold"]
| None
| utils.NotGiven = utils.NOT_GIVEN,
underlined: bool | None | utils.NotGiven = utils.NOT_GIVEN,
strikethrough: bool | None | utils.NotGiven = utils.NOT_GIVEN,
all_caps: bool | None | utils.NotGiven = utils.NOT_GIVEN,
) -> TextStyle:
"""
Returns an updated copy of the style.
@@ -139,9 +154,7 @@ class TextStyle(SelfSerializing):
## Parameters
`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.
`font`: The `Font` to use for the text.
`fill`: The fill (color, gradient, etc.) for the text.
@@ -157,19 +170,65 @@ class TextStyle(SelfSerializing):
`all_caps`: Whether the text is transformed to ALL CAPS or not.
"""
return type(self)(
font=self.font if font is None else font,
return TextStyle(
font=self.font if isinstance(font, utils.NotGiven) else font,
fill=self.fill if isinstance(fill, utils.NotGiven) else fill,
font_size=self.font_size if font_size is None else font_size,
italic=self.italic if italic is None else italic,
font_size=(
self.font_size
if isinstance(font_size, utils.NotGiven)
else font_size
),
italic=(
self.italic if isinstance(italic, utils.NotGiven) else italic
),
font_weight=(
self.font_weight if font_weight is None else font_weight
self.font_weight
if isinstance(font_weight, utils.NotGiven)
else font_weight
),
underlined=(
self.underlined
if isinstance(underlined, utils.NotGiven)
else underlined
),
underlined=self.underlined if underlined is None else underlined,
strikethrough=(
self.strikethrough if strikethrough is None else strikethrough
self.strikethrough
if isinstance(strikethrough, utils.NotGiven)
else strikethrough
),
all_caps=(
self.all_caps
if isinstance(all_caps, utils.NotGiven)
else all_caps
),
)
def _merged_with(self, other: TextStyle) -> TextStyle:
return TextStyle(
font=self.font if other.font is None else other.font,
fill=self.fill if other.fill is None else other.fill,
font_size=(
self.font_size if other.font_size is None else other.font_size
),
italic=self.italic if other.italic is None else other.italic,
font_weight=(
self.font_weight
if other.font_weight is None
else other.font_weight
),
underlined=(
self.underlined
if other.underlined is None
else other.underlined
),
strikethrough=(
self.strikethrough
if other.strikethrough is None
else other.strikethrough
),
all_caps=(
self.all_caps if other.all_caps is None else other.all_caps
),
all_caps=self.all_caps if all_caps is None else all_caps,
)
def _serialize(self, sess: rio.Session) -> JsonDoc:
+79 -21
View File
@@ -270,6 +270,26 @@ class Palette:
)
class TextStyle(text_style_module.TextStyle):
"""
For static typing purposes only. A `TextStyle` where none of the attributes
are `None`.
We don't really want the user to know that this class exists. It has the
same name as the regular `rio.TextStyle` because that name is displayed by
IDEs. It's also special-cased in the code that creates Rio's documentation.
"""
font: text_style_module.Font # type: ignore
fill: text_style_module._TextFill # type: ignore
font_size: float # type: ignore
italic: bool # type: ignore
font_weight: t.Literal["normal", "bold"] # type: ignore
underlined: bool # type: ignore
strikethrough: bool # type: ignore
all_caps: bool # type: ignore
@t.final
@dataclasses.dataclass()
class Theme:
@@ -282,8 +302,8 @@ class Theme:
Warning: The exact attributes available in themes are still subject to
change. The recommended way to create themes is using either the
`from_colors` or `pair_from_colors` method, as they provide a more
stable interface.
`Theme.from_colors` or `Theme.pair_from_colors` method, as they provide
a more stable interface.
## Attributes
@@ -328,14 +348,6 @@ class Theme:
color.
`monospace_font`: The font to use for monospace text, such as code.
`heading1_style`: The text style to use for the largest headings.
`heading2_style`: The text style to use for the second largest headings.
`heading3_style`: The text style to use for the third largest headings.
`text_style`: The text style to use for regular text.
"""
_: dataclasses.KW_ONLY
@@ -361,11 +373,44 @@ class Theme:
monospace_font: text_style_module.Font
# Text styles
heading1_style: rio.TextStyle
heading2_style: rio.TextStyle
heading3_style: rio.TextStyle
text_style: rio.TextStyle
# Text styles are defined as properties for type checking reasons. Users can
# assign a regular `rio.TextStyle`, but accessing an attribute returns a
# style where all attributes are not `None`.
@property
def heading1_style(self) -> TextStyle:
"The text style to use for the largest headings."
return self._heading1_style
@heading1_style.setter
def heading1_style(self, style: text_style_module.TextStyle) -> None:
self._heading1_style = self._heading1_style._merged_with(style)
@property
def heading2_style(self) -> TextStyle:
"The text style to use for the second largest headings."
return self._heading2_style
@heading2_style.setter
def heading2_style(self, style: text_style_module.TextStyle) -> None:
self._heading2_style = self._heading2_style._merged_with(style)
@property
def heading3_style(self) -> TextStyle:
"The text style to use for the third largest headings."
return self._heading3_style
@heading3_style.setter
def heading3_style(self, style: text_style_module.TextStyle) -> None:
self._heading3_style = self._heading3_style._merged_with(style)
@property
def text_style(self) -> TextStyle:
"The text style to use for regular text."
return self._text_style
@text_style.setter
def text_style(self, style: text_style_module.TextStyle) -> None:
self._text_style = self._text_style._merged_with(style)
def __init__(self) -> None:
# Themes are still very much in flux. New attributes will be added,
@@ -410,6 +455,15 @@ class Theme:
"""
self = object.__new__(Theme)
# Make sure the TextStyles have all attributes set
for style in (
heading1_style,
heading2_style,
heading3_style,
text_style,
):
assert None not in vars(style).values()
self.__dict__.update(
{
"primary_palette": primary_palette,
@@ -426,10 +480,10 @@ class Theme:
"corner_radius_large": corner_radius_large,
"shadow_color": shadow_color,
"monospace_font": monospace_font,
"heading1_style": heading1_style,
"heading2_style": heading2_style,
"heading3_style": heading3_style,
"text_style": text_style,
"_heading1_style": heading1_style,
"_heading2_style": heading2_style,
"_heading3_style": heading3_style,
"_text_style": text_style,
}
)
@@ -710,10 +764,15 @@ class Theme:
heading_fill = heading_fill
# Text styles
heading1_style = rio.TextStyle(
heading1_style = text_style_module.TextStyle(
font=font if heading_font is None else heading_font,
fill=heading_fill,
font_size=2.3,
italic=False,
font_weight="normal",
underlined=False,
strikethrough=False,
all_caps=False,
)
heading2_style = heading1_style.replace(font_size=1.7)
heading3_style = heading1_style.replace(font_size=1.2)
@@ -723,7 +782,6 @@ class Theme:
fill=neutral_and_background_text_color,
)
# Build the final theme
# Instantiate the theme. `__init__` is blocked to prevent users from
# doing something foolish. Work around that.
return rio.Theme._create_new(