Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
8 changes: 7 additions & 1 deletion example/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ export default function App() {
setLayoutInfo(`Layout: ${Math.round(width)}×${Math.round(height)}px`);
};

const handleTextLayout = (event: any) => {
const { lines } = event.nativeEvent;
console.log('lines', lines);
// setLayoutInfo(`Lines: ${lines.length}`);
};

return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
{/* Header Section */}
Expand Down Expand Up @@ -53,7 +59,7 @@ export default function App() {
{/* Layout Measurement */}
<View style={styles.section}>
<NitroText style={styles.sectionTitle}>Layout Measurement</NitroText>
<NitroText style={styles.measuredText} onLayout={handleLayout}>
<NitroText style={styles.measuredText} onLayout={handleLayout} onTextLayout={handleTextLayout}>
This text demonstrates layout measurement capabilities. The component
can measure its dimensions and report back to JavaScript.
{'\n\n'}
Expand Down
2 changes: 1 addition & 1 deletion example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2644,7 +2644,7 @@ SPEC CHECKSUMS:
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
hermes-engine: 4f8246b1f6d79f625e0d99472d1f3a71da4d28ca
NitroModules: 8d96528777600e967d371fd62b7eb183e9204530
NitroText: b514533a354c45c3135e58a54bc7cdaf7e68a72a
NitroText: 642e3f213559b0070662b9420f795abe24bc5cd9
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
RCTDeprecation: c4b9e2fd0ab200e3af72b013ed6113187c607077
RCTRequired: e97dd5dafc1db8094e63bc5031e0371f092ae92a
Expand Down
97 changes: 30 additions & 67 deletions ios/HybridNitroText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import Foundation
import UIKit

class HybridNitroText : HybridNitroTextSpec, NitroTextViewDelegate {
var view: UIView = NitroTextView()
var nitroTextImpl: NitroTextImpl?
private let textView = NitroTextView()
var view: UIView { textView }
let nitroTextImpl: NitroTextImpl

override init() {
nitroTextImpl = NitroTextImpl(view as! NitroTextView)
self.nitroTextImpl = NitroTextImpl(textView)
super.init()
}

func onNitroTextMeasured(height: Double) {
Expand All @@ -30,17 +32,17 @@ class HybridNitroText : HybridNitroTextSpec, NitroTextViewDelegate {

var selectable: Bool? {
didSet {
nitroTextImpl?.setSelectable(selectable)
nitroTextImpl.setSelectable(selectable)
}
}

var onSelectableTextMeasured: ((Double) -> Void)? {
didSet {
if onSelectableTextMeasured == nil {
(view as! NitroTextView).nitroTextDelegate = nil
textView.nitroTextDelegate = nil
return
}
(view as! NitroTextView).nitroTextDelegate = self
textView.nitroTextDelegate = self
}
}

Expand All @@ -58,7 +60,7 @@ class HybridNitroText : HybridNitroTextSpec, NitroTextViewDelegate {

var fontColor: String? {
didSet {
(view as! NitroTextView).textColor = ColorParser.parse(fontColor)
textView.textColor = ColorParser.parse(fontColor)
applyFragmentsAndProps()
}
}
Expand All @@ -71,15 +73,13 @@ class HybridNitroText : HybridNitroTextSpec, NitroTextViewDelegate {

var textAlign: TextAlign? {
didSet {
nitroTextImpl?.setTextAlign(textAlign)
applyFragmentsAndProps()
nitroTextImpl.setTextAlign(textAlign)
}
}

var textTransform: TextTransform? {
didSet {
nitroTextImpl?.setTextTransform(textTransform)
applyFragmentsAndProps()
nitroTextImpl.setTextTransform(textTransform)
}
}

Expand All @@ -97,68 +97,31 @@ class HybridNitroText : HybridNitroTextSpec, NitroTextViewDelegate {

var numberOfLines: Double? {
didSet {
nitroTextImpl?.setNumberOfLines(numberOfLines)
nitroTextImpl.setNumberOfLines(numberOfLines)
}
}

// Merge per-fragment props with top-level fallbacks and apply to NitroTextImpl
private func applyFragmentsAndProps() {
guard let nitroTextImpl else { return }

// If there are no per-fragment entries, but we have `text`, build a single fragment
guard let source = fragments, !source.isEmpty else {
if let t = text {
let single = Fragment(
fontSize: fontSize,
fontWeight: fontWeight,
fontColor: fontColor,
fontStyle: fontStyle,
lineHeight: lineHeight,
text: t,
numberOfLines: numberOfLines,
textAlign: textAlign,
textTransform: textTransform
)
nitroTextImpl.setFragments([single])
} else {
nitroTextImpl.setFragments(nil)
}
return
}

var merged: [Fragment] = []
merged.reserveCapacity(source.count)

for var frag in source {
if frag.text == nil {
frag.text = ""
}
if frag.fontSize == nil, let top = fontSize {
frag.fontSize = top
}
if frag.fontWeight == nil, let top = fontWeight {
frag.fontWeight = top
}
if frag.fontStyle == nil, let top = fontStyle {
frag.fontStyle = top
}
if frag.lineHeight == nil, let top = lineHeight, top > 0 {
frag.lineHeight = top
}

if frag.fontColor == nil, let top = fontColor, !top.isEmpty {
frag.fontColor = top
}
if frag.textAlign == nil, let ta = textAlign { frag.textAlign = ta }
if frag.textTransform == nil, let tt = textTransform { frag.textTransform = tt }

merged.append(frag)

var ellipsizeMode: EllipsizeMode? {
didSet {
nitroTextImpl.setEllipsizeMode(ellipsizeMode)
}
}

nitroTextImpl.setFragments(merged)
// Merge per-fragment props with top-level fallbacks and apply (delegated to NitroTextImpl)
private func applyFragmentsAndProps() {
let top = NitroTextImpl.FragmentTopDefaults(
fontSize: fontSize,
fontWeight: fontWeight,
fontColor: fontColor,
fontStyle: fontStyle,
lineHeight: lineHeight,
textAlign: textAlign,
textTransform: textTransform
)
nitroTextImpl.apply(fragments: fragments, text: text, top: top)
}

func afterUpdate() {
view.setNeedsLayout()
textView.setNeedsLayout()
}
}
84 changes: 84 additions & 0 deletions ios/NitroTextImpl+Attributes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// NitroTextImpl+Attributes.swift
// Pods
//
// Attribute-building helpers for NitroTextImpl (colors, paragraph style, transforms).
//

import UIKit

extension NitroTextImpl {
func makeAttributes(
for fragment: Fragment,
defaultColor: UIColor
) -> [NSAttributedString.Key: Any] {
var attrs: [NSAttributedString.Key: Any] = [:]

let font = makeFont(for: fragment, defaultPointSize: nitroTextView?.font?.pointSize)
attrs[.font] = font.value
if font.isItalic { attrs[.obliqueness] = 0.2 }

let para = makeParagraphStyle(for: fragment)
attrs[.paragraphStyle] = para

let color = resolveColor(for: fragment, defaultColor: defaultColor)
attrs[.foregroundColor] = color

return attrs
}

func makeParagraphStyle(for fragment: Fragment) -> NSMutableParagraphStyle {
let para = NSMutableParagraphStyle()

if let lineHeight = fragment.lineHeight, lineHeight > 0 {
para.minimumLineHeight = lineHeight
para.maximumLineHeight = lineHeight
}

if let align = fragment.textAlign {
switch align {
case .center: para.alignment = .center
case .right: para.alignment = .right
case .justify: para.alignment = .justified
case .left: para.alignment = .left
case .auto: para.alignment = .natural
}
} else {
para.alignment = currentTextAlignment
}

if let n = nitroTextView?.textContainer.maximumNumberOfLines {
para.lineBreakMode = effectiveLineBreakMode(forLines: n)
}
return para
}

func resolveColor(for fragment: Fragment, defaultColor: UIColor) -> UIColor {
if let value = fragment.fontColor, let parsed = ColorParser.parse(value) {
return parsed
}
return defaultColor
}

func transform(_ text: String, with fragment: Fragment) -> String {
let effective: TextTransform = {
if let ft = fragment.textTransform {
switch ft {
case .uppercase: return .uppercase
case .lowercase: return .lowercase
case .capitalize: return .capitalize
case .none: return .none
}
}
return currentTransform
}()

switch effective {
case .uppercase: return text.uppercased()
case .lowercase: return text.lowercased()
case .capitalize: return text.capitalized
case .none: return text
}
}
}

71 changes: 71 additions & 0 deletions ios/NitroTextImpl+Font.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//
// NitroTextImpl+Font.swift
// Pods
//
// Font-related helpers for NitroTextImpl.
//

import UIKit

struct FontKey: Hashable {
let size: CGFloat
let weightRaw: CGFloat
let italic: Bool
}

extension NitroTextImpl {
func makeFont(for fragment: Fragment, defaultPointSize: CGFloat?) -> (value: UIFont, isItalic: Bool) {
let resolvedSize: CGFloat = {
if let s = fragment.fontSize { return CGFloat(s) }
if let current = defaultPointSize { return current }
return 14.0
}()
let weightToken = fragment.fontWeight ?? FontWeight.normal
let uiWeight = Self.uiFontWeight(for: weightToken)
let isItalic = fragment.fontStyle == FontStyle.italic

let key = FontKey(size: resolvedSize, weightRaw: uiWeight.rawValue, italic: isItalic)
if let cached = fontCache[key] {
return (cached, isItalic)
}

var base = UIFont.systemFont(ofSize: resolvedSize, weight: uiWeight)
if isItalic {
var traits = base.fontDescriptor.symbolicTraits
traits.insert(.traitItalic)
if let italicDesc = base.fontDescriptor.withSymbolicTraits(traits) {
let traitsDict: [UIFontDescriptor.TraitKey: Any] = [.weight: uiWeight]
let finalDesc = italicDesc.addingAttributes([
UIFontDescriptor.AttributeName.traits: traitsDict
])
base = UIFont(descriptor: finalDesc, size: resolvedSize)
}
}
fontCache[key] = base
return (base, isItalic)
}
static func uiFontWeight(for weight: FontWeight) -> UIFont.Weight {
switch weight {
case .ultralight:
return .ultraLight
case .thin:
return .thin
case .light:
return .light
case .regular:
return .regular
case .medium:
return .medium
case .semibold:
return .semibold
case .bold:
return .bold
case .heavy:
return .heavy
case .black:
return .black
default:
return .regular
}
}
}
Loading
Loading