Selkie.git | t/ | 62-tree-helpers.rakutest


use Test;
use lib 'lib';

use nqp;
use Selkie::Widget;
use Selkie::Tree;

# Mock widget composing Selkie::Widget so we get the real abs-y/x +
# rows/cols accessors and mark-dirty / is-dirty plumbing, without
# requiring a live notcurses plane. We push values into the slots
# directly so we can position widgets at arbitrary cell coordinates
# for the tree-walk tests.
class Plotted does Selkie::Widget {
    has Bool $.was-dirtied is rw = False;
    submethod TWEAK(:$abs-y = 0, :$abs-x = 0, :$rows = 0, :$cols = 0,
                    Selkie::Widget :$parent) {
        nqp::bindattr(self, Plotted, '$!abs-y', $abs-y.Int);
        nqp::bindattr(self, Plotted, '$!abs-x', $abs-x.Int);
        nqp::bindattr(self, Plotted, '$!rows',  $rows.UInt);
        nqp::bindattr(self, Plotted, '$!cols',  $cols.UInt);
        self.parent = $parent if $parent.defined;
    }
    method render() { self.clear-dirty }
}

# Container exposing .children for the walk to reach.
class PlottedContainer does Selkie::Widget {
    has @.children;
    submethod TWEAK(:$abs-y = 0, :$abs-x = 0, :$rows = 0, :$cols = 0,
                    :@children) {
        nqp::bindattr(self, PlottedContainer, '$!abs-y', $abs-y.Int);
        nqp::bindattr(self, PlottedContainer, '$!abs-x', $abs-x.Int);
        nqp::bindattr(self, PlottedContainer, '$!rows',  $rows.UInt);
        nqp::bindattr(self, PlottedContainer, '$!cols',  $cols.UInt);
        @!children = @children;
        $_.parent = self for @children;
    }
    method render() { self.clear-dirty }
}

plan 6;

subtest 'mark-widgets-in-rect-dirty marks intersecting widgets' => {
    plan 3;
    # Two siblings: one inside the rect, one outside.
    my $a = Plotted.new(:abs-y(5),  :abs-x(5),  :rows(4), :cols(8));
    my $b = Plotted.new(:abs-y(50), :abs-x(50), :rows(4), :cols(8));
    my $root = PlottedContainer.new(
        :abs-y(0), :abs-x(0), :rows(100), :cols(100),
        children => [$a, $b]
    );
    set-tree-roots-provider(-> { ($root,) });

    $a.clear-dirty; $b.clear-dirty; $root.clear-dirty;
    mark-widgets-in-rect-dirty(:abs-y(4), :abs-x(4), :rows(6), :cols(10));

    ok  $a.is-dirty,    'intersecting widget marked dirty';
    nok $b.is-dirty,    'distant widget not marked';
    ok  $root.is-dirty, 'enclosing root marked dirty (it intersects too)';
};

subtest 'rectangle entirely outside any widget marks nothing' => {
    plan 1;
    my $a = Plotted.new(:abs-y(5), :abs-x(5), :rows(4), :cols(8));
    my $root = PlottedContainer.new(
        :abs-y(0), :abs-x(0), :rows(20), :cols(20),
        children => [$a]
    );
    set-tree-roots-provider(-> { ($root,) });
    $a.clear-dirty; $root.clear-dirty;
    mark-widgets-in-rect-dirty(:abs-y(100), :abs-x(100), :rows(4), :cols(4));
    nok $a.is-dirty, 'no widget marked when rect is far away';
};

subtest 'edge-touching rectangles do NOT count as intersecting' => {
    plan 1;
    # Widget at (5, 5) size 4x8 — right edge at x=13, bottom at y=9.
    # Rectangle starting at x=13 should NOT intersect (exclusive edges).
    my $a = Plotted.new(:abs-y(5), :abs-x(5), :rows(4), :cols(8));
    my $root = PlottedContainer.new(
        :abs-y(0), :abs-x(0), :rows(100), :cols(100),
        children => [$a]
    );
    set-tree-roots-provider(-> { ($root,) });
    $a.clear-dirty; $root.clear-dirty;
    mark-widgets-in-rect-dirty(:abs-y(5), :abs-x(13), :rows(4), :cols(8));
    nok $a.is-dirty, 'widget abutting on the right is not "in" the rect';
};

subtest 'multiple roots all walked' => {
    plan 2;
    my $a = Plotted.new(:abs-y(5), :abs-x(5), :rows(4), :cols(8));
    my $b = Plotted.new(:abs-y(5), :abs-x(5), :rows(4), :cols(8));
    set-tree-roots-provider(-> { ($a, $b) });
    $a.clear-dirty; $b.clear-dirty;
    mark-widgets-in-rect-dirty(:abs-y(4), :abs-x(4), :rows(8), :cols(12));
    ok $a.is-dirty, 'first root marked';
    ok $b.is-dirty, 'second root marked';
};

subtest 'modal provider round-trips a widget' => {
    plan 3;
    my $modal = Plotted.new(:abs-y(0), :abs-x(0), :rows(20), :cols(40));
    is  current-active-modal(), Nil, 'no modal initially';
    set-modal-provider(-> { $modal });
    ok  current-active-modal() === $modal, 'modal returned';
    set-modal-provider(-> { Nil });
    is  current-active-modal(), Nil, 'cleared';
};

subtest 'active modal occlusion follows widget ancestry' => {
    plan 4;
    my $modal = PlottedContainer.new(:abs-y(0), :abs-x(0), :rows(20), :cols(40));
    my $inside = Plotted.new(:parent($modal));
    my $outside = Plotted.new;

    set-modal-provider(-> { Nil });
    nok widget-occluded-by-active-modal($outside),
        'without active modal, no widget is occluded';

    set-modal-provider(-> { $modal });
    nok widget-occluded-by-active-modal($modal),
        'the active modal itself is not occluded';
    nok widget-occluded-by-active-modal($inside),
        'descendants of the active modal are not occluded';
    ok widget-occluded-by-active-modal($outside),
        'widgets outside the active modal tree are occluded';
};

# Reset providers so any test files that follow start fresh
set-tree-roots-provider(-> { () });
set-modal-provider(-> { Nil });

done-testing;