SynEdit: WrappedView, caret calculations for RTL/LTR text

This commit is contained in:
Martin 2025-02-15 15:55:02 +01:00
parent 6f2525c144
commit 5b080436ce
2 changed files with 303 additions and 48 deletions

View File

@ -234,13 +234,18 @@ public
function GetSublineCount (ALine: String; AMaxWidth: Integer; const APhysCharWidths: TPhysicalCharWidths): Integer; inline;
procedure GetSublineBounds(ALine: String; AMaxWidth, AWrapIndent: Integer;
const APhysCharWidths: TPhysicalCharWidths; ASubLine: Integer; out ALogStartX,
ANextLogStartX, APhysStart: IntIdx; out APhysWidth: integer);
ANextLogStartX, APhysStart: IntIdx; out APhysWidth: integer); // APhysStart is based on LTR only
procedure GetSublineBounds(ALine: String; AMaxWidth, AWrapIndent: Integer; const APhysCharWidths: TPhysicalCharWidths;
ASubLine: Integer;
out ALogStartX, ANextLogStartX, APhysStart: IntIdx; out APhysWidth: integer;
var AViewedX: integer;
out APhysX: integer);
function GetSubLineFromX (ALine: String; AMaxWidth, AWrapIndent: Integer; const APhysCharWidths: TPhysicalCharWidths; var APhysToViewedXPos: Integer): integer;
procedure GetWrapInfoForViewedXY(var AViewedXY: TViewedPoint; AFlags: TViewedXYInfoFlags; out AFirstViewedX: IntPos; ALogPhysConvertor: TSynLogicalPhysicalConvertor);
function TextXYToLineXY(ATextXY: TPhysPoint): TViewedSubPoint_0;
function LineXYToTextX(ARealLine: IntPos; ALineXY: TViewedSubPoint_0): Integer;
function ViewedSubLineXYToTextX(ARealLine: IntPos; ALineXY: TViewedSubPoint_0): Integer;
function CalculateWrapForLine(ALineIdx: IntIdx; AMaxWidth: integer): Integer; inline;
public
constructor Create(AOwner: TComponent); override;
@ -1296,7 +1301,7 @@ begin
Result := inherited ViewXYIdxToTextXYIdx(AViewXYIdx, ANodeStartLine);
Result.y := ANodeStartLine + GetOffsetForWrap(Result.y - ANodeStartLine, SubOffset);
Result.x := FSynEditWrappedPlugin.LineXYToTextX(Result.y, Point(Result.x, SubOffset) );
Result.x := FSynEditWrappedPlugin.ViewedSubLineXYToTextX(Result.y, Point(Result.x, SubOffset) );
end;
procedure TSynWordWrapIndexPage.InvalidateLines(AFromOffset, AToOffset: Integer);
@ -1866,6 +1871,35 @@ begin
end;
end;
function FindPreviousCharStart(const APhysCharWidths: TPhysicalCharWidths; AnX: integer): Integer; inline;
begin
assert(AnX > 0, 'FindPreviousCharStart: AnX > 0');
Result := AnX - 1;
while (Result >= 0) and (APhysCharWidths[Result] = 0) do
dec(Result);
end;
function CalculateRtlLead(const APhysCharWidths: TPhysicalCharWidths; AnX: integer): Integer; //inline;
begin
Result := 0;
while (AnX >= 0) and
( (APhysCharWidths[AnX] = 0) or ((APhysCharWidths[AnX] and PCWFlagRTL) <> 0) )
do begin
Result := Result + (APhysCharWidths[AnX] and PCWMask);
dec(AnX);
end;
end;
function CalculateRtlRemainder(const APhysCharWidths: TPhysicalCharWidths; ALineLen, AnX: integer): Integer; //inline;
begin
Result := 0;
while (AnX < ALineLen) and
( (APhysCharWidths[AnX] = 0) or ((APhysCharWidths[AnX] and PCWFlagRTL) <> 0) )
do begin
Result := Result + (APhysCharWidths[AnX] and PCWMask);
inc(AnX);
end;
end;
procedure TLazSynEditLineWrapPlugin.GetSublineBounds(ALine: String; AMaxWidth,
AWrapIndent: Integer; const APhysCharWidths: TPhysicalCharWidths; ASubLine: Integer; out
ALogStartX, ANextLogStartX, APhysStart: IntIdx; out APhysWidth: integer);
@ -1887,16 +1921,71 @@ begin
end;
end;
procedure TLazSynEditLineWrapPlugin.GetSublineBounds(ALine: String; AMaxWidth,
AWrapIndent: Integer; const APhysCharWidths: TPhysicalCharWidths; ASubLine: Integer; out
ALogStartX, ANextLogStartX, APhysStart: IntIdx; out APhysWidth: integer; var AViewedX: integer;
out APhysX: integer);
var
x, RtlRemainderWidth, RtlLeadWitdh: Integer;
begin
GetSublineBounds(ALine, AMaxWidth, AWrapIndent, APhysCharWidths, ASubLine, ALogStartX, ANextLogStartX, APhysStart, APhysWidth);
if (ASubLine > 0) then begin
AViewedX := max(1, AViewedX - AWrapIndent);
if (AViewedX = 1) and (FCaretWrapPos = wcpEOL) then
AViewedX := 2;
end;
if (ANextLogStartX > 0) and (ANextLogStartX < Length(ALine)) then begin
x := APhysWidth;
if FCaretWrapPos = wcpEOL then
inc(x);
if AViewedX > x then
AViewedX := x;
end;
APhysX := AViewedX;
if (ALogStartX > 0) and ((APhysCharWidths[ALogStartX] and PCWFlagRTL) <> 0) then begin
x := FindPreviousCharStart(APhysCharWidths, ALogStartX);
if (APhysCharWidths[x] and PCWFlagRTL) <> 0 then begin
RtlRemainderWidth := CalculateRtlRemainder(APhysCharWidths, Length(ALine), ALogStartX);
if APhysX < RtlRemainderWidth then begin
RtlLeadWitdh := CalculateRtlLead(APhysCharWidths, x);
APhysX := APhysX
+ (APhysStart - RtlLeadWitdh)
+ Max(0,RtlRemainderWidth - APhysWidth);
exit;
end;
end;
end;
if (ANextLogStartX > 0) and (ANextLogStartX < Length(ALine)) and ((APhysCharWidths[ANextLogStartX] and PCWFlagRTL) <> 0) then begin
x := FindPreviousCharStart(APhysCharWidths, ANextLogStartX);
RtlLeadWitdh := CalculateRtlLead(APhysCharWidths, x);
assert(APhysWidth >= RtlLeadWitdh, 'TLazSynEditLineWrapPlugin.GetSublineBounds: APhysWidth > RtlLeadWitdh');
if APhysX - 1 > APhysWidth - RtlLeadWitdh then begin
RtlRemainderWidth := CalculateRtlRemainder(APhysCharWidths, Length(ALine), ANextLogStartX);
APhysX := APhysX + APhysStart + RtlRemainderWidth;
exit;
end;
end;
APhysX := APhysX + APhysStart;
end;
function TLazSynEditLineWrapPlugin.GetSubLineFromX(ALine: String; AMaxWidth, AWrapIndent: Integer;
const APhysCharWidths: TPhysicalCharWidths; var APhysToViewedXPos: Integer): integer;
var
x, PhysWidth, AMaxWidth2: Integer;
x, PhysWidth, AMaxWidth2, x2: Integer;
RtlLeadWitdh, RtlRemainderWidth, PhysToViewedInRTLOffs, LtrLead: integer;
begin
Result := 0;
if Length(ALine) = 0 then
exit;
Result := -1;
x := 0;
RtlRemainderWidth := 0;
PhysToViewedInRTLOffs := 0;
AMaxWidth2 := AMaxWidth - AWrapIndent;
assert(AMaxWidth2>0, 'TLazSynEditLineWrapPlugin.GetSublineCount: AMaxWidth>0');
@ -1906,17 +1995,56 @@ begin
inc(Result);
x := CalculateNextBreak(PChar(ALine), x, AMaxWidth, APhysCharWidths, PhysWidth);
AMaxWidth := AMaxWidth2;
if x >= Length(ALine) then
break;
if (FCaretWrapPos = wcpBOL) and (PhysWidth = APhysToViewedXPos) and (x < Length(ALine))
then begin
inc(Result);
APhysToViewedXPos := APhysToViewedXPos - PhysWidth;
if (x >= Length(ALine)) and (PhysToViewedInRTLOffs = 0) then
break;
if RtlRemainderWidth > 0 then begin
LtrLead := 0;
RtlRemainderWidth := Max(0, RtlRemainderWidth - PhysWidth);
end;
if (RtlRemainderWidth = 0) and
(PhysToViewedInRTLOffs = 0) and
(x > 0) and ((APhysCharWidths[x] and PCWFlagRTL) <> 0) then begin
x2 := FindPreviousCharStart(APhysCharWidths, x);
if ((APhysCharWidths[x2] and PCWFlagRTL) <> 0) then begin
// IN RTL run
RtlLeadWitdh := CalculateRtlLead(APhysCharWidths, x2);
assert(RtlLeadWitdh <= PhysWidth, 'TLazSynEditLineWrapPlugin.GetSubLineFromX: RtlLeadWitdh <= PhysWidth');
if (APhysToViewedXPos <= PhysWidth - RtlLeadWitdh) then
break;
RtlRemainderWidth := CalculateRtlRemainder(APhysCharWidths, Length(ALine), x);
LtrLead := PhysWidth - RtlLeadWitdh;
PhysToViewedInRTLOffs := APhysToViewedXPos - LtrLead;
if PhysToViewedInRTLOffs >= RtlLeadWitdh + RtlRemainderWidth then //NOT_IN_THIS_RUN;
PhysToViewedInRTLOffs := 0;
end;
end;
if (PhysToViewedInRTLOffs > 0) then begin
if (PhysToViewedInRTLOffs > RtlRemainderWidth) or
( (FCaretWrapPos = wcpBOL) and (PhysToViewedInRTLOffs = RtlRemainderWidth) )
then begin // within RtlLeadWitdh on the right side
//APhysToViewedXPos := APhysToViewedXPos - RtlRemainderWidth;
APhysToViewedXPos := LtrLead + PhysToViewedInRTLOffs - RtlRemainderWidth;
break;
end;
end
else begin
if (FCaretWrapPos = wcpBOL) and (PhysWidth = APhysToViewedXPos) and (x < Length(ALine))
then begin
inc(Result);
APhysToViewedXPos := APhysToViewedXPos - PhysWidth;
break;
end;
if PhysWidth >= APhysToViewedXPos then
break;
APhysToViewedXPos := APhysToViewedXPos - PhysWidth;
end;
if PhysWidth >= APhysToViewedXPos then
break;
APhysToViewedXPos := APhysToViewedXPos - PhysWidth;
end;
APhysToViewedXPos := ToPos(APhysToViewedXPos);
end;
@ -1928,8 +2056,8 @@ var
SubLineOffset, YIdx: TLineIdx;
LineTxt: String;
PWidth: TPhysicalCharWidths;
LogX, NextLogX, PhysX: IntIdx;
PhysWidth, WrapInd, AMaxWidth: Integer;
BoundLogX, NextBoundLogX, BoundPhysX: IntIdx;
PhysWidth, WrapInd, AMaxWidth, PhysXPos: Integer;
begin
YIdx := FLineMapView.Tree.GetLineForForWrap(ToIdx(AViewedXY.y), SubLineOffset);
@ -1941,26 +2069,19 @@ begin
AMaxWidth := WrapColumn;
WrapInd := CalculateIndentFor(PChar(LineTxt), AMaxWidth, PWidth);
GetSublineBounds(LineTxt, AMaxWidth, WrapInd, PWidth, SubLineOffset, LogX, NextLogX, PhysX, PhysWidth);
GetSublineBounds(LineTxt, AMaxWidth, WrapInd, PWidth, SubLineOffset, BoundLogX, NextBoundLogX, BoundPhysX, PhysWidth, AViewedXY.x, PhysXPos);
case CaretWrapPos of
wcpEOL: begin
if (SubLineOffset > 0) and (AViewedXY.x <= 1) then
AViewedXY.x := 2
else
if (NextLogX < length(LineTxt)) and (AViewedXY.x > ToPos(PhysWidth)) then
AViewedXY.x := ToPos(PhysWidth);
AFirstViewedX := 2;
end;
wcpBOL: begin
if (NextLogX < length(LineTxt)) and (AViewedXY.x >= ToPos(PhysWidth)) then
AViewedXY.x := ToPos(PhysWidth) - 1;
AFirstViewedX := 1;
end;
end;
AViewedXY.x := PhysXPos;
AViewedXY.y := ToPos(YIdx);
AViewedXY.x := AViewedXY.x + PhysX;
end;
function TLazSynEditLineWrapPlugin.TextXYToLineXY(ATextXY: TPhysPoint
@ -1978,17 +2099,18 @@ begin
Result.x := ATextXY.x;
Result.y :=
GetSubLineFromX(ALine, AMaxWidth, WrapInd, APhysCharWidths, Result.x);
if Result.x <> ATextXY.x then
//if Result.x <> ATextXY.x then
if Result.y > 0 then
Result.x := Result.x + WrapInd; // Result is on sub-line
end;
function TLazSynEditLineWrapPlugin.LineXYToTextX(ARealLine: IntPos;
function TLazSynEditLineWrapPlugin.ViewedSubLineXYToTextX(ARealLine: IntPos;
ALineXY: TViewedSubPoint_0): Integer;
var
AMaxWidth, WrapInd, ANextLogX, APhysWidth: Integer;
ALine: String;
APhysCharWidths: TPhysicalCharWidths;
dummy: integer;
dummy, PhysXPos: integer;
begin
FLineMapView.LogPhysConvertor.CurrentLine := ARealLine;
ALine := FLineMapView.NextLines.Strings[ARealLine];
@ -1996,22 +2118,8 @@ begin
AMaxWidth := WrapColumn;
WrapInd := CalculateIndentFor(PChar(ALine), AMaxWidth, APhysCharWidths);
GetSublineBounds(ALine, AMaxWidth, WrapInd, APhysCharWidths, ALineXY.y, dummy, ANextLogX, Result, APhysWidth);
if (ALineXY.y > 0) then
ALineXY.x := max(1, ALineXY.x -WrapInd);
if (ALineXY.y > 0) and (ALineXY.X <= 1) and (FCaretWrapPos = wcpEOL) then begin
ALineXY.x := 2;
end
else
if (ANextLogX > 0) and (ANextLogX < Length(ALine)) then begin
if FCaretWrapPos = wcpEOL then
inc(APhysWidth);
if ALineXY.X > APhysWidth then
ALineXY.x := APhysWidth;
end;
Result := Result + ALineXY.x;
GetSublineBounds(ALine, AMaxWidth, WrapInd, APhysCharWidths, ALineXY.y, dummy, ANextLogX, Result, APhysWidth, ALineXY.x, PhysXPos);
Result := PhysXPos;
end;
function TLazSynEditLineWrapPlugin.CalculateWrapForLine(ALineIdx: IntIdx;

View File

@ -2014,15 +2014,36 @@ begin
SynEdit.TabWidth := 4;
SetLines([
// 0
'abc def ' + 'ABC DEFG ' + 'XYZ',
//'A' #9'B' #9'C ' + 'DEF G'#9'H' #9 + '' #9 #9'xy',
'A'#9'B'#9'C ' + 'DEF G'#9'H'#9 + #9#9'xy',
'äää ööö ' + 'ÄÄÄ ÖÖÖ ' + 'ÜÜÜ',
'999'
'999',
// 4
'A نمت X',
'A نمت X Foo',
// 6
'Abc d نمت Foo',
'Abc de نمت Foo',
'Abcde def نمت Foo',
'Abcde def نمت Foo',
// 10
'Abcde نمت نمت Foo',
'Abc de نمت نمت Foo',
'Abc de نمت نمت F نمت نمت Foo',
'Abc de نمت نمت نمت نمت Foo',
// 13
'نمت) نمت نمت 789 123 نمت', // digits are weak LTR/RTL
''
]);
SetSynEditWidth(10);
CheckLines('', 0, [
// 0 // Virt = 0
'abc def ',
'ABC DEFG ',
'XYZ',
@ -2032,7 +2053,41 @@ begin
'äää ööö ',
'ÄÄÄ ÖÖÖ ',
'ÜÜÜ',
'999'
'999',
// 4 // Virt= 10
'A نمت X',
'A نمت X ',
'Foo',
// 6 // Virt=13
'Abc d نمت ',
'Foo',
'Abc de نمت',
' Foo',
'Abcde def ',
'نمت Foo',
'Abcde def',
' نمت Foo',
// 10 // Virt = 21
'Abcde نمت ',
'نمت Foo',
'Abc de نمت',
' نمت Foo',
'Abc de نمت',
' نمت F نمت'
,' نمت Foo',
'Abc de نمت',
' نمت نمت '
,'نمت Foo',
// 14 // Virt = 31
'نمت) نمت ',
'نمت 789 ',
'123 نمت',
''
], True);
CheckLines('', ViewedExp(0, [
@ -2045,7 +2100,41 @@ begin
l(2, 0, 'äää ööö '),
l(2, 1, 'ÄÄÄ ÖÖÖ '),
l(2, 2, 'ÜÜÜ'),
l(3, 0, '999')
l(3, 0, '999'),
// 4 // Virt= 10
l(4, 0, 'A نمت X'),
l(5, 0, 'A نمت X '),
l(5, 1, 'Foo'),
// 6 // Virt=13
l(6, 0, 'Abc d نمت '),
l(6, 1, 'Foo'),
l(7, 0, 'Abc de نمت'),
l(7, 1, ' Foo'),
l(8, 0, 'Abcde def '),
l(8, 1, 'نمت Foo'),
l(9, 0, 'Abcde def'),
l(9, 1, ' نمت Foo'),
// 10 // Virt = 21
l(10, 0, 'Abcde نمت '),
l(10, 1, 'نمت Foo'),
l(11, 0, 'Abc de نمت'),
l(11, 1, ' نمت Foo'),
l(12, 0, 'Abc de نمت'),
l(12, 1, ' نمت F نمت'),
l(12, 2, ' نمت Foo'),
l(13, 0, 'Abc de نمت'),
l(13, 1, ' نمت نمت '),
l(13, 2, 'نمت Foo'),
// 14 // Virt = 31
l(14, 0, 'نمت) نمت '),
l(14, 1, 'نمت 789 '),
l(14, 2, '123 نمت'),
l(15, 0, '')
], tTrue));
@ -2224,6 +2313,64 @@ begin
CheckXyMap('wcpBOL', p( 2,8, 10,3, 17,3)); // after "Ä"
CheckXyMap('wcpBOL', p( 1,9, 17,3, 29,3)); // after " "
//FWordWrap.CaretWrapPos := wcpBOL;
FWordWrap.CaretWrapPos := wcpEOL;
// viewed phys log x,y
// line 5 (idx=4)
CheckXyMap('RTL', p( 2,11, 2,5, 2,5)); // after A
CheckXyMap('RTL', p( 3,11, 3,5, 3,5)); // after A space // befare first RTL / could be PX = 6
CheckXyMap('RTL', p( 5,11, 5,5, 5,5)); // after first RTL
CheckXyMap('RTL', p( 4,11, 4,5, 7,5)); // after 2nd RTL
CheckXyMap('RTL', p( 6,11, 6,5, 9,5)); // after 3rd RTL // before space / could be PX=3
CheckXyMap('RTL', p( 7,11, 7,5, 10,5)); // after 2nd space
CheckXyMap('RTL', p( 8,11, 8,5, 11,5)); // after X
// line 6 (idx=5)
CheckXyMap('RTL', p( 8,12, 8,6, 11,6)); // after X
CheckXyMap('RTL', p( 9,12, 9,6, 12,6)); // after X space
CheckXyMap('RTL', p( 2,13, 10,6, 13,6)); // after F of Foo
// line 7 (idx=6)
CheckXyMap('RTL', p(11,14, 11,7, 14,7)); // after last space in first line
CheckXyMap('RTL', p( 2,15, 12,7, 15,7)); // after F of Foo (subline)
// line 8 (idx=7)
CheckXyMap('RTL', p(11,16, 11,8, 14,8)); // after last RTL
CheckXyMap('RTL', p( 2,17, 12,8, 15,8)); // after space, before Foo (subline)
// line 9 (idx=8)
FWordWrap.CaretWrapPos := wcpBOL;
CheckXyMap('RTL BOL', p( 1,19, 11,9, 11,9)); // subline at start (before first RTL)
CheckXyMap('RTL BOL', p( 3,19, 13,9, 13,9)); // subline (after first RTL)
CheckXyMap('RTL BOL', p( 2,19, 12,9, 15,9)); // subline (after 2nd RTL)
FWordWrap.CaretWrapPos := wcpEOL;
CheckXyMap('RTL', p(11,18, 11,9, 11,9)); // at end of first line
CheckXyMap('RTL', p( 3,19, 13,9, 13,9)); // subline (after first RTL)
CheckXyMap('RTL', p( 2,19, 12,9, 15,9)); // subline (after 2nd RTL)
// line 10 (idx=9) / Virt 20
// line 11 (idx=10) / Virt 22
(* The space at the end of the first line is the space between the 2 RTL sequences.
(the space is part of the overall RTL sequence)
That space is therefore displayed at the left end of the RTL run.
I.e. On the screen it is right after the normal space from "ABCDE "
The RTL sequence in the first line is the sequence from the right of the RTL run
*)
FWordWrap.CaretWrapPos := wcpEOL;
CheckXyMap('RTL', p(10,22, 13,11, 9,11)); // after first RTL (first line)
CheckXyMap('RTL', p( 9,22, 12,11, 11,11)); // after 2nd RTL (first line)
CheckXyMap('RTL', p( 8,22, 11,11, 13,11)); // after 3rd / before space
// The viewed-x is between the leading LTR and the RTL text. So it can not be distinguished
// CheckXyMap('RTL', p( 7,22, 10,11, 14,11)); // after space //???????????? TEST BOL <> EOL
// same at the end
// CheckXyMap('RTL', p( 3,23, 9,11, 16,11)); // 1st subline: after 1st RTL (2nd sequence)
CheckXyMap('RTL', p( 2,23, 8,11, 18,11)); // 1st subline: after 2nd RTL (2nd sequence)
CheckXyMap('RTL', p( 4,23, 14,11, 20,11)); // 1st subline: after 3rd RTL (2nd sequence) => go to LTR sequence
end;
@ -2234,8 +2381,8 @@ begin
SynEdit.ExecuteCommand(ecEditorBottom, '', nil);
AssertEquals('ecEditorBottom', 4 ,SynEdit.CaretY);
AssertEquals('ecEditorBottom', 10 ,SynEdit.CaretObj.ViewedLineCharPos.y);
AssertEquals('ecEditorBottom', 15 ,SynEdit.CaretY);
AssertEquals('ecEditorBottom', 34 ,SynEdit.CaretObj.ViewedLineCharPos.y);
SetSynEditWidth(65);
AddLines(0, 6000, 60, 'A');