Fix Terminal character position and add word selection (#906)

* Improve horizontal interactive selection
* Using half-character positions to implement selection
  similar to Fl_Input.
* Add word and line selection
* Fix vertical position of text
This commit is contained in:
Matthias Melcher 2024-02-18 13:29:37 +01:00 committed by GitHub
parent eb4916344b
commit 2f343ad64d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 136 additions and 41 deletions

View File

@ -669,30 +669,34 @@ private:
// Class to manage mouse selection
//
class FL_EXPORT Selection {
Fl_Terminal *terminal_;
int srow_, scol_, erow_, ecol_; // selection start/end. NOTE: start *might* be > end
int push_row_, push_col_; // global row/col for last FL_PUSH
bool push_char_right_;
Fl_Color selectionbgcolor_;
Fl_Color selectionfgcolor_;
int state_ ; // 0=none, 1=started, 2=extended, 3=done
bool is_selection_; // false: no selection
public:
Selection(void);
Selection(Fl_Terminal *terminal);
int srow(void) const { return srow_; }
int scol(void) const { return scol_; }
int erow(void) const { return erow_; }
int ecol(void) const { return ecol_; }
void push_clear() { push_row_ = push_col_ = -1; }
void push_rowcol(int row,int col) { push_row_ = row; push_col_ = col; }
void start_push() { start(push_row_, push_col_); }
bool dragged_off(int row,int col) { return (push_row_ != row) || (push_col_ != col); }
void push_clear() { push_row_ = push_col_ = -1; push_char_right_ = false; }
void push_rowcol(int row,int col,bool char_right) {
push_row_ = row; push_col_ = col; push_char_right_ = char_right; }
void start_push() { start(push_row_, push_col_, push_char_right_); }
bool dragged_off(int row,int col,bool char_right) {
return (push_row_ != row) || (push_col_+push_char_right_ != col+char_right); }
void selectionfgcolor(Fl_Color val) { selectionfgcolor_ = val; }
void selectionbgcolor(Fl_Color val) { selectionbgcolor_ = val; }
Fl_Color selectionfgcolor(void) const { return selectionfgcolor_; }
Fl_Color selectionbgcolor(void) const { return selectionbgcolor_; }
bool is_selection(void) const { return is_selection_; }
bool get_selection(int &srow,int &scol,int &erow,int &ecol) const; // guarantees return (start < end)
bool start(int row, int col);
bool extend(int row, int col);
bool start(int row, int col, bool char_right);
bool extend(int row, int col, bool char_right);
void end(void);
void select(int srow, int scol, int erow, int ecol);
bool clear(void);
@ -882,8 +886,8 @@ protected:
CharStyle& current_style(void) const;
void current_style(const CharStyle& sty);
private:
int x_to_glob_col(int X, int grow, int &gcol) const;
int xy_to_glob_rowcol(int X, int Y, int &grow, int &gcol) const;
int x_to_glob_col(int X, int grow, int &gcol, bool &gcr) const;
int xy_to_glob_rowcol(int X, int Y, int &grow, int &gcol, bool &gcr) const;
protected:
int w_to_col(int W) const;
int h_to_row(int H) const;
@ -906,6 +910,8 @@ protected:
const char* selection_text(void) const;
void clear_mouse_selection(void);
bool selection_extend(int X,int Y);
void select_word(int grow, int gcol);
void select_line(int grow);
void scroll(int rows);
void insert_rows(int count);
void delete_rows(int count);

View File

@ -137,7 +137,9 @@ static bool is_frame(Fl_Boxtype b) {
///////////////////////////////////////
// Ctor
Fl_Terminal::Selection::Selection(void) {
Fl_Terminal::Selection::Selection(Fl_Terminal *terminal)
: terminal_(terminal)
{
// These are used to set/get the mouse selection
srow_ = scol_ = erow_ = ecol_ = 0;
// FL_PUSH event row/col
@ -172,7 +174,7 @@ bool Fl_Terminal::Selection::get_selection(int &srow,int &scol,
// Start new selection at specified row,col
// Always returns true.
//
bool Fl_Terminal::Selection::start(int row, int col) {
bool Fl_Terminal::Selection::start(int row, int col, bool char_right) {
srow_ = erow_ = row;
scol_ = ecol_ = col;
state_ = 1; // state: "started selection"
@ -183,14 +185,42 @@ bool Fl_Terminal::Selection::start(int row, int col) {
// Extend existing selection to row,col
// Returns true if anything changed, false if not.
//
bool Fl_Terminal::Selection::extend(int row, int col) {
bool Fl_Terminal::Selection::extend(int row, int col, bool char_right) {
// no selection started yet? start and return true
if (!is_selection()) return start(row, col);
int osrow = srow_, oerow = erow_, oscol = scol_, oecol = ecol_;
int oselection = is_selection_;
if (state_ == 0) return start(row, col, char_right);
state_ = 2; // state: "extending selection"
if (erow_ == row && ecol_ == col) return false; // no change
if ((row==push_row_) && (col+char_right==push_col_+push_char_right_)) {
// we are in the box of the original push event
srow_ = erow_ = row;
scol_ = ecol_ = col;
is_selection_ = false;
} else if ((row>push_row_) || ((row==push_row_) && (col+char_right>push_col_+push_char_right_))) {
// extend to the right and down
scol_ = push_col_ + push_char_right_;
ecol_ = col - 1 + char_right;
is_selection_ = true;
} else {
// extend to the left and up
scol_ = push_col_ - 1 + push_char_right_;
ecol_ = col + char_right;
is_selection_ = true;
}
if (scol_<0) scol_ = 0;
if (ecol_<0) ecol_ = 0;
int maxCol = terminal_->ring_cols()-1;
if (scol_>maxCol) scol_ = maxCol;
if (ecol_>maxCol) ecol_ = maxCol;
srow_ = push_row_;
erow_ = row;
ecol_ = col;
return true;
bool changed = ( (osrow != srow_) || (oerow != erow_)
|| (oscol != scol_) || (oecol != ecol_)
|| (oselection != is_selection_) );
return !changed;
}
// End selection (turn dragging() off)
@ -1718,13 +1748,16 @@ uchar Fl_Terminal::textattrib() const {
- 1 if 'gcol' was found
- 0 if X not within any char in 'grow'
*/
int Fl_Terminal::x_to_glob_col(int X, int grow, int &gcol) const {
int cx = x() + margin_.left(); // char x position
int Fl_Terminal::x_to_glob_col(int X, int grow, int &gcol, bool &gcr) const {
int cx = scrn_.x(); // leftmost char x position
const Utf8Char *u8c = utf8_char_at_glob(grow, 0);
for (gcol=0; gcol<ring_cols(); gcol++,u8c++) { // walk the cols looking for X
u8c->fl_font_set(*current_style_); // pwidth_int() needs fl_font set
int cx2 = cx + u8c->pwidth_int(); // char x2 (right edge of char)
if (X >= cx && X < cx2) return 1; // found? return with gcol set
if (X >= cx && X < cx2) {
gcr = (X > ((cx+cx2)/2)); // X is in right half of character
return 1; // found? return with gcol and gcr set
}
cx += u8c->pwidth_int(); // move cx to start x of next char
}
gcol = ring_cols()-1; // don't leave larger than #cols
@ -1737,7 +1770,7 @@ int Fl_Terminal::x_to_glob_col(int X, int grow, int &gcol) const {
// 0 -- not found, outside display's character area
// -1/-2/-3/-4 -- not found, off top/bot/lt/rt edge respectively
//
int Fl_Terminal::xy_to_glob_rowcol(int X, int Y, int &grow, int &gcol) const {
int Fl_Terminal::xy_to_glob_rowcol(int X, int Y, int &grow, int &gcol, bool &gcr) const {
// X,Y outside terminal area? early exit
if (Y<scrn_.y()) return -1; // up (off top edge)
if (Y>scrn_.b()) return -2; // dn (off bot edge)
@ -1747,7 +1780,7 @@ int Fl_Terminal::xy_to_glob_rowcol(int X, int Y, int &grow, int &gcol) const {
int toprow = disp_srow() - scrollbar->value();
// Find row the 'Y' value is in
grow = toprow + ( (Y-scrn_.y()) / current_style_->fontheight());
return x_to_glob_col(X, grow, gcol);
return x_to_glob_col(X, grow, gcol, gcr);
}
/**
@ -2051,9 +2084,10 @@ void Fl_Terminal::clear_mouse_selection(void) {
*/
bool Fl_Terminal::selection_extend(int X,int Y) {
if (is_selection()) { // selection already?
int grow,gcol;
if (xy_to_glob_rowcol(X, Y, grow, gcol) > 0) {
select_.extend(grow, gcol); // extend it
int grow, gcol;
bool gcr;
if (xy_to_glob_rowcol(X, Y, grow, gcol, gcr) > 0) {
select_.extend(grow, gcol, gcr); // extend it
return true;
} else {
// TODO: If X,Y outside row/col area and SHIFT down,
@ -2063,6 +2097,36 @@ bool Fl_Terminal::selection_extend(int X,int Y) {
return false;
}
/**
Select the word around the given row and column.
*/
void Fl_Terminal::select_word(int grow, int gcol) {
int i, c0, c1;
int r = grow, c = gcol;
Utf8Char *row = u8c_ring_row(r);
int n = ring_cols();
if (c >= n) return;
if (row[c].text_utf8()[0]==' ') {
for (i=c; i>0; i--) if (row[i-1].text_utf8()[0]!=' ') break;
c0 = i;
for (i=c; i<n-2; i++) if (row[i+1].text_utf8()[0]!=' ') break;
c1 = i;
} else {
for (i=c; i>0; i--) if (row[i-1].text_utf8()[0]==' ') break;
c0 = i;
for (i=c; i<n-2; i++) if (row[i+1].text_utf8()[0]==' ') break;
c1 = i;
}
select_.select(r, c0, r, c1);
}
/**
Select the entire row.
*/
void Fl_Terminal::select_line(int grow) {
select_.select(grow, 0, grow, ring_cols()-1);
}
/**
Scroll the display up(+) or down(-) the specified \p rows.
@ -3205,7 +3269,10 @@ void Fl_Terminal::redraw_timer_cb(void *udata) {
\param[in] X,Y,W,H position and size.
\param[in] L label string (optional), may be NULL.
*/
Fl_Terminal::Fl_Terminal(int X,int Y,int W,int H,const char*L) : Fl_Group(X,Y,W,H,L) {
Fl_Terminal::Fl_Terminal(int X,int Y,int W,int H,const char*L)
: Fl_Group(X,Y,W,H,L),
select_(this)
{
bool fontsize_defer = false;
init_(X,Y,W,H,L,-1,-1,100,fontsize_defer);
}
@ -3221,7 +3288,10 @@ Fl_Terminal::Fl_Terminal(int X,int Y,int W,int H,const char*L) : Fl_Group(X,Y,W,
\note fluid uses this constructor internally to avoid font calculations that opens
the display, useful for when running in a headless context. (issue 837)
*/
Fl_Terminal::Fl_Terminal(int X,int Y,int W,int H,const char*L,int rows,int cols,int hist) : Fl_Group(X,Y,W,H,L) {
Fl_Terminal::Fl_Terminal(int X,int Y,int W,int H,const char*L,int rows,int cols,int hist)
: Fl_Group(X,Y,W,H,L),
select_(this)
{
bool fontsize_defer = true;
init_(X,Y,W,H,L,rows,cols,hist,fontsize_defer);
}
@ -3344,10 +3414,13 @@ void Fl_Terminal::scrollbar_size(int val) {
If the bg color for a character is the special "see through" color 0xffffffff,
no pixels are drawn.
\param[in] grow row number
\param[in] X, Y top left corner of the row in FLTK coordinates
*/
void Fl_Terminal::draw_row_bg(int grow, int X, int Y) const {
int bg_h = current_style_->fontheight();
int bg_y = Y - current_style_->fontheight() + current_style_->fontdescent();
int bg_y = Y;
Fl_Color bg_col;
int pwidth = 9;
const Utf8Char *u8c = u8c_ring_row(grow); // start of spec'd row
@ -3376,6 +3449,9 @@ void Fl_Terminal::draw_row_bg(int grow, int X, int Y) const {
/**
Draw the specified global row, which is the row in ring_chars[].
The global row includes history + display buffers.
\param[in] grow row number
\param[in] Y top position of characters in the row in FLTK coordinates
*/
void Fl_Terminal::draw_row(int grow, int Y) const {
// Draw background color spans, if any
@ -3383,12 +3459,16 @@ void Fl_Terminal::draw_row(int grow, int Y) const {
draw_row_bg(grow, X, Y);
// Draw forground text
int baseline = Y + current_style_->fontheight() - current_style_->fontdescent();
int scrollval = scrollbar->value();
int disp_top = (disp_srow() - scrollval); // top row we need to view
int drow = grow - disp_top; // disp row
bool inside_display = is_disp_ring_row(grow); // row inside 'display'?
int strikeout_y = Y - (current_style_->fontheight() / 3);
int underline_y = Y;
// This looks better on macOS, but too low for X. Maybe we can get better results using fl_text_extents()?
// int strikeout_y = baseline - (current_style_->fontheight() / 4);
// int underline_y = baseline + (current_style_->fontheight() / 5);
int strikeout_y = baseline - (current_style_->fontheight() / 3);
int underline_y = baseline;
const Utf8Char *u8c = u8c_ring_row(grow);
uchar lastattr = -1;
bool is_cursor;
@ -3406,7 +3486,7 @@ void Fl_Terminal::draw_row(int grow, int Y) const {
// DRAW CURSOR BLOCK - TODO: support other cursor types?
if (is_cursor) {
int cx = X;
int cy = Y - cursor_.h() + current_style_->fontdescent();
int cy = Y + current_style_->fontheight() - cursor_.h();
int cw = pwidth;
int ch = cursor_.h();
fl_color(cursorbgcolor());
@ -3428,7 +3508,7 @@ void Fl_Terminal::draw_row(int grow, int Y) const {
lastattr = -1; // (ensure font reset on next iter)
}
// 3) Draw text for UTF-8 char. No need to draw spaces
if (!u8c->is_char(' ')) fl_draw(u8c->text_utf8(), u8c->length(), X, Y);
if (!u8c->is_char(' ')) fl_draw(u8c->text_utf8(), u8c->length(), X, baseline);
// 4) Strike or underline?
if (u8c->attrib() & Fl_Terminal::UNDERLINE) fl_line(X, underline_y, X+pwidth, underline_y);
if (u8c->attrib() & Fl_Terminal::STRIKEOUT) fl_line(X, strikeout_y, X+pwidth, strikeout_y);
@ -3445,14 +3525,16 @@ void Fl_Terminal::draw_row(int grow, int Y) const {
depends on what position the scrollbar is set to.
Handles attributes, colors, text selections, cursor.
\param[in] Y top position of top left character in the window in FLTK coordinates
*/
void Fl_Terminal::draw_buff(int Y) const {
int srow = disp_srow() - scrollbar->value();
int erow = srow + disp_rows();
const int rowheight = current_style_->fontheight();
for (int grow=srow; (grow<erow) && (Y<scrn_.b()); grow++) {
Y += rowheight; // advance Y to bottom left corner of row
draw_row(grow, Y); // draw global row at Y
Y += rowheight; // advance Y to bottom left corner of row
}
}
@ -3499,7 +3581,7 @@ void Fl_Terminal::draw(void) {
This is used by the constructor to size the row/cols to fit the widget size.
*/
int Fl_Terminal::w_to_col(int W) const {
return int((float(W) / current_style_->charwidth()) + 0.0); // +.5 overshoots
return W / current_style_->charwidth();
}
/**
@ -3507,7 +3589,7 @@ int Fl_Terminal::w_to_col(int W) const {
This is used by the constructor to size the row/cols to fit the widget size.
*/
int Fl_Terminal::h_to_row(int H) const {
return int((float(H) / current_style_->fontheight()) + -0.5); // +.5 overshoots
return H / current_style_->fontheight();
}
/**
@ -3555,21 +3637,29 @@ void Fl_Terminal::handle_selection_autoscroll(void) {
Returns: 1 if 'handled', 0 if not.
*/
int Fl_Terminal::handle_selection(int e) {
int grow=0,gcol=0;
bool is_rowcol = (xy_to_glob_rowcol(Fl::event_x(), Fl::event_y(), grow, gcol) > 0)
int grow=0, gcol=0;
bool gcr = false;
bool is_rowcol = (xy_to_glob_rowcol(Fl::event_x(), Fl::event_y(), grow, gcol, gcr) > 0)
? true : false;
switch (e) {
case FL_PUSH: {
select_.push_rowcol(grow, gcol);
// SHIFT-LEFT-CLICK? Extend or start new
if (Fl::event_state(FL_SHIFT)) {
if (is_selection()) { // extend if select in progress
selection_extend(Fl::event_x(), Fl::event_y());
redraw();
return 1; // express interest in FL_DRAG
}
} else { // Start a new selection
select_.push_rowcol(grow, gcol, gcr);
if (select_.clear()) redraw(); // clear prev selection
if (is_rowcol) return 1; // express interest in FL_DRAG
if (is_rowcol) {
switch (Fl::event_clicks()) {
case 1: select_word(grow, gcol); break;
case 2: select_line(grow); break;
}
return 1; // express interest in FL_DRAG
}
}
// Left-Click outside terminal area?
if (!Fl::event_state(FL_SHIFT)) {
@ -3582,11 +3672,11 @@ int Fl_Terminal::handle_selection(int e) {
case FL_DRAG: {
if (is_rowcol) {
if (!is_selection()) { // no selection yet?
if (select_.dragged_off(grow, gcol)) { // dragged off FL_PUSH? enough to start
if (select_.dragged_off(grow, gcol, gcr)) { // dragged off FL_PUSH? enough to start
select_.start_push(); // ..start drag with FL_PUSH position
}
} else {
if (select_.extend(grow, gcol)) redraw(); // redraw if selection changed
if (select_.extend(grow, gcol, gcr)) redraw(); // redraw if selection changed
}
}
// If we leave scrn area, start timer to auto-scroll+select
@ -3594,7 +3684,6 @@ int Fl_Terminal::handle_selection(int e) {
return 1;
}
case FL_RELEASE: {
select_.push_clear();
select_.end();
// middlemouse gets immediate copy of selection
if (is_selection()) {