From e0f636139da74a1ed502aae6acdf90217a99d4f5 Mon Sep 17 00:00:00 2001 From: Sean Murphy Date: Sun, 5 Apr 2026 23:58:15 -0400 Subject: [PATCH] feat: add image.verticalAlign option to htmlStyle Adds an image.verticalAlign property to htmlStyle that controls inline image attachment positioning: - 'baseline' (default): image bottom aligns with the descender - 'bottom': image bottom aligns with the line fragment bottom Implemented for iOS. Adjusts both the layout glyph bounds and the overlay image view position. --- ios/EnrichedTextInputView.mm | 20 ++++++++++++++--- ios/config/InputConfig.h | 2 ++ ios/config/InputConfig.mm | 10 +++++++++ ios/interfaces/ImageAttachment.mm | 23 +++++++++++++++----- src/spec/EnrichedTextInputNativeComponent.ts | 3 +++ src/types.ts | 3 +++ src/utils/normalizeHtmlStyle.ts | 3 +++ 7 files changed, 56 insertions(+), 8 deletions(-) diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 66c22512..1b142fe6 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -678,6 +678,14 @@ - (void)updateProps:(Props::Shared const &)props } } + if (newViewProps.htmlStyle.image.verticalAlign != + oldViewProps.htmlStyle.image.verticalAlign) { + [newConfig setImageVerticalAlign: + [NSString fromCppString:newViewProps.htmlStyle.image + .verticalAlign]]; + stylePropChanged = YES; + } + if (newViewProps.htmlStyle.a.textDecorationLine != oldViewProps.htmlStyle.a.textDecorationLine) { NSString *objcString = @@ -2320,9 +2328,15 @@ - (CGRect)frameForAttachment:(ImageAttachment *)attachment font = [config primaryFont]; } - // Calculate (Baseline Alignment) - CGFloat targetY = - CGRectGetMaxY(lineRect) + font.descender - attachmentSize.height; + // Calculate vertical position based on config + CGFloat targetY; + if ([[config imageVerticalAlign] isEqualToString:@"bottom"]) { + targetY = CGRectGetMaxY(lineRect) - attachmentSize.height; + } else { + // baseline (default): align image bottom with the descender + targetY = + CGRectGetMaxY(lineRect) + font.descender - attachmentSize.height; + } CGRect rect = CGRectMake(glyphRect.origin.x + textView.textContainerInset.left, targetY + textView.textContainerInset.top, diff --git a/ios/config/InputConfig.h b/ios/config/InputConfig.h index 73685213..c3a507e0 100644 --- a/ios/config/InputConfig.h +++ b/ios/config/InputConfig.h @@ -101,4 +101,6 @@ - (void)setCheckboxListBoxColor:(UIColor *)newValue; - (UIImage *)checkboxCheckedImage; - (UIImage *)checkboxUncheckedImage; +- (NSString *)imageVerticalAlign; +- (void)setImageVerticalAlign:(NSString *)newValue; @end diff --git a/ios/config/InputConfig.mm b/ios/config/InputConfig.mm index 6bc55d7f..d307b5d8 100644 --- a/ios/config/InputConfig.mm +++ b/ios/config/InputConfig.mm @@ -54,6 +54,7 @@ @implementation InputConfig { UIColor *_checkboxListBoxColor; UIImage *_checkboxCheckedImage; UIImage *_checkboxUncheckedImage; + NSString *_imageVerticalAlign; } - (instancetype)init { @@ -115,6 +116,7 @@ - (id)copyWithZone:(NSZone *)zone { copy->_checkboxListBoxColor = [_checkboxListBoxColor copy]; copy->_checkboxCheckedImage = _checkboxCheckedImage; copy->_checkboxUncheckedImage = _checkboxUncheckedImage; + copy->_imageVerticalAlign = [_imageVerticalAlign copy]; return copy; } @@ -661,4 +663,12 @@ - (UIImage *)generateCheckboxImage:(BOOL)isChecked { return result; } +- (NSString *)imageVerticalAlign { + return _imageVerticalAlign ?: @"baseline"; +} + +- (void)setImageVerticalAlign:(NSString *)newValue { + _imageVerticalAlign = newValue; +} + @end diff --git a/ios/interfaces/ImageAttachment.mm b/ios/interfaces/ImageAttachment.mm index 5c86c6ab..db4c00bb 100644 --- a/ios/interfaces/ImageAttachment.mm +++ b/ios/interfaces/ImageAttachment.mm @@ -1,4 +1,5 @@ #import "ImageAttachment.h" +#import "EnrichedTextInputView.h" #import "ImageExtension.h" // NSTextStorage frequently recreates NSTextAttachment objects during attribute @@ -62,12 +63,24 @@ - (CGRect)attachmentBoundsForTextContainer:(NSTextContainer *)textContainer return baseBounds; } - // Extend the layout bounds below the baseline by the font's descender. - // Without this, a line containing only the attachment has no descender space - // below the baseline, but adding a text character introduces it — causing - // the line height to jump. By reserving descender space upfront the line - // height stays consistent regardless of whether text is present. CGFloat descender = font.descender; + + // Check vertical alignment config + NSString *verticalAlign = @"baseline"; + if ([self.delegate isKindOfClass:[EnrichedTextInputView class]]) { + EnrichedTextInputView *inputView = (EnrichedTextInputView *)self.delegate; + verticalAlign = [inputView->config imageVerticalAlign]; + } + + if ([verticalAlign isEqualToString:@"bottom"]) { + // Align glyph to line fragment bottom — no descender offset on y, + // but include descender in height so the line fragment is tall enough. + return CGRectMake(baseBounds.origin.x, 0, baseBounds.size.width, + baseBounds.size.height); + } + + // Default baseline: extend layout bounds below the baseline by the font's + // descender so the line height stays consistent. return CGRectMake(baseBounds.origin.x, descender, baseBounds.size.width, baseBounds.size.height - descender); } diff --git a/src/spec/EnrichedTextInputNativeComponent.ts b/src/spec/EnrichedTextInputNativeComponent.ts index 83e557d5..b60f05b5 100644 --- a/src/spec/EnrichedTextInputNativeComponent.ts +++ b/src/spec/EnrichedTextInputNativeComponent.ts @@ -348,6 +348,9 @@ export interface HtmlStyleInternal { marginLeft?: Float; boxColor?: ColorValue; }; + image?: { + verticalAlign?: string; + }; } export interface NativeProps extends ViewProps { diff --git a/src/types.ts b/src/types.ts index ada3ceb6..183b1002 100644 --- a/src/types.ts +++ b/src/types.ts @@ -232,6 +232,9 @@ export interface HtmlStyle { marginLeft?: number; boxColor?: ColorValue; }; + image?: { + verticalAlign?: 'baseline' | 'bottom'; + }; } // Event types diff --git a/src/utils/normalizeHtmlStyle.ts b/src/utils/normalizeHtmlStyle.ts index 3392d080..89b1dd9c 100644 --- a/src/utils/normalizeHtmlStyle.ts +++ b/src/utils/normalizeHtmlStyle.ts @@ -69,6 +69,9 @@ const defaultStyle: Required = { marginLeft: 16, boxColor: 'blue', }, + image: { + verticalAlign: 'baseline', + }, }; const isMentionStyleRecord = (