@@ -35,6 +35,9 @@ use crate::bottom_pane::prompt_args::parse_slash_name;
3535use crate :: bottom_pane:: prompt_args:: prompt_argument_names;
3636use crate :: bottom_pane:: prompt_args:: prompt_command_with_arg_placeholders;
3737use crate :: bottom_pane:: prompt_args:: prompt_has_numeric_placeholders;
38+ use crate :: render:: Insets ;
39+ use crate :: render:: RectExt ;
40+ use crate :: render:: renderable:: Renderable ;
3841use crate :: slash_command:: SlashCommand ;
3942use crate :: slash_command:: built_in_slash_commands;
4043use crate :: style:: user_message_style;
@@ -158,24 +161,6 @@ impl ChatComposer {
158161 this
159162 }
160163
161- pub fn desired_height ( & self , width : u16 ) -> u16 {
162- let footer_props = self . footer_props ( ) ;
163- let footer_hint_height = self
164- . custom_footer_height ( )
165- . unwrap_or_else ( || footer_height ( footer_props) ) ;
166- let footer_spacing = Self :: footer_spacing ( footer_hint_height) ;
167- let footer_total_height = footer_hint_height + footer_spacing;
168- const COLS_WITH_MARGIN : u16 = LIVE_PREFIX_COLS + 1 ;
169- self . textarea
170- . desired_height ( width. saturating_sub ( COLS_WITH_MARGIN ) )
171- + 2
172- + match & self . active_popup {
173- ActivePopup :: None => footer_total_height,
174- ActivePopup :: Command ( c) => c. calculate_required_height ( width) ,
175- ActivePopup :: File ( c) => c. calculate_required_height ( ) ,
176- }
177- }
178-
179164 fn layout_areas ( & self , area : Rect ) -> [ Rect ; 3 ] {
180165 let footer_props = self . footer_props ( ) ;
181166 let footer_hint_height = self
@@ -190,18 +175,9 @@ impl ChatComposer {
190175 ActivePopup :: File ( popup) => Constraint :: Max ( popup. calculate_required_height ( ) ) ,
191176 ActivePopup :: None => Constraint :: Max ( footer_total_height) ,
192177 } ;
193- let mut area = area;
194- if area. height > 1 {
195- area. height -= 1 ;
196- area. y += 1 ;
197- }
198178 let [ composer_rect, popup_rect] =
199- Layout :: vertical ( [ Constraint :: Min ( 1 ) , popup_constraint] ) . areas ( area) ;
200- let mut textarea_rect = composer_rect;
201- textarea_rect. width = textarea_rect. width . saturating_sub (
202- LIVE_PREFIX_COLS + 1 , /* keep a one-column right margin for wrapping */
203- ) ;
204- textarea_rect. x = textarea_rect. x . saturating_add ( LIVE_PREFIX_COLS ) ;
179+ Layout :: vertical ( [ Constraint :: Min ( 3 ) , popup_constraint] ) . areas ( area) ;
180+ let textarea_rect = composer_rect. inset ( Insets :: tlbr ( 1 , LIVE_PREFIX_COLS , 1 , 1 ) ) ;
205181 [ composer_rect, textarea_rect, popup_rect]
206182 }
207183
@@ -213,12 +189,6 @@ impl ChatComposer {
213189 }
214190 }
215191
216- pub fn cursor_pos ( & self , area : Rect ) -> Option < ( u16 , u16 ) > {
217- let [ _, textarea_rect, _] = self . layout_areas ( area) ;
218- let state = * self . textarea_state . borrow ( ) ;
219- self . textarea . cursor_pos_with_state ( textarea_rect, state)
220- }
221-
222192 /// Returns true if the composer currently contains no user input.
223193 pub ( crate ) fn is_empty ( & self ) -> bool {
224194 self . textarea . is_empty ( )
@@ -1541,8 +1511,32 @@ impl ChatComposer {
15411511 }
15421512}
15431513
1544- impl WidgetRef for ChatComposer {
1545- fn render_ref ( & self , area : Rect , buf : & mut Buffer ) {
1514+ impl Renderable for ChatComposer {
1515+ fn cursor_pos ( & self , area : Rect ) -> Option < ( u16 , u16 ) > {
1516+ let [ _, textarea_rect, _] = self . layout_areas ( area) ;
1517+ let state = * self . textarea_state . borrow ( ) ;
1518+ self . textarea . cursor_pos_with_state ( textarea_rect, state)
1519+ }
1520+
1521+ fn desired_height ( & self , width : u16 ) -> u16 {
1522+ let footer_props = self . footer_props ( ) ;
1523+ let footer_hint_height = self
1524+ . custom_footer_height ( )
1525+ . unwrap_or_else ( || footer_height ( footer_props) ) ;
1526+ let footer_spacing = Self :: footer_spacing ( footer_hint_height) ;
1527+ let footer_total_height = footer_hint_height + footer_spacing;
1528+ const COLS_WITH_MARGIN : u16 = LIVE_PREFIX_COLS + 1 ;
1529+ self . textarea
1530+ . desired_height ( width. saturating_sub ( COLS_WITH_MARGIN ) )
1531+ + 2
1532+ + match & self . active_popup {
1533+ ActivePopup :: None => footer_total_height,
1534+ ActivePopup :: Command ( c) => c. calculate_required_height ( width) ,
1535+ ActivePopup :: File ( c) => c. calculate_required_height ( ) ,
1536+ }
1537+ }
1538+
1539+ fn render ( & self , area : Rect , buf : & mut Buffer ) {
15461540 let [ composer_rect, textarea_rect, popup_rect] = self . layout_areas ( area) ;
15471541 match & self . active_popup {
15481542 ActivePopup :: Command ( popup) => {
@@ -1591,16 +1585,15 @@ impl WidgetRef for ChatComposer {
15911585 }
15921586 }
15931587 let style = user_message_style ( ) ;
1594- let mut block_rect = composer_rect;
1595- block_rect. y = composer_rect. y . saturating_sub ( 1 ) ;
1596- block_rect. height = composer_rect. height . saturating_add ( 1 ) ;
1597- Block :: default ( ) . style ( style) . render_ref ( block_rect, buf) ;
1598- buf. set_span (
1599- composer_rect. x ,
1600- composer_rect. y ,
1601- & "›" . bold ( ) ,
1602- composer_rect. width ,
1603- ) ;
1588+ Block :: default ( ) . style ( style) . render_ref ( composer_rect, buf) ;
1589+ if !textarea_rect. is_empty ( ) {
1590+ buf. set_span (
1591+ textarea_rect. x - LIVE_PREFIX_COLS ,
1592+ textarea_rect. y ,
1593+ & "›" . bold ( ) ,
1594+ textarea_rect. width ,
1595+ ) ;
1596+ }
16041597
16051598 let mut state = self . textarea_state . borrow_mut ( ) ;
16061599 StatefulWidgetRef :: render_ref ( & ( & self . textarea ) , textarea_rect, buf, & mut state) ;
@@ -1692,7 +1685,7 @@ mod tests {
16921685
16931686 let area = Rect :: new ( 0 , 0 , 40 , 6 ) ;
16941687 let mut buf = Buffer :: empty ( area) ;
1695- composer. render_ref ( area, & mut buf) ;
1688+ composer. render ( area, & mut buf) ;
16961689
16971690 let row_to_string = |y : u16 | {
16981691 let mut row = String :: new ( ) ;
@@ -1756,7 +1749,7 @@ mod tests {
17561749 let height = footer_lines + footer_spacing + 8 ;
17571750 let mut terminal = Terminal :: new ( TestBackend :: new ( width, height) ) . unwrap ( ) ;
17581751 terminal
1759- . draw ( |f| f . render_widget_ref ( composer , f. area ( ) ) )
1752+ . draw ( |f| composer . render ( f . area ( ) , f. buffer_mut ( ) ) )
17601753 . unwrap ( ) ;
17611754 insta:: assert_snapshot!( name, terminal. backend( ) ) ;
17621755 }
@@ -2276,7 +2269,7 @@ mod tests {
22762269 }
22772270
22782271 terminal
2279- . draw ( |f| f . render_widget_ref ( composer , f. area ( ) ) )
2272+ . draw ( |f| composer . render ( f . area ( ) , f. buffer_mut ( ) ) )
22802273 . unwrap_or_else ( |e| panic ! ( "Failed to draw {name} composer: {e}" ) ) ;
22812274
22822275 insta:: assert_snapshot!( name, terminal. backend( ) ) ;
@@ -2302,12 +2295,12 @@ mod tests {
23022295 // Type "/mo" humanlike so paste-burst doesn’t interfere.
23032296 type_chars_humanlike ( & mut composer, & [ '/' , 'm' , 'o' ] ) ;
23042297
2305- let mut terminal = match Terminal :: new ( TestBackend :: new ( 60 , 4 ) ) {
2298+ let mut terminal = match Terminal :: new ( TestBackend :: new ( 60 , 5 ) ) {
23062299 Ok ( t) => t,
23072300 Err ( e) => panic ! ( "Failed to create terminal: {e}" ) ,
23082301 } ;
23092302 terminal
2310- . draw ( |f| f . render_widget_ref ( composer , f. area ( ) ) )
2303+ . draw ( |f| composer . render ( f . area ( ) , f. buffer_mut ( ) ) )
23112304 . unwrap_or_else ( |e| panic ! ( "Failed to draw composer: {e}" ) ) ;
23122305
23132306 // Visual snapshot should show the slash popup with /model as the first entry.
0 commit comments