Selkie.git | lib/Selkie/App/Internal/ | HitTest.rakumod


=begin pod

=head1 NAME

Selkie::App::Internal::HitTest - internal coordinate hit-testing helpers

=head1 DESCRIPTION

Implementation detail for C<Selkie::App>. Use C<Selkie::App.widget-at-in> from
application code.

=end pod

unit module Selkie::App::Internal::HitTest;

use Selkie::Widget;

# Coordinate-based depth-first hit-test against a widget tree. Children are
# walked in reverse so later-added, visually-on-top siblings win.
sub walk-for-point($w, Int $y, Int $x) {
    return Nil without $w;
    return Nil unless $w.contains-point($y, $x);
    if $w.can('children') {
        for $w.children.reverse -> $child {
            my $hit = walk-for-point($child, $y, $x);
            return $hit if $hit.defined;
        }
    }
    if $w.can('content') {
        my $c = $w.content;
        if $c.defined {
            my $hit = walk-for-point($c, $y, $x);
            return $hit if $hit.defined;
        }
    }
    $w;
}

# Whole-tree walk for overlays that paint outside their nominal rectangle.
sub walk-for-overlay($w, Int $y, Int $x) {
    return Nil without $w;
    if $w.can('children') {
        for $w.children -> $child {
            my $hit = walk-for-overlay($child, $y, $x);
            return $hit if $hit.defined;
        }
    }
    if $w.can('content') {
        my $c = $w.content;
        if $c.defined {
            my $hit = walk-for-overlay($c, $y, $x);
            return $hit if $hit.defined;
        }
    }
    return $w if $w.claims-overlay-at($y, $x);
    Nil;
}

sub app-widget-at-in($root, Int $y, Int $x --> Selkie::Widget) is export {
    return Selkie::Widget without $root;
    my $overlay = walk-for-overlay($root, $y, $x);
    return $overlay if $overlay.defined;
    walk-for-point($root, $y, $x) // Selkie::Widget;
}