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

View File

@ -137,7 +137,9 @@ static bool is_frame(Fl_Boxtype b) {
/////////////////////////////////////// ///////////////////////////////////////
// Ctor // Ctor
Fl_Terminal::Selection::Selection(void) { Fl_Terminal::Selection::Selection(Fl_Terminal *terminal)
: terminal_(terminal)
{
// These are used to set/get the mouse selection // These are used to set/get the mouse selection
srow_ = scol_ = erow_ = ecol_ = 0; srow_ = scol_ = erow_ = ecol_ = 0;
// FL_PUSH event row/col // 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 // Start new selection at specified row,col
// Always returns true. // 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; srow_ = erow_ = row;
scol_ = ecol_ = col; scol_ = ecol_ = col;
state_ = 1; // state: "started selection" state_ = 1; // state: "started selection"
@ -183,14 +185,42 @@ bool Fl_Terminal::Selection::start(int row, int col) {
// Extend existing selection to row,col // Extend existing selection to row,col
// Returns true if anything changed, false if not. // 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 // 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" 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; 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) // End selection (turn dragging() off)
@ -1718,13 +1748,16 @@ uchar Fl_Terminal::textattrib() const {
- 1 if 'gcol' was found - 1 if 'gcol' was found
- 0 if X not within any char in 'grow' - 0 if X not within any char in 'grow'
*/ */
int Fl_Terminal::x_to_glob_col(int X, int grow, int &gcol) const { int Fl_Terminal::x_to_glob_col(int X, int grow, int &gcol, bool &gcr) const {
int cx = x() + margin_.left(); // char x position int cx = scrn_.x(); // leftmost char x position
const Utf8Char *u8c = utf8_char_at_glob(grow, 0); const Utf8Char *u8c = utf8_char_at_glob(grow, 0);
for (gcol=0; gcol<ring_cols(); gcol++,u8c++) { // walk the cols looking for X 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 u8c->fl_font_set(*current_style_); // pwidth_int() needs fl_font set
int cx2 = cx + u8c->pwidth_int(); // char x2 (right edge of char) 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 cx += u8c->pwidth_int(); // move cx to start x of next char
} }
gcol = ring_cols()-1; // don't leave larger than #cols 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 // 0 -- not found, outside display's character area
// -1/-2/-3/-4 -- not found, off top/bot/lt/rt edge respectively // -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 // X,Y outside terminal area? early exit
if (Y<scrn_.y()) return -1; // up (off top edge) if (Y<scrn_.y()) return -1; // up (off top edge)
if (Y>scrn_.b()) return -2; // dn (off bot 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(); int toprow = disp_srow() - scrollbar->value();
// Find row the 'Y' value is in // Find row the 'Y' value is in
grow = toprow + ( (Y-scrn_.y()) / current_style_->fontheight()); 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) { bool Fl_Terminal::selection_extend(int X,int Y) {
if (is_selection()) { // selection already? if (is_selection()) { // selection already?
int grow,gcol; int grow, gcol;
if (xy_to_glob_rowcol(X, Y, grow, gcol) > 0) { bool gcr;
select_.extend(grow, gcol); // extend it if (xy_to_glob_rowcol(X, Y, grow, gcol, gcr) > 0) {
select_.extend(grow, gcol, gcr); // extend it
return true; return true;
} else { } else {
// TODO: If X,Y outside row/col area and SHIFT down, // 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; 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. 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] X,Y,W,H position and size.
\param[in] L label string (optional), may be NULL. \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; bool fontsize_defer = false;
init_(X,Y,W,H,L,-1,-1,100,fontsize_defer); 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 \note fluid uses this constructor internally to avoid font calculations that opens
the display, useful for when running in a headless context. (issue 837) 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; bool fontsize_defer = true;
init_(X,Y,W,H,L,rows,cols,hist,fontsize_defer); 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, If the bg color for a character is the special "see through" color 0xffffffff,
no pixels are drawn. 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 { void Fl_Terminal::draw_row_bg(int grow, int X, int Y) const {
int bg_h = current_style_->fontheight(); int bg_h = current_style_->fontheight();
int bg_y = Y - current_style_->fontheight() + current_style_->fontdescent(); int bg_y = Y;
Fl_Color bg_col; Fl_Color bg_col;
int pwidth = 9; int pwidth = 9;
const Utf8Char *u8c = u8c_ring_row(grow); // start of spec'd row 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[]. Draw the specified global row, which is the row in ring_chars[].
The global row includes history + display buffers. 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 { void Fl_Terminal::draw_row(int grow, int Y) const {
// Draw background color spans, if any // 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_row_bg(grow, X, Y);
// Draw forground text // Draw forground text
int baseline = Y + current_style_->fontheight() - current_style_->fontdescent();
int scrollval = scrollbar->value(); int scrollval = scrollbar->value();
int disp_top = (disp_srow() - scrollval); // top row we need to view int disp_top = (disp_srow() - scrollval); // top row we need to view
int drow = grow - disp_top; // disp row int drow = grow - disp_top; // disp row
bool inside_display = is_disp_ring_row(grow); // row inside 'display'? bool inside_display = is_disp_ring_row(grow); // row inside 'display'?
int strikeout_y = Y - (current_style_->fontheight() / 3); // This looks better on macOS, but too low for X. Maybe we can get better results using fl_text_extents()?
int underline_y = Y; // 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); const Utf8Char *u8c = u8c_ring_row(grow);
uchar lastattr = -1; uchar lastattr = -1;
bool is_cursor; 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? // DRAW CURSOR BLOCK - TODO: support other cursor types?
if (is_cursor) { if (is_cursor) {
int cx = X; int cx = X;
int cy = Y - cursor_.h() + current_style_->fontdescent(); int cy = Y + current_style_->fontheight() - cursor_.h();
int cw = pwidth; int cw = pwidth;
int ch = cursor_.h(); int ch = cursor_.h();
fl_color(cursorbgcolor()); 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) lastattr = -1; // (ensure font reset on next iter)
} }
// 3) Draw text for UTF-8 char. No need to draw spaces // 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? // 4) Strike or underline?
if (u8c->attrib() & Fl_Terminal::UNDERLINE) fl_line(X, underline_y, X+pwidth, underline_y); 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); 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. depends on what position the scrollbar is set to.
Handles attributes, colors, text selections, cursor. 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 { void Fl_Terminal::draw_buff(int Y) const {
int srow = disp_srow() - scrollbar->value(); int srow = disp_srow() - scrollbar->value();
int erow = srow + disp_rows(); int erow = srow + disp_rows();
const int rowheight = current_style_->fontheight(); const int rowheight = current_style_->fontheight();
for (int grow=srow; (grow<erow) && (Y<scrn_.b()); grow++) { 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 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. 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 { 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. 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 { 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. Returns: 1 if 'handled', 0 if not.
*/ */
int Fl_Terminal::handle_selection(int e) { int Fl_Terminal::handle_selection(int e) {
int grow=0,gcol=0; int grow=0, gcol=0;
bool is_rowcol = (xy_to_glob_rowcol(Fl::event_x(), Fl::event_y(), grow, gcol) > 0) bool gcr = false;
bool is_rowcol = (xy_to_glob_rowcol(Fl::event_x(), Fl::event_y(), grow, gcol, gcr) > 0)
? true : false; ? true : false;
switch (e) { switch (e) {
case FL_PUSH: { case FL_PUSH: {
select_.push_rowcol(grow, gcol);
// SHIFT-LEFT-CLICK? Extend or start new // SHIFT-LEFT-CLICK? Extend or start new
if (Fl::event_state(FL_SHIFT)) { if (Fl::event_state(FL_SHIFT)) {
if (is_selection()) { // extend if select in progress if (is_selection()) { // extend if select in progress
selection_extend(Fl::event_x(), Fl::event_y()); selection_extend(Fl::event_x(), Fl::event_y());
redraw();
return 1; // express interest in FL_DRAG return 1; // express interest in FL_DRAG
} }
} else { // Start a new selection } else { // Start a new selection
select_.push_rowcol(grow, gcol, gcr);
if (select_.clear()) redraw(); // clear prev selection 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? // Left-Click outside terminal area?
if (!Fl::event_state(FL_SHIFT)) { if (!Fl::event_state(FL_SHIFT)) {
@ -3582,11 +3672,11 @@ int Fl_Terminal::handle_selection(int e) {
case FL_DRAG: { case FL_DRAG: {
if (is_rowcol) { if (is_rowcol) {
if (!is_selection()) { // no selection yet? 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 select_.start_push(); // ..start drag with FL_PUSH position
} }
} else { } 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 // If we leave scrn area, start timer to auto-scroll+select
@ -3594,7 +3684,6 @@ int Fl_Terminal::handle_selection(int e) {
return 1; return 1;
} }
case FL_RELEASE: { case FL_RELEASE: {
select_.push_clear();
select_.end(); select_.end();
// middlemouse gets immediate copy of selection // middlemouse gets immediate copy of selection
if (is_selection()) { if (is_selection()) {