1+ //! Box drawing utilities for creating formatted text boxes in the terminal
2+
3+ use colored:: * ;
4+ use crate :: analyzer:: display:: utils:: { visual_width, truncate_to_width} ;
5+
6+ /// Content line for measuring and drawing
7+ #[ derive( Debug , Clone ) ]
8+ struct ContentLine {
9+ label : String ,
10+ value : String ,
11+ label_colored : bool ,
12+ }
13+
14+ impl ContentLine {
15+ fn new ( label : & str , value : & str , label_colored : bool ) -> Self {
16+ Self {
17+ label : label. to_string ( ) ,
18+ value : value. to_string ( ) ,
19+ label_colored,
20+ }
21+ }
22+
23+ fn separator ( ) -> Self {
24+ Self {
25+ label : "SEPARATOR" . to_string ( ) ,
26+ value : String :: new ( ) ,
27+ label_colored : false ,
28+ }
29+ }
30+ }
31+
32+ /// Box drawer that pre-calculates optimal dimensions
33+ pub struct BoxDrawer {
34+ title : String ,
35+ lines : Vec < ContentLine > ,
36+ min_width : usize ,
37+ max_width : usize ,
38+ }
39+
40+ impl BoxDrawer {
41+ pub fn new ( title : & str ) -> Self {
42+ Self {
43+ title : title. to_string ( ) ,
44+ lines : Vec :: new ( ) ,
45+ min_width : 60 ,
46+ max_width : 120 , // Reduced from 150 for better terminal compatibility
47+ }
48+ }
49+
50+ pub fn add_line ( & mut self , label : & str , value : & str , label_colored : bool ) {
51+ self . lines . push ( ContentLine :: new ( label, value, label_colored) ) ;
52+ }
53+
54+ pub fn add_value_only ( & mut self , value : & str ) {
55+ self . lines . push ( ContentLine :: new ( "" , value, false ) ) ;
56+ }
57+
58+ pub fn add_separator ( & mut self ) {
59+ self . lines . push ( ContentLine :: separator ( ) ) ;
60+ }
61+
62+ /// Calculate optimal box width based on content
63+ fn calculate_optimal_width ( & self ) -> usize {
64+ let title_width = visual_width ( & self . title ) + 6 ; // "┌─ " + title + " " + extra padding
65+ let mut max_content_width = 0 ;
66+
67+ // Calculate the actual rendered width for each line
68+ for line in & self . lines {
69+ if line. label == "SEPARATOR" {
70+ continue ;
71+ }
72+
73+ let rendered_width = self . calculate_rendered_line_width ( line) ;
74+ max_content_width = max_content_width. max ( rendered_width) ;
75+ }
76+
77+ // Add reasonable buffer for content
78+ let content_width_with_buffer = max_content_width + 4 ; // More buffer for safety
79+
80+ // Box needs padding: "│ " + content + " │" = content + 4
81+ let needed_width = content_width_with_buffer + 4 ;
82+
83+ // Use the maximum of title width and content width
84+ let optimal_width = title_width. max ( needed_width) . max ( self . min_width ) ;
85+ optimal_width. min ( self . max_width )
86+ }
87+
88+ /// Calculate the actual rendered width of a line as it will appear
89+ fn calculate_rendered_line_width ( & self , line : & ContentLine ) -> usize {
90+ let label_width = visual_width ( & line. label ) ;
91+ let value_width = visual_width ( & line. value ) ;
92+
93+ if !line. label . is_empty ( ) && !line. value . is_empty ( ) {
94+ // Label + value: need space between them
95+ // For colored labels, ensure minimum spacing
96+ let min_label_space = if line. label_colored { 25 } else { label_width } ;
97+ min_label_space + 2 + value_width // 2 spaces minimum between label and value
98+ } else if !line. value . is_empty ( ) {
99+ // Value only
100+ value_width
101+ } else if !line. label . is_empty ( ) {
102+ // Label only
103+ label_width
104+ } else {
105+ // Empty line
106+ 0
107+ }
108+ }
109+
110+ /// Draw the complete box
111+ pub fn draw ( & self ) -> String {
112+ let box_width = self . calculate_optimal_width ( ) ;
113+ let content_width = box_width - 4 ; // Available space for content
114+
115+ let mut output = Vec :: new ( ) ;
116+
117+ // Top border
118+ output. push ( self . draw_top ( box_width) ) ;
119+
120+ // Content lines
121+ for line in & self . lines {
122+ if line. label == "SEPARATOR" {
123+ output. push ( self . draw_separator ( box_width) ) ;
124+ } else if line. label . is_empty ( ) && line. value . is_empty ( ) {
125+ output. push ( self . draw_empty_line ( box_width) ) ;
126+ } else {
127+ output. push ( self . draw_content_line ( line, content_width) ) ;
128+ }
129+ }
130+
131+ // Bottom border
132+ output. push ( self . draw_bottom ( box_width) ) ;
133+
134+ output. join ( "\n " )
135+ }
136+
137+ fn draw_top ( & self , width : usize ) -> String {
138+ let title_colored = self . title . bright_cyan ( ) ;
139+ let title_len = visual_width ( & self . title ) ;
140+
141+ // "┌─ " + title + " " + remaining dashes + "┐"
142+ let prefix_len = 3 ; // "┌─ "
143+ let suffix_len = 1 ; // "┐"
144+ let title_space = 1 ; // space after title
145+
146+ let remaining_space = width - prefix_len - title_len - title_space - suffix_len;
147+
148+ format ! ( "┌─ {} {}┐" ,
149+ title_colored,
150+ "─" . repeat( remaining_space)
151+ )
152+ }
153+
154+ fn draw_bottom ( & self , width : usize ) -> String {
155+ format ! ( "└{}┘" , "─" . repeat( width - 2 ) )
156+ }
157+
158+ fn draw_separator ( & self , width : usize ) -> String {
159+ format ! ( "│ {} │" , "─" . repeat( width - 4 ) . dimmed( ) )
160+ }
161+
162+ fn draw_empty_line ( & self , width : usize ) -> String {
163+ format ! ( "│ {} │" , " " . repeat( width - 4 ) )
164+ }
165+
166+ fn draw_content_line ( & self , line : & ContentLine , content_width : usize ) -> String {
167+ // Format the label with color if needed
168+ let formatted_label = if line. label_colored && !line. label . is_empty ( ) {
169+ line. label . bright_white ( ) . to_string ( )
170+ } else {
171+ line. label . clone ( )
172+ } ;
173+
174+ // Calculate actual display widths (use original label for width)
175+ let label_display_width = visual_width ( & line. label ) ;
176+ let value_display_width = visual_width ( & line. value ) ;
177+
178+ // Build the content
179+ let content = if !line. label . is_empty ( ) && !line. value . is_empty ( ) {
180+ // Both label and value - ensure proper spacing
181+ let min_label_space = if line. label_colored { 25 } else { label_display_width } ;
182+ let label_padding = min_label_space. saturating_sub ( label_display_width) ;
183+ let remaining_space = content_width. saturating_sub ( min_label_space + 2 ) ; // 2 for spacing
184+
185+ if value_display_width <= remaining_space {
186+ // Value fits - right align it
187+ let value_padding = remaining_space. saturating_sub ( value_display_width) ;
188+ format ! ( "{}{:<width$} {}{}" ,
189+ formatted_label,
190+ "" ,
191+ " " . repeat( value_padding) ,
192+ line. value,
193+ width = label_padding
194+ )
195+ } else {
196+ // Value too long - truncate it
197+ let truncated_value = truncate_to_width ( & line. value , remaining_space. saturating_sub ( 3 ) ) ;
198+ format ! ( "{}{:<width$} {}" ,
199+ formatted_label,
200+ "" ,
201+ truncated_value,
202+ width = label_padding
203+ )
204+ }
205+ } else if !line. value . is_empty ( ) {
206+ // Value only - left align
207+ if value_display_width <= content_width {
208+ format ! ( "{:<width$}" , line. value, width = content_width)
209+ } else {
210+ truncate_to_width ( & line. value , content_width)
211+ }
212+ } else if !line. label . is_empty ( ) {
213+ // Label only - left align
214+ if label_display_width <= content_width {
215+ format ! ( "{:<width$}" , formatted_label, width = content_width)
216+ } else {
217+ truncate_to_width ( & formatted_label, content_width)
218+ }
219+ } else {
220+ // Empty line
221+ " " . repeat ( content_width)
222+ } ;
223+
224+ // Ensure final content is exactly the right width
225+ let actual_width = visual_width ( & content) ;
226+ let final_content = if actual_width < content_width {
227+ format ! ( "{}{}" , content, " " . repeat( content_width - actual_width) )
228+ } else if actual_width > content_width {
229+ truncate_to_width ( & content, content_width)
230+ } else {
231+ content
232+ } ;
233+
234+ format ! ( "│ {} │" , final_content)
235+ }
236+ }
0 commit comments