@@ -37,8 +37,7 @@ def syntax_highlight_text(code: str, syntax: str) -> Text:
3737
3838
3939class  TextInput (Widget ):
40-     """ 
41-     A simple text input widget. 
40+     """A simple text input widget. 
4241
4342    Args: 
4443        name (Optional[str]): The unique name of the widget. If None, the 
@@ -50,21 +49,21 @@ class TextInput(Widget):
5049            of the widget's border. 
5150        password (bool, optional): Defaults to False. Hides the text 
5251            input, replacing it with bullets. 
53-         syntax (Optional[str]): the  name of the language for syntax highlighting. 
52+         syntax (Optional[str]): The  name of the language for syntax highlighting. 
5453
5554    Attributes: 
56-         value (str): the  value of the text field 
55+         value (str): The  value of the text field 
5756        placeholder (str): The placeholder message. 
5857        title (str): The displayed title of the widget. 
5958        has_password (bool): True if the text field masks the input. 
6059        syntax (Optional[str]): the name of the language for syntax highlighting. 
6160        has_focus (bool): True if the widget is focused. 
6261        cursor (Tuple[str, Style]): The character used for the cursor 
6362            and a rich Style object defining its appearance. 
64-         on_change_handler_name (str): name of handler function to be 
63+         on_change_handler_name (str): The  name of handler function to be 
6564            called when an on change event occurs. Defaults to 
6665            handle_input_on_change. 
67-         on_focus_handler_name (name): name of handler function to be 
66+         on_focus_handler_name (name): The  name of handler function to be 
6867            called when an on focus event occurs. Defaults to 
6968            handle_input_on_focus. 
7069
@@ -95,6 +94,7 @@ class TextInput(Widget):
9594            bold = True ,
9695        ),
9796    )
97+     """Character and style of the cursor.""" 
9898    _cursor_position : Reactive [int ] =  Reactive (0 )
9999    _has_focus : Reactive [bool ] =  Reactive (False )
100100
@@ -111,13 +111,22 @@ def __init__(
111111    ) ->  None :
112112        super ().__init__ (name )
113113        self .value  =  value 
114+         """The value of the text field""" 
114115        self .placeholder  =  placeholder 
116+         """ 
117+         Text that appears in the widget when value is "" and the widget 
118+         is not focused. 
119+         """ 
115120        self .title  =  title 
121+         """The displayed title of the widget.""" 
116122        self .has_password  =  password 
123+         """True if the text field masks the input.""" 
117124        self .syntax  =  syntax 
125+         """The name of the language for syntax highlighting.""" 
118126        self ._on_change_message_class  =  InputOnChange 
119127        self ._on_focus_message_class  =  InputOnFocus 
120128        self ._cursor_position  =  len (self .value )
129+         self ._text_offset  =  0 
121130
122131    def  __rich_repr__ (self ):
123132        yield  "name" , self .name 
@@ -200,119 +209,170 @@ def render(self) -> RenderableType:
200209        )
201210
202211    def  _modify_text (self , segment : str ) ->  Union [str , Text ]:
203-         """ 
204-         Produces the text with modifications, such as password concealing. 
205-         """ 
212+         """Produces the text with modifications, such as password concealing.""" 
206213        if  self .has_password :
207214            return  conceal_text (segment )
208215        if  self .syntax :
209216            return  syntax_highlight_text (segment , self .syntax )
210217        return  segment 
211218
212-     def  _render_text_with_cursor (self ) ->  List [Union [str , Text , Tuple [str , Style ]]]:
219+     @property  
220+     def  _visible_width (self ):
221+         """Width in characters of the inside of the input""" 
222+         # remove 2, 1 for each of the border's edges 
223+         # remove 1 more for the cursor 
224+         # remove 2 for the padding either side of the input 
225+         width , _  =  self .size 
226+         if  self .border :
227+             width  -=  2 
228+         if  self ._has_focus :
229+             width  -=  1 
230+         width  -=  2 
231+         return  width 
232+ 
233+     def  _text_offset_window (self ):
213234        """ 
214-         Produces the renderable Text object combining value and cursor 
235+         Produce the start and end indices of the visible portions of the 
236+         text value. 
215237        """ 
238+         return  self ._text_offset , self ._text_offset  +  self ._visible_width 
239+ 
240+     def  _render_text_with_cursor (self ) ->  List [Union [str , Text , Tuple [str , Style ]]]:
241+         """Produces the renderable Text object combining value and cursor""" 
216242        text  =  self ._modify_text (self .value )
243+ 
244+         # trim the string to fit within the widgets dimensions 
245+         left , right  =  self ._text_offset_window ()
246+         text  =  text [left :right ]
247+ 
248+         # convert the cursor to be relative to this view 
249+         cursor_relative_position  =  self ._cursor_position  -  self ._text_offset 
217250        return  [
218-             text [:  self . _cursor_position ],
251+             text [:cursor_relative_position ],
219252            self .cursor ,
220-             text [self . _cursor_position   :],
253+             text [cursor_relative_position :],
221254        ]
222255
223256    async  def  on_focus (self , event : events .Focus ) ->  None :
257+         """Handle Focus events 
258+ 
259+         Args: 
260+             event (events.Focus): A Textual Focus event 
261+         """ 
224262        self ._has_focus  =  True 
225263        await  self ._emit_on_focus ()
226264
227265    async  def  on_blur (self , event : events .Blur ) ->  None :
266+         """Handle Blur events 
267+ 
268+         Args: 
269+             event (events.Blur): A Textual Blur event 
270+         """ 
228271        self ._has_focus  =  False 
229272
273+     def  _update_offset_left (self ):
274+         """ 
275+         Decrease the text offset if the cursor moves less than 3 characters 
276+         from the left edge. This will shift the text to the right and keep 
277+         the cursor 3 characters from the left edge. If the text offset is 0 
278+         then the cursor may continue to move until it reaches the left edge. 
279+         """ 
280+         visibility_left  =  3 
281+         if  self ._cursor_position  <  self ._text_offset  +  visibility_left :
282+             self ._text_offset  =  max (0 , self ._cursor_position  -  visibility_left )
283+ 
284+     def  _update_offset_right (self ):
285+         """ 
286+         Increase the text offset if the cursor moves beyond the right 
287+         edge of the widget. This will shift the text left and make the 
288+         cursor visible at the right edge of the widget. 
289+         """ 
290+         _ , right  =  self ._text_offset_window ()
291+         if  self ._cursor_position  >  right :
292+             self ._text_offset  =  self ._cursor_position  -  self ._visible_width 
293+ 
294+     def  _cursor_left (self ):
295+         """Handle key press Left""" 
296+         if  self ._cursor_position  >  0 :
297+             self ._cursor_position  -=  1 
298+             self ._update_offset_left ()
299+ 
300+     def  _cursor_right (self ):
301+         """Handle key press Right""" 
302+         if  self ._cursor_position  <  len (self .value ):
303+             self ._cursor_position  =  self ._cursor_position  +  1 
304+             self ._update_offset_right ()
305+ 
306+     def  _cursor_home (self ):
307+         """Handle key press Home""" 
308+         self ._cursor_position  =  0 
309+         self ._update_offset_left ()
310+ 
311+     def  _cursor_end (self ):
312+         """Handle key press End""" 
313+         self ._cursor_position  =  len (self .value )
314+         self ._update_offset_right ()
315+ 
316+     def  _key_backspace (self ):
317+         """Handle key press Backspace""" 
318+         if  self ._cursor_position  >  0 :
319+             self .value  =  (
320+                 self .value [: self ._cursor_position  -  1 ]
321+                 +  self .value [self ._cursor_position  :]
322+             )
323+             self ._cursor_position  -=  1 
324+             self ._update_offset_left ()
325+ 
326+     def  _key_delete (self ):
327+         """Handle key press Delete""" 
328+         if  self ._cursor_position  <  len (self .value ):
329+             self .value  =  (
330+                 self .value [: self ._cursor_position ]
331+                 +  self .value [self ._cursor_position  +  1  :]
332+             )
333+ 
334+     def  _key_printable (self , event : events .Key ):
335+         """Handle all printable keys""" 
336+         self .value  =  (
337+             self .value [: self ._cursor_position ]
338+             +  event .key 
339+             +  self .value [self ._cursor_position  :]
340+         )
341+ 
342+         if  not  self ._cursor_position  >  len (self .value ):
343+             self ._cursor_position  +=  1 
344+             self ._update_offset_right ()
345+ 
230346    async  def  on_key (self , event : events .Key ) ->  None :
231-         if  event .key  ==  "left" :
232-             if  self ._cursor_position  ==  0 :
233-                 self ._cursor_position  =  0 
234-             else :
235-                 self ._cursor_position  -=  1 
347+         """Handle key events 
236348
349+         Args: 
350+             event (events.Key): A Textual Key event 
351+         """ 
352+         BACKSPACE  =  "ctrl+h" 
353+         if  event .key  ==  "left" :
354+             self ._cursor_left ()
237355        elif  event .key  ==  "right" :
238-             if  self ._cursor_position  !=  len (self .value ):
239-                 self ._cursor_position  =  self ._cursor_position  +  1 
240- 
356+             self ._cursor_right ()
241357        elif  event .key  ==  "home" :
242-             self ._cursor_position  =  0 
243- 
358+             self ._cursor_home ()
244359        elif  event .key  ==  "end" :
245-             self ._cursor_position  =  len (self .value )
246- 
247-         elif  event .key  ==  "ctrl+h" :  # Backspace 
248-             if  self ._cursor_position  ==  0 :
249-                 return 
250-             elif  len (self .value ) ==  1 :
251-                 self .value  =  "" 
252-                 self ._cursor_position  =  0 
253-             elif  len (self .value ) ==  2 :
254-                 if  self ._cursor_position  ==  1 :
255-                     self .value  =  self .value [1 ]
256-                     self ._cursor_position  =  0 
257-                 else :
258-                     self .value  =  self .value [0 ]
259-                     self ._cursor_position  =  1 
260-             else :
261-                 if  self ._cursor_position  ==  1 :
262-                     self .value  =  self .value [1 :]
263-                     self ._cursor_position  =  0 
264-                 elif  self ._cursor_position  ==  len (self .value ):
265-                     self .value  =  self .value [:- 1 ]
266-                     self ._cursor_position  -=  1 
267-                 else :
268-                     self .value  =  (
269-                         self .value [: self ._cursor_position  -  1 ]
270-                         +  self .value [self ._cursor_position  :]
271-                     )
272-                     self ._cursor_position  -=  1 
273- 
360+             self ._cursor_end ()
361+         elif  event .key  ==  BACKSPACE :
362+             self ._key_backspace ()
274363            await  self ._emit_on_change (event )
275- 
276364        elif  event .key  ==  "delete" :
277-             if  self ._cursor_position  ==  len (self .value ):
278-                 return 
279-             elif  len (self .value ) ==  1 :
280-                 self .value  =  "" 
281-             elif  len (self .value ) ==  2 :
282-                 if  self ._cursor_position  ==  1 :
283-                     self .value  =  self .value [0 ]
284-                 else :
285-                     self .value  =  self .value [1 ]
286-             else :
287-                 if  self ._cursor_position  ==  0 :
288-                     self .value  =  self .value [1 :]
289-                 else :
290-                     self .value  =  (
291-                         self .value [: self ._cursor_position ]
292-                         +  self .value [self ._cursor_position  +  1  :]
293-                     )
365+             self ._key_delete ()
294366            await  self ._emit_on_change (event )
295- 
296367        elif  len (event .key ) ==  1  and  event .key .isprintable ():
297-             if  self ._cursor_position  ==  0 :
298-                 self .value  =  event .key  +  self .value 
299-             elif  self ._cursor_position  ==  len (self .value ):
300-                 self .value  =  self .value  +  event .key 
301-             else :
302-                 self .value  =  (
303-                     self .value [: self ._cursor_position ]
304-                     +  event .key 
305-                     +  self .value [self ._cursor_position  :]
306-                 )
307- 
308-             if  not  self ._cursor_position  >  len (self .value ):
309-                 self ._cursor_position  +=  1 
310- 
368+             self ._key_printable (event )
311369            await  self ._emit_on_change (event )
312370
313371    async  def  _emit_on_change (self , event : events .Key ) ->  None :
372+         """Emit custom message class on Change events""" 
314373        event .stop ()
315374        await  self .emit (self ._on_change_message_class (self ))
316375
317376    async  def  _emit_on_focus (self ) ->  None :
377+         """Emit custom message class on Focus events""" 
318378        await  self .emit (self ._on_focus_message_class (self ))
0 commit comments