Skip to main content

ftui_text/
segment.rs

1#![forbid(unsafe_code)]
2
3//! Segment system for styled text units.
4//!
5//! A Segment is the atomic unit of styled text that can be:
6//! - Cheaply borrowed (`Cow<str>`) for string literals / static content
7//! - Split at **cell positions** (not byte positions) for correct wrapping
8//!
9//! Segments bridge higher-level text/layout systems to the render pipeline.
10//!
11//! # Example
12//! ```
13//! use ftui_text::Segment;
14//! use ftui_style::Style;
15//!
16//! // Static text (zero-copy)
17//! let seg = Segment::text("Hello, world!");
18//! assert_eq!(seg.cell_length(), 13);
19//!
20//! // Styled text
21//! let styled = Segment::styled("Error!", Style::new().bold());
22//!
23//! // Split at cell position
24//! let (left, right) = seg.split_at_cell(5);
25//! assert_eq!(left.as_str(), "Hello");
26//! assert_eq!(right.as_str(), ", world!");
27//! ```
28
29use crate::grapheme_width;
30use ftui_style::Style;
31use smallvec::SmallVec;
32use std::borrow::Cow;
33use unicode_segmentation::UnicodeSegmentation;
34
35/// Control codes that can be carried by a segment.
36///
37/// Control segments do not consume display width and are used for
38/// non-textual actions like cursor movement or clearing.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub enum ControlCode {
41    /// Carriage return (move to start of line)
42    CarriageReturn,
43    /// Line feed (move to next line)
44    LineFeed,
45    /// Bell (audible alert)
46    Bell,
47    /// Backspace
48    Backspace,
49    /// Tab
50    Tab,
51    /// Home (move to start of line, used in some contexts)
52    Home,
53    /// Clear to end of line
54    ClearToEndOfLine,
55    /// Clear line
56    ClearLine,
57}
58
59impl ControlCode {
60    /// Whether this control code should cause a line break.
61    #[inline]
62    #[must_use]
63    pub const fn is_newline(&self) -> bool {
64        matches!(self, Self::LineFeed)
65    }
66
67    /// Whether this control code is a carriage return.
68    #[inline]
69    #[must_use]
70    pub const fn is_cr(&self) -> bool {
71        matches!(self, Self::CarriageReturn)
72    }
73}
74
75/// A segment of styled text.
76///
77/// Segments are the atomic units of text rendering. They can contain:
78/// - Regular text with optional styling
79/// - Control codes for non-textual actions
80///
81/// Text is stored as `Cow<str>` to allow zero-copy for static strings
82/// while still supporting owned data when needed.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct Segment<'a> {
85    /// The text content (may be empty for control-only segments).
86    pub text: Cow<'a, str>,
87    /// Optional style applied to this segment.
88    pub style: Option<Style>,
89    /// Optional control codes (stack-allocated for common cases).
90    pub control: Option<SmallVec<[ControlCode; 2]>>,
91}
92
93impl<'a> Segment<'a> {
94    /// Create a new text segment without styling.
95    #[inline]
96    #[must_use]
97    pub fn text(s: impl Into<Cow<'a, str>>) -> Self {
98        Self {
99            text: s.into(),
100            style: None,
101            control: None,
102        }
103    }
104
105    /// Create a new styled text segment.
106    #[inline]
107    #[must_use]
108    pub fn styled(s: impl Into<Cow<'a, str>>, style: Style) -> Self {
109        Self {
110            text: s.into(),
111            style: Some(style),
112            control: None,
113        }
114    }
115
116    /// Create a control segment (no text, just control codes).
117    #[inline]
118    #[must_use]
119    pub fn control(code: ControlCode) -> Self {
120        let mut codes = SmallVec::new();
121        codes.push(code);
122        Self {
123            text: Cow::Borrowed(""),
124            style: None,
125            control: Some(codes),
126        }
127    }
128
129    /// Create a newline segment.
130    #[inline]
131    #[must_use]
132    pub fn newline() -> Self {
133        Self::control(ControlCode::LineFeed)
134    }
135
136    /// Create an empty segment.
137    #[inline]
138    #[must_use]
139    pub const fn empty() -> Self {
140        Self {
141            text: Cow::Borrowed(""),
142            style: None,
143            control: None,
144        }
145    }
146
147    /// Get the text as a string slice.
148    #[inline]
149    #[must_use]
150    pub fn as_str(&self) -> &str {
151        &self.text
152    }
153
154    /// Check if this segment is empty (no text and no control codes).
155    #[inline]
156    #[must_use]
157    pub fn is_empty(&self) -> bool {
158        self.text.is_empty() && self.control.is_none()
159    }
160
161    /// Check if this segment has text content.
162    #[inline]
163    #[must_use]
164    pub fn has_text(&self) -> bool {
165        !self.text.is_empty()
166    }
167
168    /// Check if this is a control-only segment.
169    #[inline]
170    #[must_use]
171    pub fn is_control(&self) -> bool {
172        self.control.is_some() && self.text.is_empty()
173    }
174
175    /// Check if this segment contains a newline control code.
176    #[inline]
177    #[must_use]
178    pub fn is_newline(&self) -> bool {
179        self.control
180            .as_ref()
181            .is_some_and(|codes| codes.iter().any(|c| c.is_newline()))
182    }
183
184    /// Get the display width in terminal cells.
185    ///
186    /// Control segments have zero width.
187    /// Text width is calculated using Unicode width rules.
188    #[inline]
189    #[must_use]
190    pub fn cell_length(&self) -> usize {
191        if self.is_control() {
192            return 0;
193        }
194        crate::display_width(&self.text)
195    }
196
197    /// Calculate cell length with a specific width function.
198    ///
199    /// This allows custom width calculations (e.g., for testing or
200    /// terminal-specific behavior).
201    #[inline]
202    #[must_use]
203    pub fn cell_length_with<F>(&self, width_fn: F) -> usize
204    where
205        F: Fn(&str) -> usize,
206    {
207        if self.is_control() {
208            return 0;
209        }
210        width_fn(&self.text)
211    }
212
213    /// Split the segment at a cell position.
214    ///
215    /// Returns `(left, right)` where:
216    /// - `left` contains content up to (but not exceeding) `cell_pos` cells
217    /// - `right` contains the remaining content
218    ///
219    /// The split respects grapheme cluster boundaries to avoid breaking
220    /// emoji, combining characters, or other complex graphemes.
221    ///
222    /// # Panics
223    /// Does not panic; if `cell_pos` is beyond the segment length,
224    /// returns `(self, empty)`.
225    #[must_use]
226    pub fn split_at_cell(&self, cell_pos: usize) -> (Self, Self) {
227        // Control segments cannot be split
228        if self.is_control() {
229            if cell_pos == 0 {
230                return (Self::empty(), self.clone());
231            }
232            return (self.clone(), Self::empty());
233        }
234
235        // Empty text
236        if self.text.is_empty() || cell_pos == 0 {
237            return (
238                Self {
239                    text: Cow::Borrowed(""),
240                    style: self.style,
241                    control: None,
242                },
243                self.clone(),
244            );
245        }
246
247        let total_width = self.cell_length();
248        if cell_pos >= total_width {
249            return (
250                self.clone(),
251                Self {
252                    text: Cow::Borrowed(""),
253                    style: self.style,
254                    control: None,
255                },
256            );
257        }
258
259        // Find the byte position that corresponds to the cell position
260        let (byte_pos, _actual_width) = find_cell_boundary(&self.text, cell_pos);
261
262        let left_text = &self.text[..byte_pos];
263        let right_text = &self.text[byte_pos..];
264
265        (
266            Self {
267                text: Cow::Owned(left_text.to_string()),
268                style: self.style,
269                control: None,
270            },
271            Self {
272                text: Cow::Owned(right_text.to_string()),
273                style: self.style,
274                control: None,
275            },
276        )
277    }
278
279    /// Apply a style to this segment.
280    #[inline]
281    #[must_use]
282    pub fn with_style(mut self, style: Style) -> Self {
283        self.style = Some(style);
284        self
285    }
286
287    /// Convert to an owned segment (no lifetime constraints).
288    #[must_use]
289    pub fn into_owned(self) -> Segment<'static> {
290        Segment {
291            text: Cow::Owned(self.text.into_owned()),
292            style: self.style,
293            control: self.control,
294        }
295    }
296
297    /// Add a control code to this segment.
298    #[must_use]
299    pub fn with_control(mut self, code: ControlCode) -> Self {
300        if let Some(ref mut codes) = self.control {
301            codes.push(code);
302        } else {
303            let mut codes = SmallVec::new();
304            codes.push(code);
305            self.control = Some(codes);
306        }
307        self
308    }
309}
310
311impl<'a> Default for Segment<'a> {
312    fn default() -> Self {
313        Self::empty()
314    }
315}
316
317impl<'a> From<&'a str> for Segment<'a> {
318    fn from(s: &'a str) -> Self {
319        Self::text(s)
320    }
321}
322
323impl From<String> for Segment<'static> {
324    fn from(s: String) -> Self {
325        Self::text(s)
326    }
327}
328
329/// Find the byte position that corresponds to a cell position.
330///
331/// Returns `(byte_pos, actual_cell_width)` where `actual_cell_width`
332/// is the width up to `byte_pos` (may be less than target if we can't
333/// reach it exactly without breaking a grapheme).
334pub fn find_cell_boundary(text: &str, target_cells: usize) -> (usize, usize) {
335    let mut current_cells = 0;
336    let mut byte_pos = 0;
337
338    for grapheme in text.graphemes(true) {
339        let grapheme_width = grapheme_width(grapheme);
340
341        // Check if adding this grapheme would exceed the target
342        if current_cells + grapheme_width > target_cells {
343            // Stop before this grapheme
344            break;
345        }
346
347        current_cells += grapheme_width;
348        byte_pos += grapheme.len();
349
350        if current_cells >= target_cells {
351            break;
352        }
353    }
354
355    (byte_pos, current_cells)
356}
357
358/// A line of segments.
359///
360/// Represents a single line of text that may contain multiple styled segments.
361#[derive(Debug, Clone, Default, PartialEq, Eq)]
362pub struct SegmentLine<'a> {
363    segments: Vec<Segment<'a>>,
364}
365
366impl<'a> SegmentLine<'a> {
367    /// Create an empty line.
368    #[inline]
369    #[must_use]
370    pub const fn new() -> Self {
371        Self {
372            segments: Vec::new(),
373        }
374    }
375
376    /// Create a line from a vector of segments.
377    #[inline]
378    #[must_use]
379    pub fn from_segments(segments: Vec<Segment<'a>>) -> Self {
380        Self { segments }
381    }
382
383    /// Create a line from a single segment.
384    #[inline]
385    #[must_use]
386    pub fn from_segment(segment: Segment<'a>) -> Self {
387        Self {
388            segments: vec![segment],
389        }
390    }
391
392    /// Check if the line is empty.
393    #[inline]
394    #[must_use]
395    pub fn is_empty(&self) -> bool {
396        self.segments.is_empty() || self.segments.iter().all(|s| s.is_empty())
397    }
398
399    /// Get the number of segments.
400    #[inline]
401    #[must_use]
402    pub fn len(&self) -> usize {
403        self.segments.len()
404    }
405
406    /// Get the total cell width of the line.
407    #[must_use]
408    pub fn cell_length(&self) -> usize {
409        self.segments.iter().map(|s| s.cell_length()).sum()
410    }
411
412    /// Add a segment to the end of the line.
413    #[inline]
414    pub fn push(&mut self, segment: Segment<'a>) {
415        self.segments.push(segment);
416    }
417
418    /// Get the segments as a slice.
419    #[inline]
420    #[must_use]
421    pub fn segments(&self) -> &[Segment<'a>] {
422        &self.segments
423    }
424
425    /// Get mutable access to segments.
426    #[inline]
427    pub fn segments_mut(&mut self) -> &mut Vec<Segment<'a>> {
428        &mut self.segments
429    }
430
431    /// Iterate over segments.
432    #[inline]
433    pub fn iter(&self) -> impl Iterator<Item = &Segment<'a>> {
434        self.segments.iter()
435    }
436
437    /// Split the line at a cell position.
438    ///
439    /// Returns `(left, right)` where the split respects grapheme boundaries.
440    #[must_use]
441    pub fn split_at_cell(&self, cell_pos: usize) -> (Self, Self) {
442        if cell_pos == 0 {
443            return (Self::new(), self.clone());
444        }
445
446        let total_width = self.cell_length();
447        if cell_pos >= total_width {
448            return (self.clone(), Self::new());
449        }
450
451        let mut left_segments = Vec::new();
452        let mut right_segments = Vec::new();
453        let mut consumed = 0;
454        let mut found_split = false;
455
456        for segment in &self.segments {
457            if found_split {
458                right_segments.push(segment.clone());
459                continue;
460            }
461
462            let seg_width = segment.cell_length();
463            if consumed + seg_width <= cell_pos {
464                // Entire segment goes to left
465                left_segments.push(segment.clone());
466                consumed += seg_width;
467            } else if consumed >= cell_pos {
468                // Entire segment goes to right
469                right_segments.push(segment.clone());
470                found_split = true;
471            } else {
472                // Need to split this segment
473                let split_at = cell_pos - consumed;
474                let (left, right) = segment.split_at_cell(split_at);
475                if left.has_text() {
476                    left_segments.push(left);
477                }
478                if right.has_text() {
479                    right_segments.push(right);
480                }
481                found_split = true;
482            }
483        }
484
485        (
486            Self::from_segments(left_segments),
487            Self::from_segments(right_segments),
488        )
489    }
490
491    /// Concatenate plain text from all segments.
492    #[must_use]
493    pub fn to_plain_text(&self) -> String {
494        self.segments.iter().map(|s| s.as_str()).collect()
495    }
496
497    /// Convert all segments to owned (remove lifetime constraints).
498    #[must_use]
499    pub fn into_owned(self) -> SegmentLine<'static> {
500        SegmentLine {
501            segments: self.segments.into_iter().map(|s| s.into_owned()).collect(),
502        }
503    }
504}
505
506impl<'a> IntoIterator for SegmentLine<'a> {
507    type Item = Segment<'a>;
508    type IntoIter = std::vec::IntoIter<Segment<'a>>;
509
510    fn into_iter(self) -> Self::IntoIter {
511        self.segments.into_iter()
512    }
513}
514
515impl<'a, 'b> IntoIterator for &'b SegmentLine<'a> {
516    type Item = &'b Segment<'a>;
517    type IntoIter = std::slice::Iter<'b, Segment<'a>>;
518
519    fn into_iter(self) -> Self::IntoIter {
520        self.segments.iter()
521    }
522}
523
524/// Collection of lines (multi-line text).
525#[derive(Debug, Clone, Default, PartialEq, Eq)]
526pub struct SegmentLines<'a> {
527    lines: Vec<SegmentLine<'a>>,
528}
529
530impl<'a> SegmentLines<'a> {
531    /// Create empty lines collection.
532    #[inline]
533    #[must_use]
534    pub const fn new() -> Self {
535        Self { lines: Vec::new() }
536    }
537
538    /// Create from a vector of lines.
539    #[inline]
540    #[must_use]
541    pub fn from_lines(lines: Vec<SegmentLine<'a>>) -> Self {
542        Self { lines }
543    }
544
545    /// Check if empty.
546    #[inline]
547    #[must_use]
548    pub fn is_empty(&self) -> bool {
549        self.lines.is_empty()
550    }
551
552    /// Get number of lines.
553    #[inline]
554    #[must_use]
555    pub fn len(&self) -> usize {
556        self.lines.len()
557    }
558
559    /// Add a line.
560    #[inline]
561    pub fn push(&mut self, line: SegmentLine<'a>) {
562        self.lines.push(line);
563    }
564
565    /// Get lines as slice.
566    #[inline]
567    #[must_use]
568    pub fn lines(&self) -> &[SegmentLine<'a>] {
569        &self.lines
570    }
571
572    /// Iterate over lines.
573    #[inline]
574    pub fn iter(&self) -> impl Iterator<Item = &SegmentLine<'a>> {
575        self.lines.iter()
576    }
577
578    /// Get the maximum cell width across all lines.
579    #[must_use]
580    pub fn max_width(&self) -> usize {
581        self.lines
582            .iter()
583            .map(|l| l.cell_length())
584            .max()
585            .unwrap_or(0)
586    }
587
588    /// Convert to owned.
589    #[must_use]
590    pub fn into_owned(self) -> SegmentLines<'static> {
591        SegmentLines {
592            lines: self.lines.into_iter().map(|l| l.into_owned()).collect(),
593        }
594    }
595}
596
597impl<'a> IntoIterator for SegmentLines<'a> {
598    type Item = SegmentLine<'a>;
599    type IntoIter = std::vec::IntoIter<SegmentLine<'a>>;
600
601    fn into_iter(self) -> Self::IntoIter {
602        self.lines.into_iter()
603    }
604}
605
606/// Split segments by newlines into lines.
607///
608/// This function processes a sequence of segments and splits them into
609/// separate lines whenever a newline control code is encountered.
610///
611/// A newline creates a new line, so "a\nb" becomes ["a", "b"] (2 lines),
612/// and "\n" becomes ["", ""] (2 empty lines).
613#[must_use]
614pub fn split_into_lines<'a>(segments: impl IntoIterator<Item = Segment<'a>>) -> SegmentLines<'a> {
615    let mut lines = SegmentLines::new();
616    let mut current_line = SegmentLine::new();
617    let mut has_content = false;
618
619    for segment in segments {
620        has_content = true;
621        if segment.is_newline() {
622            lines.push(std::mem::take(&mut current_line));
623        } else if segment.has_text() {
624            // Check if text contains literal newlines
625            let text = segment.as_str();
626            if text.contains('\n') {
627                // Split on newlines within the text
628                let parts: Vec<&str> = text.split('\n').collect();
629                for (i, part) in parts.iter().enumerate() {
630                    if !part.is_empty() {
631                        current_line.push(Segment {
632                            text: Cow::Owned((*part).to_string()),
633                            style: segment.style,
634                            control: None,
635                        });
636                    }
637                    // Push line after each newline (but not after the last part)
638                    if i < parts.len() - 1 {
639                        lines.push(std::mem::take(&mut current_line));
640                    }
641                }
642            } else {
643                current_line.push(segment);
644            }
645        } else if !segment.is_empty() {
646            current_line.push(segment);
647        }
648    }
649
650    // Always push the final line (even if empty, it represents content after last newline)
651    // Only exception: if we had no segments at all, push one empty line
652    if has_content || lines.is_empty() {
653        lines.push(current_line);
654    }
655
656    lines
657}
658
659/// Join lines into a flat sequence of segments with newlines between.
660pub fn join_lines<'a>(lines: &SegmentLines<'a>) -> Vec<Segment<'a>> {
661    let mut result = Vec::new();
662    let line_count = lines.len();
663
664    for (i, line) in lines.iter().enumerate() {
665        for segment in line.iter() {
666            result.push(segment.clone());
667        }
668        if i < line_count - 1 {
669            result.push(Segment::newline());
670        }
671    }
672
673    result
674}
675
676#[cfg(test)]
677mod tests {
678    use super::*;
679
680    // ==========================================================================
681    // Basic Segment tests
682    // ==========================================================================
683
684    #[test]
685    fn segment_text_creates_unstyled_segment() {
686        let seg = Segment::text("hello");
687        assert_eq!(seg.as_str(), "hello");
688        assert!(seg.style.is_none());
689        assert!(seg.control.is_none());
690    }
691
692    #[test]
693    fn segment_styled_creates_styled_segment() {
694        let style = Style::new().bold();
695        let seg = Segment::styled("hello", style);
696        assert_eq!(seg.as_str(), "hello");
697        assert_eq!(seg.style, Some(style));
698    }
699
700    #[test]
701    fn segment_control_creates_control_segment() {
702        let seg = Segment::control(ControlCode::LineFeed);
703        assert!(seg.is_control());
704        assert!(seg.is_newline());
705        assert_eq!(seg.cell_length(), 0);
706    }
707
708    #[test]
709    fn segment_empty_is_empty() {
710        let seg = Segment::empty();
711        assert!(seg.is_empty());
712        assert!(!seg.has_text());
713        assert_eq!(seg.cell_length(), 0);
714    }
715
716    // ==========================================================================
717    // cell_length tests
718    // ==========================================================================
719
720    #[test]
721    fn cell_length_ascii() {
722        let seg = Segment::text("hello");
723        assert_eq!(seg.cell_length(), 5);
724    }
725
726    #[test]
727    fn cell_length_cjk() {
728        // CJK characters are typically 2 cells wide
729        let seg = Segment::text("你好");
730        assert_eq!(seg.cell_length(), 4); // 2 chars * 2 cells each
731    }
732
733    #[test]
734    fn cell_length_mixed() {
735        let seg = Segment::text("hi你好");
736        assert_eq!(seg.cell_length(), 6); // 2 ASCII + 4 CJK
737    }
738
739    #[test]
740    fn cell_length_emoji() {
741        // Basic emoji (varies by terminal, but unicode-width treats it as 2)
742        let seg = Segment::text("😀");
743        // unicode-width may return 2 for emoji
744        assert!(seg.cell_length() >= 1);
745    }
746
747    #[test]
748    fn cell_length_zwj_sequence() {
749        // Family emoji (ZWJ sequence)
750        let seg = Segment::text("👨‍👩‍👧");
751        // This is a complex case - just ensure it doesn't panic
752        let _width = seg.cell_length();
753    }
754
755    #[test]
756    fn cell_length_control_is_zero() {
757        let seg = Segment::control(ControlCode::Bell);
758        assert_eq!(seg.cell_length(), 0);
759    }
760
761    // ==========================================================================
762    // split_at_cell tests
763    // ==========================================================================
764
765    #[test]
766    fn split_at_cell_ascii() {
767        let seg = Segment::text("hello world");
768        let (left, right) = seg.split_at_cell(5);
769        assert_eq!(left.as_str(), "hello");
770        assert_eq!(right.as_str(), " world");
771    }
772
773    #[test]
774    fn split_at_cell_zero() {
775        let seg = Segment::text("hello");
776        let (left, right) = seg.split_at_cell(0);
777        assert_eq!(left.as_str(), "");
778        assert_eq!(right.as_str(), "hello");
779    }
780
781    #[test]
782    fn split_at_cell_beyond_length() {
783        let seg = Segment::text("hi");
784        let (left, right) = seg.split_at_cell(10);
785        assert_eq!(left.as_str(), "hi");
786        assert_eq!(right.as_str(), "");
787    }
788
789    #[test]
790    fn split_at_cell_cjk() {
791        // Each CJK char is 2 cells
792        let seg = Segment::text("你好世界");
793        let (left, right) = seg.split_at_cell(2);
794        assert_eq!(left.as_str(), "你");
795        assert_eq!(right.as_str(), "好世界");
796    }
797
798    #[test]
799    fn split_at_cell_cjk_mid_char() {
800        // Try to split at cell 1 (middle of a 2-cell char)
801        // Should not break the char, so left should be empty
802        let seg = Segment::text("你好");
803        let (left, right) = seg.split_at_cell(1);
804        // Can't include half a character, so left is empty
805        assert_eq!(left.as_str(), "");
806        assert_eq!(right.as_str(), "你好");
807    }
808
809    #[test]
810    fn split_at_cell_mixed() {
811        let seg = Segment::text("hi你");
812        let (left, right) = seg.split_at_cell(2);
813        assert_eq!(left.as_str(), "hi");
814        assert_eq!(right.as_str(), "你");
815    }
816
817    #[test]
818    fn split_at_cell_preserves_style() {
819        let style = Style::new().bold();
820        let seg = Segment::styled("hello", style);
821        let (left, right) = seg.split_at_cell(2);
822        assert_eq!(left.style, Some(style));
823        assert_eq!(right.style, Some(style));
824    }
825
826    #[test]
827    fn split_at_cell_control_segment() {
828        let seg = Segment::control(ControlCode::LineFeed);
829        let (left, right) = seg.split_at_cell(0);
830        assert!(left.is_empty());
831        assert!(right.is_control());
832    }
833
834    // ==========================================================================
835    // SegmentLine tests
836    // ==========================================================================
837
838    #[test]
839    fn segment_line_cell_length() {
840        let mut line = SegmentLine::new();
841        line.push(Segment::text("hello "));
842        line.push(Segment::text("world"));
843        assert_eq!(line.cell_length(), 11);
844    }
845
846    #[test]
847    fn segment_line_split_at_cell() {
848        let mut line = SegmentLine::new();
849        line.push(Segment::text("hello "));
850        line.push(Segment::text("world"));
851
852        let (left, right) = line.split_at_cell(8);
853        assert_eq!(left.to_plain_text(), "hello wo");
854        assert_eq!(right.to_plain_text(), "rld");
855    }
856
857    #[test]
858    fn segment_line_split_at_segment_boundary() {
859        let mut line = SegmentLine::new();
860        line.push(Segment::text("hello"));
861        line.push(Segment::text(" world"));
862
863        let (left, right) = line.split_at_cell(5);
864        assert_eq!(left.to_plain_text(), "hello");
865        assert_eq!(right.to_plain_text(), " world");
866    }
867
868    // ==========================================================================
869    // Line splitting tests
870    // ==========================================================================
871
872    #[test]
873    fn split_into_lines_single_line() {
874        let segments = vec![Segment::text("hello world")];
875        let lines = split_into_lines(segments);
876        assert_eq!(lines.len(), 1);
877        assert_eq!(lines.lines()[0].to_plain_text(), "hello world");
878    }
879
880    #[test]
881    fn split_into_lines_with_newline_control() {
882        let segments = vec![
883            Segment::text("line one"),
884            Segment::newline(),
885            Segment::text("line two"),
886        ];
887        let lines = split_into_lines(segments);
888        assert_eq!(lines.len(), 2);
889        assert_eq!(lines.lines()[0].to_plain_text(), "line one");
890        assert_eq!(lines.lines()[1].to_plain_text(), "line two");
891    }
892
893    #[test]
894    fn split_into_lines_with_embedded_newline() {
895        let segments = vec![Segment::text("line one\nline two")];
896        let lines = split_into_lines(segments);
897        assert_eq!(lines.len(), 2);
898        assert_eq!(lines.lines()[0].to_plain_text(), "line one");
899        assert_eq!(lines.lines()[1].to_plain_text(), "line two");
900    }
901
902    #[test]
903    fn split_into_lines_empty_input() {
904        let segments: Vec<Segment> = vec![];
905        let lines = split_into_lines(segments);
906        assert_eq!(lines.len(), 1); // One empty line
907        assert!(lines.lines()[0].is_empty());
908    }
909
910    // ==========================================================================
911    // Join lines test
912    // ==========================================================================
913
914    #[test]
915    fn join_lines_roundtrip() {
916        let segments = vec![
917            Segment::text("line one"),
918            Segment::newline(),
919            Segment::text("line two"),
920        ];
921        let lines = split_into_lines(segments);
922        let joined = join_lines(&lines);
923
924        // Should have: "line one", newline, "line two"
925        assert_eq!(joined.len(), 3);
926        assert_eq!(joined[0].as_str(), "line one");
927        assert!(joined[1].is_newline());
928        assert_eq!(joined[2].as_str(), "line two");
929    }
930
931    // ==========================================================================
932    // Ownership tests
933    // ==========================================================================
934
935    #[test]
936    fn segment_into_owned() {
937        let s = String::from("hello");
938        let seg: Segment = Segment::text(&s[..]);
939        let owned: Segment<'static> = seg.into_owned();
940        assert_eq!(owned.as_str(), "hello");
941    }
942
943    #[test]
944    fn segment_from_string() {
945        let seg: Segment<'static> = Segment::from(String::from("hello"));
946        assert_eq!(seg.as_str(), "hello");
947    }
948
949    #[test]
950    fn segment_from_str() {
951        let seg: Segment = Segment::from("hello");
952        assert_eq!(seg.as_str(), "hello");
953    }
954
955    // ==========================================================================
956    // Control code tests
957    // ==========================================================================
958
959    #[test]
960    fn control_code_is_newline() {
961        assert!(ControlCode::LineFeed.is_newline());
962        assert!(!ControlCode::CarriageReturn.is_newline());
963        assert!(!ControlCode::Bell.is_newline());
964    }
965
966    #[test]
967    fn control_code_is_cr() {
968        assert!(ControlCode::CarriageReturn.is_cr());
969        assert!(!ControlCode::LineFeed.is_cr());
970    }
971
972    #[test]
973    fn segment_with_control() {
974        let seg = Segment::text("hello").with_control(ControlCode::Bell);
975        assert!(seg.control.is_some());
976        assert_eq!(seg.control.as_ref().unwrap().len(), 1);
977    }
978
979    // ==========================================================================
980    // Edge cases
981    // ==========================================================================
982
983    #[test]
984    fn split_empty_segment() {
985        let seg = Segment::text("");
986        let (left, right) = seg.split_at_cell(5);
987        assert_eq!(left.as_str(), "");
988        assert_eq!(right.as_str(), "");
989    }
990
991    #[test]
992    fn combining_characters() {
993        // e followed by combining acute accent
994        let seg = Segment::text("e\u{0301}"); // é as two code points
995        // Should be treated as single grapheme, 1 cell wide
996        let width = seg.cell_length();
997        assert!(width >= 1);
998
999        // Split should keep the grapheme together
1000        let (left, right) = seg.split_at_cell(1);
1001        assert_eq!(left.cell_length() + right.cell_length(), width);
1002    }
1003
1004    #[test]
1005    fn segment_line_is_empty() {
1006        let line = SegmentLine::new();
1007        assert!(line.is_empty());
1008
1009        let mut line2 = SegmentLine::new();
1010        line2.push(Segment::empty());
1011        assert!(line2.is_empty());
1012
1013        let mut line3 = SegmentLine::new();
1014        line3.push(Segment::text("x"));
1015        assert!(!line3.is_empty());
1016    }
1017
1018    // ==========================================================================
1019    // Cow<str> ownership behavior tests
1020    // ==========================================================================
1021
1022    #[test]
1023    fn cow_borrowed_from_static_str() {
1024        let seg = Segment::text("static string");
1025        // Cow should be Borrowed for static strings
1026        assert!(matches!(seg.text, Cow::Borrowed(_)));
1027    }
1028
1029    #[test]
1030    fn cow_owned_from_string() {
1031        let owned = String::from("owned string");
1032        let seg = Segment::text(owned);
1033        // Cow should be Owned for String
1034        assert!(matches!(seg.text, Cow::Owned(_)));
1035    }
1036
1037    #[test]
1038    fn cow_borrowed_reference() {
1039        let s = String::from("reference");
1040        let seg = Segment::text(&s[..]);
1041        // Cow should be Borrowed when created from &str
1042        assert!(matches!(seg.text, Cow::Borrowed(_)));
1043    }
1044
1045    #[test]
1046    fn into_owned_converts_borrowed_to_owned() {
1047        let seg = Segment::text("borrowed");
1048        assert!(matches!(seg.text, Cow::Borrowed(_)));
1049
1050        let owned = seg.into_owned();
1051        // After into_owned, text should be Owned
1052        assert!(matches!(owned.text, Cow::Owned(_)));
1053        assert_eq!(owned.as_str(), "borrowed");
1054    }
1055
1056    #[test]
1057    fn clone_borrowed_segment_stays_borrowed() {
1058        let seg = Segment::text("static");
1059        let cloned = seg.clone();
1060        // Clone of borrowed should still be borrowed
1061        assert!(matches!(cloned.text, Cow::Borrowed(_)));
1062    }
1063
1064    #[test]
1065    fn clone_owned_segment_allocates() {
1066        let owned = String::from("owned");
1067        let seg = Segment::text(owned);
1068        let cloned = seg.clone();
1069        // Clone of owned allocates
1070        assert!(matches!(cloned.text, Cow::Owned(_)));
1071        assert_eq!(cloned.as_str(), "owned");
1072    }
1073
1074    // ==========================================================================
1075    // Default trait tests
1076    // ==========================================================================
1077
1078    #[test]
1079    fn segment_default_is_empty() {
1080        let seg = Segment::default();
1081        assert!(seg.is_empty());
1082        assert_eq!(seg.as_str(), "");
1083        assert!(seg.style.is_none());
1084        assert!(seg.control.is_none());
1085    }
1086
1087    #[test]
1088    fn segment_line_default_is_empty() {
1089        let line = SegmentLine::default();
1090        assert!(line.is_empty());
1091        assert_eq!(line.len(), 0);
1092    }
1093
1094    #[test]
1095    fn segment_lines_default_is_empty() {
1096        let lines = SegmentLines::default();
1097        assert!(lines.is_empty());
1098        assert_eq!(lines.len(), 0);
1099    }
1100
1101    // ==========================================================================
1102    // Debug trait tests
1103    // ==========================================================================
1104
1105    #[test]
1106    fn segment_debug_impl() {
1107        let seg = Segment::text("hello");
1108        let debug = format!("{:?}", seg);
1109        assert!(debug.contains("Segment"));
1110        assert!(debug.contains("hello"));
1111    }
1112
1113    #[test]
1114    fn control_code_debug_impl() {
1115        let code = ControlCode::LineFeed;
1116        let debug = format!("{:?}", code);
1117        assert!(debug.contains("LineFeed"));
1118    }
1119
1120    #[test]
1121    fn segment_line_debug_impl() {
1122        let line = SegmentLine::from_segment(Segment::text("test"));
1123        let debug = format!("{:?}", line);
1124        assert!(debug.contains("SegmentLine"));
1125    }
1126
1127    // ==========================================================================
1128    // SegmentLine additional tests
1129    // ==========================================================================
1130
1131    #[test]
1132    fn segment_line_into_owned() {
1133        let s = String::from("test");
1134        let mut line = SegmentLine::new();
1135        line.push(Segment::text(&s[..]));
1136
1137        let owned = line.into_owned();
1138        drop(s); // Original dropped
1139        assert_eq!(owned.to_plain_text(), "test");
1140    }
1141
1142    #[test]
1143    fn segment_line_segments_mut() {
1144        let mut line = SegmentLine::from_segment(Segment::text("hello"));
1145        line.segments_mut().push(Segment::text(" world"));
1146        assert_eq!(line.to_plain_text(), "hello world");
1147    }
1148
1149    #[test]
1150    fn segment_line_iter() {
1151        let line = SegmentLine::from_segments(vec![Segment::text("a"), Segment::text("b")]);
1152        let collected: Vec<_> = line.iter().collect();
1153        assert_eq!(collected.len(), 2);
1154    }
1155
1156    #[test]
1157    fn segment_line_into_iter_ref() {
1158        let line = SegmentLine::from_segments(vec![Segment::text("x"), Segment::text("y")]);
1159        let mut count = 0;
1160        for _seg in &line {
1161            count += 1;
1162        }
1163        assert_eq!(count, 2);
1164    }
1165
1166    // ==========================================================================
1167    // SegmentLines additional tests
1168    // ==========================================================================
1169
1170    #[test]
1171    fn segment_lines_into_owned() {
1172        let s = String::from("line one");
1173        let mut lines = SegmentLines::new();
1174        let mut line = SegmentLine::new();
1175        line.push(Segment::text(&s[..]));
1176        lines.push(line);
1177
1178        let owned = lines.into_owned();
1179        drop(s);
1180        assert_eq!(owned.lines()[0].to_plain_text(), "line one");
1181    }
1182
1183    #[test]
1184    fn segment_lines_iter() {
1185        let mut lines = SegmentLines::new();
1186        lines.push(SegmentLine::from_segment(Segment::text("a")));
1187        lines.push(SegmentLine::from_segment(Segment::text("b")));
1188
1189        let collected: Vec<_> = lines.iter().collect();
1190        assert_eq!(collected.len(), 2);
1191    }
1192
1193    #[test]
1194    fn segment_lines_max_width() {
1195        let mut lines = SegmentLines::new();
1196        lines.push(SegmentLine::from_segment(Segment::text("short")));
1197        lines.push(SegmentLine::from_segment(Segment::text("longer line here")));
1198        lines.push(SegmentLine::from_segment(Segment::text("med")));
1199
1200        assert_eq!(lines.max_width(), 16);
1201    }
1202
1203    #[test]
1204    fn segment_lines_max_width_empty() {
1205        let lines = SegmentLines::new();
1206        assert_eq!(lines.max_width(), 0);
1207    }
1208
1209    // ==========================================================================
1210    // Segment builder tests
1211    // ==========================================================================
1212
1213    #[test]
1214    fn segment_with_style_applies_style() {
1215        let style = Style::new().bold();
1216        let seg = Segment::text("hello").with_style(style);
1217        assert_eq!(seg.style, Some(style));
1218    }
1219
1220    #[test]
1221    fn segment_with_multiple_controls() {
1222        let seg = Segment::text("x")
1223            .with_control(ControlCode::Bell)
1224            .with_control(ControlCode::Tab);
1225        let codes = seg.control.unwrap();
1226        assert_eq!(codes.len(), 2);
1227    }
1228
1229    // ==========================================================================
1230    // cell_length_with custom function tests
1231    // ==========================================================================
1232
1233    #[test]
1234    fn cell_length_with_custom_width() {
1235        let seg = Segment::text("hello");
1236        // Custom width function: double each char
1237        let width = seg.cell_length_with(|s| s.len() * 2);
1238        assert_eq!(width, 10);
1239    }
1240
1241    #[test]
1242    fn cell_length_with_on_control_is_zero() {
1243        let seg = Segment::control(ControlCode::Bell);
1244        let width = seg.cell_length_with(|_| 100);
1245        assert_eq!(width, 0);
1246    }
1247
1248    // ==========================================================================
1249    // Control code hash/equality tests
1250    // ==========================================================================
1251
1252    #[test]
1253    fn control_code_equality() {
1254        assert_eq!(ControlCode::LineFeed, ControlCode::LineFeed);
1255        assert_ne!(ControlCode::LineFeed, ControlCode::CarriageReturn);
1256    }
1257
1258    #[test]
1259    fn control_code_hash_consistency() {
1260        use std::collections::HashSet;
1261        let mut set = HashSet::new();
1262        set.insert(ControlCode::LineFeed);
1263        set.insert(ControlCode::CarriageReturn);
1264        assert_eq!(set.len(), 2);
1265        assert!(set.contains(&ControlCode::LineFeed));
1266    }
1267
1268    // ==========================================================================
1269    // Edge cases
1270    // ==========================================================================
1271
1272    #[test]
1273    fn split_at_cell_exact_boundary() {
1274        let seg = Segment::text("abcde");
1275        let (left, right) = seg.split_at_cell(5);
1276        assert_eq!(left.as_str(), "abcde");
1277        assert_eq!(right.as_str(), "");
1278    }
1279
1280    #[test]
1281    fn segment_line_split_at_zero() {
1282        let line = SegmentLine::from_segment(Segment::text("hello"));
1283        let (left, right) = line.split_at_cell(0);
1284        assert!(left.is_empty());
1285        assert_eq!(right.to_plain_text(), "hello");
1286    }
1287
1288    #[test]
1289    fn segment_line_split_at_end() {
1290        let line = SegmentLine::from_segment(Segment::text("hello"));
1291        let (left, right) = line.split_at_cell(100);
1292        assert_eq!(left.to_plain_text(), "hello");
1293        assert!(right.is_empty());
1294    }
1295
1296    #[test]
1297    fn join_lines_single_line() {
1298        let mut lines = SegmentLines::new();
1299        lines.push(SegmentLine::from_segment(Segment::text("only")));
1300        let joined = join_lines(&lines);
1301        assert_eq!(joined.len(), 1);
1302        assert_eq!(joined[0].as_str(), "only");
1303    }
1304
1305    #[test]
1306    fn join_lines_empty() {
1307        let lines = SegmentLines::new();
1308        let joined = join_lines(&lines);
1309        assert!(joined.is_empty());
1310    }
1311
1312    #[test]
1313    fn split_into_lines_multiple_newlines() {
1314        let segments = vec![
1315            Segment::text("a"),
1316            Segment::newline(),
1317            Segment::newline(),
1318            Segment::text("b"),
1319        ];
1320        let lines = split_into_lines(segments);
1321        assert_eq!(lines.len(), 3);
1322        assert_eq!(lines.lines()[0].to_plain_text(), "a");
1323        assert!(lines.lines()[1].is_empty());
1324        assert_eq!(lines.lines()[2].to_plain_text(), "b");
1325    }
1326
1327    #[test]
1328    fn split_into_lines_trailing_newline() {
1329        let segments = vec![Segment::text("hello"), Segment::newline()];
1330        let lines = split_into_lines(segments);
1331        assert_eq!(lines.len(), 2);
1332        assert_eq!(lines.lines()[0].to_plain_text(), "hello");
1333        assert!(lines.lines()[1].is_empty());
1334    }
1335}
1336
1337#[cfg(test)]
1338mod proptests {
1339    use super::*;
1340    use proptest::prelude::*;
1341
1342    proptest! {
1343        #[test]
1344        fn split_preserves_total_width(s in "[a-zA-Z0-9 ]{1,100}", pos in 0usize..200) {
1345            let seg = Segment::text(s);
1346            let total = seg.cell_length();
1347            let (left, right) = seg.split_at_cell(pos);
1348
1349            // Total width should be preserved
1350            prop_assert_eq!(left.cell_length() + right.cell_length(), total);
1351        }
1352
1353        #[test]
1354        fn split_preserves_content(s in "[a-zA-Z0-9 ]{1,100}", pos in 0usize..200) {
1355            let seg = Segment::text(s.clone());
1356            let (left, right) = seg.split_at_cell(pos);
1357
1358            // Concatenating left and right text should give original
1359            let combined = format!("{}{}", left.as_str(), right.as_str());
1360            prop_assert_eq!(combined, s);
1361        }
1362
1363        #[test]
1364        fn cell_length_matches_display_width(s in "[a-zA-Z0-9 ]{1,100}") {
1365            let seg = Segment::text(s.clone());
1366            let expected = crate::display_width(s.as_str());
1367            prop_assert_eq!(seg.cell_length(), expected);
1368        }
1369
1370        #[test]
1371        fn line_split_preserves_total_width(
1372            parts in prop::collection::vec("[a-z]{1,10}", 1..5),
1373            pos in 0usize..100
1374        ) {
1375            let mut line = SegmentLine::new();
1376            for part in &parts {
1377                line.push(Segment::text(part.as_str()));
1378            }
1379
1380            let total = line.cell_length();
1381            let (left, right) = line.split_at_cell(pos);
1382
1383            prop_assert_eq!(left.cell_length() + right.cell_length(), total);
1384        }
1385
1386        #[test]
1387        fn split_into_lines_preserves_content(s in "[a-zA-Z0-9 \n]{1,200}") {
1388            let segments = vec![Segment::text(s.clone())];
1389            let lines = split_into_lines(segments);
1390
1391            // Join all lines with newlines
1392            let mut result = String::new();
1393            for (i, line) in lines.lines().iter().enumerate() {
1394                if i > 0 {
1395                    result.push('\n');
1396                }
1397                result.push_str(&line.to_plain_text());
1398            }
1399
1400            prop_assert_eq!(result, s);
1401        }
1402    }
1403}