Concepts and core objects

The Scene

To render a UI, you need to use a UIScene:

from clubsandwich.director import DirectorLoop, Scene
from clubsandwich.ui import (
    ButtonView,
)
class MainMenuScene(UIScene):
    def __init__(self, *args, **kwargs):
        views = [ButtonView(text="Quit", callback=self.quit)]
        super().__init__(views, *args, **kwargs)

    def quit(self):
        self.director.pop_scene()

class GameLoop(DirectorLoop):
    def get_initial_scene(self):
        return MainMenuScene()

GameLoop().run()
class clubsandwich.ui.UIScene(views, *args, **kwargs)
Parameters:views (list|View) – One or more subviews of the root view

See Scene for the other args.

Scene that renders a view hierarchy inside a FirstResponderContainerView.

Log the view hierarchy by pressing the backslash key at any time.

Layout

Layouts, most of the time, are specified using LayoutOptions objects. These more or less follow the UIKit springs-and-struts model. Here’s a diagram:

+----------------------------+
|             ^              |
|            top             |
|             v              |
|        +---------+         |
|        |       ^ |         |
|        |       | |         |
|<-left->|<-width->|<-right->|
|        |       | |         |
|        |   height|         |
|        |       v |         |
|        +---------+         |
|             ^              |
|          bottom            |
|             v              |
+----------------------------+

Each listed metric can either be a spring or a strut. Struts are exact values based on either a constant or a percentage of the superview. Springs are totally unconstrained, and their values are derived after the struts have been enforced.

In clubsandwich, springs are represented by None. All the other values define some kind of strut:

  • Ints >= 1 are constants.
  • Floats from 0 (inclusive) to 1 (exclusive) are fractions of the superview’s width or height.
  • The string intrinsic means “at layout time, take the value from my View.intrinsic_size property.” This is useful when you don’t know in advance how big your content is.
  • The string frame means “at layout time, take the value from the corresponding attribute of my View.frame property.”

So to put something 3 cells from the left that is 50% as wide as its superview, you’d set left to 3 and width to 0.5. To fill the superview (this is the default), set left/right/top/bottom to 0 and width/height to None.

There is a special case: when width is set but left and right are None, the view is centered horizontally. It works the same way for the vertical axis.

The LayoutOptions class has several convenience initializers and methods to make these specs extremely concise.

class clubsandwich.ui.LayoutOptionValue

This is not a real class, but in these docs, it represents the possible values for the attributes of LayoutOptions.

  • None: Do not constrain this value. It is a spring.
  • 0.0-1.0 left-inclusive: Use a fraction of the superview’s size on the appropriate axis.
  • >=1: Use a constant integer
  • 'intrinsic': The view defines an intrinsic_size property; use this value. Mostly useful for LabelView.
  • 'frame': Derive a constant from the initial frame of this view. This initial frame is stored in View.layout_spec, so if you need to change it, you can just change that attribute.
class clubsandwich.ui.layout_options.LayoutOptions
Parameters:

It is possible to define values that conflict. The behavior in these cases is undefined.

width

A LayoutOptionValue constraining this view’s width (or not).

height

A LayoutOptionValue constraining this view’s height (or not).

top

A LayoutOptionValue constraining this view’s distance from the top of its superview (or not).

right

A LayoutOptionValue constraining this view’s distance from the right of its superview (or not).

bottom

A LayoutOptionValue constraining this view’s distance from the bottom of its superview (or not).

left

A LayoutOptionValue constraining this view’s distance from the left of its superview (or not).

classmethod centered(width, height)

Create a LayoutOptions object that positions the view in the center of the superview with a constant width and height.

classmethod column_left(width)

Create a LayoutOptions object that positions the view as a full-height left column with a constant width.

classmethod column_right(width)

Create a LayoutOptions object that positions the view as a full-height right column with a constant width.

classmethod row_bottom(height)

Create a LayoutOptions object that positions the view as a full-height bottom row with a constant height.

classmethod row_top(height)

Create a LayoutOptions object that positions the view as a full-height top row with a constant height.

with_updates(**kwargs)

Returns a new LayoutOptions object with the given changes to its attributes. For example, here’s a view with a constant width, on the right side of its superview, with half the height of its superview:

# "right column, but only half height"
LayoutOptions.column_right(10).with_updates(bottom=0.5)

Views

class clubsandwich.ui.View(frame=None, subviews=None, scene=None, layout_options=None, clear=False)
Parameters:
  • frame (Rect) – Rect relative to superview’s View.bounds
  • subviews (list) – List of subviews
  • scene (UIScene) – Scene that’s handling this view
  • layout_options (LayoutOptions) – How to position this view
  • clear (bool) – If True, clear bounds each render. (Each individual view implements this because it depends on color.)

Renders itself and its subviews in an area of 2D space relative to its superview.

Variables:
  • subviews (list) – List of subviews
  • is_hidden (bool) – True iff this view will not be drawn. Feel free to set this yourself.
  • layout_options (LayoutOptions) –
  • layout_spec (Rect) – A copy of the frame made at init time, used by LayoutOptions to derive values during layout.
  • is_first_responder (bool) – True iff this view is currently the first responder.

Positioning

View.bounds

This view’s rect from its internal frame of reference. That means self.bounds.origin is always Point(0, 0).

View.frame

This view’s rect relative to its superview’s bounds.

View.scene

The scene this view is being rendered in, or None.

View.intrinsic_size

Optional. Values for intrinsic-valued attributes of LayoutOptions.

View hierarchy

View.superview

Weak reference to the view this view is a child of, or None.

View.add_subviews(subviews)
Parameters:subviews (list) – List of View objects

Append to this view’s subviews

View.remove_subviews(subviews)
Parameters:subviews (list) – List of View objects

Remove the given subviews from this view

Layout

View.set_needs_layout(val=True)
Parameters:val (bool) – If True, view needs to be redrawn. (default True)

Call this if the view’s frame() or content changes. draw() is only called if this was called first.

Note that if you’re changing either View.layout_options or changing something that affects the view’s springs-and-struts layout metrics, you may need to call self.superview.set_needs_layout() to have the layout algorithm re-run on your view.

View.layout_subviews()

Set the frames of all subviews relative to self.bounds. By default, applies the springs-and-struts algorithm using each view’s layout_options and layout_spec properties.

You shouldn’t need to override this unless LayoutOptions isn’t expressive enough for you.

Drawing

View.draw(ctx)
Parameters:ctx (BearLibTerminalContext) –

Draw this view. ctx is a full copy of the BearLibTerminal API moved into this view’s frame of reference, so you can use (0, 0) as the upper left corner.

This method will not be called if View.is_hidden is True.

First Responder

You might want to read about FirstResponderContainerView before diving into this section.

View.terminal_read(val)
Parameters:val – Return value of terminal_read()
Returns:bool (True if you handled the event)

Fires when an input event occurs, and either:

  • This view is the first responder
  • The first responder is a descendant, and no other descendants have already handled this event

You must return a truthy value if you handled the event so it doesn’t get handled twice.

View.can_become_first_responder

View subclasses should return True iff they want to be selectable and handle user input.

View.did_become_first_responder()

Called immediately after view becomes the first responder.

View.descendant_did_become_first_responder(view)
Parameters:view (View) –

Called when any descendant of this view becomes the first responder. This is so scrollable view containers can scroll it into view.

View.can_resign_first_responder

View subclass can return True to prevent the tab key from taking focus away. It should be rare to need this.

View.did_resign_first_responder()

Called immediately after view resigns first responder status.

View.descendant_did_resign_first_responder(view)
Parameters:view (View) –

Called when any descendant of this view unbecomes the first responder. This is so scrollable view containers can release keyboard event handlers.

View.first_responder_container_view

The ancestor (including self) that is a FirstResponderContainerView.

The most common use for this will probably be to manually change the first responder:

def a_method_on_your_view(self):
  # forceably become first responder, muahaha!
  self.first_responder_container_view.set_first_responder(self)

Tree traversal

View.leftmost_leaf

Leftmost leaf of the tree.

View.postorder_traversal

Generator of all nodes in this subtree, including self, such that a view is visited after all its subviews.

View.ancestors

Generator of all ancestors of this view, not including self.

View.get_ancestor_matching(predicate)
Parameters:predicate (func) – predicate(View) -> bool

Returns the ancestor matching the given predicate, or None.

Debugging

View.debug_string

A string containing helpful debugging info for this view

View.debug_print(indent=0)

Print hierarchical representation of this view and its subviews to stdout

First Responder

class clubsandwich.ui.FirstResponderContainerView(*args, **kwargs)

Manages the “first responder” system. The control that receives BearLibTerminal events at a given time is the first responder.

This container view listens for the tab key. When it’s pressed, the subview tree is walked until another candidate is found, or there are no others. That new subview is the new first responder.

You don’t need to create this class yourself. UIScene makes it for you.

If you want to write a control that handles input, read the source of the ButtonView class.

find_next_responder()

Resign active first responder and switch to the next one.

find_prev_responder()

Resign active first responder and switch to the previous one.

set_first_responder(new_value)

Resign the active first responder and set a new one.

terminal_read_after_first_responder(val, can_resign)
Parameters:
  • val (int) – Return value of terminal_read()
  • can_resign (bool) – True iff there is an active first responder that can resign

If writing a custom first responder container view, override this to customize input behavior. For example, if writing a list view, you might want to use the arrows to change the first responder.