1- use std:: cmp:: Ordering ;
2-
3- use core_foundation:: attributed_string:: CFAttributedStringRef ;
1+ use core_foundation:: { attributed_string:: CFAttributedStringRef , base:: CFRange } ;
2+ use core_graphics_types:: geometry:: CGSize ;
43use core_text:: framesetter:: CTFramesetter ;
54use objc2:: rc:: Retained ;
65use objc2_core_foundation:: { CGPoint , CGRect , CGSize as OCGSize } ;
76use objc2_foundation:: { NSMutableAttributedString , NSSize } ;
87use regex:: Regex ;
8+ use std:: { borrow:: Cow , cmp:: Ordering } ;
99use unicode_width:: { UnicodeWidthChar , UnicodeWidthStr } ;
1010
1111#[ derive( Debug , Clone , PartialEq , Copy , Default ) ]
@@ -20,15 +20,11 @@ pub fn estimate_frame_for_text(
2020) -> ( OCGSize , isize ) {
2121 let cf_attr_string = Retained :: as_ptr ( attr_string) as CFAttributedStringRef ;
2222 let framesetter = CTFramesetter :: new_with_attributed_string ( cf_attr_string) ;
23- let ( core_graphics_types:: geometry:: CGSize { width, height } , range) = framesetter
24- . suggest_frame_size_with_constraints (
25- core_foundation:: base:: CFRange {
26- location : 0 ,
27- length : 0 ,
28- } ,
29- std:: ptr:: null ( ) ,
30- core_graphics_types:: geometry:: CGSize :: new ( size. 0 , size. 1 ) ,
31- ) ;
23+ let ( CGSize { width, height } , range) = framesetter. suggest_frame_size_with_constraints (
24+ CFRange :: init ( 0 , 0 ) ,
25+ std:: ptr:: null ( ) ,
26+ CGSize :: new ( size. 0 , size. 1 ) ,
27+ ) ;
3228 ( OCGSize :: new ( width, height) , range. length )
3329}
3430
@@ -290,49 +286,30 @@ pub fn lower_ascii(text: &str) -> String {
290286 any_ascii:: any_ascii ( text) . to_ascii_lowercase ( )
291287}
292288
293- pub fn format_fixed_width ( input : & str , fixed_length : usize ) -> String {
289+ pub fn format_fixed_width ( input : & str , fixed_length : usize ) -> Cow < ' _ , str > {
290+ if input. width ( ) <= fixed_length {
291+ return Cow :: Borrowed ( input) ;
292+ }
293+
294294 if fixed_length == 0 {
295295 return "" . into ( ) ;
296296 }
297- let target_suffix_w = fixed_length - 1 ;
297+
298298 let mut current_width = 0 ;
299299 let mut trunc_byte_idx = input. len ( ) ;
300- let mut trunc_width = 0 ;
301300
302- // Scan backwards: Only inspects up to `fixed_length + 1` worth of characters
303- for ( idx, ch) in input. char_indices ( ) . rev ( ) {
304- let ch_width = ch. width ( ) . unwrap_or ( 1 ) ;
301+ for ch in input. chars ( ) . rev ( ) {
302+ let ch_width = ch. width ( ) . unwrap_or_default ( ) ;
305303 current_width += ch_width;
306-
307- // Track the best byte cut-off point for the suffix
308- if current_width <= target_suffix_w {
309- trunc_byte_idx = idx;
310- trunc_width = current_width;
311- }
312-
313- // Early exit: The moment we know the string is too long, we build the truncation
314- if current_width > fixed_length {
315- let suffix = & input[ trunc_byte_idx..] ;
316- let padding_spaces = target_suffix_w - trunc_width;
317-
318- let mut s = String :: with_capacity ( 1 + padding_spaces + suffix. len ( ) ) ;
319- s. push ( '.' ) ;
320- for _ in 0 ..padding_spaces {
321- s. push ( ' ' ) ;
322- }
323- s. push_str ( suffix) ;
324- return s;
304+ if current_width > fixed_length - 1 {
305+ break ;
325306 }
307+ trunc_byte_idx -= ch. len_utf8 ( ) ;
326308 }
327309
328- // Case: The entire string fits within the layout -> Pad it out
329- let padding_spaces = fixed_length - current_width;
330- let mut s = String :: with_capacity ( input. len ( ) + padding_spaces) ;
331- s. push_str ( input) ;
332- for _ in 0 ..padding_spaces {
333- s. push ( ' ' ) ;
334- }
335- s
310+ let mut s = "." . to_string ( ) ;
311+ s. push_str ( & input[ trunc_byte_idx..] ) ;
312+ Cow :: Owned ( s)
336313}
337314
338315#[ cfg( test) ]
@@ -683,78 +660,74 @@ mod select_range_tests {
683660}
684661
685662#[ cfg( test) ]
686- mod misc_tests {
663+ mod format_str_tests {
687664 use super :: format_fixed_width;
688665
689666 #[ test]
690- fn test_exact_and_padding_ascii ( ) {
691- // Exact match: No padding, no truncation
692- assert_eq ! ( format_fixed_width( "hello" , 5 ) , "hello" ) ;
667+ fn test_empty_and_zero_edges ( ) {
668+ // Empty input should always result in an empty string
669+ assert_eq ! ( format_fixed_width( "" , 5 ) . to_string( ) , "" ) ;
670+ assert_eq ! ( format_fixed_width( "" , 0 ) . to_string( ) , "" ) ;
693671
694- // Needs padding: Short string, padded to the right with spaces
695- assert_eq ! ( format_fixed_width( "abc " , 6 ) , "abc " ) ;
672+ // Fixed length of 0 on non-empty input should return an empty string
673+ assert_eq ! ( format_fixed_width( "hello " , 0 ) . to_string ( ) , "" ) ;
696674 }
697675
698676 #[ test]
699- fn test_truncation_ascii ( ) {
700- // Standard truncation from the left with a single dot
701- // "abcdefghijk" length is 11. Target is 10.
702- // 10 - 1 (for ".") = 9 slots for the suffix ("cdefghijk")
703- assert_eq ! ( format_fixed_width( "abcdefghijk" , 10 ) , ".cdefghijk" ) ;
704-
705- // Tight truncation
706- assert_eq ! ( format_fixed_width( "abcdef" , 4 ) , ".def" ) ;
677+ fn test_ascii_no_truncation ( ) {
678+ assert_eq ! ( format_fixed_width( "hello" , 5 ) . to_string( ) , "hello" ) ;
679+ assert_eq ! ( format_fixed_width( "abc" , 5 ) . to_string( ) , "abc" ) ;
707680 }
708681
709682 #[ test]
710- fn test_empty_and_minimal_lengths ( ) {
711- // Empty string should just turn into pure padding
712- assert_eq ! ( format_fixed_width( "" , 5 ) , " " ) ;
713- assert_eq ! ( format_fixed_width( "" , 0 ) , "" ) ;
683+ fn test_ascii_truncation ( ) {
684+ // Budget = 3. Suffix width = 3 - 1 = 2 ("lo")
685+ assert_eq ! ( format_fixed_width( "hello" , 3 ) . to_string( ) , ".lo" ) ;
714686
715- // Extreme edge case: Requested length is exactly 1 or 2
716- assert_eq ! ( format_fixed_width( "longstring" , 2 ) , ".g" ) ;
717- assert_eq ! ( format_fixed_width( "longstring" , 1 ) , "." ) ;
687+ // Budget = 1. Suffix width = 1 - 1 = 0 ("")
688+ assert_eq ! ( format_fixed_width( "hello" , 1 ) . to_string( ) , "." ) ;
718689 }
719690
720691 #[ test]
721- fn test_unicode_padding ( ) {
722- // "こんにちは" (Konnichiwa) has 5 characters, but a visual width of 10.
723- // Target 12 means it needs exactly 2 trailing spaces.
724- assert_eq ! ( format_fixed_width( "こんにちは" , 12 ) , "こんにちは " ) ;
692+ fn test_cjk_characters ( ) {
693+ // "こんにちは" (Konnichiwa) has a total visual width of 10
694+ let input = "こんにちは" ;
695+
696+ assert_eq ! ( format_fixed_width( input, 10 ) . to_string( ) , "こんにちは" ) ;
697+
698+ // Exact fit truncation: Dot (1) + "に" (2) + "は" (2) = 5
699+ assert_eq ! ( format_fixed_width( input, 5 ) . to_string( ) , ".ちは" ) ;
700+
701+ // Truncation with 1 unit of slack space (next character doesn't fit)
702+ assert_eq ! ( format_fixed_width( input, 6 ) . to_string( ) , ".ちは" ) ;
725703 }
726704
727705 #[ test]
728- fn test_unicode_clean_truncation ( ) {
729- // Target 7 -> Suffix target is 6 (7 - 1).
730- // "にちは" is 3 characters of width 2 each = 6 total width. Fits perfectly.
731- assert_eq ! ( format_fixed_width( "こんにちは" , 7 ) , ".にちは" ) ;
706+ fn test_emojis ( ) {
707+ // "🦀" has a visual width of 2. "🦀🦀🦀" total width = 6.
708+ let input = "🦀🦀🦀" ;
709+
710+ assert_eq ! ( format_fixed_width( input, 6 ) . to_string( ) , "🦀🦀🦀" ) ;
711+
712+ // Truncation: Dot (1) + "🦀" (2) + "🦀" (2) = 5
713+ assert_eq ! ( format_fixed_width( input, 5 ) . to_string( ) , ".🦀🦀" ) ;
732714 }
733715
734716 #[ test]
735- fn test_unicode_mid_character_truncation_alignment ( ) {
736- // CRITICAL EDGE CASE: Truncation hits right in the middle of a wide character.
737- // Target 8 -> Suffix target is 7 (8 - 1).
738- // "にちは" has a width of 6. The next char "ん" has a width of 2 (Total 8).
739- // 8 exceeds our suffix target of 7, so "ん" must be dropped.
740- // This leaves 1 empty visual slot (7 - 6 = 1), which must be filled with a space.
741- assert_eq ! ( format_fixed_width( "こんにちは" , 8 ) , ". にちは" ) ;
742-
743- // Verify total visual width of the output is exactly 8
744- // "." (1) + " " (1) + "に" (2) + "ち" (2) + "は" (2) = 8
745- let result = format_fixed_width ( "こんにちは" , 8 ) ;
746- let actual_width: usize = result
747- . chars ( )
748- . map ( |c| unicode_width:: UnicodeWidthChar :: width ( c) . unwrap_or ( 1 ) )
749- . sum ( ) ;
750- assert_eq ! ( actual_width, 8 ) ;
717+ fn test_mixed_width_strings ( ) {
718+ // "Rust🦀" -> "Rust" (4) + "🦀" (2) = Total width 6
719+ let input = "Rust🦀" ;
720+
721+ assert_eq ! ( format_fixed_width( input, 3 ) . to_string( ) , ".🦀" ) ;
722+ assert_eq ! ( format_fixed_width( input, 5 ) . to_string( ) , ".st🦀" ) ;
751723 }
752724
753725 #[ test]
754- fn test_fallback_width_handling ( ) {
755- // Control characters like '\n' return None from .width()
756- // Your updated logic defaults them to 1 via .unwrap_or(1)
757- // "\n\n" is treated as width 2. Target 5 -> pads with 3 spaces.
758- assert_eq ! ( format_fixed_width( "\n \n " , 5 ) , "\n \n " ) ;
726+ fn test_combining_diacritic_quirk ( ) {
727+ // "xyz" + combining acute accent (\u{301})
728+ let input = "xyz\u{301} " ;
729+
730+ // Accent detaches and glues itself to the ellipsis dot
731+ assert_eq ! ( format_fixed_width( input, 1 ) . to_string( ) , ".\u{301} " ) ;
759732 }
760733}
0 commit comments