///|
/// A composable TEA unit: the same five-tuple as `Program` (init, update,
/// view, subscriptions, commands) packaged as a value that can be defined,
/// tested, and mounted into a parent with `Feature::scope` independently of
/// the runtime root. `Program` stays the runtime-facing root; bridge the two
/// with `Program::from_feature`.
///
/// P13 note: the `write` half of a `Feature::scope` lens is a pure function
/// `(Child, Parent) -> Parent`. It is not an arbitrary setter — application
/// state still changes only inside `update`, and `Feature` introduces no
/// lifecycle state machine, binding, context, or mutable holder. Lens laws
/// expected of every `read`/`write` pair: `read(write(child, parent)) ==
/// child`, `write(read(parent), parent) == parent`, and a later `write`
/// supersedes an earlier one (last writer wins).
///
/// There is intentionally no `Feature::map`: a one-way message map is not
/// closed over `update`, because the input side would also need the inverse
/// function. Message lifting lives in `Feature::scope(wrap)`, where the
/// parent match arm has already unwrapped the child message — the same shape
/// as hand-written wrapper variants that lift with `Effect::map`.
///
/// Terminology: this `Feature` (core TEA composition unit) is unrelated to
/// `RendererFeature` (renderer capability tags) and to the feature-status /
/// feature-proof documentation pages.
struct Feature[Model, Msg] {
init : () -> (Model, Effect[Msg])
update : (Model, Msg) -> (Model, Effect[Msg])
view : (Model, ViewEnvironment) -> View[Msg]
subscriptions : (Model) -> Subscription[Msg]
commands : (Model) -> Array[ProgramCommand[Msg]]
}
///|
pub fn[Model, Msg] Feature::new(
init~ : () -> (Model, Effect[Msg]),
update~ : (Model, Msg) -> (Model, Effect[Msg]),
view~ : (Model) -> View[Msg],
subscriptions? : (Model) -> Subscription[Msg] = _model => Subscription::none(),
) -> Feature[Model, Msg] {
{
init,
update,
view: (model, _env) => view(model),
subscriptions,
commands: _model => [],
}
}
///|
pub fn[Model, Msg] Feature::new_with_environment(
init~ : () -> (Model, Effect[Msg]),
update~ : (Model, Msg) -> (Model, Effect[Msg]),
view~ : (Model, ViewEnvironment) -> View[Msg],
subscriptions? : (Model) -> Subscription[Msg] = _model => Subscription::none(),
) -> Feature[Model, Msg] {
{ init, update, view, subscriptions, commands: _model => [], }
}
///|
pub fn[Model, Msg] Feature::with_commands(
self : Feature[Model, Msg],
commands~ : (Model) -> Array[ProgramCommand[Msg]],
) -> Feature[Model, Msg] {
{ ..self, commands, }
}
///|
pub fn[Model, Msg] Feature::runtime_init(
self : Feature[Model, Msg],
) -> (Model, Effect[Msg]) {
(self.init)()
}
///|
pub fn[Model, Msg] Feature::runtime_update(
self : Feature[Model, Msg],
model : Model,
message : Msg,
) -> (Model, Effect[Msg]) {
(self.update)(model, message)
}
///|
pub fn[Model, Msg] Feature::runtime_view(
self : Feature[Model, Msg],
model : Model,
environment : ViewEnvironment,
) -> View[Msg] {
(self.view)(model, environment)
}
///|
pub fn[Model, Msg] Feature::runtime_subscriptions(
self : Feature[Model, Msg],
model : Model,
) -> Subscription[Msg] {
(self.subscriptions)(model)
}
///|
pub fn[Model, Msg] Feature::runtime_commands(
self : Feature[Model, Msg],
model : Model,
) -> Array[ProgramCommand[Msg]] {
(self.commands)(model)
}
///|
/// Mount `self` into a parent model through a `read`/`write` lens pair and a
/// `wrap` message constructor. The returned handle lifts child messages,
/// effects, views, subscriptions, and commands into the parent's types; the
/// parent `update` routes through one match arm per wrapper variant, exactly
/// like the hand-written delegation this replaces.
pub fn[Child, ChildMsg, Parent, ParentMsg] Feature::scope(
self : Feature[Child, ChildMsg],
read~ : (Parent) -> Child,
write~ : (Child, Parent) -> Parent,
wrap~ : (ChildMsg) -> ParentMsg,
) -> ScopedFeature[Parent, Child, ChildMsg, ParentMsg] {
{ read, write, feature: self, wrap, }
}
///|
/// A `Feature` mounted into a parent model, produced by `Feature::scope`.
/// Holds only pure functions (lens pair, message constructor, and the child
/// feature); it carries no state and can be rebuilt at any time.
pub struct ScopedFeature[Parent, Child, ChildMsg, ParentMsg] {
priv read : (Parent) -> Child
priv write : (Child, Parent) -> Parent
priv feature : Feature[Child, ChildMsg]
priv wrap : (ChildMsg) -> ParentMsg
}
///|
/// Run the child feature's `init`, returning the initial child model for the
/// parent to place and the startup effect already lifted to the parent's
/// message type. Batch several scopes' startup effects with
/// `Effect::batch`.
pub fn[Parent, Child, ChildMsg, ParentMsg] ScopedFeature::init(
self : ScopedFeature[Parent, Child, ChildMsg, ParentMsg],
) -> (Child, Effect[ParentMsg]) {
let (child, effect) = (self.feature.init)()
(child, effect.map(self.wrap))
}
///|
/// Delegate one child message: read the child model through the lens, run
/// the child update, write the result back, and lift the effect to the
/// parent's message type.
pub fn[Parent, Child, ChildMsg, ParentMsg] ScopedFeature::update(
self : ScopedFeature[Parent, Child, ChildMsg, ParentMsg],
parent : Parent,
message : ChildMsg,
) -> (Parent, Effect[ParentMsg]) {
let (child, effect) = (self.feature.update)((self.read)(parent), message)
((self.write)(child, parent), effect.map(self.wrap))
}
///|
/// Build the child view from the parent model and lift its messages to the
/// parent's message type.
pub fn[Parent, Child, ChildMsg, ParentMsg] ScopedFeature::view(
self : ScopedFeature[Parent, Child, ChildMsg, ParentMsg],
parent : Parent,
environment : ViewEnvironment,
) -> View[ParentMsg] {
(self.feature.view)((self.read)(parent), environment).map(self.wrap)
}
///|
/// Derive the child subscriptions from the parent model and lift their
/// messages to the parent's message type.
pub fn[Parent, Child, ChildMsg, ParentMsg] ScopedFeature::subscriptions(
self : ScopedFeature[Parent, Child, ChildMsg, ParentMsg],
parent : Parent,
) -> Subscription[ParentMsg] {
(self.feature.subscriptions)((self.read)(parent)).map(self.wrap)
}
///|
/// Derive the child program commands from the parent model and lift their
/// messages to the parent's message type.
pub fn[Parent, Child, ChildMsg, ParentMsg] ScopedFeature::commands(
self : ScopedFeature[Parent, Child, ChildMsg, ParentMsg],
parent : Parent,
) -> Array[ProgramCommand[ParentMsg]] {
(self.feature.commands)((self.read)(parent)).map(command => {
command.map(self.wrap)
})
}
///|
/// Bridge a `Feature` into the runtime-facing `Program` root. The two types
/// are field-isomorphic; this is a record copy, not a wrapper with extra
/// behavior. App roots stay `Program`; children stay `Feature` until they
/// are scoped.
pub fn[Model, Msg] Program::from_feature(
feature~ : Feature[Model, Msg],
) -> Program[Model, Msg] {
{
init: feature.init,
update: feature.update,
view: feature.view,
subscriptions: feature.subscriptions,
commands: feature.commands,
}
}