The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Tickit::Widget::Layout::Relative;
# ABSTRACT: align widgets relative to one another
use strict;
use warnings;
use parent qw(Tickit::ContainerWidget);

our $VERSION = '0.005';

=head1 NAME

Tickit::Widget::Layout::Relative - apply sizing to a group of L<Tickit> widgets

=head1 VERSION

version 0.005

=head1 SYNOPSIS

 my $l = Tickit::Widget::Layout::Relative->new;
 $l->add(
  title  => 'Little panel',
  id     => 'second',
  border => 'round dashed single',
  width  => '33%',
  height => '5em',
 );
 $l->add(
  title     => 'Another panel',
  id        => 'first',
  below     => 'second',
  top_align => 'second',
  border    => 'round dashed single',
  width     => '33%',
  height    => '10em',
 );
 $l->add(
  title        => 'Something on the right',
  id           => 'overview',
  right_of     => 'first',
  bottom_align => 'first',
  margin_top   => '1em',
  margin_right => '3em',
 );
 Tickit->new(root => $l)->run;

=head1 DESCRIPTION

A container widget which provides 'relative' layout for widgets:
specify the relations between the widget locations and this will
attempt to fit them to the available space.

=begin HTML

<p><img src="http://tickit.perlsite.co.uk/cpan-screenshot/tickit-widget-layout-relative1.png" alt="Relative layout" width="642" height="420"></p>

=end HTML

=cut

# This does all the hard work
use Tickit::Layout::Relative;

use List::UtilsBy qw(extract_by);
use Scalar::Util qw(weaken refaddr);

use Tickit::Style;
use Tickit::RenderBuffer qw(LINE_SINGLE LINE_DOUBLE);

use constant CLEAR_BEFORE_RENDER => 0;
use constant WIDGET_PEN_FROM_STYLE => 1;

BEGIN {
	style_definition base =>
		title_fg              => 'white',
		frame_fg              => 'white',
		frame_linestyle       => 'rounded',
		focus_title_fg        => 'white',
		focus_frame_fg        => 'green',
		focus_frame_linestyle => 'thick';
}

=head1 METHODS

=cut

=head2 new

Instantiate a new layout. Takes a single named parameter:

=over 4

=item * layout - the optional L<Tickit::Layout::Relative> layout to use
for initial positioning, will create a new one if none is supplied

=back

=cut

sub new {
	my $class = shift;
	my %args = @_;
	my $layout = delete($args{layout}) || Tickit::Layout::Relative->new;
	my $self = $class->SUPER::new(%args);
	$self->{layout} = $layout;
	$self
}

=head2 layout

Returns the L<Tickit::Layout::Relative> instance.

=cut

sub layout { shift->{layout} }

=head2 lines

Returns the number of lines, carefully calculated using science.

=cut

sub lines { 1 }

=head2 cols

Number of columns.

=cut

sub cols { 1 }

=head2 add

Adds the given widget. Also takes a plethora of named options to help
decide where to put said widget and how it should be rendered:

=over 4

=item * title - a label to apply to this pane, default is blank

=item * id - an ID used for looking up widgets in an existing layout,
see L</widget_by_id> and L</window_by_id> for more details

=item * left_of - attempt to position this to the left of the pane
with the given ID

=item * right_of - try to arrange this widget on the right of the given
ID

=item * above - if we can, stick this widget above the given pane ID

=item * below - we want to be below the given ID

=item * top_align - try to align the top edge with the given widgets
(string containing space-separated list, or arrayref, of IDs)

=item * bottom_align - align the bottom edge with the given panes
(as top_align)

=item * left_align - we would like this things to be aligned on the
left (as top_align)

=item * right_align - we would like this things to be aligned on the
right (as top_align)

=item * margin - margin to apply around this widget, this is a measurement
(see L</MEASUREMENTS>).

=item * margin_left - left margin

=item * margin_right - right margin

=item * margin_top - top margin

=item * margin_bottom - bottom margin

=item * padding - padding to apply around this widget, this is a measurement
(see L</MEASUREMENTS>).

=item * padding_left - left padding

=item * padding_right - right padding

=item * padding_top - top padding

=item * padding_bottom - bottom padding

=item * width - how big we'd like to be, see L</MEASUREMENTS>

=item * height - how big we'd like to be, see L</MEASUREMENTS>

=back

Don't rely on the return value. It may change in future.

Example:

 $layout->add(
  Tickit::Widget::Static->new(text => '...'),
  title  => 'Some panel',
  id     => 'send',
  border => 'single',
  width  => '85%',
  height => '15em',
 )

=cut

sub add {
	my $self = shift;
	my $w = shift;
	my %args = @_;
	$self->layout->add(widget => $w, %args);
	$self->SUPER::add($w);
}

=head2 render_to_rb

Renders the layout to the given L<Tickit::RenderBuffer>. Used internally.

=cut

sub render_to_rb {
	my $self = shift;
	my ($rb, $rect) = @_;
	my $win = $self->window or return;

	$rb->clear;
	my $linestyle = $self->get_style_values('frame_linestyle');
	my %corner;
	# Draw outlines for all contained widgets first
	my @ready = @{$self->{layout}{ready}};
	my ($focus) = extract_by { $_->{widget} && $self->{child_focus} && $_->{widget}->{window} && refaddr($_->{widget}->{window}) == $self->{child_focus} } @ready;
	push @ready, $focus if $focus;
	my $rectset = Tickit::RectSet->new;
	ITEM:
	foreach my $item (@ready) {
		next ITEM if ($item->{border} // '') eq 'none';

		# Tickit::Window
		my $child_focus = 0;
		if($item->{widget} && (my $widget_win = $item->{widget}->window)) {
			$child_focus = 1 if $widget_win->{focused} || $widget_win->{focused_child};
		}
		my $outline_pen = $self->get_style_pen($child_focus ? 'focus_frame' : 'frame');
		{
			# Avoid the 'pen clash' warnings
			local $SIG{__WARN__} = sub {};
			$rb->hline_at($item->{y}, $item->{x}, $item->{x} + $item->{w}, LINE_SINGLE, $outline_pen);
			$rb->hline_at($item->{y} + $item->{h}, $item->{x}, $item->{x} + $item->{w}, LINE_SINGLE, $outline_pen);
			$rb->vline_at($item->{y}, $item->{y} + $item->{h}, $item->{x}, LINE_SINGLE, $outline_pen);
			$rb->vline_at($item->{y}, $item->{y} + $item->{h}, $item->{x} + $item->{w}, LINE_SINGLE, $outline_pen);
		}

		$rectset->add(
			Tickit::Rect->new(
				top => $item->{y},
				left => $item->{x},
				lines => $item->{h} + 1,
				cols => $item->{w} + 1,
			)
		);

		# Rounded mode means we need to keep track of corner locations
		if($linestyle eq 'rounded') {
			$corner{join ',', $item->{y}, $item->{x}} = 1;
			$corner{join ',', $item->{y}, $item->{x} + $item->{w}} = 1;
			$corner{join ',', $item->{y} + $item->{h}, $item->{x}} = 1;
			$corner{join ',', $item->{y} + $item->{h}, $item->{x} + $item->{w}} = 1;
		}
	}

	# Overlay titles if we have them
	foreach my $item (@{$self->{layout}{ready}}) {
		next unless exists $item->{title};
		my $child_focus = 0;
		if($item->{widget} && (my $widget_win = $item->{widget}->window)) {
			$child_focus = 1 if $widget_win->{focused} || $widget_win->{focused_child};
		}
		my $title_pen = $self->get_style_pen($child_focus ? 'focus_title' : 'title');
		$rb->text_at($item->{y}, $item->{x} + 1, ' ' . $item->{title} . ' ', $title_pen);
	}
	$self->{frame_rectset} = $rectset;

	# In rounded mode, replace corners where possible
	$self->render_corners($rb, $rect, map [ split /,/, $_ ], keys %corner) if $linestyle eq 'rounded';
}

{
my %override = (
	southeast => 0x256D,
	northeast => 0x2570,
	southwest => 0x256E,
	northwest => 0x256f,
);

=head2 render_corners

Render the corners. Purely for aesthetic reasons (rounded corners look
better than the usual square corners formed by vline/hline). Used internally.

=cut

sub render_corners {
	my $self = shift;
	my $rb = shift;
	my $rect = shift;
	my @corners = @_;

	CORNER:
	foreach my $corner (@corners) {
		my ($y, $x) = @$corner;
		my $cell = $rb->get_cell($y, $x);
		next CORNER unless $cell and my $linemask = $cell->linemask;
		my $corners = join "", grep { $linemask->$_ == LINE_SINGLE } qw( north south east west );
		# Keep the same pen
		$rb->char_at($y, $x, $override{$corners}, $cell->pen) if exists $override{$corners};
	}
}
}

=head2 window_gained

When we get a window, we perform some unfortunate hacks to allow focus
notification. Most of this is highly likely to change in future.

=cut

sub window_gained {
	my $self = shift;
	my ($win) = @_;
	bless $win, 'Tickit::Widget::Layout::Relative::Window'; # aforementioned haxx
	$win->set_focus_callback(sub {
		weaken($self->{child_focus} = shift);
		if($self->{frame_rectset}) {
			$self->window->expose($_) for $self->{frame_rectset}->rects;
		} else {
			$self->redraw
		}
	});
	$self->SUPER::window_gained($win, @_);
	$win->{on_focus} = sub {
#		warn "given focus\n";
		return unless $self->window->is_visible;
		$self->redraw;
	};
}

=head2 reshape

Called when our main window changes shape. We recalculate layout to match
the new dimensions then update all child widgets accordingly.

=cut

sub reshape {
	my $self = shift;
	my $win = $self->window;
	$self->layout->{width} = $win->cols - 1;
	$self->layout->{height} = $win->lines - 1;
	$self->layout->render;
	foreach my $item (@{$self->{layout}{ready}}) {
#		warn "Checking " . $item->{id} . " from ready list\n";
		next unless my $widget = $item->{widget};

		my $border_size = ($item->{border} eq 'none') ? 0 : 1;
		my $rect = Tickit::Rect->new(
			left  => $item->{x} + $border_size,
			top   => $item->{y} + $border_size,
			lines => $item->{h} - $border_size,
			cols  => $item->{w} - $border_size
		);
#		warn "Item " . $item->{id} . " has $rect\n";

		# Reshape if we have one already
		if($widget->window) {
			$widget->window->change_geometry(
				$rect->top,
				$rect->left,
				$rect->lines,
				$rect->cols,
			) unless $widget->window->rect->equals($rect);
#			$widget->window->hide unless $win->is_visible;
		} else {
			my $sub = $win->make_sub(
				$rect->top,
				$rect->left,
				$rect->lines,
				$rect->cols
			);
#			$sub->hide unless $win->is_visible;
			$widget->set_window($sub);
		}
	}
	$self->SUPER::reshape(@_);
}

sub children {
	my $self = shift;
	my @children = grep defined, map $_->{widget}, @{$self->layout->{ready}};
	@children
}

# i really want this to go away
package
	Tickit::Widget::Layout::Relative::Window;
use parent qw(Tickit::Window);

sub set_focus_callback {
	my $self = shift;
	$self->{focus_callback} = shift;
}
sub _focus_gained {
	my $self = shift;
	my $child = shift;
	$self->{focus_callback}->($child) if $child && $self->{focus_callback};
	$self->SUPER::_focus_gained($child, @_)
}
sub expose {
	my $self = shift;
	my @children = @{ $self->{child_windows} || [] };
	my $visible = 1;
	{
		my $w = $self;
		while($w != $w->root) {
			unless($w->is_visible) {
				$visible = 0;
				last;
			}
			$w = $w->parent;
		}
	}
	if($visible) {
		$_->show for grep !$_->is_visible, @children;
	} else {
		$_->hide for grep $_->is_visible, @children;
	}
	$self->SUPER::expose(@_)
}

1;

__END__

=head1 AUTHOR

Tom Molesworth <cpan@entitymodel.com>

=head1 LICENSE

Copyright Tom Molesworth 2012-2014. Licensed under the same terms as Perl itself.