Views API Guide
MoUI application code should prefer the root moui facade plus the views
package for ordinary UI. core remains the lower-level contract, layout,
paint, rich text, geometry, and advanced testing surface; runtime construction
belongs in moui/runtime.
Local DSL Imports
In application and example UI packages, import only the views functions the
package uses:
using @views { column, text, container, scroll_view }
MoonBit packages share top-level identifiers across files, so multi-file
packages should keep these imports in one small package-level DSL file instead
of repeating the same using block in every file.
Then call those functions without the @views. prefix:
container(
column([...], align=@views.CrossAlign::Start),
variant=@views.ContainerVariant::Raised,
theme~,
)
Keep enum and type names qualified by default:
align=@views.CrossAlign::Start
variant=@views.ButtonVariant::Primary
variant=@views.ContainerVariant::Raised
This keeps the DSL compact while making Start, Raised, and similar variant
names unambiguous in larger files.
Descriptor Helpers
Many reusable views take small descriptor values for actions, menus, sidebars, breadcrumbs, navigation cards, and selectable-list rows. Prefer the free helper constructors in app DSL code so ordinary view trees read like compositions:
using @views {
action_item,
menu_item,
section_nav_item,
selectable_list_item,
command_bar,
menu_bar,
section_nav,
selectable_list,
}
Then build descriptors next to the view that consumes them:
command_bar([
action_item(id="open", label="Open", message=OpenDocument),
action_item(id="save", label="Save", message=SaveDocument, enabled=can_save),
])
section_nav(
"Workspace",
[
section_nav_item(id="overview", label="Overview", message=Select("overview")),
section_nav_item(
id="reports",
label="Reports",
summary="Charts",
message=Select("reports"),
),
],
selected=route,
)
Use the Type::new form only when it makes a type annotation or cross-package
API boundary clearer. This keeps ordinary app packages on moui + views while
leaving lower-level runtime, host, and renderer packages out of view code.
Message Types and View::map Cost
View::map rebuilds the entire wrapped subtree on every view call: each layer
re-creates one adapter per node, snapshots its children and semantics-handler
arrays, and wraps its event closure. The full_cycle benchmark measures about
+10% rebuild cost per map layer on a 511-node deep tree (F-deep-click vs
G-deep-dblmap-click in benchmarks/full_cycle/native/main.mbt).
Prefer a flat top-level Msg enum for the program, and build child messages
directly in the parent view (or via plain constructor helpers) instead of
nesting map per feature module. map stays the right tool at real
composition boundaries — mounting a standalone feature view whose messages are
its own Msg into the app — but keep the number of live map layers on any
root-to-leaf path small (one layer is fine; stacked layers multiply). The
construction snapshots are a documented ownership contract (from_node must
survive a producer-owned children array being mutated afterwards, pinned in
moui/core/view_node_test.mbt), so the copies are not incidental overhead to
strip; full message-adapter fusion is tracked as framework debt instead.
Boundary
Use views for ordinary app authoring: controls, layout, surfaces, scrolling,
and simple composition.
Use moui/runtime for platform entrypoint runtime setup and white-box runtime
smoke tests. Use core directly only for lower-level state and binding types,
custom paint/layout, rich text models, geometry calculations, and advanced test
assertions that are not covered by root aliases or views helpers.
Advanced helpers such as custom layout and navigation destination construction
may stay qualified as @views.* when that makes the call site clearer.