Rewrite (again!) the code for caret drawing

The caret geometry is now computed in BufferView as a list of shapes
(caret, horizontal l-shape if needed, completion triangle if needed)
kept in a variable of type CaretGeometry.

The code in WorkArea.cpp only has to draw these shapes. The
CaretWidget (which never was a widget) in GuiWorkArea.cpp is gone
now.

As a consequence, the bounding box for the cursor is known precisely
and therefore rows should be repainted correctly now. This avoids
caret droppings.

Fixes bug #12024.
This commit is contained in:
Jean-Marc Lasgouttes 2020-11-24 18:35:25 +01:00
parent 4487af367c
commit 1b3ffa6627
5 changed files with 150 additions and 129 deletions

View File

@ -53,6 +53,7 @@
#include "mathed/MathData.h" #include "mathed/MathData.h"
#include "frontends/alert.h" #include "frontends/alert.h"
#include "frontends/CaretGeometry.h"
#include "frontends/Delegates.h" #include "frontends/Delegates.h"
#include "frontends/FontMetrics.h" #include "frontends/FontMetrics.h"
#include "frontends/NullPainter.h" #include "frontends/NullPainter.h"
@ -288,6 +289,8 @@ struct BufferView::Private
CursorSlice current_row_slice_; CursorSlice current_row_slice_;
/// are we hovering something that we can click /// are we hovering something that we can click
bool clickable_inset_; bool clickable_inset_;
/// shape of the caret
frontend::CaretGeometry caret_geometry_;
}; };
@ -3067,10 +3070,91 @@ void BufferView::caretPosAndDim(Point & p, Dimension & dim) const
p = getPos(cur); p = getPos(cur);
// center fat carets horizontally // center fat carets horizontally
p.x_ -= dim.wid / 2; p.x_ -= dim.wid / 2;
// p is top-left
p.y_ -= dim.asc; p.y_ -= dim.asc;
} }
void BufferView::buildCaretGeometry(bool complet)
{
Point p;
Dimension dim;
caretPosAndDim(p, dim);
Cursor const & cur = d->cursor_;
Font const & realfont = cur.real_current_font;
frontend::FontMetrics const & fm = theFontMetrics(realfont.fontInfo());
bool const isrtl = realfont.isVisibleRightToLeft();
int const dir = isrtl ? -1 : 1;
frontend::CaretGeometry & cg = d->caret_geometry_;
cg.shapes.clear();
// The caret itself, slanted for italics in text edit mode except
// for selections because the selection rect does not slant
bool const slant = fm.italic() && cur.inTexted() && !cur.selection();
double const slope = slant ? fm.italicSlope() : 0;
cg.shapes.push_back(
{{iround(p.x_ + dim.asc * slope), p.y_},
{iround(p.x_ - dim.des * slope), p.y_ + dim.height()},
{iround(p.x_ + dir * dim.wid - dim.des * slope), p.y_ + dim.height()},
{iround(p.x_ + dir * dim.wid + dim.asc * slope), p.y_}}
);
// The language indicator _| (if needed)
Language const * doclang = buffer().params().language;
if (!((realfont.language() == doclang && isrtl == doclang->rightToLeft())
|| realfont.language() == latex_language)) {
int const lx = dim.height() / 3;
int const xx = iround(p.x_ - dim.des * slope);
int const yy = p.y_ + dim.height();
cg.shapes.push_back(
{{xx, yy - dim.wid},
{xx + dir * (dim.wid + lx - 1), yy - dim.wid},
{xx + dir * (dim.wid + lx - 1), yy},
{xx, yy}}
);
}
// The completion triangle |> (if needed)
if (complet) {
int const m = p.y_ + dim.height() / 2;
int const d = dim.height() / 8;
// offset for slanted carret
int const sx = iround((dim.asc - (dim.height() / 2 - d)) * slope);
// starting position x
int const xx = p.x_ + dir * dim.wid + sx;
cg.shapes.push_back(
{{xx, m - d},
{xx + dir * d, m},
{xx, m + d},
{xx, m + d - dim.wid},
{xx + dir * d - dim.wid, m},
{xx, m - d + dim.wid}}
);
}
// compute extremal x values
cg.left = 1000000;
cg.right = -1000000;
cg.top = 1000000;
cg.bottom = -1000000;
for (auto const & shape : cg.shapes)
for (Point const & p : shape) {
cg.left = min(cg.left, p.x_);
cg.right = max(cg.right, p.x_);
cg.top = min(cg.top, p.y_);
cg.bottom = max(cg.bottom, p.y_);
}
}
frontend::CaretGeometry const & BufferView::caretGeometry() const
{
return d->caret_geometry_;
}
bool BufferView::caretInView() const bool BufferView::caretInView() const
{ {
if (!paragraphVisible(cursor())) if (!paragraphVisible(cursor()))
@ -3296,19 +3380,13 @@ void BufferView::draw(frontend::Painter & pain, bool paint_caret)
*/ */
if (paint_caret) { if (paint_caret) {
Cursor cur(d->cursor_); Cursor cur(d->cursor_);
Point p;
Dimension dim;
caretPosAndDim(p, dim);
while (cur.depth() > 1) { while (cur.depth() > 1) {
if (cur.inTexted()) { if (!cur.inTexted())
TextMetrics const & tm = textMetrics(cur.text()); break;
if (p.x_ >= tm.origin().x_ TextMetrics const & tm = textMetrics(cur.text());
&& p.x_ + dim.width() <= tm.origin().x_ + tm.dim().width()) if (d->caret_geometry_.left >= tm.origin().x_
break; && d->caret_geometry_.right <= tm.origin().x_ + tm.dim().width())
} else {
// in mathed
break; break;
}
cur.pop(); cur.pop();
} }
cur.textRow().changed(true); cur.textRow().changed(true);

View File

@ -26,6 +26,7 @@ namespace lyx {
namespace support { class FileName; } namespace support { class FileName; }
namespace frontend { class CaretGeometry; }
namespace frontend { class Painter; } namespace frontend { class Painter; }
namespace frontend { class GuiBufferViewDelegate; } namespace frontend { class GuiBufferViewDelegate; }
@ -311,6 +312,10 @@ public:
bool caretInView() const; bool caretInView() const;
/// get the position and height of the caret /// get the position and height of the caret
void caretPosAndDim(Point & p, Dimension & dim) const; void caretPosAndDim(Point & p, Dimension & dim) const;
/// compute the shape of the caret
void buildCaretGeometry(bool complet);
/// the shape of the caret
frontend::CaretGeometry const & caretGeometry() const;
/// ///
void draw(frontend::Painter & pain, bool paint_caret); void draw(frontend::Painter & pain, bool paint_caret);

View File

@ -12,6 +12,7 @@ AM_CPPFLAGS += -I$(srcdir)/.. \
liblyxfrontends_a_SOURCES = \ liblyxfrontends_a_SOURCES = \
alert.h \ alert.h \
Application.h \ Application.h \
CaretGeometry.h \
FontLoader.h \ FontLoader.h \
FontMetrics.h \ FontMetrics.h \
Delegates.h \ Delegates.h \

View File

@ -15,7 +15,6 @@
#include "GuiWorkArea_Private.h" #include "GuiWorkArea_Private.h"
#include "ColorCache.h" #include "ColorCache.h"
#include "FontLoader.h"
#include "GuiApplication.h" #include "GuiApplication.h"
#include "GuiCompleter.h" #include "GuiCompleter.h"
#include "GuiKeySymbol.h" #include "GuiKeySymbol.h"
@ -33,7 +32,6 @@
#include "Font.h" #include "Font.h"
#include "FuncRequest.h" #include "FuncRequest.h"
#include "KeySymbol.h" #include "KeySymbol.h"
#include "Language.h"
#include "LyX.h" #include "LyX.h"
#include "LyXRC.h" #include "LyXRC.h"
#include "LyXVC.h" #include "LyXVC.h"
@ -51,6 +49,8 @@
#include "support/TempFile.h" #include "support/TempFile.h"
#include "frontends/Application.h" #include "frontends/Application.h"
#include "frontends/CaretGeometry.h"
#include "frontends/FontMetrics.h" #include "frontends/FontMetrics.h"
#include "frontends/WorkAreaManager.h" #include "frontends/WorkAreaManager.h"
@ -125,100 +125,6 @@ mouse_button::state q_motion_state(Qt::MouseButtons state)
namespace frontend { namespace frontend {
class CaretWidget {
public:
CaretWidget() : dir(1), l_shape(false), completable(false),
x(0), y(0), slope(0)
{}
/* Draw the caret. Parameter \c horiz_offset is not 0 when there
* has been horizontal scrolling in current row
*/
void draw(QPainter & painter, int horiz_offset)
{
if (dim.empty())
return;
// correction for horizontal scrolling
int const xx = x - horiz_offset;
int const lx = dim.height() / 3;
// draw caret box
painter.setPen(color);
QPainterPath path;
path.moveTo(xx + dim.asc * slope, y);
path.lineTo(xx - dim.des * slope, y + dim.height());
path.lineTo(xx + dir * dim.wid - dim.des * slope, y + dim.height());
path.lineTo(xx + dir * dim.wid + dim.asc * slope, y);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.fillPath(path, color);
painter.setRenderHint(QPainter::Antialiasing, false);
// draw RTL/LTR indication
if (l_shape)
painter.fillRect(xx - dim.des * slope,
y + dim.height() - dim.wid + 1,
dir * (dim.wid + lx - 1), dim.wid, color);
// draw completion triangle
if (completable) {
int const m = y + dim.height() / 2;
int const d = dim.height() / 8;
// offset for slanted carret
int const sx = (dim.asc - (dim.height() / 2 - d)) * slope;
painter.setPen(QPen(color, dim.width()));
painter.drawLine(xx + dir * dim.wid + sx, m - d,
xx + dir * (dim.wid + d) + sx, m);
painter.drawLine(xx + dir * dim.wid + sx, m + d,
xx + dir * (dim.wid + d) + sx, m);
}
}
void update(BufferView const * bv, bool complet) {
// Cursor size and position
Point point;
bv->caretPosAndDim(point, dim);
x = point.x_;
y = point.y_;
completable = complet;
Cursor const & cur = bv->cursor();
Font const & realfont = cur.real_current_font;
FontMetrics const & fm = theFontMetrics(realfont.fontInfo());
BufferParams const & bp = bv->buffer().params();
bool const samelang = realfont.language() == bp.language;
bool const isrtl = realfont.isVisibleRightToLeft();
dir = isrtl ? -1 : 1;
// special shape
l_shape = (!samelang || isrtl != bp.language->rightToLeft())
&& realfont.language() != latex_language;
// use slanted caret for italics in text edit mode
// except for selections because the selection rect does not slant
bool const slant = fm.italic() && cur.inTexted() && !cur.selection();
slope = slant ? fm.italicSlope() : 0;
color = guiApp->colorCache().get(Color_cursor);
}
/// text direction (1 for LtR, -1 for RtL)
int dir;
/// indication for language change
bool l_shape;
/// triangle to show that a completion is available
bool completable;
///
QColor color;
/// dimension uf base caret
Dimension dim;
/// x position (were the vertical line is drawn)
int x;
/// y position (the top of the caret)
int y;
/// the slope for drawing slanted caret
double slope;
};
// This is a 'heartbeat' generating synthetic mouse move events when the // This is a 'heartbeat' generating synthetic mouse move events when the
// cursor is at the top or bottom edge of the viewport. One scroll per 0.2 s // cursor is at the top or bottom edge of the viewport. One scroll per 0.2 s
SyntheticMouseEvent::SyntheticMouseEvent() SyntheticMouseEvent::SyntheticMouseEvent()
@ -227,7 +133,7 @@ SyntheticMouseEvent::SyntheticMouseEvent()
GuiWorkArea::Private::Private(GuiWorkArea * parent) GuiWorkArea::Private::Private(GuiWorkArea * parent)
: p(parent), buffer_view_(nullptr), lyx_view_(nullptr), caret_(nullptr), : p(parent), buffer_view_(nullptr), lyx_view_(nullptr),
caret_visible_(false), need_resize_(false), preedit_lines_(1), caret_visible_(false), need_resize_(false), preedit_lines_(1),
last_pixel_ratio_(1.0), completer_(new GuiCompleter(p, p)), last_pixel_ratio_(1.0), completer_(new GuiCompleter(p, p)),
dialog_mode_(false), shell_escape_(false), read_only_(false), dialog_mode_(false), shell_escape_(false), read_only_(false),
@ -266,7 +172,6 @@ GuiWorkArea::Private::~Private()
buffer_view_->buffer().workAreaManager().remove(p); buffer_view_->buffer().workAreaManager().remove(p);
} catch(...) {} } catch(...) {}
delete buffer_view_; delete buffer_view_;
delete caret_;
// Completer has a QObject parent and is thus automatically destroyed. // Completer has a QObject parent and is thus automatically destroyed.
// See #4758. // See #4758.
// delete completer_; // delete completer_;
@ -316,7 +221,6 @@ void GuiWorkArea::init()
// With Qt4.5 a mouse event will happen before the first paint event // With Qt4.5 a mouse event will happen before the first paint event
// so make sure that the buffer view has an up to date metrics. // so make sure that the buffer view has an up to date metrics.
d->buffer_view_->resize(viewport()->width(), viewport()->height()); d->buffer_view_->resize(viewport()->width(), viewport()->height());
d->caret_ = new frontend::CaretWidget();
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setAcceptDrops(true); setAcceptDrops(true);
@ -606,7 +510,7 @@ void GuiWorkArea::Private::resetCaret()
&& !completer_->popupVisible() && !completer_->popupVisible()
&& !completer_->inlineVisible(); && !completer_->inlineVisible();
caret_->update(buffer_view_, completable); buffer_view_->buildCaretGeometry(completable);
needs_caret_geometry_update_ = true; needs_caret_geometry_update_ = true;
caret_visible_ = true; caret_visible_ = true;
@ -647,6 +551,33 @@ void GuiWorkArea::Private::hideCaret()
} }
/* Draw the caret. Parameter \c horiz_offset is not 0 when there
* has been horizontal scrolling in current row
*/
void GuiWorkArea::Private::drawCaret(QPainter & painter, int horiz_offset) const
{
if (buffer_view_->caretGeometry().shapes.empty())
return;
QColor const color = guiApp->colorCache().get(Color_cursor);
painter.setPen(color);
painter.setRenderHint(QPainter::Antialiasing, true);
for (auto const & shape : buffer_view_->caretGeometry().shapes) {
bool first = true;
QPainterPath path;
for (Point const & p : shape) {
if (first) {
path.moveTo(p.x_ - horiz_offset, p.y_);
first = false;
} else
path.lineTo(p.x_ - horiz_offset, p.y_);
}
painter.fillPath(path, color);
}
painter.setRenderHint(QPainter::Antialiasing, false);
}
void GuiWorkArea::Private::updateScrollbar() void GuiWorkArea::Private::updateScrollbar()
{ {
// Prevent setRange() and setSliderPosition from causing recursive calls via // Prevent setRange() and setSliderPosition from causing recursive calls via
@ -1194,9 +1125,11 @@ void GuiWorkArea::Private::paintPreeditText(GuiPainter & pain)
// FIXME: shall we use real_current_font here? (see #10478) // FIXME: shall we use real_current_font here? (see #10478)
FontInfo const font = buffer_view_->cursor().getFont().fontInfo(); FontInfo const font = buffer_view_->cursor().getFont().fontInfo();
FontMetrics const & fm = theFontMetrics(font); FontMetrics const & fm = theFontMetrics(font);
int const height = fm.maxHeight(); Point point;
int cur_x = caret_->x; Dimension dim;
int cur_y = caret_->y + height; buffer_view_->caretPosAndDim(point, dim);
int cur_x = point.x_;
int cur_y = point.y_ + dim.height();
// get attributes of input method cursor. // get attributes of input method cursor.
// cursor_pos : cursor position in preedit string. // cursor_pos : cursor position in preedit string.
@ -1249,7 +1182,7 @@ void GuiWorkArea::Private::paintPreeditText(GuiPainter & pain)
// if we reached the right extremity of the screen, go to next line. // if we reached the right extremity of the screen, go to next line.
if (cur_x + fm.width(typed_char) > p->viewport()->width() - right_margin) { if (cur_x + fm.width(typed_char) > p->viewport()->width() - right_margin) {
cur_x = right_margin; cur_x = right_margin;
cur_y += height + 1; cur_y += dim.height() + 1;
++preedit_lines_; ++preedit_lines_;
} }
// preedit strings are displayed with dashed underline // preedit strings are displayed with dashed underline
@ -1347,7 +1280,7 @@ void GuiWorkArea::paintEvent(QPaintEvent * ev)
if (d->caret_visible_) { if (d->caret_visible_) {
if (d->needs_caret_geometry_update_) if (d->needs_caret_geometry_update_)
d->updateCaretGeometry(); d->updateCaretGeometry();
d->caret_->draw(pain, d->buffer_view_->horizScrollOffset()); d->drawCaret(pain, d->buffer_view_->horizScrollOffset());
} }
d->updateScreen(ev->rect()); d->updateScreen(ev->rect());
@ -1390,10 +1323,11 @@ void GuiWorkArea::inputMethodEvent(QInputMethodEvent * e)
// redraw area of preedit string. // redraw area of preedit string.
int height = d->caret_->dim.height(); // int height = d->caret_->dim.height();
int cur_y = d->caret_->y; // int cur_y = d->caret_->y;
viewport()->update(0, cur_y, viewport()->width(), // viewport()->update(0, cur_y, viewport()->width(),
(height + 1) * d->preedit_lines_); // (height + 1) * d->preedit_lines_);
viewport()->update();
if (d->preedit_string_.empty()) { if (d->preedit_string_.empty()) {
d->preedit_lines_ = 1; d->preedit_lines_ = 1;
@ -1411,12 +1345,14 @@ QVariant GuiWorkArea::inputMethodQuery(Qt::InputMethodQuery query) const
switch (query) { switch (query) {
// this is the CJK-specific composition window position and // this is the CJK-specific composition window position and
// the context menu position when the menu key is pressed. // the context menu position when the menu key is pressed.
case Qt::ImMicroFocus: case Qt::ImMicroFocus: {
return QRect(d->caret_->x - 10 * (d->preedit_lines_ != 1), CaretGeometry const & cg = bufferView().caretGeometry();
d->caret_->y + d->caret_->dim.height() * d->preedit_lines_, return QRect(cg.left - 10 * (d->preedit_lines_ != 1),
d->caret_->dim.width(), d->caret_->dim.height()); cg.top + cg.height() * d->preedit_lines_,
default: cg.width(), cg.height());
return QWidget::inputMethodQuery(query); }
default:
return QWidget::inputMethodQuery(query);
} }
} }

View File

@ -68,7 +68,6 @@ public:
/** /**
* Implementation of the work area (buffer view GUI) * Implementation of the work area (buffer view GUI)
*/ */
class CaretWidget;
struct GuiWorkArea::Private struct GuiWorkArea::Private
{ {
@ -91,6 +90,10 @@ struct GuiWorkArea::Private
void showCaret(); void showCaret();
/// hide the caret if it is visible /// hide the caret if it is visible
void hideCaret(); void hideCaret();
/* Draw the caret. Parameter \c horiz_offset is not 0 when there
* has been horizontal scrolling in current row
*/
void drawCaret(QPainter & painter, int horiz_offset) const;
/// Set the range and value of the scrollbar and connect to its valueChanged /// Set the range and value of the scrollbar and connect to its valueChanged
/// signal. /// signal.
void updateScrollbar(); void updateScrollbar();
@ -118,8 +121,6 @@ struct GuiWorkArea::Private
/// ///
QImage screen_; QImage screen_;
///
CaretWidget * caret_;
/// is the caret currently displayed /// is the caret currently displayed
bool caret_visible_; bool caret_visible_;
/// ///