The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# Games::Construder - A 3D Game written in Perl with an infinite and modifiable world.
# Copyright (C) 2011  Robin Redeker
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
package Games::Construder::Client::UI;
use common::sense;
use SDL;
use SDL::Surface;
use SDL::Video;
use SDL::TTF;
use OpenGL qw(:all);
use JSON;
use Games::Construder::Vector;
use Games::Construder;
use File::ShareDir::PAR;
use Games::Construder::Logging;

use base qw/Object::Event/;

=head1 NAME

Games::Construder::Client::UI - Client GUI implementation

=over 4

=cut

our $RES; # set by Games::Construder::Client

my $BIG_FONT; # should be around 35 pixel
my $NORM_FONT; # should be around 20 pixel
my $SMALL_FONT; # should be around 12 pixel

sub init_ui {
   unless (SDL::Config->has('SDL_ttf')) {
      Carp::cluck("SDL_ttf support has not been compiled");
   }

   unless (SDL::TTF::was_init()) {
      SDL::TTF::init () == 0
         or Carp::cluck "SDL::TTF could not be initialized: "
            . SDL::get_error . "\n";
   }

   my $fnt =
      File::ShareDir::PAR::dist_file ('Games-Construder', 'font/FreeMonoBold.ttf');

   $BIG_FONT   = SDL::TTF::open_font ($fnt, 35)
      or die "Couldn't load font from $fnt: " . SDL::get_error . "\n";
   $NORM_FONT = SDL::TTF::open_font ($fnt, 20)
      or die "Couldn't load font from $fnt: " . SDL::get_error . "\n";
   $SMALL_FONT = SDL::TTF::open_font ($fnt, 12)
      or die "Couldn't load font from $fnt: " . SDL::get_error . "\n";
}

sub new {
   my $this  = shift;
   my $class = ref ($this) || $this;
   my $self  = { @_ };
   bless $self, $class;

   $self->init_object_events;

   $self->{opengl_texture_size} = 1024;

   return $self
}

sub pre_resize_screen {
   my ($self) = @_;
   glDeleteTextures_p (delete $self->{gl_id})
      if $self->{gl_id};
}

sub resize_screen {
   my ($self, $w, $h) = @_;
   $self->{W} = $w;
   $self->{H} = $h;
}

sub animation_step {
   my ($self) = @_;
   $self->{anim_state} = not $self->{anim_state};
   $self->{anim_step}++;
   if (@{$self->{active_elements}}) {
      $self->update;
   }
}

sub _fnt2font {
   my $fnt = shift;
   $fnt eq 'big' ? $BIG_FONT : $fnt eq 'small' ? $SMALL_FONT : $NORM_FONT;
}

sub _clr2color {
   my ($clr) = @_;
   if ($clr =~ /#(..)(..)(..)(..)?/) {
      #d#if ($4 ne '') {
      #d#   return (hex ($1), hex ($2), hex ($3), hex ($4))
      #d#} else {
         return (hex ($1), hex ($2), hex ($3))
      #d#}
   }
   return (0, 0, 0);
}

sub window_position {
   my ($self, $pos, $size) = @_;

   my ($sw, $sh) = ($self->{W}, $self->{H});
   my ($x, $y, $ro_x, $ro_y) = @$pos;

   if ($x eq 'right') {
      $x = $sw - $size->[0];
   } elsif ($x eq 'center') {
      $x = ($sw - $size->[0]) / 2;
   } else {
      $x = 0;
   }

   if ($y eq 'down') {
      $y = $sh - $size->[1];
   } elsif ($y eq 'center') {
      $y = ($sh - $size->[1]) / 2;
   } else {
      $y = 0;
   }

   $x += $ro_x * $sw;
   $y += $ro_y * $sh;

   [int ($x), int ($y)]
}

sub layout_text {
   my ($font, $text, $wrap, $txtalign, $line_range, $min_chars) = @_;
   my $layout = {
      font => $font
   };

   my @lines     = split /\n/, $text, -1;
   my $line_skip = SDL::TTF::font_line_skip ($font);

   my $txt_w;

   if ($wrap > 0) { # word wrapping
      my @olines;
      for (@lines) {
         my @words = split /\s+/, $_;
         my $line = "";
         my $force = 0;
         while (@words) {
            my $w = shift @words;
            my $new_line = $line . "$w ";

            if ($force || length ($new_line) <= $wrap) {
               $line = $new_line;
               $force = 0;

            } else {
               push @olines, $line;
               $line = "";
               unshift @words, $w;
               $force = 1;
            }
         }
         push @olines, $line;
      }
      (@olines) = map { s/\s*$//; $_ } @olines;

      my $max_w;
      for (@olines) {
         next if $_ eq '';
         my ($w) = @{ SDL::TTF::size_utf8 ($font, $_) };
         $max_w = $w if $max_w < $w;
      }
      $txt_w = $max_w;

      (@lines) = @olines;

   } elsif ($wrap < 0) { # character wrapping
      $wrap = -$wrap;
      my @olines;
      for my $line (@lines) {
         while (length ($line) > $wrap) {
            push @olines, substr $line, 0, $wrap, "";
         }
         push @olines, $line;
      }

      my $max_w;
      for (@olines) {
         next if $_ eq '';
         my ($w) = @{ SDL::TTF::size_utf8 ($font, $_) };
         $max_w = $w if $max_w < $w;
      }
      $txt_w = $max_w;

      (@lines) = @olines;

   } else {
      for my $l (@lines) {
         next if $l eq '';
         my ($w, $h) = @{ SDL::TTF::size_utf8 ($font, $l) };
         $txt_w = $w if $txt_w < $w;
      }
   }

   if ($line_range) {
      splice @lines, 0, $line_range->[0];
      splice @lines, ($line_range->[1] - $line_range->[0]) + 1;
   }

   my $txt_h;
   for my $l (@lines) {
      my ($w, $h) = @{ SDL::TTF::size_utf8 ($font, $l) };

      if ($txtalign eq 'center') {
         push @{$layout->{lines}}, [($txt_w - $w) / 2, $txt_h, $l];

      } elsif ($txtalign eq 'right') {
         push @{$layout->{lines}}, [$txt_w - $w, $txt_h, $l];

      } else {
         push @{$layout->{lines}}, [0, $txt_h, $l];
      }

      $txt_h += $line_skip;
   }

   if (defined $min_chars) {
      my ($w) = @{ SDL::TTF::size_utf8 ($font, "m" x $min_chars) };
      $txt_w = $w if $txt_w < $w;
      $txt_h = $line_skip if $txt_h < $line_skip;
   }

   $layout->{size} = [$txt_w, $txt_h];

   $layout
}

sub add_active {
   my ($self, $el) = @_;
   push @{$self->{active_elements}}, $el;
}

sub setup_sizes {
   my ($self, $el) = @_;
   my ($type, $attr, @childs) = @$el;

   if ($type eq 'box' || $type eq 'select_box') {
      if ($type eq 'select_box') {
         $self->add_active ($el);
      }

      my ($mw, $mh);

      for (@childs) {
         my $size = $self->setup_sizes ($_);
         if ($attr->{dir} eq 'vert') {
            $mw = $size->[0] if $mw < $size->[0];
            $mh += $size->[1];
         } else {
            $mw += $size->[0];
            $mh = $size->[1] if $mh < $size->[1];
         }
      }

      if ($attr->{aspect}) {
         my $max = $mh;
         $max = $mw if $max < $mw;
         ($mw, $mh) = ($max, $max);
      }

      $attr->{padding_y} = $attr->{padding} unless defined $attr->{padding_y};

      $attr->{size} = [$mw + $attr->{padding} * 2,
                       $mh + $attr->{padding_y} * 2];
      $attr->{inner_size} = [$mw, $mh];
      return $attr->{size};

   } elsif ($type eq 'text' || $type eq 'entry' || $type eq 'range' || $type eq 'multiline') {
      if ($type eq 'entry' || $type eq 'range' || $type eq 'multiline') {
         if ($type eq 'multiline') {
            $self->do_multiline ($el);
            ($type, $attr, @childs) = @$el;
         }
         $self->add_active ($el);
      }

      my ($fnt) = element_font ($el);
      my $fmt = $attr->{fmt} ne '' ? $attr->{fmt} : "%s";
      my $txt =
         $type eq 'range'
            ? "< " . sprintf ($fmt, $childs[0]) . " >" 
            : sprintf ($fmt, $childs[0]);
      my $lyout =
         layout_text ($fnt, $txt, $attr->{wrap},
                      $attr->{align}, $attr->{line_range},
                      $attr->{max_chars});

      $attr->{size}   = $lyout->{size};
      $attr->{layout} = $lyout;
      return $attr->{size}

   } elsif ($type eq 'model') {
      return $attr->{size} = [$attr->{width}, $attr->{width}];
   }
}

sub draw_text {
   my ($self, $pos, $layout, $color) = @_;

   my $font = $layout->{font};
   my $surf = $self->{sdl_surf};

   my $curp = [@$pos];
   for my $line (@{$layout->{lines}}) {
      my ($x, $y, $txt) = @$line;
      next if $txt eq '';

      my $tsurf = SDL::TTF::render_utf8_blended (
         $layout->{font}, $txt, SDL::Color->new (_clr2color ($color)));

      unless ($tsurf) {
         warn "SDL::TTF::render_utf8_blended could not render \"$txt\": "
              . SDL::get_error . "\n";
         next;
      }

      SDL::Video::blit_surface (
         $tsurf, SDL::Rect->new (0, 0, $tsurf->w, $tsurf->h),
         $surf,  SDL::Rect->new ($pos->[0] + $x, $pos->[1] + $y, $tsurf->w, $tsurf->h));
   }
}

sub draw_box {
   my ($self, $pos, $size, $bgcolor, $border) = @_;

   if ($bgcolor) {
      my $clr = SDL::Video::map_RGB (
         $self->{sdl_surf}->format, _clr2color ($bgcolor)
      );
      SDL::Video::fill_rect (
         $self->{sdl_surf},
         SDL::Rect->new (@$pos, @$size),
         $clr
      );
   }

   if ($border) {
      my $clr = SDL::Video::map_RGB (
         $self->{sdl_surf}->format, _clr2color ($border->{color}),
      );

      my $w = $border->{width} || 1;
      my ($x, $y, $bw, $bh) = (@$pos, @$size);

      SDL::Video::fill_rect (
         $self->{sdl_surf}, SDL::Rect->new ($x, $y, $w, $bh), $clr
      );
      SDL::Video::fill_rect (
         $self->{sdl_surf}, SDL::Rect->new ($x + ($bw - $w), $y, $w, $bh), $clr
      );
      SDL::Video::fill_rect (
         $self->{sdl_surf}, SDL::Rect->new ($x, $y, $bw, $w), $clr
      );
      SDL::Video::fill_rect (
         $self->{sdl_surf}, SDL::Rect->new ($x, $y + ($bh - $w), $bw, $w), $clr
      );
   }
}

sub draw_element {
   my ($self, $el, $offs) = @_;
   my ($type, $attr, @childs) = @$el;

   if ($type eq 'box' || $type eq 'select_box') {
      my ($bgcolor, $border) = ($attr->{bgcolor}, $attr->{border});
      if ($type eq 'select_box'
          && $self->{active_element} eq $el
      ) {
         $bgcolor = $attr->{select_bgcolor}
            if $attr->{select_bgcolor};
         $border = $attr->{select_border}
            if $attr->{select_border};
      }

      $self->draw_box ($offs, $attr->{size}, $bgcolor, $border);

      my $loffs = [$offs->[0] + $attr->{padding}, $offs->[1] + $attr->{padding_y}];
      my $isize = $attr->{inner_size};

      my $x = $loffs->[0];
      my $y = $loffs->[1];

      if ($attr->{dir} eq 'vert') {
         for (@childs) {
            my $size = $_->[1]->{size};
            my $pos  = [$x, $y];

            if ($_->[1]->{align} eq 'center') {
               $pos->[0] += ($isize->[0] - $size->[0]) / 2;
            } elsif ($_->[1]->{align} eq 'right') {
               $pos->[0] += $isize->[0] - $size->[0];
            }

            $self->draw_element ($_, $pos);
            $y += $size->[1];
         }

      } else {
         for (@childs) {
            my $size = $_->[1]->{size};
            my $pos  = [$x, $y];

            if ($_->[1]->{align} eq 'center') {
               $pos->[1] += ($isize->[1] - $size->[1]) / 2;

            } elsif ($_->[1]->{align} eq 'right') {
               $pos->[1] += $isize->[1] - $size->[1];
            }

            $self->draw_element ($_, $pos);
            $x += $size->[0];
         }
      }

   } elsif ($type eq 'text') {
      $self->draw_text ($offs, $attr->{layout}, $attr->{color});

   } elsif ($type eq 'entry') {
      if ($self->{active_element} eq $el) {
         $self->draw_box (
            $offs, $attr->{size},
            ($self->{anim_state} ? $attr->{highlight}->[0] : $attr->{highlight}->[1]));
      }
      $self->draw_text ($offs, $attr->{layout}, $attr->{color});

   } elsif ($type eq 'multiline') {
      if ($self->{active_element} eq $el) {
         if ($attr->{active_input}) {
            $self->draw_box ($offs, $attr->{size}, $attr->{highlight}->[2]);
         } else {
            $self->draw_box ($offs, $attr->{size}, $attr->{highlight}->[1]);
         }
      } else {
         $self->draw_box ($offs, $attr->{size}, $attr->{highlight}->[0]);
      }
      $self->draw_text ($offs, $attr->{layout}, $attr->{color});

   } elsif ($type eq 'range') {
      if ($self->{active_element} eq $el) {
         $self->draw_box (
            $offs, $attr->{size},
            ($self->{anim_state} ? $attr->{highlight}->[0] : $attr->{highlight}->[1]));
      }
      $self->draw_text ($offs, $attr->{layout}, $attr->{color});

   } elsif ($type eq 'model') {
      push @{$self->{models}}, [$offs, $attr->{size}, $childs[0], $attr->{animated}];
   }
}

sub element_font {
   my ($el) = @_;
   my $font = $el->[1]->{font};
   $font eq 'big' ? $BIG_FONT : $font eq 'small' ? $SMALL_FONT : $NORM_FONT;
}

sub fit_size_pot {
   my ($size) = @_;
   my $gls = 1;
   $gls++ while $size->[0] > (2**$gls) || $size->[1] > (2**$gls);
   2**$gls
}

sub update {
   my ($self, $gui_desc) = @_;

   if (defined $gui_desc) {
      delete $self->{key_repeat};
      $self->{desc} = $gui_desc;
   }

   my $win = $self->{desc}->{window};

   $self->{element_offset} = 0;
   $self->{relative_extents} = [];

   $self->{commands}   = $self->{desc}->{commands};
   $self->{command_cb} = $self->{desc}->{command_cb};
   $self->{sticky}     = $win->{sticky};
   $self->{models}     = [];

   $self->{entries}    = [];

   if ($gui_desc && $gui_desc->{layout}) {
      $self->{layout} = decode_json (encode_json ($gui_desc->{layout}));
   }

   my $layout = $self->{layout};

   $self->{active_elements} = [];

   my $size;
   $size = $self->setup_sizes ($layout);
   $self->{layout} = $layout;
   $self->{window_size} = $size;
   $self->{window_pos}  = $self->window_position ($win->{pos}, $size);
   $self->{opengl_texture_size} = fit_size_pot ($size);

   unless (grep {
             $self->{active_element} eq $_
           } @{$self->{active_elements}}
   ) {
      $self->{active_element} = $self->{active_elements}->[0];
   }

   $self->prepare_opengl_texture;

   # window_size_inside is initialized here, and window_padding too
   $self->prepare_sdl_surface ($win->{bgcolor}, $size); # creates a new sdl surface for this window

   ctr_prof ("draw elements", sub {
      $self->draw_element ($layout, [0, 0]);
   });

   ctr_prof ("render_view", sub {
      $self->render_view; # refresh rendering to opengl texture
   });
}

sub active {
   my ($self, $act) = @_;
   $self->{active} = $act;
   delete $self->{key_repeat};
}

sub switch_active {
   my ($self, $dir) = @_;
   return unless @{$self->{active_elements}};

   if ($dir < 0) {
      my $last = $self->{active_elements}->[-1];
      for (@{$self->{active_elements}}) {
         if ($_ eq $self->{active_element}) {
            $self->{active_element} = $last;
            last;
         }
         $last = $_;
      }
   } else {
      my $next = $self->{active_elements}->[0];
      for (reverse @{$self->{active_elements}}) {
         if ($_ eq $self->{active_element}) {
            $self->{active_element} = $next;
            last;
         }
         $next = $_;
      }
   }

   $self->update;
}

sub prepare_opengl_texture {
   my ($self) = @_;
   return if $self->{gl_id};

   my ($nr) = glGenTextures_p (1);
   $self->{gl_id} = $nr;
   $self->{gl_texture} = 0;
}

sub prepare_sdl_surface {
   my ($self, $clear_color, $winsize) = @_;

   $clear_color = "#000000" unless defined $clear_color;

   my $size = $self->{opengl_texture_size};
   delete $self->{sdl_surf}
      if $self->{surf_size} != $size;

   unless ($self->{sdl_surf}) {
      $self->{sdl_surf} = SDL::Surface->new (
         SDL_SWSURFACE, $size, $size, 24, 0, 0, 0);
      $self->{surf_size} = $size;
      $self->{gl_texture} = 0;
   }

   ctr_prof ("prepsurf($size)", sub {
      my $clr = SDL::Video::map_RGB (
         $self->{sdl_surf}->format, _clr2color ($clear_color),
      );
      SDL::Video::fill_rect (
         $self->{sdl_surf},
         SDL::Rect->new (0, 0, @$winsize),
         $clr
      );
   });

}

sub _get_texfmt {
   my ($surface) = @_;
   my $ncol = $surface->format->BytesPerPixel;
   my $rmsk = $surface->format->Rmask;
   #d# warn "SURF $ncol ; " . sprintf ("%02x", $rmsk) . "\n";
   ($ncol == 4 ? ($rmsk == 0x000000ff ? GL_RGBA : GL_BGRA)
               : ($rmsk == 0x000000ff ? GL_RGB  : GL_BGR))
}

our %MODEL_CACHE;

sub render_object_type_sample {
   my ($type, $skip) = @_;

   my ($txtid) = $RES->obj2texture (1);
   glBindTexture (GL_TEXTURE_2D, $txtid);

   if ($skip >= 0) {
      $skip++;
      my $geom = Games::Construder::Renderer::new_geom ();
      Games::Construder::Renderer::model ($type, 0, 1, 0, 0, 0, $geom, $skip, 1);
      Games::Construder::Renderer::draw_geom ($geom);
      Games::Construder::Renderer::free_geom ($geom);
      return;
   }

   if (my $g = $MODEL_CACHE{$type}) {
      Games::Construder::Renderer::draw_geom ($g);

   } else {
      my $geom = $MODEL_CACHE{$type} = Games::Construder::Renderer::new_geom ();
      Games::Construder::Renderer::model ($type, 0, 1, 0, 0, 0, $geom, -1, 0);
      Games::Construder::Renderer::draw_geom ($geom);
   }
}

sub render_view {
   my ($self) = @_;

   my $surf = $self->{sdl_surf};
   my $texture_format = _get_texfmt ($surf);

   glBindTexture (GL_TEXTURE_2D, $self->{gl_id});
   glTexParameterf (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
   glTexParameterf (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

   SDL::Video::lock_surface($surf);
   if ($self->{gl_texture}) {
      glTexSubImage2D_s (GL_TEXTURE_2D,
         0, 0, 0, $surf->w, $surf->h,
         $texture_format, GL_UNSIGNED_BYTE, ${$surf->get_pixels_ptr});

   } else {
      # without SubImage it seems to be faster in nytprof...
      glTexImage2D_s (GL_TEXTURE_2D,
         0, $surf->format->BytesPerPixel, $surf->w, $surf->h,
         0, $texture_format, GL_UNSIGNED_BYTE, ${$surf->get_pixels_ptr});
      $self->{gl_texture} = 1;
   }
   SDL::Video::unlock_surface($surf);

   $self->{rendered} = 1;
}

sub display {
   my ($self) = @_;

   return unless $self->{rendered};

   my ($pos, $size) = ($self->{window_pos}, $self->{window_size});
   my $wins = [@$size];
   my ($u, $v) = (
      $wins->[0] / $self->{opengl_texture_size},
      $wins->[1] / $self->{opengl_texture_size}
   );

   glPushMatrix;
   my $z = -8;
   $z-- if $self->{sticky};
   $z += 0.5 if $self->{desc}->{window}->{force_one_higher};
   glTranslatef (@$pos, $z);
   my $a = $self->{desc}->{window}->{alpha};
   $a = 1 unless defined $a;
   glColor4f (1, 1, 1, $a);
   glBindTexture (GL_TEXTURE_2D, $self->{gl_id});
   glBegin (GL_QUADS);

   glTexCoord2f(0, $v);
   glVertex3f (0, $size->[1], 0);

   glTexCoord2f($u, $v);
   glVertex3f ($size->[0], $size->[1], 0);

   glTexCoord2f($u, 0);
   glVertex3f ($size->[0], 0, 0);

   glTexCoord2f(0, 0);
   glVertex3f (0, 0, 0);

   glEnd ();

   for (@{$self->{models}}) {
      my ($pos, $size, $model, $anim) = @$_;

      glPushMatrix;
      my ($w, $h) = ($size->[0] * 0.65, $size->[1] * 0.65);
      glTranslatef ($pos->[0] + ($h * 0.05), $pos->[1] + ($h * 1.2), $self->{sticky} ? 0.5 : 1);
      glScalef ($w, $h, 0.01);
      glScalef (1, -1, 1);
      glRotatef (25, 1, 0, 0);
      glRotatef (45, 0, 1, 0);

      if ($anim) {
         my $model_blocks = $RES->type_model_blocks ($_->[2]);
         render_object_type_sample ($_->[2], $self->{anim_step} % $model_blocks);
      } else {
         render_object_type_sample ($_->[2], -1);
      }
      glPopMatrix;
   }

   glPopMatrix;
}

our @MLBUFFER;

sub do_multiline {
   my ($self, $el, $key, $name, $unicode) = @_;

   my $ml = ($el->[3]      ||= { text => $el->[2], l_offs => 0 });
   my $c  = ($ml->{cursor} ||= []);

   my (@lines) = split /\r?\n/, $ml->{text}, -1;

   my $hdl = 0;

   if ($name eq 'up') {
      $c->[0]--;
      $hdl = 1;
   } elsif ($name eq 'down') {
      $c->[0]++;
      $hdl = 1;
   } elsif ($name eq 'left') {
      $c->[1]--;
      $hdl = 1;
   } elsif ($name eq 'right') {
      $c->[1]++;
      $hdl = 1;
   } elsif ($name eq 'home') {
      $c->[1] = 0;
      $hdl = 1;
   } elsif ($name eq 'end') {
      $c->[1] = 99999;
      $hdl = 1;
   } elsif ($name eq 'f2') {
      (@MLBUFFER) = ($lines[$c->[0]]);
      $hdl = 1;

   } elsif ($name eq 'f3') {
      push @MLBUFFER, $lines[$c->[0]];
      $c->[0]++;
      $hdl = 1;

   } elsif ($name eq 'f4') {
      push @MLBUFFER, splice @lines, $c->[0], 1;
      $hdl = 1;

   } elsif ($name eq 'f5') {
      splice @lines, $c->[0], 0, @MLBUFFER;
      $hdl = 1;

   } elsif ($name eq 'f6') {
      (@MLBUFFER) = ();
      $hdl = 1;

   } elsif ($name eq 'backspace') {
      if ($c->[1] > 0) {
         substr $lines[$c->[0]], $c->[1] - 1, 1, '';
         $c->[1]--;

      } elsif ($c->[0] > 0) {
         my $pl = length $lines[$c->[0] - 1];
         $lines[$c->[0] - 1] .= splice @lines, $c->[0], 1, ();
         $c->[0]--;
         $c->[1] = $pl;
      }
      $hdl = 1;

   } elsif ($name eq 'delete') {
      if ($c->[1] == length ($lines[$c->[0]])) {
         $lines[$c->[0]] .= splice @lines, $c->[0] + 1, 1, ();
      } else {
         substr $lines[$c->[0]], $c->[1], 1, '';
      }
      $hdl = 1;

   } elsif ($name eq 'return') {
      my $rest = substr $lines[$c->[0]], $c->[1];
      $lines[$c->[0]] = substr $lines[$c->[0]], 0, $c->[1];
      splice @lines, $c->[0] + 1, 0, $rest;
      $c->[0]++;
      $c->[1] = 0;
      $hdl = 1;

   } elsif ($unicode =~ /(\p{IsWord}|\p{IsSpace}|\p{IsPunct}|[[:punct:]])/) {
      substr $lines[$c->[0]], $c->[1], 0, $unicode;
      $c->[1]++;
      $hdl = 1;

   } else {
      $hdl = 1;
   }

   $c->[0] = 0 if $c->[0] < 0;
   if (@lines) {
      $c->[0] = (@lines - 1) if $c->[0] >= @lines;
   } else {
      $c->[0] = 0;
   }

   $c->[1] = 0 if $c->[1] < 0;
   $c->[1] = length ($lines[$c->[0]]) if $c->[1] > length ($lines[$c->[0]]);

   $ml->{text} = join "\n", @lines;
   substr $lines[$c->[0]], $c->[1], 0, "|";
   $el->[2] = join "\n", @lines;

   $hdl
}

sub input_key_press : event_cb {
   my ($self, $key, $name, $unicode, $rhandled) = @_;
   ctr_log (debug => "UI(%s) keypress %s/%s/%d", $self->{name}, $key, $name, ord $unicode);
   my $cmd;

   my $el = $self->{active_element};
   if ($el && $el->[1]->{active_input} && $name eq 'escape') {
      $el->[1]->{active_input} = 0;
      $$rhandled = 1;
      $self->update;
      $cmd = "save_text";

   } elsif ($name eq 'escape') {
      $cmd = "cancel" unless $self->{sticky};

   } elsif (defined $self->{active_element}) {
      my $el = $self->{active_element};

      if ($el->[0] eq 'multiline' && $el->[1]->{active_input} && $self->do_multiline ($el, $key, $name, $unicode)) {
         $$rhandled = 1;
         $self->update;
         return;

      } elsif ($el->[0] eq 'multiline' && not ($el->[1]->{active_input}) && $name eq 'return') {
         $el->[1]->{active_input} = 1;
         $$rhandled = 1;
         $self->update;
         return;

      } elsif ($el->[0] eq 'entry' && ($name eq 'backspace' || $name eq 'delete')) {
         chop $el->[2];
         $self->update;
         $$rhandled = 1;
         return;

      } elsif ($el->[0] eq 'range' && ($name eq 'left' || $name eq 'right')) {
         $el->[2] += ($name eq 'left' ? -1 : 1) * $el->[1]->{step};
         if ($el->[2] < $el->[1]->{range}->[0]) {
            $el->[2] = $el->[1]->{range}->[0];
         }
         if ($el->[2] > $el->[1]->{range}->[1]) {
            $el->[2] = $el->[1]->{range}->[1];
         }
         $self->update;
         $$rhandled = 1;
         return;

      } elsif ($name eq 'down' || $name eq 'tab' || $name eq 'right') {
         $self->switch_active (1);
         $$rhandled = 1;
         return;

      } elsif ($name eq 'up' || $name eq 'left') {
         $self->switch_active (-1);
         $$rhandled = 1;
         return;

      } elsif ($self->{commands} && $self->{commands}->{default_keys}->{$name}) {
         $cmd = $self->{commands}->{default_keys}->{$name}

      } elsif ($el->[0] eq 'entry'
               && $unicode =~ /(\p{IsWord}|\p{IsSpace}|\p{IsPunct}|[[:punct:]])/
      ) {
         warn "UNICODE ADD:'".ord ($unicode)."'\n";
         if (
            not ($el->[1]->{max_chars} && length ($el->[2]) >= $el->[1]->{max_chars})
            && ($el->[1]->{allowed_chars} ne ''
                   ? $unicode =~ /^([$el->[1]->{allowed_chars}]+)$/
                   : 1)
         ) {
            $el->[2] .= $unicode;
         }
         $self->update;
         $$rhandled = 1;
         return;
      }
   } elsif ($self->{commands} && $self->{commands}->{default_keys}->{$name}) {
      $cmd = $self->{commands}->{default_keys}->{$name}
   }

   if ($cmd ne '') {
      my $arg;
      if (@{$self->{active_elements}}) {
         $arg = {
            map {
               my @a;
               if ($_->[0] eq 'entry') {
                  (@a) = ($_->[1]->{arg} => $_->[2]);
               } elsif ($_->[0] eq 'multiline') {
                  (@a) = ($_->[1]->{arg} => $_->[3]->{text});
               } elsif ($_->[0] eq 'range') {
                  (@a) = ($_->[1]->{arg} => $_->[2]);
               }
               @a
            } @{$self->{active_elements}}
         };

         if ($self->{active_element}->[0] eq 'select_box') {
            $arg->{$self->{active_element}->[1]->{arg}} =
               $self->{active_element}->[1]->{tag};
         }
      }

      $self->{command_cb}->($cmd, $arg, $self->{commands}->{need_selected_boxes})
         if $self->{command_cb};
      $$rhandled = $cmd eq 'cancel' ? 2 : 1;
   }
}

sub DESTROY {
   my ($self) = @_;
   glDeleteTextures_p (delete $self->{gl_id}) if $self->{gl_id};
}

=back

=head1 AUTHOR

Robin Redeker, C<< <elmex@ta-sa.org> >>

=head1 COPYRIGHT & LICENSE

Copyright 2011 Robin Redeker, all rights reserved.

This program is free software; you can redistribute it and/or modify it
under the terms of the GNU Affero General Public License.

=cut

1;