///|
fn ElementNode::dispatch_pointer(
  self : ElementNode,
  event : PointerEvent,
  render : RenderNode,
  environment? : Environment = Environment::neutral(),
  style? : ViewStyle = ViewStyle::empty(),
  text_system? : TextSystem = TextSystem::fallback(),
  capture_lookup? : Bool = true,
  now_ms? : Double = 0.0,
) -> ElementEventResult {
  let child_result = self.dispatch_pointer_to_children(
    event,
    render,
    environment~,
    style~,
    text_system~,
    capture_lookup~,
    now_ms~,
  )
  if child_result.activated {
    child_result
  } else {
    let own = self.dispatch_view_event(
      self.view_node,
      Pointer(event),
      render,
      text_system,
      environment~,
      style~,
      now_ms~,
    )
    let deliveries = child_result.deliveries.copy()
    deliveries.append(own.deliveries)
    {
      changed: child_result.changed || own.changed,
      activated: own.activated,
      captured: child_result.captured || own.captured,
      capture_target: match child_result.capture_target {
        Some(target) => Some(target)
        None => own.capture_target
      },
      focused: match own.focused {
        Some(value) => Some(value)
        None => child_result.focused
      },
      deliveries,
    }
  }
}

///|
fn ElementNode::dispatch_view_event(
  self : ElementNode,
  data : &RuntimeViewNode,
  event : AppEvent,
  render : RenderNode,
  text_system : TextSystem,
  environment? : Environment = Environment::neutral(),
  style? : ViewStyle = ViewStyle::empty(),
  now_ms? : Double = 0.0,
  focused? : Bool = false,
) -> ElementEventResult {
  let result = data.event({
    frame: render.frame,
    child_frames: render.children.map(child => child.frame),
    event,
    focused,
    state: self.control.view_context(focused),
    text_control: self.control.text_context(),
    text_system,
    environment,
    style,
    now_ms,
  })
  match result.state {
    Some(next_state) => self.control.apply_view_context(next_state)
    None => ()
  }
  match result.text_control {
    Some(next_control) => self.control.apply_text_context(next_control)
    None => ()
  }
  if result.state is Some(_) || result.text_control is Some(_) {
    self.dirty.mark_semantics()
  }
  self.apply_view_dirty_hint(result.dirty)
  {
    changed: result.changed || result.captured,
    activated: result.activated,
    captured: result.captured,
    capture_target: if result.captured {
      Some(self.id)
    } else {
      None
    },
    focused: if result.focused {
      Some(self.id)
    } else {
      None
    },
    deliveries: result.deliveries,
  }
}

///|
fn ElementNode::apply_view_dirty_hint(
  self : ElementNode,
  dirty : ViewDirtyHint,
) -> Unit {
  match dirty {
    ViewClean => ()
    ViewPaintDirty => self.mark_pointer_paint_dirty()
    ViewLayoutDirty => {
      self.dirty.mark_layout()
      self.clear_cached_layout_and_paint()
    }
    ViewBuildDirty => self.dirty.mark_build_only()
  }
}

///|
fn ElementNode::mark_pointer_paint_dirty(self : ElementNode) -> Unit {
  self.dirty.mark_paint()
  self.render_cache.clear()
}

///|
fn ElementNode::mark_pointer_descendant_paint_dirty(self : ElementNode) -> Unit {
  self.dirty.mark_paint_descendant()
  self.render_cache.invalidate_composition()
}

///|
fn ElementNode::dispatch_pointer_to_children(
  self : ElementNode,
  event : PointerEvent,
  render : RenderNode,
  environment~ : Environment,
  style~ : ViewStyle,
  text_system~ : TextSystem,
  capture_lookup~ : Bool,
  now_ms~ : Double,
) -> ElementEventResult {
  let mut changed = false
  let mut activated = false
  let mut captured = false
  let mut capture_target : ElementId? = None
  let mut focused : ElementId? = None
  let deliveries : Array[() -> Unit] = []
  let mut layout_changed = false
  let child_environment = self.view_node.child_environment(environment)
  let child_style = self.view_node.child_style(style)
  // On a `Move`, the pointer can only be over one element, so only one child
  // receives the event. Any *other* child that still holds hover (from a prior
  // move) has been left behind and must get a synthetic `Exit` to clear it —
  // host backends never emit a leave event, and a move outside a child is not
  // delivered to it by hit-testing. Doing this in the same traversal that
  // delivers the `Move` keeps the `Exit` target identical to whatever the
  // `Move` previously hovered, regardless of repaint boundaries, overlays, or
  // element-id churn, which a separate top-down hover resolver got wrong. An
  // `Exit` itself cascades the same way, so a hovered leaf deep inside an
  // overlay still gets cleared when the pointer leaves the overlay's frame.
  //
  // Touch hosts (HarmonyOS / Android) often never emit Move between taps: Up
  // leaves the control in `Hovered` (button_control treats Up-inside as hover),
  // and the next Down hits a *different* control without a Move. Without Exit
  // on Down, the previous button keeps its hover/press tint (gray-white) until
  // some later full redraw. Include Down (and Cancel) in the stale-hover clear.
  let clear_stale_hover = event.phase == Move ||
    event.phase == Exit ||
    event.phase == Down ||
    event.phase == Cancel
  let mut delivered_main_event = false
  // Once a hoisted overlay surface covering the point has been dispatched,
  // in-flow siblings must not receive the same main event — even when the
  // overlay content itself is non-interactive (text, tooltips, spacer
  // containers). Without this, clicks fall through a popup onto whatever it
  // covers (focusing covered text fields, pressing covered buttons). Active
  // pointer captures are exempt so an ongoing gesture keeps its target.
  let mut overlay_covered = false
  // Hoisted overlay subtrees (control popups, presentations) receive pointer
  // events before in-flow content, top-most first — globally, via the
  // `subtree_overlay_contains` prepass: a popup hoisted anywhere inside a
  // child subtree wins over in-flow siblings painted underneath it. Their
  // placed frames bound the hit region, so a click outside the popup falls
  // through to in-flow content below.
  let dispatch_order : Array[Int] = []
  let is_overlay_owner : Array[Bool] = []
  for _ in self.children {
    is_overlay_owner.push(false)
  }
  let mut overlay_search = render.children.length() - 1
  while overlay_search >= 0 {
    if overlay_search < self.children.length() &&
      render.children[overlay_search].is_overlay_child &&
      render.children[overlay_search].hit_bounds().contains(event.position) {
      dispatch_order.push(overlay_search)
      is_overlay_owner[overlay_search] = true
    } else if overlay_search < self.children.length() &&
      render.children[overlay_search].subtree_overlay_contains(event.position) {
      dispatch_order.push(overlay_search)
      is_overlay_owner[overlay_search] = true
    }
    overlay_search = overlay_search - 1
  }
  let mut regular_index = self.children.length() - 1
  while regular_index >= 0 {
    // Skip children the overlay prepass already enqueued (subtree overlay
    // owners): a nested overlay host is not itself `is_overlay_child`, so
    // without this guard it would be dispatched twice — the second pass ran
    // the stale-hover branch against the subtree the first pass just pressed
    // and synthesized an Exit that cancelled the fresh press (dialogs inside
    // a nested host — settings inside a toast host — never received taps).
    if regular_index < render.children.length() &&
      !render.children[regular_index].is_overlay_child &&
      !is_overlay_owner[regular_index] {
      dispatch_order.push(regular_index)
    }
    regular_index = regular_index - 1
  }
  for index in dispatch_order {
    if index < render.children.length() {
      let child = self.children[index]
      let child_render = render.children[index]
      let has_capture = capture_lookup &&
        child.has_active_pointer_capture(event.phase)
      // An overlay owner is dispatched for its hoisted popup even when the
      // point lies outside the owner's own (possibly clipped) frame.
      let hit = is_overlay_owner[index] ||
        has_capture ||
        child_render.hit_bounds().contains(event.position)
      // In-flow children are skipped while an overlay surface covering the
      // point has been dispatched (unless an active capture targets them).
      let blocked_by_overlay = overlay_covered &&
        !is_overlay_owner[index] &&
        !has_capture
      if hit && !delivered_main_event && !blocked_by_overlay {
        if is_overlay_owner[index] {
          overlay_covered = true
        }
        let result = child.dispatch_pointer(
          event,
          child_render,
          environment=child_environment,
          style=child_style,
          text_system~,
          capture_lookup~,
          now_ms~,
        )
        changed = changed || result.changed
        activated = activated || result.activated
        captured = captured || result.captured
        deliveries.append(result.deliveries)
        match (capture_target, result.capture_target) {
          (None, Some(target)) => capture_target = Some(target)
          _ => ()
        }
        layout_changed = layout_changed ||
          (result.changed && child.has_dirty_layout())
        match result.focused {
          Some(_) => focused = result.focused
          None => ()
        }
        // Only consider the main event delivered when the child actually
        // handled it — matching the original `break` condition. A hit child
        // that did not handle the event (e.g. a non-interactive container)
        // lets the traversal continue so a sibling beneath it can receive
        // the event; overlay owners are the exception — reaching one means
        // the point is inside a popup surface, so in-flow content is blocked
        // via `overlay_covered`. For Move/Exit, the loop keeps going
        // regardless so that previously hovered siblings get a synthetic
        // Exit to clear hover.
        if has_capture ||
          result.changed ||
          result.activated ||
          result.focused is Some(_) {
          delivered_main_event = true
        }
      } else if clear_stale_hover && child.has_hovered_descendant() {
        // Pointer left this child (Move) or pressed elsewhere (Down on touch)
        // without a host leave event — send `Exit` so hover/press clears. The
        // exit carries the current position, outside this child by definition.
        let exit_event = PointerEvent::new(position=event.position, phase=Exit)
        let exit_result = child.dispatch_pointer(
          exit_event,
          child_render,
          environment=child_environment,
          style=child_style,
          text_system~,
          capture_lookup=false,
          now_ms~,
        )
        changed = changed || exit_result.changed
        captured = captured || exit_result.captured
        deliveries.append(exit_result.deliveries)
        if exit_result.changed && child.has_dirty_layout() {
          layout_changed = true
        }
      }
    }
  }
  if changed {
    if layout_changed {
      self.dirty.mark_layout_descendant()
      self.layout_cache.clear()
      self.render_cache.clear()
    } else {
      self.mark_pointer_descendant_paint_dirty()
    }
  }
  { changed, activated, captured, capture_target, focused, deliveries, }
}

///|
/// True if this element or any descendant currently reports `hovered` or
/// `pressed` in its view state. Used to decide which children to send a
/// synthetic `Exit` to when the pointer leaves them (Move) or presses
/// elsewhere (Down on touch). A child without hover/press has nothing to clear,
/// so it is skipped — this keeps the extra dispatch off the common case.
fn ElementNode::has_hovered_descendant(self : ElementNode) -> Bool {
  if self.control.view_state.hovered || self.control.view_state.pressed {
    return true
  }
  for child in self.children {
    if child.has_hovered_descendant() {
      return true
    }
  }
  false
}

///|
fn RenderNode::hit_bounds(self : RenderNode) -> Rect {
  match self.hit_rect {
    Some(hit) => hit
    None => self.paint_bounds
  }
}

///|

///|
fn ElementNode::has_active_pointer_capture(
  self : ElementNode,
  phase : PointerPhase,
) -> Bool {
  if phase == Down {
    false
  } else if self.has_local_active_pointer_capture() {
    true
  } else {
    self.children.any(child => child.has_active_pointer_capture(phase))
  }
}

///|
fn ElementNode::has_local_active_pointer_capture(_self : ElementNode) -> Bool {
  false
}

///|
fn ElementNode::active_pointer_capture_id(
  self : ElementNode,
  phase : PointerPhase,
) -> ElementId? {
  if phase == Down {
    None
  } else if self.has_local_active_pointer_capture() {
    Some(self.id)
  } else {
    let mut index = self.children.length() - 1
    while index >= 0 {
      match self.children[index].active_pointer_capture_id(phase) {
        Some(id) => return Some(id)
        None => ()
      }
      index = index - 1
    }
    None
  }
}

///|
fn ElementNode::dispatch_pointer_to_target(
  self : ElementNode,
  target : ElementId,
  event : PointerEvent,
  render : RenderNode,
  environment? : Environment = Environment::neutral(),
  style? : ViewStyle = ViewStyle::empty(),
  text_system~ : TextSystem,
  now_ms? : Double = 0.0,
) -> ElementEventResult? {
  let (result, _) = self.dispatch_pointer_to_target_with_scroll_ancestor(
    target,
    event,
    render,
    environment~,
    style~,
    text_system~,
    now_ms~,
  )
  result
}

///|
/// Deliver a captured event to its target and, for Move/Up/Cancel, let the
/// nearest scroll-view ancestor observe the same stream. A child control can
/// keep its normal capture/click semantics while a surrounding scroll view
/// still recognizes a touch drag that started on that control.
fn ElementNode::dispatch_pointer_to_target_with_scroll_ancestor(
  self : ElementNode,
  target : ElementId,
  event : PointerEvent,
  render : RenderNode,
  environment? : Environment = Environment::neutral(),
  style? : ViewStyle = ViewStyle::empty(),
  text_system~ : TextSystem,
  now_ms? : Double = 0.0,
  bubble_scroll? : Bool = true,
) -> (ElementEventResult?, Bool) {
  if self.id == target {
    (
      Some(
        self.dispatch_pointer(
          event,
          render,
          environment~,
          style~,
          text_system~,
          capture_lookup=false,
          now_ms~,
        ),
      ),
      false,
    )
  } else {
    let child_environment = self.view_node.child_environment(environment)
    let child_style = self.view_node.child_style(style)
    let mut index = 0
    while index < self.children.length() {
      if index < render.children.length() {
        match
          self.children[index].dispatch_pointer_to_target_with_scroll_ancestor(
            target,
            event,
            render.children[index],
            environment=child_environment,
            style=child_style,
            text_system~,
            now_ms~,
            bubble_scroll~,
          ) {
          (Some(result), bubbled) => {
            // Pointer capture normally routes Move/Up/Cancel only to the
            // target. Gesture modifiers are often stacked, though (for
            // example a tap wrapped by a drag), so ancestors must observe the
            // captured stream as it unwinds. A child activation wins the
            // gesture; ancestors receive a synthetic Cancel in that case so
            // they can release pending drag/tap state without activating a
            // second command.
            let should_bubble = bubble_scroll &&
              event.phase != Down &&
              // A nested scroll view has already observed this stream when
              // `bubbled` is true. Keep walking so non-scroll gesture
              // ancestors can still clean up, but avoid feeding a second
              // scroll container the same captured event.
              (!bubbled || !self.is_scroll_view_node())
            if should_bubble {
              let ancestor_event = if result.activated && event.phase == Up {
                PointerEvent::new(
                  position=event.position,
                  phase=Cancel,
                  button=event.button,
                  modifiers=event.modifiers,
                  time_ms=event.time_ms,
                )
              } else {
                event
              }
              let own = self.dispatch_view_event(
                self.view_node,
                Pointer(ancestor_event),
                render,
                text_system,
                environment~,
                style~,
                now_ms~,
              )
              let combined = merge_element_event_results(result, own)
              // Once an ancestor drag recognizes a Move, cancel the child
              // press recognizer. Otherwise the captured child can still
              // activate on Up after the drag has already started.
              let combined = if event.phase == Move &&
                own.changed &&
                (own.activated || self.is_scroll_view_node()) &&
                result.captured {
                let cancel_event = PointerEvent::new(
                  position=event.position,
                  phase=Cancel,
                  button=event.button,
                  modifiers=event.modifiers,
                  time_ms=event.time_ms,
                )
                match
                  self.children[index].dispatch_pointer_to_target_with_scroll_ancestor(
                    target,
                    cancel_event,
                    render.children[index],
                    environment=child_environment,
                    style=child_style,
                    text_system~,
                    now_ms~,
                    bubble_scroll=false,
                  ) {
                  (Some(cancelled), _) =>
                    merge_element_event_results(combined, cancelled)
                  (None, _) => combined
                }
              } else {
                combined
              }
              if combined.changed {
                self.mark_pointer_paint_dirty()
              }
              // `bubbled` tracks whether a scroll ancestor has already
              // observed the stream, rather than whether an arbitrary
              // gesture ancestor handled it. This lets a scroll view above a
              // tap/drag stack continue to receive the captured Move.
              return (
                Some(combined),
                bubbled || (self.is_scroll_view_node() && own.captured),
              )
            }
            if result.changed {
              self.mark_pointer_paint_dirty()
            }
            return (Some(result), bubbled)
          }
          (None, _) => ()
        }
      }
      index = index + 1
    }
    (None, false)
  }
}

///|
fn merge_element_event_results(
  first : ElementEventResult,
  second : ElementEventResult,
) -> ElementEventResult {
  let deliveries = first.deliveries.copy()
  deliveries.append(second.deliveries)
  {
    changed: first.changed || second.changed,
    activated: first.activated || second.activated,
    captured: first.captured || second.captured,
    capture_target: match first.capture_target {
      Some(target) => Some(target)
      None => second.capture_target
    },
    focused: match second.focused {
      Some(value) => Some(value)
      None => first.focused
    },
    deliveries,
  }
}

///|
fn ElementNode::is_scroll_view_node(self : ElementNode) -> Bool {
  self.view_node.semantics().role == Some(ScrollView)
}