// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// Bevy-aligned UI (in progress).
///
/// Baseline: Bevy v0.15.3 (see docs/BEVY_BASELINE.md).

///|
pub(all) enum Val {
  Auto
  Px(Float)
  Percent(Float)
  Vw(Float)
  Vh(Float)
  VMin(Float)
  VMax(Float)
}

///|
pub struct UiRect {
  left : Val
  top : Val
  right : Val
  bottom : Val
}

///|
pub fn UiRect::new(left : Val, top : Val, right : Val, bottom : Val) -> UiRect {
  UiRect::{ left, top, right, bottom }
}

///|
pub fn UiRect::default() -> UiRect {
  UiRect::new(Val::Auto, Val::Auto, Val::Auto, Val::Auto)
}

///|
pub fn UiRect::all(value : Val) -> UiRect {
  UiRect::new(value, value, value, value)
}

///|
/// Bevy-aligned display (matches Taffy).
pub(all) enum Display {
  Block
  Flex
  Grid
  None
} derive(Eq, Show)

///|
/// Bevy-aligned position type (subset).
pub(all) enum PositionType {
  Relative
  Absolute
} derive(Eq, Show)

///|
pub(all) enum OverflowAxis {
  Clip
  Hidden
  Scroll
  Visible
} derive(Eq, Show)

///|
pub struct Overflow {
  x : OverflowAxis
  y : OverflowAxis
}

///|
pub fn Overflow::new(x : OverflowAxis, y : OverflowAxis) -> Overflow {
  Overflow::{ x, y }
}

///|
pub fn Overflow::default() -> Overflow {
  Overflow::new(OverflowAxis::Visible, OverflowAxis::Visible)
}

///|
pub(all) enum OverflowClipBox {
  BorderBox
  PaddingBox
  ContentBox
} derive(Eq, Show)

///|
pub struct OverflowClipMargin {
  visual_box : OverflowClipBox
  margin : Float
}

///|
pub fn OverflowClipMargin::default() -> OverflowClipMargin {
  OverflowClipMargin::{ visual_box: OverflowClipBox::ContentBox, margin: 0.0F }
}

///|
pub fn OverflowClipMargin::new(
  visual_box : OverflowClipBox,
  margin : Float,
) -> OverflowClipMargin {
  OverflowClipMargin::{ visual_box, margin }
}

///|
pub fn OverflowClipMargin::content_box() -> OverflowClipMargin {
  OverflowClipMargin::new(OverflowClipBox::ContentBox, 0.0F)
}

///|
pub fn OverflowClipMargin::padding_box() -> OverflowClipMargin {
  OverflowClipMargin::new(OverflowClipBox::PaddingBox, 0.0F)
}

///|
pub fn OverflowClipMargin::border_box() -> OverflowClipMargin {
  OverflowClipMargin::new(OverflowClipBox::BorderBox, 0.0F)
}

///|
pub fn OverflowClipMargin::with_margin(
  self : OverflowClipMargin,
  margin : Float,
) -> OverflowClipMargin {
  OverflowClipMargin::{ ..self, margin, }
}

///|
/// Bevy-aligned flex direction.
pub(all) enum FlexDirection {
  Column
  ColumnReverse
  Row
  RowReverse
} derive(Eq, Show)

///|
/// Bevy-aligned flex wrap.
pub(all) enum FlexWrap {
  NoWrap
  Wrap
  WrapReverse
} derive(Eq, Show)

///|
/// Bevy-aligned justify content.
pub(all) enum JustifyContent {
  Center
  Default
  End
  FlexEnd
  FlexStart
  SpaceAround
  SpaceBetween
  SpaceEvenly
  Start
  Stretch
} derive(Eq, Show)

///|
/// Bevy-aligned justify items.
pub(all) enum JustifyItems {
  Baseline
  Center
  Default
  End
  Start
  Stretch
} derive(Eq, Show)

///|
/// Bevy-aligned justify self.
pub(all) enum JustifySelf {
  Auto
  Baseline
  Center
  End
  Start
  Stretch
} derive(Eq, Show)

///|
/// Bevy-aligned align items.
pub(all) enum AlignItems {
  Baseline
  Center
  Default
  End
  FlexEnd
  FlexStart
  Start
  Stretch
} derive(Eq, Show)

///|
/// Bevy-aligned align self.
pub(all) enum AlignSelf {
  Auto
  Baseline
  Center
  End
  FlexEnd
  FlexStart
  Start
  Stretch
} derive(Eq, Show)

///|
/// Bevy-aligned align content.
pub(all) enum AlignContent {
  Center
  Default
  End
  FlexEnd
  FlexStart
  SpaceAround
  SpaceBetween
  SpaceEvenly
  Start
  Stretch
} derive(Eq, Show)

///|
pub(all) enum GridAutoFlow {
  Column
  ColumnDense
  Row
  RowDense
} derive(Eq, Show)

///|
pub(all) enum GridTrack {
  Auto
  Px(Float)
  Percent(Float)
  Fr(Float)
  MinContent
  MaxContent
} derive(Eq, Show)

///|
pub(all) enum GridTrackRepetition {
  Count(Int)
  AutoFill
  AutoFit
} derive(Eq, Show)

///|
pub struct RepeatedGridTrack {
  repetition : GridTrackRepetition
  tracks : Array[GridTrack]
}

///|
pub fn RepeatedGridTrack::new(
  repetition : GridTrackRepetition,
  tracks : Array[GridTrack],
) -> RepeatedGridTrack {
  RepeatedGridTrack::{ repetition, tracks }
}

///|
pub fn RepeatedGridTrack::default() -> RepeatedGridTrack {
  RepeatedGridTrack::{
    repetition: GridTrackRepetition::Count(1),
    tracks: [GridTrack::Auto],
  }
}

///|
pub struct GridPlacement {
  start : Int?
  span : Int?
  end : Int?
}

///|
pub fn GridPlacement::default() -> GridPlacement {
  GridPlacement::{ start: None, span: Some(1), end: None }
}

///|
pub fn GridPlacement::auto() -> GridPlacement {
  GridPlacement::default()
}

///|
pub fn GridPlacement::span(span : Int) -> GridPlacement {
  if span == 0 {
    GridPlacement::default()
  } else {
    GridPlacement::{ start: None, end: None, span: Some(span) }
  }
}

///|
pub fn GridPlacement::start(start : Int) -> GridPlacement {
  if start == 0 {
    GridPlacement::default()
  } else {
    GridPlacement::{ ..GridPlacement::default(), start: Some(start) }
  }
}

///|
pub fn GridPlacement::end(end : Int) -> GridPlacement {
  if end == 0 {
    GridPlacement::default()
  } else {
    GridPlacement::{ ..GridPlacement::default(), end: Some(end) }
  }
}

///|
pub fn GridPlacement::start_span(start : Int, span : Int) -> GridPlacement {
  if start == 0 || span == 0 {
    GridPlacement::default()
  } else {
    GridPlacement::{ start: Some(start), end: None, span: Some(span) }
  }
}

///|
pub fn GridPlacement::start_end(start : Int, end : Int) -> GridPlacement {
  if start == 0 || end == 0 {
    GridPlacement::default()
  } else {
    GridPlacement::{ start: Some(start), end: Some(end), span: None }
  }
}

///|
pub fn GridPlacement::end_span(end : Int, span : Int) -> GridPlacement {
  if end == 0 || span == 0 {
    GridPlacement::default()
  } else {
    GridPlacement::{ start: None, end: Some(end), span: Some(span) }
  }
}

///|
#ecs.component
pub struct ZIndex {
  value : Int
}

///|
pub fn ZIndex::new(value : Int) -> ZIndex {
  ZIndex::{ value, }
}

///|
#ecs.component
pub struct GlobalZIndex {
  value : Int
}

///|
pub fn GlobalZIndex::new(value : Int) -> GlobalZIndex {
  GlobalZIndex::{ value, }
}

///|
#ecs.resource
pub struct UiScale {
  value : Float
}

///|
pub fn UiScale::new(value : Float) -> UiScale {
  UiScale::{ value, }
}

///|
#ecs.resource
pub struct UiStack {
  mut uinodes : Array[@core.Entity]
}

///|
pub fn UiStack::default() -> UiStack {
  UiStack::{ uinodes: [] }
}

///|
pub fn UiStack::uinodes(self : UiStack) -> Array[@core.Entity] {
  self.uinodes
}

///|
pub fn UiStack::clear(self : Ref[UiStack]) -> Unit {
  self.val.uinodes = []
}

///|
#ecs.component
pub struct ScrollPosition {
  offset_x : Float
  offset_y : Float
}

///|
pub fn ScrollPosition::default() -> ScrollPosition {
  ScrollPosition::{ offset_x: 0.0F, offset_y: 0.0F }
}

///|
pub fn ScrollPosition::new(
  offset_x : Float,
  offset_y : Float,
) -> ScrollPosition {
  ScrollPosition::{ offset_x, offset_y }
}

///|
pub fn ScrollPosition::with_delta(
  self : ScrollPosition,
  dx : Float,
  dy : Float,
) -> ScrollPosition {
  ScrollPosition::{ offset_x: self.offset_x + dx, offset_y: self.offset_y + dy }
}

///|
#ecs.component
pub struct CalculatedClip {
  clip : @math.Rect
}

///|
pub fn CalculatedClip::new(clip : @math.Rect) -> CalculatedClip {
  CalculatedClip::{ clip, }
}

///|
#ecs.component
pub struct RelativeCursorPosition {
  normalized_visible_node_rect : @math.Rect
  normalized : @math.Vec2?
}

///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub(all) enum FocusPolicy {
  Block
  Pass
} derive(Eq, Show)

///|
pub fn FocusPolicy::default() -> FocusPolicy {
  FocusPolicy::Pass
}

///|
/// Controls whether UI should render with antialiasing.
///
/// This is Bevy-aligned (`UiAntiAlias`) and is currently consumed by the UI
/// render pipeline (tracked separately).
#ecs.component
pub(all) enum UiAntiAlias {
  Off
  On
} derive(Eq, Show)

///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct BorderRadius {
  top_left : Val
  top_right : Val
  bottom_left : Val
  bottom_right : Val
}

///|
pub fn BorderRadius::default() -> BorderRadius {
  BorderRadius::{
    top_left: Val::Px(0.0F),
    top_right: Val::Px(0.0F),
    bottom_left: Val::Px(0.0F),
    bottom_right: Val::Px(0.0F),
  }
}

///|
pub fn BorderRadius::all(radius : Val) -> BorderRadius {
  BorderRadius::{
    top_left: radius,
    top_right: radius,
    bottom_left: radius,
    bottom_right: radius,
  }
}

///|
pub fn BorderRadius::new(
  top_left : Val,
  top_right : Val,
  bottom_right : Val,
  bottom_left : Val,
) -> BorderRadius {
  BorderRadius::{ top_left, top_right, bottom_left, bottom_right }
}

///|
pub fn BorderRadius::px(
  top_left : Float,
  top_right : Float,
  bottom_right : Float,
  bottom_left : Float,
) -> BorderRadius {
  BorderRadius::new(
    Val::Px(top_left),
    Val::Px(top_right),
    Val::Px(bottom_right),
    Val::Px(bottom_left),
  )
}

///|
pub fn BorderRadius::percent(
  top_left : Float,
  top_right : Float,
  bottom_right : Float,
  bottom_left : Float,
) -> BorderRadius {
  BorderRadius::new(
    Val::Percent(top_left),
    Val::Percent(top_right),
    Val::Percent(bottom_right),
    Val::Percent(bottom_left),
  )
}

///|
#ecs.component
pub struct Outline {
  width : Val
  offset : Val
  color : @render2d.Color
}

///|
pub fn Outline::new(
  width : Val,
  offset : Val,
  color : @render2d.Color,
) -> Outline {
  Outline::{ width, offset, color }
}

///|
pub fn Outline::default() -> Outline {
  Outline::{
    width: Val::Px(0.0F),
    offset: Val::Px(0.0F),
    color: @render2d.Color::new(0.0F, 0.0F, 0.0F, 0.0F),
  }
}

///|
#ecs.component
pub struct BoxShadow {
  color : @render2d.Color
  x_offset : Val
  y_offset : Val
  spread_radius : Val
  blur_radius : Val
}

///|
pub fn BoxShadow::new(
  color : @render2d.Color,
  x_offset : Val,
  y_offset : Val,
  spread_radius : Val,
  blur_radius : Val,
) -> BoxShadow {
  BoxShadow::{ color, x_offset, y_offset, spread_radius, blur_radius }
}

///|
pub fn BoxShadow::default() -> BoxShadow {
  // Match Bevy defaults (v0.15.3).
  BoxShadow::{
    color: @render2d.Color::black(),
    x_offset: Val::Percent(20.0F),
    y_offset: Val::Percent(20.0F),
    spread_radius: Val::Px(0.0F),
    blur_radius: Val::Percent(10.0F),
  }
}

///|
#ecs.component
pub struct UiBoxShadowSamples {
  samples : Int
}

///|
pub fn UiBoxShadowSamples::new(samples : Int) -> UiBoxShadowSamples {
  let safe = if samples < 0 { 0 } else { samples }
  UiBoxShadowSamples::{ samples: safe }
}

///|
pub fn UiBoxShadowSamples::default() -> UiBoxShadowSamples {
  UiBoxShadowSamples::new(4)
}

///|
#ecs.component
pub struct BorderColor {
  color : @render2d.Color
}

///|
pub fn BorderColor::new(color : @render2d.Color) -> BorderColor {
  BorderColor::{ color, }
}

///|
/// The style of a UI node (Bevy v0.15.3-aligned surface).
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct Node {
  display : Display
  position_type : PositionType
  overflow : Overflow
  overflow_clip_margin : OverflowClipMargin
  left : Val
  right : Val
  top : Val
  bottom : Val
  width : Val
  height : Val
  min_width : Val
  min_height : Val
  max_width : Val
  max_height : Val
  aspect_ratio : Float?
  align_items : AlignItems
  justify_items : JustifyItems
  align_self : AlignSelf
  justify_self : JustifySelf
  align_content : AlignContent
  justify_content : JustifyContent
  margin : UiRect
  padding : UiRect
  border : UiRect
  flex_direction : FlexDirection
  flex_wrap : FlexWrap
  flex_grow : Float
  flex_shrink : Float
  flex_basis : Val
  row_gap : Val
  column_gap : Val
  grid_auto_flow : GridAutoFlow
  grid_template_rows : Array[RepeatedGridTrack]
  grid_template_columns : Array[RepeatedGridTrack]
  grid_auto_rows : Array[GridTrack]
  grid_auto_columns : Array[GridTrack]
  grid_row : GridPlacement
  grid_column : GridPlacement
}

///|
pub fn Node::default() -> Node {
  Node::{
    display: Display::Flex,
    position_type: PositionType::Relative,
    overflow: Overflow::default(),
    overflow_clip_margin: OverflowClipMargin::default(),
    left: Val::Auto,
    right: Val::Auto,
    top: Val::Auto,
    bottom: Val::Auto,
    width: Val::Auto,
    height: Val::Auto,
    min_width: Val::Auto,
    min_height: Val::Auto,
    max_width: Val::Auto,
    max_height: Val::Auto,
    aspect_ratio: None,
    align_items: AlignItems::Default,
    justify_items: JustifyItems::Default,
    align_self: AlignSelf::Auto,
    justify_self: JustifySelf::Auto,
    align_content: AlignContent::Default,
    justify_content: JustifyContent::Default,
    margin: UiRect::default(),
    padding: UiRect::default(),
    border: UiRect::default(),
    flex_direction: FlexDirection::Row,
    flex_wrap: FlexWrap::NoWrap,
    flex_grow: 0.0F,
    flex_shrink: 1.0F,
    flex_basis: Val::Auto,
    row_gap: Val::Px(0.0F),
    column_gap: Val::Px(0.0F),
    grid_auto_flow: GridAutoFlow::Row,
    grid_template_rows: [],
    grid_template_columns: [],
    grid_auto_rows: [],
    grid_auto_columns: [],
    grid_row: GridPlacement::default(),
    grid_column: GridPlacement::default(),
  }
}

///|
pub fn Node::with_position_type(
  self : Node,
  position_type : PositionType,
) -> Node {
  Node::{ ..self, position_type, }
}

///|
pub fn Node::with_display(self : Node, display : Display) -> Node {
  Node::{ ..self, display, }
}

///|
pub fn Node::with_overflow(self : Node, overflow : Overflow) -> Node {
  Node::{ ..self, overflow, }
}

///|
pub fn Node::with_overflow_clip_margin(
  self : Node,
  overflow_clip_margin : OverflowClipMargin,
) -> Node {
  Node::{ ..self, overflow_clip_margin, }
}

///|
pub fn Node::with_padding(self : Node, padding : UiRect) -> Node {
  Node::{ ..self, padding, }
}

///|
pub fn Node::with_border(self : Node, border : UiRect) -> Node {
  Node::{ ..self, border, }
}

///|
pub fn Node::with_left(self : Node, left : Val) -> Node {
  Node::{ ..self, left, }
}

///|
pub fn Node::with_top(self : Node, top : Val) -> Node {
  Node::{ ..self, top, }
}

///|
pub fn Node::with_width(self : Node, width : Val) -> Node {
  Node::{ ..self, width, }
}

///|
pub fn Node::with_height(self : Node, height : Val) -> Node {
  Node::{ ..self, height, }
}

///|
pub fn Node::with_justify_content(
  self : Node,
  justify_content : JustifyContent,
) -> Node {
  Node::{ ..self, justify_content, }
}

///|
pub fn Node::with_align_items(self : Node, align_items : AlignItems) -> Node {
  Node::{ ..self, align_items, }
}

///|
pub fn Node::with_flex_direction(
  self : Node,
  flex_direction : FlexDirection,
) -> Node {
  Node::{ ..self, flex_direction, }
}

///|
pub fn Node::with_row_gap(self : Node, row_gap : Val) -> Node {
  Node::{ ..self, row_gap, }
}

///|
pub fn Node::with_grid_template_rows(
  self : Node,
  grid_template_rows : Array[RepeatedGridTrack],
) -> Node {
  Node::{ ..self, grid_template_rows, }
}

///|
pub fn Node::with_grid_template_columns(
  self : Node,
  grid_template_columns : Array[RepeatedGridTrack],
) -> Node {
  Node::{ ..self, grid_template_columns, }
}

///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct UiRoot {}

///|
pub fn UiRoot::default() -> UiRoot {
  UiRoot::{  }
}

///|
/// Computed layout rectangle in logical pixels, top-left origin (viewport space).
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct UiLayout {
  rect : @math.Rect
}

///|
pub fn UiLayout::new(rect : @math.Rect) -> UiLayout {
  UiLayout::{ rect, }
}

///|
/// Intrinsic content size used by the layout engine when width/height are `Auto`.
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct ContentSize {
  size : @math.Vec2
}

///|
pub fn ContentSize::new(size : @math.Vec2) -> ContentSize {
  ContentSize::{ size, }
}

///|
/// Bevy-aligned image size component (Bevy v0.15.3 `ImageNodeSize`).
///
/// Stores the size of the selected image region in pixels (rect/atlas/texture).
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct ImageNodeSize {
  size : @math.UVec2
}

///|
pub fn ImageNodeSize::default() -> ImageNodeSize {
  ImageNodeSize::{ size: @math.UVec2::zero() }
}

///|
pub fn ImageNodeSize::size(self : ImageNodeSize) -> @math.UVec2 {
  self.size
}

///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct BackgroundColor {
  color : @render2d.Color
}

///|
pub fn BackgroundColor::new(color : @render2d.Color) -> BackgroundColor {
  BackgroundColor::{ color, }
}

///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct UiImage {
  color : @render2d.Color
  image : @asset.Handle[@asset.Image]
  texture_atlas : @asset.TextureAtlas?
  flip_x : Bool
  flip_y : Bool
  rect : @math.Rect?
}

///|
pub fn UiImage::new(image : @asset.Handle[@asset.Image]) -> UiImage {
  UiImage::{
    color: @render2d.Color::white(),
    image,
    texture_atlas: None,
    flip_x: false,
    flip_y: false,
    rect: None,
  }
}

///|
pub fn UiImage::with_color(self : UiImage, color : @render2d.Color) -> UiImage {
  UiImage::{ ..self, color, }
}

///|
pub fn UiImage::with_texture_atlas(
  self : UiImage,
  texture_atlas : @asset.TextureAtlas?,
) -> UiImage {
  UiImage::{ ..self, texture_atlas, }
}

///|
pub fn UiImage::with_flip_x(self : UiImage) -> UiImage {
  UiImage::{ ..self, flip_x: true }
}

///|
pub fn UiImage::with_flip_y(self : UiImage) -> UiImage {
  UiImage::{ ..self, flip_y: true }
}

///|
pub fn UiImage::with_rect(self : UiImage, rect : @math.Rect?) -> UiImage {
  UiImage::{ ..self, rect, }
}

///|

///|
/// Controls how an image widget is altered to fit within layout constraints.
///
/// This is Bevy-aligned (`NodeImageMode` / `ImageNode`), but currently only
/// `Auto` and `Stretch` are always supported, with `Sliced` / `Tiled` backed by
/// the texture slicing shader pipeline.

///|
/// Pixel padding for 9-slicing borders (Bevy `BorderRect`).
pub struct BorderRect {
  left : Float
  right : Float
  top : Float
  bottom : Float
}

///|
pub fn BorderRect::new(
  left : Float,
  right : Float,
  top : Float,
  bottom : Float,
) -> BorderRect {
  BorderRect::{ left, right, top, bottom }
}

///|
pub fn BorderRect::square(value : Float) -> BorderRect {
  BorderRect::{ left: value, right: value, top: value, bottom: value }
}

///|
pub fn BorderRect::rectangle(
  horizontal : Float,
  vertical : Float,
) -> BorderRect {
  BorderRect::{
    left: horizontal,
    right: horizontal,
    top: vertical,
    bottom: vertical,
  }
}

///|
pub fn BorderRect::default() -> BorderRect {
  BorderRect::square(0.0F)
}

///|
/// Defines how a texture slice scales when resized (Bevy `SliceScaleMode`).
pub(all) enum SliceScaleMode {
  Stretch
  Tile(Float)
} derive(Eq, Show)

///|
pub fn SliceScaleMode::default() -> SliceScaleMode {
  SliceScaleMode::Stretch
}

///|
/// 9-slicing configuration (Bevy `TextureSlicer`).
pub struct TextureSlicer {
  border : BorderRect
  center_scale_mode : SliceScaleMode
  sides_scale_mode : SliceScaleMode
  max_corner_scale : Float
}

///|
pub fn TextureSlicer::new(border : BorderRect) -> TextureSlicer {
  TextureSlicer::{
    border,
    center_scale_mode: SliceScaleMode::Stretch,
    sides_scale_mode: SliceScaleMode::Stretch,
    max_corner_scale: 1.0F,
  }
}

///|
pub fn TextureSlicer::default() -> TextureSlicer {
  TextureSlicer::new(BorderRect::default())
}

///|
pub(all) enum NodeImageMode {
  /// Size is determined by the source image, then clamped by layout constraints.
  Auto
  /// The image is resized to match the node's layout size (ignores aspect ratio).
  Stretch
  /// 9-sliced scaling using a `TextureSlicer`.
  Sliced(TextureSlicer)
  /// Tiled scaling for the full texture region.
  Tiled(Bool, Bool, Float)
}

///|
pub fn NodeImageMode::default() -> NodeImageMode {
  NodeImageMode::Auto
}

///|
pub fn NodeImageMode::uses_slices(self : NodeImageMode) -> Bool {
  match self {
    NodeImageMode::Sliced(_) => true
    NodeImageMode::Tiled(_, _, _) => true
    _ => false
  }
}

///|
fn clamp_stretch_value(v : Float) -> Float {
  if v < 0.001F {
    0.001F
  } else {
    v
  }
}

///|
fn min3(a : Float, b : Float, c : Float) -> Float {
  let ab = if a < b { a } else { b }
  if ab < c {
    ab
  } else {
    c
  }
}

///|
fn compute_tiled_axis(
  tile : Bool,
  image_extent : Float,
  target_extent : Float,
  stretch_value : Float,
) -> Float {
  if tile {
    let denom = image_extent * clamp_stretch_value(stretch_value)
    if denom == 0.0F {
      1.0F
    } else {
      target_extent / denom
    }
  } else {
    1.0F
  }
}

///|
fn compute_tiled_subaxis(
  image_extent : Float,
  target_extent : Float,
  mode : SliceScaleMode,
) -> Float {
  match mode {
    SliceScaleMode::Stretch => 1.0F
    SliceScaleMode::Tile(stretch_value) => {
      let denom = image_extent * clamp_stretch_value(stretch_value)
      if denom == 0.0F {
        1.0F
      } else {
        target_extent / denom
      }
    }
  }
}

///|
fn compute_texture_slices(
  image_size : @math.Vec2,
  target_size : @math.Vec2,
  mode : NodeImageMode,
) -> (@math.Vec4, @math.Vec4, @math.Vec4) {
  match mode {
    NodeImageMode::Sliced(slicer) => {
      let denom_x = if image_size.x == 0.0F { 1.0F } else { image_size.x }
      let denom_y = if image_size.y == 0.0F { 1.0F } else { image_size.y }
      let tdenom_x = if target_size.x == 0.0F { 1.0F } else { target_size.x }
      let tdenom_y = if target_size.y == 0.0F { 1.0F } else { target_size.y }
      let min_coeff = min3(
        target_size.x / denom_x,
        target_size.y / denom_y,
        slicer.max_corner_scale,
      )
      let slices = @math.Vec4::new(
        slicer.border.left / denom_x,
        slicer.border.top / denom_y,
        1.0F - slicer.border.right / denom_x,
        1.0F - slicer.border.bottom / denom_y,
      )
      let border = @math.Vec4::new(
        slicer.border.left / tdenom_x * min_coeff,
        slicer.border.top / tdenom_y * min_coeff,
        1.0F - slicer.border.right / tdenom_x * min_coeff,
        1.0F - slicer.border.bottom / tdenom_y * min_coeff,
      )
      let image_side_width = denom_x * (slices.z - slices.x)
      let image_side_height = denom_y * (slices.w - slices.y)
      let target_side_width = tdenom_x * (border.z - border.x)
      let target_side_height = tdenom_y * (border.w - border.y)
      let repeat_side_x = compute_tiled_subaxis(
        image_side_width,
        target_side_width,
        slicer.sides_scale_mode,
      )
      let repeat_side_y = compute_tiled_subaxis(
        image_side_height,
        target_side_height,
        slicer.sides_scale_mode,
      )
      let repeat_center_x = compute_tiled_subaxis(
        image_side_width,
        target_side_width,
        slicer.center_scale_mode,
      )
      let repeat_center_y = compute_tiled_subaxis(
        image_side_height,
        target_side_height,
        slicer.center_scale_mode,
      )
      (
        slices,
        border,
        @math.Vec4::new(
          repeat_side_x, repeat_side_y, repeat_center_x, repeat_center_y,
        ),
      )
    }
    NodeImageMode::Tiled(tile_x, tile_y, stretch_value) => {
      let rx = compute_tiled_axis(
        tile_x,
        image_size.x,
        target_size.x,
        stretch_value,
      )
      let ry = compute_tiled_axis(
        tile_y,
        image_size.y,
        target_size.y,
        stretch_value,
      )
      (
        @math.Vec4::new(0.0F, 0.0F, 1.0F, 1.0F),
        @math.Vec4::new(0.0F, 0.0F, 1.0F, 1.0F),
        @math.Vec4::new(1.0F, 1.0F, rx, ry),
      )
    }
    _ =>
      (
        @math.Vec4::new(0.0F, 0.0F, 1.0F, 1.0F),
        @math.Vec4::new(0.0F, 0.0F, 1.0F, 1.0F),
        @math.Vec4::new(1.0F, 1.0F, 1.0F, 1.0F),
      )
  }
}

///|
/// A UI node that renders an image (Bevy v0.15.3 `ImageNode`).
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct ImageNode {
  color : @render2d.Color
  image : @asset.Handle[@asset.Image]
  texture_atlas : @asset.TextureAtlas?
  flip_x : Bool
  flip_y : Bool
  rect : @math.Rect?
  image_mode : NodeImageMode
}

///|
pub fn ImageNode::default() -> ImageNode {
  ImageNode::{
    color: @render2d.Color::white(),
    image: @asset.asset_transparent_texture(),
    texture_atlas: None,
    flip_x: false,
    flip_y: false,
    rect: None,
    image_mode: NodeImageMode::Auto,
  }
}

///|
pub fn ImageNode::new(image : @asset.Handle[@asset.Image]) -> ImageNode {
  ImageNode::{ ..ImageNode::default(), image, }
}

///|
pub fn ImageNode::with_color(
  self : ImageNode,
  color : @render2d.Color,
) -> ImageNode {
  ImageNode::{ ..self, color, }
}

///|
pub fn ImageNode::with_texture_atlas(
  self : ImageNode,
  texture_atlas : @asset.TextureAtlas?,
) -> ImageNode {
  ImageNode::{ ..self, texture_atlas, }
}

///|
pub fn ImageNode::with_flip_x(self : ImageNode) -> ImageNode {
  ImageNode::{ ..self, flip_x: true }
}

///|
pub fn ImageNode::with_flip_y(self : ImageNode) -> ImageNode {
  ImageNode::{ ..self, flip_y: true }
}

///|
pub fn ImageNode::with_rect(self : ImageNode, rect : @math.Rect?) -> ImageNode {
  ImageNode::{ ..self, rect, }
}

///|
pub fn ImageNode::with_mode(
  self : ImageNode,
  image_mode : NodeImageMode,
) -> ImageNode {
  ImageNode::{ ..self, image_mode, }
}

///|
/// Bevy-aligned interaction state component.
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub(all) enum Interaction {
  None
  Hovered
  Pressed
} derive(Eq, Show)

///|
/// Bevy-aligned button marker component.
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct Button {}

///|
pub fn Button::default() -> Button {
  Button::{  }
}

///|
/// Bevy-aligned label marker component (Bevy v0.15.3 `Label` widget marker).
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct Label {}

///|
pub fn Label::default() -> Label {
  Label::{  }
}

///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct ButtonColors {
  normal : @render2d.Color
  hovered : @render2d.Color
  pressed : @render2d.Color
}

///|
pub fn ButtonColors::new(
  normal : @render2d.Color,
  hovered : @render2d.Color,
  pressed : @render2d.Color,
) -> ButtonColors {
  ButtonColors::{ normal, hovered, pressed }
}

///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct UiText {
  handle : @text.TextHandle
}

///|
pub fn UiText::new(handle : @text.TextHandle) -> UiText {
  UiText::{ handle, }
}

///|
/// Optional accessibility label for UI nodes (used by the host a11y bridge).
#ecs.component
pub struct AccessibilityLabel {
  text : String
}

///|
pub fn AccessibilityLabel::new(text : String) -> AccessibilityLabel {
  AccessibilityLabel::{ text, }
}

///|
// ECS resource marker for codegen (`mgstudio gen`).
#ecs.resource
pub struct UiContext {
  mut white_texture : @asset.Handle[@asset.Image]
  mut viewport_origin : @math.Vec2
  mut viewport_size : @math.Vec2
  mut camera_entity : @core.Entity?
  mut camera_transform : @math.Transform
  mut camera_scale : Float
  mut pointer_pos : @math.Vec2?
  mut pointer_down : Bool
  mut pointer_just_pressed : Bool
  mut pointer_just_released : Bool
  mut wheel_delta : @math.Vec2
}

///|
pub fn UiContext::default() -> UiContext {
  UiContext::{
    white_texture: @asset.Handle::new(-1),
    viewport_origin: @math.Vec2::new(0.0F, 0.0F),
    viewport_size: @math.Vec2::new(0.0F, 0.0F),
    camera_entity: None,
    camera_transform: @math.Transform::identity(),
    camera_scale: 1.0F,
    pointer_pos: None,
    pointer_down: false,
    pointer_just_pressed: false,
    pointer_just_released: false,
    wheel_delta: @math.Vec2::new(0.0F, 0.0F),
  }
}

///|
/// Public constructor (required because `UiContext` contains `mut` fields).
pub fn UiContext::new(
  white_texture : @asset.Handle[@asset.Image],
  viewport_origin : @math.Vec2,
  viewport_size : @math.Vec2,
  camera_entity : @core.Entity?,
  camera_transform : @math.Transform,
  camera_scale : Float,
  pointer_pos : @math.Vec2?,
  pointer_down : Bool,
  pointer_just_pressed? : Bool = false,
  pointer_just_released? : Bool = false,
  wheel_delta? : @math.Vec2 = @math.Vec2::new(0.0F, 0.0F),
) -> UiContext {
  UiContext::{
    white_texture,
    viewport_origin,
    viewport_size,
    camera_entity,
    camera_transform,
    camera_scale,
    pointer_pos,
    pointer_down,
    pointer_just_pressed,
    pointer_just_released,
    wheel_delta,
  }
}

///|
pub let ui_set_context : @app.SystemSet = @app.system_set("mgstudio.ui.context")

///|
pub let ui_set_layout : @app.SystemSet = @app.system_set("mgstudio.ui.layout")

///|
pub let ui_set_interaction : @app.SystemSet = @app.system_set(
  "mgstudio.ui.interaction",
)

///|
pub let ui_set_scroll : @app.SystemSet = @app.system_set("mgstudio.ui.scroll")

///|
pub let ui_set_measure : @app.SystemSet = @app.system_set("mgstudio.ui.measure")

///|
pub let ui_set_clip : @app.SystemSet = @app.system_set("mgstudio.ui.clip")

///|
pub let ui_set_stack : @app.SystemSet = @app.system_set("mgstudio.ui.stack")

///|
pub let ui_set_a11y : @app.SystemSet = @app.system_set("mgstudio.ui.a11y")

///|
pub let ui_set_a11y_apply : @app.SystemSet = @app.system_set(
  "mgstudio.ui.a11y_apply",
)

///|
pub let ui_set_render_extract : @app.SystemSet = @app.system_set(
  "mgstudio.ui.render_extract",
)

///|
const UI_DRAW_FLAG_TEXTURED : Int = 1

///|
const UI_DRAW_FLAG_BORDER : Int = 2

///|
const UI_DRAW_FLAG_ANTIALIAS : Int = 4

///|
const UI_STACK_Z_BASE : Float = 10000.0F

///|
const UI_STACK_Z_STEP : Float = 10.0F

///|
const UI_STACK_Z_BACKGROUND : Float = 0.0F

///|
const UI_STACK_Z_SHADOW : Float = -1.0F

///|
const UI_STACK_Z_CONTENT : Float = 1.0F

///|
const UI_STACK_Z_BORDER : Float = 2.0F

///|
const UI_STACK_Z_TEXT : Float = 3.0F

///|
const UI_STACK_Z_OUTLINE : Float = 4.0F

///|
fn int_to_float(value : Int) -> Float {
  Float::from_double(value.to_double())
}

///|
fn safe_scale_factor(scale : Float) -> Float {
  if scale <= 0.0F {
    1.0F
  } else {
    scale
  }
}

///|
fn safe_camera_scale(scale : Float) -> Float {
  if scale <= 0.0F {
    1.0F
  } else {
    scale
  }
}

///|
fn rotate_vec2(v : @math.Vec2, radians : Float) -> @math.Vec2 {
  let cosv = @math.cos(radians)
  let sinv = @math.sin(radians)
  @math.Vec2::new(v.x * cosv - v.y * sinv, v.x * sinv + v.y * cosv)
}

///|
/// Convert a window-space pointer into viewport-space coordinates.
///
/// Returns `None` if the pointer is outside the viewport bounds.
pub fn ui_pointer_pos_in_viewport(
  mouse_pos : @math.Vec2,
  viewport_origin : @math.Vec2,
  viewport_size : @math.Vec2,
) -> @math.Vec2? {
  let pos_in_view = @math.Vec2::new(
    mouse_pos.x - viewport_origin.x,
    mouse_pos.y - viewport_origin.y,
  )
  if pos_in_view.x >= 0.0F &&
    pos_in_view.y >= 0.0F &&
    pos_in_view.x <= viewport_size.x &&
    pos_in_view.y <= viewport_size.y {
    Some(pos_in_view)
  } else {
    None
  }
}

///|
fn[W : HasRes_UiContext + @core.ChangeTickWorld] ensure_ui_context_resource(
  world : W,
) -> Ref[UiContext]? {
  let tick = @core.ChangeTickWorld::read_change_tick(world)
  let res = HasRes_UiContext::get_ui_context_resource(world)
  if !res.is_present() {
    res.insert(UiContext::default(), tick)
  }
  res.get_ref_mut(tick)
}

///|
fn[W : HasRes_UiScale + @core.ChangeTickWorld] ensure_ui_scale_resource(
  world : W,
) -> Ref[UiScale]? {
  let tick = @core.ChangeTickWorld::read_change_tick(world)
  let res = HasRes_UiScale::get_ui_scale_resource(world)
  if !res.is_present() {
    res.insert(UiScale::new(1.0F), tick)
  }
  res.get_ref_mut(tick)
}

///|
fn create_white_texture_if_needed(ctx : Ref[UiContext]) -> Unit {
  // In unit tests we may pre-seed a dummy handle; avoid touching host APIs then.
  if ctx.val.white_texture.id() > 0 {
    return
  }
  let size = @math.UVec2::new(1, 1)
  let handle = @asset.asset_create_dynamic_texture(size, true)
  let rect = @math.URect::new(@math.UVec2::new(0, 0), @math.UVec2::new(1, 1))
  // moonbitlang/parser may not accept method calls on integer literals.
  let u : Int = 255
  let b : Byte = u.to_byte()
  let bytes = Bytes::from_array([b, b, b, b])
  @asset.asset_update_texture_region_rgba8(handle, rect, bytes)
  ctx.val.white_texture = handle
}

///|
fn clamp_viewport_to_window(
  viewport : @render2d.Viewport,
  win_w : Int,
  win_h : Int,
) -> @render2d.Viewport {
  let mut x = viewport.physical_position.x
  let mut y = viewport.physical_position.y
  let mut w = viewport.physical_size.x
  let mut h = viewport.physical_size.y
  if x < 0 {
    x = 0
  }
  if y < 0 {
    y = 0
  }
  if w < 0 {
    w = 0
  }
  if h < 0 {
    h = 0
  }
  let max_w = if win_w - x < 0 { 0 } else { win_w - x }
  let max_h = if win_h - y < 0 { 0 } else { win_h - y }
  if w > max_w {
    w = max_w
  }
  if h > max_h {
    h = max_h
  }
  @render2d.Viewport::new(@math.IVec2::new(x, y), @math.IVec2::new(w, h))
}

///|
pub fn[
  W : HasRes_UiContext + @render2d.Has_Camera2d + @render2d.Has_Camera + @render2d.Has_Projection + @render2d.Has_RenderTarget + @math.Has_Transform + @transform.Has_GlobalTransform + @core.ChangeTickWorld,
] ui_update_context_system(
  world : W,
) -> Unit {
  guard ensure_ui_context_resource(world) is Some(ctx) else { return }
  create_white_texture_if_needed(ctx)
  let win_w = @window.window_width()
  let win_h = @window.window_height()
  let sf = safe_scale_factor(@window.window_scale_factor())

  // Select a screen Camera2d with the smallest order (Bevy-like).
  let camera2d_store = @render2d.Has_Camera2d::get_camera2d_store(world)
  let camera_store = @render2d.Has_Camera::get_camera_store(world)
  let projection_store = @render2d.Has_Projection::get_projection_store(world)
  let target_store = @render2d.Has_RenderTarget::get_render_target_store(world)
  let transform_store = @math.Has_Transform::get_transform_store(world)
  let global_store = @transform.Has_GlobalTransform::get_global_transform_store(
    world,
  )
  let mut best_entity : @core.Entity? = None
  let mut best_order : Int = 0
  camera2d_store.for_each(fn(entity, _marker) {
    let target = match target_store.get(entity) {
      Some(t) => t
      None => @render2d.RenderTarget::Screen
    }
    if target is @render2d.Image(_) {
      return
    }
    let camera = match camera_store.get(entity) {
      Some(v) => v
      None => @render2d.Camera::default()
    }
    match best_entity {
      None => {
        best_entity = Some(entity)
        best_order = camera.order
      }
      Some(_) =>
        if camera.order < best_order {
          best_entity = Some(entity)
          best_order = camera.order
        }
    }
  })
  let mut viewport_origin = @math.Vec2::new(0.0F, 0.0F)
  let mut viewport_size = @math.Vec2::new(
    int_to_float(win_w) / sf,
    int_to_float(win_h) / sf,
  )
  let mut camera_transform = @math.Transform::identity()
  let mut camera_scale = 1.0F
  if best_entity is Some(cam_entity) {
    let camera = match camera_store.get(cam_entity) {
      Some(v) => v
      None => @render2d.Camera::default()
    }
    let projection = match projection_store.get(cam_entity) {
      Some(@render2d.Orthographic(p)) => p
      _ => @render2d.OrthographicProjection::default_2d()
    }
    camera_scale = safe_camera_scale(projection.scale)
    camera_transform = match global_store.get(cam_entity) {
      Some(gt) => gt.to_transform_approx()
      None =>
        match transform_store.get(cam_entity) {
          Some(t) => t
          None => @math.Transform::identity()
        }
    }
    if camera.viewport is Some(vp0) {
      let vp = clamp_viewport_to_window(vp0, win_w, win_h)
      viewport_origin = @math.Vec2::new(
        int_to_float(vp.physical_position.x) / sf,
        int_to_float(vp.physical_position.y) / sf,
      )
      viewport_size = @math.Vec2::new(
        int_to_float(vp.physical_size.x) / sf,
        int_to_float(vp.physical_size.y) / sf,
      )
    }
  }

  // Pointer (window space -> viewport space).
  let pointer_pos = match @input.mouse_position() {
    Some(mouse) =>
      ui_pointer_pos_in_viewport(mouse, viewport_origin, viewport_size)
    None => None
  }
  ctx.val.viewport_origin = viewport_origin
  ctx.val.viewport_size = viewport_size
  ctx.val.camera_entity = best_entity
  ctx.val.camera_transform = camera_transform
  ctx.val.camera_scale = camera_scale
  ctx.val.pointer_pos = pointer_pos
  ctx.val.pointer_down = @input.mouse_button_pressed(@window.MouseButton::Left)
  ctx.val.pointer_just_pressed = @input.mouse_button_just_pressed(
    @window.MouseButton::Left,
  )
  ctx.val.pointer_just_released = @input.mouse_button_just_released(
    @window.MouseButton::Left,
  )
  ctx.val.wheel_delta = match @input.mouse_wheel_delta_opt() {
    Some(v) => v
    None => @math.Vec2::new(0.0F, 0.0F)
  }
}

///|
fn[
  W : @core.ChangeTickWorld + @visibility.Has_Visibility + @visibility.Has_InheritedVisibility + @visibility.Has_ViewVisibility,
] ensure_visibility_defaults(
  world : W,
  entity : @core.Entity,
) -> Unit {
  let tick = @core.ChangeTickWorld::read_change_tick(world)
  let vis_store = @visibility.Has_Visibility::get_visibility_store(world)
  if !vis_store.contains(entity) {
    vis_store.insert(entity, @visibility.Visibility::Inherited, tick)
  }
  let inherited_store = @visibility.Has_InheritedVisibility::get_inherited_visibility_store(
    world,
  )
  if !inherited_store.contains(entity) {
    inherited_store.insert(
      entity,
      @visibility.InheritedVisibility::new(true),
      tick,
    )
  }
  let view_store = @visibility.Has_ViewVisibility::get_view_visibility_store(
    world,
  )
  if !view_store.contains(entity) {
    view_store.insert(entity, @visibility.ViewVisibility::new(true), tick)
  }
}

///|
fn[W : @core.ChangeTickWorld + @render2d.Has_RenderLayers] ensure_layers_default(
  world : W,
  entity : @core.Entity,
) -> @render2d.RenderLayers {
  let store = @render2d.Has_RenderLayers::get_render_layers_store(world)
  match store.get(entity) {
    Some(v) => v
    None => {
      let tick = @core.ChangeTickWorld::read_change_tick(world)
      let layers = @render2d.RenderLayers::default()
      store.insert(entity, layers, tick)
      layers
    }
  }
}

///|
fn ui_world_transform(
  ctx : UiContext,
  rect : @math.Rect,
  z : Float,
) -> @math.Transform {
  let viewport_w = ctx.viewport_size.x
  let viewport_h = ctx.viewport_size.y
  let cam_scale = safe_camera_scale(ctx.camera_scale)
  let cam_rot = ctx.camera_transform.rotation_z()
  let cam_pos = @math.Vec2::new(
    ctx.camera_transform.translation.x,
    ctx.camera_transform.translation.y,
  )
  let center = rect.center()
  let half_w = viewport_w / 2.0F
  let half_h = viewport_h / 2.0F
  // Viewport top-left origin -> viewport center origin, with +Y up.
  let center_px = @math.Vec2::new(center.x - half_w, half_h - center.y)
  let center_scaled = @math.Vec2::new(
    center_px.x * cam_scale,
    center_px.y * cam_scale,
  )
  let offset = rotate_vec2(center_scaled, cam_rot)
  let world_pos = @math.Vec2::new(cam_pos.x + offset.x, cam_pos.y + offset.y)
  @math.Transform::from_xy_rotation_scale(
    world_pos,
    cam_rot,
    @math.Vec2::new(cam_scale, cam_scale),
    z~,
  )
}

///|
fn button_color_for_state(
  colors : ButtonColors,
  state : Interaction?,
) -> @render2d.Color {
  match state {
    Some(s) =>
      match s {
        Interaction::Pressed => colors.pressed
        Interaction::Hovered => colors.hovered
        Interaction::None => colors.normal
      }
    None => colors.normal
  }
}

///|
fn image_measure_size_px(
  rect : @math.Rect?,
  atlas_rect : @math.Rect?,
  texture_size_fn : () -> @math.Vec2,
) -> @math.Vec2 {
  match rect {
    Some(r) => r.size()
    None =>
      match atlas_rect {
        Some(ar) => ar.size()
        None => texture_size_fn()
      }
  }
}

///|
/// Update `ContentSize` for image nodes when `NodeImageMode::Auto`.
///
/// This mirrors Bevy's `update_image_content_size_system`, adapted to mgstudio's
/// logical UI coordinate space.
pub fn[
  W : HasRes_UiContext + HasRes_UiScale + Has_Node + Has_ContentSize + Has_ImageNodeSize + Has_UiText + Has_UiImage + Has_ImageNode + @core.ChangeTickWorld,
] ui_image_content_size_system(
  world : W,
) -> Unit {
  let res = HasRes_UiContext::get_ui_context_resource(world)
  guard res.get() is Some(_) else { return }
  guard ensure_ui_scale_resource(world) is Some(ui_scale) else { return }
  let node_store = Has_Node::get_node_store(world)
  let ui_text_store = Has_UiText::get_ui_text_store(world)
  let ui_image_store = Has_UiImage::get_ui_image_store(world)
  let image_node_store = Has_ImageNode::get_image_node_store(world)
  let image_size_store = Has_ImageNodeSize::get_image_node_size_store(world)
  let content_size_store = Has_ContentSize::get_content_size_store(world)
  let tick = @core.ChangeTickWorld::read_change_tick(world)
  let sf = safe_scale_factor(@window.window_scale_factor())
  let ui_scale_value = if ui_scale.val.value <= 0.0F {
    1.0F
  } else {
    ui_scale.val.value
  }
  let inv_px_scale = ui_scale_value / sf
  let transparent_id = @asset.asset_transparent_texture().id()
  fn texture_size(handle : @asset.Handle[@asset.Image]) -> @math.Vec2 {
    let w = @asset.asset_texture_width(handle)
    let h = @asset.asset_texture_height(handle)
    @math.Vec2::new(int_to_float(w), int_to_float(h))
  }

  fn atlas_rect_for(atlas : @asset.TextureAtlas) -> @math.Rect? {
    match @asset.asset_get_texture_atlas_layout(atlas.layout) {
      Some(layout_value) =>
        if atlas.index >= 0 && atlas.index < layout_value.textures.length() {
          Some(layout_value.textures[atlas.index].as_rect())
        } else {
          None
        }
      None => None
    }
  }

  fn uvec2_from_size_px(size_px : @math.Vec2) -> @math.UVec2 {
    let w = if size_px.x <= 0.0F { 0 } else { size_px.x.to_int() }
    let h = if size_px.y <= 0.0F { 0 } else { size_px.y.to_int() }
    @math.UVec2::new(w, h)
  }

  fn update_content_size(entity : @core.Entity, size_px : @math.Vec2) -> Unit {
    if size_px.x <= 0.0F || size_px.y <= 0.0F {
      return
    }
    let next = ContentSize::new(
      @math.Vec2::new(size_px.x * inv_px_scale, size_px.y * inv_px_scale),
    )
    match content_size_store.get(entity) {
      Some(prev) =>
        if prev.size.x == next.size.x && prev.size.y == next.size.y {
          ()
        } else {
          content_size_store.set(entity, next, tick) |> ignore
        }
      None => content_size_store.insert(entity, next, tick)
    }
  }

  fn update_image_node_size(
    entity : @core.Entity,
    size_px : @math.Vec2,
  ) -> Unit {
    let next = ImageNodeSize::{ size: uvec2_from_size_px(size_px) }
    match image_size_store.get(entity) {
      Some(prev) =>
        if prev.size.x == next.size.x && prev.size.y == next.size.y {
          ()
        } else {
          image_size_store.set(entity, next, tick) |> ignore
        }
      None => image_size_store.insert(entity, next, tick)
    }
  }

  // ImageNode (Bevy-aligned).
  image_node_store.for_each(fn(entity, image_node) {
    if !node_store.contains(entity) || ui_text_store.contains(entity) {
      return
    }
    let is_auto = match image_node.image_mode {
      NodeImageMode::Auto => true
      _ => false
    }
    if image_node.image.id() == transparent_id || !is_auto {
      if content_size_store.contains(entity) {
        content_size_store.remove(entity) |> ignore
      }
      return
    }
    let atlas_rect = match image_node.texture_atlas {
      Some(atlas) => atlas_rect_for(atlas)
      None => None
    }
    let size_px = image_measure_size_px(image_node.rect, atlas_rect, () => {
      texture_size(image_node.image)
    })
    update_content_size(entity, size_px)
    update_image_node_size(entity, size_px)
  })

  // Legacy UiImage compatibility: treat as Auto-sized content when ImageNode is absent.
  ui_image_store.for_each(fn(entity, ui_image) {
    if image_node_store.contains(entity) {
      return
    }
    if !node_store.contains(entity) || ui_text_store.contains(entity) {
      return
    }
    if ui_image.image.id() == transparent_id {
      if content_size_store.contains(entity) {
        content_size_store.remove(entity) |> ignore
      }
      return
    }
    let atlas_rect = match ui_image.texture_atlas {
      Some(atlas) => atlas_rect_for(atlas)
      None => None
    }
    let size_px = image_measure_size_px(ui_image.rect, atlas_rect, () => {
      texture_size(ui_image.image)
    })
    update_content_size(entity, size_px)
  })
}

///|
pub fn[
  W : HasRes_UiContext + Has_UiRoot + Has_Node + Has_ContentSize + Has_ScrollPosition + Has_UiLayout + Has_UiImage + Has_UiText + @hierarchy.Has_Children + @render2d.Has_Sprite + @render2d.Has_RenderLayers + @visibility.Has_Visibility + @visibility.Has_InheritedVisibility + @visibility.Has_ViewVisibility + @math.Has_Transform + @core.ChangeTickWorld,
] ui_layout_system(
  world : W,
) -> Unit {
  let res = HasRes_UiContext::get_ui_context_resource(world)
  guard res.get() is Some(ctx0) else { return }
  guard ctx0.camera_entity is Some(_) else { return }
  let root_store = Has_UiRoot::get_ui_root_store(world)
  let node_store = Has_Node::get_node_store(world)
  let content_size_store = Has_ContentSize::get_content_size_store(world)
  let scroll_store = Has_ScrollPosition::get_scroll_position_store(world)
  let layout_store = Has_UiLayout::get_ui_layout_store(world)
  let children_store = @hierarchy.Has_Children::get_children_store(world)
  let sprite_store = @render2d.Has_Sprite::get_sprite_store(world)
  let img_store = Has_UiImage::get_ui_image_store(world)
  let ui_text_store = Has_UiText::get_ui_text_store(world)
  let transform_store = @math.Has_Transform::get_transform_store(world)
  let tick = @core.ChangeTickWorld::read_change_tick(world)
  let root_rect = @math.Rect::new(
    @math.Vec2::new(0.0F, 0.0F),
    @math.Vec2::new(ctx0.viewport_size.x, ctx0.viewport_size.y),
  )
  fn auto_size_for_entity(entity : @core.Entity) -> @math.Vec2 {
    match content_size_store.get(entity) {
      Some(v) => v.size
      None =>
        match ui_text_store.get(entity) {
          Some(text_node) =>
            match @text.text_get_layout_size(text_node.handle) {
              Some(sz) => sz
              None => @math.Vec2::new(0.0F, 0.0F)
            }
          None => @math.Vec2::new(0.0F, 0.0F)
        }
    }
  }

  fn f2d(v : Float) -> Double {
    v.to_double()
  }

  fn d2f(v : Double) -> Float {
    Float::from_double(v)
  }

  fn dim_for_val(val : Val) -> @moon_taffy.Dimension {
    match val {
      Auto => @moon_taffy.auto()
      Px(px) => @moon_taffy.length(f2d(px))
      Percent(p) => @moon_taffy.percent(f2d(p))
      Vw(p) => @moon_taffy.length(f2d(ctx0.viewport_size.x * (p / 100.0F)))
      Vh(p) => @moon_taffy.length(f2d(ctx0.viewport_size.y * (p / 100.0F)))
      VMin(p) => {
        let m = if ctx0.viewport_size.x < ctx0.viewport_size.y {
          ctx0.viewport_size.x
        } else {
          ctx0.viewport_size.y
        }
        @moon_taffy.length(f2d(m * (p / 100.0F)))
      }
      VMax(p) => {
        let m = if ctx0.viewport_size.x > ctx0.viewport_size.y {
          ctx0.viewport_size.x
        } else {
          ctx0.viewport_size.y
        }
        @moon_taffy.length(f2d(m * (p / 100.0F)))
      }
    }
  }

  fn clamp_nonneg(v : Float) -> Float {
    if v < 0.0F {
      0.0F
    } else {
      v
    }
  }

  fn resolve_axis(
    val : Val,
    node_axis : Float,
    viewport_size : @math.Vec2,
  ) -> Float {
    match val {
      Val::Auto => 0.0F
      Val::Px(px) => px
      Val::Percent(p) => node_axis * (p / 100.0F)
      Val::Vw(p) => viewport_size.x * (p / 100.0F)
      Val::Vh(p) => viewport_size.y * (p / 100.0F)
      Val::VMin(p) => {
        let m = if viewport_size.x < viewport_size.y {
          viewport_size.x
        } else {
          viewport_size.y
        }
        m * (p / 100.0F)
      }
      Val::VMax(p) => {
        let m = if viewport_size.x > viewport_size.y {
          viewport_size.x
        } else {
          viewport_size.y
        }
        m * (p / 100.0F)
      }
    }
  }

  fn rect_for_uirect(r : UiRect) -> @moon_taffy.Rect[@moon_taffy.Dimension] {
    @moon_taffy.Rect::new(
      left=dim_for_val(r.left),
      right=dim_for_val(r.right),
      top=dim_for_val(r.top),
      bottom=dim_for_val(r.bottom),
    )
  }

  fn size_for_vals(
    w : Val,
    h : Val,
  ) -> @moon_taffy.Size[@moon_taffy.Dimension] {
    @moon_taffy.Size::new(width=dim_for_val(w), height=dim_for_val(h))
  }

  fn grid_track_to_taffy(track : GridTrack) -> @moon_taffy.Dimension {
    match track {
      GridTrack::Auto => @moon_taffy.auto()
      GridTrack::Px(px) => @moon_taffy.length(f2d(px))
      GridTrack::Percent(p) => @moon_taffy.percent(f2d(p))
      GridTrack::Fr(w) => @moon_taffy.fr(f2d(w))
      GridTrack::MinContent => @moon_taffy.grid_min_content()
      GridTrack::MaxContent => @moon_taffy.grid_max_content()
    }
  }

  fn grid_track_repetition_to_taffy(
    repetition : GridTrackRepetition,
  ) -> @moon_taffy.GridTrackRepetition {
    match repetition {
      GridTrackRepetition::Count(n) => @moon_taffy.repeat_count(n)
      GridTrackRepetition::AutoFill => @moon_taffy.repeat_auto_fill()
      GridTrackRepetition::AutoFit => @moon_taffy.repeat_auto_fit()
    }
  }

  fn repeated_grid_track_to_taffy(
    rep : RepeatedGridTrack,
  ) -> @moon_taffy.Dimension {
    let tracks : Array[@moon_taffy.Dimension] = []
    for track in rep.tracks {
      tracks.push(grid_track_to_taffy(track))
    }
    match rep.repetition {
      GridTrackRepetition::Count(1) =>
        if tracks.length() == 1 {
          tracks[0]
        } else {
          @moon_taffy.repeat(
            grid_track_repetition_to_taffy(rep.repetition),
            tracks,
          )
        }
      _ =>
        @moon_taffy.repeat(
          grid_track_repetition_to_taffy(rep.repetition),
          tracks,
        )
    }
  }

  fn grid_template_to_taffy(
    template : Array[RepeatedGridTrack],
  ) -> Array[@moon_taffy.Dimension] {
    let dims : Array[@moon_taffy.Dimension] = []
    for rep in template {
      dims.push(repeated_grid_track_to_taffy(rep))
    }
    dims
  }

  fn grid_tracks_to_taffy(
    tracks : Array[GridTrack],
  ) -> Array[@moon_taffy.Dimension] {
    let dims : Array[@moon_taffy.Dimension] = []
    for track in tracks {
      dims.push(grid_track_to_taffy(track))
    }
    dims
  }

  fn overflow_axis_to_taffy(axis : OverflowAxis) -> @moon_taffy.Overflow {
    match axis {
      OverflowAxis::Visible => @moon_taffy.overflow_visible()
      OverflowAxis::Hidden => @moon_taffy.overflow_hidden()
      OverflowAxis::Scroll => @moon_taffy.overflow_scroll()
      OverflowAxis::Clip => @moon_taffy.overflow_clip()
    }
  }

  fn overflow_to_taffy(
    overflow : Overflow,
  ) -> @moon_taffy.Point[@moon_taffy.Overflow] {
    @moon_taffy.Point::new(
      x=overflow_axis_to_taffy(overflow.x),
      y=overflow_axis_to_taffy(overflow.y),
    )
  }

  fn display_to_taffy(display : Display) -> @moon_taffy.Display {
    match display {
      Display::Block => @moon_taffy.display_block()
      Display::Flex => @moon_taffy.display_flex()
      Display::Grid => @moon_taffy.display_grid()
      Display::None => @moon_taffy.display_none()
    }
  }

  fn position_to_taffy(position_type : PositionType) -> @moon_taffy.Position {
    match position_type {
      PositionType::Relative => @moon_taffy.position_relative()
      PositionType::Absolute => @moon_taffy.position_absolute()
    }
  }

  fn flex_direction_to_taffy(dir : FlexDirection) -> @moon_taffy.FlexDirection {
    match dir {
      FlexDirection::Row => @moon_taffy.flex_direction_row()
      FlexDirection::RowReverse => @moon_taffy.flex_direction_row_reverse()
      FlexDirection::Column => @moon_taffy.flex_direction_column()
      FlexDirection::ColumnReverse =>
        @moon_taffy.flex_direction_column_reverse()
    }
  }

  fn flex_wrap_to_taffy(wrap : FlexWrap) -> @moon_taffy.FlexWrap {
    match wrap {
      FlexWrap::NoWrap => @moon_taffy.flex_wrap_no_wrap()
      FlexWrap::Wrap => @moon_taffy.flex_wrap_wrap()
      FlexWrap::WrapReverse => @moon_taffy.flex_wrap_wrap_reverse()
    }
  }

  fn align_items_to_taffy(items : AlignItems) -> @moon_taffy.AlignItems {
    match items {
      AlignItems::Baseline => @moon_taffy.align_items_baseline()
      AlignItems::Center => @moon_taffy.align_items_center()
      AlignItems::Default => @moon_taffy.align_items_stretch()
      AlignItems::End => @moon_taffy.align_items_end()
      AlignItems::FlexEnd => @moon_taffy.align_items_flex_end()
      AlignItems::FlexStart => @moon_taffy.align_items_flex_start()
      AlignItems::Start => @moon_taffy.align_items_start()
      AlignItems::Stretch => @moon_taffy.align_items_stretch()
    }
  }

  fn justify_items_to_taffy(items : JustifyItems) -> @moon_taffy.JustifyItems {
    match items {
      JustifyItems::Baseline => @moon_taffy.align_items_baseline()
      JustifyItems::Center => @moon_taffy.align_items_center()
      JustifyItems::Default => @moon_taffy.align_items_stretch()
      JustifyItems::End => @moon_taffy.align_items_end()
      JustifyItems::Start => @moon_taffy.align_items_start()
      JustifyItems::Stretch => @moon_taffy.align_items_stretch()
    }
  }

  fn align_self_to_taffy(align_self : AlignSelf) -> @moon_taffy.AlignSelf? {
    match align_self {
      AlignSelf::Auto => None
      AlignSelf::Baseline => Some(@moon_taffy.align_items_baseline())
      AlignSelf::Center => Some(@moon_taffy.align_items_center())
      AlignSelf::End => Some(@moon_taffy.align_items_end())
      AlignSelf::FlexEnd => Some(@moon_taffy.align_items_flex_end())
      AlignSelf::FlexStart => Some(@moon_taffy.align_items_flex_start())
      AlignSelf::Start => Some(@moon_taffy.align_items_start())
      AlignSelf::Stretch => Some(@moon_taffy.align_items_stretch())
    }
  }

  fn justify_self_to_taffy(
    justify_self : JustifySelf,
  ) -> @moon_taffy.JustifySelf? {
    match justify_self {
      JustifySelf::Auto => None
      JustifySelf::Baseline => Some(@moon_taffy.align_items_baseline())
      JustifySelf::Center => Some(@moon_taffy.align_items_center())
      JustifySelf::End => Some(@moon_taffy.align_items_end())
      JustifySelf::Start => Some(@moon_taffy.align_items_start())
      JustifySelf::Stretch => Some(@moon_taffy.align_items_stretch())
    }
  }

  fn align_content_to_taffy(
    content : AlignContent,
  ) -> @moon_taffy.AlignContent {
    match content {
      AlignContent::Center => @moon_taffy.align_content_center()
      AlignContent::Default => @moon_taffy.align_content_stretch()
      AlignContent::End => @moon_taffy.align_content_end()
      AlignContent::FlexEnd => @moon_taffy.align_content_flex_end()
      AlignContent::FlexStart => @moon_taffy.align_content_flex_start()
      AlignContent::SpaceAround => @moon_taffy.align_content_space_around()
      AlignContent::SpaceBetween => @moon_taffy.align_content_space_between()
      AlignContent::SpaceEvenly => @moon_taffy.align_content_space_evenly()
      AlignContent::Start => @moon_taffy.align_content_start()
      AlignContent::Stretch => @moon_taffy.align_content_stretch()
    }
  }

  fn justify_content_to_taffy(
    content : JustifyContent,
  ) -> @moon_taffy.JustifyContent {
    match content {
      JustifyContent::Center => @moon_taffy.justify_content_center()
      JustifyContent::Default => @moon_taffy.justify_content_flex_start()
      JustifyContent::End => @moon_taffy.justify_content_end()
      JustifyContent::FlexEnd => @moon_taffy.justify_content_flex_end()
      JustifyContent::FlexStart => @moon_taffy.justify_content_flex_start()
      JustifyContent::SpaceAround => @moon_taffy.align_content_space_around()
      JustifyContent::SpaceBetween => @moon_taffy.align_content_space_between()
      JustifyContent::SpaceEvenly => @moon_taffy.align_content_space_evenly()
      JustifyContent::Start => @moon_taffy.justify_content_start()
      JustifyContent::Stretch => @moon_taffy.align_content_stretch()
    }
  }

  fn grid_auto_flow_to_taffy(flow : GridAutoFlow) -> @moon_taffy.GridAutoFlow {
    match flow {
      GridAutoFlow::Row => @moon_taffy.grid_auto_flow_row()
      GridAutoFlow::RowDense => @moon_taffy.grid_auto_flow_row_dense()
      GridAutoFlow::Column => @moon_taffy.grid_auto_flow_column()
      GridAutoFlow::ColumnDense => @moon_taffy.grid_auto_flow_column_dense()
    }
  }

  fn grid_placement_to_taffy(
    value : GridPlacement,
  ) -> @moon_taffy.Line[@moon_taffy.GridPlacement] {
    let span = match value.span {
      Some(v) => if v == 0 { 1 } else { v }
      None => 1
    }
    match (value.start, value.end) {
      (Some(start), Some(end)) =>
        @moon_taffy.Line::new(
          start=@moon_taffy.line(start),
          end=@moon_taffy.line(end),
        )
      (Some(start), None) =>
        @moon_taffy.Line::new(
          start=@moon_taffy.line(start),
          end=@moon_taffy.span(span),
        )
      (None, Some(end)) =>
        @moon_taffy.Line::new(
          start=@moon_taffy.span(span),
          end=@moon_taffy.line(end),
        )
      (None, None) =>
        @moon_taffy.Line::new(
          start=@moon_taffy.span(span),
          end=@moon_taffy.place_auto(),
        )
    }
  }

  fn entity_key(e : @core.Entity) -> (Int, Int) {
    (e.id, e.generation)
  }

  fn style_for_entity(
    entity : @core.Entity,
    is_root : Bool,
  ) -> @moon_taffy.Style {
    let node = match node_store.get(entity) {
      Some(v) => v
      None => Node::default()
    }
    let mut style = @moon_taffy.Style::default()
      .with_display(display_to_taffy(node.display))
      .with_position(position_to_taffy(node.position_type))
      .with_inset(
        @moon_taffy.Rect::new(
          left=dim_for_val(node.left),
          right=dim_for_val(node.right),
          top=dim_for_val(node.top),
          bottom=dim_for_val(node.bottom),
        ),
      )
      .with_margin(rect_for_uirect(node.margin))
      .with_padding(rect_for_uirect(node.padding))
      .with_border(rect_for_uirect(node.border))
      .with_size(size_for_vals(node.width, node.height))
      .with_min_size(size_for_vals(node.min_width, node.min_height))
      .with_max_size(size_for_vals(node.max_width, node.max_height))
      .with_flex_direction(flex_direction_to_taffy(node.flex_direction))
      .with_flex_wrap(flex_wrap_to_taffy(node.flex_wrap))
      .with_flex_grow(node.flex_grow.to_double())
      .with_flex_shrink(node.flex_shrink.to_double())
      .with_flex_basis(dim_for_val(node.flex_basis))
      .with_align_items(align_items_to_taffy(node.align_items))
      .with_justify_items(justify_items_to_taffy(node.justify_items))
      .with_align_content(align_content_to_taffy(node.align_content))
      .with_justify_content(justify_content_to_taffy(node.justify_content))
      .with_grid_template_rows(grid_template_to_taffy(node.grid_template_rows))
      .with_grid_template_columns(
        grid_template_to_taffy(node.grid_template_columns),
      )
      .with_grid_auto_rows(grid_tracks_to_taffy(node.grid_auto_rows))
      .with_grid_auto_columns(grid_tracks_to_taffy(node.grid_auto_columns))
      .with_grid_auto_flow(grid_auto_flow_to_taffy(node.grid_auto_flow))
      .with_grid_row(grid_placement_to_taffy(node.grid_row))
      .with_grid_column(grid_placement_to_taffy(node.grid_column))
      .with_gap(
        @moon_taffy.Size::new(
          width=dim_for_val(node.column_gap),
          height=dim_for_val(node.row_gap),
        ),
      )
      .with_overflow(overflow_to_taffy(node.overflow))
    if node.aspect_ratio is Some(r) {
      style = style.with_aspect_ratio(r.to_double())
    }
    match align_self_to_taffy(node.align_self) {
      Some(v) => style = style.with_align_self(v)
      None => ()
    }
    match justify_self_to_taffy(node.justify_self) {
      Some(v) => style = style.with_justify_self(v)
      None => ()
    }
    if is_root {
      style = style
        .with_position(@moon_taffy.position_relative())
        .with_size(
          @moon_taffy.Size::new(
            width=@moon_taffy.length(f2d(ctx0.viewport_size.x)),
            height=@moon_taffy.length(f2d(ctx0.viewport_size.y)),
          ),
        )
    }
    style
  }

  fn layout_root(root : @core.Entity) -> Unit {
    let taffy = @moon_taffy.TaffyTree::new()
    let entity_to_node : @hashmap.HashMap[(Int, Int), Int] = @hashmap.HashMap::new()
    let node_to_entity : @hashmap.HashMap[Int, @core.Entity] = @hashmap.HashMap::new()
    fn build(entity : @core.Entity, is_root : Bool) -> Int {
      let style = style_for_entity(entity, is_root)
      let child_ids : Array[Int] = []
      if children_store.get(entity) is Some(children) {
        for child in children.entities {
          child_ids.push(build(child, false))
        }
      }
      let id = if child_ids.length() == 0 {
        taffy.new_leaf(style)
      } else {
        taffy.new_with_children(style, child_ids)
      }
      entity_to_node.set(entity_key(entity), id)
      node_to_entity.set(id, entity)
      id
    }

    let root_node_id = build(root, true)
    let avail = @moon_taffy.Size::new(
      width=@moon_taffy.available_space_definite(f2d(ctx0.viewport_size.x)),
      height=@moon_taffy.available_space_definite(f2d(ctx0.viewport_size.y)),
    )
    fn available_to_option(v : @moon_taffy.AvailableSpace) -> Double? {
      match v {
        @moon_taffy.AvailDefinite(x) => Some(x)
        _ => None
      }
    }

    fn resolve_dimension(
      dim : @moon_taffy.Dimension,
      parent : Double?,
    ) -> Double? {
      match dim {
        @moon_taffy.DimLength(x) => Some(x)
        @moon_taffy.DimPercent(p) =>
          match parent {
            Some(base) => Some(base * (p / 100.0))
            None => None
          }
        _ => None
      }
    }

    fn clamp_f64(v : Double, min_v : Double?, max_v : Double?) -> Double {
      let mut out = v
      match min_v {
        Some(mn) => if out < mn { out = mn }
        None => ()
      }
      match max_v {
        Some(mx) => if out > mx { out = mx }
        None => ()
      }
      out
    }

    fn maybe_clamp_f64(
      v : Double?,
      min_v : Double?,
      max_v : Double?,
    ) -> Double? {
      match v {
        Some(x) => Some(clamp_f64(x, min_v, max_v))
        None => None
      }
    }

    fn measure(
      known : @moon_taffy.Size[Double?],
      available : @moon_taffy.Size[@moon_taffy.AvailableSpace],
      node_id : Int,
      _ctx : Unit?,
      style : @moon_taffy.Style,
    ) -> @moon_taffy.Size[Double] {
      match node_to_entity.get(node_id) {
        Some(entity) => {
          let intrinsic = auto_size_for_entity(entity)
          let intrinsic_w = f2d(intrinsic.x)
          let intrinsic_h = f2d(intrinsic.y)
          let parent_w = available_to_option(available.width)
          let parent_h = available_to_option(available.height)
          let s_w = resolve_dimension(style.size.width, parent_w)
          let s_min_w = resolve_dimension(style.min_size.width, parent_w)
          let s_max_w = resolve_dimension(style.max_size.width, parent_w)
          let s_h = resolve_dimension(style.size.height, parent_h)
          let s_min_h = resolve_dimension(style.min_size.height, parent_h)
          let s_max_h = resolve_dimension(style.max_size.height, parent_h)
          let width0 = match known.width {
            Some(w) => Some(w)
            None => {
              let base = match s_w {
                Some(x) => Some(x)
                None => s_min_w
              }
              maybe_clamp_f64(base, s_min_w, s_max_w)
            }
          }
          let height0 = match known.height {
            Some(h) => Some(h)
            None => {
              let base = match s_h {
                Some(x) => Some(x)
                None => s_min_h
              }
              maybe_clamp_f64(base, s_min_h, s_max_h)
            }
          }
          let intrinsic_aspect = if intrinsic_h == 0.0 {
            1.0
          } else {
            intrinsic_w / intrinsic_h
          }
          let aspect = match style.aspect_ratio {
            Some(ar) => ar
            None => intrinsic_aspect
          }
          let mut width = width0
          let mut height = height0
          match (width, height) {
            (Some(w), None) => if aspect != 0.0 { height = Some(w / aspect) }
            (None, Some(h)) => width = Some(h * aspect)
            _ => ()
          }
          let out_w = clamp_f64(width.unwrap_or(intrinsic_w), s_min_w, s_max_w)
          let out_h = clamp_f64(height.unwrap_or(intrinsic_h), s_min_h, s_max_h)
          @moon_taffy.Size::new(width=out_w, height=out_h)
        }
        None => @moon_taffy.Size::zero()
      }
    }

    let compute_res = try? taffy.compute_layout_with_measure(
      root_node_id, avail, measure,
    )
    match compute_res {
      Ok(_) => ()
      Err(_) => return
    }
    fn apply_layout(entity : @core.Entity, parent_rect : @math.Rect) -> Unit {
      guard entity_to_node.get(entity_key(entity)) is Some(node_id) else {
        return
      }
      let layout_res = try? taffy.layout(node_id)
      guard layout_res is Ok(tl) else { return }
      let parent_origin = parent_rect.min
      let loc = @math.Vec2::new(d2f(tl.location.x), d2f(tl.location.y))
      let size = @math.Vec2::new(d2f(tl.size.width), d2f(tl.size.height))
      let min = @math.Vec2::new(
        parent_origin.x + loc.x,
        parent_origin.y + loc.y,
      )
      let max = @math.Vec2::new(min.x + size.x, min.y + size.y)
      let rect = @math.Rect::new(min, max)
      let layout = UiLayout::new(rect)
      if layout_store.contains(entity) {
        layout_store.set(entity, layout, tick) |> ignore
      } else {
        layout_store.insert(entity, layout, tick)
      }

      // Update render2d / transform for UI nodes (Node component required).
      if node_store.contains(entity) {
        let transform = ui_world_transform(ctx0, rect, 0.0F)
        if transform_store.contains(entity) {
          transform_store.set(entity, transform, tick) |> ignore
        } else {
          transform_store.insert(entity, transform, tick)
        }
        let layers = ensure_layers_default(world, entity)
        ensure_visibility_defaults(world, entity)
        if img_store.contains(entity) && sprite_store.contains(entity) {
          // UiImage rendering is handled by the UI pipeline, not the Sprite pipeline.
          sprite_store.remove(entity) |> ignore
        }

        // Sync text handles (state-based text pipeline) for label nodes.
        if ui_text_store.get(entity) is Some(text_node) {
          let node = match node_store.get(entity) {
            Some(v) => v
            None => Node::default()
          }
          let border_l = clamp_nonneg(
            resolve_axis(node.border.left, size.x, ctx0.viewport_size),
          )
          let border_r = clamp_nonneg(
            resolve_axis(node.border.right, size.x, ctx0.viewport_size),
          )
          let border_t = clamp_nonneg(
            resolve_axis(node.border.top, size.y, ctx0.viewport_size),
          )
          let border_b = clamp_nonneg(
            resolve_axis(node.border.bottom, size.y, ctx0.viewport_size),
          )
          let pad_l = clamp_nonneg(
            resolve_axis(node.padding.left, size.x, ctx0.viewport_size),
          )
          let pad_r = clamp_nonneg(
            resolve_axis(node.padding.right, size.x, ctx0.viewport_size),
          )
          let pad_t = clamp_nonneg(
            resolve_axis(node.padding.top, size.y, ctx0.viewport_size),
          )
          let pad_b = clamp_nonneg(
            resolve_axis(node.padding.bottom, size.y, ctx0.viewport_size),
          )
          let inset_l = border_l + pad_l
          let inset_r = border_r + pad_r
          let inset_t = border_t + pad_t
          let inset_b = border_b + pad_b
          let content_size = @math.Vec2::new(
            clamp_nonneg(size.x - inset_l - inset_r),
            clamp_nonneg(size.y - inset_t - inset_b),
          )
          let content_min = @math.Vec2::new(
            rect.min.x + inset_l,
            rect.min.y + inset_t,
          )
          let content_rect = @math.Rect::new(
            content_min,
            @math.Vec2::new(
              content_min.x + content_size.x,
              content_min.y + content_size.y,
            ),
          )
          let text_transform = ui_world_transform(ctx0, content_rect, 0.0F)
          @text.text_set_y_axis(
            text_node.handle,
            @text.YAxisOrientation::TopToBottom,
          )
          @text.text_set_ui_node_size(text_node.handle, Some(content_size))
          @text.text_set_transform(text_node.handle, text_transform)
          @text.text_set_layers(text_node.handle, layers)
        }
      }
      let scroll_raw = match scroll_store.get(entity) {
        Some(v) => v
        None => ScrollPosition::default()
      }
      let scroll = match node_store.get(entity) {
        Some(node) => {
          let x = if node.overflow.x == OverflowAxis::Scroll &&
            scroll_raw.offset_x > 0.0F {
            scroll_raw.offset_x
          } else {
            0.0F
          }
          let y = if node.overflow.y == OverflowAxis::Scroll &&
            scroll_raw.offset_y > 0.0F {
            scroll_raw.offset_y
          } else {
            0.0F
          }
          ScrollPosition::new(x, y)
        }
        None => ScrollPosition::default()
      }
      let child_rect = @math.Rect::new(
        @math.Vec2::new(
          rect.min.x - scroll.offset_x,
          rect.min.y - scroll.offset_y,
        ),
        @math.Vec2::new(
          rect.max.x - scroll.offset_x,
          rect.max.y - scroll.offset_y,
        ),
      )
      if children_store.get(entity) is Some(children) {
        for child in children.entities {
          apply_layout(child, child_rect)
        }
      }
    }

    apply_layout(root, root_rect)
  }

  let mut any_root = false
  root_store.for_each(fn(root, _marker) {
    any_root = true
    layout_root(root)
  })
  any_root |> ignore
}

///|
/// Update `ScrollPosition` using the current mouse wheel delta.
///
/// This is an MVP scroll implementation:
/// - Picks the topmost scroll container under the pointer by `UiStack`.
/// - Applies wheel deltas directly to the stored offsets (clamped to >= 0).
pub fn[
  W : HasRes_UiContext + HasRes_UiStack + Has_Node + Has_UiLayout + Has_ScrollPosition + @hierarchy.Has_Children + @core.ChangeTickWorld,
] ui_scroll_system(
  world : W,
) -> Unit {
  let res = HasRes_UiContext::get_ui_context_resource(world)
  guard res.get() is Some(ctx0) else { return }
  let stack_res = HasRes_UiStack::get_ui_stack_resource(world)
  guard stack_res.get() is Some(stack0) else { return }
  guard ctx0.pointer_pos is Some(pointer) else { return }
  let delta = ctx0.wheel_delta
  if delta.x == 0.0F && delta.y == 0.0F {
    return
  }
  let node_store = Has_Node::get_node_store(world)
  let layout_store = Has_UiLayout::get_ui_layout_store(world)
  let scroll_store = Has_ScrollPosition::get_scroll_position_store(world)
  let children_store = @hierarchy.Has_Children::get_children_store(world)
  let mut target : @core.Entity? = None
  for i in 0.. v
    None => ScrollPosition::default()
  }
  guard node_store.get(target_e) is Some(target_node) else { return }
  let scrolls_x = target_node.overflow.x == OverflowAxis::Scroll
  let scrolls_y = target_node.overflow.y == OverflowAxis::Scroll
  let abs_dx = if delta.x < 0.0F { 0.0F - delta.x } else { delta.x }
  let abs_dy = if delta.y < 0.0F { 0.0F - delta.y } else { delta.y }
  let mut dx = delta.x
  let mut dy = delta.y
  // Match the host accessibility wheel bridge: when both axes are scrollable,
  // pick the dominant axis to avoid accidental horizontal drift from touchpads.
  if scrolls_x && scrolls_y {
    if abs_dy >= abs_dx {
      dx = 0.0F
    } else {
      dy = 0.0F
    }
  }
  let mut max_offset_x = 0.0F
  let mut max_offset_y = 0.0F
  if layout_store.get(target_e) is Some(target_layout) &&
    children_store.get(target_e) is Some(children) {
    let target_size = target_layout.rect.size()
    if target_size.x > 0.0F && target_size.y > 0.0F {
      let mut content_min : @math.Vec2? = None
      let mut content_max : @math.Vec2? = None
      for i in 0..
              Some(
                @math.Vec2::new(
                  if unscrolled_min.x < current.x {
                    unscrolled_min.x
                  } else {
                    current.x
                  },
                  if unscrolled_min.y < current.y {
                    unscrolled_min.y
                  } else {
                    current.y
                  },
                ),
              )
            None => Some(unscrolled_min)
          }
          content_max = match content_max {
            Some(current) =>
              Some(
                @math.Vec2::new(
                  if unscrolled_max.x > current.x {
                    unscrolled_max.x
                  } else {
                    current.x
                  },
                  if unscrolled_max.y > current.y {
                    unscrolled_max.y
                  } else {
                    current.y
                  },
                ),
              )
            None => Some(unscrolled_max)
          }
        }
      }
      match (content_min, content_max) {
        (Some(min_v), Some(max_v)) => {
          let content_w = max_v.x - min_v.x
          let content_h = max_v.y - min_v.y
          let overflow_x = content_w - target_size.x
          let overflow_y = content_h - target_size.y
          if overflow_x > 0.0F {
            max_offset_x = overflow_x
          }
          if overflow_y > 0.0F {
            max_offset_y = overflow_y
          }
        }
        _ => ()
      }
    }
  }
  let mut next_x = if target_node.overflow.x == OverflowAxis::Scroll {
    prev.offset_x + dx
  } else {
    0.0F
  }
  let mut next_y = if target_node.overflow.y == OverflowAxis::Scroll {
    prev.offset_y + dy
  } else {
    0.0F
  }
  if next_x < 0.0F {
    next_x = 0.0F
  }
  if next_y < 0.0F {
    next_y = 0.0F
  }
  if next_x > max_offset_x {
    next_x = max_offset_x
  }
  if next_y > max_offset_y {
    next_y = max_offset_y
  }
  let next = ScrollPosition::{ offset_x: next_x, offset_y: next_y }
  if scroll_store.contains(target_e) {
    scroll_store.set(target_e, next, tick) |> ignore
  } else {
    scroll_store.insert(target_e, next, tick)
  }
}

///|
/// Compute `CalculatedClip` and attach `@render2d.ScissorRect` for clipped UI.
pub fn[
  W : HasRes_UiContext + Has_UiRoot + Has_Node + Has_UiLayout + Has_CalculatedClip + Has_UiText + @render2d.Has_ScissorRect + @hierarchy.Has_Children + @core.ChangeTickWorld,
] ui_clip_system(
  world : W,
) -> Unit {
  let res = HasRes_UiContext::get_ui_context_resource(world)
  guard res.get() is Some(ctx0) else { return }
  guard ctx0.camera_entity is Some(_) else { return }
  let root_store = Has_UiRoot::get_ui_root_store(world)
  let node_store = Has_Node::get_node_store(world)
  let layout_store = Has_UiLayout::get_ui_layout_store(world)
  let clip_store = Has_CalculatedClip::get_calculated_clip_store(world)
  let ui_text_store = Has_UiText::get_ui_text_store(world)
  let scissor_store = @render2d.Has_ScissorRect::get_scissor_rect_store(world)
  let children_store = @hierarchy.Has_Children::get_children_store(world)
  let tick = @core.ChangeTickWorld::read_change_tick(world)
  let viewport_rect = @math.Rect::new(
    @math.Vec2::new(0.0F, 0.0F),
    @math.Vec2::new(ctx0.viewport_size.x, ctx0.viewport_size.y),
  )
  fn clamp_nonneg(v : Float) -> Float {
    if v < 0.0F {
      0.0F
    } else {
      v
    }
  }

  fn resolve_axis(
    val : Val,
    node_axis : Float,
    viewport_size : @math.Vec2,
  ) -> Float {
    match val {
      Val::Auto => 0.0F
      Val::Px(px) => px
      Val::Percent(p) => node_axis * (p / 100.0F)
      Val::Vw(p) => viewport_size.x * (p / 100.0F)
      Val::Vh(p) => viewport_size.y * (p / 100.0F)
      Val::VMin(p) => {
        let m = if viewport_size.x < viewport_size.y {
          viewport_size.x
        } else {
          viewport_size.y
        }
        m * (p / 100.0F)
      }
      Val::VMax(p) => {
        let m = if viewport_size.x > viewport_size.y {
          viewport_size.x
        } else {
          viewport_size.y
        }
        m * (p / 100.0F)
      }
    }
  }

  fn resolve_uirect_px(
    rect : UiRect,
    node_size : @math.Vec2,
    viewport_size : @math.Vec2,
  ) -> (Float, Float, Float, Float) {
    let left = clamp_nonneg(resolve_axis(rect.left, node_size.x, viewport_size))
    let right = clamp_nonneg(
      resolve_axis(rect.right, node_size.x, viewport_size),
    )
    let top = clamp_nonneg(resolve_axis(rect.top, node_size.y, viewport_size))
    let bottom = clamp_nonneg(
      resolve_axis(rect.bottom, node_size.y, viewport_size),
    )
    (left, top, right, bottom)
  }

  fn rect_intersect(a : @math.Rect, b : @math.Rect) -> @math.Rect {
    let min_x = if a.min.x > b.min.x { a.min.x } else { b.min.x }
    let min_y = if a.min.y > b.min.y { a.min.y } else { b.min.y }
    let max_x = if a.max.x < b.max.x { a.max.x } else { b.max.x }
    let max_y = if a.max.y < b.max.y { a.max.y } else { b.max.y }
    let min = @math.Vec2::new(min_x, min_y)
    let max = @math.Vec2::new(
      if max_x < min_x {
        min_x
      } else {
        max_x
      },
      if max_y < min_y {
        min_y
      } else {
        max_y
      },
    )
    @math.Rect::new(min, max)
  }

  fn rect_is_full_viewport(r : @math.Rect) -> Bool {
    r.min.x <= 0.0F &&
    r.min.y <= 0.0F &&
    r.max.x >= viewport_rect.max.x &&
    r.max.y >= viewport_rect.max.y
  }

  fn walk(
    entity : @core.Entity,
    inherited_clip : @math.Rect,
    clipped_by_parent : Bool,
  ) -> Unit {
    // Default to viewport clip if this node does not have layout yet.
    let self_clip = inherited_clip
    let calc = CalculatedClip::{ clip: self_clip }
    if clip_store.contains(entity) {
      clip_store.set(entity, calc, tick) |> ignore
    } else {
      clip_store.insert(entity, calc, tick)
    }
    if clipped_by_parent && !rect_is_full_viewport(self_clip) {
      let scissor = @render2d.ScissorRect::new(self_clip)
      if scissor_store.contains(entity) {
        scissor_store.set(entity, scissor, tick) |> ignore
      } else {
        scissor_store.insert(entity, scissor, tick)
      }
    } else if scissor_store.contains(entity) {
      scissor_store.remove(entity) |> ignore
    }
    if ui_text_store.get(entity) is Some(text_node) {
      let text_scissor = if clipped_by_parent &&
        !rect_is_full_viewport(self_clip) {
        Some(self_clip)
      } else {
        None
      }
      @text.text_set_ui_scissor(text_node.handle, text_scissor)
    }
    let node = match node_store.get(entity) {
      Some(v) => v
      None => Node::default()
    }
    let mut child_clip = self_clip
    let mut child_clipped = clipped_by_parent
    if layout_store.get(entity) is Some(layout) {
      let overflow_clips = node.overflow.x != OverflowAxis::Visible ||
        node.overflow.y != OverflowAxis::Visible
      if overflow_clips {
        let size = layout.rect.size()
        let (border_l, border_t, border_r, border_b) = resolve_uirect_px(
          node.border,
          size,
          ctx0.viewport_size,
        )
        let (padding_l, padding_t, padding_r, padding_b) = resolve_uirect_px(
          node.padding,
          size,
          ctx0.viewport_size,
        )
        let (inset_l, inset_t, inset_r, inset_b) = match
          node.overflow_clip_margin.visual_box {
          OverflowClipBox::BorderBox => (0.0F, 0.0F, 0.0F, 0.0F)
          OverflowClipBox::PaddingBox =>
            (border_l, border_t, border_r, border_b)
          OverflowClipBox::ContentBox =>
            (
              border_l + padding_l,
              border_t + padding_t,
              border_r + padding_r,
              border_b + padding_b,
            )
        }
        let margin = clamp_nonneg(node.overflow_clip_margin.margin)
        let min = @math.Vec2::new(
          layout.rect.min.x + inset_l - margin,
          layout.rect.min.y + inset_t - margin,
        )
        let max = @math.Vec2::new(
          layout.rect.max.x - inset_r + margin,
          layout.rect.max.y - inset_b + margin,
        )
        let mut clip_rect = @math.Rect::new(min, max)
        if node.overflow.x == OverflowAxis::Visible {
          clip_rect = @math.Rect::new(
            @math.Vec2::new(self_clip.min.x, clip_rect.min.y),
            @math.Vec2::new(self_clip.max.x, clip_rect.max.y),
          )
        }
        if node.overflow.y == OverflowAxis::Visible {
          clip_rect = @math.Rect::new(
            @math.Vec2::new(clip_rect.min.x, self_clip.min.y),
            @math.Vec2::new(clip_rect.max.x, self_clip.max.y),
          )
        }
        child_clip = rect_intersect(self_clip, clip_rect)
        child_clipped = true
      }
    }
    if children_store.get(entity) is Some(children) {
      for child in children.entities {
        walk(child, child_clip, child_clipped)
      }
    }
  }

  root_store.for_each(fn(root, _marker) { walk(root, viewport_rect, false) })
}

///|
/// Build `UiStack` ordering (back-to-front), aligned with Bevy `ui_stack_system`.
///
/// Roots are:
/// - Any UI entity with `UiLayout` and no `Parent`
/// - Any entity with `GlobalZIndex` and `UiLayout` (even if parented)
/// - Any entity tagged `UiRoot` (compat with older code paths)
///
/// Children are visited in depth-first order, sorted by `ZIndex` ascending,
/// while preserving insertion order for ties.
pub fn[
  W : HasRes_UiStack + Has_UiRoot + Has_UiLayout + Has_ZIndex + Has_GlobalZIndex + Has_UiText + @hierarchy.Has_Children + @hierarchy.Has_Parent + @math.Has_Transform + @core.ChangeTickWorld,
] ui_stack_system(
  world : W,
) -> Unit {
  let tick = @core.ChangeTickWorld::read_change_tick(world)
  let res = HasRes_UiStack::get_ui_stack_resource(world)
  if !res.is_present() {
    res.insert(UiStack::default(), tick)
  }
  guard res.get_ref_mut(tick) is Some(stack) else { return }
  UiStack::clear(stack)
  let root_store = Has_UiRoot::get_ui_root_store(world)
  let layout_store = Has_UiLayout::get_ui_layout_store(world)
  let z_store = Has_ZIndex::get_z_index_store(world)
  let gz_store = Has_GlobalZIndex::get_global_z_index_store(world)
  let children_store = @hierarchy.Has_Children::get_children_store(world)
  let parent_store = @hierarchy.Has_Parent::get_parent_store(world)
  let transform_store = @math.Has_Transform::get_transform_store(world)
  let ui_text_store = Has_UiText::get_ui_text_store(world)
  fn entity_key(e : @core.Entity) -> (Int, Int) {
    (e.id, e.generation)
  }

  fn cmp_int(a : Int, b : Int) -> Int {
    if a < b {
      -1
    } else if a > b {
      1
    } else {
      0
    }
  }

  fn local_z(
    z_store : @core.ComponentStore[ZIndex],
    entity : @core.Entity,
  ) -> Int {
    match z_store.get(entity) {
      Some(v) => v.value
      None => 0
    }
  }

  fn global_z(
    gz_store : @core.ComponentStore[GlobalZIndex],
    entity : @core.Entity,
  ) -> Int {
    match gz_store.get(entity) {
      Some(v) => v.value
      None => 0
    }
  }

  let visited : @hashmap.HashMap[(Int, Int), Bool] = @hashmap.HashMap::new()
  let roots : Array[(@core.Entity, Int, Int, Int)] = []

  // Root candidates: unparented UI layout nodes.
  let mut root_tie = 0
  layout_store.for_each(fn(entity, _layout) {
    if parent_store.contains(entity) {
      return
    }
    let key = entity_key(entity)
    if visited.contains(key) {
      return
    }
    visited.set(key, true)
    roots.push(
      (entity, global_z(gz_store, entity), local_z(z_store, entity), root_tie),
    )
    root_tie = root_tie + 1
  })

  // Compat roots: explicit UiRoot markers.
  root_store.for_each(fn(entity, _marker) {
    guard layout_store.contains(entity) else { return }
    let key = entity_key(entity)
    if visited.contains(key) {
      return
    }
    visited.set(key, true)
    roots.push(
      (entity, global_z(gz_store, entity), local_z(z_store, entity), root_tie),
    )
    root_tie = root_tie + 1
  })

  // Global-z roots (even if parented).
  gz_store.for_each(fn(entity, _gz) {
    guard layout_store.contains(entity) else { return }
    let key = entity_key(entity)
    if visited.contains(key) {
      return
    }
    visited.set(key, true)
    roots.push(
      (entity, global_z(gz_store, entity), local_z(z_store, entity), root_tie),
    )
    root_tie = root_tie + 1
  })

  // Sort roots by (global_z, local_z, tie).
  roots.sort_by(fn(a, b) {
    let cg = cmp_int(a.1, b.1)
    if cg != 0 {
      return cg
    }
    let cl = cmp_int(a.2, b.2)
    if cl != 0 {
      return cl
    }
    cmp_int(a.3, b.3)
  })
  fn visit(
    entity : @core.Entity,
    layout_store : @core.ComponentStore[UiLayout],
    z_store : @core.ComponentStore[ZIndex],
    gz_store : @core.ComponentStore[GlobalZIndex],
    children_store : @core.ComponentStore[@hierarchy.Children],
    stack : Ref[UiStack],
  ) -> Unit {
    guard layout_store.contains(entity) else { return }
    stack.val.uinodes.push(entity)
    guard children_store.get(entity) is Some(children) else { return }
    let buf : Array[(@core.Entity, Int, Int)] = []
    for i in 0.. {
        let pos = @math.Vec2::new(t0.translation.x, t0.translation.y)
        let scale = @math.Vec2::new(t0.scale.x, t0.scale.y)
        let t = @math.Transform::from_xy_rotation_scale(
          pos,
          t0.rotation_z(),
          scale,
          z~,
        )
        if transform_store.contains(entity) {
          transform_store.set(entity, t, tick) |> ignore
        } else {
          transform_store.insert(entity, t, tick)
        }
      }
      None => ()
    }
    if ui_text_store.get(entity) is Some(text_node) {
      match @text.text_get_transform(text_node.handle) {
        Some(t1) => {
          let pos = @math.Vec2::new(t1.translation.x, t1.translation.y)
          let scale = @math.Vec2::new(t1.scale.x, t1.scale.y)
          let z = z_base + UI_STACK_Z_TEXT
          let t_text = @math.Transform::from_xy_rotation_scale(
            pos,
            t1.rotation_z(),
            scale,
            z~,
          )
          @text.text_set_transform(text_node.handle, t_text)
        }
        None => ()
      }
    }
  }
}

///|
/// Build an AccessKit `TreeUpdate` for the current UI tree.
pub fn[
  W : @a11y.HasRes_A11yState + HasRes_UiContext + HasRes_UiStack + Has_UiLayout + Has_Button + Has_Label + Has_ImageNode + Has_AccessibilityLabel + Has_UiText + Has_CalculatedClip + Has_ScrollPosition + @visibility.Has_ViewVisibility + @hierarchy.Has_Parent + @hierarchy.Has_Children,
] ui_build_a11y_tree_update(
  world : W,
) -> @moon_accesskit.TreeUpdate {
  let res = HasRes_UiContext::get_ui_context_resource(world)
  guard res.get() is Some(ctx0) else {
    let root = @moon_accesskit.NodeId::root()
    let update = @moon_accesskit.TreeUpdate::new(
      @moon_accesskit.TreeId::root(),
      root,
    )
    update.set_tree(@moon_accesskit.Tree::new(root))
    update
  }
  guard ctx0.camera_entity is Some(_) else {
    let root = @moon_accesskit.NodeId::root()
    let update = @moon_accesskit.TreeUpdate::new(
      @moon_accesskit.TreeId::root(),
      root,
    )
    update.set_tree(@moon_accesskit.Tree::new(root))
    update
  }
  let stack_res = HasRes_UiStack::get_ui_stack_resource(world)
  guard stack_res.get() is Some(stack0) else {
    let root = @moon_accesskit.NodeId::root()
    let update = @moon_accesskit.TreeUpdate::new(
      @moon_accesskit.TreeId::root(),
      root,
    )
    update.set_tree(@moon_accesskit.Tree::new(root))
    update
  }
  let layout_store = Has_UiLayout::get_ui_layout_store(world)
  let button_store = Has_Button::get_button_store(world)
  let label_widget_store = Has_Label::get_label_store(world)
  let image_node_store = Has_ImageNode::get_image_node_store(world)
  let label_store = Has_AccessibilityLabel::get_accessibility_label_store(world)
  let ui_text_store = Has_UiText::get_ui_text_store(world)
  let scroll_store = Has_ScrollPosition::get_scroll_position_store(world)
  let parent_store = @hierarchy.Has_Parent::get_parent_store(world)
  let children_store = @hierarchy.Has_Children::get_children_store(world)
  let clip_store = Has_CalculatedClip::get_calculated_clip_store(world)
  let view_visibility_store = @visibility.Has_ViewVisibility::get_view_visibility_store(
    world,
  )
  let a11y_state_res = @a11y.HasRes_A11yState::get_a11y_state_resource(world)

  // Build the set of required nodes (focusables + their ancestor chain), so
  // every node's parent always exists in this update.
  let required : @hashmap.HashMap[(Int, Int), Bool] = @hashmap.HashMap::new()
  fn key(entity : @core.Entity) -> (Int, Int) {
    (entity.id, entity.generation)
  }

  fn mark_required(
    required : @hashmap.HashMap[(Int, Int), Bool],
    parent_store : @core.ComponentStore[@hierarchy.Parent],
    layout_store : @core.ComponentStore[UiLayout],
    entity : @core.Entity,
  ) -> Unit {
    let mut cur = Some(entity)
    while cur is Some(e) {
      if !layout_store.contains(e) {
        break
      }
      let k = key(e)
      if required.contains(k) {
        break
      }
      required.set(k, true)
      cur = match parent_store.get(e) {
        Some(p) => Some(p.entity)
        None => None
      }
    }
  }

  for entity in stack0.uinodes {
    if layout_store.get(entity) is None {
      continue
    }
    if button_store.contains(entity) ||
      label_widget_store.contains(entity) ||
      image_node_store.contains(entity) ||
      label_store.contains(entity) {
      mark_required(required, parent_store, layout_store, entity)
    }
  }
  fn push_nonempty(parts : Ref[Array[String]], text : String) -> Unit {
    let trimmed = text.trim()
    if trimmed != "" {
      parts.val.push(trimmed.to_string())
    }
  }

  fn collect_subtree_text(
    entity : @core.Entity,
    children_store : @core.ComponentStore[@hierarchy.Children],
    ui_text_store : @core.ComponentStore[UiText],
    out : Ref[Array[String]],
  ) -> Unit {
    if ui_text_store.get(entity) is Some(text_node) {
      match @text.text_get_content(text_node.handle) {
        Some(s) => push_nonempty(out, s)
        None => ()
      }
    }
    guard children_store.get(entity) is Some(children) else { return }
    for i in 0.. String {
    if parts.length() == 0 {
      return ""
    }
    let sb = StringBuilder::new(size_hint=0)
    for i in 0.. 0 {
        sb.write_char(' ')
      }
      sb.write_string(parts[i])
    }
    sb.to_string()
  }

  fn calc_label_from_children(
    entity : @core.Entity,
    children_store : @core.ComponentStore[@hierarchy.Children],
    ui_text_store : @core.ComponentStore[UiText],
  ) -> String? {
    guard children_store.get(entity) is Some(children) else { return None }
    let mut name : String? = None
    for i in 0.. 0 {
        name = Some(join_with_space(out.val))
      }
    }
    name
  }

  fn calc_value_from_self(
    entity : @core.Entity,
    children_store : @core.ComponentStore[@hierarchy.Children],
    ui_text_store : @core.ComponentStore[UiText],
  ) -> String {
    let parts : Array[String] = []
    let out = Ref::new(parts)
    collect_subtree_text(entity, children_store, ui_text_store, out)
    join_with_space(out.val)
  }

  // Root node.
  let tree_id = @moon_accesskit.TreeId::root()
  let root_id = @moon_accesskit.NodeId::root()
  let mut focus_id = match a11y_state_res.get() {
    Some(state) => @a11y.A11yState::focused_node(state)
    None => root_id
  }
  if focus_id != root_id {
    let mut exists = false
    for entity in stack0.uinodes {
      if required.contains(key(entity)) &&
        @a11y.node_id_for_entity(entity) == focus_id {
        exists = true
      }
    }
    if !exists {
      focus_id = root_id
    }
  }
  let update = @moon_accesskit.TreeUpdate::new(tree_id, focus_id)
  update.set_tree(@moon_accesskit.Tree::new(root_id))
  let root_node = @moon_accesskit.Node::new(@moon_accesskit.Role::Window)
  root_node.set_bounds(
    @moon_accesskit.Rect::new(
      ctx0.viewport_origin.x.to_double(),
      ctx0.viewport_origin.y.to_double(),
      (ctx0.viewport_origin.x + ctx0.viewport_size.x).to_double(),
      (ctx0.viewport_origin.y + ctx0.viewport_size.y).to_double(),
    ),
  )

  // Attach top-level required nodes under the root.
  for entity in stack0.uinodes {
    if !required.contains(key(entity)) {
      continue
    }
    let parent_required = match parent_store.get(entity) {
      Some(p) =>
        required.contains(key(p.entity)) && layout_store.contains(p.entity)
      None => false
    }
    if !parent_required {
      root_node.push_child(@a11y.node_id_for_entity(entity))
    }
  }
  update.push_node(root_id, root_node)
  for entity in stack0.uinodes {
    if !required.contains(key(entity)) {
      continue
    }
    guard layout_store.get(entity) is Some(layout) else { continue }
    let node_id = @a11y.node_id_for_entity(entity)
    let explicit_label = match label_store.get(entity) {
      Some(v) => v.text
      None => ""
    }
    let is_editable = label_widget_store.contains(entity) &&
      ui_text_store.contains(entity) &&
      explicit_label != ""
    let role = if button_store.contains(entity) {
      @moon_accesskit.Role::Button
    } else if is_editable {
      @moon_accesskit.Role::TextInput
    } else if label_widget_store.contains(entity) {
      @moon_accesskit.Role::Label
    } else if image_node_store.contains(entity) {
      @moon_accesskit.Role::Image
    } else {
      @moon_accesskit.Role::GenericContainer
    }
    let node = @moon_accesskit.Node::new(role)

    // Children in hierarchy order (filtered to required set).
    if children_store.get(entity) is Some(children) {
      for i in 0.. if !vv.is_visible() { node.set_hidden() }
      None => ()
    }
    let can_set_value = role == @moon_accesskit.Role::TextInput
    if role == @moon_accesskit.Role::Button {
      node.add_action(@moon_accesskit.Action::Focus)
      node.add_action(@moon_accesskit.Action::Click)
    }
    if scroll_store.contains(entity) {
      node.add_action(@moon_accesskit.Action::ScrollUp)
      node.add_action(@moon_accesskit.Action::ScrollDown)
      node.add_action(@moon_accesskit.Action::ScrollLeft)
      node.add_action(@moon_accesskit.Action::ScrollRight)
    }
    if can_set_value {
      node.add_action(@moon_accesskit.Action::SetValue)
      node.add_action(@moon_accesskit.Action::Focus)
    }
    if role == @moon_accesskit.Role::Button ||
      role == @moon_accesskit.Role::Image {
      let name = if explicit_label != "" {
        Some(explicit_label)
      } else {
        calc_label_from_children(entity, children_store, ui_text_store)
      }
      match name {
        Some(s) => if s != "" { node.set_label(s) }
        None => ()
      }
    } else if role == @moon_accesskit.Role::Label {
      let value = calc_value_from_self(entity, children_store, ui_text_store)
      if explicit_label != "" {
        node.set_label(explicit_label)
      }
      // Bevy-aligned: keep a `Label` node's value even if it is empty.
      node.set_value(value)
    } else if role == @moon_accesskit.Role::TextInput {
      if explicit_label != "" {
        node.set_label(explicit_label)
      }
      node.set_value(
        calc_value_from_self(entity, children_store, ui_text_store),
      )
    } else if explicit_label != "" {
      node.set_label(explicit_label)
    }
    update.push_node(node_id, node)
  }
  update
}

///|
/// Extract UI accessibility nodes and send them to the host bridge.
pub fn[
  W : @a11y.HasRes_A11yState + HasRes_UiContext + HasRes_UiStack + Has_UiLayout + Has_Button + Has_Label + Has_ImageNode + Has_AccessibilityLabel + Has_UiText + Has_CalculatedClip + Has_ScrollPosition + @visibility.Has_ViewVisibility + @hierarchy.Has_Parent + @hierarchy.Has_Children,
] ui_a11y_update_system(
  world : W,
) -> Unit {
  let update = ui_build_a11y_tree_update(world)
  @a11y.a11y_host_apply_update(update)
}

///|
pub fn[
  W : HasRes_UiContext + HasRes_UiStack + Has_UiLayout + Has_Button + Has_Interaction + Has_RelativeCursorPosition + Has_FocusPolicy + Has_CalculatedClip + @visibility.Has_ViewVisibility + @core.ChangeTickWorld,
] ui_interaction_system(
  world : W,
) -> Unit {
  let res = HasRes_UiContext::get_ui_context_resource(world)
  guard res.get() is Some(ctx0) else { return }
  let stack_res = HasRes_UiStack::get_ui_stack_resource(world)
  guard stack_res.get() is Some(stack0) else { return }
  let layout_store = Has_UiLayout::get_ui_layout_store(world)
  let button_store = Has_Button::get_button_store(world)
  let state_store = Has_Interaction::get_interaction_store(world)
  let cursor_store = Has_RelativeCursorPosition::get_relative_cursor_position_store(
    world,
  )
  let focus_store = Has_FocusPolicy::get_focus_policy_store(world)
  let clip_store = Has_CalculatedClip::get_calculated_clip_store(world)
  let vv_store = @visibility.Has_ViewVisibility::get_view_visibility_store(
    world,
  )
  let tick = @core.ChangeTickWorld::read_change_tick(world)
  let pointer = ctx0.pointer_pos
  let down = ctx0.pointer_down
  fn entity_eq(a : @core.Entity, b : @core.Entity) -> Bool {
    a.id == b.id && a.generation == b.generation
  }

  fn candidates_contains(xs : Array[@core.Entity], e : @core.Entity) -> Bool {
    for v in xs {
      if entity_eq(v, e) {
        return true
      }
    }
    false
  }

  // Collect interactive candidates:
  // - Anything that already has `Interaction`
  // - Anything tagged as `Button` (we auto-initialize `Interaction` for convenience)
  let candidates : Array[@core.Entity] = []
  state_store.for_each(fn(entity, _interaction) { candidates.push(entity) })
  button_store.for_each(fn(entity, _button) {
    if !candidates_contains(candidates, entity) {
      candidates.push(entity)
    }
    if !state_store.contains(entity) {
      state_store.insert(entity, Interaction::None, tick)
    }
  })
  fn rect_intersect(a : @math.Rect, b : @math.Rect) -> @math.Rect {
    let min_x = if a.min.x > b.min.x { a.min.x } else { b.min.x }
    let min_y = if a.min.y > b.min.y { a.min.y } else { b.min.y }
    let max_x = if a.max.x < b.max.x { a.max.x } else { b.max.x }
    let max_y = if a.max.y < b.max.y { a.max.y } else { b.max.y }
    let min = @math.Vec2::new(min_x, min_y)
    let max = @math.Vec2::new(
      if max_x < min_x {
        min_x
      } else {
        max_x
      },
      if max_y < min_y {
        min_y
      } else {
        max_y
      },
    )
    @math.Rect::new(min, max)
  }

  // Apply interactions in UiStack order (front-to-back) until a `Block` focus
  // policy is hit. This matches Bevy: the last entry in UiStack receives input
  // first.
  let active : Array[@core.Entity] = []
  let mut captured = false
  if pointer is Some(p) {
    for i in 0.. v.clip
        None => node_rect
      }
      let visible_rect = rect_intersect(node_rect, clip_rect)
      if !visible_rect.contains(p) {
        continue
      }

      // Update (or insert) RelativeCursorPosition.
      let size = node_rect.size()
      let mut normalized : @math.Vec2? = None
      let mut normalized_visible = @math.Rect::new(
        @math.Vec2::new(0.0F, 0.0F),
        @math.Vec2::new(1.0F, 1.0F),
      )
      if size.x > 0.0F && size.y > 0.0F {
        let rel = @math.Vec2::new(
          (p.x - node_rect.min.x) / size.x,
          (p.y - node_rect.min.y) / size.y,
        )
        normalized = Some(rel)
        let vmin = @math.Vec2::new(
          (visible_rect.min.x - node_rect.min.x) / size.x,
          (visible_rect.min.y - node_rect.min.y) / size.y,
        )
        let vmax = @math.Vec2::new(
          (visible_rect.max.x - node_rect.min.x) / size.x,
          (visible_rect.max.y - node_rect.min.y) / size.y,
        )
        normalized_visible = @math.Rect::new(vmin, vmax)
      }
      let rcp = RelativeCursorPosition::{
        normalized_visible_node_rect: normalized_visible,
        normalized,
      }
      if cursor_store.contains(entity) {
        cursor_store.set(entity, rcp, tick) |> ignore
      } else {
        cursor_store.insert(entity, rcp, tick)
      }
      let next = if down { Interaction::Pressed } else { Interaction::Hovered }
      if state_store.contains(entity) {
        state_store.set(entity, next, tick) |> ignore
      } else {
        state_store.insert(entity, next, tick)
      }
      active.push(entity)
      let focus = match focus_store.get(entity) {
        Some(v) => v
        None => FocusPolicy::Block
      }
      if focus == FocusPolicy::Block {
        captured = true
      }
    }
  }

  // Reset remaining candidates to None (unless currently pressed by the pointer).
  for entity in candidates {
    if candidates_contains(active, entity) {
      continue
    }
    let prev = match state_store.get(entity) {
      Some(v) => v
      None => Interaction::None
    }
    if prev != Interaction::None {
      state_store.set(entity, Interaction::None, tick) |> ignore
    }
  }
}

///|
fn vec2_add(a : @math.Vec2, b : @math.Vec2) -> @math.Vec2 {
  @math.Vec2::new(a.x + b.x, a.y + b.y)
}

///|
fn ui_uv_from_image_rects(
  image_size : @math.Vec2,
  atlas_rect : @math.Rect?,
  rect : @math.Rect?,
  flip_x : Bool,
  flip_y : Bool,
) -> (@math.Vec2, @math.Vec2) {
  let denom_x = image_size.x
  let denom_y = image_size.y
  if denom_x <= 0.0F || denom_y <= 0.0F {
    return (@math.Vec2::new(0.0F, 0.0F), @math.Vec2::new(1.0F, 1.0F))
  }
  let image_rect = @math.Rect::new(
    @math.Vec2::new(0.0F, 0.0F),
    @math.Vec2::new(denom_x, denom_y),
  )
  let texture_rect = match (atlas_rect, rect) {
    (None, None) => image_rect
    (None, Some(r)) => r
    (Some(ar), None) => ar
    (Some(ar), Some(r)) => {
      let min = vec2_add(r.min, ar.min)
      let max = vec2_add(r.max, ar.min)
      @math.Rect::new(min, max)
    }
  }
  let mut uv_min = @math.Vec2::new(
    texture_rect.min.x / denom_x,
    texture_rect.min.y / denom_y,
  )
  let mut uv_max = @math.Vec2::new(
    texture_rect.max.x / denom_x,
    texture_rect.max.y / denom_y,
  )
  if flip_x {
    let tmp = uv_min.x
    uv_min = @math.Vec2::new(uv_max.x, uv_min.y)
    uv_max = @math.Vec2::new(tmp, uv_max.y)
  }
  if flip_y {
    let tmp = uv_min.y
    uv_min = @math.Vec2::new(uv_min.x, uv_max.y)
    uv_max = @math.Vec2::new(uv_max.x, tmp)
  }
  (uv_min, uv_max)
}

///|
pub fn[
  W : HasRes_UiContext + HasRes_UiStack + HasRes_UiScale + Has_UiLayout + Has_Node + Has_BackgroundColor + Has_UiImage + Has_ImageNode + Has_ButtonColors + Has_Interaction + Has_Outline + Has_BoxShadow + Has_UiBoxShadowSamples + Has_BorderColor + Has_BorderRadius + Has_UiAntiAlias + @render2d.HasRes_RenderState + @render2d.Has_RenderLayers + @render2d.Has_ScissorRect + @math.Has_Transform + @visibility.Has_ViewVisibility + @core.ChangeTickWorld,
] ui_render_extract_system(
  world : W,
) -> Unit {
  let res = HasRes_UiContext::get_ui_context_resource(world)
  guard res.get() is Some(ctx0) else { return }
  let stack_res = HasRes_UiStack::get_ui_stack_resource(world)
  guard stack_res.get() is Some(stack0) else { return }
  @render2d.render2d_clear_overlay_ui_rects(world)
  @render2d.render2d_clear_overlay_ui_box_shadows(world)
  @render2d.render2d_clear_overlay_ui_texture_slices(world)
  let layout_store = Has_UiLayout::get_ui_layout_store(world)
  let node_store = Has_Node::get_node_store(world)
  let bg_store = Has_BackgroundColor::get_background_color_store(world)
  let img_store = Has_UiImage::get_ui_image_store(world)
  let image_node_store = Has_ImageNode::get_image_node_store(world)
  let button_store = Has_ButtonColors::get_button_colors_store(world)
  let interaction_store = Has_Interaction::get_interaction_store(world)
  let outline_store = Has_Outline::get_outline_store(world)
  let shadow_store = Has_BoxShadow::get_box_shadow_store(world)
  let shadow_samples_store = Has_UiBoxShadowSamples::get_ui_box_shadow_samples_store(
    world,
  )
  let border_color_store = Has_BorderColor::get_border_color_store(world)
  let radius_store = Has_BorderRadius::get_border_radius_store(world)
  let aa_store = Has_UiAntiAlias::get_ui_anti_alias_store(world)
  let layers_store = @render2d.Has_RenderLayers::get_render_layers_store(world)
  let scissor_store = @render2d.Has_ScissorRect::get_scissor_rect_store(world)
  let transform_store = @math.Has_Transform::get_transform_store(world)
  let vv_store = @visibility.Has_ViewVisibility::get_view_visibility_store(
    world,
  )
  fn clamp_nonneg(v : Float) -> Float {
    if v < 0.0F {
      0.0F
    } else {
      v
    }
  }

  fn resolve_axis(
    val : Val,
    node_axis : Float,
    viewport_size : @math.Vec2,
  ) -> Float {
    match val {
      Val::Auto => 0.0F
      Val::Px(px) => px
      Val::Percent(p) => node_axis * (p / 100.0F)
      Val::Vw(p) => viewport_size.x * (p / 100.0F)
      Val::Vh(p) => viewport_size.y * (p / 100.0F)
      Val::VMin(p) => {
        let m = if viewport_size.x < viewport_size.y {
          viewport_size.x
        } else {
          viewport_size.y
        }
        m * (p / 100.0F)
      }
      Val::VMax(p) => {
        let m = if viewport_size.x > viewport_size.y {
          viewport_size.x
        } else {
          viewport_size.y
        }
        m * (p / 100.0F)
      }
    }
  }

  fn resolve_radius_single(
    val : Val,
    node_size : @math.Vec2,
    viewport_size : @math.Vec2,
  ) -> Float {
    let min_elem = if node_size.x < node_size.y {
      node_size.x
    } else {
      node_size.y
    }
    let raw = match val {
      Val::Auto => 0.0F
      Val::Px(px) => px
      Val::Percent(p) => min_elem * (p / 100.0F)
      Val::Vw(p) => viewport_size.x * (p / 100.0F)
      Val::Vh(p) => viewport_size.y * (p / 100.0F)
      Val::VMin(p) => {
        let m = if viewport_size.x < viewport_size.y {
          viewport_size.x
        } else {
          viewport_size.y
        }
        m * (p / 100.0F)
      }
      Val::VMax(p) => {
        let m = if viewport_size.x > viewport_size.y {
          viewport_size.x
        } else {
          viewport_size.y
        }
        m * (p / 100.0F)
      }
    }
    let clamped = clamp_nonneg(raw)
    let max_r = 0.5F * min_elem
    if clamped > max_r {
      max_r
    } else {
      clamped
    }
  }

  fn clamp_radius_nonneg(radius : Float, node_size : @math.Vec2) -> Float {
    let clamped = clamp_nonneg(radius)
    let min_elem = if node_size.x < node_size.y {
      node_size.x
    } else {
      node_size.y
    }
    let max_r = 0.5F * min_elem
    if clamped > max_r {
      max_r
    } else {
      clamped
    }
  }

  fn transform_with_z(t : @math.Transform, z : Float) -> @math.Transform {
    @math.Transform::from_xy_rotation_scale(
      @math.Vec2::new(t.translation.x, t.translation.y),
      t.rotation_z(),
      @math.Vec2::new(t.scale.x, t.scale.y),
      z~,
    )
  }

  let shadow_samples = match ctx0.camera_entity {
    Some(cam) =>
      match shadow_samples_store.get(cam) {
        Some(v) => v.samples
        None => UiBoxShadowSamples::default().samples
      }
    None => UiBoxShadowSamples::default().samples
  }
  let uv_min = @math.Vec2::new(0.0F, 0.0F)
  let uv_max = @math.Vec2::new(1.0F, 1.0F)
  for i in 0.. v
      None => @render2d.RenderLayers::default()
    }
    let scissor = match scissor_store.get(entity) {
      Some(v) => Some(v.rect)
      None => None
    }
    let scale_x = t0.scale.x
    let scale_y = t0.scale.y
    let draw_scale = @math.Vec2::new(scale_x * size.x, scale_y * size.y)
    let scale_min = if scale_x < scale_y { scale_x } else { scale_y }
    let border_left = clamp_nonneg(
        resolve_axis(node.border.left, size.x, ctx0.viewport_size),
      ) *
      scale_x
    let border_right = clamp_nonneg(
        resolve_axis(node.border.right, size.x, ctx0.viewport_size),
      ) *
      scale_x
    let border_top = clamp_nonneg(
        resolve_axis(node.border.top, size.y, ctx0.viewport_size),
      ) *
      scale_y
    let border_bottom = clamp_nonneg(
        resolve_axis(node.border.bottom, size.y, ctx0.viewport_size),
      ) *
      scale_y
    let border = @render2d.UiBorderRect::new(
      border_left, border_top, border_right, border_bottom,
    )
    let radius = match radius_store.get(entity) {
      Some(r) => {
        let tl = resolve_radius_single(r.top_left, size, ctx0.viewport_size) *
          scale_min
        let tr = resolve_radius_single(r.top_right, size, ctx0.viewport_size) *
          scale_min
        let br = resolve_radius_single(r.bottom_right, size, ctx0.viewport_size) *
          scale_min
        let bl = resolve_radius_single(r.bottom_left, size, ctx0.viewport_size) *
          scale_min
        @render2d.UiCornerRadii::new(tl, tr, br, bl)
      }
      None => @render2d.UiCornerRadii::zero()
    }
    let mut flags = 0
    let aa_enabled = match aa_store.get(entity) {
      Some(UiAntiAlias::Off) => false
      _ => true
    }
    if aa_enabled {
      flags = flags | UI_DRAW_FLAG_ANTIALIAS
    }
    let z_base = UI_STACK_Z_BASE + int_to_float(i) * UI_STACK_Z_STEP

    // BoxShadow (behind background/content/border/text).
    match shadow_store.get(entity) {
      Some(shadow) =>
        if shadow.color.a > 0.0F {
          let spread_x = resolve_axis(
            shadow.spread_radius,
            size.x,
            ctx0.viewport_size,
          )
          let spread_ratio = (spread_x + size.x) / size.x
          let spread_y = size.y * spread_ratio - size.y
          let shadow_size = @math.Vec2::new(
            size.x + spread_x,
            size.y + spread_y,
          )
          if shadow_size.x > 0.0F && shadow_size.y > 0.0F {
            let blur = clamp_nonneg(
              resolve_axis(shadow.blur_radius, size.x, ctx0.viewport_size),
            )
            let bounds = @math.Vec2::new(
              shadow_size.x + 6.0F * blur,
              shadow_size.y + 6.0F * blur,
            )
            if bounds.x > 0.0F && bounds.y > 0.0F {
              let offset_x = resolve_axis(
                shadow.x_offset,
                size.x,
                ctx0.viewport_size,
              )
              let offset_y = resolve_axis(
                shadow.y_offset,
                size.y,
                ctx0.viewport_size,
              )
              // UI uses +Y down; world uses +Y up.
              let offset_local = @math.Vec2::new(
                offset_x * scale_x,
                -offset_y * scale_y,
              )
              let offset_world = rotate_vec2(offset_local, t0.rotation_z())
              let t_shadow = @math.Transform::from_xy_rotation_scale(
                @math.Vec2::new(
                  t0.translation.x + offset_world.x,
                  t0.translation.y + offset_world.y,
                ),
                t0.rotation_z(),
                @math.Vec2::new(t0.scale.x, t0.scale.y),
                z=z_base + UI_STACK_Z_SHADOW,
              )
              let radius_shadow = match radius_store.get(entity) {
                Some(r) => {
                  let tl = resolve_radius_single(
                      r.top_left,
                      size,
                      ctx0.viewport_size,
                    ) *
                    spread_ratio *
                    scale_min
                  let tr = resolve_radius_single(
                      r.top_right,
                      size,
                      ctx0.viewport_size,
                    ) *
                    spread_ratio *
                    scale_min
                  let br = resolve_radius_single(
                      r.bottom_right,
                      size,
                      ctx0.viewport_size,
                    ) *
                    spread_ratio *
                    scale_min
                  let bl = resolve_radius_single(
                      r.bottom_left,
                      size,
                      ctx0.viewport_size,
                    ) *
                    spread_ratio *
                    scale_min
                  @render2d.UiCornerRadii::new(tl, tr, br, bl)
                }
                None => @render2d.UiCornerRadii::zero()
              }
              let draw_scale_shadow = @math.Vec2::new(
                scale_x * bounds.x,
                scale_y * bounds.y,
              )
              let size_shadow_world = @math.Vec2::new(
                scale_x * shadow_size.x,
                scale_y * shadow_size.y,
              )
              @render2d.render2d_push_ui_box_shadow_scissor(
                world,
                entity,
                ctx0.white_texture,
                t_shadow,
                draw_scale_shadow,
                shadow.color,
                layers,
                scissor,
                size_shadow_world,
                radius_shadow,
                blur * scale_min,
                shadow_samples,
              )
            }
          }
        }
      None => ()
    }

    // Background color.
    let bg_color_opt = match button_store.get(entity) {
      Some(colors) =>
        Some(button_color_for_state(colors, interaction_store.get(entity)))
      None =>
        match bg_store.get(entity) {
          Some(v) => Some(v.color)
          None => None
        }
    }
    match bg_color_opt {
      Some(color) =>
        if color.a > 0.0F {
          let t_bg = transform_with_z(t0, z_base + UI_STACK_Z_BACKGROUND)
          @render2d.render2d_push_ui_rect_uv_scissor(
            world,
            entity,
            ctx0.white_texture,
            t_bg,
            draw_scale,
            uv_min,
            uv_max,
            color,
            layers,
            scissor,
            flags,
            radius,
            border,
          )
        }
      None => ()
    }

    // Image content (Bevy `ImageNode`, with `UiImage` compatibility).
    let image_info = match image_node_store.get(entity) {
      Some(img) =>
        Some(
          (
            img.image,
            img.color,
            img.texture_atlas,
            img.rect,
            img.flip_x,
            img.flip_y,
            Some(img.image_mode),
          ),
        )
      None =>
        match img_store.get(entity) {
          Some(img) =>
            Some(
              (
                img.image,
                img.color,
                img.texture_atlas,
                img.rect,
                img.flip_x,
                img.flip_y,
                None,
              ),
            )
          None => None
        }
    }
    match image_info {
      Some(info) => {
        let texture = info.0
        let color = info.1
        let texture_atlas = info.2
        let rect = info.3
        let flip_x = info.4
        let flip_y = info.5
        let mode = match info.6 {
          Some(m) => m
          None => NodeImageMode::Stretch
        }
        let mut image_width = @asset.asset_texture_width(texture)
        let mut image_height = @asset.asset_texture_height(texture)
        if image_width <= 0 || image_height <= 0 {
          // Avoid division by zero for not-yet-loaded textures.
          image_width = 1
          image_height = 1
        }
        let image_size = @math.Vec2::new(
          Float::from_double(image_width.to_double()),
          Float::from_double(image_height.to_double()),
        )
        let atlas_rect = match texture_atlas {
          Some(atlas) =>
            match @asset.asset_get_texture_atlas_layout(atlas.layout) {
              Some(layout_value) =>
                if atlas.index >= 0 &&
                  atlas.index < layout_value.textures.length() {
                  Some(layout_value.textures[atlas.index].as_rect())
                } else {
                  None
                }
              None => None
            }
          None => None
        }
        let pair = ui_uv_from_image_rects(
          image_size, atlas_rect, rect, flip_x, flip_y,
        )
        let uv_min_img = pair.0
        let uv_max_img = pair.1
        let t_content = transform_with_z(t0, z_base + UI_STACK_Z_CONTENT)
        if color.a > 0.0F && NodeImageMode::uses_slices(mode) {
          let sf = safe_scale_factor(@window.window_scale_factor())
          let ui_scale_value = match ensure_ui_scale_resource(world) {
            Some(ui_scale) =>
              if ui_scale.val.value <= 0.0F {
                1.0F
              } else {
                ui_scale.val.value
              }
            None => 1.0F
          }
          // `size` is in logical UI units. Convert to physical pixels before
          // computing slice/tiling parameters to match Bevy.
          let px_per_ui = sf / ui_scale_value
          let target_size = @math.Vec2::new(
            size.x * px_per_ui,
            size.y * px_per_ui,
          )
          // Image slicing uses the size of the selected texture region (atlas/rect).
          let image_rect = @math.Rect::new(
            @math.Vec2::new(0.0F, 0.0F),
            @math.Vec2::new(image_size.x, image_size.y),
          )
          let texture_rect = match (atlas_rect, rect) {
            (None, None) => image_rect
            (None, Some(r)) => r
            (Some(ar), None) => ar
            (Some(ar), Some(r)) => {
              let min = vec2_add(r.min, ar.min)
              let max = vec2_add(r.max, ar.min)
              @math.Rect::new(min, max)
            }
          }
          let region_size = texture_rect.size()
          let (texture_slices, target_slices, repeat) = compute_texture_slices(
            region_size, target_size, mode,
          )
          let atlas_rect_uv = @math.Vec4::new(
            uv_min_img.x,
            uv_min_img.y,
            uv_max_img.x,
            uv_max_img.y,
          )
          @render2d.render2d_push_ui_texture_slice_scissor(
            world, entity, texture, t_content, draw_scale, color, layers, scissor,
            texture_slices, target_slices, repeat, atlas_rect_uv,
          )
        } else if color.a > 0.0F {
          @render2d.render2d_push_ui_rect_uv_scissor(
            world,
            entity,
            texture,
            t_content,
            draw_scale,
            uv_min_img,
            uv_max_img,
            color,
            layers,
            scissor,
            flags | UI_DRAW_FLAG_TEXTURED,
            radius,
            border,
          )
        }
      }
      None => ()
    }

    // Border.
    let has_border = border_left > 0.0F ||
      border_right > 0.0F ||
      border_top > 0.0F ||
      border_bottom > 0.0F
    if has_border &&
      border_color_store.get(entity) is Some(bc) &&
      bc.color.a > 0.0F {
      let t_border = transform_with_z(t0, z_base + UI_STACK_Z_BORDER)
      @render2d.render2d_push_ui_rect_uv_scissor(
        world,
        entity,
        ctx0.white_texture,
        t_border,
        draw_scale,
        uv_min,
        uv_max,
        bc.color,
        layers,
        scissor,
        flags | UI_DRAW_FLAG_BORDER,
        radius,
        border,
      )
    }

    // Outline.
    match outline_store.get(entity) {
      Some(outline) =>
        if outline.color.a > 0.0F {
          let outline_width = clamp_nonneg(
            resolve_axis(outline.width, size.x, ctx0.viewport_size),
          )
          if outline_width > 0.0F {
            let outline_offset = resolve_axis(
              outline.offset,
              size.x,
              ctx0.viewport_size,
            )
            let outer = outline_offset + outline_width
            let outline_size = @math.Vec2::new(
              size.x + 2.0F * outer,
              size.y + 2.0F * outer,
            )
            if outline_size.x > 0.0F && outline_size.y > 0.0F {
              let t_outline = transform_with_z(t0, z_base + UI_STACK_Z_OUTLINE)
              let outline_draw_scale = @math.Vec2::new(
                scale_x * outline_size.x,
                scale_y * outline_size.y,
              )
              let outline_border = @render2d.UiBorderRect::new(
                outline_width * scale_x,
                outline_width * scale_y,
                outline_width * scale_x,
                outline_width * scale_y,
              )
              let outline_radius = match radius_store.get(entity) {
                Some(r) => {
                  let tl = clamp_radius_nonneg(
                      resolve_radius_single(
                        r.top_left,
                        size,
                        ctx0.viewport_size,
                      ) +
                      outer,
                      outline_size,
                    ) *
                    scale_min
                  let tr = clamp_radius_nonneg(
                      resolve_radius_single(
                        r.top_right,
                        size,
                        ctx0.viewport_size,
                      ) +
                      outer,
                      outline_size,
                    ) *
                    scale_min
                  let br = clamp_radius_nonneg(
                      resolve_radius_single(
                        r.bottom_right,
                        size,
                        ctx0.viewport_size,
                      ) +
                      outer,
                      outline_size,
                    ) *
                    scale_min
                  let bl = clamp_radius_nonneg(
                      resolve_radius_single(
                        r.bottom_left,
                        size,
                        ctx0.viewport_size,
                      ) +
                      outer,
                      outline_size,
                    ) *
                    scale_min
                  @render2d.UiCornerRadii::new(tl, tr, br, bl)
                }
                None =>
                  @render2d.UiCornerRadii::new(
                    clamp_radius_nonneg(outer, outline_size) * scale_min,
                    clamp_radius_nonneg(outer, outline_size) * scale_min,
                    clamp_radius_nonneg(outer, outline_size) * scale_min,
                    clamp_radius_nonneg(outer, outline_size) * scale_min,
                  )
              }
              @render2d.render2d_push_ui_rect_uv_scissor(
                world,
                entity,
                ctx0.white_texture,
                t_outline,
                outline_draw_scale,
                uv_min,
                uv_max,
                outline.color,
                layers,
                scissor,
                flags | UI_DRAW_FLAG_BORDER,
                outline_radius,
                outline_border,
              )
            }
          }
        }
      None => ()
    }
  }
}

///|
pub fn[
  W : @a11y.HasRes_A11yState + HasRes_UiContext + HasRes_UiScale + Has_UiRoot + Has_Node + Has_ContentSize + Has_ImageNodeSize + Has_ZIndex + Has_GlobalZIndex + HasRes_UiStack + Has_ScrollPosition + Has_UiLayout + Has_CalculatedClip + Has_RelativeCursorPosition + Has_FocusPolicy + Has_BackgroundColor + Has_UiImage + Has_ImageNode + Has_ButtonColors + Has_Button + Has_Label + Has_Interaction + Has_UiText + Has_AccessibilityLabel + Has_Outline + Has_BoxShadow + Has_UiBoxShadowSamples + Has_BorderColor + Has_BorderRadius + Has_UiAntiAlias + @hierarchy.Has_Children + @hierarchy.Has_Parent + @render2d.HasRes_RenderState + @render2d.Has_Camera2d + @render2d.Has_Camera + @render2d.Has_Projection + @render2d.Has_RenderTarget + @render2d.Has_RenderLayers + @render2d.Has_Sprite + @render2d.Has_ScissorRect + @math.Has_Transform + @transform.Has_GlobalTransform + @visibility.Has_Visibility + @visibility.Has_InheritedVisibility + @visibility.Has_ViewVisibility + @core.ChangeTickWorld,
] ui_plugin(
  app : @app.App[W],
) -> @app.App[W] {
  app
  .add_pre_update_system_config(
    @app.system(ui_update_context_system).in_set(ui_set_context),
  )
  .add_pre_update_system_config(
    @app.system(ui_scroll_system).in_set(ui_set_scroll),
  )
  .add_pre_update_system_config(
    @app.system(ui_image_content_size_system).in_set(ui_set_measure),
  )
  .add_pre_update_system_config(
    @app.system(ui_layout_system).in_set(ui_set_layout),
  )
  .add_pre_update_system_config(@app.system(ui_clip_system).in_set(ui_set_clip))
  .add_pre_update_system_config(
    @app.system(ui_stack_system).in_set(ui_set_stack),
  )
  .add_pre_update_system_config(
    @app.system(ui_a11y_update_system).in_set(ui_set_a11y),
  )
  .add_pre_update_system_config(
    @app.system(ui_interaction_system).in_set(ui_set_interaction),
  )
  .add_pre_update_system_config(
    @app.system(ui_render_extract_system).in_set(ui_set_render_extract),
  )
  .configure_set(@app.PreUpdate, ui_set_context, before=[ui_set_scroll])
  .configure_set(@app.PreUpdate, ui_set_scroll, after=[ui_set_context], before=[
    ui_set_measure,
  ])
  .configure_set(@app.PreUpdate, ui_set_measure, after=[ui_set_scroll], before=[
    ui_set_layout,
  ])
  .configure_set(
    @app.PreUpdate,
    ui_set_layout,
    after=[@transform.transform_set_update_children],
    before=[@transform.transform_set_propagate],
  )
  .configure_set(@app.PreUpdate, ui_set_clip, after=[ui_set_layout], before=[
    ui_set_stack,
  ])
  .configure_set(@app.PreUpdate, ui_set_stack, after=[ui_set_clip], before=[
    ui_set_a11y,
  ])
  .configure_set(@app.PreUpdate, ui_set_a11y, after=[ui_set_stack], before=[
    ui_set_interaction,
  ])
  .configure_set(@app.PreUpdate, ui_set_interaction, after=[ui_set_a11y])
  .configure_set(@app.PreUpdate, ui_set_render_extract, after=[
    ui_set_interaction,
  ])
}