Usage Examples

Quick Example

A simple counter with two buttons to increment and decrement a value:

from pyiced import (
    Align, button, ButtonState, column, container, IcedApp, Length, text,
)


class ExampleApp(IcedApp):
    def __init__(self):
        self.__incr_button_state = ButtonState()
        self.__decr_button_state = ButtonState()
        self.__value = 0

    def title(self):
        return 'Counter'

    def view(self):
        increment_button = button(
            self.__incr_button_state,  # To track the state across redraws.
            text('Increment'),         # This is content on the button.
            on_press='incr',           # This value is received in update().
        )
        value_label = text(f'{self.__value}', size=50)
        decrement_button = button(
            self.__decr_button_state,
            text('Decrement'),
            on_press='decr',
        )
        return container(
            column(
                [increment_button, value_label, decrement_button],
                align_items=Align.CENTER,
            ),
            padding=20, align_x=Align.CENTER, align_y=Align.CENTER,
            width=Length.FILL, height=Length.FILL,
        )

    def update(self, msg, clipboard):
        # When an event occurs, this method is called.
        # It can optionally return a list of async functions,
        # to handle the event.
        match msg:
            case 'incr':
                self.__value += 1
            case 'decr':
                self.__value -= 1


if __name__ == '__main__':
    # This function only returns if there is an error on start-up.
    # Otherwise the program gets terminated when the window is closed.
    ExampleApp().run()

Custom Styles

from pyiced import (
    Align, button, ButtonState, ButtonStyle, ButtonStyleSheet, Color,
    container, ContainerStyle, IcedApp, Length, Settings, text,
    WindowSettings,
)


class ButtonExample(IcedApp):
    class settings(Settings):
        class window(WindowSettings):
            size = (640, 320)

    def __init__(self):
        self.__button_state = ButtonState()

    def title(self):
        return 'A Button'

    def view(self):
        styled_button = button(
            self.__button_state,
            text('Hello, world!', size=40),
            '',
            style=ButtonStyleSheet(ButtonStyle(
                shadow_offset=(8, 8), border_radius=40, border_width=6,
                background=Color(0.17, 0.17, 0.17),
                border_color=Color(0.95, 0.87, 0.22),
                text_color=Color(1.00, 0.18, 0.13)
            )),
            padding=40,
        )
        return container(
            styled_button,
            style=ContainerStyle(background=Color(0.38, 0.60, 0.23)),
            padding=20, align_x=Align.CENTER, align_y=Align.CENTER,
            width=Length.FILL, height=Length.FILL,
        )


if __name__ == '__main__':
    ButtonExample().run()

Asychronous Messages

new() and update() can either return a Message (or a sequence of messages in the latter case), or a coroutine / coroutines to asynchronously generate a messages.

from asyncio import open_connection
from contextlib import closing

from pyiced import (
    Align, Color, container, ContainerStyle, Font, IcedApp, Length,
    Settings, text, WindowSettings,
)


class AsyncMessageExample(IcedApp):
    def __init__(self):
        self.__font = None

    class settings(Settings):
        class window(WindowSettings):
            size = (640, 320)

    def title(self):
        return 'Asynchronous Messages'

    def new(self):
        return [load_font()]

    def update(self, msg, clipboard):
        match msg:
            case ('Font', font):
                self.__font = font

    def view(self):
        return container(
            text('Hello, world!', size=80, font=self.__font),
            style=ContainerStyle(
                text_color=Color(0.95, 0.87, 0.22),
                background=Color(0.38, 0.60, 0.23),
            ),
            padding=20, align_x=Align.CENTER, align_y=Align.CENTER,
            width=Length.FILL, height=Length.FILL,
        )


async def load_font():
    FONT_NAME = 'Yellowtail'
    FONT_HOST = 'fonts.cdnfonts.com'
    FONT_PATH = '/s/16054/Yellowtail-Regular.ttf'

    query = (
        f"GET {FONT_PATH} HTTP/1.0\r\n"
        f"Host: {FONT_HOST}\r\n"
        f"Connection: closed\r\n"
        f"\r\n"
    ).encode('US-ASCII')

    reader, writer = await open_connection(FONT_HOST, 443, ssl=True)
    with closing(writer):
        writer.write(query)
        await writer.drain()
        while (await reader.readline()) != b'\r\n':
            continue

        data = await reader.read()
    await writer.wait_closed()

    return ('Font', Font(FONT_NAME, data))


if __name__ == '__main__':
    AsyncMessageExample().run()

AsyncGenerator Generating Messages

An application can subscribe to AsyncGenerators to receive Messages about asynchronously generated information, e.g. a pending web download.

from asyncio import sleep

from pyiced import column, IcedApp, stream, text


class StreamExample(IcedApp):
    def __init__(self):
        self.__stream = stream(self.__generator_func())
        self.__index = 0

    class settings:
        class window:
            size = (640, 40)

    def title(self):
        return 'Stream Example'

    def view(self):
        return column([text(f'Index: {self.__index / 10:.1f}')])

    def subscriptions(self):
        if self.__stream is not None:
            return [self.__stream]

    def update(self, msg, clipboard):
        match msg:
            case 'done':
                self.__stream = None
            case int(index):
                self.__index = index

    async def __generator_func(self):
        for i in range(1, 101):
            yield i
            await sleep(0.1)
        yield 'done'


if __name__ == '__main__':
    StreamExample().run()

Capturing Keystrokes

To capture any keystoke (or indeed any event that original from user interaction), you can make pyiced.IcedApp.subscriptions() return a list [pyced.Subscription.UNCAPTURED].

from pyiced import (
    Align, container, Message, IcedApp, Length, Settings, Subscription,
    text, WindowSettings,
)


class FullscreenExample(IcedApp):
    def __init__(self):
        self.__fullscreen = False
        self.__should_exit = False

    class settings(Settings):
        class window(WindowSettings):
            size = (640, 320)

    def subscriptions(self):
        return [Subscription.UNCAPTURED]

    def fullscreen(self):
        return self.__fullscreen

    def should_exit(self):
        return self.__should_exit

    def title(self):
        return self.__message

    def update(self, msg, clipboard):
        match msg:
            case Message(keyboard='keyreleased', key_code='F11'):
                self.__fullscreen = not self.__fullscreen
            case Message(keyboard='keyreleased', key_code='Escape'):
                self.__should_exit = True

    def view(self):
        return container(
            text(self.__message, size=40),
            padding=20, align_x=Align.CENTER, align_y=Align.CENTER,
            width=Length.FILL, height=Length.FILL,
        )

    @property
    def __message(self):
        if self.__fullscreen:
            return 'Fullscreen (press F11!)'
        else:
            return 'Windowed (press F11!)'


if __name__ == '__main__':
    FullscreenExample().run()

Using System Fonts

You can load use findfont() to find and load system fonts. This example gives you a preview of the installed fonts.

from bisect import bisect_left, bisect_right

from pyiced import (
    Align, column, container, findfont, IcedApp, Length, PickListState,
    pick_list, row, Settings, systemfonts, text, text_input,
    TextInputState,
)


class FontPreview(IcedApp):
    class settings(Settings):
        default_text_size = 24

    def __init__(self):
        self.__font_bold = findfont(
            ['Arial', 'Noto Sans', 'DejaVu Sans', 'sans-serif'],
            weight='bold',
        ).load()

        self.__family_prefix_state = TextInputState()
        self.__family_prefix = ''
        self.__family_state = PickListState()
        self.__family = ''
        self.__families = sorted(
            {fontid.family for fontid in systemfonts()} |
            {'serif', 'sans-serif', 'cursive', 'fantasy', 'monospace'}
        )

        self.__weight = 'normal'
        self.__weight_state = PickListState()

        self.__stretch = 'normal'
        self.__stretch_state = PickListState()

        self.__style = 'normal'
        self.__style_state = PickListState()

    def title(self):
        return 'Font Preview'

    def view(self):
        if self.__family_prefix:
            def family_key(s):
                return cmp(s[:len(family_prefix)].lower(), family_prefix)

            family_prefix = self.__family_prefix.lower()
            families_start = bisect_left(
                self.__families, 0,
                key=family_key,
            )
            families_end = bisect_right(
                self.__families, 0, families_start,
                key=family_key,
            )
            families = self.__families[families_start:families_end][:10]
        else:
            families = None
        family = column(
            [
                text('font-family:', font=self.__font_bold),
                text_input(
                    'family_prefix',
                    self.__family_prefix_state,
                    '',
                    self.__family_prefix,
                    padding=4,
                ),
                pick_list(
                    'family',
                    self.__family_state,
                    self.__family,
                    families or [
                        'serif', 'sans-serif', 'cursive', 'fantasy',
                        'monospace',
                    ],
                ),
            ],
            max_width=300,
            spacing=10,
        )
        weight = column(
            [
                text('font-weight:', font=self.__font_bold),
                pick_list(
                    'weight',
                    self.__weight_state,
                    self.__weight,
                    [
                        'thin', 'extra-light', 'light', 'normal',
                        'medium', 'semibold', 'bold', 'extra-bold',
                        'black',
                    ],
                )
            ],
            max_width=300,
            spacing=10,
        )
        stretch = column(
            [
                text('font-stretch:', font=self.__font_bold),
                pick_list(
                    'stretch',
                    self.__stretch_state,
                    self.__stretch,
                    [
                        'ultra-condensed', 'extra-condensed', 'condensed',
                        'semi-condensed', 'normal', 'semi-expanded',
                        'expanded', 'extra-expanded', 'ultra-expanded',
                    ],
                )
            ],
            max_width=300,
            spacing=10,
        )
        style = column(
            [
                text('font-style:', font=self.__font_bold),
                pick_list(
                    'style',
                    self.__style_state,
                    self.__style,
                    ['normal', 'italic', 'oblique'],
                )
            ],
            max_width=300,
            spacing=10,
        )
        search = row([family, weight, stretch, style], spacing=10)

        font = findfont(
            self.__family, self.__weight, self.__stretch, self.__style,
        )
        font_data = column(
            [
                text(
                    'Found font:',
                    font=self.__font_bold,
                ),
                row(
                    [text('family:'), text(font.family)],
                    spacing=4,
                ),
                row(
                    [text('weight:'), text(repr(font.weight))],
                    spacing=4,
                ),
                row(
                    [text('stretch:'), text(repr(font.stretch))],
                    spacing=4,
                ),
                row(
                    [text('style:'), text(repr(font.style))],
                    spacing=4,
                ),
            ],
            spacing=10,
        )

        font = font.load()
        font_preview = column(
            [
                text(
                    'Preview:',
                    font=self.__font_bold,
                ),
                text(
                    '!"#$%&\'()*+,-./0123456789:;<=>?@'
                    ' ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`'
                    ' abcdefghijklmnopqrstuvwxyz{|}~',
                    font=font,
                ),
                text(
                    'The quick brown fox jumps over the lazy dog.',
                    font=font,
                ),
                text(
                    'Zwölf laxe Typen qualmen verdächtig süße Objekte.',
                    font=font,
                ),
                text(
                    'Dès Noël où un zéphyr haï me vêt de glaçons '
                    'würmiens, je dîne d’exquis rôtis de bœuf au kir '
                    'à l’aÿ d’âge mûr & cætera !',
                    font=font,
                ),
                text(
                    'Stróż pchnął kość w quiz gędźb vel fax myjń.',
                    font=font,
                ),
                text(
                    'Příliš žluťoučký kůň úpěl ďábelské ódy.',
                    font=font,
                ),
                text(
                    'Pijamalı hasta yağız şoföre çabucak güvendi',
                    font=font,
                ),
                text(
                    'Съешь ещё этих мягких французских булок, да '
                    'выпей чаю',
                    font=font,
                ),
            ],
            spacing=10,
        )

        return container(
            column(
                [
                    search,
                    row(
                        [
                            font_data,
                            font_preview,
                        ],
                        spacing=20,
                    ),
                ],
                spacing=20,
            ),
            padding=20, align_x=Align.CENTER, align_y=Align.CENTER,
            width=Length.FILL, height=Length.FILL,
        )

    def update(self, msg, clipboard):
        match msg:
            case ('family_prefix', family_prefix):
                self.__family_prefix = family_prefix
            case ('family', family):
                self.__family = family
            case ('weight', weight):
                self.__weight = weight
            case ('stretch', stretch):
                self.__stretch = stretch
            case ('style', style):
                self.__style = style


def cmp(a, b):
    return (a > b) - (a < b)


if __name__ == '__main__':
    FontPreview().run()

Two-player Online Chess

Our last example is two-player online chess (or one player offline …)

It uses subscriptions open a TCP server / connect to a TCP server, and then await the other player’s moves. It uses commands to tell the other player about your move.

(Please notice that this simple example does not actually know the chess rules. You can move twice, move the other player’s pieces, capture your own pieces, etc.)

from asyncio import Future, open_connection, start_server
from contextlib import closing
from os.path import abspath, dirname, join
from traceback import print_exc

from pyiced import (
    Align, ContainerStyle, button, ButtonState, ButtonStyle,
    ButtonStyleSheet, Color, column, container, HorizontalAlignment,
    IcedApp, Length, no_element, row, stream, svg, SvgHandle, text,
    tooltip, TooltipPosition, text_input, TextInputState,
)


class ChessExample(IcedApp):
    def new(self):
        # select role:
        self.__role = None
        self.__select_role_btns = [
            ButtonState(),
            ButtonState(),
            ButtonState(),
        ]
        self.__subscription = None

        # server role:
        self.__server_address = None

        # client role:
        self.__client_inputs = [
            TextInputState(),
            TextInputState(),
            ButtonState(),
        ]

        # playing:
        self.__writer = None
        self.__pieces = [
            [
                'Chess_tile_rd.svg',
                'Chess_tile_nd.svg',
                'Chess_tile_bd.svg',
                'Chess_tile_qd.svg',
                'Chess_tile_kd.svg',
                'Chess_tile_bd.svg',
                'Chess_tile_nd.svg',
                'Chess_tile_rd.svg',
            ],
            ['Chess_tile_pd.svg'] * 8,
            *([None] * 8 for _ in range(4)),
            ['Chess_tile_pl.svg'] * 8,
            [
                'Chess_tile_rl.svg',
                'Chess_tile_nl.svg',
                'Chess_tile_bl.svg',
                'Chess_tile_ql.svg',
                'Chess_tile_kl.svg',
                'Chess_tile_bl.svg',
                'Chess_tile_nl.svg',
                'Chess_tile_rl.svg',
            ],
        ]
        self.__pieces_root = join(
            dirname(abspath(__file__)),
            'chess-pieces',
        )
        self.__button_states = [
            [ButtonState() for _ in range(8)] for _ in range(8)
        ]
        self.__selected = None

    def title(self):
        return 'Chess Example'

    def subscriptions(self):
        return [self.__subscription]

    def view(self):
        match self.__role:
            case 'server':
                elem = self.__view_server()
            case 'client':
                elem = self.__view_client()
            case 'playing':
                elem = self.__view_playing()
            case _:
                elem = self.__view_select_role()

        return container(
            elem,
            width=Length.FILL,
            height=Length.FILL,
            align_x=Align.CENTER,
            align_y=Align.CENTER,
        )

    def background_color(self):
        return Color(0.627, 0.612, 0.616)

    def __view_select_role(self):
        alone_state, server_state, client_state = self.__select_role_btns
        return container(
            column(
                [
                    text('Play as:'),
                    button(
                        alone_state,
                        text('Alone'),
                        ('role', 'alone'),
                        padding=4,
                    ),
                    button(
                        server_state,
                        text('Server'),
                        ('role', 'server'),
                        padding=4,
                    ),
                    button(
                        client_state,
                        text('Client'),
                        ('role', 'client'),
                        padding=4,
                    ),
                ],
                spacing=16,
                align_items=Align.CENTER,
            ),
            style=ContainerStyle(background=Color.WHITE),
            padding=32,
        )

    def __view_server(self):
        if not self.__server_address:
            return text('Opening server …')

        host, port = self.__server_address
        return container(
            column(
                [
                    text('Waiting for client:'),
                    text(f'Your IP: {host}'),
                    text(f'Your port: {port}'),
                ],
                spacing=16,
                align_items=Align.CENTER,
            ),
            style=ContainerStyle(background=Color.WHITE),
            padding=32,
        )

    def __view_client(self):
        if not self.__server_address:
            return text('Connecting to server …')

        return container(
            column(
                [
                    text('Connect to server:'),
                    row(
                        [
                            text_input(
                                'host',
                                self.__client_inputs[0],
                                'Host / IP address',
                                self.__server_address[0],
                                padding=4,
                                width=Length.units(148),
                            ),
                            text_input(
                                'port',
                                self.__client_inputs[1],
                                'Port',
                                self.__server_address[1],
                                padding=4,
                                width=Length.units(148),
                            ),
                        ],
                        spacing=16,
                    ),
                    button(
                        self.__client_inputs[2],
                        text(
                            'Connect',
                            horizontal_alignment=HorizontalAlignment.CENTER,
                        ),
                        ('client', self.__server_address),
                        padding=16,
                        width=Length.units(328),
                    ),
                ],
                spacing=16,
                align_items=Align.CENTER,
            ),
            style=ContainerStyle(background=Color.WHITE),
            padding=32,
        )

    def __view_playing(self):
        return row(
            [
                column(
                    [self.__cell_at(x, y) for y in range(8)],
                    width=Length.fill_portion(1),
                    height=Length.FILL,
                )
                for x in range(8)
            ],
            width=Length.units(8 * 80),
            height=Length.units(8 * 80),
        )

    def __cell_at(self, x, y):
        piece = self.__pieces[y][x]
        if piece:
            elem = svg(
                SvgHandle.from_path(join(self.__pieces_root, piece)),
            )
        else:
            elem = no_element()

        style = ButtonStyle(
            background=(
                Color(0.200, 0.600, 0.800)
                if self.__selected == (x, y) else
                Color(1.000, 0.808, 0.620)
                if (x + y) & 1 else
                Color(0.820, 0.545, 0.278)
            ),
            shadow_offset=(0, 0),
        )
        return tooltip(
            button(
                self.__button_states[y][x],
                container(
                    elem,
                    align_x=Align.CENTER,
                    align_y=Align.CENTER,
                    width=Length.FILL,
                    height=Length.FILL,
                ),
                ('select', x, y, True),
                width=Length.fill_portion(1),
                height=Length.fill_portion(1),
                style=ButtonStyleSheet(style, style, style, style),
            ),
            f'{chr(ord("a") + 7 - y)}{x + 1}',
            TooltipPosition.FOLLOW_CURSOR,
        )

    def update(self, msg, clipboard):
        match msg:
            case ('select', x, y, do_notify):
                if self.__selected == (x, y):
                    # deselect
                    self.__selected = None
                elif self.__selected:
                    # move
                    (x0, y0) = self.__selected
                    self.__pieces[y][x] = self.__pieces[y0][x0]
                    self.__pieces[y0][x0] = None
                    self.__selected = None
                elif self.__pieces[y][x]:
                    # select
                    self.__selected = (x, y)

                if do_notify and self.__writer:
                    async def write():
                        self.__writer.write(b'%d %d\n' % (x, y))
                        await self.__writer.drain()
                    return [write()]

            case ('role', 'alone'):
                self.__role = 'playing'

            case ('role', 'server'):
                self.__role = 'server'
                self.__subscription = stream(self.__role_server())

            case ('role', 'client'):
                self.__role = 'client'
                self.__server_address = ['0.0.0.0', '']

            case ('server', (host, port)):
                self.__server_address = host, port

            case ('client', (host, port)):
                self.__server_address = None
                self.__role = 'server'
                self.__subscription = stream(self.__role_client(host, port))

            case ('connected', (reader, writer)):
                self.__writer = writer
                self.__subscription = stream(self.__read_connection(reader))
                self.__role = 'playing'

            case ('host', value):
                self.__server_address[0] = value

            case ('port', value):
                self.__server_address[1] = value

            case ('host' | 'port', None, 'submit'):
                return [('client', self.__server_address)]

    async def __read_connection(self, reader):
        while not reader.at_eof():
            line = await reader.readline()
            if not line:
                break
            x, y = line.split()
            yield 'select', int(x), int(y), False

    async def __role_client(self, host, port):
        try:
            yield 'connected', await open_connection(host, port)
        except Exception:
            print_exc()
            yield 'role', 'client'

    async def __role_server(self):
        query = (
            b'GET / HTTP/1.0\r\n'
            b'Host: whatismyip.akamai.com\r\n'
            b'Connection: closed\r\n'
            b'\r\n'
        )
        reader, writer = await open_connection('whatismyip.akamai.com', 80)
        with closing(writer):
            writer.write(query)
            await writer.drain()
            while (await reader.readline()) != b'\r\n':
                continue
            hostname = (await reader.read()).decode('US-ASCII').strip()
        await writer.wait_closed()

        client = Future()
        server = await start_server(
            lambda reader, writer: client.set_result((reader, writer)),
            '0.0.0.0',
            0,
        )
        port = next(iter(server.sockets)).getsockname()[1]
        yield 'server', (hostname, port)
        yield 'connected', await client


if __name__ == '__main__':
    ChessExample().run()