urwid 2.6.0.post0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of urwid might be problematic. Click here for more details.

Files changed (75) hide show
  1. urwid/__init__.py +333 -0
  2. urwid/canvas.py +1413 -0
  3. urwid/command_map.py +137 -0
  4. urwid/container.py +59 -0
  5. urwid/decoration.py +65 -0
  6. urwid/display/__init__.py +97 -0
  7. urwid/display/_posix_raw_display.py +413 -0
  8. urwid/display/_raw_display_base.py +914 -0
  9. urwid/display/_web.css +12 -0
  10. urwid/display/_web.js +462 -0
  11. urwid/display/_win32.py +171 -0
  12. urwid/display/_win32_raw_display.py +269 -0
  13. urwid/display/common.py +1219 -0
  14. urwid/display/curses.py +690 -0
  15. urwid/display/escape.py +624 -0
  16. urwid/display/html_fragment.py +251 -0
  17. urwid/display/lcd.py +518 -0
  18. urwid/display/raw.py +37 -0
  19. urwid/display/web.py +636 -0
  20. urwid/event_loop/__init__.py +55 -0
  21. urwid/event_loop/abstract_loop.py +175 -0
  22. urwid/event_loop/asyncio_loop.py +231 -0
  23. urwid/event_loop/glib_loop.py +294 -0
  24. urwid/event_loop/main_loop.py +721 -0
  25. urwid/event_loop/select_loop.py +230 -0
  26. urwid/event_loop/tornado_loop.py +206 -0
  27. urwid/event_loop/trio_loop.py +302 -0
  28. urwid/event_loop/twisted_loop.py +269 -0
  29. urwid/event_loop/zmq_loop.py +275 -0
  30. urwid/font.py +695 -0
  31. urwid/graphics.py +96 -0
  32. urwid/highlight.css +19 -0
  33. urwid/listbox.py +1899 -0
  34. urwid/monitored_list.py +522 -0
  35. urwid/numedit.py +376 -0
  36. urwid/signals.py +330 -0
  37. urwid/split_repr.py +130 -0
  38. urwid/str_util.py +358 -0
  39. urwid/text_layout.py +632 -0
  40. urwid/treetools.py +515 -0
  41. urwid/util.py +557 -0
  42. urwid/version.py +16 -0
  43. urwid/vterm.py +1806 -0
  44. urwid/widget/__init__.py +181 -0
  45. urwid/widget/attr_map.py +161 -0
  46. urwid/widget/attr_wrap.py +140 -0
  47. urwid/widget/bar_graph.py +649 -0
  48. urwid/widget/big_text.py +77 -0
  49. urwid/widget/box_adapter.py +126 -0
  50. urwid/widget/columns.py +1145 -0
  51. urwid/widget/constants.py +574 -0
  52. urwid/widget/container.py +227 -0
  53. urwid/widget/divider.py +110 -0
  54. urwid/widget/edit.py +718 -0
  55. urwid/widget/filler.py +403 -0
  56. urwid/widget/frame.py +539 -0
  57. urwid/widget/grid_flow.py +539 -0
  58. urwid/widget/line_box.py +194 -0
  59. urwid/widget/overlay.py +829 -0
  60. urwid/widget/padding.py +597 -0
  61. urwid/widget/pile.py +971 -0
  62. urwid/widget/popup.py +170 -0
  63. urwid/widget/progress_bar.py +141 -0
  64. urwid/widget/scrollable.py +597 -0
  65. urwid/widget/solid_fill.py +44 -0
  66. urwid/widget/text.py +354 -0
  67. urwid/widget/widget.py +852 -0
  68. urwid/widget/widget_decoration.py +166 -0
  69. urwid/widget/wimp.py +792 -0
  70. urwid/wimp.py +23 -0
  71. urwid-2.6.0.post0.dist-info/COPYING +504 -0
  72. urwid-2.6.0.post0.dist-info/METADATA +332 -0
  73. urwid-2.6.0.post0.dist-info/RECORD +75 -0
  74. urwid-2.6.0.post0.dist-info/WHEEL +5 -0
  75. urwid-2.6.0.post0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,649 @@
1
+ from __future__ import annotations
2
+
3
+ import typing
4
+
5
+ from urwid.canvas import CanvasCombine, CompositeCanvas, SolidCanvas
6
+ from urwid.util import get_encoding_mode
7
+
8
+ from .constants import BAR_SYMBOLS, Sizing
9
+ from .text import Text
10
+ from .widget import Widget, WidgetError, WidgetMeta, nocache_widget_render, nocache_widget_render_instance
11
+
12
+ if typing.TYPE_CHECKING:
13
+ from typing_extensions import Literal
14
+
15
+
16
+ class BarGraphMeta(WidgetMeta):
17
+ """
18
+ Detect subclass get_data() method and dynamic change to
19
+ get_data() method and disable caching in these cases.
20
+
21
+ This is for backwards compatibility only, new programs
22
+ should use set_data() instead of overriding get_data().
23
+ """
24
+
25
+ def __init__(cls, name, bases, d):
26
+ # pylint: disable=protected-access
27
+
28
+ super().__init__(name, bases, d)
29
+
30
+ if "get_data" in d:
31
+ cls.render = nocache_widget_render(cls)
32
+ cls._get_data = cls.get_data
33
+ cls.get_data = property(lambda self: self._get_data, nocache_bargraph_get_data)
34
+
35
+
36
+ def nocache_bargraph_get_data(self, get_data_fn):
37
+ """
38
+ Disable caching on this bargraph because get_data_fn needs
39
+ to be polled to get the latest data.
40
+ """
41
+ self.render = nocache_widget_render_instance(self)
42
+ self._get_data = get_data_fn # pylint: disable=protected-access
43
+
44
+
45
+ class BarGraphError(WidgetError):
46
+ pass
47
+
48
+
49
+ class BarGraph(Widget, metaclass=BarGraphMeta):
50
+ _sizing = frozenset([Sizing.BOX])
51
+
52
+ ignore_focus = True
53
+
54
+ eighths = BAR_SYMBOLS.VERTICAL[:8] # Full height is done by style
55
+ hlines = "_⎺⎻─⎼⎽"
56
+
57
+ def __init__(self, attlist, hatt=None, satt=None) -> None:
58
+ """
59
+ Create a bar graph with the passed display characteristics.
60
+ see set_segment_attributes for a description of the parameters.
61
+ """
62
+ super().__init__()
63
+ self.set_segment_attributes(attlist, hatt, satt)
64
+ self.set_data([], 1, None)
65
+ self.set_bar_width(None)
66
+
67
+ def set_segment_attributes(self, attlist, hatt=None, satt=None):
68
+ """
69
+ :param attlist: list containing display attribute or
70
+ (display attribute, character) tuple for background,
71
+ first segment, and optionally following segments.
72
+ ie. len(attlist) == num segments+1
73
+ character defaults to ' ' if not specified.
74
+ :param hatt: list containing attributes for horizontal lines. First
75
+ element is for lines on background, second is for lines
76
+ on first segment, third is for lines on second segment
77
+ etc.
78
+ :param satt: dictionary containing attributes for smoothed
79
+ transitions of bars in UTF-8 display mode. The values
80
+ are in the form:
81
+
82
+ (fg,bg) : attr
83
+
84
+ fg and bg are integers where 0 is the graph background,
85
+ 1 is the first segment, 2 is the second, ...
86
+ fg > bg in all values. attr is an attribute with a
87
+ foreground corresponding to fg and a background
88
+ corresponding to bg.
89
+
90
+ If satt is not None and the bar graph is being displayed in
91
+ a terminal using the UTF-8 encoding then the character cell
92
+ that is shared between the segments specified will be smoothed
93
+ with using the UTF-8 vertical eighth characters.
94
+
95
+ eg: set_segment_attributes( ['no', ('unsure',"?"), 'yes'] )
96
+ will use the attribute 'no' for the background (the area from
97
+ the top of the graph to the top of the bar), question marks
98
+ with the attribute 'unsure' will be used for the topmost
99
+ segment of the bar, and the attribute 'yes' will be used for
100
+ the bottom segment of the bar.
101
+ """
102
+ self.attr = []
103
+ self.char = []
104
+ if len(attlist) < 2:
105
+ raise BarGraphError(f"attlist must include at least background and seg1: {attlist!r}")
106
+ if len(attlist) < 2:
107
+ raise BarGraphError("must at least specify bg and fg!")
108
+ for a in attlist:
109
+ if not isinstance(a, tuple):
110
+ self.attr.append(a)
111
+ self.char.append(" ")
112
+ else:
113
+ attr, ch = a
114
+ self.attr.append(attr)
115
+ self.char.append(ch)
116
+
117
+ self.hatt = []
118
+ if hatt is None:
119
+ hatt = [self.attr[0]]
120
+ elif not isinstance(hatt, list):
121
+ hatt = [hatt]
122
+ self.hatt = hatt
123
+
124
+ if satt is None:
125
+ satt = {}
126
+ for i in satt.items():
127
+ try:
128
+ (fg, bg), attr = i
129
+ except ValueError as exc:
130
+ raise BarGraphError(f"satt not in (fg,bg:attr) form: {i!r}").with_traceback(exc.__traceback__) from exc
131
+ if not isinstance(fg, int) or fg >= len(attlist):
132
+ raise BarGraphError(f"fg not valid integer: {fg!r}")
133
+ if not isinstance(bg, int) or bg >= len(attlist):
134
+ raise BarGraphError(f"bg not valid integer: {fg!r}")
135
+ if fg <= bg:
136
+ raise BarGraphError(f"fg ({fg}) not > bg ({bg})")
137
+ self.satt = satt
138
+
139
+ def set_data(self, bardata, top: float, hlines=None) -> None:
140
+ """
141
+ Store bar data, bargraph top and horizontal line positions.
142
+
143
+ bardata -- a list of bar values.
144
+ top -- maximum value for segments within bardata
145
+ hlines -- None or a bar value marking horizontal line positions
146
+
147
+ bar values are [ segment1, segment2, ... ] lists where top is
148
+ the maximal value corresponding to the top of the bar graph and
149
+ segment1, segment2, ... are the values for the top of each
150
+ segment of this bar. Simple bar graphs will only have one
151
+ segment in each bar value.
152
+
153
+ Eg: if top is 100 and there is a bar value of [ 80, 30 ] then
154
+ the top of this bar will be at 80% of full height of the graph
155
+ and it will have a second segment that starts at 30%.
156
+ """
157
+ if hlines is not None:
158
+ hlines = sorted(hlines[:], reverse=True) # shallow copy
159
+
160
+ self.data = bardata, top, hlines
161
+ self._invalidate()
162
+
163
+ def _get_data(self, size: tuple[int, int]):
164
+ """
165
+ Return (bardata, top, hlines)
166
+
167
+ This function is called by render to retrieve the data for
168
+ the graph. It may be overloaded to create a dynamic bar graph.
169
+
170
+ This implementation will truncate the bardata list returned
171
+ if not all bars will fit within maxcol.
172
+ """
173
+ (maxcol, maxrow) = size
174
+ bardata, top, hlines = self.data
175
+ widths = self.calculate_bar_widths((maxcol, maxrow), bardata)
176
+
177
+ if len(bardata) > len(widths):
178
+ return bardata[: len(widths)], top, hlines
179
+
180
+ return bardata, top, hlines
181
+
182
+ def set_bar_width(self, width: int | None):
183
+ """
184
+ Set a preferred bar width for calculate_bar_widths to use.
185
+
186
+ width -- width of bar or None for automatic width adjustment
187
+ """
188
+ if width is not None and width <= 0:
189
+ raise ValueError(width)
190
+ self.bar_width = width
191
+ self._invalidate()
192
+
193
+ def calculate_bar_widths(self, size: tuple[int, int], bardata):
194
+ """
195
+ Return a list of bar widths, one for each bar in data.
196
+
197
+ If self.bar_width is None this implementation will stretch
198
+ the bars across the available space specified by maxcol.
199
+ """
200
+ (maxcol, _maxrow) = size
201
+
202
+ if self.bar_width is not None:
203
+ return [self.bar_width] * min(len(bardata), maxcol // self.bar_width)
204
+
205
+ if len(bardata) >= maxcol:
206
+ return [1] * maxcol
207
+
208
+ widths = []
209
+ grow = maxcol
210
+ remain = len(bardata)
211
+ for _row in bardata:
212
+ w = int(float(grow) / remain + 0.5)
213
+ widths.append(w)
214
+ grow -= w
215
+ remain -= 1
216
+ return widths
217
+
218
+ def selectable(self) -> Literal[False]:
219
+ """
220
+ Return False.
221
+ """
222
+ return False
223
+
224
+ def use_smoothed(self) -> bool:
225
+ return self.satt and get_encoding_mode() == "utf8"
226
+
227
+ def calculate_display(self, size: tuple[int, int]):
228
+ """
229
+ Calculate display data.
230
+ """
231
+ (maxcol, maxrow) = size
232
+ bardata, top, hlines = self.get_data((maxcol, maxrow)) # pylint: disable=no-member # metaclass defined
233
+ widths = self.calculate_bar_widths((maxcol, maxrow), bardata)
234
+
235
+ if self.use_smoothed():
236
+ disp = calculate_bargraph_display(bardata, top, widths, maxrow * 8)
237
+ disp = self.smooth_display(disp)
238
+
239
+ else:
240
+ disp = calculate_bargraph_display(bardata, top, widths, maxrow)
241
+
242
+ if hlines:
243
+ disp = self.hlines_display(disp, top, hlines, maxrow)
244
+
245
+ return disp
246
+
247
+ def hlines_display(self, disp, top: int, hlines, maxrow: int):
248
+ """
249
+ Add hlines to display structure represented as bar_type tuple
250
+ values:
251
+ (bg, 0-5)
252
+ bg is the segment that has the hline on it
253
+ 0-5 is the hline graphic to use where 0 is a regular underscore
254
+ and 1-5 are the UTF-8 horizontal scan line characters.
255
+ """
256
+ if self.use_smoothed():
257
+ shiftr = 0
258
+ r = [
259
+ (0.2, 1),
260
+ (0.4, 2),
261
+ (0.6, 3),
262
+ (0.8, 4),
263
+ (1.0, 5),
264
+ ]
265
+ else:
266
+ shiftr = 0.5
267
+ r = [
268
+ (1.0, 0),
269
+ ]
270
+
271
+ # reverse the hlines to match screen ordering
272
+ rhl = []
273
+ for h in hlines:
274
+ rh = float(top - h) * maxrow / top - shiftr
275
+ if rh < 0:
276
+ continue
277
+ rhl.append(rh)
278
+
279
+ # build a list of rows that will have hlines
280
+ hrows = []
281
+ last_i = -1
282
+ for rh in rhl:
283
+ i = int(rh)
284
+ if i == last_i:
285
+ continue
286
+ f = rh - i
287
+ for spl, chnum in r:
288
+ if f < spl:
289
+ hrows.append((i, chnum))
290
+ break
291
+ last_i = i
292
+
293
+ # fill hlines into disp data
294
+ def fill_row(row, chnum):
295
+ rout = []
296
+ for bar_type, width in row:
297
+ if isinstance(bar_type, int) and len(self.hatt) > bar_type:
298
+ rout.append(((bar_type, chnum), width))
299
+ continue
300
+ rout.append((bar_type, width))
301
+ return rout
302
+
303
+ o = []
304
+ k = 0
305
+ rnum = 0
306
+ for y_count, row in disp:
307
+ if k >= len(hrows):
308
+ o.append((y_count, row))
309
+ continue
310
+ end_block = rnum + y_count
311
+ while k < len(hrows) and hrows[k][0] < end_block:
312
+ i, chnum = hrows[k]
313
+ if i - rnum > 0:
314
+ o.append((i - rnum, row))
315
+ o.append((1, fill_row(row, chnum)))
316
+ rnum = i + 1
317
+ k += 1
318
+ if rnum < end_block:
319
+ o.append((end_block - rnum, row))
320
+ rnum = end_block
321
+
322
+ # assert 0, o
323
+ return o
324
+
325
+ def smooth_display(self, disp):
326
+ """
327
+ smooth (col, row*8) display into (col, row) display using
328
+ UTF vertical eighth characters represented as bar_type
329
+ tuple values:
330
+ ( fg, bg, 1-7 )
331
+ where fg is the lower segment, bg is the upper segment and
332
+ 1-7 is the vertical eighth character to use.
333
+ """
334
+ o = []
335
+ r = 0 # row remainder
336
+
337
+ def seg_combine(a, b):
338
+ (bt1, w1), (bt2, w2) = a, b
339
+ if (bt1, w1) == (bt2, w2):
340
+ return (bt1, w1), None, None
341
+ wmin = min(w1, w2)
342
+ l1 = l2 = None
343
+ if w1 > w2:
344
+ l1 = (bt1, w1 - w2)
345
+ elif w2 > w1:
346
+ l2 = (bt2, w2 - w1)
347
+ if isinstance(bt1, tuple):
348
+ return (bt1, wmin), l1, l2
349
+ if (bt2, bt1) not in self.satt:
350
+ if r < 4:
351
+ return (bt2, wmin), l1, l2
352
+ return (bt1, wmin), l1, l2
353
+ return ((bt2, bt1, 8 - r), wmin), l1, l2
354
+
355
+ def row_combine_last(count: int, row):
356
+ o_count, o_row = o[-1]
357
+ row = row[:] # shallow copy, so we don't destroy orig.
358
+ o_row = o_row[:]
359
+ widget_list = []
360
+ while row:
361
+ (bt, w), l1, l2 = seg_combine(o_row.pop(0), row.pop(0))
362
+ if widget_list and widget_list[-1][0] == bt:
363
+ widget_list[-1] = (bt, widget_list[-1][1] + w)
364
+ else:
365
+ widget_list.append((bt, w))
366
+ if l1:
367
+ o_row = [l1, *o_row]
368
+ if l2:
369
+ row = [l2, *row]
370
+
371
+ if o_row:
372
+ raise BarGraphError(o_row)
373
+
374
+ o[-1] = (o_count + count, widget_list)
375
+
376
+ # regroup into actual rows (8 disp rows == 1 actual row)
377
+ for y_count, row in disp:
378
+ if r:
379
+ count = min(8 - r, y_count)
380
+ row_combine_last(count, row)
381
+ y_count -= count # noqa: PLW2901
382
+ r += count
383
+ r = r % 8
384
+ if not y_count:
385
+ continue
386
+ if r != 0:
387
+ raise BarGraphError
388
+ # copy whole blocks
389
+ if y_count > 7:
390
+ o.append((y_count // 8 * 8, row))
391
+ y_count %= 8 # noqa: PLW2901
392
+ if not y_count:
393
+ continue
394
+ o.append((y_count, row))
395
+ r = y_count
396
+ return [(y // 8, row) for (y, row) in o]
397
+
398
+ def render(self, size: tuple[int, int], focus: bool = False) -> CompositeCanvas:
399
+ """
400
+ Render BarGraph.
401
+ """
402
+ (maxcol, maxrow) = size
403
+ disp = self.calculate_display((maxcol, maxrow))
404
+
405
+ combinelist = []
406
+ for y_count, row in disp:
407
+ widget_list = []
408
+ for bar_type, width in row:
409
+ if isinstance(bar_type, tuple):
410
+ if len(bar_type) == 3:
411
+ # vertical eighths
412
+ fg, bg, k = bar_type
413
+ a = self.satt[(fg, bg)]
414
+ t = self.eighths[k] * width
415
+ else:
416
+ # horizontal lines
417
+ bg, k = bar_type
418
+ a = self.hatt[bg]
419
+ t = self.hlines[k] * width
420
+ else:
421
+ a = self.attr[bar_type]
422
+ t = self.char[bar_type] * width
423
+ widget_list.append((a, t))
424
+ c = Text(widget_list).render((maxcol,))
425
+ if c.rows() != 1:
426
+ raise BarGraphError("Invalid characters in BarGraph!")
427
+ combinelist += [(c, None, False)] * y_count
428
+
429
+ canv = CanvasCombine(combinelist)
430
+ return canv
431
+
432
+
433
+ def calculate_bargraph_display(bardata, top: float, bar_widths: list[int], maxrow: int):
434
+ """
435
+ Calculate a rendering of the bar graph described by data, bar_widths
436
+ and height.
437
+
438
+ bardata -- bar information with same structure as BarGraph.data
439
+ top -- maximal value for bardata segments
440
+ bar_widths -- list of integer column widths for each bar
441
+ maxrow -- rows for display of bargraph
442
+
443
+ Returns a structure as follows:
444
+ [ ( y_count, [ ( bar_type, width), ... ] ), ... ]
445
+
446
+ The outer tuples represent a set of identical rows. y_count is
447
+ the number of rows in this set, the list contains the data to be
448
+ displayed in the row repeated through the set.
449
+
450
+ The inner tuple describes a run of width characters of bar_type.
451
+ bar_type is an integer starting from 0 for the background, 1 for
452
+ the 1st segment, 2 for the 2nd segment etc..
453
+
454
+ This function should complete in approximately O(n+m) time, where
455
+ n is the number of bars displayed and m is the number of rows.
456
+ """
457
+
458
+ if len(bardata) != len(bar_widths):
459
+ raise BarGraphError
460
+
461
+ maxcol = sum(bar_widths)
462
+
463
+ # build intermediate data structure
464
+ rows = [None] * maxrow
465
+
466
+ def add_segment(seg_num: int, col: int, row: int, width: int, rows=rows) -> None:
467
+ if rows[row]:
468
+ last_seg, last_col, last_end = rows[row][-1]
469
+ if last_end > col:
470
+ if last_col >= col:
471
+ del rows[row][-1]
472
+ else:
473
+ rows[row][-1] = (last_seg, last_col, col)
474
+ elif last_seg == seg_num and last_end == col:
475
+ rows[row][-1] = (last_seg, last_col, last_end + width)
476
+ return
477
+ elif rows[row] is None:
478
+ rows[row] = []
479
+ rows[row].append((seg_num, col, col + width))
480
+
481
+ col = 0
482
+ barnum = 0
483
+ for bar in bardata:
484
+ width = bar_widths[barnum]
485
+ if width < 1:
486
+ continue
487
+ # loop through in reverse order
488
+ tallest = maxrow
489
+ segments = scale_bar_values(bar, top, maxrow)
490
+ for k in range(len(bar) - 1, -1, -1):
491
+ s = segments[k]
492
+
493
+ if s >= maxrow:
494
+ continue
495
+ s = max(s, 0)
496
+ if s < tallest:
497
+ # add only properly-overlapped bars
498
+ tallest = s
499
+ add_segment(k + 1, col, s, width)
500
+ col += width
501
+ barnum += 1
502
+
503
+ # print(repr(rows))
504
+ # build rowsets data structure
505
+ rowsets = []
506
+ y_count = 0
507
+ last = [(0, maxcol)]
508
+
509
+ for r in rows:
510
+ if r is None:
511
+ y_count = y_count + 1
512
+ continue
513
+ if y_count:
514
+ rowsets.append((y_count, last))
515
+ y_count = 0
516
+
517
+ i = 0 # index into "last"
518
+ la, ln = last[i] # last attribute, last run length
519
+ c = 0 # current column
520
+ o = [] # output list to be added to rowsets
521
+ for seg_num, start, end in r:
522
+ while start > c + ln:
523
+ o.append((la, ln))
524
+ i += 1
525
+ c += ln
526
+ la, ln = last[i]
527
+
528
+ if la == seg_num:
529
+ # same attribute, can combine
530
+ o.append((la, end - c))
531
+ else:
532
+ if start - c > 0:
533
+ o.append((la, start - c))
534
+ o.append((seg_num, end - start))
535
+
536
+ if end == maxcol:
537
+ i = len(last)
538
+ break
539
+
540
+ # skip past old segments covered by new one
541
+ while end >= c + ln:
542
+ i += 1
543
+ c += ln
544
+ la, ln = last[i]
545
+
546
+ if la != seg_num:
547
+ ln = c + ln - end
548
+ c = end
549
+ continue
550
+
551
+ # same attribute, can extend
552
+ oa, on = o[-1]
553
+ on += c + ln - end
554
+ o[-1] = oa, on
555
+
556
+ i += 1
557
+ c += ln
558
+ if c == maxcol:
559
+ break
560
+ if i >= len(last):
561
+ raise ValueError(repr((on, maxcol)))
562
+ la, ln = last[i]
563
+
564
+ if i < len(last):
565
+ o += [(la, ln)] + last[i + 1 :]
566
+ last = o
567
+ y_count += 1
568
+
569
+ if y_count:
570
+ rowsets.append((y_count, last))
571
+
572
+ return rowsets
573
+
574
+
575
+ class GraphVScale(Widget):
576
+ _sizing = frozenset([Sizing.BOX])
577
+
578
+ def __init__(self, labels, top: float) -> None:
579
+ """
580
+ GraphVScale( [(label1 position, label1 markup),...], top )
581
+ label position -- 0 < position < top for the y position
582
+ label markup -- text markup for this label
583
+ top -- top y position
584
+
585
+ This widget is a vertical scale for the BarGraph widget that
586
+ can correspond to the BarGraph's horizontal lines
587
+ """
588
+ super().__init__()
589
+ self.set_scale(labels, top)
590
+
591
+ def set_scale(self, labels, top: float) -> None:
592
+ """
593
+ set_scale( [(label1 position, label1 markup),...], top )
594
+ label position -- 0 < position < top for the y position
595
+ label markup -- text markup for this label
596
+ top -- top y position
597
+ """
598
+
599
+ labels = sorted(labels[:], reverse=True) # shallow copy
600
+
601
+ self.pos = []
602
+ self.txt = []
603
+ for y, markup in labels:
604
+ self.pos.append(y)
605
+ self.txt.append(Text(markup))
606
+ self.top = top
607
+
608
+ def selectable(self) -> Literal[False]:
609
+ """
610
+ Return False.
611
+ """
612
+ return False
613
+
614
+ def render(self, size: tuple[int, int], focus: bool = False) -> SolidCanvas | CompositeCanvas:
615
+ """
616
+ Render GraphVScale.
617
+ """
618
+ (maxcol, maxrow) = size
619
+ pl = scale_bar_values(self.pos, self.top, maxrow)
620
+
621
+ combinelist = []
622
+ rows = 0
623
+ for p, t in zip(pl, self.txt):
624
+ p -= 1 # noqa: PLW2901
625
+ if p >= maxrow:
626
+ break
627
+ if p < rows:
628
+ continue
629
+ c = t.render((maxcol,))
630
+ if p > rows:
631
+ run = p - rows
632
+ c = CompositeCanvas(c)
633
+ c.pad_trim_top_bottom(run, 0)
634
+ rows += c.rows()
635
+ combinelist.append((c, None, False))
636
+ if not combinelist:
637
+ return SolidCanvas(" ", size[0], size[1])
638
+
639
+ c = CanvasCombine(combinelist)
640
+ if maxrow - rows:
641
+ c.pad_trim_top_bottom(0, maxrow - rows)
642
+ return c
643
+
644
+
645
+ def scale_bar_values(bar, top: float, maxrow: int) -> list[int]:
646
+ """
647
+ Return a list of bar values aliased to integer values of maxrow.
648
+ """
649
+ return [maxrow - int(float(v) * maxrow / top + 0.5) for v in bar]