Skip to content

Commit 447cd54

Browse files
committed
Indent wrapped lines
1 parent 928c0e4 commit 447cd54

8 files changed

+127
-41
lines changed

core/katvan_codemodel.cpp

+8
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,14 @@ bool CodeModel::startsLeftLeaningSpan(QTextBlock block) const
306306
return false;
307307
}
308308

309+
bool CodeModel::canStartWithListItem(QTextBlock block) const
310+
{
311+
auto span = spanAtPosition(block, block.position());
312+
return !span
313+
|| span->state == State::CONTENT
314+
|| span->state == State::CONTENT_BLOCK;
315+
}
316+
309317
std::tuple<State, State> CodeModel::getStatesForBracketInsertion(QTextCursor cursor) const
310318
{
311319
State prevState = State::INVALID;

core/katvan_codemodel.h

+5-1
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ class CodeModel : public QObject
108108
// the content) is Left-to-Right.
109109
bool startsLeftLeaningSpan(QTextBlock block) const;
110110

111+
// For the given block, check if its' initial span allows for it to represent
112+
// a list item.
113+
bool canStartWithListItem(QTextBlock block) const;
114+
111115
// Find closing bracket character that should be automatically appended
112116
// if _openBracket_ is inserted at the given cursor's position.
113117
std::optional<QChar> getMatchingCloseBracket(QTextCursor cursor, QChar openBracket) const;
@@ -116,7 +120,7 @@ class CodeModel : public QObject
116120
// Find the inner most state span still in effect at the given global position
117121
std::optional<StateSpan> spanAtPosition(QTextBlock block, int globalPos) const;
118122

119-
// Find the relevant "previous" and current" states for the cursor to consider
123+
// Find the relevant "previous" and "current" states for the cursor to consider
120124
// which brackets can be auto-inserted.
121125
std::tuple<parsing::ParserState::Kind, parsing::ParserState::Kind> getStatesForBracketInsertion(QTextCursor cursor) const;
122126

core/katvan_editor.cpp

+5-20
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ void Editor::setSourceDiagnostics(QList<typstdriver::Diagnostic> diagnostics)
195195

196196
QMenu* Editor::createInsertMenu()
197197
{
198-
QFont ccFont { "KatvanControl" };
198+
QFont ccFont { utils::CONTROL_FONT_FAMILY };
199199

200200
QMenu* menu = new QMenu();
201201

@@ -454,35 +454,20 @@ QString Editor::getIndentString(QTextCursor cursor) const
454454
return QLatin1String("\t");
455455
}
456456

457-
static bool isSingleBidiMark(QChar ch)
458-
{
459-
return ch == utils::LRM_MARK || ch == utils::RLM_MARK || ch == utils::ALM_MARK;
460-
}
461-
462-
static bool isSpace(QChar ch)
463-
{
464-
return ch.category() == QChar::Separator_Space || ch == QLatin1Char('\t') || isSingleBidiMark(ch);
465-
}
466-
467-
static bool isAllWhitespace(const QString& text)
468-
{
469-
return std::all_of(text.begin(), text.end(), isSpace);
470-
}
471-
472457
static QString getLeadingIndent(const QString& text)
473458
{
474459
qsizetype i = 0;
475-
while (i < text.size() && isSpace(text[i])) {
460+
while (i < text.size() && utils::isWhitespace(text[i])) {
476461
i++;
477462
}
478-
return text.left(i).removeIf(isSingleBidiMark);
463+
return text.left(i).removeIf(utils::isSingleBidiMark);
479464
}
480465

481466
static void cursorSkipWhite(QTextCursor& cursor)
482467
{
483468
QString blockText = cursor.block().text();
484469
while (!cursor.atBlockEnd()) {
485-
if (isSpace(blockText[cursor.positionInBlock()])) {
470+
if (utils::isWhitespace(blockText[cursor.positionInBlock()])) {
486471
cursor.movePosition(QTextCursor::NextCharacter);
487472
}
488473
else {
@@ -495,7 +480,7 @@ static bool cursorInLeadingWhitespace(const QTextCursor& cursor)
495480
{
496481
QString blockText = cursor.block().text();
497482
QString textBeforeCursor = blockText.sliced(0, cursor.positionInBlock());
498-
return isAllWhitespace(textBeforeCursor);
483+
return utils::isAllWhitespace(textBeforeCursor);
499484
}
500485

501486
void Editor::handleNewLine()

core/katvan_editorlayout.cpp

+81-17
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,10 @@ int EditorLayout::hitTest(const QPointF& point, Qt::HitTestAccuracy accuracy) co
178178
int lineCount = layout->lineCount();
179179
for (int i = 0; i < lineCount; i++) {
180180
QTextLine line = layout->lineAt(i);
181-
if (!line.rect().contains(blockPoint) && i < lineCount - 1) {
181+
182+
QRectF lineRect = line.rect();
183+
bool inLine = blockPoint.y() >= lineRect.top() && blockPoint.y() <= lineRect.bottom();
184+
if (!inLine && i < lineCount - 1) {
182185
continue;
183186
}
184187

@@ -333,10 +336,9 @@ void EditorLayout::documentChanged(int position, int charsRemoved, int charsAdde
333336
Q_EMIT update();
334337
}
335338

336-
static void buildDisplayLayout(const QTextBlock& block, LayoutBlockData* blockData)
339+
static void buildDisplayLayout(const QTextBlock& block, QString blockText, LayoutBlockData* blockData)
337340
{
338-
QString text = block.text();
339-
size_t newHash = qHash(text);
341+
size_t newHash = qHash(blockText);
340342

341343
// Calculating a display layout is expensive, only do it if content actually changed
342344
if (newHash == blockData->textHash) {
@@ -355,14 +357,16 @@ static void buildDisplayLayout(const QTextBlock& block, LayoutBlockData* blockDa
355357
QList<QTextLayout::FormatRange> formats = defaultLayout->formats();
356358
parsing::IsolateRangeList isolates = isolateData->isolates();
357359

360+
blockText.detach();
361+
358362
// Inject Unicode BiDi control characters the block's text for the needed
359363
// isolate ranges, creating a different text that is used for shaping but
360364
// not edit purposes. This moves around positions of visible characters
361365
// relative to the editable block content stored in the QTextDocument, so
362366
// we need to save an offset mapping.
363367

364368
auto& offsets = blockData->displayOffsets;
365-
offsets.resize(text.size());
369+
offsets.resize(blockText.size());
366370
offsets.fill(0);
367371

368372
QMap<int, int> isolatesStartingAt;
@@ -379,8 +383,8 @@ static void buildDisplayLayout(const QTextBlock& block, LayoutBlockData* blockDa
379383
int start = static_cast<int>(isolate.startPos);
380384
int end = static_cast<int>(isolate.endPos);
381385

382-
text.insert(start + offsets[start], startChar);
383-
text.insert(end + offsets[end] + 2, utils::PDI_MARK);
386+
blockText.insert(start + offsets[start], startChar);
387+
blockText.insert(end + offsets[end] + 2, utils::PDI_MARK);
384388

385389
for (int i = start; i <= end; i++) {
386390
offsets[i] += 1;
@@ -414,6 +418,7 @@ static void buildDisplayLayout(const QTextBlock& block, LayoutBlockData* blockDa
414418
// for each control char we injected that prevents font merging.
415419
QTextLayout::FormatRange r;
416420
r.format.setFontStyleStrategy(QFont::NoFontMerging);
421+
r.format.setFontPointSize(0.01);
417422

418423
for (const auto& [pos, count] : isolatesStartingAt.asKeyValueRange()) {
419424
r.start = pos + offsets[pos] - count;
@@ -426,7 +431,7 @@ static void buildDisplayLayout(const QTextBlock& block, LayoutBlockData* blockDa
426431
formats.append(r);
427432
}
428433

429-
blockData->displayLayout = std::make_unique<QTextLayout>(text);
434+
blockData->displayLayout = std::make_unique<QTextLayout>(blockText);
430435
blockData->displayLayout->setFormats(formats);
431436
}
432437

@@ -438,24 +443,28 @@ void EditorLayout::layoutBlock(QTextBlock& block, qreal topY)
438443
BlockData::set<LayoutBlockData>(block, blockData);
439444
}
440445

441-
Qt::LayoutDirection dir = getBlockDirection(block);
446+
QString blockText = block.text();
447+
Qt::LayoutDirection dir = getBlockDirection(block, blockText);
442448

443449
QTextOption option = document()->defaultTextOption();
444450
option.setTextDirection(dir);
445451
option.setAlignment(QStyle::visualAlignment(dir, option.alignment()));
446452

453+
bool inContent = d_codeModel->canStartWithListItem(block);
454+
qreal wrappingIndentWidth = calculateIndentWidth(blockText, option, inContent);
455+
447456
QTextLayout* defaultLayout = block.layout();
448-
doBlockLayout(defaultLayout, option, topY);
457+
doBlockLayout(defaultLayout, option, wrappingIndentWidth, topY);
449458
block.setLineCount(defaultLayout->lineCount());
450459

451-
buildDisplayLayout(block, blockData);
460+
buildDisplayLayout(block, blockText, blockData);
452461

453462
QTextLayout* displayLayout = blockData->displayLayout.get();
454463
if (displayLayout != nullptr) {
455464
displayLayout->setFont(document()->defaultFont());
456465
displayLayout->setCursorMoveStyle(document()->defaultCursorMoveStyle());
457466

458-
doBlockLayout(displayLayout, option, topY);
467+
doBlockLayout(displayLayout, option, wrappingIndentWidth, topY);
459468

460469
if (displayLayout->boundingRect() != defaultLayout->boundingRect()) {
461470
qWarning() << "Block" << block.blockNumber() << "display bounding rect differs from default one!"
@@ -464,7 +473,11 @@ void EditorLayout::layoutBlock(QTextBlock& block, qreal topY)
464473
}
465474
}
466475

467-
void EditorLayout::doBlockLayout(QTextLayout* layout, const QTextOption& option, qreal topY)
476+
void EditorLayout::doBlockLayout(
477+
QTextLayout* layout,
478+
const QTextOption& option,
479+
qreal wrappingIndentWidth,
480+
qreal topY)
468481
{
469482
qreal margin = document()->documentMargin();
470483
qreal availableWidth = document()->textWidth() - 2 * margin;
@@ -473,6 +486,7 @@ void EditorLayout::doBlockLayout(QTextLayout* layout, const QTextOption& option,
473486
layout->beginLayout();
474487

475488
qreal lineHeight = 0;
489+
bool first = true;
476490

477491
while (true) {
478492
QTextLine line = layout->createLine();
@@ -481,8 +495,25 @@ void EditorLayout::doBlockLayout(QTextLayout* layout, const QTextOption& option,
481495
}
482496

483497
line.setLeadingIncluded(true);
484-
line.setLineWidth(availableWidth);
485-
line.setPosition(QPointF(0, lineHeight));
498+
499+
if (first) {
500+
line.setLineWidth(availableWidth);
501+
line.setPosition(QPointF(0, lineHeight));
502+
first = false;
503+
}
504+
else {
505+
qreal restrictedWidth = qMax(
506+
availableWidth - wrappingIndentWidth,
507+
0.2 * availableWidth);
508+
509+
line.setLineWidth(restrictedWidth);
510+
if (option.textDirection() == Qt::RightToLeft) {
511+
line.setPosition(QPointF(0, lineHeight));
512+
}
513+
else {
514+
line.setPosition(QPointF(availableWidth - restrictedWidth, lineHeight));
515+
}
516+
}
486517

487518
lineHeight += line.height();
488519
}
@@ -491,9 +522,9 @@ void EditorLayout::doBlockLayout(QTextLayout* layout, const QTextOption& option,
491522
layout->endLayout();
492523
}
493524

494-
Qt::LayoutDirection EditorLayout::getBlockDirection(const QTextBlock& block)
525+
Qt::LayoutDirection EditorLayout::getBlockDirection(const QTextBlock& block, const QString& blockText)
495526
{
496-
Qt::LayoutDirection dir = utils::naturalTextDirection(block.text());
527+
Qt::LayoutDirection dir = utils::naturalTextDirection(blockText);
497528
if (dir != Qt::LayoutDirectionAuto) {
498529
return dir;
499530
}
@@ -520,6 +551,39 @@ Qt::LayoutDirection EditorLayout::getBlockDirection(const QTextBlock& block)
520551
return qGuiApp->layoutDirection();
521552
}
522553

554+
qreal EditorLayout::calculateIndentWidth(const QString& text, const QTextOption& option, bool inContent)
555+
{
556+
qreal controlCharsWidth = 0;
557+
QFontMetricsF ccFontMetrics { QFont(utils::CONTROL_FONT_FAMILY) };
558+
559+
QString prefix;
560+
prefix.reserve(qRound(text.size() * 0.2));
561+
562+
for (QChar ch : text) {
563+
if (utils::isWhitespace(ch)) {
564+
if (utils::isSingleBidiMark(ch)) {
565+
#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
566+
if (option.flags() & QTextOption::ShowDefaultIgnorables) {
567+
controlCharsWidth += ccFontMetrics.horizontalAdvance(ch);
568+
}
569+
#endif
570+
}
571+
else {
572+
prefix.append(ch);
573+
}
574+
}
575+
else if (inContent && (ch == '-' || ch == '+')) {
576+
prefix.append(ch);
577+
}
578+
else {
579+
break;
580+
}
581+
}
582+
583+
QFontMetricsF metrics { document()->defaultFont() };
584+
return metrics.horizontalAdvance(prefix, option) + controlCharsWidth;
585+
}
586+
523587
QTextBlock EditorLayout::findContainingBlock(qreal y) const
524588
{
525589
// Skip top margin

core/katvan_editorlayout.h

+3-2
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,10 @@ class EditorLayout : public QAbstractTextDocumentLayout
4949

5050
private:
5151
void layoutBlock(QTextBlock& block, qreal topY);
52-
void doBlockLayout(QTextLayout* layout, const QTextOption& option, qreal topY);
52+
void doBlockLayout(QTextLayout* layout, const QTextOption& option, qreal wrappingIndentWidth, qreal topY);
53+
Qt::LayoutDirection getBlockDirection(const QTextBlock& block, const QString& blockText);
54+
qreal calculateIndentWidth(const QString& text, const QTextOption& option, bool inContent);
5355
void recalculateDocumentSize();
54-
Qt::LayoutDirection getBlockDirection(const QTextBlock& block);
5556

5657
CodeModel* d_codeModel;
5758

core/katvan_highlighter.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ void Highlighter::doShowControlChars(
167167
QList<QTextCharFormat>& charFormats)
168168
{
169169
QTextCharFormat controlCharFormat;
170-
controlCharFormat.setFontFamilies(QStringList() << "KatvanControl");
170+
controlCharFormat.setFontFamilies(QStringList() << utils::CONTROL_FONT_FAMILY);
171171

172172
for (qsizetype i = 0; i < text.size(); i++) {
173173
if (utils::isBidiControlChar(text[i])) {

core/katvan_text_utils.cpp

+19
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
#include <QPainter>
2323
#include <QPalette>
2424

25+
#include <algorithm>
26+
2527
namespace katvan::utils {
2628

2729
bool isBidiControlChar(QChar ch)
@@ -35,6 +37,23 @@ bool isBidiControlChar(QChar ch)
3537
|| ch == PDI_MARK;
3638
}
3739

40+
bool isSingleBidiMark(QChar ch)
41+
{
42+
return ch == utils::LRM_MARK
43+
|| ch == utils::RLM_MARK
44+
|| ch == utils::ALM_MARK;
45+
}
46+
47+
bool isWhitespace(QChar ch)
48+
{
49+
return ch.category() == QChar::Separator_Space || ch == QLatin1Char('\t') || isSingleBidiMark(ch);
50+
}
51+
52+
bool isAllWhitespace(const QString& text)
53+
{
54+
return std::all_of(text.begin(), text.end(), isWhitespace);
55+
}
56+
3857
Qt::LayoutDirection naturalTextDirection(const QString& text)
3958
{
4059
int count = 0;

core/katvan_text_utils.h

+5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
namespace katvan::utils {
2424

25+
static constexpr QLatin1StringView CONTROL_FONT_FAMILY = QLatin1StringView("KatvanControl");
26+
2527
static constexpr QChar ALM_MARK = (ushort)0x061c;
2628
static constexpr QChar LRM_MARK = (ushort)0x200e;
2729
static constexpr QChar RLM_MARK = (ushort)0x200f;
@@ -31,6 +33,9 @@ static constexpr QChar FSI_MARK = (ushort)0x2068;
3133
static constexpr QChar PDI_MARK = (ushort)0x2069;
3234

3335
bool isBidiControlChar(QChar ch);
36+
bool isSingleBidiMark(QChar ch);
37+
bool isWhitespace(QChar ch);
38+
bool isAllWhitespace(const QString& text);
3439

3540
Qt::LayoutDirection naturalTextDirection(const QString& text);
3641

0 commit comments

Comments
 (0)