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