diff --git a/frontend/code/components/text.ts b/frontend/code/components/text.ts index d83f2e7d..c83f3f06 100644 --- a/frontend/code/components/text.ts +++ b/frontend/code/components/text.ts @@ -3,22 +3,21 @@ import { textStyleToCss } from '../cssUtils'; import { ComponentBase, ComponentState } from './componentBase'; import { LayoutContext } from '../layouting'; import { getTextDimensions } from '../layoutHelpers'; -import { firstDefined } from '../utils'; export type TextState = ComponentState & { _type_: 'Text-builtin'; text?: string; - multiline?: boolean; selectable?: boolean; style?: 'heading1' | 'heading2' | 'heading3' | 'text' | 'dim' | TextStyle; - text_align: number | 'justify'; + justify?: 'left' | 'right' | 'center' | 'justify'; + line_overflow?: 'none' | 'wrap' | 'ellipsize'; }; export class TextComponent extends ComponentBase { state: Required; private inner: HTMLElement; - private cachedSingleLineTextDimensions: [number, number]; + private cachedNoWrapDimensions: [number, number]; createElement(): HTMLElement { let element = document.createElement('div'); @@ -37,28 +36,24 @@ export class TextComponent extends ComponentBase { // Text content // // Make sure not to allow any linebreaks if the text is not multiline. - if ( - deltaState.text !== undefined || - deltaState.multiline !== undefined - ) { - let text = firstDefined(deltaState.text, this.state.text); - let multiline = firstDefined( - deltaState.multiline, - this.state.multiline - ); - - if (!multiline) { - text = text.replace(/\n/g, ' '); - } - - this.inner.textContent = text; + if (deltaState.text !== undefined) { + this.inner.textContent = deltaState.text; } - // Multiline - if (deltaState.multiline !== undefined) { - this.inner.style.whiteSpace = deltaState.multiline - ? 'pre-wrap' - : 'pre'; + // Wrap lines + switch (deltaState.line_overflow) { + case 'none': + this.inner.style.whiteSpace = 'pre'; + this.inner.style.textOverflow = 'clip'; + break; + case 'wrap': + this.inner.style.whiteSpace = 'pre-wrap'; + this.inner.style.textOverflow = 'clip'; + break; + case 'ellipsize': + this.inner.style.whiteSpace = 'pre'; + this.inner.style.textOverflow = 'ellipsis'; + break; } // Selectable @@ -74,56 +69,45 @@ export class TextComponent extends ComponentBase { } // Text alignment - if (deltaState.text_align === 0) { - this.inner.style.textAlign = 'left'; - } else if (deltaState.text_align === 0.5) { - this.inner.style.textAlign = 'center'; - } else if (deltaState.text_align === 1) { - this.inner.style.textAlign = 'right'; - } else if (deltaState.text_align === 'justify') { - this.inner.style.textAlign = 'justify'; + if (deltaState.justify !== undefined) { + this.inner.style.textAlign = deltaState.justify; } if ( deltaState.text !== undefined || - deltaState.multiline !== undefined || + deltaState.line_overflow !== undefined || deltaState.style !== undefined ) { this.makeLayoutDirty(); - // If it's single-line, compute and cache the text dimensions - let multiline = firstDefined( - deltaState.multiline, - this.state.multiline + // Compute and cache the dimensions that our text requires if line + // wrapping is disabled + this.cachedNoWrapDimensions = getTextDimensions( + this.element.textContent!, + this.state.style ); - - if (!multiline) { - this.cachedSingleLineTextDimensions = getTextDimensions( - this.element.textContent!, - this.state.style - ); - } } } updateNaturalWidth(ctx: LayoutContext): void { - if (this.state.multiline) { - this.naturalWidth = 0; + if (this.state.line_overflow === 'none') { + this.naturalWidth = this.cachedNoWrapDimensions[0]; } else { - [this.naturalWidth, this.naturalHeight] = - this.cachedSingleLineTextDimensions; + this.naturalWidth = 0; } } updateNaturalHeight(ctx: LayoutContext): void { - if (this.state.multiline) { + if (this.state.line_overflow === 'wrap') { + // Calculate how much height we need given the allocated width this.naturalHeight = getTextDimensions( this.state.text, this.state.style, this.allocatedWidth )[1]; + } else { + // 'wrap' and 'ellipsize' both require the same height + this.naturalHeight = this.cachedNoWrapDimensions[1]; } - - // Single-line case is already handled in `updateNaturalWidth` } } diff --git a/frontend/css/style.scss b/frontend/css/style.scss index e2fffd6c..96849ede 100644 --- a/frontend/css/style.scss +++ b/frontend/css/style.scss @@ -652,6 +652,7 @@ select { .rio-text > div { flex-grow: 1; + overflow: hidden; } // Rectangle diff --git a/rio/components/text.py b/rio/components/text.py index e853f73c..afb379b6 100644 --- a/rio/components/text.py +++ b/rio/components/text.py @@ -51,9 +51,12 @@ class Text(FundamentalComponent): """ text: str - selectable: bool - style: Literal["heading1", "heading2", "heading3", "text", "dim"] | rio.TextStyle + selectable: bool = True + style: ( + Literal["heading1", "heading2", "heading3", "text", "dim"] | rio.TextStyle + ) = "text" justify: Literal["left", "right", "center", "justify"] = "left" + line_overflow: Literal["none", "wrap", "ellipsize"] = "none" def _custom_serialize(self) -> JsonDoc: # Serialization doesn't handle unions. Hence the custom serialization @@ -65,17 +68,8 @@ class Text(FundamentalComponent): return { "style": style, - "text_align": self._text_align, } - def get_debug_details(self) -> dict[str, Any]: - result = super().get_debug_details() - - # Pretend `text-align` is the same as `align_x` - result["align_x"] = self._text_align - - return result - def __repr__(self) -> str: if len(self.text) > 40: text = self.text[:40] + "..."