diff --git a/android/src/main/java/com/swmansion/enriched/markdown/spans/OrderedListSpan.kt b/android/src/main/java/com/swmansion/enriched/markdown/spans/OrderedListSpan.kt index 313fb711..b74dad3f 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/spans/OrderedListSpan.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/spans/OrderedListSpan.kt @@ -52,7 +52,7 @@ class OrderedListSpan( override fun getMarkerWidth(): Float { val paint = configureMarkerPaint() - return paint.measureText("99.") + return listStyle.effectiveMarkerWidth(paint.measureText("99.")) } var itemNumber: Int = 1 diff --git a/android/src/main/java/com/swmansion/enriched/markdown/spans/TaskListSpan.kt b/android/src/main/java/com/swmansion/enriched/markdown/spans/TaskListSpan.kt index 0f0e258b..f1a1897e 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/spans/TaskListSpan.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/spans/TaskListSpan.kt @@ -13,7 +13,7 @@ import com.swmansion.enriched.markdown.styles.TaskListStyle class TaskListSpan( private val taskStyle: TaskListStyle, - listStyle: ListStyle, + private val listStyle: ListStyle, depth: Int, context: Context, styleCache: SpanStyleCache, @@ -34,6 +34,7 @@ class TaskListSpan( gapWidth = listStyle.gapWidth, ) { private val checkboxSize = taskStyle.checkboxSize + private val markerColumnWidth = listStyle.effectiveMarkerWidth(checkboxSize) private val cornerRadius = taskStyle.checkboxBorderRadius private val rect = RectF() private val checkPath = Path() @@ -55,7 +56,7 @@ class TaskListSpan( strokeJoin = Paint.Join.ROUND } - override fun getMarkerWidth(): Float = checkboxSize + override fun getMarkerWidth(): Float = markerColumnWidth override fun drawMarker( canvas: Canvas, @@ -71,7 +72,10 @@ class TaskListSpan( val fontMetrics = paint.fontMetrics val capHeight = -fontMetrics.ascent * CAP_HEIGHT_RATIO val centerY = baseline - capHeight / HALF_DIVISOR - val centerX = x + (depth * marginLeft + checkboxSize / HALF_DIVISOR) * dir + // Right-align the checkbox inside the reserved marker column so it hugs + // the gap before the text. At the default (markerColumnWidth == + // checkboxSize) this is identical to the previous flush-left layout. + val centerX = x + (depth * marginLeft + markerColumnWidth - checkboxSize / HALF_DIVISOR) * dir val half = checkboxSize / HALF_DIVISOR rect.set(centerX - half, centerY - half, centerX + half, centerY + half) diff --git a/android/src/main/java/com/swmansion/enriched/markdown/spans/UnorderedListSpan.kt b/android/src/main/java/com/swmansion/enriched/markdown/spans/UnorderedListSpan.kt index 432f1e4b..6fc33c86 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/spans/UnorderedListSpan.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/spans/UnorderedListSpan.kt @@ -35,13 +35,14 @@ class UnorderedListSpan( } private val radius: Float = listStyle.bulletSize / 2f + private val markerColumnWidth: Float = listStyle.effectiveMarkerWidth(radius) private fun configureBulletPaint(): Paint = sharedBulletPaint.apply { color = listStyle.bulletColor } - override fun getMarkerWidth(): Float = radius + override fun getMarkerWidth(): Float = markerColumnWidth override fun drawMarker( canvas: Canvas, @@ -55,7 +56,11 @@ class UnorderedListSpan( start: Int, ) { val bulletPaint = configureBulletPaint() - val bulletX = x + (depth * marginLeft + radius) * dir + // Center the bullet at the right edge of the reserved marker column so + // it hugs the gap before the text — matches iOS behavior and stays + // visually flush-left when the column width equals the bullet radius + // (the default). + val bulletX = x + (depth * marginLeft + markerColumnWidth) * dir val fontMetrics = paint.fontMetrics val bulletY = baseline + (fontMetrics.ascent + fontMetrics.descent) / 2f diff --git a/android/src/main/java/com/swmansion/enriched/markdown/styles/ListStyle.kt b/android/src/main/java/com/swmansion/enriched/markdown/styles/ListStyle.kt index 23fc75de..9863a3ba 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/styles/ListStyle.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/styles/ListStyle.kt @@ -12,11 +12,14 @@ data class ListStyle( override val lineHeight: Float, val bulletColor: Int, val bulletSize: Float, + val markerMinWidth: Float, val markerColor: Int, val markerFontWeight: String, val gapWidth: Float, val marginLeft: Float, ) : BaseBlockStyle { + fun effectiveMarkerWidth(naturalWidth: Float): Float = naturalWidth.coerceAtLeast(markerMinWidth) + companion object { fun fromReadableMap( map: ReadableMap, @@ -32,6 +35,7 @@ data class ListStyle( val lineHeight = parser.toPixelFromSP(lineHeightRaw) val bulletColor = parser.parseColor(map, "bulletColor") val bulletSize = parser.toPixelFromDIP(map.getDouble("bulletSize").toFloat()) + val markerMinWidth = parser.toPixelFromDIP(map.getDouble("markerMinWidth").toFloat().coerceAtLeast(0f)) val markerColor = parser.parseColor(map, "markerColor") val markerFontWeight = parser.parseString(map, "markerFontWeight", "normal") val gapWidth = parser.toPixelFromDIP(map.getDouble("gapWidth").toFloat()) @@ -47,6 +51,7 @@ data class ListStyle( lineHeight, bulletColor, bulletSize, + markerMinWidth, markerColor, markerFontWeight, gapWidth, diff --git a/apps/example/src/markdownStyles.ts b/apps/example/src/markdownStyles.ts index f741cadd..8bc6f79d 100644 --- a/apps/example/src/markdownStyles.ts +++ b/apps/example/src/markdownStyles.ts @@ -69,6 +69,7 @@ export const customMarkdownStyle: MarkdownStyle = { lineHeight: Platform.select({ ios: 22, android: 26, default: 26 }), bulletColor: '#6b7280', bulletSize: 6, + markerMinWidth: 20, markerColor: '#6b7280', markerFontWeight: '500', gapWidth: 8, diff --git a/docs/STYLES.md b/docs/STYLES.md index fdc0009a..8b7377a6 100644 --- a/docs/STYLES.md +++ b/docs/STYLES.md @@ -238,6 +238,7 @@ The library provides sensible default styles for all Markdown elements out of th |----------|------|-------------| | `bulletColor` | `string` | Bullet point color | | `bulletSize` | `number` | Bullet point size | +| `markerMinWidth` | `number` | Minimum reserved marker column width (floors the natural width of every list type) | | `markerColor` | `string` | Number marker color | | `markerFontWeight` | `string` | Number marker font weight | | `gapWidth` | `number` | Gap between marker and text | diff --git a/ios/internals/MeasurementCache.h b/ios/internals/MeasurementCache.h index a1ca9dc0..e669d6cc 100644 --- a/ios/internals/MeasurementCache.h +++ b/ios/internals/MeasurementCache.h @@ -103,7 +103,7 @@ template inline size_t computeStyleFingerprint(const Styl hashFields(s.blockquote.borderWidth, s.blockquote.gapWidth); hashTextLayout(s.list); - hashFields(s.list.bulletSize, s.list.markerFontWeight, s.list.gapWidth, s.list.marginLeft); + hashFields(s.list.bulletSize, s.list.markerMinWidth, s.list.markerFontWeight, s.list.gapWidth, s.list.marginLeft); // Code & Inlines hashFields(s.codeBlock.fontFamily, s.codeBlock.fontSize, s.codeBlock.fontWeight, s.codeBlock.marginTop, diff --git a/ios/renderer/ListItemRenderer.m b/ios/renderer/ListItemRenderer.m index 85b5a5e9..3d315585 100644 --- a/ios/renderer/ListItemRenderer.m +++ b/ios/renderer/ListItemRenderer.m @@ -73,7 +73,7 @@ - (void)renderNode:(MarkdownASTNode *)node into:(NSMutableAttributedString *)out // currentDepth - 1 handles the horizontal offset for nested lists const NSInteger nestingLevel = currentDepth - 1; - const CGFloat baseMarkerWidth = isTask ? [_config taskListCheckboxSize] + const CGFloat baseMarkerWidth = isTask ? [_config effectiveListMarginLeftForTask] : (context.listType == ListTypeOrdered) ? [_config effectiveListMarginLeftForNumber] : [_config effectiveListMarginLeftForBullet]; diff --git a/ios/styles/StyleConfig.h b/ios/styles/StyleConfig.h index 7ad552d3..f9746c4a 100644 --- a/ios/styles/StyleConfig.h +++ b/ios/styles/StyleConfig.h @@ -236,6 +236,8 @@ - (void)setListStyleBulletColor:(RCTUIColor *)newValue; - (CGFloat)listStyleBulletSize; - (void)setListStyleBulletSize:(CGFloat)newValue; +- (CGFloat)listStyleMarkerMinWidth; +- (void)setListStyleMarkerMinWidth:(CGFloat)newValue; - (RCTUIColor *)listStyleMarkerColor; - (void)setListStyleMarkerColor:(RCTUIColor *)newValue; - (NSString *)listStyleMarkerFontWeight; @@ -249,6 +251,7 @@ - (CGFloat)effectiveListGapWidth; - (CGFloat)effectiveListMarginLeftForBullet; - (CGFloat)effectiveListMarginLeftForNumber; +- (CGFloat)effectiveListMarginLeftForTask; // Code block properties - (CGFloat)codeBlockFontSize; - (void)setCodeBlockFontSize:(CGFloat)newValue; diff --git a/ios/styles/StyleConfig.mm b/ios/styles/StyleConfig.mm index 3b4e668e..964ca1d4 100644 --- a/ios/styles/StyleConfig.mm +++ b/ios/styles/StyleConfig.mm @@ -151,6 +151,7 @@ @implementation StyleConfig { CGFloat _listStyleLineHeight; RCTUIColor *_listStyleBulletColor; CGFloat _listStyleBulletSize; + CGFloat _listStyleMarkerMinWidth; RCTUIColor *_listStyleMarkerColor; NSString *_listStyleMarkerFontWeight; CGFloat _listStyleGapWidth; @@ -434,6 +435,7 @@ - (id)copyWithZone:(NSZone *)zone copy->_listStyleLineHeight = _listStyleLineHeight; copy->_listStyleBulletColor = [_listStyleBulletColor copy]; copy->_listStyleBulletSize = _listStyleBulletSize; + copy->_listStyleMarkerMinWidth = _listStyleMarkerMinWidth; copy->_listStyleMarkerColor = [_listStyleMarkerColor copy]; copy->_listStyleMarkerFontWeight = [_listStyleMarkerFontWeight copy]; copy->_listStyleGapWidth = _listStyleGapWidth; @@ -1706,6 +1708,16 @@ - (void)setListStyleBulletSize:(CGFloat)newValue _listStyleBulletSize = newValue; } +- (CGFloat)listStyleMarkerMinWidth +{ + return _listStyleMarkerMinWidth; +} + +- (void)setListStyleMarkerMinWidth:(CGFloat)newValue +{ + _listStyleMarkerMinWidth = newValue; +} + - (RCTUIColor *)listStyleMarkerColor { return _listStyleMarkerColor; @@ -1786,16 +1798,20 @@ - (CGFloat)effectiveListGapWidth - (CGFloat)effectiveListMarginLeftForBullet { - // Just the minimum width needed for bullet (radius) - return _listStyleBulletSize / 2.0; + return MAX(_listStyleMarkerMinWidth, _listStyleBulletSize / 2.0); } - (CGFloat)effectiveListMarginLeftForNumber { - // Reserve width for numbers up to 99 (matching Android) UIFont *font = [self listMarkerFont]; - return + CGFloat natural = [@"99." sizeWithAttributes:@{NSFontAttributeName : font ?: [UIFont systemFontOfSize:_listStyleFontSize]}].width; + return MAX(_listStyleMarkerMinWidth, natural); +} + +- (CGFloat)effectiveListMarginLeftForTask +{ + return MAX(_listStyleMarkerMinWidth, [self taskListCheckboxSize]); } // Code block properties diff --git a/ios/utils/StylePropsUtils.h b/ios/utils/StylePropsUtils.h index f74311aa..cbf73cad 100644 --- a/ios/utils/StylePropsUtils.h +++ b/ios/utils/StylePropsUtils.h @@ -713,6 +713,11 @@ BOOL applyMarkdownStyleToConfig(StyleConfig *config, const MarkdownStyle &newSty changed = YES; } + if (newStyle.list.markerMinWidth != oldStyle.list.markerMinWidth) { + [config setListStyleMarkerMinWidth:newStyle.list.markerMinWidth]; + changed = YES; + } + if (newStyle.list.markerColor != oldStyle.list.markerColor) { RCTUIColor *markerColor = RCTUIColorFromSharedColor(newStyle.list.markerColor); [config setListStyleMarkerColor:markerColor]; diff --git a/src/EnrichedMarkdownNativeComponent.ts b/src/EnrichedMarkdownNativeComponent.ts index e81e2d96..06024b47 100644 --- a/src/EnrichedMarkdownNativeComponent.ts +++ b/src/EnrichedMarkdownNativeComponent.ts @@ -34,6 +34,7 @@ interface BlockquoteStyleInternal extends BaseBlockStyleInternal { interface ListStyleInternal extends BaseBlockStyleInternal { bulletColor: ColorValue; bulletSize: CodegenTypes.Float; + markerMinWidth: CodegenTypes.Float; markerColor: ColorValue; markerFontWeight: string; gapWidth: CodegenTypes.Float; diff --git a/src/EnrichedMarkdownTextNativeComponent.ts b/src/EnrichedMarkdownTextNativeComponent.ts index a76a48d6..e238ecfc 100644 --- a/src/EnrichedMarkdownTextNativeComponent.ts +++ b/src/EnrichedMarkdownTextNativeComponent.ts @@ -34,6 +34,7 @@ interface BlockquoteStyleInternal extends BaseBlockStyleInternal { interface ListStyleInternal extends BaseBlockStyleInternal { bulletColor: ColorValue; bulletSize: CodegenTypes.Float; + markerMinWidth: CodegenTypes.Float; markerColor: ColorValue; markerFontWeight: string; gapWidth: CodegenTypes.Float; diff --git a/src/normalizeMarkdownStyle.ts b/src/normalizeMarkdownStyle.ts index b9b1c2fa..240c83e8 100644 --- a/src/normalizeMarkdownStyle.ts +++ b/src/normalizeMarkdownStyle.ts @@ -112,6 +112,7 @@ const DEFAULT_NORMALIZED_STYLE = Object.freeze({ marginBottom: 16, bulletColor: normalizeColor('#6B7280')!, bulletSize: 6, + markerMinWidth: 0, markerColor: normalizeColor('#6B7280')!, markerFontWeight: '500', gapWidth: 12, diff --git a/src/normalizeMarkdownStyle.web.ts b/src/normalizeMarkdownStyle.web.ts index 5167d3f4..5045a9c0 100644 --- a/src/normalizeMarkdownStyle.web.ts +++ b/src/normalizeMarkdownStyle.web.ts @@ -98,6 +98,7 @@ const DEFAULT_NORMALIZED_STYLE: MarkdownStyleInternal = Object.freeze({ marginBottom: 16, bulletColor: '#6B7280', bulletSize: 6, + markerMinWidth: 0, markerColor: '#6B7280', markerFontWeight: '500', gapWidth: 12, diff --git a/src/types/MarkdownStyle.ts b/src/types/MarkdownStyle.ts index 75fb5185..c05a97ab 100644 --- a/src/types/MarkdownStyle.ts +++ b/src/types/MarkdownStyle.ts @@ -28,6 +28,11 @@ interface BlockquoteStyle extends BaseBlockStyle { interface ListStyle extends BaseBlockStyle { bulletColor?: string; bulletSize?: number; + /** + * Minimum reserved marker column width applied uniformly to UL/OL/task lists. + * `0` (the default) means no minimum — each list uses its natural marker width. + */ + markerMinWidth?: number; markerColor?: string; markerFontWeight?: string; gapWidth?: number; diff --git a/src/types/MarkdownStyleInternal.ts b/src/types/MarkdownStyleInternal.ts index 8cb52ae0..b62d1337 100644 --- a/src/types/MarkdownStyleInternal.ts +++ b/src/types/MarkdownStyleInternal.ts @@ -34,6 +34,7 @@ interface BlockquoteStyleInternal extends BaseBlockStyleInternal { interface ListStyleInternal extends BaseBlockStyleInternal { bulletColor: string; bulletSize: number; + markerMinWidth: number; markerColor: string; markerFontWeight: string; gapWidth: number;