The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl
use strict;
use warnings;
use Glib qw(TRUE FALSE);
use Gtk2 '-init';
use Cairo;

my $window = Gtk2::Window->new;
$window->set_title("Rotation example");
$window->signal_connect( delete_event => sub { exit } );
$window->set_border_width(5);

my $rotated= Gtk2::Ex::RotatedBin->new;

my $entry=Gtk2::Entry->new;
my $button=Gtk2::Button->new_from_stock('gtk-ok');
$button->signal_connect(clicked=>sub {warn "clicked\n";});

my $checkh=Gtk2::CheckButton->new('mirror horizontally');
$checkh->signal_connect(toggled=>sub { $rotated->set_mirror( horizontal=> $_[0]->get_active ) });
my $checkv=Gtk2::CheckButton->new('mirror vertically');
$checkv->signal_connect(toggled=>sub { $rotated->set_mirror( vertical=> $_[0]->get_active ) });

my $adj=Gtk2::Adjustment->new(10, 0, 360, 1,10,0);
$adj->signal_connect(value_changed=> sub { $rotated->set_angle( $_[0]->value ); });
my $scale=Gtk2::HScale->new($adj);

my $vbox=Gtk2::VBox->new;
$vbox->add($_) for $entry,$button,$checkh,$checkv,$scale;

$rotated->add($vbox);
$window->add($rotated);
$window->show_all;

#Gtk2::Gdk::Window->set_debug_updates(1);
Gtk2->main;


#FIXME the child needs to be added before the RotatedBin is realized
# I haven't managed to make it work if the child is added or replaced after
package Gtk2::Ex::RotatedBin;
use Gtk2;
use List::Util qw/min max/;
use Glib::Object::Subclass
	Gtk2::EventBox::,
	signals =>
	{	size_allocate	=> \&do_size_allocate,
		size_request	=> \&do_size_request,
	};
use constant PI => 4 * atan2(1,1); # needed for the rotation

sub INIT_INSTANCE {
	my $self=shift;
	$self->{angle}=10;
	$self->signal_connect( expose_event	=> \&expose_cb);
	$self->signal_connect( damage_event	=> \&damage_cb);
	$self->signal_connect( realize		=> \&realize_cb);
	return $self;
}

sub do_size_request {
	my ($self,$req)=@_;
	my $border= $self->get_border_width;
	my $child_req=$self->child->size_request;
	my $w= $child_req->width;
	my $h= $child_req->height;
	my $diag= sqrt( $w**2 + $h**2 );
	$diag= 1+int $diag  unless int($diag)==$diag; #round up
	# request enough to satisfy the child request for any angle
	$req->width( $diag+$border*2 );
	$req->height( $diag+$border*2 );
}

sub do_size_allocate {
	my ($self,$alloc)=@_;
	my $border= $self->get_border_width;
	my ($x,$y,$w,$h)=$alloc->values;
	my $olda=$self->allocation;
	 $olda->x($x); $olda->width($w);
	 $olda->y($y); $olda->height($h);
	$w-= 2*$border;
	$h-= 2*$border;
	$self->window->move_resize($x+$border,$y+$border,$w,$h) if $self->window;
	$self->update_matrix;
}

sub set_angle {
	my ($self,$angle)=@_;
	$self->{angle}=$angle;
	$self->update_matrix;
	$self->queue_resize;
}

sub set_mirror {
	my ($self,$h_or_v,$on)=@_;
	my $key= $h_or_v eq 'vertical' ? 'vmirror' : 'hmirror';
	$self->{$key}=$on;
	$self->update_matrix;
	$self->queue_draw;
}

# transform the rectangle and find a rectangle containing the transformed rectangle
sub transform_expose_rectangle {
	my ($self,$rect,$inv)=@_;
	my ($x,$y,$w,$h)=$rect->values;
	my $matrix= $inv ? $self->{imatrix} : $self->{matrix};
	my ($xa,$ya)=$matrix->transform_point($x,   $y);
	my ($xb,$yb)=$matrix->transform_point($x+$w,$y);
	my ($xc,$yc)=$matrix->transform_point($x,   $y+$h);
	my ($xd,$yd)=$matrix->transform_point($x+$w,$y+$h);
	$x= min($xa,$xb,$xc,$xd);
	$y= min($ya,$yb,$yc,$yd);
	$w= max($xa,$xb,$xc,$xd) -$x;
	$h= max($ya,$yb,$yc,$yd) -$y;
	return Gtk2::Gdk::Rectangle->new($x,$y,$w,$h);
}

sub update_matrix {
	my $self=shift;
	my ($x,$y,$w,$h)= $self->allocation->values;
	my $border= $self->get_border_width;
	$x+=$border; $w-=2*$border;
	$y+=$border; $h-=2*$border;
	my $angle= $self->{angle}*PI/180;
	my $matrix0=Cairo::Matrix->init_rotate($angle);
	my ($xa,$ya)=$matrix0->transform_distance(0,0);
	my ($xb,$yb)=$matrix0->transform_distance($w,0);
	my ($xc,$yc)=$matrix0->transform_distance(0,$h);
	my ($xd,$yd)=$matrix0->transform_distance($w,$h);
	my $rw= $w / ( max($xa,$xb,$xc,$xd) - min($xa,$xb,$xc,$xd) );
	my $rh= $h / ( max($ya,$yb,$yc,$yd) - min($ya,$yb,$yc,$yd) );
	my $r= min($rw,$rh);
	my $cw= $w*$r;
	my $ch= $h*$r;

	my $matrix=Cairo::Matrix->init_identity;
	$matrix->translate($w/2,$h/2);
	$matrix->rotate($angle);
	$matrix->translate( -$cw/2,-$ch/2 );
	if ($self->{hmirror}) {
		$matrix->scale(-1,1);
		$matrix->translate( -$cw,0 );
	}
	if ($self->{vmirror}) {
		$matrix->scale(1,-1);
		$matrix->translate( 0,-$ch );
	}
	$self->{matrix}=$matrix;

	my $imatrix= $matrix->multiply( Cairo::Matrix->init_identity ); #copy matrix
	$imatrix->invert;
	$self->{imatrix}= $imatrix;

	if (my $o=$self->{offscreen})
	{	$o->{matrix}= $self->{matrix};
		$o->{imatrix}=$self->{imatrix};
		$o->move_resize(0,0,$cw,$ch);
		$o->geometry_changed;
	}
	if (my $child=$self->child)
	{	my $rect=Gtk2::Gdk::Rectangle->new(0,0, $cw,$ch);
		$self->child->size_allocate($rect);
	}
}

sub damage_cb {
	my ($self,$event)=@_;
	my $rect= transform_expose_rectangle($self,$event->area);
	$self->window->invalidate_rect( $rect, 0);
	1;
}

sub realize_cb {
	my ($self)=@_;
	my ($x,$y,$w,$h)=$self->allocation->values;
	my %attr=
	 (	window_type	=> 'offscreen',
		wclass		=> 'output',
		event_mask	=> [qw/pointer-motion-mask button-press-mask button-release-mask exposure_mask/],
	 );
	$self->{offscreen}= my $offscreen= Gtk2::Gdk::Window->new($self->get_root_window,\%attr);
	$self->update_matrix;	# will resize $offscreen to the correct size
	$offscreen->set_user_data($self->Glib::Object::get_pointer());
	$self->window->signal_connect( pick_embedded_child =>sub { return $offscreen; }); #could check if transformed position is inside offscreen window
	$self->child->set_parent_window($offscreen) if $self->child;
	$offscreen->set_embedder($self->window);
	$offscreen->signal_connect( to_embedder  => sub { return $_[0]{ matrix}->transform_point($_[1],$_[2]); });
	$offscreen->signal_connect( from_embedder=> sub { return $_[0]{imatrix}->transform_point($_[1],$_[2]); });
	$self->style->set_background($offscreen,'normal');
	$offscreen->show;
}

sub expose_cb {
	my ($self,$event)=@_;
	my $offscreen= $self->{offscreen};
	if ($event->window == $self->window) {
		my $pixmap = $offscreen->get_pixmap;
		return 1 unless $pixmap;
		my $cr=Gtk2::Gdk::Cairo::Context->create($self->window);
		$cr->rectangle($event->area);
		$cr->clip;
		$cr->set_matrix( $self->{matrix} );
		$cr->set_source_pixmap($pixmap,0,0);
		$cr->paint;
	}
	elsif ($event->window == $offscreen) {
		$self->propagate_expose($self->child,$event) if $self->child;
	}
	1;
}