Widget

Basic blocks for building interactive elements.

This is a low-level module upon which yuio.io builds its higher-level abstraction.

Widget basics

All widgets are are derived from the Widget class, where they implement event handlers, layout and rendering routines. Specifically, Widget.layout() and Widget.draw() are required to implement a widget.

class yuio.widget.Widget

Base class for all interactive console elements.

Widgets are displayed with their run() method. They always go through the same event loop:

flowchart TD
Start([Start]) --> Layout["`layout()`"]
Layout --> Draw["`draw()`"]
Draw -->|Wait for keyboard event| Event["`Event()`"]
Event --> Result{{Returned result?}}
Result -->|no| Layout
Result -->|yes| Finish([Finish])

Widgets run indefinitely until they stop themselves and return a value. For example, Input will return when user presses Enter. When widget needs to stop, it can return the Result() class from its event handler.

For typing purposes, Widget is generic. That is, Widget[T] returns T from its run() method. So, Input, for example, is Widget[str].

Some widgets are Widget[Never] (see typing.Never), indicating that they don’t ever stop. Others are Widget[None], indicating that they stop, but don’t return a value.

event(
e: KeyboardEvent,
/,
) Result[T_co] | None

Handle incoming keyboard event.

By default, this function dispatches event to handlers registered via bind(). If no handler is found, it calls default_event_handler().

default_event_handler(
e: KeyboardEvent,
/,
) Result[T_co] | None

Process any event that wasn’t caught by other event handlers.

abstractmethod layout(rc: RenderContext, /) tuple[int, int]

Prepare widget for drawing, and recalculate its dimensions according to new frame dimensions.

Yuio’s widgets always take all available width. They should return their minimum height that they will definitely take, and their maximum height that they can potentially take.

abstractmethod draw(rc: RenderContext, /)

Draw the widget.

Render context’s drawing frame dimensions are guaranteed to be between the minimum and the maximum height returned from the last call to layout().

final run(
term: Term,
theme: Theme,
/,
) T_co

Read user input and run the widget.

property help_data: WidgetHelp

Data for displaying help messages.

See help() for more info.

class yuio.widget.Result(value: T_co)

Result of a widget run.

We have to wrap the return value of event processors into this class. Otherwise we won’t be able to distinguish between returning None as result of a Widget[None], and not returning anything.

value: T_co

Result of a widget run.

yuio.widget.bind(
key: Key | str,
*,
ctrl: bool = False,
alt: bool = False,
shift: bool = False,
show_in_inline_help: bool = False,
show_in_detailed_help: bool = True,
) _Binding

Register an event handler for a widget.

Widget’s methods can be registered as handlers for keyboard events. When a new event comes in, it is checked to match arguments of this decorator. If there is a match, the decorated method is called instead of the Widget.default_event_handler().

Note

Ctrl+L and F1 are always reserved by the widget itself.

If show_in_help is True, this binding will be shown in the widget’s inline help. If show_in_detailed_help is True, this binding will be shown in the widget’s help menu.

Example:

class MyWidget(Widget):
    @bind(Key.ENTER)
    def enter(self):
        # all `ENTER` events go here.
        ...

    def default_event_handler(self, e: KeyboardEvent):
        # all non-`ENTER` events go here (including `ALT+ENTER`).
        ...
class yuio.widget.Key(*values)

Non-character keys.

ENTER = 1

Enter key.

ESCAPE = 2

Escape key.

INSERT = 3

Insert key.

DELETE = 4

Delete key.

BACKSPACE = 5

Backspace key.

TAB = 6

Tab key.

HOME = 7

Home key.

END = 8

End key.

PAGE_UP = 9

PageUp key.

PAGE_DOWN = 10

PageDown key.

ARROW_UP = 11

ArrowUp key.

ARROW_DOWN = 12

ArrowDown key.

ARROW_LEFT = 13

ArrowLeft key.

ARROW_RIGHT = 14

ArrowRight key.

F1 = 15

F1 key.

F2 = 16

F2 key.

F3 = 17

F3 key.

F4 = 18

F4 key.

F5 = 19

F5 key.

F6 = 20

F6 key.

F7 = 21

F7 key.

F8 = 22

F8 key.

F9 = 23

F9 key.

F10 = 24

F10 key.

F11 = 25

F11 key.

F12 = 26

F12 key.

PASTE = 27

Triggered when a text is pasted into a terminal.

class yuio.widget.KeyboardEvent(
key: Key | str,
ctrl: bool = False,
alt: bool = False,
shift: bool = False,
*,
paste_str: str | None = None,
)

A single keyboard event.

Warning

Protocol for interacting with terminals is quite old, and not all terminals support all keystroke combinations.

Use python -m yuio.scripts.showkey to check how your terminal reports keystrokes, and how Yuio interprets them.

key: Key | str

Which key was pressed? Can be a single character, or a Key for non-character keys.

ctrl: bool

Whether a Ctrl modifier was pressed with keystroke.

For letter keys modified with control, the letter is always lowercase; if terminal supports reporting Shift being pressed, the shift attribute will be set. This does not affect punctuation keys, though:

# `Ctrl+X` was pressed.
KeyboardEvent("x", ctrl=True)

# `Ctrl+Shift+X` was pressed. Not all terminals are able
# to report this correctly, though.
KeyboardEvent("x", ctrl=True, shift=True)

# This can't happen.
KeyboardEvent("X", ctrl=True)

# `Ctrl+_` was pressed. On most keyboards, the actual keystroke
# is `Ctrl+Shift+-`, but most terminals can't properly report this.
KeyboardEvent("_", ctrl=True)
alt: bool

Whether an Alt (Option on macs) modifier was pressed with keystroke.

shift: bool

Whether a Shift modifier was pressed with keystroke.

Note that, when letters are typed with shift, they will not have this flag. Instead, their upper case version will be set as key:

KeyboardEvent("x")  # `X` was pressed.
KeyboardEvent("X")  # `Shift+X` was pressed.

Warning

Only Shift+Tab can be reliably reported by all terminals.

paste_str: str | None

If key is Key.PASTE, this attribute will contain pasted string.

Drawing and rendering widgets

Widgets are rendered through RenderContext. It provides simple facilities to print characters on screen and manipulate screen cursor.

final class yuio.widget.RenderContext(term: Term, theme: Theme, /)

A canvas onto which widgets render themselves.

This class represents a canvas with size equal to the available space on the terminal. Like a real terminal, it has a character grid and a virtual cursor that can be moved around freely.

Before each render, context’s canvas is cleared, and then widgets print themselves onto it. When render ends, context compares new canvas with what’s been rendered previously, and then updates those parts of the real terminal’s grid that changed between renders.

This approach allows simplifying widgets (they don’t have to track changes and do conditional screen updates themselves), while still minimizing the amount of data that’s sent between the program and the terminal. It is especially helpful with rendering larger widgets over ssh.

property term: Term

Terminal where we render the widgets.

property theme: Theme

Current color theme.

property spinner_state: int

A timer that ticks once every Theme.spinner_update_rate_ms.

frame(
x: int,
y: int,
/,
*,
width: int | None = None,
height: int | None = None,
)

Override drawing frame.

Widgets are always drawn in the frame’s top-left corner, and they can take the entire frame size.

The idea is that, if you want to draw a widget at specific coordinates, you make a frame and draw the widget inside said frame.

When new frame is created, cursor’s position and color are reset. When frame is dropped, they are restored. Therefore, drawing widgets in a frame will not affect current drawing state.

Example:

>>> rc = RenderContext(term, theme)
>>> rc.prepare()

>>> # By default, our frame is located at (0, 0)...
>>> rc.write("+")

>>> # ...and spans the entire canvas.
>>> print(rc.width, rc.height)
20 5

>>> # Let's write something at (4, 0).
>>> rc.set_pos(4, 0)
>>> rc.write("Hello, world!")

>>> # Now we set our drawing frame to be at (2, 2).
>>> with rc.frame(2, 2):
...     # Out current pos was reset to the frame's top-left corner,
...     # which is now (2, 2).
...     rc.write("+")
...
...     # Frame dimensions were automatically reduced.
...     print(rc.width, rc.height)
...
...     # Set pos and all other functions work relative
...     # to the current frame, so writing at (4, 0)
...     # in the current frame will result in text at (6, 2).
...     rc.set_pos(4, 0)
...     rc.write("Hello, world!")
18 3

>>> rc.render()
+   Hello, world!

  +   Hello, world!

Usually you don’t have to think about frames. If you want to stack multiple widgets one on top of another, simply use VerticalLayout. In cases where it’s not enough though, you’ll have to call layout() for each of the nested widgets, and then manually create frames and execute draw() methods:

class MyWidget(Widget):
    # Let's say we want to print a text indented by four spaces,
    # and limit its with by 15. And we also want to print a small
    # un-indented heading before it.

    def __init__(self):
        # This is the text we'll print.
        self._nested_widget = Text(
            "very long paragraph which potentially can span multiple lines"
        )

    def layout(self, rc: RenderContext) -> tuple[int, int]:
        # The text will be placed at (4, 1), and we'll also limit
        # its width. So we'll reflect those constrains
        # by arranging a drawing frame.
        with rc.frame(4, 1, width=min(rc.width - 4, 15)):
            min_h, max_h = self._nested_widget.layout(rc)

        # Our own widget will take as much space as the nested text,
        # plus one line for our heading.
        return min_h + 1, max_h + 1

    def draw(self, rc: RenderContext):
        # Print a small heading.
        rc.set_color_path("bold")
        rc.write("Small heading")

        # And draw our nested widget, controlling its position
        # via a frame.
        with rc.frame(4, 1, width=min(rc.width - 4, 15)):
            self._nested_widget.draw(rc)
property width: int

Get width of the current frame.

property height: int

Get height of the current frame.

property canvas_width: int

Get width of the terminal.

property canvas_height: int

Get height of the terminal.

set_pos(x: int, y: int, /)

Set current cursor position within the frame.

move_pos(dx: int, dy: int, /)

Move current cursor position by the given amount.

new_line()

Move cursor to new line within the current frame.

set_final_pos(x: int, y: int, /)

Set position where the cursor should end up after everything has been rendered.

By default, cursor will end up at the beginning of the last line. Components such as Input can modify this behavior and move the cursor into the correct position.

set_color_path(path: str, /)

Set current color by fetching it from the theme by path.

set_color(color: Color, /)

Set current color.

reset_color()

Set current color to the default color of the terminal.

get_msg_decoration(name: str, /) str

Get message decoration by name.

write(
text: AnyString,
/,
*,
max_width: int | None = None,
)

Write string at the current position using the current color. Move cursor while printing.

While the displayed text will not be clipped at frame’s borders, its width can be limited by passing max_width. Note that rc.write(text, max_width) is not the same as rc.write(text[:max_width]), because the later case doesn’t account for double-width characters.

All whitespace characters in the text, including tabs and newlines, will be treated as single spaces. If you need to print multiline text, use yuio.string.ColorizedString.wrap() and write_text().

Example:

>>> rc = RenderContext(term, theme)
>>> rc.prepare()

>>> rc.write("Hello, world!")
>>> rc.new_line()
>>> rc.write("Hello,\nworld!")
>>> rc.new_line()
>>> rc.write(
...     "Hello, 🌍!<this text will be clipped>",
...     max_width=10
... )
>>> rc.new_line()
>>> rc.write(
...     "Hello, 🌍!<this text will be clipped>"[:10]
... )
>>> rc.new_line()

>>> rc.render()
Hello, world!
Hello, world!
Hello, 🌍!
Hello, 🌍!<

Notice that "\n" on the second line was replaced with a space. Notice also that the last line wasn’t properly clipped.

write_text(
lines: Iterable[AnyString],
/,
*,
max_width: int | None = None,
)

Write multiple lines.

Each line is printed using write(), so newline characters and tabs within each line are replaced with spaces. Use yuio.string.ColorizedString.wrap() to properly handle them.

After each line, the cursor is moved one line down, and back to its original horizontal position.

Example:

>>> rc = RenderContext(term, theme)
>>> rc.prepare()

>>> # Cursor is at (0, 0).
>>> rc.write("+ > ")

>>> # First line is printed at the cursor's position.
>>> # All consequent lines are horizontally aligned with first line.
>>> rc.write_text(["Hello,", "world!"])

>>> # Cursor is at the last line.
>>> rc.write("+")

>>> rc.render()
+ > Hello,
    world!+


bell()

Ring a terminal bell.

make_repr_context(
*,
multiline: bool | None = None,
highlighted: bool | None = None,
max_depth: int | None = None,
width: int | None = None,
) ReprContext

Create a new ReprContext for rendering colorized strings inside widgets.

Parameters:
Returns:

a new repr context suitable for rendering colorized strings.

prepare(
*,
full_redraw: bool = False,
alternative_buffer: bool = False,
reset_term_pos: bool = False,
)

Reset output canvas and prepare context for a new round of widget formatting.

clear_screen()

Clear screen and prepare for a full redraw.

render()

Render current canvas onto the terminal.

finalize()

Erase any rendered widget and move cursor to the initial position.

Stacking widgets together

To get help with drawing multiple widgets and setting their own frames, you can use the VerticalLayout class:

class yuio.widget.VerticalLayout(*widgets: Widget[object])

Helper class for stacking widgets together.

You can stack your widgets together, then calculate their layout and draw them all at once.

You can use this class as a helper component inside your own widgets, or you can use it as a standalone widget. See VerticalLayoutBuilder for an example.

append(widget: Widget[Any], /)

Add a widget to the end of the stack.

extend(
widgets: Iterable[Widget[Any]],
/,
)

Add multiple widgets to the end of the stack.

event(
e: KeyboardEvent,
) Result[T] | None

Dispatch event to the widget that was added with receive_events=True.

See VerticalLayoutBuilder for details.

layout(rc: RenderContext, /) tuple[int, int]

Calculate layout of the entire stack.

draw(rc: RenderContext, /)

Draw the stack according to the calculated layout and available height.

final class yuio.widget.VerticalLayoutBuilder

Builder for VerticalLayout that allows for precise control of keyboard events.

By default, VerticalLayout does not handle incoming keyboard events. However, you can create VerticalLayout that forwards all keyboard events to a particular widget within the stack:

widget = VerticalLayout.builder() \
    .add(Line("Enter something:")) \
    .add(Input(), receive_events=True) \
    .build()

result = widget.run(term, theme)
add(
widget: Widget[Any],
/,
*,
receive_events=False,
) Any

Add a new widget to the bottom of the layout.

If receive_events is True, all incoming events will be forwarded to the added widget. Only the latest widget added with receive_events=True will receive events.

This method does not mutate the builder, but instead returns a new one. Use it with method chaining.

Widget help

Widgets automatically generate help: the help menu is available via the F1 key, and there’s also inline help that is displayed under the widget.

By default, help items are generated from event handler docstrings: all event handlers that have them will be displayed in the help menu.

You can control which keybindings appear in the help menu and inline help by supplying show_in_inline_help and show_in_detailed_help arguments to the bind() function.

For even more detailed customization you can decorate an event handler with the help() decorator:

yuio.widget.help(
*,
group: str = 'Actions',
inline_msg: str | None = None,
long_msg: str | None = None,
msg: str | None = None,
) _Help

Set options for how this callback should be displayed.

This decorator controls automatic generation of help messages for a widget.

Parameters:
  • group – title of a group that this action will appear in when the user opens a help menu. Groups appear in order of declaration of their first element.

  • inline_msg – this parameter overrides a message in the inline help. By default, it will be taken from a docstring.

  • long_msg – this parameter overrides a message in the help menu. By default, it will be taken from a docstring.

  • msg – a shortcut parameter for setting both inline_msg and long_msg at the same time.

Example:

class MyWidget(Widget):
    NAVIGATE = "Navigate"

    @bind(Key.TAB)
    @help(group=NAVIGATE)
    def tab(self):
        """next item"""
        ...

    @bind(Key.TAB, shift=True)
    @help(group=NAVIGATE)
    def shift_tab(self):
        """previous item"""
        ...

Lastly, you can override Widget.help_data and generate the WidgetHelp yourself:

class yuio.widget.WidgetHelp(
inline_help: list[Action] = <factory>,
groups: dict[str,
list[~yuio.widget.Action]]=<factory>,
)

Data for automatic help generation.

Warning

Do not modify contents of this class in-place. This might break layout caching in the widget rendering routine, which will cause displaying outdated help messages.

Use the provided helpers to modify contents of this class.

inline_help: list[Action]

List of actions to show in the inline help.

groups: dict[str, list[Action]]

Dict of group titles and actions to show in the help menu.

with_action(
*bindings: _Binding | ActionKey,
group: str = 'Actions',
msg: str | None = None,
inline_msg: str | None = None,
long_msg: str | None = None,
prepend: bool = False,
prepend_group: bool = False,
) WidgetHelp

Return a new WidgetHelp that has an extra action.

Parameters:
  • bindings – keys that trigger an action.

  • group – title of a group that this action will appear in when the user opens a help menu. Groups appear in order of declaration of their first element.

  • inline_msg – this parameter overrides a message in the inline help. By default, it will be taken from a docstring.

  • long_msg – this parameter overrides a message in the help menu. By default, it will be taken from a docstring.

  • msg – a shortcut parameter for setting both inline_msg and long_msg at the same time.

  • prepend – if True, action will be added to the beginning of its group.

  • prepend_group – if True, group will be added to the beginning of the help menu.

merge(other: WidgetHelp, /) WidgetHelp

Merge this help data with another one and return a new instance of WidgetHelp.

Parameters:

other – other WidgetHelp for merging.

without_group(title: str, /) WidgetHelp

Return a new WidgetHelp that has a group with the given title removed.

Parameters:

title – title to remove.

rename_group(
title: str,
new_title: str,
/,
) WidgetHelp

Return a new WidgetHelp that has a group with the given title renamed.

Parameters:
  • title – title to replace.

  • new_title – new title.

class yuio.widget.ActionKey

A single key associated with an action. Can be either a hotkey or a string with an arbitrary description.

class yuio.widget.ActionKeys

A list of keys associated with an action.

class yuio.widget.Action

An action itself, i.e. a set of hotkeys and a description for them.

Pre-defined widgets

class yuio.widget.Line(text: Colorable, /)

A widget that prints a single line of text.

class yuio.widget.Text(text: Colorable, /)

A widget that prints wrapped text.

class yuio.widget.Input(
*,
text: str = '',
pos: int | None = None,
placeholder: str = '',
decoration_path: str = 'menu/input/decoration',
allow_multiline: bool = False,
allow_special_characters: bool = False,
)

An input box.

Note

Input is not optimized to handle long texts or long editing sessions. It’s best used to get relatively short answers from users with yuio.io.ask(). If you need to edit large text, especially multiline, consider using yuio.io.edit() instead.

Parameters:
  • text – initial text.

  • pos – initial cursor position, calculated as an offset from beginning of the text. Should be 0 <= pos <= len(text).

  • placeholder – placeholder text, shown when input is empty.

  • decoration_path – path that will be used to look up decoration printed before the input box.

  • allow_multiline – if True, Enter key makes a new line, otherwise it accepts input. In this mode, newlines in pasted text are also preserved.

  • allow_special_characters – If True, special characters like tabs or escape symbols are preserved and not replaced with whitespaces.

class yuio.widget.SecretInput(
*,
text: str = '',
pos: int | None = None,
placeholder: str = '',
decoration_path: str = 'menu/input/decoration',
)

An input box that shows stars instead of entered symbols.

Parameters:
  • text – initial text.

  • pos – initial cursor position, calculated as an offset from beginning of the text. Should be 0 <= pos <= len(text).

  • placeholder – placeholder text, shown when input is empty.

  • decoration – decoration printed before the input box.

class yuio.widget.Grid(
options: list[Option[T]],
/,
*,
active_item_decoration_path: str = 'menu/choice/decoration/active_item',
selected_item_decoration_path: str = '',
deselected_item_decoration_path: str = '',
default_index: int | None = 0,
min_rows: int | None = 5,
)

A helper widget that shows up in Choice and InputWithCompletion.

Note

On its own, Grid doesn’t return when you press Enter or Ctrl+D. It’s meant to be used as part of another widget.

Parameters:
  • options – list of options displayed in the grid.

  • decoration – decoration printed before the selected option.

  • default_index – index of the initially selected option.

  • min_rows – minimum number of rows that the grid should occupy before it starts splitting options into columns. This option is ignored if there isn’t enough space on the screen.

class yuio.widget.Option(
value: T_co,
display_text: str,
*,
display_text_prefix: str = '',
display_text_suffix: str = '',
comment: str | None = None,
color_tag: str | None = None,
selected: bool = False,
)

An option for the Grid and Choice widgets.

value: T_co

Option’s value that will be returned from widget.

display_text: str

What should be displayed in the autocomplete list.

display_text_prefix: str

Prefix that will be displayed before display_text.

display_text_suffix: str

Suffix that will be displayed after display_text.

comment: str | None

Option’s short comment.

color_tag: str | None

Option’s color tag.

This color tag will be used to display option. Specifically, color for the option will be looked up py path :samp:menu/{element}:choice/{status}/{color_tag}.

selected: bool

For multi-choice widgets, whether this option is chosen or not.

class yuio.widget.Choice(options: list[~yuio.widget.Option[~yuio.widget.T]], /, *, mapper: ~typing.Callable[[~yuio.widget.Option[~yuio.widget.T]], str] = <function Choice.<lambda>>, filter: ~typing.Callable[[~yuio.widget.Option[~yuio.widget.T], str], bool] | None = None, default_index: int = 0, search_bar_decoration_path: str = 'menu/input/decoration_search', active_item_decoration_path: str = 'menu/choice/decoration/active_item')

Allows choosing from pre-defined options.

Parameters:
  • options – list of choice options.

  • mapper – maps option to a text that will be used for filtering. By default, uses Option.display_text. This argument is ignored if a custom filter is given.

  • filter – customizes behavior of list filtering. The default filter extracts text from an option using the mapper, and checks if it starts with the search query.

  • default_index – index of the initially selected option.

class yuio.widget.Multiselect(options: list[~yuio.widget.Option[~yuio.widget.T]], /, *, mapper: ~typing.Callable[[~yuio.widget.Option[~yuio.widget.T]], str] = <function Multiselect.<lambda>>, filter: ~typing.Callable[[~yuio.widget.Option[~yuio.widget.T], str], bool] | None = None, search_bar_decoration_path: str = 'menu/input/decoration_search', active_item_decoration_path: str = 'menu/choice/decoration/active_item', selected_item_decoration_path: str = 'menu/choice/decoration/selected_item', deselected_item_decoration_path: str = 'menu/choice/decoration/deselected_item')

Like Choice, but allows selecting multiple items.

Parameters:
  • options – list of choice options.

  • mapper – maps option to a text that will be used for filtering. By default, uses Option.display_text. This argument is ignored if a custom filter is given.

  • filter – customizes behavior of list filtering. The default filter extracts text from an option using the mapper, and checks if it starts with the search query.

  • default_index – index of the initially selected option.

class yuio.widget.InputWithCompletion(
completer: Completer,
/,
*,
placeholder: str = '',
decoration_path: str = 'menu/input/decoration',
active_item_decoration_path: str = 'menu/choice/decoration/active_item',
)

An input box with tab completion.

class yuio.widget.Map(
inner: Widget[U],
fn: Callable[[U], T],
/,
)

A wrapper that maps result of the given widget using the given function.

Example:

>>> # Run `Input` widget, then parse user input as `int`.
>>> int_input = Map(Input(), int)
>>> int_input.run(term, theme)
10
class yuio.widget.Apply(
inner: Widget[T],
fn: Callable[[T], None],
/,
)

A wrapper that applies the given function to the result of a wrapped widget.

Example:

>>> # Run `Input` widget, then print its output before returning
>>> print_output = Apply(Input(), print)
>>> result = print_output.run(term, theme)
foobar!
>>> result
'foobar!'
class yuio.widget.Task(msg: str, /, *args, comment: str | None = None)

Widget that’s used to render Tasks.

class Status(*values)

Task status.

DONE = 'done'

Task has finished successfully.

ERROR = 'error'

Task has finished with an error.

RUNNING = 'running'

Task is running.

PENDING = 'pending'

Task is waiting to start.

progress(
*args: float | int | None,
unit: str = '',
ndigits: int | None = None,
)

See progress().

progress_size(
done: float | int,
total: float | int,
/,
*,
ndigits: int = 2,
)

See progress_size().

progress_scale(
done: float | int,
total: float | int,
/,
*,
unit: str = '',
ndigits: int = 2,
)

See progress_scale().

comment(comment: str | None, /, *args)

See comment().

_format_task(ctx: ReprContext) ColorizedString

Format this task for printing to the log.

_format_task_msg(
ctx: ReprContext,
) ColorizedString

Format task’s message.

_format_task_comment(
rc: RenderContext,
) ColorizedString | None

Format task’s comment.

_draw_task(rc: RenderContext)

Draw task.

_draw_task_progress(rc: RenderContext)

Draw number that indicates task’s progress.

_draw_task_progressbar(rc: RenderContext)

Draw task’s progressbar.