diff --git a/.changeset/tiny-peas-grin.md b/.changeset/tiny-peas-grin.md new file mode 100644 index 000000000..698684a91 --- /dev/null +++ b/.changeset/tiny-peas-grin.md @@ -0,0 +1,7 @@ +--- +"@react-pdf/vite-example": minor +"@react-pdf/stylesheet": minor +"@react-pdf/layout": minor +--- + +feat: wrapping text around image and view (float) diff --git a/packages/examples/vite/src/examples/float/index.tsx b/packages/examples/vite/src/examples/float/index.tsx new file mode 100644 index 000000000..f61e951bc --- /dev/null +++ b/packages/examples/vite/src/examples/float/index.tsx @@ -0,0 +1,354 @@ +import React from 'react'; +import { + Document, + Page, + View, + Text, + Image, + StyleSheet, +} from '@react-pdf/renderer'; + +import Quijote from '../../../public/quijote1.jpg'; + +const styles = StyleSheet.create({ + page: { + padding: 40, + }, + section: { + marginBottom: 25, + backgroundColor: '#f5f5f5', + padding: 10, + }, + sectionTitle: { + fontSize: 24, + fontWeight: 'bold', + marginBottom: 10, + color: '#333', + }, + content: { + backgroundColor: '#fff', + }, + floatLeft: { + float: 'left', + width: 80, + height: 80, + backgroundColor: '#3498db', + marginRight: 10, + justifyContent: 'center', + alignItems: 'center', + }, + floatRight: { + float: 'right', + width: 80, + height: 80, + backgroundColor: '#e74c3c', + marginLeft: 10, + justifyContent: 'center', + alignItems: 'center', + }, + floatText: { + color: 'white', + fontSize: 18, + }, + text: { + fontSize: 12, + color: '#333', + }, + note: { + fontSize: 8, + color: '#666', + marginTop: 20, + }, + clearLeft: { + clear: 'left', + }, + clearRight: { + clear: 'right', + }, + clearBoth: { + clear: 'both', + }, + clearIndicator: { + backgroundColor: '#2ecc71', + padding: 5, + marginTop: 5, + }, + clearText: { + fontSize: 10, + color: '#fff', + }, +}); + +const longText = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.`; + +const articleText = `The quick brown fox jumps over the lazy dog. This pangram contains every letter of the alphabet at least once. Typography and typesetting have long used this sentence to display fonts and test equipment. The phrase has been used since at least the late 19th century. + +In the world of digital design, layout and composition remain fundamental skills. Understanding how text flows around images and other elements is crucial for creating professional documents. The float property, borrowed from CSS, allows designers to position elements while maintaining readable text flow. + +Modern document generation requires flexibility in positioning elements. Whether creating reports, magazines, or books, the ability to wrap text around images and other content blocks enables rich, engaging layouts that capture readers' attention. + +The art of typography extends beyond mere font selection. Line length, spacing, and the relationship between text and images all contribute to readability. When text wraps around floated elements, maintaining consistent line lengths becomes a consideration for optimal reading experience. + +Professional publications often employ multiple float patterns within a single article. Left-aligned images might introduce a topic, while right-aligned callout boxes highlight key points. Center-aligned layouts with elements on both sides create visual interest and break up long passages of text.`; + +const FloatExample = () => ( + + {/* Magazine-style layout with left and right floats */} + + + Magazine Article Layout + + + + {/* Left float - header image */} + + + {/* Right float - callout box */} + + + Key Points + + + • Left float{'\n'}• Right float{'\n'}• Center float + + + + {/* Centered float - text flows on both sides */} + {/* Content width = 535pt, element 200px, margin = (535-200)/2 ≈ 167 */} + + Centered + + + {/* Single long text that wraps around both floats */} + + {articleText} + {articleText} + + + + {/* New section with float - will appear on page 2 after pagination */} + + + Continued Article + + + + + NEW{'\n'}FLOAT + + + + This section appears after the page break with a new float element. + The text wraps around the green float box on the left. {articleText} + + + + + + + + Left Float + + + FLOAT + LEFT + + + {longText} + {longText} + + + + + + Right Float + + + FLOAT + RIGHT + + + {longText} + {longText} + + + + + + Multiple Floats + + + L + + + R + + + {longText} + {longText} + + + + + + Image Float + + + + This example shows an image floated to the left with text wrapping + around it. This is a common pattern for magazine-style layouts where + images are placed alongside article text. {longText} + + + + + + Float property allows elements to be positioned to the left or right, + with text wrapping around them. This is similar to CSS float behavior. + + + + + + Clear: left (Left is taller) + + + LEFT + 100px + + + RIGHT + 50px + + + This text wraps around both floats. The left float is 100px tall and + the right float is only 50px tall. + + + + clear: left - This green box should be below the TALL left float + (at 100px position) + + + + This text comes after clear:left. It should start below the left + float (100px). Since the right float was only 50px, it ended above + this position. + + + + + + Clear: right (Right is taller) + + + LEFT + 50px + + + RIGHT + 100px + + + This text wraps around both floats. The left float is only 50px tall + and the right float is 100px tall. + + + + clear: right - This green box should be below the TALL right float + (at 100px position) + + + + This text comes after clear:right. It should start below the right + float (100px). Since the left float was only 50px, it ended above + this position. + + + + + + Clear: both + + + LEFT + 60px + + + RIGHT + 90px + + + This text wraps around both floats. The left float is 60px and the + right float is 90px tall. + + + + clear: both - This green box should be below BOTH floats (at 90px, + the taller one) + + + + This text comes after clear:both. It should start below whichever + float is taller (the right one at 90px in this case). Both floats + have ended above this position. + + + + + +); + +export default { + id: 'float', + name: 'Float (Text Wrapping)', + description: 'Text wrapping around floated elements using float: left/right', + Document: FloatExample, +}; diff --git a/packages/examples/vite/src/examples/index.ts b/packages/examples/vite/src/examples/index.ts index 42dba4794..a00500d46 100644 --- a/packages/examples/vite/src/examples/index.ts +++ b/packages/examples/vite/src/examples/index.ts @@ -2,6 +2,7 @@ import scripts from './scripts'; import duplicatedImages from './duplicated-images'; import ellipsis from './ellipsis'; import emoji from './emoji'; +import float from './float'; import fontFamilyFallback from './font-family-fallback'; import fontWeight from './font-weight'; import goTo from './go-to'; @@ -30,6 +31,7 @@ const EXAMPLES = [ duplicatedImages, ellipsis, emoji, + float, fontFamilyFallback, fontWeight, goTo, diff --git a/packages/layout/src/index.ts b/packages/layout/src/index.ts index 7247df2fb..5e9022d25 100644 --- a/packages/layout/src/index.ts +++ b/packages/layout/src/index.ts @@ -5,6 +5,7 @@ import resolveYoga from './steps/resolveYoga'; import resolveZIndex from './steps/resolveZIndex'; import resolveAssets from './steps/resolveAssets'; import resolveStyles from './steps/resolveStyles'; +import resolveFloats from './steps/resolveFloats'; import resolveOrigins from './steps/resolveOrigins'; import resolveBookmarks from './steps/resolveBookmarks'; import resolvePageSizes from './steps/resolvePageSizes'; @@ -23,6 +24,7 @@ const layout = asyncCompose( resolveAssets, resolvePagination, resolveTextLayout, + resolveFloats, resolvePercentRadius, resolveDimensions, resolveSvg, diff --git a/packages/layout/src/steps/resolveDimensions.ts b/packages/layout/src/steps/resolveDimensions.ts index 3db12abb4..c983b17c7 100644 --- a/packages/layout/src/steps/resolveDimensions.ts +++ b/packages/layout/src/steps/resolveDimensions.ts @@ -80,6 +80,18 @@ const setNodeHeight = (node: SafeNode) => { return setHeight(value); }; +/** + * Get position type for node, treating float elements as absolute + */ +const getPositionType = (node: SafeNode): string | undefined => { + const float = node.style?.float; + // Float elements are treated as absolute positioned + if (float === 'left' || float === 'right') { + return 'absolute'; + } + return node.style.position; +}; + /** * Set styles valeus into yoga node before layout calculation * @@ -101,7 +113,7 @@ const setYogaValues = (node: SafeNode) => { setPaddingRight(node.style.paddingRight), setPaddingBottom(node.style.paddingBottom), setPaddingLeft(node.style.paddingLeft), - setPositionType(node.style.position), + setPositionType(getPositionType(node)), setPositionTop(node.style.top), setPositionRight(node.style.right), setPositionBottom(node.style.bottom), diff --git a/packages/layout/src/steps/resolveFloats.ts b/packages/layout/src/steps/resolveFloats.ts new file mode 100644 index 000000000..e1acf4d76 --- /dev/null +++ b/packages/layout/src/steps/resolveFloats.ts @@ -0,0 +1,204 @@ +import * as P from '@react-pdf/primitives'; +import { Clear } from '@react-pdf/stylesheet'; + +import { + SafeDocumentNode, + SafeNode, + SafePageNode, + SafeTextNode, + FloatSibling, +} from '../types'; + +const isText = (node: SafeNode): node is SafeTextNode => node.type === P.Text; + +const getNumericMargin = (value: number | string | undefined): number => { + return typeof value === 'number' ? value : 0; +}; + +const isFloated = (node: SafeNode): boolean => { + const float = node.style?.float; + return float === 'left' || float === 'right'; +}; + +const hasFloats = (children: SafeNode[] | undefined): boolean => { + if (!children) return false; + return children.some(isFloated); +}; + +/** + * Calculate the minimum Y position that clears the specified float elements + */ +const getClearY = (floats: FloatSibling[], clearType: Clear): number => { + if (clearType === 'none' || floats.length === 0) return 0; + + let maxY = 0; + + for (const float of floats) { + if (clearType === 'both' || clearType === float.float) { + const floatBottom = float.y + float.height + float.marginBottom; + maxY = Math.max(maxY, floatBottom); + } + } + + return maxY; +}; + +/** + * Calculate the Y offset adjustment needed to clear float siblings + */ +const applyClear = (node: SafeNode, floats: FloatSibling[]): number => { + const clearType = node.style?.clear; + + if (!clearType || clearType === 'none') return 0; + + const currentY = node.box?.top ?? 0; + const clearY = getClearY(floats, clearType); + + return Math.max(0, clearY - currentY); +}; + +/** + * Calculate left position for float element based on float direction + */ +const getFloatLeft = (node: SafeNode, parentWidth: number): number => { + const float = node.style?.float; + const marginLeft = getNumericMargin(node.style?.marginLeft); + const marginRight = getNumericMargin(node.style?.marginRight); + + if (float === 'left') { + return marginLeft; + } + + if (float === 'right') { + return parentWidth - (node.box?.width ?? 0) - marginRight; + } + + return node.box?.left ?? 0; +}; + +/** + * Position float element to the left or right edge of parent + * Note: Yoga already applies marginTop to box.top for absolute positioned elements + */ +const positionFloatElement = ( + node: T, + parentWidth: number, +): T => { + if (!node.box) return node; + + const newLeft = getFloatLeft(node, parentWidth); + const newBox = Object.assign({}, node.box, { left: newLeft }); + + return Object.assign({}, node, { box: newBox }) as T; +}; + +/** + * Create FloatSibling from a positioned node + * Note: box.top already includes marginTop adjustment from positionFloatElement + */ +const createFloatSibling = (node: SafeNode): FloatSibling => { + const { box, style } = node; + + return { + float: style?.float as 'left' | 'right', + x: box!.left, + y: box!.top, + width: box!.width, + height: box!.height, + marginTop: getNumericMargin(style?.marginTop), + marginRight: getNumericMargin(style?.marginRight), + marginBottom: getNumericMargin(style?.marginBottom), + marginLeft: getNumericMargin(style?.marginLeft), + }; +}; + +/** + * Apply clear offset to a node's vertical position + */ +const applyClearOffset = (node: SafeNode, offset: number): SafeNode => { + if (offset <= 0 || !node.box) return node; + + const newBox = Object.assign({}, node.box, { top: node.box.top + offset }); + return Object.assign({}, node, { box: newBox }) as SafeNode; +}; + +/** + * Attach float siblings to a text node for excludeRects generation. + * Skip if no floats or if text was split during pagination. + */ +const attachFloatSiblings = ( + node: SafeTextNode, + floats: FloatSibling[], +): SafeTextNode => { + if (floats.length === 0 || node.__wasSplit__) return node; + + return Object.assign({}, node, { __floatSiblings__: floats }); +}; + +/** + * Apply float resolution to a container node (View, Page, etc.) + */ +const resolveFloatsInContainer = (node: T): T => { + if (!node.children || node.children.length === 0) return node; + + const nodeChildren = node.children as SafeNode[]; + + if (!hasFloats(nodeChildren)) { + const children = nodeChildren.map(resolveFloatsInContainer); + return Object.assign({}, node, { children }) as T; + } + + const parentWidth = node.box?.width ?? 0; + const processedFloats: FloatSibling[] = []; + const children: SafeNode[] = []; + let clearOffset = 0; + + for (const child of nodeChildren) { + let processedChild: SafeNode = child; + + if (isFloated(child)) { + processedChild = positionFloatElement(child, parentWidth); + + if (processedChild.box) { + processedFloats.push(createFloatSibling(processedChild)); + } + } else { + const additionalOffset = applyClear(child, processedFloats); + + if (additionalOffset > 0) { + clearOffset = additionalOffset; + } + + processedChild = applyClearOffset(processedChild, clearOffset); + + if (isText(processedChild)) { + processedChild = attachFloatSiblings(processedChild, processedFloats); + } + } + + processedChild = resolveFloatsInContainer(processedChild); + children.push(processedChild); + } + + return Object.assign({}, node, { children }) as T; +}; + +/** + * Resolve floats for a page (exported for use in relayoutPage during pagination) + */ +export const resolvePageFloats = (page: SafePageNode): SafePageNode => { + return resolveFloatsInContainer(page) as SafePageNode; +}; + +/** + * Resolve floats in the document (should be called after resolveDimensions) + */ +const resolveFloats = (node: SafeDocumentNode): SafeDocumentNode => { + if (!node.children) return node; + + const children = node.children.map(resolvePageFloats); + + return Object.assign({}, node, { children }); +}; + +export default resolveFloats; diff --git a/packages/layout/src/steps/resolvePagination.ts b/packages/layout/src/steps/resolvePagination.ts index f029d7619..d1ccadd84 100644 --- a/packages/layout/src/steps/resolvePagination.ts +++ b/packages/layout/src/steps/resolvePagination.ts @@ -10,6 +10,7 @@ import getWrapArea from '../page/getWrapArea'; import getContentArea from '../page/getContentArea'; import createInstances from '../node/createInstances'; import shouldNodeBreak from '../node/shouldBreak'; +import { resolvePageFloats } from './resolveFloats'; import resolveTextLayout from './resolveTextLayout'; import resolveInheritance from './resolveInheritance'; import { resolvePageDimensions } from './resolveDimensions'; @@ -44,6 +45,7 @@ const isDynamic = ( const relayoutPage = compose( resolveTextLayout, + resolvePageFloats, resolvePageDimensions, resolveInheritance, resolvePageStyles, diff --git a/packages/layout/src/steps/resolveTextLayout.ts b/packages/layout/src/steps/resolveTextLayout.ts index 991fda953..5acdd553f 100644 --- a/packages/layout/src/steps/resolveTextLayout.ts +++ b/packages/layout/src/steps/resolveTextLayout.ts @@ -1,8 +1,9 @@ import * as P from '@react-pdf/primitives'; import FontStore from '@react-pdf/font'; +import { Rect } from '@react-pdf/textkit'; import layoutText from '../text/layoutText'; -import { SafeNode, SafeSvgNode, SafeTextNode } from '../types'; +import { FloatSibling, SafeNode, SafeSvgNode, SafeTextNode } from '../types'; const isSvg = (node: SafeNode): node is SafeSvgNode => node.type === P.Svg; @@ -10,8 +11,41 @@ const isText = (node: SafeNode): node is SafeTextNode => node.type === P.Text; const shouldIterate = (node: SafeNode) => !isSvg(node) && !isText(node); +/** + * Check if text node needs layout. + * Re-layout is needed if no lines calculated yet or has float siblings. + */ const shouldLayoutText = (node: SafeNode): node is SafeTextNode => - isText(node) && !node.lines; + isText(node) && (!node.lines || (node.__floatSiblings__?.length ?? 0) > 0); + +/** + * Generate exclude rects from float elements for textkit. + * The rects are in coordinates relative to the text container. + */ +const generateExcludeRects = ( + floats: FloatSibling[], + textOffsetY: number = 0, +): Rect[] => { + const excludeRects: Rect[] = []; + + for (const float of floats) { + const rectX = float.x - (float.float === 'right' ? float.marginLeft : 0); + const rectY = float.y - textOffsetY; + const rectWidth = + float.width + + (float.float === 'left' ? float.marginRight : float.marginLeft); + const rectHeight = float.height + float.marginBottom; + + excludeRects.push({ + x: rectX, + y: rectY, + width: rectWidth, + height: rectHeight, + }); + } + + return excludeRects; +}; /** * Performs text layout on text node if wasn't calculated before. @@ -23,12 +57,23 @@ const shouldLayoutText = (node: SafeNode): node is SafeTextNode => */ const resolveTextLayout = (node: SafeNode, fontStore: FontStore): SafeNode => { if (shouldLayoutText(node)) { - const width = - node.box.width - (node.box.paddingRight + node.box.paddingLeft); - const height = - node.box.height - (node.box.paddingTop + node.box.paddingBottom); + const width = node.box.width - node.box.paddingRight - node.box.paddingLeft; + const originalHeight = + node.box.height - node.box.paddingTop - node.box.paddingBottom; + const floatSiblings = node.__floatSiblings__ || []; + + let excludeRects: Rect[]; + let height = originalHeight; + + if (floatSiblings.length > 0) { + // Allow text to expand vertically when wrapping around floats + height = Infinity; + + const textOffsetY = node.box.top + node.box.paddingTop; + excludeRects = generateExcludeRects(floatSiblings, textOffsetY); + } - node.lines = layoutText(node, width, height, fontStore); + node.lines = layoutText(node, width, height, fontStore, excludeRects); } if (shouldIterate(node)) { diff --git a/packages/layout/src/text/getLineTop.ts b/packages/layout/src/text/getLineTop.ts new file mode 100644 index 000000000..9a994ea98 --- /dev/null +++ b/packages/layout/src/text/getLineTop.ts @@ -0,0 +1,34 @@ +import { Paragraph } from '@react-pdf/textkit'; + +/** + * Get Y position of a line at given index. + * Uses actual line y position when available (float wrapping), + * otherwise uses cumulative height calculation. + */ +const getLineTop = (lines: Paragraph | undefined, index: number): number => { + if (!lines?.length || index <= 0) return 0; + + // Use actual y position if available (for float wrapping) + if (lines[0].box?.y !== undefined) { + const startY = lines[0].box.y; + + if (index < lines.length) { + return lines[index].box.y - startY; + } + + const lastLine = lines[lines.length - 1]; + return lastLine.box.y - startY + lastLine.box.height; + } + + // Fallback: cumulative height + let y = 0; + const limit = Math.min(index, lines.length); + + for (let i = 0; i < limit; i += 1) { + y += lines[i].box.height; + } + + return y; +}; + +export default getLineTop; diff --git a/packages/layout/src/text/heightAtLineIndex.ts b/packages/layout/src/text/heightAtLineIndex.ts index 23bae6596..4ccf2d5f8 100644 --- a/packages/layout/src/text/heightAtLineIndex.ts +++ b/packages/layout/src/text/heightAtLineIndex.ts @@ -1,25 +1,15 @@ import { SafeTextNode } from '../types'; +import getLineTop from './getLineTop'; /** - * Get height for given text line index + * Get height for given text line index. + * Uses actual line y position when available (float wrapping), + * otherwise uses cumulative height calculation. * * @param node * @param index */ -const heightAtLineIndex = (node: SafeTextNode, index: number) => { - let counter = 0; - - if (!node.lines) return counter; - - for (let i = 0; i < index; i += 1) { - const line = node.lines[i]; - - if (!line) break; - - counter += line.box.height; - } - - return counter; -}; +const heightAtLineIndex = (node: SafeTextNode, index: number) => + getLineTop(node.lines, index); export default heightAtLineIndex; diff --git a/packages/layout/src/text/layoutText.ts b/packages/layout/src/text/layoutText.ts index e87235b67..ec5c5285f 100644 --- a/packages/layout/src/text/layoutText.ts +++ b/packages/layout/src/text/layoutText.ts @@ -6,6 +6,7 @@ import layoutEngine, { wordHyphenation, textDecoration, fontSubstitution, + Rect, } from '@react-pdf/textkit'; import FontStore from '@react-pdf/font'; @@ -34,9 +35,15 @@ const getTextOverflow = (node) => node.style?.textOverflow; * @param {number} width * @param {number} height * @param {Object} node + * @param {Rect[]} excludeRects - Areas to exclude from text layout (for float) * @returns {Object} layout container */ -const getContainer = (width, height, node) => { +const getContainer = ( + width: number, + height: number, + node: SafeTextNode, + excludeRects?: Rect[], +) => { const maxLines = getMaxLines(node); const textOverflow = getTextOverflow(node); @@ -47,6 +54,7 @@ const getContainer = (width, height, node) => { maxLines, height: height || Infinity, truncateMode: textOverflow, + excludeRects, }; }; @@ -72,6 +80,7 @@ const getLayoutOptions = (fontStore, node) => ({ * @param width - Container width * @param height - Container height * @param fontStore - Font store + * @param excludeRects - Areas to exclude from text layout (for float) * @returns Layout lines */ const layoutText = ( @@ -79,9 +88,10 @@ const layoutText = ( width: number, height: number, fontStore: FontStore, + excludeRects?: Rect[], ) => { const attributedString = getAttributedString(fontStore, node); - const container = getContainer(width, height, node); + const container = getContainer(width, height, node, excludeRects); const options = getLayoutOptions(fontStore, node); const lines = engine(attributedString, container, options); diff --git a/packages/layout/src/text/lineIndexAtHeight.ts b/packages/layout/src/text/lineIndexAtHeight.ts index 4099580e5..fdcbd52c6 100644 --- a/packages/layout/src/text/lineIndexAtHeight.ts +++ b/packages/layout/src/text/lineIndexAtHeight.ts @@ -1,20 +1,20 @@ import { SafeTextNode } from '../types'; +import getLineTop from './getLineTop'; /** - * Get line index at given height + * Get line index at given height. * * @param node * @param height */ const lineIndexAtHeight = (node: SafeTextNode, height: number) => { - let y = 0; - if (!node.lines) return 0; for (let i = 0; i < node.lines.length; i += 1) { - const line = node.lines[i]; - if (y + line.box.height > height) return i; - y += line.box.height; + const lineTop = getLineTop(node.lines, i); + const lineBottom = lineTop + node.lines[i].box.height; + + if (lineBottom > height) return i; } return node.lines.length; diff --git a/packages/layout/src/text/splitText.ts b/packages/layout/src/text/splitText.ts index 848b3b68e..f11458a0a 100644 --- a/packages/layout/src/text/splitText.ts +++ b/packages/layout/src/text/splitText.ts @@ -53,6 +53,8 @@ const splitText = (node: SafeTextNode, height: number) => { borderBottomRightRadius: 0, }, lines: node.lines.slice(0, slicedLineIndex), + __floatSiblings__: undefined, + __wasSplit__: true, }); const next: SafeTextNode = Object.assign({}, node, { @@ -71,6 +73,8 @@ const splitText = (node: SafeTextNode, height: number) => { borderTopRightRadius: 0, }, lines: node.lines.slice(slicedLineIndex), + __floatSiblings__: undefined, + __wasSplit__: true, }); return [current, next]; diff --git a/packages/layout/src/types/text.ts b/packages/layout/src/types/text.ts index 6f2256e0d..bb3e14b30 100644 --- a/packages/layout/src/types/text.ts +++ b/packages/layout/src/types/text.ts @@ -9,6 +9,21 @@ import { SafeTextInstanceNode, TextInstanceNode } from './text-instance'; import { ImageNode, SafeImageNode } from './image'; import { SafeTspanNode, TspanNode } from './tspan'; +/** + * Float element info attached to sibling text nodes + */ +export type FloatSibling = { + float: 'left' | 'right'; + x: number; + y: number; + width: number; + height: number; + marginTop: number; + marginRight: number; + marginBottom: number; + marginLeft: number; +}; + interface TextProps extends NodeProps { /** * Enable/disable page wrapping for element. @@ -56,4 +71,8 @@ export type SafeTextNode = Omit & { | SafeImageNode | SafeTspanNode )[]; + /** Float siblings attached by resolveFloats for text wrapping */ + __floatSiblings__?: FloatSibling[]; + /** Flag indicating this text node was split during pagination */ + __wasSplit__?: boolean; }; diff --git a/packages/stylesheet/src/resolve/layout.ts b/packages/stylesheet/src/resolve/layout.ts index 4e5d39d24..ff049c150 100644 --- a/packages/stylesheet/src/resolve/layout.ts +++ b/packages/stylesheet/src/resolve/layout.ts @@ -7,7 +7,9 @@ import { const handlers = { aspectRatio: processNumberValue<'aspectRatio'>, bottom: processUnitValue<'bottom'>, + clear: processNoopValue<'clear'>, display: processNoopValue<'display'>, + float: processNoopValue<'float'>, left: processUnitValue<'left'>, position: processNoopValue<'position'>, right: processUnitValue<'right'>, diff --git a/packages/stylesheet/src/types.ts b/packages/stylesheet/src/types.ts index e7bd5adc6..c93970dcc 100644 --- a/packages/stylesheet/src/types.ts +++ b/packages/stylesheet/src/types.ts @@ -218,10 +218,16 @@ export type Display = 'flex' | 'none'; export type Position = 'absolute' | 'relative' | 'static'; +export type Float = 'left' | 'right' | 'none'; + +export type Clear = Float | 'both'; + export type LayoutStyle = { aspectRatio?: number | string; bottom?: number | string; + clear?: Clear; display?: Display; + float?: Float; left?: number | string; position?: Position; right?: number | string; @@ -235,6 +241,8 @@ export type LayoutExpandedStyle = LayoutStyle; export type LayoutSafeStyle = LayoutExpandedStyle & { aspectRatio?: number; bottom?: number; + clear?: Clear; + float?: Float; left?: number; right?: number; top?: number;