@@ -1789,6 +1789,7 @@ def _write_to_screen_at_index(
17891789 has_focus = get_app ().layout .current_control == self .content ,
17901790 align = align ,
17911791 get_line_prefix = self .get_line_prefix ,
1792+ wrap_finder = self ._whitespace_wrap_finder (ui_content ),
17921793 )
17931794
17941795 # Remember render info. (Set before generating the margins. They need this.)
@@ -1920,6 +1921,47 @@ def render_margin(m: Margin, width: int) -> UIContent:
19201921 # position.
19211922 screen .visible_windows_to_write_positions [self ] = write_position
19221923
1924+ def _whitespace_wrap_finder (
1925+ self ,
1926+ ui_content : UIContent ,
1927+ sep : str | re .Pattern = r'\s' ,
1928+ split : str = 'remove' ,
1929+ continuation : StyleAndTextTuples = [],
1930+ ) -> Callable [[int , int , int ], tuple [int , int , StyleAndTextTuples ]]:
1931+ """ Returns a function that defines where to break """
1932+ sep_re = sep if isinstance (sep , re .Pattern ) else re .compile (sep )
1933+ if sep_re .groups :
1934+ raise ValueError (f'Pattern { sep_re .pattern !r} has capture group – use non-capturing groups instead' )
1935+ elif split == 'after' :
1936+ sep_re = re .compile ('(?<={sep_re.pattern})()' )
1937+ elif split == 'before' :
1938+ sep_re = re .compile ('(?={sep_re.pattern})()' )
1939+ elif split == 'remove' :
1940+ sep_re = re .compile (f'({ sep_re .pattern } )' )
1941+ else :
1942+ raise ValueError (f'Unrecognized value of split paramter: { split !r} ' )
1943+
1944+ cont_width = fragment_list_width (text )
1945+
1946+ def wrap_finder (lineno : int , start : int , end : int ) -> tuple [int , int , StyleAndTextTuples ]:
1947+ line = explode_text_fragments (ui_content .get_line (lineno ))
1948+ cont_reserved = 0
1949+ while cont_reserved < cont_width :
1950+ style , char , * _ = line [end - 1 ]
1951+ cont_reserved += _CHAR_CACHE [style , char ].width
1952+ end -= 1
1953+
1954+ segment = to_plain_text (line [start :end ])
1955+ try :
1956+ after , sep , before = sep_re .split (segment [::- 1 ], maxsplit = 1 )
1957+ except ValueError :
1958+ return (end , 0 , continuation )
1959+ else :
1960+ return (start + len (before ), len (sep ), continuation )
1961+
1962+ return wrap_finder
1963+
1964+
19231965 def _copy_body (
19241966 self ,
19251967 ui_content : UIContent ,
@@ -1936,6 +1978,7 @@ def _copy_body(
19361978 has_focus : bool = False ,
19371979 align : WindowAlign = WindowAlign .LEFT ,
19381980 get_line_prefix : Callable [[int , int ], AnyFormattedText ] | None = None ,
1981+ wrap_finder : Callable [[int , int , int ], tuple [int , int , AnyFormattedText ] | None ] | None = None ,
19391982 ) -> tuple [dict [int , tuple [int , int ]], dict [tuple [int , int ], tuple [int , int ]]]:
19401983 """
19411984 Copy the UIContent into the output screen.
@@ -1957,6 +2000,42 @@ def _copy_body(
19572000 # Maps (row, col) from the input to (y, x) screen coordinates.
19582001 rowcol_to_yx : dict [tuple [int , int ], tuple [int , int ]] = {}
19592002
2003+ def find_next_wrap (remaining_width , is_input , lineno , fragment = 0 , char_pos = 0 ):
2004+ if not wrap_lines :
2005+ return sys .maxsize , 0 , []
2006+
2007+ line = ui_content .get_line (lineno )
2008+ style0 , text0 , * more = line [fragment ]
2009+ fragment_pos = char_pos - fragment_list_len (line [:fragment ])
2010+ line_part = [(style0 , text0 [char_pos :], * more ), * line [fragment + 1 :]]
2011+ line_width = [fragment_list_width ([fragment ]) for fragment in line_part ]
2012+
2013+ orig_remaining_width = remaining_width
2014+ if sum (line_width ) <= remaining_width :
2015+ return sys .maxsize , 0 , []
2016+
2017+ min_wrap_pos = max_wrap_pos = char_pos
2018+ for next_fragment , fragment_width in zip (line_part , line_width ):
2019+ if remaining_width < fragment_width :
2020+ break
2021+ remaining_width -= fragment_width
2022+ max_wrap_pos += fragment_list_len ([next_fragment ])
2023+ else :
2024+ # Should never happen
2025+ return sys .maxsize , 0 , []
2026+
2027+ style , text , * _ = next_fragment
2028+ for char_width in (_CHAR_CACHE [char , style ].width for char in text ):
2029+ if remaining_width < char_width :
2030+ break
2031+ remaining_width -= char_width
2032+ max_wrap_pos += 1
2033+
2034+ if is_input and wrap_finder :
2035+ return wrap_finder (lineno , min_wrap_pos , max_wrap_pos )
2036+ else :
2037+ return max_wrap_pos , 0 , []
2038+
19602039 def copy_line (
19612040 line : StyleAndTextTuples ,
19622041 lineno : int ,
@@ -2003,27 +2082,46 @@ def copy_line(
20032082 if line_width < width :
20042083 x += width - line_width
20052084
2085+ new_buffer_row = new_buffer [y + ypos ]
2086+ wrap_start , wrap_replaced , continuation = find_next_wrap (width - x , is_input , lineno )
2087+
20062088 col = 0
20072089 wrap_count = 0
2008- for style , text , * _ in line :
2009- new_buffer_row = new_buffer [ y + ypos ]
2010-
2090+ wrap_skip = 0
2091+ text_end = 0
2092+ for fragment_count , ( style , text , * _ ) in enumerate ( line ):
20112093 # Remember raw VT escape sequences. (E.g. FinalTerm's
20122094 # escape sequences.)
20132095 if "[ZeroWidthEscape]" in style :
20142096 new_screen .zero_width_escapes [y + ypos ][x + xpos ] += text
20152097 continue
20162098
2017- for c in text :
2099+ text_start , text_end = text_end , text_end + len (text )
2100+
2101+ for char_count , c in enumerate (text , text_start ):
20182102 char = _CHAR_CACHE [c , style ]
20192103 char_width = char .width
20202104
20212105 # Wrap when the line width is exceeded.
2022- if wrap_lines and x + char_width > width :
2106+ if wrap_lines and char_count == wrap_start :
2107+ skipped_width = sum (
2108+ _CHAR_CACHE [char , style ].width
2109+ for char in text [wrap_start - text_start :][:wrap_replaced ]
2110+ )
2111+ col += wrap_replaced
20232112 visible_line_to_row_col [y + 1 ] = (
20242113 lineno ,
2025- visible_line_to_row_col [y ][1 ] + x ,
2114+ visible_line_to_row_col [y ][1 ] + x + skipped_width ,
20262115 )
2116+
2117+ # Append continuation (e.g. hyphen)
2118+ if continuation :
2119+ x , y = copy_line (continuation , lineno , x , y , is_input = False )
2120+ # Make sure to erase rest of the line
2121+ for i in range (x , width ):
2122+ new_buffer_row [i + xpos ] = empty_char
2123+ wrap_skip = wrap_replaced
2124+
20272125 y += 1
20282126 wrap_count += 1
20292127 x = 0
@@ -2036,10 +2134,18 @@ def copy_line(
20362134 x , y = copy_line (prompt , lineno , x , y , is_input = False )
20372135
20382136 new_buffer_row = new_buffer [y + ypos ]
2137+ wrap_start , wrap_replaced , continuation = find_next_wrap (
2138+ width - x , is_input , lineno , fragment_count , wrap_start + wrap_replaced
2139+ )
20392140
20402141 if y >= write_position .height :
20412142 return x , y # Break out of all for loops.
20422143
2144+ # Chars skipped by wrapping (e.g. whitespace)
2145+ if wrap_lines and wrap_skip > 0 :
2146+ wrap_skip -= 1
2147+ continue
2148+
20432149 # Set character in screen and shift 'x'.
20442150 if x >= 0 and y >= 0 and x < width :
20452151 new_buffer_row [x + xpos ] = char
0 commit comments