Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion samples/glasses_ui/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class GlassesUISample extends xb.Script {
const randomEmoji1 = EMOJIS[Math.floor(Math.random() * EMOJIS.length)];
const randomEmoji2 = EMOJIS[Math.floor(Math.random() * EMOJIS.length)];
cardTitleSignal.value = `Card ${this.cardNumber} ${randomEmoji1}`;
cardBodySignal.value = `This is card ${this.cardNumber} ${randomEmoji2}.`;
cardBodySignal.value = `This is \ncard ${this.cardNumber} ${randomEmoji2}.`;
}

private onRightXrCamera(rightCamera: THREE.Camera) {
Expand Down
47 changes: 37 additions & 10 deletions src/addons/uiblocks/src/core/primitives/TextWithEmoji.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ describe('TextWithEmoji Primitives', () => {
});
parent.add(textWithEmoji);

// Should have: Text('Hello'), Container(space), Text('World')
expect(textWithEmoji.children).toHaveLength(3);
// Should have: Text('Hello'), Text('World')
expect(textWithEmoji.children).toHaveLength(2);
expect(textWithEmoji.children[0]).toBeInstanceOf(Text);
expect(textWithEmoji.children[1]).toBeInstanceOf(Container);
expect(textWithEmoji.children[2]).toBeInstanceOf(Text);
expect(textWithEmoji.children[1]).toBeInstanceOf(Text);

const text1 = textWithEmoji.children[0] as Text;
expect(text1.properties.value.text).toBe('Hello');
expect(text1.properties.value.marginRight).toBe(16 * 0.26);
});

it('should parse and render emojis correctly', () => {
Expand All @@ -26,13 +29,18 @@ describe('TextWithEmoji Primitives', () => {
});
parent.add(textWithEmoji);

// Should have: Text('Hello'), Container(space), Image(emoji), Container(space), Text('World')
expect(textWithEmoji.children).toHaveLength(5);
// Should have: Text('Hello'), Image(emoji), Text('World')
expect(textWithEmoji.children).toHaveLength(3);
expect(textWithEmoji.children[0]).toBeInstanceOf(Text);
expect(textWithEmoji.children[1]).toBeInstanceOf(Container);
expect(textWithEmoji.children[2]).toBeInstanceOf(Image);
expect(textWithEmoji.children[3]).toBeInstanceOf(Container);
expect(textWithEmoji.children[4]).toBeInstanceOf(Text);
expect(textWithEmoji.children[1]).toBeInstanceOf(Image);
expect(textWithEmoji.children[2]).toBeInstanceOf(Text);

const text1 = textWithEmoji.children[0] as Text;
const emoji = textWithEmoji.children[1] as Image;

expect(text1.properties.value.text).toBe('Hello');
expect(text1.properties.value.marginRight).toBe(16 * 0.26); // 1 space = fontSize * 0.26
expect(emoji.properties.value.marginRight).toBe(16 * 0.26); // 1 space = fontSize * 0.26
});

it('should handle single newline correctly', () => {
Expand Down Expand Up @@ -93,4 +101,23 @@ describe('TextWithEmoji Primitives', () => {
expect(newline.properties.value.width).toBe('100%');
expect(newline.properties.value.height).toBe(16); // matches fontSize 16
});

it('should preserve explicit spaces after a newline', () => {
const parent = new Container();
const textWithEmoji = new TextWithEmoji({
text: 'Hello\n World',
fontSize: 16,
});
parent.add(textWithEmoji);

// Should have: Text('Hello'), Container(newline), Container(space), Text('World')
expect(textWithEmoji.children).toHaveLength(4);
expect(textWithEmoji.children[0]).toBeInstanceOf(Text);
expect(textWithEmoji.children[1]).toBeInstanceOf(Container);
expect(textWithEmoji.children[2]).toBeInstanceOf(Container);
expect(textWithEmoji.children[3]).toBeInstanceOf(Text);

const space = textWithEmoji.children[2] as Container;
expect(space.properties.value.width).toBe(16 * 0.26);
});
});
12 changes: 11 additions & 1 deletion src/addons/uiblocks/src/core/primitives/TextWithEmoji.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export class TextWithEmoji extends Container<TextWithEmojiOutProperties> {
type: 'space' | 'newline' | 'emoji' | 'word';
text: string;
isConsecutiveNewline?: boolean;
trailingSpaceWidth?: number;
}> = [];
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
Expand All @@ -153,7 +154,12 @@ export class TextWithEmoji extends Container<TextWithEmojiOutProperties> {
isConsecutiveNewline,
});
} else if (/^[ \t\r]+$/.test(segment)) {
activeSegments.push({type: 'space', text: segment});
const prev = activeSegments[activeSegments.length - 1];
if (prev && (prev.type === 'word' || prev.type === 'emoji')) {
prev.trailingSpaceWidth = currentFontSize * 0.26 * segment.length;
} else {
activeSegments.push({type: 'space', text: segment});
}
} else if (
/(?:\p{Emoji_Presentation}|\p{Emoji}\uFE0F)/u.test(segment)
) {
Expand Down Expand Up @@ -218,6 +224,7 @@ export class TextWithEmoji extends Container<TextWithEmojiOutProperties> {
width: calculatedEmojiSize,
height: calculatedEmojiSize,
transformTranslateY: calculatedEmojiOffsetY,
marginRight: seg.trailingSpaceWidth,
});
} else {
const txt = child as Text;
Expand All @@ -226,6 +233,7 @@ export class TextWithEmoji extends Container<TextWithEmojiOutProperties> {
fontSize: currentFontSize,
lineHeight: this.properties.value.lineHeight,
color: this.properties.value.color,
marginRight: seg.trailingSpaceWidth,
});
}
}
Expand Down Expand Up @@ -271,6 +279,7 @@ export class TextWithEmoji extends Container<TextWithEmojiOutProperties> {
height: calculatedEmojiSize,
keepAspectRatio: true,
transformTranslateY: calculatedEmojiOffsetY,
marginRight: seg.trailingSpaceWidth,
});
this.add(img);
} else {
Expand All @@ -280,6 +289,7 @@ export class TextWithEmoji extends Container<TextWithEmojiOutProperties> {
lineHeight: this.properties.value.lineHeight,
color: this.properties.value.color,
whiteSpace: 'pre',
marginRight: seg.trailingSpaceWidth,
});
this.add(txt);
}
Expand Down