/* textview.cc
 * This file belongs to Worker, a file manager for UN*X/X11.
 * Copyright (C) 2005-2025 Ralf Hoffmann.
 * You can contact me at: ralf@boomerangsworld.de
 *   or http://www.boomerangsworld.de/worker
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "textview.h"
#include "acontainer.h"
#include "drawablecont.hh"
#include "utf8.hh"

unsigned int TextView::s_scroll_button = Button3;
unsigned int TextView::s_scroll_buttonmask = Button3Mask;
unsigned int TextView::s_scroll_mod = 0;

const static int s_pan_prepare_delta = 10;

TVCallBack::TVCallBack( TextView *_tv )
{
  tv = _tv;
}

TVCallBack::~TVCallBack()
{
}

void TVCallBack::run_int( Widget *elem, int value )
{
  if ( tv != NULL ) tv->gui_callback( elem, value );
}

TextView::TextView( AGUIX *parent,
                    int x,
                    int y,
                    int width,
                    int height,
                    std::string title,
                    std::shared_ptr< TextStorage > ts ) :
    AWindow( parent, x, y, width, height, title ),
    m_ts( ts ),
    tvcb( this ),
    _show_line_numbers( false ),
    m_buffer( 0 ),
    m_show_frame( true )
{
  hbar = vbar = NULL;
  cont = NULL;
  font = NULL;
  last_w = last_h = last_cont_width = -1;
  line_wrap = false;
  _display_focus = false;

  _highlight_line.real_line_number = -1;
  _highlight_line.y_offset = -1;

  m_selection.start_uline = m_selection.start_offset = -1;
  m_selection.end_uline = m_selection.end_offset = -1;

  setCanHandleFocus();
  setAcceptFocus( true );
  
  gettimeofday( &_lastwheel, NULL );

  initColors();

  if ( ! m_ts ) {
      m_ts = std::make_shared< TextStorageString >( "",
                                                    std::make_shared< ACharWidth >() );
  }
}

TextView::~TextView()
{
  destroy();
}

void TextView::doCreateStuff()
{
  AWindow::doCreateStuff();
  if ( _created == false ) return;

  if ( m_buffer == 0 ) {
      m_buffer = _aguix->createPixmap( win, _w, _h );
  }

  hbar = new Slider( _aguix, 0, 0, 30, 12, false, 0 );
  hbar->connect( &tvcb );
  hbar->setAcceptFocus( false );
  vbar = new Slider( _aguix, 0, 0, 12, 30, true, 1 );
  vbar->connect( &tvcb );
  vbar->setAcceptFocus( false );
  
  createContainer();
  updateBars();
  return;
}

void TextView::createContainer()
{
  int bw;

  if ( isCreated() == false ) return;
  
  if ( cont != NULL ) {
    setContainer( NULL );
    delete cont;
  }
  cont = new AContainer( this, 2, ( line_wrap == true ) ? 1 : 2 );
  cont->setMinSpace( 0 );
  cont->setMaxSpace( 0 );

  bw = ( _display_focus == true ) ? 2 : 1;
  cont->setBorderWidth( bw );
  setContainer( cont, true );
  
  if ( vbar != NULL ) {
    cont->add( vbar, 1, 0, AContainer::CO_INCH );
    cont->setMinHeight( 30, 1, 0 );
  }
  if ( hbar != NULL ) {
    if ( line_wrap == false ) {
      cont->add( hbar, 0, 1, AContainer::CO_INCW );
      cont->setMinWidth( 10, 0, 1 );
      hbar->show();
    } else {
      hbar->hide();
    }
  }
  cont->setMinWidth( 2, 0, 0 );
  cont->setMinHeight( 2, 0, 0 );
  
  cont->resize( _w, _h );
  cont->rearrange();
}

void TextView::boxRedraw()
{
  checkBuffer();

  prepareBG();
  //_aguix->ClearWin( win );

  _aguix->setFG( getBG() );
  _aguix->FillRectangle( m_buffer, 0, 0, _w, _h );

  if ( m_show_frame ) {
      _aguix->setFG( 0, _aguix->getFaceCol_3d_bright() );
      _aguix->DrawLine( m_buffer, 0, 0, _w - 1, 0 );
      _aguix->DrawLine( m_buffer, 0, 0, 0, _h - 1 );
      _aguix->setFG( 0, _aguix->getFaceCol_3d_dark() );
      _aguix->DrawLine( m_buffer, 0, _h - 1, _w - 1, _h - 1 );
      _aguix->DrawLine( m_buffer, _w - 1, _h - 1, _w - 1, 1 );
  }

  if ( getDisplayFocus() == true ) {
    if ( getHasFocus() == true ) {
      _aguix->setFG( 0, _aguix->getFaceCol_3d_bright() );
    } else {
      _aguix->setFG( 0, _aguix->getFaceCol_3d_dark() );
    }
    _aguix->DrawLine( m_buffer, 1, _h - 2, _w - 2, _h - 2 );
    _aguix->DrawLine( m_buffer, _w - 2, _h - 2, _w - 2, 2 );
    
    if ( getHasFocus() == true ) {
      _aguix->setFG( 0, _aguix->getFaceCol_3d_dark() );
    } else {
      _aguix->setFG( 0, _aguix->getFaceCol_3d_bright() );
    }
    _aguix->DrawLine( m_buffer, 1, 1, _w - 2, 1 );
    _aguix->DrawLine( m_buffer, 1, 1, 1, _h - 2 );
  }
}

bool TextView::handleMessage(XEvent *E,Message *msg)
{
  struct timeval t2;
  int dt, scrollspeed;
  
  if ( isCreated() == false ) return false;
  
  if ( msg->lockElement == this &&
       ( m_selection_scroll_active ||
         m_pan_info.m_pan_mode != m_pan_info.PAN_INACTIVE ) ) {
      msg->ack = true;
  }

  if ( msg->type == Expose &&
       msg->window == win ) {
      drawBuffer( msg->x, msg->y, msg->width, msg->height );
      return AWindow::handleMessage( E, msg );
  } else if ( msg->type == MapNotify &&
              msg->window == win ) {
      redraw();
      return AWindow::handleMessage( E, msg );
  }

  if ( m_pan_info.m_pan_mode != m_pan_info.PAN_INACTIVE ) {
      handleThird( msg );
      return AWindow::handleMessage( E, msg );
  }
  
  if ( msg->type == KeyPress ) {
    if ( ( getAcceptFocus() == true ) && ( getHasFocus() == true ) ) {
      if ( isVisible() == true ) {
	if ( isTopParent( msg->window ) == true ) {
            // we have the focus so let's react to some keys
            // lets call an extra handler so it can be overwritten
            handleKeyMessage( msg->key, msg->keystate );
	}
      }
    }
  } else if ( msg->type == MotionNotify &&
              msg->window == win ) {
      int mx, my;

      _aguix->queryPointer( win, &mx, &my );

      updateSelection( mx, my, false );
  } else if ( ( msg->type == ButtonPress ) && ( msg->window == win ) ) {
    if ( ( msg->button == Button4 ) || ( msg->button == Button5 ) ) {
      gettimeofday( &t2, NULL );
      dt = ldiffgtod_m( &t2, &_lastwheel );
      
      if ( dt < 200 ) scrollspeed = 5;
      else if ( dt < 400 ) scrollspeed = 2;
      else scrollspeed = 1;
      
      if ( msg->button == Button4 ) {
	if ( vbar != NULL ) {
	  int old_offset = vbar->getOffset();
	  vbar->setOffset( vbar->getOffset() - scrollspeed );
	  if ( vbar->getOffset() != old_offset )
	    redraw();
	}
      } else if ( msg->button == Button5 ) {
	if ( vbar != NULL ) {
	  int old_offset = vbar->getOffset();
	  vbar->setOffset( vbar->getOffset() + scrollspeed );
	  if ( vbar->getOffset() != old_offset )
	    redraw();
	}
      }

      checkForEndReached();
      
      _lastwheel = t2;
    } else if ( msg->button == Button1 ) {
      applyFocus( this );

      clearSelection();

      redraw();

      m_selection_scroll_active = true;
      _aguix->Flush();
      _aguix->msgLock( this );
      _aguix->enableTimer();

      int mx, my;
      _aguix->queryPointer( win, &mx, &my );

      if ( m_click_mode == AGUIX::NO_CLICK ) {
          m_last_click_time = msg->time;
          m_click_mode = AGUIX::NORMAL_CLICK;

          updateSelection( mx, my, true );
      } else if ( m_click_mode == AGUIX::NORMAL_CLICK ) {
          if ( _aguix->isDoubleClick( msg->time, m_last_click_time ) &&
               isNearFirstClick( mx, my ) ) {
              m_last_click_time = msg->time;
              m_click_mode = AGUIX::DOUBLE_CLICK;

              updateSelection( mx, my, false );
          } else {
              m_last_click_time = msg->time;
              m_click_mode = AGUIX::NORMAL_CLICK;

              updateSelection( mx, my, true );
          }
      } else if ( m_click_mode == AGUIX::DOUBLE_CLICK ) {
          if ( _aguix->isDoubleClick( msg->time, m_last_click_time ) &&
               isNearFirstClick( mx, my ) ) {
              m_last_click_time = msg->time;
              m_click_mode = AGUIX::TRIPLE_CLICK;

              updateSelection( mx, my, false );
          } else {
              m_last_click_time = msg->time;
              m_click_mode = AGUIX::NORMAL_CLICK;

              updateSelection( mx, my, true );
          }
      } else {
          m_last_click_time = msg->time;
          m_click_mode = AGUIX::NORMAL_CLICK;

          updateSelection( mx, my, true );
      }
    } else if ( msg->button == s_scroll_button ) {
        handleThird( msg );
    }
  } else if ( msg->type == ButtonRelease && msg->window == win ) {
      if ( msg->button == Button1 ) {
          copySelectionToClipboard();

          if ( m_selection_scroll_active ) {
              _aguix->Flush();
              _aguix->disableTimer();
              _aguix->msgUnlock( this );
              m_selection_scroll_active = false;
          }
      }
  } else if ( msg->type == ClientMessage ) {
      if ( msg->specialType == Message::TIMEREVENT &&
           m_selection_scroll_active ) {
          int mx, my;
          const int hscroll_delta = getHScrollStep();
          int textarea_x, textarea_y, textarea_w, textarea_h;

          getTextArea( textarea_x,
                       textarea_y,
                       textarea_w,
                       textarea_h );

          _aguix->queryPointer( win, &mx, &my );

          int xdelta = 0, ydelta = 0;

          if ( mx < textarea_x ) {
              xdelta = -hscroll_delta;
          } else if ( mx > textarea_x + textarea_w ) {
              xdelta = hscroll_delta;
          }
          if ( my < textarea_y ) {
              ydelta = -1;
          } else if ( my > textarea_y + textarea_h ) {
              ydelta = 1;
          }

          if ( scrollView( xdelta, ydelta ) != 0 ) {
              updateSelection( mx, my, false );
          }
      }
  }

  return AWindow::handleMessage( E, msg );
}

void TextView::handleThird(Message *msg)
{
    int mx, my;

    if ( isCreated() == false ) return;

    if ( msg->type == ButtonPress ) {
        if ( msg->window != win ) return;
        if ( m_pan_info.m_pan_mode != m_pan_info.PAN_INACTIVE ) return;

        mx = msg->mousex;
        my = msg->mousey;

        m_pan_info.m_pan_mode = m_pan_info.PAN_PREPARE;
        m_pan_info.m_pan_base_mx = mx;
        m_pan_info.m_pan_base_my = my;
        _aguix->setCursor( win, AGUIX::SCROLLFOUR_CURSOR );
        _aguix->Flush();
        _aguix->msgLock( this );
    } else if ( msg->type == MotionNotify ) {
        if ( m_pan_info.m_pan_mode != m_pan_info.PAN_INACTIVE ) {
            _aguix->queryPointer( win, &mx, &my );

            if ( m_pan_info.m_pan_mode == m_pan_info.PAN_PREPARE ) {
                if ( abs( m_pan_info.m_pan_base_mx - mx ) > s_pan_prepare_delta ||
                     abs( m_pan_info.m_pan_base_my - my ) > s_pan_prepare_delta ) {
                    int iw = cont->getWidth( 0, 0 ) - 2;
                    if ( iw < 0 ) iw = 0;
                    int ih = cont->getHeight( 0, 0 ) - 2;
                    if ( ih < 0 ) ih = 0;

                    if ( abs( m_pan_info.m_pan_base_mx - mx ) > abs( m_pan_info.m_pan_base_my - my ) ) {
                        m_pan_info.m_pan_mode = m_pan_info.PAN_HORIZONTAL;

                        _aguix->setCursor( win, AGUIX::SCROLLH_CURSOR );

                        // mouse position areas
                        m_pan_info.init_mouse_positions( mx, iw );

                        // scroll positions
                        m_pan_info.init_scroll_positions( hbar->getOffset(),
                                                          hbar->getMaxLen() - hbar->getMaxDisplay(),
                                                          hbar->getMaxDisplay() );
                    } else {
                        m_pan_info.m_pan_mode = m_pan_info.PAN_VERTICAL;

                        _aguix->setCursor( win, AGUIX::SCROLLV_CURSOR );

                        // mouse position areas
                        m_pan_info.init_mouse_positions( my, ih );

                        // scroll positions
                        m_pan_info.init_scroll_positions( vbar->getOffset(),
                                                          vbar->getMaxLen() - vbar->getMaxDisplay(),
                                                          vbar->getMaxDisplay() );
                    }
                }
            }

            if ( m_pan_info.m_pan_mode == m_pan_info.PAN_VERTICAL ) {
                vbar->setOffset( m_pan_info.get_scroll_offset( m_pan_info.m_pan_mode, mx, my ) );
            } else if ( m_pan_info.m_pan_mode == m_pan_info.PAN_HORIZONTAL ) {
                hbar->setOffset( m_pan_info.get_scroll_offset( m_pan_info.m_pan_mode, mx, my ) );
            }
            redraw();
            checkForEndReached();
        }
    } else if ( msg->type == ButtonRelease ) {
        if ( msg->window == win &&
             msg->button == s_scroll_button &&
             m_pan_info.m_pan_mode != m_pan_info.PAN_INACTIVE ) {
            m_pan_info.m_pan_mode = m_pan_info.PAN_INACTIVE;
            _aguix->unsetCursor( win );
            _aguix->msgUnlock( this );
        }
    }
}

void TextView::doDestroyStuff()
{
  if ( m_buffer != 0 ) {
      _aguix->freePixmap( m_buffer );
      m_buffer = 0;
  }

  AWindow::doDestroyStuff();
  delete cont;
  cont = NULL;
  setContainer( NULL );
  delete hbar;
  delete vbar;
  hbar = vbar = NULL;
}

void TextView::gui_callback( Widget *elem, int value )
{
  if ( ( elem == vbar ) || ( elem == hbar ) ) {
    redraw();
    checkForEndReached();
  }
}

static bool linePairGE( const std::pair< int, int > &p1,
                        const std::pair< int, int > &p2 )
{
    if ( p1.first > p2.first ||
         ( p1.first == p2.first && p1.second >= p2.second ) ) {
        return true;
    }
    return false;
}

static bool linePairGT( const std::pair< int, int > &p1,
                        const std::pair< int, int > &p2 )
{
    if ( p1.first > p2.first ||
         ( p1.first == p2.first && p1.second > p2.second ) ) {
        return true;
    }
    return false;
}

static bool linePairLT( const std::pair< int, int > &p1,
                        const std::pair< int, int > &p2 )
{
    return !linePairGE( p1, p2 );
}

static bool linePairLE( const std::pair< int, int > &p1,
                        const std::pair< int, int > &p2 )
{
    return !linePairGT( p1, p2 );
}

static bool linePairEQ( const std::pair< int, int > &p1,
                        const std::pair< int, int > &p2 )
{
    if ( p1.first == p2.first && p1.second == p2.second ) return true;
    return false;
}

void TextView::redraw()
{
  if ( isCreated() == false ) return;
  if ( isVisible() == false ) return;

  forceRedraw();
}

void TextView::forceRedraw()
{
  int tx, ty, lines, width, basetx;
  int bw;
  std::pair<int,int> real_line( -1, -1 );

  checkBuffer();
  
  boxRedraw();

  bw = ( _display_focus == true ) ? 3 : 2;

  tx = ty = bw;

  basetx = tx;
  tx += getTextStartOffset();
  getLinesWidth( lines, width );

  if ( ( _w != last_w ) || ( _h != last_h ) || ( width != last_cont_width ) ) {
    real_line = m_ts->getRealLinePair( vbar->getOffset() );

    if ( line_wrap == true ) {
      m_ts->setLineLimit( width );
    } else {
      m_ts->setLineLimit( -1 );
    }
    updateBars();
    last_w = _w;
    last_h = _h;
    last_cont_width = width;

    if ( _highlight_line.real_line_number >= 0 ) {
        _highlight_line.y_offset = m_ts->findLineNr( std::pair<int,int>( _highlight_line.real_line_number, 0 ) );
    }
  } else {
      // just update the horizontal slider
      updateHBarForVisibleLines();
  }

  if ( real_line.first >= 0 ) {
    int linenr = m_ts->findLineNr( real_line );

    if ( linenr == 0 && ( real_line.first != 0 || real_line.second != 0 ) ) {
      // wasn't able to found line, probably the subline doesn't exists anymore because of
      // rewrapping so try subline 0
      real_line.second = 0;
      linenr = m_ts->findLineNr( real_line );
    }
    vbar->setOffset( linenr );
  }

  drawLines( tx, ty, width, lines );

  drawLineNumbers( basetx, tx, ty, lines );

  drawBuffer();
}

void TextView::drawLines( int start_x, int start_y, int width, int lines )
{
    int elementHeight;

    if ( m_buffer == 0 ) return;

    if ( font == NULL ) elementHeight = _aguix->getCharHeight();
    else elementHeight = font->getCharHeight();

    std::pair< std::pair< int, int >, int > actual_sel_start = m_ts->getLineForOffset( m_selection.start_uline,
                                                                                    m_selection.start_offset );
    std::pair< std::pair< int, int >, int > actual_sel_end = m_ts->getLineForOffset( m_selection.end_uline,
                                                                                  m_selection.end_offset );

    if ( linePairGT( actual_sel_start.first, actual_sel_end.first ) == true ||
         ( linePairEQ( actual_sel_start.first, actual_sel_end.first ) == true &&
           actual_sel_start.second > actual_sel_end.second ) ) {
        return;
    }
    
    DrawableCont dc( _aguix, m_buffer );
    _aguix->setClip( font, &dc, start_x, 0, width, getHeight() );
    
    for ( int i = 0; i < lines; i++ ) {
        std::string s1;
        // width is a good upper limit for the line length
        // if I start at the first visible character
        // but since I currently always ask for the line
        // starting at offset 0, width is not enough
        // possible values are -1 or width+hbar->getOffset()
        // (was width)
        m_ts->getLine( vbar->getOffset() + i, 0, -1, s1 );
        
        int fg = _colors.getTextColor();
        if ( _highlight_line.real_line_number >= 0 &&
             _highlight_line.y_offset == ( vbar->getOffset() + i ) ) {
            if ( ! ( _highlight_line.y_offset == 0 && _highlight_line.real_line_number != 0 ) ) {
                _aguix->setFG( _colors.getHighlightedBG() );
                _aguix->FillRectangle( dc.getDrawable(), start_x, i * elementHeight + start_y, width, elementHeight );
                
                fg = _colors.getHighlightedFG();
            }
        }
        
        // I use a maxlen limit so getStrlen4Width is fast for very long lines if hbar->getOffset() is small
        // the limit is not exactly right in utf8 mode but getStrlen4Width takes care of it
        // and even if it wouldn't it should matter much
        
        // ask how much bytes fit into the invisible area (due to horizontal scrolling)
        int hidden_length, hidden_width;
        hidden_length = m_ts->getAWidth().getStrlen4WidthMaxlen( s1.c_str(), hbar->getOffset() / 2, hbar->getOffset(), &hidden_width );
        
        int overscan_width = width + ( hbar->getOffset() - hidden_width );
        int visible_length = m_ts->getAWidth().getStrlen4WidthMaxlen( s1.c_str() + hidden_length, overscan_width / 2, overscan_width, NULL );
        
        std::pair<int,int> cur_line = m_ts->getRealLinePair( vbar->getOffset() + i );

        // line can be outside ( cur_line < actual_sel_start or cur_line > actual_sel_end )
        // line can be inside ( cur_line > actual_sel_start and cur_line < actual_sel_end )
        // line can be partially inside at start or end or both
        if ( m_selection.start_uline >= 0 &&
             linePairGE( cur_line, actual_sel_start.first ) == true &&
             linePairLE( cur_line, actual_sel_end.first ) == true ) {
            
            if ( linePairGT( cur_line, actual_sel_start.first ) == true &&
                 linePairLT( cur_line, actual_sel_end.first ) == true ) {
                // inside selection
                std::string s2( s1, hidden_length, visible_length );
                
                _aguix->setFG( _colors.getSelectionBG() );
                _aguix->FillRectangle( dc.getDrawable(), start_x, i * elementHeight + start_y, width, elementHeight );
                
                _aguix->DrawText( dc, font, s2.c_str(),
                                  start_x - hbar->getOffset() + hidden_width, i * elementHeight + start_y,
                                  _colors.getSelectionFG() );
            } else {
                // partially visible selection
                
                // the unselected part
                if ( linePairEQ( cur_line, actual_sel_start.first ) == true &&
                     hidden_length < actual_sel_start.second ) {
                    
                    if ( actual_sel_start.second <= (int)s1.length() &&
                         UTF8::isValidCharacter( s1.c_str() + actual_sel_start.second ) == true ) {
                        std::string s2( s1, hidden_length, a_min( visible_length, actual_sel_start.second - hidden_length ) );
                        
                        _aguix->DrawText( dc, font, s2.c_str(),
                                          start_x - hbar->getOffset() + hidden_width, i * elementHeight + start_y, fg );
                        
                        hidden_length += s2.length();
                        hidden_width += m_ts->getAWidth().getWidth( s2.c_str() );
                        visible_length -= s2.length();
                        overscan_width -= m_ts->getAWidth().getWidth( s2.c_str() );
                    } else {
                        // invalid selection start offset
                        // just draw the whole visible string
                        std::string s2( s1, hidden_length, visible_length );
                        
                        _aguix->DrawText( dc, font, s2.c_str(),
                                          start_x - hbar->getOffset() + hidden_width, i * elementHeight + start_y, fg );
                        
                        hidden_length += s2.length();
                        hidden_width += m_ts->getAWidth().getWidth( s2.c_str() );
                        visible_length -= s2.length();
                        overscan_width -= m_ts->getAWidth().getWidth( s2.c_str() );
                    }
                }
                
                // the selected part
                if ( visible_length > 0 ) {
                    int selection_len = visible_length;
                    
                    if ( linePairEQ( cur_line, actual_sel_end.first ) == true &&
                         actual_sel_end.second <= (int)s1.length() ) {
                        
                        int my_end = actual_sel_end.second;
                        while ( my_end > 0 &&
                                UTF8::isValidCharacter( s1.c_str() + my_end ) == false ) {
                            my_end--;
                        }
                        selection_len = my_end - hidden_length;
                    }
                    
                    if ( selection_len > 0 ) {
                        std::string s2( s1, hidden_length, selection_len );
                        int selection_width = m_ts->getAWidth().getWidth( s2.c_str() );
                        int max_line_width;
                        int rectangle_x = start_x - hbar->getOffset() + hidden_width;

                        int rectangle_hidden = 0; // pixels left of start_x that are clipped for the font, but must be
                                                  // removed from drawn rectangle
                        if ( rectangle_x < start_x ) {
                            rectangle_hidden = start_x - rectangle_x;
                        }

                        if ( linePairEQ( cur_line, actual_sel_end.first ) == true ) {
                            max_line_width = a_min( selection_width, overscan_width );
                        } else {
                            max_line_width = overscan_width;
                        }
                        
                        _aguix->setFG( _colors.getSelectionBG() );
                        _aguix->FillRectangle( dc.getDrawable(),
                                               rectangle_x + rectangle_hidden, i * elementHeight + start_y,
                                               max_line_width - rectangle_hidden,
                                               elementHeight );
                        
                        _aguix->DrawText( dc, font, s2.c_str(),
                                          start_x - hbar->getOffset() + hidden_width, i * elementHeight + start_y,
                                          _colors.getSelectionFG() );
                        
                        hidden_length += s2.length();
                        hidden_width += selection_width;
                        visible_length -= s2.length();
                        overscan_width -= selection_width;
                    }
                }
                
                // unselected part after the selection
                if ( visible_length > 0 ) {
                    std::string s2( s1, hidden_length, visible_length );
                    
                    _aguix->DrawText( dc, font, s2.c_str(),
                                      start_x - hbar->getOffset() + hidden_width, i * elementHeight + start_y, fg );
                }
            }
        } else {
            // no selection or outside selection
            std::string s2( s1, hidden_length, visible_length );
            
            _aguix->DrawText( dc, font, s2.c_str(),
                              start_x - hbar->getOffset() + hidden_width, i * elementHeight + start_y, fg );
        }
    }
    _aguix->unclip( font, &dc );
}

void TextView::drawLineNumbers( int start_x, int end_x, int start_y, int lines )
{
    if ( m_buffer == 0 ) return;

    if ( _show_line_numbers == true ) {
        char buf[A_BYTESFORNUMBER(int)];
        int w, elementHeight;
        std::pair<int,int> real_line( -1, -1 );

        DrawableCont dc( _aguix, m_buffer );
        int fg = _colors.getTextColor();
        
        if ( font == NULL ) elementHeight = _aguix->getCharHeight();
        else elementHeight = font->getCharHeight();

        _aguix->setClip( font, &dc, start_x, 0, end_x, getHeight() );
        
        for ( int i = 0; i < lines; i++ ) {
            real_line = m_ts->getRealLinePair( vbar->getOffset() + i );
            if ( real_line.second == 0 ) {
                sprintf( buf, "%d", real_line.first + 1 );
                
                w = m_ts->getAWidth().getWidth( buf );
                
                _aguix->DrawText( dc, font, buf, end_x - w - 5, i * elementHeight + start_y, fg );
            }
        }
        _aguix->unclip( font, &dc );
    }
}

int TextView::setFont( const char *fontname )
{
  font = _aguix->getFont( fontname );
  last_w = last_h = last_cont_width = -1;
  if ( font == NULL ) return -1;
  return 0;
}

void TextView::updateBars()
{
  int lines, width;
  
  if ( isCreated() == false ) return;

  getLinesWidth( lines, width );
  
  vbar->setMaxDisplay( lines );
  hbar->setMaxDisplay( width );
  vbar->setMaxLen( m_ts->getNrOfLines() );
  updateHBarForVisibleLines();
}

void TextView::setLineWrap( bool nv )
{
  line_wrap = nv;
  last_w = last_h = last_cont_width = -1; // force textstorage rebuild
  createContainer();
  redraw();
}

bool TextView::getLineWrap() const
{
  return line_wrap;
}

void TextView::getLinesWidth( int &lines, int &width ) const
{
  int tw ,th, elementHeight;

  if ( cont != NULL ) {
    tw = cont->getWidth( 0, 0 ) - 2;
    if ( tw < 0 ) tw = 0;
    th = cont->getHeight( 0, 0 ) - 2;
    if ( th < 0 ) th = 0;
  } else {
    tw = th = 0;
  }

  if ( font == NULL ) elementHeight = _aguix->getCharHeight();
  else elementHeight = font->getCharHeight();

  lines = th / elementHeight;

  tw -= getTextStartOffset();
  if ( tw < 0 ) tw = 0;
  
  width = tw;
}

void TextView::setDisplayFocus( bool nv )
{
  _display_focus = nv;
  redraw();
}

bool TextView::getDisplayFocus() const
{
  return _display_focus;
}

void TextView::maximizeX( int max_width )
{
  int l;
  int bw;
  int rw;

  if ( line_wrap == true ) return;
  
  bw = ( _display_focus == true ) ? 2 : 1;

  l = m_ts->getMaxLineWidth() + 5;
  l += 2 * bw + 2 + ( ( vbar != NULL ) ? vbar->getWidth() : 0 );
  l += getTextStartOffset();

  _aguix->getLargestDimensionOfCurrentScreen( NULL, NULL, &rw, NULL );

  if ( l >= rw )
    l = rw;

  if ( max_width > 0 && l > max_width ) {
      l = max_width;
  }

  resize( l, getHeight() );
}

void TextView::maximizeYLines( int max_lines, int width )
{
  int l, elementHeight;
  int bw;
  int rh;

  if ( font == NULL ) elementHeight = _aguix->getCharHeight();
  else elementHeight = font->getCharHeight();
  
  bw = ( _display_focus == true ) ? 2 : 1;

  if ( width > 0 ) {
      if ( line_wrap == true ) {
          m_ts->setLineLimit( width );
      } else {
          m_ts->setLineLimit( -1 );
      }
  }

  l = m_ts->getNrOfLines();
  if ( l > max_lines ) l = max_lines;
  l = ( l + 1 ) * elementHeight;
  l += 2 * bw + 2 + ( ( ( hbar != NULL ) && ( line_wrap == false ) ) ? hbar->getHeight() : 0 );

  _aguix->getLargestDimensionOfCurrentScreen( NULL, NULL, NULL, &rh );

  if ( l >= rh )
    l = rh;

  resize( getWidth(), l );
}

void TextView::prepareBG( bool force )
{
  if ( isCreated() == false ) return;

  _aguix->SetWindowBG( win, getBG() );
}

int TextView::getHScrollStep() const
{
  //TODO some better value
  return 8;
}

int TextView::getTextStartOffset() const
{
  int off = 0;

  if ( _show_line_numbers == false )
    return off;

  int lines = m_ts->getNrOfLines();
  int digits = 1;
  if ( lines > 0 ) {
    digits = (int)( log( (double)lines ) / log( 10.0 ) ) ;
    digits++;
  }
  std::string maxnumber( digits, '9' );
  off = m_ts->getAWidth().getWidth( maxnumber.c_str() );
  off += 5;
  return off;
}

void TextView::setShowLineNumbers( bool nv )
{
  _show_line_numbers = nv;
  last_w = last_h = last_cont_width = -1; // force textstorage rebuild
  createContainer();
  redraw();
}

bool TextView::getShowLineNumbers() const
{
  return _show_line_numbers;
}

bool TextView::jumpToLine( int line_nr, bool highlight_line )
{
  bool res = true;
  int l;
  l = m_ts->findLineNr( std::pair<int,int>( line_nr, 0 ) );

  if ( l == 0 && line_nr > 0 ) {
      // probably not within textstorage
      res = false;
  }

  if ( highlight_line == true ) {
    _highlight_line.real_line_number = line_nr;
    _highlight_line.y_offset = l;
  }
  
  vbar->setOffset( l );
  redraw();

  checkForEndReached();

  return res;
}

TextView::ColorDef::ColorDef()
{
    _textcolor = 1;
    _background = 0;
    _highlighted_fg = 1;
    _highlighted_bg = 2;
    _selection_fg = 2;
    _selection_bg = 1;
}

TextView::ColorDef::~ColorDef()
{
}

void TextView::ColorDef::setTextColor( int col )
{
    if ( col >= 0 )
        _textcolor = col;
}

int TextView::ColorDef::getTextColor() const
{
    return _textcolor;
}

void TextView::ColorDef::setBackground( int col )
{
    if ( col >= 0 )
        _background = col;
}

int TextView::ColorDef::getBackground() const
{
    return _background;
}

void TextView::ColorDef::setHighlightedFG( int col )
{
    if ( col >= 0 )
        _highlighted_fg = col;
}

int TextView::ColorDef::getHighlightedFG() const
{
    return _highlighted_fg;
}

void TextView::ColorDef::setHighlightedBG( int col )
{
    if ( col >= 0 )
        _highlighted_bg = col;
}

int TextView::ColorDef::getHighlightedBG() const
{
    return _highlighted_bg;
}

void TextView::ColorDef::setSelectionFG( int col )
{
    if ( col >= 0 )
        _selection_fg = col;
}

int TextView::ColorDef::getSelectionFG() const
{
    return _selection_fg;
}

void TextView::ColorDef::setSelectionBG( int col )
{
    if ( col >= 0 )
        _selection_bg = col;
}

int TextView::ColorDef::getSelectionBG() const
{
    return _selection_bg;
}

void TextView::setColors( const ColorDef &col )
{
    _colors = col;
    setBG( _colors.getBackground() );
}

TextView::ColorDef TextView::getColors() const
{
    return _colors;
}

void TextView::initColors()
{
    _colors.setTextColor( _aguix->getFaces().getColor( "textview-fg" ) );
    _colors.setBackground( _aguix->getFaces().getColor( "textview-bg" ) );
    _colors.setHighlightedFG( _aguix->getFaces().getColor( "textview-highlight-fg" ) );
    _colors.setHighlightedBG( _aguix->getFaces().getColor( "textview-highlight-bg" ) );
    _colors.setSelectionFG( _aguix->getFaces().getColor( "textview-selection-fg" ) );
    _colors.setSelectionBG( _aguix->getFaces().getColor( "textview-selection-bg" ) );
    setBG( _colors.getBackground() );
}

void TextView::textStorageChanged()
{
    updateBars();
    redraw();
}

void TextView::replaceTextStorage( std::shared_ptr< TextStorage > ts )
{
    m_ts = ts;
    if ( ! m_ts ) {
        m_ts = std::make_shared< TextStorageString >( "",
                                                      std::make_shared< ACharWidth >() );
    }
    textStorageChanged();
}

void TextView::updateHBarForVisibleLines()
{
    int lines, width, maxlen = 0;
    getLinesWidth( lines, width );

    for ( int i = 0; i < lines; i++ ) {
        int l = m_ts->getLineWidth( i + vbar->getOffset() );
        if ( l > maxlen ) {
            maxlen = l;
        }
    }
    if ( hbar->getMaxLen() != maxlen ) {
        hbar->setMaxLen( maxlen );
    }
}

void TextView::setSelectionStart( int unwrapped_line_nr, int offset )
{
    if ( unwrapped_line_nr >= 0 && unwrapped_line_nr < m_ts->getNrOfUnwrappedLines() ) {
        m_selection.start_uline = unwrapped_line_nr;
        m_selection.start_offset = offset;
    } else {
        clearSelection();
    }
}

void TextView::getSelectionStart( int &unwrapped_line_nr, int &offset ) const
{
    unwrapped_line_nr = m_selection.start_uline;
    offset = m_selection.start_offset;
}

void TextView::setSelectionEnd( int unwrapped_line_nr, int offset )
{
    if ( unwrapped_line_nr >= 0 && unwrapped_line_nr < m_ts->getNrOfUnwrappedLines() ) {
        m_selection.end_uline = unwrapped_line_nr;
        m_selection.end_offset = offset;
    } else {
        clearSelection();
    }
}

void TextView::getSelectionEnd( int &unwrapped_line_nr, int &offset ) const
{
    unwrapped_line_nr = m_selection.end_uline;
    offset = m_selection.end_offset;
}

void TextView::clearSelection()
{
    m_selection.start_uline = m_selection.start_offset = -1;
    m_selection.end_uline = m_selection.end_offset = -1;
}

void TextView::selectAll()
{
    setSelectionStart( 0, 0 );

    std::string line_str;

    m_ts->getLine( m_ts->getNrOfLines() - 1, 0, line_str );

    setSelectionEnd( m_ts->getNrOfUnwrappedLines() - 1, (int)line_str.size() );
}

void TextView::sizeChanged( int width, int height, bool explicit_resize )
{
    AWindow::sizeChanged( width, height, explicit_resize );
    //TODO it would be better to send a X expose message to prevent
    //  additional redraws if X already send such a message
    redraw();
}

void TextView::makeSelectionVisible()
{
    std::pair< std::pair< int, int >, int > actual_sel_start = m_ts->getLineForOffset( m_selection.start_uline,
                                                                                    m_selection.start_offset );
    std::pair< std::pair< int, int >, int > actual_sel_end = m_ts->getLineForOffset( m_selection.end_uline,
                                                                                  m_selection.end_offset );

    if ( linePairGT( actual_sel_start.first, actual_sel_end.first ) == true ||
         ( linePairEQ( actual_sel_start.first, actual_sel_end.first ) == true &&
           actual_sel_start.second > actual_sel_end.second ) ) {
        redraw();
        return;
    }

    if ( linePairLT( actual_sel_start.first, std::pair<int, int>( 0, 0 ) ) == true ) {
        redraw();
        return;
    }

    int l;
    l = m_ts->findLineNr( std::pair<int,int>( actual_sel_start.first.first, actual_sel_start.first.second ) );
    
    if ( l >=0 && l < vbar->getMaxLen() ){
        if ( l < vbar->getOffset() ) {
            vbar->setOffset( l - 1 );
        } else if ( ( l - vbar->getOffset() ) >= vbar->getMaxDisplay() ) {
            vbar->setOffset( l - vbar->getMaxDisplay() + 2 );
        }

        if ( line_wrap == false ) {
            std::string str1;

            // need to update hbar to be able to set valid offsets
            updateHBarForVisibleLines();

            if ( m_ts->getLine( l, 0, actual_sel_start.second, str1 ) == 0 ) {
                int selstart_offset = m_ts->getAWidth().getWidth( str1.c_str() );
                int selend_offset = -1;
                int new_offset = hbar->getOffset();
                
                if ( linePairEQ( actual_sel_start.first, actual_sel_end.first ) == true ) {
                    if ( m_ts->getLine( l, 0, actual_sel_end.second, str1 ) == 0 ) {
                        selend_offset = m_ts->getAWidth().getWidth( str1.c_str() );
                    }
                }

                if ( selend_offset >= 0 &&
                     ( selend_offset < new_offset || selend_offset > ( new_offset + hbar->getMaxDisplay() - 1 ) ) ) {
                    new_offset = selend_offset - hbar->getMaxDisplay();
                }

                if ( selstart_offset < new_offset || selstart_offset > ( new_offset + hbar->getMaxDisplay() - 1 ) ) {
                    new_offset = selstart_offset;
                }
                
                hbar->setOffset( new_offset );
            }
        }
    }

    redraw();
}

void TextView::drawBuffer()
{
  drawBuffer( 0, 0, _w, _h );
}

void TextView::drawBuffer( int dx, int dy, int dw, int dh )
{
    if ( isCreated() == true && m_buffer != 0 ) {
        if ( ( dx < 0 ) ||
             ( dy < 0 ) ||
             ( dx >= _w ) ||
             ( dy >= _h ) ||
             ( dw < 1 ) ||
             ( dh < 1 ) ||
             ( dw > ( _w - dx ) ) ||
             ( dh > ( _h - dy ) ) ) return;
        
        _aguix->copyArea( m_buffer, win, dx, dy, dw, dh, dx, dy );
    }
}

void TextView::checkBuffer()
{
    if ( _w != last_w || _h != last_h || m_buffer == 0 ) {
        if ( m_buffer != 0 ) {
            _aguix->freePixmap( m_buffer );
        }
        m_buffer = _aguix->createPixmap( win, _w, _h );
    }
}

int TextView::getYOffset()
{
    if ( vbar != NULL ) {
        return vbar->getOffset();
    }
    return -1;
}

bool TextView::handleKeyMessage( KeySym key,
                                 unsigned int keystate )
{
    switch ( key ) {
        case XK_Up:
            if ( vbar != NULL ) {
		setVBarOffset( vbar->getOffset() - 1 );
		redraw();
            }
            break;
        case XK_Left:
            if ( hbar != NULL ) {
		hbar->setOffset( hbar->getOffset() - getHScrollStep() );
		redraw();
            }
            break;
        case XK_Down:
            if ( vbar != NULL ) {
                setVBarOffset( vbar->getOffset() + 1 );
		redraw();
            }
            break;
        case XK_Right:
            if ( hbar != NULL ) {
		hbar->setOffset( hbar->getOffset() + getHScrollStep() );
		redraw();
            }
            break;
        case XK_Home:
            if ( vbar != NULL ) {
		setVBarOffset( 0 );
		redraw();
            }
            break;
        case XK_End:
            if ( vbar != NULL ) {
		setVBarOffset( vbar->getMaxLen() );
		redraw();
            }
            break;
        case XK_Prior:
            if ( vbar != NULL ) {
		setVBarOffset( vbar->getOffset() - vbar->getMaxDisplay() + 1 );
		redraw();
            }
            break;
        case XK_Next:
            if ( vbar != NULL ) {
		setVBarOffset( vbar->getOffset() + vbar->getMaxDisplay() - 1 );
		redraw();
            }
            break;
        case XK_w:
            if ( KEYSTATEMASK( keystate ) == 0 ) {
                setLineWrap( ( getLineWrap() == true ) ? false : true );
            }
            break;
        case XK_l:
            if ( KEYSTATEMASK( keystate ) == 0 ) {
                setShowLineNumbers( ( getShowLineNumbers() == true ) ? false : true );
            }
            break;
              
            // another pair of handy shortcuts
        case XK_BackSpace:
            if ( vbar != NULL ) {
		setVBarOffset( vbar->getOffset() - vbar->getMaxDisplay() + 1 );
		redraw();
            }
            break;
        case XK_space:
            if ( vbar != NULL ) {
		setVBarOffset( vbar->getOffset() + vbar->getMaxDisplay() - 1 );
		redraw();
            }
            break;
        case XK_a:
            selectAll();
            copySelectionToClipboard();
            redraw();
            break;
        case XK_c:
            if ( KEYSTATEMASK( keystate ) == ControlMask ) {
                copySelectionToClipboard();
                _aguix->copyPrimaryToClipboard();
            }
            break;
        default:
            break;
    }
    return false;
}

void TextView::setVBarOffset( int offset )
{
    if ( vbar != NULL ) {
        vbar->setOffset( offset );

        checkForEndReached();
    }
}

void TextView::checkForEndReached()
{
    if ( vbar != NULL ) {
        if ( vbar->getOffset() + vbar->getMaxDisplay() >= vbar->getMaxLen() ) {
	    AGMessage *agmsg = AGUIX_allocAGMessage();
	    agmsg->type = AG_TEXTVIEW_END_REACHED;
	    agmsg->textview.textview = this;

            _aguix->putAGMsg( agmsg );
        }
    }
}

std::pair< int, int > TextView::getTextPosForMousePos( int x, int y )
{
    int elementHeight;

    if ( font == NULL ) elementHeight = _aguix->getCharHeight();
    else elementHeight = font->getCharHeight();

    int bw = ( _display_focus == true ) ? 3 : 2;

    int base_y = y - bw;

    int relative_row = base_y / elementHeight;

    if ( base_y < 0 ) relative_row--;

    int base_x = x - bw;
    base_x -= getTextStartOffset();
    base_x += hbar->getOffset();

    if ( base_x < 0 ) base_x = 0;

    int absolute_row;

    if ( relative_row >= 0 ) {
        absolute_row = relative_row + vbar->getOffset();
    } else {
        // stop at the first visible row
        absolute_row = vbar->getOffset();
        base_x = 0;
    }

    bool bottom_outside = false;

    if ( absolute_row >= m_ts->getNrOfLines() ) {
        absolute_row = m_ts->getNrOfLines() - 1;
        bottom_outside = true;
    } else if ( relative_row >= vbar->getMaxDisplay() ) {
        absolute_row = vbar->getOffset() + vbar->getMaxDisplay() - 1;
        bottom_outside = true;
    }
    
    std::pair<int,int> cur_line = m_ts->getRealLinePair( absolute_row );

    if ( cur_line.first < 0 ) return cur_line;

    int prev_sublines_chars = 0;

    if ( cur_line.second > 0 ) {
        for ( int sl = 0; sl < cur_line.second; sl++ ) {
            std::string s;

            m_ts->getLine( absolute_row - cur_line.second + sl, 0, s );
            prev_sublines_chars += s.size();
        }
    }

    std::string clicked_line;

    m_ts->getLine( absolute_row, 0, clicked_line );

    int offset_length;
    if ( ! bottom_outside ) {
        offset_length = m_ts->getAWidth().getStrlen4WidthMaxlen( clicked_line.c_str(), base_x / 2, base_x, NULL );
    } else {
        offset_length = (int)clicked_line.size();
    }

    return std::make_pair( cur_line.first, prev_sublines_chars + offset_length );
}

void TextView::copySelectionToClipboard()
{
    std::string s;

    int unwrapped_start_line, unwrapped_start_offset,
        unwrapped_end_line, unwrapped_end_offset;

    getSelectionStart( unwrapped_start_line, unwrapped_start_offset );
    getSelectionEnd( unwrapped_end_line, unwrapped_end_offset );

    if ( unwrapped_start_line >= 0 && unwrapped_end_line >= unwrapped_start_line ) {

        if ( unwrapped_start_line == unwrapped_end_line ) {
            m_ts->getUnwrappedLineRaw( unwrapped_start_line,
                                    unwrapped_start_offset,
                                    unwrapped_end_offset - unwrapped_start_offset, s );
        } else {
            m_ts->getUnwrappedLineRaw( unwrapped_start_line,
                                    unwrapped_start_offset, s );

            std::string temp_line_str;

            // all full lines between start and end
            for ( int l = unwrapped_start_line + 1; l < unwrapped_end_line; l++ ) {
                m_ts->getUnwrappedLineRaw( l, 0, temp_line_str );

                s += '\n';

                s += temp_line_str;
            }

            // and the rest up to the end offset
            m_ts->getUnwrappedLineRaw( unwrapped_end_line, 0, unwrapped_end_offset, temp_line_str );

            s += '\n';

            s += temp_line_str;
        }

        _aguix->copyToClipboard( s, AGUIX::PRIMARY_BUFFER );
    }
}

void TextView::showFrame( bool nv )
{
    m_show_frame = nv;
}

int TextView::scrollView( int xdelta, int ydelta )
{
    int res = 0;

    if ( xdelta != 0 && hbar ) {
        int old_offset = hbar->getOffset();
        hbar->setOffset( old_offset + xdelta );
        if ( hbar->getOffset() != old_offset ) {
            res |= 1;
        }
    }
    if ( ydelta != 0 && vbar ) {
        int old_offset = vbar->getOffset();
        vbar->setOffset( old_offset + ydelta );
        if ( vbar->getOffset() != old_offset ) {
            res |= 2;
        }
    }

    if ( res != 0 ) {
        redraw();
    }

    return res;
}

void TextView::updateSelection( int mx, int my, bool initial )
{
    int x, y, w, h;

    getTextArea( x, y, w, h );

    // allow hitting exactly w/h to allow selection of last char/line,
    // but limit to that area to avoid selection invisible text
    if ( mx > x + w ) mx = x + w;
    if ( mx < x ) mx = x;
    if ( my > y + h ) my = y + h;
    if ( my < y ) my = y;

    std::pair< int, int > cur = getTextPosForMousePos( mx, my );

    if ( initial ) {
        m_first_click_text_pos = cur;
        m_first_click_pos = { mx, my };
        return;
    }

    if ( m_click_mode == AGUIX::NO_CLICK ) return;

    auto first = getSelectionElement( m_first_click_text_pos );
    auto second = getSelectionElement( cur );

    if ( first.first.first < second.first.first ||
         ( first.first.first == second.first.first &&
           first.first.second < second.first.second ) ) {
        setSelectionStart( first.first.first,
                           first.first.second );
        setSelectionEnd( second.second.first,
                         second.second.second );
    } else {
        setSelectionStart( second.first.first,
                           second.first.second );
        setSelectionEnd( first.second.first,
                         first.second.second );
    }

    redraw();
}

void TextView::getTextArea( int &x, int &y,
                            int &w, int &h )
{
    int lines;
    int bw;

    bw = ( _display_focus == true ) ? 3 : 2;

    x = y = bw;

    x += getTextStartOffset();

    getLinesWidth( lines, w );

    if ( cont != NULL ) {
        h = cont->getHeight( 0, 0 ) - 2;
        if ( h < 0 ) h = 0;
    } else {
        h = 0;
    }
}

bool TextView::isAtEnd() const
{
    return vbar->getOffset() + vbar->getMaxDisplay() >= vbar->getMaxLen();
}

void TextView::scrollToEnd()
{
    if ( vbar != NULL ) {
		setVBarOffset( vbar->getMaxLen() );
		redraw();
    }
}

std::pair< std::pair< int, int >,
           std::pair< int, int > > TextView::getSelectionElement( const std::pair< int, int > &pos )
{
    if ( m_click_mode == AGUIX::NORMAL_CLICK ) {
        return { pos, pos };
    }

    if ( m_click_mode == AGUIX::TRIPLE_CLICK ) {
        if ( pos.first + 1 == m_ts->getNrOfUnwrappedLines() ) {
            std::string line_str;

            if ( m_ts->getUnwrappedLine( pos.first, 0, -1, line_str ) != 0 ) {
                return { pos, pos };
            } else {
                return { { pos.first, 0 }, { pos.first, line_str.size() } };
            }
        } else {
            return { { pos.first, 0 }, { pos.first + 1, 0 } };
        }
    }

    std::string line_str;

    if ( m_ts->getUnwrappedLine( pos.first, 0, -1, line_str ) != 0 ) {
        return { pos, pos };
    }

    int start_pos = pos.second;
    int end_pos = pos.second;
    int word_start = start_pos;
    int word_end = end_pos;

    if ( start_pos < 0 || start_pos >= (int)line_str.size() ) return { pos, pos };
    if ( end_pos < 0 || end_pos >= (int)line_str.size() ) return { pos, pos };
    
    for (;;) {
        if ( std::isspace( line_str[ start_pos ] ) ) {
            break;
        }

        word_start = start_pos;

        UTF8::movePosToPrevChar( line_str.c_str(), start_pos );

        if ( word_start == start_pos ) {
            break;
        }
    }

    for (;;) {
        word_end = end_pos;

        if ( end_pos >= (int)line_str.size() ) {
            break;
        }
        if ( std::isspace( line_str[ end_pos ] ) ) {
            break;
        }

        UTF8::movePosToNextChar( line_str.c_str(), end_pos );

        if ( word_end == end_pos ) {
            break;
        }
    }

    return { { pos.first, word_start }, { pos.first, word_end } };
}

bool TextView::isNearFirstClick( int mx, int my ) const
{
    if ( labs( mx - m_first_click_pos.x ) < 5 &&
         labs( my - m_first_click_pos.y ) < 5 ) {
        return true;
    }
    return false;
}

void TextView::unsetHighlightLine()
{
    _highlight_line.real_line_number = -1;
    _highlight_line.y_offset = -1;
}
