19
19
20
20
from abc import ABC
21
21
from pygame_menu .locals import FINGERUP , CURSOR_HAND
22
- from pygame_menu .utils import assert_color , get_finger_pos , warn
22
+ from pygame_menu .utils import assert_color , get_finger_pos , make_surface , warn
23
23
from pygame_menu .widgets .core .widget import AbstractWidgetManager , Widget
24
24
25
25
from pygame_menu ._types import Any , CallbackType , Callable , Union , List , Tuple , \
@@ -45,10 +45,16 @@ class Button(Widget):
45
45
:param title: Button title
46
46
:param button_id: Button ID
47
47
:param onreturn: Callback when pressing the button
48
+ :param wordwrap: Wraps label if newline is found on widget
49
+ :param leading: Font leading for ``wordwrap``. If ``None`` retrieves from widget font
50
+ :param max_nlines: Number of maximum lines for ``wordwrap``. If ``None`` the number is dynamically computed. If exceded, ``label.get_overflow_lines()`` will return the lines not displayed
48
51
:param args: Optional arguments for callbacks
49
52
:param kwargs: Optional keyword arguments
50
53
"""
51
54
_last_underline : List [Union [str , Optional [Tuple [ColorType , int , int ]]]] # deco id, (color, offset, width)
55
+ _leading : Optional [int ]
56
+ _max_nlines : Optional [int ]
57
+ _wordwrap : bool
52
58
to_menu : bool
53
59
54
60
def __init__ (
@@ -59,6 +65,9 @@ def __init__(
59
65
* args ,
60
66
** kwargs
61
67
) -> None :
68
+ leading = kwargs .pop ('leading' , None )
69
+ max_nlines = kwargs .pop ('max_nlines' , None )
70
+ wordwrap = kwargs .pop ('wordwrap' , False )
62
71
super (Button , self ).__init__ (
63
72
args = args ,
64
73
kwargs = kwargs ,
@@ -68,6 +77,10 @@ def __init__(
68
77
)
69
78
self ._accept_events = True
70
79
self ._last_underline = ['' , None ]
80
+ self ._leading = leading
81
+ self ._lines = [] # Lines of text displayed
82
+ self ._max_nlines = max_nlines
83
+ self ._wordwrap = wordwrap
71
84
self .to_menu = False # True if the button opens a new Menu
72
85
73
86
def _apply_font (self ) -> None :
@@ -131,14 +144,16 @@ def add_underline(
131
144
force_render : bool = False
132
145
) -> 'Button' :
133
146
"""
134
- Adds an underline to text. This is added if widget is rendered.
147
+ Adds an underline to text. This is added if widget is rendered. Underline
148
+ is only enabled for non wordwrap label.
135
149
136
150
:param color: Underline color
137
151
:param offset: Underline offset
138
152
:param width: Underline width
139
153
:param force_render: If ``True`` force widget render after addition
140
154
:return: Self reference
141
155
"""
156
+ assert not self ._wordwrap , 'underline is not enabled for wordwrap is active'
142
157
color = assert_color (color )
143
158
assert isinstance (offset , int )
144
159
assert isinstance (width , int ) and width > 0
@@ -153,6 +168,7 @@ def remove_underline(self) -> 'Button':
153
168
154
169
:return: Self reference
155
170
"""
171
+ assert not self ._wordwrap , 'underline is not enabled for wordwrap is active'
156
172
if self ._last_underline [0 ] != '' :
157
173
self ._decorator .remove (self ._last_underline [0 ])
158
174
self ._last_underline [0 ] = ''
@@ -165,28 +181,158 @@ def _render(self) -> Optional[bool]:
165
181
if not self ._render_hash_changed (self ._selected , self ._title , self ._visible , self .readonly ,
166
182
self ._last_underline [1 ]):
167
183
return True
184
+ self ._lines = []
168
185
169
- # Render surface
170
- self ._surface = self ._render_string (self ._title , self .get_font_color_status ())
186
+ # Generate surface
187
+ if not self ._wordwrap :
188
+ self ._surface = self ._render_string (self ._title , self .get_font_color_status ())
189
+ self ._lines .append (self ._title )
190
+
191
+ else :
192
+ self ._overflow_lines = []
193
+ if self ._font is None or self ._menu is None :
194
+ self ._surface = make_surface (0 , 0 , alpha = True )
195
+ else :
196
+ lines = self ._title .split ('\n ' )
197
+ lines = sum (
198
+ (
199
+ self ._wordwrap_line (
200
+ line = line ,
201
+ font = self ._font ,
202
+ max_width = self ._get_max_container_width (),
203
+ tab_size = self ._tab_size
204
+ )
205
+ for line in lines
206
+ ),
207
+ []
208
+ )
209
+ num_lines = len (lines )
210
+ if isinstance (self ._max_nlines , int ):
211
+ if num_lines > self ._max_nlines :
212
+ for j in range (num_lines - self ._max_nlines ):
213
+ self ._overflow_lines .append (lines [num_lines - j - 1 ])
214
+ num_lines = min (num_lines , self ._max_nlines )
215
+
216
+ self ._surface = make_surface (
217
+ max (self ._font .size (line )[0 ] for line in lines ),
218
+ num_lines * self ._get_leading (),
219
+ alpha = True
220
+ )
221
+
222
+ for n_line , line in enumerate (lines ):
223
+ line_surface = self ._render_string (line , self ._font_color )
224
+ self ._surface .blit (
225
+ line_surface ,
226
+ pygame .Rect (
227
+ 0 ,
228
+ n_line * self ._get_leading (),
229
+ self ._rect .width ,
230
+ self ._rect .height
231
+ )
232
+ )
233
+ self ._lines .append (line )
234
+ if n_line + 1 == num_lines :
235
+ break
236
+
237
+ # Update rect object
171
238
self ._apply_transforms ()
172
239
self ._rect .width , self ._rect .height = self ._surface .get_size ()
173
240
174
- # Add underline if enabled
175
- self .remove_underline ()
176
- if self ._last_underline [1 ] is not None :
177
- w = self ._surface .get_width ()
178
- h = self ._surface .get_height ()
179
- color , offset , width = self ._last_underline [1 ]
180
- if w > 0 and h > 0 :
181
- self ._last_underline [0 ] = self ._decorator .add_line (
182
- pos1 = (- w / 2 , h / 2 + offset ),
183
- pos2 = (w / 2 , h / 2 + offset ),
184
- color = color ,
185
- width = width
186
- )
241
+ # Add underline
242
+ if not self ._wordwrap :
243
+ self .remove_underline ()
244
+ if self ._last_underline [1 ] is not None :
245
+ w = self ._surface .get_width ()
246
+ h = self ._surface .get_height ()
247
+ color , offset , width = self ._last_underline [1 ]
248
+ if w > 0 and h > 0 :
249
+ self ._last_underline [0 ] = self ._decorator .add_line (
250
+ pos1 = (- w / 2 , h / 2 + offset ),
251
+ pos2 = (w / 2 , h / 2 + offset ),
252
+ color = color ,
253
+ width = width
254
+ )
187
255
188
256
self .force_menu_surface_update ()
189
257
258
+ def _get_leading (self ) -> int :
259
+ """
260
+ Computes the font leading.
261
+
262
+ :return: Leading
263
+ """
264
+ assert self ._font
265
+ return (
266
+ self ._font .get_linesize ()
267
+ if self ._leading is None
268
+ else self ._leading
269
+ )
270
+
271
+ def get_lines (self ) -> List [str ]:
272
+ """
273
+ Return the lines of text displayed. Each new line belongs to an item on list.
274
+
275
+ :return: List of displayed lines
276
+ """
277
+ return self ._lines
278
+
279
+ @staticmethod
280
+ def _wordwrap_line (
281
+ line : str ,
282
+ font : pygame .font .Font ,
283
+ max_width : int ,
284
+ tab_size : int ,
285
+ ) -> List [str ]:
286
+ """
287
+ Wordwraps line.
288
+
289
+ :param line: Line
290
+ :param font: Font
291
+ :param max_width: Max width
292
+ :param tab_size: Tab size
293
+ :return: List of strings
294
+ """
295
+ final_lines = []
296
+ words = line .split (' ' )
297
+ i , current_line = 0 , ''
298
+
299
+ while True :
300
+ split_line = False
301
+ for i , _ in enumerate (words ):
302
+ current_line = ' ' .join (words [:i + 1 ])
303
+ current_line = current_line .replace ('\t ' , ' ' * tab_size )
304
+ current_line_size = font .size (current_line )
305
+ if current_line_size [0 ] > max_width :
306
+ split_line = True
307
+ break
308
+
309
+ if split_line :
310
+ i = i if i > 0 else 1
311
+ final_lines .append (' ' .join (words [:i ]))
312
+ words = words [i :]
313
+ else :
314
+ final_lines .append (current_line )
315
+ break
316
+
317
+ return final_lines
318
+
319
+ def _get_max_container_width (self ) -> int :
320
+ """
321
+ Return the maximum label container width. It can be the column width,
322
+ menu width or frame width if horizontal.
323
+
324
+ :return: Container width
325
+ """
326
+ menu = self ._menu
327
+ if menu is None :
328
+ return 0
329
+ try :
330
+ # noinspection PyProtectedMember
331
+ max_width = menu ._column_widths [self .get_col_row_index ()[0 ]]
332
+ except IndexError :
333
+ max_width = menu .get_width (inner = True )
334
+ return max_width - self ._padding [1 ] - self ._padding [3 ] - self ._selection_effect .get_width ()
335
+
190
336
def update (self , events : EventVectorType ) -> bool :
191
337
self .apply_update_callbacks (events )
192
338
rect = self .get_rect (to_real_position = True )
@@ -330,8 +476,8 @@ def button(
330
476
"""
331
477
Adds a button to the Menu.
332
478
333
- The arguments and unknown keyword arguments are passed to the action, if
334
- it's a callable object:
479
+ # The arguments and unknown keyword arguments are passed to the action, if
480
+ # it's a callable object:
335
481
336
482
.. code-block:: python
337
483
@@ -389,6 +535,7 @@ def button(
389
535
- ``underline_offset`` (int) – Vertical offset in px. ``2`` by default
390
536
- ``underline_width`` (int) – Underline width in px. ``2`` by default
391
537
- ``underline`` (bool) – Enables text underline, using a properly placed decoration. ``False`` by default
538
+ - ``wordwrap`` (bool) – Wraps label if newline is found on widget. If ``False`` the manager splits the string and creates a list of widgets, else, the widget itself splits and updates the height
392
539
393
540
.. note::
394
541
@@ -416,6 +563,10 @@ def button(
416
563
:return: Widget object
417
564
:rtype: :py:class:`pygame_menu.widgets.Button`
418
565
"""
566
+
567
+ # wordwrap = kwargs.pop('wordwrap', False)
568
+ # assert isinstance(wordwrap, bool)
569
+
419
570
total_back = kwargs .pop ('back_count' , 1 )
420
571
assert isinstance (total_back , int ) and 1 <= total_back
421
572
@@ -457,29 +608,29 @@ def button(
457
608
f'back_count number of menus to return from, default is 1'
458
609
)
459
610
460
- widget = Button (title , button_id , self ._menu ._open , action )
611
+ widget = Button (title , button_id , self ._menu ._open , action , * args , ** kwargs )
461
612
widget .to_menu = True
462
613
463
614
# If element is a MenuAction
464
615
elif action == _events .BACK : # Back to Menu
465
- widget = Button (title , button_id , self ._menu .reset , total_back )
616
+ widget = Button (title , button_id , self ._menu .reset , total_back , * args , ** kwargs )
466
617
467
618
elif action == _events .CLOSE : # Close Menu
468
- widget = Button (title , button_id , self ._menu ._close )
619
+ widget = Button (title , button_id , self ._menu ._close , * args , ** kwargs )
469
620
470
621
elif action == _events .EXIT : # Exit program
471
- widget = Button (title , button_id , self ._menu ._exit )
622
+ widget = Button (title , button_id , self ._menu ._exit , * args , ** kwargs )
472
623
473
624
elif action == _events .NONE : # None action
474
625
widget = Button (title , button_id )
475
626
476
627
elif action == _events .RESET : # Back to Top Menu
477
- widget = Button (title , button_id , self ._menu .full_reset )
628
+ widget = Button (title , button_id , self ._menu .full_reset , * args , ** kwargs )
478
629
479
630
# If element is a function or callable
480
631
elif callable (action ):
481
632
if not accept_kwargs :
482
- widget = Button (title , button_id , action , * args )
633
+ widget = Button (title , button_id , action , * args , ** kwargs )
483
634
else :
484
635
widget = Button (title , button_id , action , * args , ** kwargs )
485
636
@@ -600,4 +751,4 @@ def url(
600
751
kwargs ['underline' ] = True
601
752
602
753
# Return new button
603
- return self .button (title if title != '' else href , lambda : webbrowser .open (href ), ** kwargs )
754
+ return self .button (title if title != '' else href , lambda : webbrowser .open (href ), ** kwargs )
0 commit comments