Skip to content

Commit

Permalink
Improve multiline text truncation
Browse files Browse the repository at this point in the history
  • Loading branch information
MatiPl01 committed Oct 27, 2023
1 parent 8533bea commit 972f483
Show file tree
Hide file tree
Showing 7 changed files with 456 additions and 262 deletions.
47 changes: 43 additions & 4 deletions example/src/components/StyleEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Dimensions, ScrollView, StyleSheet, Text, View } from 'react-native';
import {
EllipsizeMode,
HorizontalAlignment,
VerticalAlignment
} from 'react-native-skia-responsive-text';
Expand All @@ -14,7 +15,6 @@ const horizontalAlignmentOptions: Array<{
{ label: 'left', value: 'left' },
{ label: 'center', value: 'center' },
{ label: 'right', value: 'right' },
// { label: 'justify', value: 'justify' } // TODO - implement
{ label: 'center-left', value: 'center-left' },
{ label: 'center-right', value: 'center-right' }
];
Expand All @@ -28,6 +28,16 @@ const verticalAlignmentOptions: Array<{
{ label: 'bottom', value: 'bottom' }
];

const ellipsizeModeOptions: Array<{
label: EllipsizeMode;
value: EllipsizeMode;
}> = [
{ label: 'head', value: 'head' },
{ label: 'middle', value: 'middle' },
{ label: 'tail', value: 'tail' },
{ label: 'clip', value: 'clip' }
];

type StyleEditorProps = {
canvasDimensions: { height: number; width: number };
previewInnerPadding: number;
Expand All @@ -38,12 +48,16 @@ export default function StyleEditor({
previewInnerPadding
}: StyleEditorProps) {
const {
ellipsizeMode,
height,
horizontalAlignment,
lineHeight,
numberOfLines,
setEllipsizeMode,
setHeight,
setHorizontalAlignment,
setLineHeight,
setNumberOfLines,
setText,
setVerticalAlignment,
setWidth,
Expand Down Expand Up @@ -83,7 +97,7 @@ export default function StyleEditor({
longPressStep={2}
max={30}
min={10}
placeholder='Line Height'
placeholder='Line height'
value={lineHeight}
onChange={setLineHeight}
/>
Expand All @@ -96,7 +110,7 @@ export default function StyleEditor({
<Text style={styles.subSectionLabel}>horizontal</Text>
<SelectInput
items={horizontalAlignmentOptions}
placeholder='Horizontal Alignment'
placeholder='Horizontal alignment'
value={horizontalAlignment}
onChange={setHorizontalAlignment}
/>
Expand All @@ -105,7 +119,7 @@ export default function StyleEditor({
<Text style={styles.subSectionLabel}>vertical</Text>
<SelectInput
items={verticalAlignmentOptions}
placeholder='Vertical Alignment'
placeholder='Vertical alignment'
value={verticalAlignment}
onChange={setVerticalAlignment}
/>
Expand Down Expand Up @@ -137,6 +151,31 @@ export default function StyleEditor({
/>
</View>
</View>

<View style={styles.sectionGroup}>
<Text style={styles.sectionLabel}>Text overflow</Text>
<View style={styles.subSection}>
<Text style={styles.subSectionLabel}>numberOfLines</Text>
<View style={styles.sectionInput}>
<NumberInput
max={5}
min={1}
placeholder='Number of lines'
value={numberOfLines}
onChange={setNumberOfLines}
/>
</View>
</View>
<View style={styles.subSection}>
<Text style={styles.subSectionLabel}>ellipsizeMode</Text>
<SelectInput
items={ellipsizeModeOptions}
placeholder='Ellipsize mode'
value={ellipsizeMode}
onChange={setEllipsizeMode}
/>
</View>
</View>
</View>
</ScrollView>
);
Expand Down
6 changes: 6 additions & 0 deletions example/src/components/TextPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ export default function TextPreview({
setCanvasDimensions
}: TextPreviewProps) {
const {
ellipsizeMode,
height,
horizontalAlignment,
lineHeight,
numberOfLines,
text,
verticalAlignment,
width
Expand All @@ -65,6 +67,8 @@ export default function TextPreview({
<View
style={[styles.previewContainer, { padding: previewInnerPadding }]}>
<Text
ellipsizeMode={ellipsizeMode}
numberOfLines={numberOfLines}
style={[
styles.previewText,
{
Expand Down Expand Up @@ -96,10 +100,12 @@ export default function TextPreview({
}}>
<ResponsiveText
color='white'
ellipsizeMode={ellipsizeMode}
font={font}
height={height}
horizontalAlignment={horizontalAlignment}
lineHeight={lineHeight}
numberOfLines={numberOfLines}
text={text}
verticalAlignment={verticalAlignment}
width={width}
Expand Down
1 change: 1 addition & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ELLIPSIS = '...';
107 changes: 107 additions & 0 deletions src/utils/ellipsize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { SkFont } from '@shopify/react-native-skia';

import { ELLIPSIS } from '../constants';
import { TextLineData } from '../types';

export const trimLineStart = (
line: TextLineData,
font: SkFont,
width: number,
prefix = ELLIPSIS
): TextLineData => {
// If the line is too short, return it as is
if (line.width <= width) {
return line;
}

// Go from the first char and find how many chars must be sliced to
// display ellipsis at the beginning
let firstIndex = 0;
let lineWidth = font.getTextWidth(line.text) + font.getTextWidth(prefix);

while (lineWidth > width && firstIndex < line.text.length) {
lineWidth -= font.getTextWidth(line.text[firstIndex]!);
firstIndex++;
}

return {
text: `${prefix}${line.text.slice(firstIndex).trimStart()}`,
width: lineWidth
};
};

export const trimLineEnd = (
line: TextLineData,
font: SkFont,
width: number,
suffix = ELLIPSIS
): TextLineData => {
// If the line is too short, return it as is
if (line.width <= width) {
return line;
}

// Go from the last char and find how many chars must be sliced to
// display ellipsis at the end or clip the end of the line
let lastIndex = line.text.length - 1;
let lineWidth = font.getTextWidth(line.text) + font.getTextWidth(suffix);

while (lineWidth > width && lastIndex >= 0) {
lineWidth -= font.getTextWidth(line.text[lastIndex]!);
lastIndex--;
}

return {
text: `${line.text.slice(0, lastIndex + 1).trimEnd()}${suffix}`,
width: lineWidth
};
};

export const trimLineCenter = (
line: TextLineData,
font: SkFont,
width: number,
infix = ELLIPSIS
): TextLineData => {
// If the line is too short, return it as is
if (line.width <= width) {
return line;
}

// Get first half of the line in terms of width
let firstHalfWidth = 0;
let firstHalfIndex = 0;

while (firstHalfWidth < line.width / 2) {
firstHalfWidth += font.getTextWidth(line.text[firstHalfIndex]!);
firstHalfIndex++;
}

// Get second half of the line in terms of width
let secondHalfWidth = 0;
let secondHalfIndex = line.text.length - 1;

while (secondHalfWidth < line.width / 2) {
secondHalfWidth += font.getTextWidth(line.text[secondHalfIndex]!);
secondHalfIndex--;
}

// Remove chars from the end of the first half and from the start of the second half
// until the line width with infix is less than or equal to the width
let lineWidth = firstHalfWidth + secondHalfWidth + font.getTextWidth(infix);
while (lineWidth > width) {
if (firstHalfWidth > secondHalfWidth) {
firstHalfWidth -= font.getTextWidth(line.text[--firstHalfIndex]!);
} else {
secondHalfWidth -= font.getTextWidth(line.text[++secondHalfIndex]!);
}
lineWidth = firstHalfWidth + secondHalfWidth + font.getTextWidth(infix);
}

return {
text: `${line.text.slice(0, firstHalfIndex).trimEnd()}${infix}${line.text
.slice(secondHalfIndex)
.trimStart()}`,
width: lineWidth
};
};
2 changes: 2 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './alignment';
export * from './ellipsize';
export * from './reanimated';
export * from './text';
export * from './wrapping';
Loading

0 comments on commit 972f483

Please sign in to comment.