teksilo-core 0.13.1

Core of the Teksilo GUI framework — widget trait, arena, layout engine, event dispatch, focus, signals and theming.
Documentation
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 FernTech

//! Branching widget types and child-insertion trait for the `teksu!` DSL.
//!
//! `TeksiBranch{,3,4}` are two-, three-, and four-way sum types over Widget
//! implementations. They exist so `if`/`else` and small `match` arms in
//! `teksu!` can yield heterogeneous widget types from the same position
//! without boxing. Each variant implements Widget by delegating every
//! method to the active arm.
//!
//! `IntoTeksiChild` is the dispatch trait the macro uses when it cannot
//! decide at expansion time whether a child expression is a widget value
//! or a pre-registered `WidgetId` (the `#{ expr }` escape case). It
//! produces a `PendingChild`, which Category A containers already know
//! how to route through their `child()` / `add_child()` path.

use teksilo_canvas::{Canvas, Point, Rect, SizeProposal};

use crate::accessibility::AccessNodeBuilder;
use crate::build_context::BuildContext;
use crate::widget::{
    LayoutContext, PaintContext, PendingChild, Widget, WidgetPlacement, WidgetTreeView,
};
use crate::widget_builder::HandlerSet;
use crate::widget_id::WidgetId;

// ---------------------------------------------------------------------------
// The delegation, written once
// ---------------------------------------------------------------------------

/// Emit `impl Widget` for one branch enum, forwarding every method to the
/// active arm.
///
/// Each branch type occupies its arm's own arena node — the tree never sees the
/// arm again — so a method absent from this list is not overridden, it is gone:
/// the trait's default answers in its place and the arm silently loses the
/// behaviour behind it. The three branch widths would be three copies of that
/// hazard, so they share one list here, and each generated impl denies
/// `missing_trait_methods` so a method added to `Widget` fails the lint rather
/// than the app.
///
/// The variant names and the type-parameter names are the same identifiers by
/// construction (`L`/`R`, `A`/`B`/`C`, …), which is what lets one repetition
/// serve as both.
macro_rules! impl_widget_for_branch {
    ($branch:ident, $($arm:ident),+) => {
        #[deny(clippy::missing_trait_methods)]
        impl<$($arm: Widget),+> Widget for $branch<$($arm),+> {
            fn type_name(&self) -> &'static str {
                match self { $($branch::$arm(w) => w.type_name()),+ }
            }

            fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
                match self { $($branch::$arm(w) => w.build(ctx)),+ }
            }

            fn layout_response(
                &self,
                proposal: SizeProposal,
                ctx: &LayoutContext,
            ) -> crate::widget::LayoutResponse {
                match self { $($branch::$arm(w) => w.layout_response(proposal, ctx)),+ }
            }

            fn cacheable_layout(&self) -> bool {
                match self { $($branch::$arm(w) => w.cacheable_layout()),+ }
            }

            fn place_children(
                &self,
                bounds: Rect,
                proposal: SizeProposal,
                children: &mut [WidgetPlacement],
                ctx: &LayoutContext,
            ) {
                match self {
                    $($branch::$arm(w) => w.place_children(bounds, proposal, children, ctx)),+
                }
            }

            fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
                match self { $($branch::$arm(w) => w.paint(bounds, canvas, ctx)),+ }
            }

            fn wants_after_paint(&self) -> bool {
                match self { $($branch::$arm(w) => w.wants_after_paint()),+ }
            }

            fn after_paint(&self, view: &WidgetTreeView<'_>, ctx: &PaintContext) {
                match self { $($branch::$arm(w) => w.after_paint(view, ctx)),+ }
            }

            fn wants_post_paint(&self) -> bool {
                match self { $($branch::$arm(w) => w.wants_post_paint()),+ }
            }

            fn post_paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
                match self { $($branch::$arm(w) => w.post_paint(bounds, canvas, ctx)),+ }
            }

            fn accessibility(&self, builder: &mut AccessNodeBuilder) {
                match self { $($branch::$arm(w) => w.accessibility(builder)),+ }
            }

            fn culls_children(&self) -> bool {
                match self { $($branch::$arm(w) => w.culls_children()),+ }
            }

            fn wants_descendant_redirects(&self) -> bool {
                match self { $($branch::$arm(w) => w.wants_descendant_redirects()),+ }
            }

            fn a11y_redirect_descendant(
                &self,
                self_id: WidgetId,
                descendant: WidgetId,
            ) -> Option<accesskit::NodeId> {
                match self {
                    $($branch::$arm(w) => w.a11y_redirect_descendant(self_id, descendant)),+
                }
            }

            fn accessible_title_hint(&self) -> Option<String> {
                match self { $($branch::$arm(w) => w.accessible_title_hint()),+ }
            }

            fn accessible_title_node(&self) -> Option<crate::widget_id::WidgetId> {
                match self { $($branch::$arm(w) => w.accessible_title_node()),+ }
            }

            fn initial_focus_hint(&self) -> Option<WidgetId> {
                match self { $($branch::$arm(w) => w.initial_focus_hint()),+ }
            }

            fn context_menu_key_target(&self) -> Option<WidgetId> {
                match self { $($branch::$arm(w) => w.context_menu_key_target()),+ }
            }

            fn children(&self) -> Vec<WidgetId> {
                match self { $($branch::$arm(w) => w.children()),+ }
            }

            fn accessibility_children(&self) -> Option<Vec<WidgetId>> {
                match self { $($branch::$arm(w) => w.accessibility_children()),+ }
            }

            fn as_any(&self) -> Option<&dyn std::any::Any> {
                match self { $($branch::$arm(w) => w.as_any()),+ }
            }

            fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
                match self { $($branch::$arm(w) => w.as_any_mut()),+ }
            }

            fn clips_children(&self) -> bool {
                match self { $($branch::$arm(w) => w.clips_children()),+ }
            }

            fn focus_reveal_rect(&self, bounds: Rect) -> Option<Rect> {
                match self { $($branch::$arm(w) => w.focus_reveal_rect(bounds)),+ }
            }

            fn hit_shape(&self, local_point: Point, bounds: Rect) -> bool {
                match self { $($branch::$arm(w) => w.hit_shape(local_point, bounds)),+ }
            }

            fn accepts_child_hit(&self, child: WidgetId, point: Point) -> bool {
                match self { $($branch::$arm(w) => w.accepts_child_hit(child, point)),+ }
            }

            fn hit_outset(
                &self,
                kind: teksilo_tokens::PointerKind,
                tokens: &teksilo_tokens::InputTokens,
            ) -> teksilo_canvas::EdgeInsets {
                match self { $($branch::$arm(w) => w.hit_outset(kind, tokens)),+ }
            }

            fn hit_slop(
                &self,
                kind: teksilo_tokens::PointerKind,
                tokens: &teksilo_tokens::InputTokens,
            ) -> Option<crate::pointer::hit_slop::HitSlop> {
                // Named on `WidgetBuilder` too, where it is a consuming builder
                // method — spelled out so the arm's `Widget` impl is the one
                // called no matter what is in scope at the expansion site.
                match self { $($branch::$arm(w) => Widget::hit_slop(w, kind, tokens)),+ }
            }

            fn hit_distance(&self, local_point: Point, bounds: Rect) -> Option<f32> {
                match self { $($branch::$arm(w) => w.hit_distance(local_point, bounds)),+ }
            }

            fn target_regions(&self, bounds: Rect) -> Vec<crate::partition::TargetRegion> {
                match self { $($branch::$arm(w) => w.target_regions(bounds)),+ }
            }

            fn preserves_children_on_rebuild(&self) -> bool {
                match self { $($branch::$arm(w) => w.preserves_children_on_rebuild()),+ }
            }

            fn tooltip_has_content(&self) -> bool {
                match self { $($branch::$arm(w) => w.tooltip_has_content()),+ }
            }

            fn declare_shortcuts(&self) -> Vec<crate::shortcut::Shortcut> {
                match self { $($branch::$arm(w) => w.declare_shortcuts()),+ }
            }

            fn take_handler_set(&mut self) -> Option<HandlerSet> {
                match self { $($branch::$arm(w) => w.take_handler_set()),+ }
            }
        }
    };
}

// ---------------------------------------------------------------------------
// TeksiBranch — two-way sum type
// ---------------------------------------------------------------------------

#[derive(Debug)]
pub enum TeksiBranch<L: Widget, R: Widget> {
    L(L),
    R(R),
}

impl_widget_for_branch!(TeksiBranch, L, R);

// ---------------------------------------------------------------------------
// TeksiBranch3 — three-way sum type
// ---------------------------------------------------------------------------

#[derive(Debug)]
pub enum TeksiBranch3<A: Widget, B: Widget, C: Widget> {
    A(A),
    B(B),
    C(C),
}

impl_widget_for_branch!(TeksiBranch3, A, B, C);

// ---------------------------------------------------------------------------
// TeksiBranch4 — four-way sum type
// ---------------------------------------------------------------------------

#[derive(Debug)]
pub enum TeksiBranch4<A: Widget, B: Widget, C: Widget, D: Widget> {
    A(A),
    B(B),
    C(C),
    D(D),
}

impl_widget_for_branch!(TeksiBranch4, A, B, C, D);

// ---------------------------------------------------------------------------
// IntoTeksiChild — widget-or-id dispatch for #{ expr } child positions
// ---------------------------------------------------------------------------

/// Dispatch trait the `teksu!` macro uses to route child expressions whose
/// static type isn't known at expansion time (the `#{ expr }` escape).
/// `impl Widget + 'static` values lower to `PendingChild::Deferred`;
/// pre-registered `WidgetId` values lower to `PendingChild::Id`.
pub trait IntoTeksiChild {
    fn into_pending(self) -> PendingChild;
}

impl<W: Widget + 'static> IntoTeksiChild for W {
    fn into_pending(self) -> PendingChild {
        PendingChild::Deferred(Box::new(self))
    }
}

impl IntoTeksiChild for WidgetId {
    fn into_pending(self) -> PendingChild {
        PendingChild::Id(self)
    }
}

// ---------------------------------------------------------------------------
// IntoTeksiCondition — reactive/static dispatch for `if bare_ident { ... }`
// ---------------------------------------------------------------------------

/// Dispatch trait written for `teksu!`'s reactive conditional
/// (`if bare_ident { Element }`): which impl fires — and thus whether the
/// element is conditionally built or always built with bound visibility —
/// is decided at monomorphization.
///
/// **The macro does not emit it.** That lowering rule was never shipped:
/// an `if` at body position lowers to a plain Rust conditional
/// (`.child_opt(if cond { Some(..) } else { None })`), so the condition
/// must be a `bool` and a `Signal<bool>` there is a type error. The
/// reactive form that does work is the `visible_when:` property. The
/// trait stays public for hand-written builder chains — see
/// `docs/teksu-language-spec-v3.md` §5.1.
///
/// - `bool`: static — the element is built only when the flag is true.
///   Returns `Some(id)` if built, `None` if skipped.
/// - `Signal<bool>` / `Prop<bool>`: reactive — the element is always
///   built, and its visibility is bound to the signal via
///   `BuildContext::visible_when`. Returns `Some(id)` unconditionally.
///
/// The return type is `Option<WidgetId>` so the macro can use a single
/// lowering shape (`if let Some(id) = ... { parent.child(id) }`)
/// that works for both cases.
pub trait IntoTeksiCondition {
    fn teksilo_into_conditional_child<W: crate::widget::Widget + 'static>(
        self,
        child: W,
        ctx: &mut crate::build_context::BuildContext,
    ) -> Option<WidgetId>;
}

impl IntoTeksiCondition for bool {
    fn teksilo_into_conditional_child<W: crate::widget::Widget + 'static>(
        self,
        child: W,
        ctx: &mut crate::build_context::BuildContext,
    ) -> Option<WidgetId> {
        if self { Some(ctx.add(child)) } else { None }
    }
}

impl IntoTeksiCondition for crate::signal::Signal<bool> {
    fn teksilo_into_conditional_child<W: crate::widget::Widget + 'static>(
        self,
        child: W,
        ctx: &mut crate::build_context::BuildContext,
    ) -> Option<WidgetId> {
        let id = ctx.add(child);
        ctx.visible_when(id, self);
        Some(id)
    }
}

impl IntoTeksiCondition for crate::signal::Prop<bool> {
    fn teksilo_into_conditional_child<W: crate::widget::Widget + 'static>(
        self,
        child: W,
        ctx: &mut crate::build_context::BuildContext,
    ) -> Option<WidgetId> {
        let id = ctx.add(child);
        ctx.visible_when(id, self);
        Some(id)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_widgets::FillWidget;
    use crate::widget_tree::WidgetTree;
    use teksilo_canvas::SizeProposal;
    use teksilo_tokens::Color;

    #[test]
    fn teksilo_branch_dispatches_to_active_variant() {
        // Build two trees, one with each variant, confirm each variant's
        // widget actually runs its own build/size/paint path.
        let mut tree_l = WidgetTree::new();
        let id_l = tree_l.add(TeksiBranch::<FillWidget, FillWidget>::L(
            FillWidget::new().background(Color::RED),
        ));
        tree_l.layout(SizeProposal::exact(100.0, 50.0));
        assert!((tree_l.bounds(id_l).width - 100.0).abs() < 0.01);

        let mut tree_r = WidgetTree::new();
        let id_r = tree_r.add(TeksiBranch::<FillWidget, FillWidget>::R(
            FillWidget::new().background(Color::BLUE),
        ));
        tree_r.layout(SizeProposal::exact(80.0, 40.0));
        assert!((tree_r.bounds(id_r).width - 80.0).abs() < 0.01);
    }

    #[test]
    fn teksilo_branch3_dispatches_to_active_variant() {
        let mut tree = WidgetTree::new();
        let id = tree.add(TeksiBranch3::<FillWidget, FillWidget, FillWidget>::B(
            FillWidget::new(),
        ));
        tree.layout(SizeProposal::exact(120.0, 60.0));
        assert!((tree.bounds(id).width - 120.0).abs() < 0.01);
    }

    #[test]
    fn into_teksilo_child_routes_widget_to_deferred() {
        let pending = FillWidget::new().into_pending();
        assert!(matches!(pending, PendingChild::Deferred(_)));
    }

    #[test]
    fn into_teksilo_child_routes_widget_id_to_id() {
        let mut tree = WidgetTree::new();
        let leaf = tree.add(FillWidget::new());
        let pending = leaf.into_pending();
        match pending {
            PendingChild::Id(id) => assert_eq!(id, leaf),
            _ => panic!("expected PendingChild::Id"),
        }
    }
}