স্টাইল রিচ টেক্সট ইনপুট

রিচ টেক্সট ইনপুট ব্যবহারকারীদের টাইপ করা টেক্সট ফর্ম্যাট করতে দেয়, যার মধ্যে রয়েছে কোনও শব্দ বোল্ড করা, শিরোনাম যোগ করা, অনুচ্ছেদ অ্যালাইন করা বা হাইলাইট করা @mention যোগ করা। Compose-এ, আপনি এটি স্টেট-ভিত্তিক টেক্সট ফিল্ডের (BasicTextField এবং TextField) উপরে তৈরি করেন। স্টাইল টেক্সটের সাথে TextFieldState-এ থাকে, তাই এডিট, বেছে নেওয়া পরিবর্তন এবং রিকম্পোজিশন করার পরেও তা থেকে যায় এবং ব্যবহারকারী টাইপ, মুছে ফেলা এবং পেস্ট করার সময় সিঙ্ক করা থাকে।

এই পৃষ্ঠায় দেখানো হয়েছে যে কীভাবে TextFieldBuffer ব্যবহার করে TextFieldState-এ স্টাইল প্রয়োগ, আপডেট, কোয়েরি, এক্সট্র্যাক্ট ও সরানো যায় এবং কীভাবে TextFieldState.textStyles থেকে ফর্ম্যাটিং টুলবার চালানো যায়।

ইনলাইন ও প্যারাগ্রাফ স্টাইল প্রয়োগ করা

আপনি যেকোনও TextFieldBuffer রিসিভার স্কোপের মধ্যে অক্ষর-লেভেল এবং অনুচ্ছেদ-লেভেল ফর্ম্যাটিং প্রয়োগ করতে পারবেন, যার মধ্যে TextFieldState.edit, InputTransformation.transformInput এবং OutputTransformation.transformOutput অন্তর্ভুক্ত।

ExpandPolicy-এর মাধ্যমে বাউন্ডারি এক্সপ্যানশন নিয়ন্ত্রণ করুন

আপনি addStyle কল করার সময়, ExpandPolicy পাস করতে পারেন যাতে ব্যবহারকারী সরাসরি তার start বা end সীমানায় নতুন অক্ষর যোগ করলে, স্টাইল করা রেঞ্জ কীভাবে কাজ করবে তা নির্ধারণ করা যায়:

  • (start, end)-এর মধ্যে ইনসার্ট করা: স্টাইল করা রেঞ্জ সবসময় বড় করে।
  • start-এর আগে যোগ করা: সবসময় start ও end অফসেট দুটিকেই ডানদিকে সরিয়ে দেয়।
  • সরাসরি start বা end-এ ইনসার্ট করা: আপনার উল্লেখ করা ExpandPolicy-এর দ্বারা নিয়ন্ত্রিত হয়।

নিম্নলিখিত সারণীতে উপলভ্য ExpandPolicy বিকল্প এবং সেগুলির সাধারণ ব্যবহারের উদাহরণ বর্ণনা করা হয়েছে:

নীতি সীমা সংক্রান্ত আচরণ সাজেস্ট করা ব্যবহারিক ক্ষেত্র উদাহরণ
ExpandPolicy.InsideOnly start বা end-এ টাইপ করা টেক্সট স্টাইল করা রেঞ্জের বাইরে থাকে। @mentions, #hashtags-এর মতো ডিসক্রিট টোকেন বা ব্যাকগ্রাউন্ড পিল যেখানে ট্যাগের পাশে টাইপ করলে ট্যাগের ব্যাকগ্রাউন্ড কালার ইনহেরিট করা উচিত নয়। অ্যানিমেশনে দেখানো হচ্ছে, ExpandPolicy.InsideOnly রেঞ্জের শুরুতে বা শেষে টাইপ করা টেক্সট আনস্টাইলড থেকে যাচ্ছে।
ExpandPolicy.AtStart start-এ টাইপ করা টেক্সট স্টাইল ইনহেরিট করে এবং end-এ টাইপ করা টেক্সট স্টাইল করা থাকে না। স্টাইল করা টেক্সটের ঠিক সামনে টাইপ করা টেক্সটেও একই ফর্ম্যাটিং প্রয়োগ করা উচিত। অ্যানিমেশনে দেখানো হচ্ছে, ExpandPolicy.AtStart রেঞ্জের শুরুতে টাইপ করা টেক্সট স্টাইল ইনহেরিট করছে, যেখানে শেষে টাইপ করা টেক্সট স্টাইল করা হয়নি।
ExpandPolicy.AtEnd end-এ টাইপ করা টেক্সট স্টাইল ইনহেরিট করে এবং start-এ টাইপ করা টেক্সট স্টাইল করা থাকে না। স্ট্যান্ডার্ড রিচ টেক্সট এডিটর টাইপিং, যেখানে বোল্ড বা ইটালিক শব্দের পরে টাইপ করা চালিয়ে গেলে সেই ফর্ম্যাটিং বজায় থাকে। অ্যানিমেশন দেখাচ্ছে যে ExpandPolicy.AtEnd রেঞ্জের শেষে টাইপ করা টেক্সট স্টাইল ইনহেরিট করছে, যেখানে শুরুতে টাইপ করা টেক্সট স্টাইল করা হয়নি।
ExpandPolicy.AtBoth start বা end-এ টাইপ করা টেক্সট রেঞ্জকে প্রসারিত করে এবং স্টাইলটি উত্তরাধিকার সূত্রে পায়। অ্যাক্টিভ হাইলাইট বা একটানা ফর্ম্যাট করা ব্লক যা যেকোনও প্রান্ত থেকে বাড়তে পারে। অ্যানিমেশনে দেখানো হচ্ছে, ExpandPolicy.AtBoth রেঞ্জের শুরুতে বা শেষে টাইপ করা টেক্সট স্টাইল ইনহেরিট করছে।

SpanStyle-এর সাহায্যে অক্ষর ফর্ম্যাটিং প্রয়োগ করুন

ক্যারেক্টার-লেভেল ফর্ম্যাটিং প্রয়োগ করতে SpanStyle-এর সাথে addStyle ব্যবহার করুন, যেমন টেক্সটের রঙ, ব্যাকগ্রাউন্ড হাইলাইট, FontWeight, FontStyle, অথবা বাফারে নির্দিষ্ট TextRange-এর জন্য আন্ডারলাইন।

SpanStyle রেঞ্জের জন্য মূল আচরণ

  • লেয়ারিং অর্ডার: আপনি যে ক্রমে বাফারে স্টাইল যোগ করেন সেই ক্রমেই স্টাইল প্রয়োগ করা হয়। একাধিক ওভারল্যাপ করা SpanStyle রেঞ্জ যদি একই প্রপার্টিতে আলাদা আলাদা ভ্যালু সেট করে, তাহলে পরে যোগ করা স্টাইলটি উপরে দেখায়।
  • খালি নয় এমন রেঞ্জের প্রয়োজনীয়তা: আপনাকে অবশ্যই এমন একটি নন-কোল্যাপ্সড রেঞ্জ পাস করতে হবে যেখানে start < end. 0 দৈর্ঘ্যের একটি সঙ্কুচিত নির্বাচনে addStyle কল করার কোনও প্রভাব নেই এবং বিপরীত বা সীমার বাইরের সূচক পাস করলে একটি IllegalArgumentException থ্রো করে।

TextField(
    state = rememberTextFieldState(),
    inputTransformation = InputTransformation {
        for (i in 0 until changes.changeCount) {
            val range = changes.getRange(i)
            if (!range.collapsed) {
                addStyle(
                    spanStyle = SpanStyle(
                        fontWeight = FontWeight.ExtraBold,
                        fontStyle = FontStyle.Italic,
                        color = Color.Red,
                    ),
                    range = range,
                    expandPolicy = ExpandPolicy.AtEnd
                )
            }
        }
    }
)

ব্যবহারকারী টাইপ করার সাথে সাথে TextField ডাইনামিক পদ্ধতিতে বোল্ড ও ইটালিক লাল ফর্ম্যাটিং প্রয়োগ করছে।
ছবি ১. ব্যবহারকারীর ইনপুটে ডাইনামিক পদ্ধতিতে বোল্ড ও ইটালিক লাল ফর্ম্যাটিং প্রয়োগ করা।

ParagraphStyle-এর মাধ্যমে ব্লক ফর্ম্যাটিং প্রয়োগ করুন

প্যারাগ্রাফ রেঞ্জে textAlign, lineHeight, textDirection বা textIndent-এর মতো ব্লক-লেভেল স্ট্রাকচারাল ফর্ম্যাটিং প্রয়োগ করতে ParagraphStyle-এর সাথে addStyle ব্যবহার করুন।

fun setParagraphAlignment(state: TextFieldState, alignment: TextAlign) {
    val targetRange = if (state.selection.collapsed) {
        TextRange(0, state.text.length)
    } else {
        state.selection
    }
    if (targetRange.collapsed) return
    state.edit {
        // 1. Remove existing paragraph styles in this range
        getParagraphStyles(targetRange).forEach { removeStyle(it) }
        // 2. Apply the new paragraph alignment
        addStyle(
            paragraphStyle = ParagraphStyle(textAlign = alignment),
            range = targetRange,
            expandPolicy = ExpandPolicy.InsideOnly
        )
    }
}

ParagraphStyle ব্যবহার করে শুরু, মাঝখান ও শেষের মধ্যে প্যারাগ্রাফ অ্যালাইনমেন্ট টগল করা TextField।
ছবি ২. ParagraphStyle ব্যবহার করে শুরু, মধ্য ও শেষের মধ্যে প্যারাগ্রাফ অ্যালাইনমেন্ট টগল করা।

ParagraphStyle-এর সাথে যেসব সমস্যা এড়িয়ে চলবেন

যেহেতু ParagraphStyle টেক্সটের সম্পূর্ণ ব্লকে প্রযোজ্য, তাই নিম্নলিখিত পরিস্থিতিতে এটি ব্যবহার করা এড়িয়ে চলুন:

  • সংলগ্ন অনুচ্ছেদে প্রসারিত করা: ExpandPolicy সাবধানে সেট করুন যাতে ব্যবহারকারীর এডিট করার পরে সংলগ্ন ParagraphStyle রেঞ্জ ওভারল্যাপ না করে। যেমন, যদি [0, 5), ExpandPolicy.AtEnd ব্যবহার করে এবং [5, 10), ExpandPolicy.AtStart ব্যবহার করে, তাহলে 5 ইন্ডেক্সে টেক্সট যোগ করলে দুটি স্টাইলই ওভারল্যাপে প্রসারিত হয় এবং IllegalArgumentException দেখায়।
  • ParagraphStyle রেঞ্জ নেস্ট করা: সম্পূর্ণভাবে নেস্ট করা রেঞ্জ (যেমন [0, 10) এবং [2, 8)) AnnotatedString-এ বৈধ হলেও, টেক্সট ফিল্ডে ParagraphStyle রেঞ্জ নেস্ট করা এড়িয়ে চলুন। লেআউট নেস্ট করা রেঞ্জকে আলাদা আলাদা অনুচ্ছেদে ভাগ করে এবং পরবর্তী এডিটগুলি অসাবধানতাবশত নেস্ট করা রেঞ্জকে আংশিক ওভারল্যাপে পরিণত করতে পারে।

TrackedRange-এর মাধ্যমে কোনও জায়গায় স্টাইল আপডেট করুন

TextRange এবং ExpandPolicy সহ addStyle কল করলে একটি TrackedRange হ্যান্ডেল রিটার্ন করে, যা TrackedRange<SpanStyle> বা TrackedRange<ParagraphStyle> হতে পারে। TrackedRange হল TextFieldBuffer-এ স্টাইল ইন্টারভ্যালের একটি ডায়নামিক রেফারেন্স যা টেক্সট যোগ, মুছে ফেলা বা পরিবর্তন করা হলে, অটোমেটিক এর start এবং end ক্যারেক্টার অফসেট আপডেট করে।

ইন-প্লেস পরিবর্তনযোগ্যতা

আগে থেকে থাকা স্টাইল আপডেট করতে বা এর সীমানা অ্যাডজাস্ট করতে আপনাকে removeStyle এবং addStyle-এ কল করতে হবে না। TextFieldBuffer স্কোপের মধ্যে, TrackedRange এক্সটেনশন প্রপার্টি প্রদান করে যা অন্যান্য স্টাইলের সাপেক্ষে এর আসল লেয়ারিং অর্ডার বজায় রেখে ইন্টারভ্যাল আপডেট করে:

  • TrackedRange<SpanStyle>.spanStyle: TrackedRange<SpanStyle>-এ SpanStyle পে-লোড পড়ে বা পরিবর্তন করে।
  • TrackedRange<ParagraphStyle>.paragraphStyle: TrackedRange<ParagraphStyle>-এ ParagraphStyle পেলোড পড়ে বা পরিবর্তন করে।
  • TrackedRange<*>.textRange: বর্তমান TextRange বাউন্ডারি পড়ে বা আপডেট করে। সংকুচিত, বিপরীত বা সীমার বাইরের রেঞ্জ সেট করলে IllegalArgumentException দেখায়।
  • TrackedRange<*>.expandPolicy: ExpandPolicy-এ পড়ে বা আপডেট করে।
  • TrackedRange<*>.isValid: স্টাইল ইন্টারভাল এখনও বাফারে অ্যাক্টিভ থাকলে true রিটার্ন করে, অথবা এডিট করার কারণে এর দৈর্ঘ্য কমে 0 হয়ে গেলে বা এটি সরিয়ে দেওয়া হলে false রিটার্ন করে।

// Trim an existing bold span in place when the selection overlaps one edge
val range = style.textRange
if (range.start < selection.min && range.end <= selection.max) {
    // Selection covers the end of the span - shrink end boundary in place
    style.textRange = TextRange(range.start, selection.min)
} else if (range.start >= selection.min && range.end > selection.max) {
    // Selection covers the start of the span - shrink start boundary in place
    style.textRange = TextRange(selection.max, range.end)
}

স্কোপ কনফাইনমেন্ট ও লাইফসাইকেল সংক্রান্ত নিয়ম

TrackedRange ইনস্ট্যান্স নিয়ে কাজ করার সময় নিম্নলিখিত স্কোপ ও লাইফসাইকেল সংক্রান্ত নিয়ম মনে রাখবেন:

  • TextFieldBuffer-এ সীমাবদ্ধ: TrackedRange এক্সটেনশন প্রপার্টি শুধুমাত্র TextFieldBuffer স্কোপের মধ্যে থাকে যেখানে হ্যান্ডেল তৈরি বা কোয়েরি করা হয়েছে।
  • কলাপ্স করার সময় অটোমেটিক প্রুনিং: ব্যবহারকারী যদি TrackedRange-এর মাধ্যমে কভার করা সব অক্ষর মুছে দেওয়ার জন্য এডিট করেন, তাহলে এর দৈর্ঘ্য কমে 0 হয়ে যায়, isValid false রিটার্ন করে এবং বাফার থেকে স্টাইল সরিয়ে দেওয়া হয়।

কোয়েরি ও স্টাইল সরানো

আপনি বাফার মিউটেট করছেন নাকি আপনার UI-এর জন্য স্টেট রিড করছেন তার উপর নির্ভর করে, অ্যাক্টিভ স্টাইল পরীক্ষা করার জন্য Compose দুটি উপায় প্রদান করে:

  • TextFieldBuffer ব্লকের মধ্যে: মিউটেবল TrackedRange হ্যান্ডেল পেতে সরাসরি TextFieldBuffer রিসিভারে getSpanStyles এবং getParagraphStyles কল করুন।
  • এডিট ব্লকের বাইরে: TextFieldState.textStyles পড়ুন, যা কমিট করা স্ট্যাটাস দেখায় এমন অপরিবর্তনীয় AnnotatedString.Range তালিকা ফেরত পাঠায়।

TextFieldBuffer-এ ইন্টারসেক্টিং স্টাইল কোয়েরি করুন

TextFieldBuffer.getSpanStyles এবং TextFieldBuffer.getParagraphStyles প্রদত্ত TextRange-এর সাথে ইন্টারসেক্ট করে এমন সব TrackedRange ইনস্ট্যান্স ফেরত দেয়, যেগুলি বাফারে যোগ করার সময় অনুযায়ী সাজানো থাকে।

যেমন, ব্যবহারকারীর বর্তমান সিলেকশন কভার করে এমন স্টাইল পরীক্ষা বা আপডেট করতে, আপনি state.edit-এর মধ্যে getSpanStyles(selection) কোয়েরি করতে পারেন:

state.edit {
    if (selection.collapsed) return@edit
    // Query all bold spans that overlap with the current selection
    val boldStyles: List<TrackedRange<SpanStyle>> = getSpanStyles(selection).filter {
        it.spanStyle.fontWeight == FontWeight.Bold
    }
    // ...
}

স্টাইল ইন্টারভ্যাল সেমি-ওপেন গাণিতিক রেঞ্জ [start, end) ব্যবহার করে। টাচিং স্টাইল ফেরত দেওয়া হবে কিনা তা নির্ভর করে আপনি কোনও সিলেকশন বা একটি কার্সর পয়েন্ট কোয়েরি করেছেন কিনা তার উপর:

  • কোনও বিকল্প বেছে নিলে [start, end):
    • ফেরত দেওয়া হয়েছে: বেছে নেওয়ার রেঞ্জের মধ্যে ওভারল্যাপ করে, থাকে বা বসে এমন যেকোনও স্টাইল। যেমন, কোয়েরি [5, 15), [0, 20), [8, 12) বা [0, 10)-এর সাথে ম্যাচ করে।
    • রিটার্ন করা হয়নি: এমন স্টাইল যা ওভারল্যাপ করা অক্ষর ছাড়াই শুধু বাইরের সীমানার প্রান্তকে স্পর্শ করে। যেমন, কোয়েরি [5, 15)-এ [0, 5) এবং [15, 25) বাদ দেওয়া হয়েছে।
  • গুটিয়ে নেওয়া কার্সরের জন্য [index, index]:
    • ফেরত এসেছে: কার্সর আছে বা কার্সরের পজিশন থেকে শুরু হয় এমন যেকোনও স্টাইল। যেমন, 10-এ থাকা কার্সর [0, 20) ও [10, 20)-এর সাথে ম্যাচ করে।
    • ফেরত দেওয়া হয়নি (শেষের নিয়ম স্পর্শ করা): কার্সারের পজিশনে শেষ হওয়া যেকোনও স্টাইল। যেমন, 10-এ কার্সর থাকলে [0, 10) বাদ যায়।

removeStyle-এর সাহায্যে স্টাইল সরান বা টগল করুন

TextFieldBuffer থেকে নির্দিষ্ট স্টাইল ইন্টারভাল মুছে দিতে removeStyle-এ TrackedRange হ্যান্ডেল পাস করুন।

TrackedRange খুঁজে পাওয়া ও সরানো গেলে removeStyle, true রিটার্ন করে, অথবা এটি আগে থেকেই সরানো হয়ে থাকলে বা অন্য বাফারের অন্তর্গত হলে false রিটার্ন করে। এটি শুধুমাত্র টার্গেট করা TrackedRange হ্যান্ডেল সরিয়ে দেয়, অন্যান্য ওভারল্যাপ করা স্টাইল অপরিবর্তিত রাখে।

fun toggleBold(state: TextFieldState) {
    state.edit {
        if (selection.collapsed) return@edit
        // Query all bold spans that overlap with the current selection
        val boldStyles: List<TrackedRange<SpanStyle>> = getSpanStyles(selection).filter {
            it.spanStyle.fontWeight == FontWeight.Bold
        }
        if (boldStyles.isNotEmpty()) {
            // Remove or trim existing bold spans covering the selection
            for (style in boldStyles) {
                val range = style.textRange
                if (range.start >= selection.min && range.end <= selection.max) {
                    removeStyle(style)
                } else if (range.start < selection.min && range.end > selection.max) {
                    val oldEnd = range.end
                    style.textRange = TextRange(range.start, selection.min)
                    addStyle(
                        spanStyle = SpanStyle(fontWeight = FontWeight.Bold),
                        range = TextRange(selection.max, oldEnd),
                        expandPolicy = ExpandPolicy.AtEnd
                    )
                } else if (range.start < selection.min) {
                    style.textRange = TextRange(range.start, selection.min)
                } else {
                    style.textRange = TextRange(selection.max, range.end)
                }
            }
        } else {
            // No bold spans intersect the selection - apply bold
            addStyle(
                spanStyle = SpanStyle(fontWeight = FontWeight.Bold),
                range = selection,
                expandPolicy = ExpandPolicy.AtEnd
            )
        }
    }
}

বেছে নেওয়া শব্দের জন্য বোল্ড ফর্ম্যাটিং চালু ও বন্ধ করার TextField।
ছবি ৩. টেক্সট বোল্ড করা ও বোল্ড না করার মধ্যে টগল করা।

রিচ টেক্সট এক্সট্র্যাক্ট বা সিরিয়ালাইজ করুন

শুধু-পড়া-যায় এমন Text কম্পোজেবলে ফর্ম্যাট করা ইনপুট দেখাতে অথবা ব্যাকএন্ড পরিষেবায় এটি সিরিয়ালাইজ করতে, এডিট ব্লকের বাইরে TextFieldState.textStyles পড়ুন।

state.textStyles.getSpanStyles এবং state.textStyles.getParagraphStyles, List<AnnotatedString.Range<SpanStyle>> এবং List<AnnotatedString.Range<ParagraphStyle>> রিটার্ন করে, যা আপনি সরাসরি AnnotatedString কনস্ট্রাক্টরে পাস করতে পারবেন:

fun TextFieldState.toAnnotatedString(): AnnotatedString {
    val fullRange = TextRange(0, text.length)
    return AnnotatedString(
        text = text.toString(),
        spanStyles = textStyles.getSpanStyles(fullRange),
        paragraphStyles = textStyles.getParagraphStyles(fullRange)
    )
}

Jetchat-এ সম্পূর্ণ উদাহরণ

ইনলাইন ক্যারেক্টার স্টাইলিং (বোল্ড, ইটালিক, আন্ডারলাইন, স্ট্রাইকথ্রু ও রঙ) একত্রিত করে এমন একটি সম্পূর্ণ প্রয়োগ দেখতে, GitHub-এ Jetchat স্যাম্পেল দেখুন:

  • RichTextState.kt: TextFieldState স্টাইল কোয়েরি, স্প্যান টগল করা, ট্রিম করা বা বেছে নেওয়া সীমার মধ্যে স্প্লিট করা এবং আরও অনেক কিছু এনক্যাপসুলেট করে।
  • UserInput.kt: RichTextState-কে রিচ টেক্সট ফর্ম্যাটিং টুলবার ও মেসেজ ইনপুট ফিল্ডের সাথে কানেক্ট করে।
Jetchat স্যাম্পেল অ্যাপ মেসেজ ইনপুট বারে রিচ টেক্সট ফর্ম্যাটিং প্রদর্শন করছে।
ছবি ৪. Jetchat-এ রিচ টেক্সট এডিটিং।