[ Concepts ]
Immediate-Mode Rendering and State Structs
The idea in plain words
Section titled “The idea in plain words”In libkoto, you do not keep a persistent tree of UI widget objects that you mutate across callbacks. Instead, you store your data and cursor/selection positions in plain C++ variables or a state struct, and inside App::on_render you construct lightweight widget builders on the stack and call .draw(frame, area) to paint the current state into the frame.
How koto implements it
Section titled “How koto implements it”Every drawable component derives from koto::Widget (include/koto/widget.hpp:8) and implements a single virtual method:
// frame and body_area are provided by the render contextListState tasks({"Compile library", "Run ctest"});List(tasks) .border_style(BorderStyle::Rounded) .title("Tasks") .highlight_symbol("> ") .draw(frame, body_area);- Stateless widgets (
Box,Text,Fill,Clear,Rule,Gauge,Sparkline) hold only their styling and content parameters (m_fg,m_bg,m_title,m_ratio) and write cells directly intoFrame::m_back_bufferduringdraw(). - Stateful widgets (
List,Tabs,TextInput,TextArea) take a reference to a persistentstate struct(ListState&,TabsState&,TextInputState&,TextAreaState&). Duringdraw(), thewidgetreads items or text from thestate struct, normalizes scroll offsets (m_state.scroll,m_state.scroll_row,m_state.scroll_col,m_state.scroll_offset) against the currentareadimensions, and records hit rectangles (m_state.rendered_area,m_state.rendered_inner,m_state->rendered_tabs) for subsequent mouse events. - Render pass lifecycle: In
App::draw_frame(bool flush)(src/app.cpp:184-197),Appclears the back buffer viam_frame.clear_back_buffer(), invokesm_render_handler(m_frame), callsm_focus_manager->sync_geometry(m_frame)to harvest bounding boxes, and callsm_frame.render(flush).
Consequences for your code
Section titled “Consequences for your code”- Declare every
state structoutsideon_render: If you declareListStateorTextInputStateinside theon_renderlambda, its selection and cursor reset on every frame and any reference registered inFocusManagerdangles as soon ason_renderreturns. - Conditional UI requires no teardown: To hide a panel or close a dialog, stop calling
.draw(frame, area)for it insideon_render. Becausem_frame.clear_back_buffer()runs at the start of everydraw_frame(), omitted widgets disappear on the next frame andFocusManager::sync_geometry()seesframe.bounds_of(id) == std::nullopt, skipping the hidden target duringTabnavigation (src/focus.cpp:382-406).