Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,7 @@ class EnrichedMarkdownTextInputManager :
view: EnrichedMarkdownTextInputView?,
value: Float,
) {
if (value > 0 && view != null) {
view.setLineSpacing(value - view.textSize, 1f)
}
view?.setLineHeightFromProps(value)
}

@ReactProp(name = "fontFamily")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ class EnrichedMarkdownTextInputView(
private var headingOverrideBaseSizePx: Float? = null
private var baseHintColor: Int? = null

private var lineHeightDip: Float = 0f
private var baseFontSizePx: Float = ceil(PixelUtil.toPixelFromSP(16f))

// Number of ZWSP empty-list anchors believed live in the buffer. Bumped on insert,
// made exact on every strip scan (which recounts what it keeps), and reset when the
// buffer is replaced. Incoming markdown is scrubbed of U+200B at parse time, so in
Expand Down Expand Up @@ -950,10 +953,26 @@ class EnrichedMarkdownTextInputView(
fun setFontSizeFromProps(size: Float) {
if (size <= 0f) return
val sizePx = ceil(PixelUtil.toPixelFromSP(size))
baseFontSizePx = sizePx
setTextSize(TypedValue.COMPLEX_UNIT_PX, sizePx)
applyLineHeight()
layoutManager.invalidateLayout()
}

fun setLineHeightFromProps(value: Float) {
lineHeightDip = value
applyLineHeight()
layoutManager.invalidateLayout()
}

private fun applyLineHeight() {
if (lineHeightDip > 0f) {
setLineSpacing(PixelUtil.toPixelFromDIP(lineHeightDip) - baseFontSizePx, 1f)
} else {
setLineSpacing(0f, 1f)
}
}

fun setColorFromProps(colorInt: Int?) {
setTextColor(colorInt ?: Color.BLACK)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ class InputLayoutManager(
val text = view.text
val paint = view.paint

val needUpdate = InputMeasurementStore.store(view.id, text, paint)
val needUpdate =
InputMeasurementStore.store(view.id, text, paint, view.lineSpacingExtra, view.lineSpacingMultiplier)
if (!needUpdate) return

val state = Arguments.createMap()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ object InputMeasurementStore {
val cachedSize: Long,
val text: CharSequence?,
val paintParams: PaintParams,
val spacingExtra: Float,
val spacingMult: Float,
)

private val data = ConcurrentHashMap<Int, MeasurementParams>()
Expand All @@ -31,14 +33,16 @@ object InputMeasurementStore {
id: Int,
text: CharSequence?,
paint: TextPaint,
spacingExtra: Float,
spacingMult: Float,
): Boolean {
val cachedWidth = data[id]?.cachedWidth ?: 0f
val cachedSize = data[id]?.cachedSize ?: 0L

val size = measure(cachedWidth, text, paint)
val size = measure(cachedWidth, text, paint, spacingExtra, spacingMult)
val paintParams = PaintParams(paint.typeface, paint.textSize)

data[id] = MeasurementParams(cachedWidth, size, text, paintParams)
data[id] = MeasurementParams(cachedWidth, size, text, paintParams, spacingExtra, spacingMult)
return size != cachedSize
}

Expand Down Expand Up @@ -84,8 +88,8 @@ object InputMeasurementStore {
textSize = value.paintParams.fontSize
}

val size = measure(width, value.text, paint)
data[id] = MeasurementParams(width, size, value.text, value.paintParams)
val size = measure(width, value.text, paint, value.spacingExtra, value.spacingMult)
data[id] = MeasurementParams(width, size, value.text, value.paintParams, value.spacingExtra, value.spacingMult)
return size
}

Expand All @@ -110,13 +114,18 @@ object InputMeasurementStore {
isAntiAlias = true
}

return measure(width, text, paint)
val lineHeight = props?.getDouble("lineHeight")?.toFloat() ?: 0f
val spacingExtra = if (lineHeight > 0f) PixelUtil.toPixelFromDIP(lineHeight) - spSize else 0f

return measure(width, text, paint, spacingExtra, 1f)
}

private fun measure(
maxWidth: Float,
text: CharSequence?,
paint: TextPaint,
spacingExtra: Float,
spacingMult: Float,
): Long {
val content = text ?: ""
val widthPx = maxWidth.toInt().coerceAtLeast(0)
Expand All @@ -125,7 +134,7 @@ object InputMeasurementStore {
StaticLayout.Builder
.obtain(content, 0, content.length, paint, widthPx)
.setIncludePad(true)
.setLineSpacing(0f, 1f)
.setLineSpacing(spacingExtra, spacingMult)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
builder.setBreakStrategy(android.graphics.text.LineBreaker.BREAK_STRATEGY_HIGH_QUALITY)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ NS_ASSUME_NONNULL_BEGIN
style:(ENRMInputFormatterStyle *)style
scopedToRange:(NSRange)scope;

/// Injects the editor-wide `style.baseLineHeight` as each paragraph's
/// minimumLineHeight across `scope`, preserving whatever paragraph styling the
/// inline and block passes already applied (list indent, spacing). Runs last so
/// it covers plain paragraphs (which get no paragraph style otherwise) as well as
/// blocks. A zero baseLineHeight clears any previously-set minimum.
- (void)applyBaseLineHeightToTextView:(ENRMPlatformTextView *)textView
style:(ENRMInputFormatterStyle *)style
scopedToRange:(NSRange)scope;

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,82 @@ - (void)applyBlockRanges:(NSArray<ENRMBlockRange *> *)blockRanges
ENRMSetNeedsDisplay(textView);
}

- (void)applyBaseLineHeightToTextView:(ENRMPlatformTextView *)textView
style:(ENRMInputFormatterStyle *)style
scopedToRange:(NSRange)scope
{
NSTextStorage *textStorage = textView.textStorage;
NSUInteger textLength = textStorage.length;
if (textLength == 0) {
return;
}

NSUInteger scopeStart = MIN(scope.location, textLength);
NSUInteger scopeEnd = MIN(NSMaxRange(scope), textLength);
if (scopeEnd <= scopeStart) {
return;
}
NSRange scopeRange = NSMakeRange(scopeStart, scopeEnd - scopeStart);

CGFloat lineHeight = style.baseLineHeight > 0 ? style.baseLineHeight : 0;
CGFloat baseFontSize = style.baseFont.pointSize;

// Collect target paragraph styles first, then write them in one pass — mutating
// NSParagraphStyleAttributeName while enumerating the same attribute is unsafe.
NSMutableArray<NSValue *> *targetRanges = [NSMutableArray array];
NSMutableArray<NSParagraphStyle *> *targetStyles = [NSMutableArray array];

[textStorage enumerateAttribute:NSParagraphStyleAttributeName
inRange:scopeRange
options:0
usingBlock:^(NSParagraphStyle *existing, NSRange range, BOOL *stop) {
// Nothing to set and nothing to clear on an unstyled run.
if (existing == nil && lineHeight <= 0) {
return;
}

// Clamp (maximumLineHeight) only paragraphs whose glyphs fit the
// requested height. Larger fonts (headings) and attachments keep an
// unbounded max so a small lineHeight never clips them, mirroring
// Android where small values compress body text but spare tall lines.
__block BOOL allowClamp = YES;
if (lineHeight > 0) {
[textStorage enumerateAttributesInRange:range
options:0
usingBlock:^(NSDictionary<NSAttributedStringKey, id> *attrs,
NSRange r, BOOL *innerStop) {
UIFont *font = attrs[NSFontAttributeName];
if ((font && font.pointSize > baseFontSize + 0.5) ||
attrs[NSAttachmentAttributeName] != nil) {
allowClamp = NO;
*innerStop = YES;
}
}];
}

NSMutableParagraphStyle *paragraphStyle =
existing ? [existing mutableCopy] : [[NSMutableParagraphStyle alloc] init];
[style applyLineHeightToParagraphStyle:paragraphStyle allowClamp:allowClamp];

if (existing != nil && existing.minimumLineHeight == paragraphStyle.minimumLineHeight &&
existing.maximumLineHeight == paragraphStyle.maximumLineHeight) {
return;
}
[targetRanges addObject:[NSValue valueWithRange:range]];
[targetStyles addObject:paragraphStyle];
}];

if (targetRanges.count == 0) {
return;
}

[textStorage beginEditing];
for (NSUInteger i = 0; i < targetRanges.count; i++) {
[textStorage addAttribute:NSParagraphStyleAttributeName value:targetStyles[i] range:targetRanges[i].rangeValue];
}
[textStorage endEditing];
}

/// Applies `blockFont` over `range` while preserving the symbolic traits already
/// present on each existing font run (set by the inline formatting pass). The
/// resulting font takes its size and descriptor from `blockFont` but unions in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, strong) UIFont *baseFont;
@property (nonatomic, strong) RCTUIColor *baseTextColor;

/// Editor-wide line height (points) applied to every paragraph, mirroring
/// Android's view-level lineHeight. Set from the base `style.lineHeight` prop; 0
/// disables it (natural line height).
@property (nonatomic, assign) CGFloat baseLineHeight;

/// Writes baseLineHeight onto `paragraphStyle`. `minimumLineHeight` is always set
/// so short lines grow to the requested height. `maximumLineHeight` is set too
/// only when `allowClamp` is YES, letting a small lineHeight compress body text
/// below its natural height (matching Android's additive spacing). Callers pass
/// allowClamp = NO for lines that must not be clipped — headings and other
/// oversized/attachment lines. A zero baseLineHeight clears both.
- (void)applyLineHeightToParagraphStyle:(NSMutableParagraphStyle *)paragraphStyle allowClamp:(BOOL)allowClamp;

/// Bold — color override (nil = inherit baseTextColor)
@property (nonatomic, strong, nullable) RCTUIColor *boldColor;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ - (id)copyWithZone:(NSZone *)zone
ENRMInputFormatterStyle *copy = [[ENRMInputFormatterStyle allocWithZone:zone] init];
copy.baseFont = _baseFont;
copy.baseTextColor = _baseTextColor;
copy.baseLineHeight = _baseLineHeight;
copy.boldColor = _boldColor;
copy.italicColor = _italicColor;
copy.linkColor = _linkColor;
Expand All @@ -51,6 +52,13 @@ - (id)copyWithZone:(NSZone *)zone
return copy;
}

- (void)applyLineHeightToParagraphStyle:(NSMutableParagraphStyle *)paragraphStyle allowClamp:(BOOL)allowClamp
{
CGFloat lineHeight = _baseLineHeight > 0 ? _baseLineHeight : 0;
paragraphStyle.minimumLineHeight = lineHeight;
paragraphStyle.maximumLineHeight = (lineHeight > 0 && allowClamp) ? lineHeight : 0;
}

- (BOOL)isValidHeadingLevel:(NSInteger)level
{
return level >= 1 && level <= 6;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,22 @@ - (void)syncWithCursorBlock
RCTUIColor *headingColor = headingLevel >= 1 ? [_formatterStyle headingColorForLevel:headingLevel] : nil;
attrs[NSForegroundColorAttributeName] = headingColor ?: _formatterStyle.baseTextColor;

CGFloat baseLineHeight = _formatterStyle.baseLineHeight;
ENRMBlockRange *typingListBlock = [_dataSource listBlockForCursorParagraph];
if (typingListBlock != nil) {
NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
CGFloat indent = (typingListBlock.level + 1) * kENRMListIndentPerDepth;
paragraph.firstLineHeadIndent = indent;
paragraph.headIndent = indent;
paragraph.paragraphSpacingBefore = _formatterStyle.listItemSpacing;
if (baseLineHeight > 0) {
[_formatterStyle applyLineHeightToParagraphStyle:paragraph allowClamp:YES];
}
attrs[NSParagraphStyleAttributeName] = paragraph;
} else if (baseLineHeight > 0) {
NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
// A caret inside a heading must not clamp — the larger glyphs would clip.
[_formatterStyle applyLineHeightToParagraphStyle:paragraph allowClamp:(headingLevel < 1)];
attrs[NSParagraphStyleAttributeName] = paragraph;
} else {
[attrs removeObjectForKey:NSParagraphStyleAttributeName];
Expand Down Expand Up @@ -159,6 +168,17 @@ - (void)resetForSelectionChange

- (void)clearListParagraphStyle
{
// Leaving a list drops the indent/spacing paragraph style, but a configured
// editor-wide line height must survive, so fall back to a base paragraph style
// carrying only the line height rather than removing the attribute outright.
if (_formatterStyle.baseLineHeight > 0) {
NSMutableDictionary *attrs = [_textView.typingAttributes mutableCopy];
NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
[_formatterStyle applyLineHeightToParagraphStyle:paragraph allowClamp:YES];
attrs[NSParagraphStyleAttributeName] = paragraph;
_textView.typingAttributes = attrs;
return;
}
if (_textView.typingAttributes[NSParagraphStyleAttributeName] == nil) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,10 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &

[self resetBaseTypingAttributes];

if (_formattingStore.allRanges.count > 0) {
// Re-apply base typography (font, color, line height) to all existing text,
// not just text that carries inline markdown ranges — a plain paragraph still
// needs the new fontSize/lineHeight.
if (_textView.textStorage.length > 0) {
[self applyFormatting];
}

Expand Down Expand Up @@ -789,10 +792,16 @@ - (void)pasteMarkdown:(NSString *)markdown

- (void)resetBaseTypingAttributes
{
ENRMSetDefaultTypingAttributes(_textView, @{
NSMutableDictionary<NSAttributedStringKey, id> *attributes = [@{
NSFontAttributeName : _formatterStyle.baseFont,
NSForegroundColorAttributeName : _formatterStyle.baseTextColor,
});
} mutableCopy];
if (_formatterStyle.baseLineHeight > 0) {
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
[_formatterStyle applyLineHeightToParagraphStyle:paragraphStyle allowClamp:YES];
attributes[NSParagraphStyleAttributeName] = paragraphStyle;
}
ENRMSetDefaultTypingAttributes(_textView, attributes);
}

- (void)applyFormatting
Expand Down Expand Up @@ -835,6 +844,7 @@ - (void)applyFormattingScopedToRange:(NSRange)scope
style:_formatterStyle
scopedToRange:scope];
[_formatter applyBlockRanges:_blockStore.allRanges toTextView:_textView style:_formatterStyle scopedToRange:scope];
[_formatter applyBaseLineHeightToTextView:_textView style:_formatterStyle scopedToRange:scope];
[_detectorPipeline refreshAllStyling];
[self applyWritingDirection];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ BOOL applyInputStyleProps(ENRMInputFormatterStyle *style, const InputProps &newP
changed = YES;
}

if (newProps.lineHeight != oldProps.lineHeight) {
style.baseLineHeight = newProps.lineHeight > 0 ? newProps.lineHeight : 0;
changed = YES;
}

if (newProps.fontWeight != oldProps.fontWeight) {
CGFloat fontSize = style.baseFont.pointSize;
if (!newProps.fontWeight.empty()) {
Expand Down
Loading