ckVision

Object model

ckVision uses one explicit UI tree. There is no hidden global UI registry and no widget creates its own terminal connection. That gives an application a clear answer to “who owns this?” and makes the same graph runnable under a real terminal or deterministic test terminal.

Application
|-- CommandRegistry, Theme, root View, injected host services
`-- root View
    `-- Desktop
        |-- top dock: MenuBar
        |-- bottom dock: StatusLine
        |-- windows in z-order
        |   `-- Window -> content View -> layouts/controls
        `-- popup or modal dialog
ObjectOwnership/insertionFocus and eventsPaint and resizeHeader
ApplicationHost constructs it from Terminal, Clock, and optional clipboard.Central dispatch: modal/focused view first, then command bindings.Composes the root into a terminal frame; dispatches terminal resize.include/cvision/ui/application.hpp
ViewParent receives std::unique_ptr<View> through add_child.A view opts into tab focus and may consume key/text/mouse input; when it paints it asks has_focus(), which is the Application's answer, never a remembered one.Parent bounds define its coordinate space; children paint in tree order.include/cvision/ui/view.hpp
DesktopApp inserts it beneath Application::root().Activates windows, manages transient popup/modal state.Re-pins docks and reclamps windows on resize.include/cvision/widgets/desktop.hpp
WindowDesktop takes ownership with add_window.Its content participates in normal focus traversal, and an arrow the focused control has no use for walks the window's controls (Down/Right forward, Up/Left back). In the keyboard move/size mode (Ctrl+F5) the window itself holds the focus until Enter or Esc.Draws frame (in its chosen line set)/shadow and gives its content the inner rectangle.include/cvision/widgets/window.hpp
LayoutWindow/content parent owns it.Layouts are normally not focused.Allocates child bounds during layout/resizes.include/cvision/ui/layout.hpp and related headers

The common construction sequence

The layouts example shows the ordinary explicit construction style: make a Desktop, transfer it to the root, then build chrome and windows.


LayoutsApp::LayoutsApp(ui::Application& app) : app_(app), roles_(ui::intern_standard_roles(app.roles())) {
    app_.theme() = ui::make_classic_theme(app_.roles(), roles_);

    auto desktop = std::make_unique<widgets::Desktop>(app_.root().bounds());
    desktop_ = desktop.get();
    app_.root().add_child(std::move(desktop));

    build_chrome();
    build_window();

    app_.commands().set_handler(app_.commands().standard().quit, [this] { app_.request_quit(); });
    app_.set_focus(splitter_);

Windows on a Desktop are modeless: users may activate, move, resize, tile, and cycle them. Every presentation call states its modality in its name. Desktop::present_modal and Desktop::present_modeless place any window; the standard families are present_modal_<family> (non-blocking) and exec_modal_<family> (blocking), plus present_modeless_<family> for a family that stays beside the other windows, such as present_modeless_help_viewer. A modal presentation scopes input to the dialog subtree until it completes, then focus returns to the invoking view. A set_focus the modal scope refuses is not lost: the Application keeps it until the scope ends and then honours it in place of that return, since it is the later request. That is how a dialog can answer by sending the keyboard elsewhere, as the window list's Switch To does. Use a dialog result/completion callback instead of making the caller own a dialog window. See dialogs and commands.

Paint and data flow

Your code mutates widget state through public methods such as set_text, set_items, or set_fraction. That invalidates the affected view. On the next application step, views paint into the composed scene and the Presenter sends only the changed terminal cells/raster regions. Clients need not manually flush or manually repaint individual widgets.

For resizing, choose a layout container or a DesktopGrowPolicy before using manual bounds. Manual bounds are appropriate for the fixed placements in a small dialog; Row, Column, Grid, Dock, AnchorPane, Overlay, and Splitter express the intended relationship under changing terminal sizes.

Custom focusable views

A custom control declares tab focus in its base initializer and implements the event hooks it consumes. Construction makes the policy true from the start, so the view cannot be attached briefly with the wrong traversal behavior. Whether the view is focused is asked, not remembered: paint with has_focus(), and treat on_focus() as the moment to react — repaint, drop a press in flight — never as a flag to cache (D-065).

class ItemSurface final : public ckv::ui::View {
public:
    ItemSurface() : View({}, ckv::ui::FocusPolicy::TabStop) {}

    bool on_key(const ckv::KeyEvent& event) override;
    void draw(ckv::scene::Painter& painter) override;
};

set_enabled(false) disables a view and everything inside it. The Application gives a disabled subtree no focus, keys, text, or pointer input, and places no cursor for it. A press that lands on a disabled control goes to the nearest enabled container, so its window still activates and raises. A pointer capture held by a view that becomes disabled is released. A custom control that draws its own states should check enabled_in_tree() and draw a disabled face. Its own enabled() flag cannot report an ancestor that was disabled.

Leave the second constructor argument at its None default for layout and decorative views. Runtime state may still use set_focus_policy when a view's role genuinely changes after construction.

Temporary UI such as a context menu should retain Application::save_focus() rather than a raw View*, then pass that bookmark to Application::restore_focus() after removing itself. The bookmark carries the view's lifetime token: restoration succeeds only while the same focusable view remains attached, and otherwise clears focus without dereferencing stale memory. show_context_menu() applies this protocol automatically and makes the popup the focused keyboard recipient until it is dismissed.