-- file: IntTOC.mesa
-- last edited by Brotz, June 1, 1982 4:28 PM
-- last edited by Taft, May 9, 1983 10:56 AM

DIRECTORY
Ascii USING [CR, SP, TAB],
dsD: FROM "DisplayDefs" USING [BitBltFunction, BlackenRectangle, ChangeCursor,
ClearRectangle, CursorShape, erase, GetCharRightX, GetCursor, GetPictureParameters,
lineHeight, PaintPicture, PutCharInBitMap, PutStringInBitMap, replace, ScreenXCoord,
ScreenYCoord, SlideFullWidthRectangleVertically],
Editor USING [nextCode],
exD: FROM "ExceptionDefs" USING [SysBug],
inD: FROM "InteractorDefs" USING [consideredLeftX, digitWidth, leftMargin,
LinePair, LinePtr, markLeftX, maxLinesPerTOCEntry, NbrPtr, numberLeftX, rightMargin,
ScreenXCoord, ScreenYCoord, ThumbLineNbrPtr, TOCChangeAction, TOCLineNumber,
TOCTextNbrPtr, UpdateTOCThumbLine],
Inline USING [LongDiv, LongMult],
intCommon USING [dateLeftX, dateRightX, fromLeftX, fromRightX, source,
subjectExtensionLeftX, subjectLeftX, tocTextNbr],
opD: FROM "OperationsDefs" USING [maxTOCStringLength, substringSeparator],
String USING [AppendDecimal],
tsD: FROM "TOCSelectionDefs" USING [AddRange, ConsiderAll, IsSelected,
LastSelectedEntry, NextSelectedEntry, PrevSelectedEntry, RemoveRange],
vmD: FROM "VirtualMgrDefs" USING [FirstFreeTOCIndex, GetTOCFixedPart, GetTOCString,
TOCFixedPart, TOCHandle, TOCIndex, UnlockTOC, WaitForLock];

IntTOC: PROGRAM
IMPORTS dsD, exD, inD, Inline, intC: intCommon, String, tsD, vmD
EXPORTS inD =

BEGIN
OPEN inD;

-- Purpose: handles user interactions including the display, keyboard and mouse. This
-- division gathers together commands and their arguments and is responsible for the
-- display of all error messages.


DisplayTOCTail: PUBLIC PROCEDURE [tnp: TOCTextNbrPtr, key: CARDINAL, line: LinePtr,
index: vmD.TOCIndex, lineNum: TOCLineNumber] =
-- Paints the rest of the TOC neighborhood from line to the bottom. The first text displayed
-- is lineNum of index. Displays End of messages if necessary.
BEGIN
firstFree, i: vmD.TOCIndex;
nLines: CARDINAL;
lastLineOnScreen: LinePtr;
toc: vmD.TOCHandle ← tnp.toc;

IF ~tnp.haveToc THEN RETURN;
firstFree ← vmD.FirstFreeTOCIndex[toc, key];
IF tnp.lines = tnp.firstLineOffScreen THEN
BEGIN
IF index < firstFree THEN {tnp.lines.state ← index; tnp.lines.linePair ← LinePair[index, 1]}
ELSE {tnp.lines.state ← end; tnp.lines.linePair ← LinePair[firstFree, 1]};
RETURN;
END;
IF index < firstFree THEN
[line, nLines] ← DisplayTOCEntry[tnp, key, index, lineNum - 1, line];
FOR i IN (index .. firstFree) UNTIL line = tnp.firstLineOffScreen DO
[line, nLines] ← DisplayTOCEntry[tnp, key, i, 0, line];
ENDLOOP;
IF line = tnp.firstLineOffScreen THEN
BEGIN
FOR lastLineOnScreen ← tnp.lines, lastLineOnScreen.nextLine
UNTIL lastLineOnScreen.nextLine = tnp.firstLineOffScreen DO ENDLOOP;
IF lastLineOnScreen.linePair.index + 1 = firstFree
AND lastLineOnScreen.linePair.lineNumber = nLines THEN
BEGIN
tnp.firstLineOffScreen.state ← end;
tnp.firstLineOffScreen.linePair ← LinePair[firstFree, 1];
END
ELSE BEGIN
tnp.firstLineOffScreen.state ← index;
IF lastLineOnScreen.linePair.lineNumber = nLines THEN
tnp.firstLineOffScreen.linePair ← LinePair[lastLineOnScreen.linePair.index + 1, 1]
ELSE tnp.firstLineOffScreen.linePair
← LinePair[lastLineOnScreen.linePair.index, lastLineOnScreen.linePair.lineNumber + 1]
END;
END
ELSE BEGIN
line.state ← end;
line.linePair ← LinePair[firstFree, 1];
[] ← dsD.PutStringInBitMap[markLeftX, line.y, "End of Messages."L, italicFace];
FOR line ← line.nextLine, line.nextLine UNTIL line = NIL DO
line.state ← empty;
line.linePair ← LinePair[firstFree, 1];
ENDLOOP;
END;
tsD.ConsiderAll[tnp, key];
END; -- of DisplayTOCTail --


DisplayTOCEntry: PUBLIC PROCEDURE [tnp: TOCTextNbrPtr, key: CARDINAL,
index: vmD.TOCIndex, suppressed: CARDINAL, firstLine: LinePtr]
RETURNS [firstUnusedLine: LinePtr, nLines: CARDINAL] =
-- Processes the index’th TOCEntry, displaying it under certain circumstances: suppressed
-- gives number of lines of the TOCEntry to skip before displaying the rest of the
-- TOCEntry. firstLine points to the Line descriptor for the first line to be used.
-- DisplayTOCEntry stops displaying when either it has displayed the entire entry or there
-- is no more room in the TOC text region. In either case, it returns the total number of
-- lines (including suppressed lines) that this TOCEntry requires and the first unused line.
BEGIN
firstFreeX, subjectLeftX, subjectExtensionLeftX: ScreenXCoord;
y: ScreenYCoord;
tocFixedPart: vmD.TOCFixedPart;
tocString: STRING ← [opD.maxTOCStringLength];
stringIndex: CARDINAL ← 0;
toc: vmD.TOCHandle ← tnp.toc;

SkipToSubjectField: PROCEDURE =
BEGIN
THROUGH [1 .. 2] DO
UNTIL tocString[stringIndex] = opD.substringSeparator DO
stringIndex ← stringIndex + 1;
ENDLOOP;
stringIndex ← stringIndex + 1;
ENDLOOP;
END; -- of SkipToSubjectField --

IF tnp.displayFormatted THEN
BEGIN
vmD.GetTOCFixedPart[toc, key, index, @tocFixedPart];
subjectLeftX ← intC.subjectLeftX;
subjectExtensionLeftX ← intC.subjectExtensionLeftX;
END
ELSE {subjectLeftX ← markLeftX; subjectExtensionLeftX ← markLeftX + 15};
vmD.GetTOCString[toc, key, index, tocString];
firstUnusedLine ← firstLine;

-- Process the first line of a TOC Entry.
nLines ← 1;
IF (firstUnusedLine = NIL) OR (firstUnusedLine = tnp.firstLineOffScreen)
OR (nLines <= suppressed) THEN
-- First line is Not to be displayed.
BEGIN
SkipToSubjectField[];
-- Skip first line of subject.
[stringIndex, ] ← PretendToFillScreenWithWords
[subjectLeftX, rightMargin, stringIndex, tocString];
END
ELSE
BEGIN -- First line Is to be displayed
y ← firstUnusedLine.y;
dsD.ClearRectangle[markLeftX, rightMargin, y, y + dsD.lineHeight];
IF tnp.displayFormatted THEN
BEGIN
-- Display Mark --
IF ~tocFixedPart.seen THEN [] ← dsD.PutCharInBitMap[’?, markLeftX, y, plainFace]
ELSE [] ← dsD.PutCharInBitMap[tocFixedPart.mark, markLeftX, y, plainFace];
-- display message number --
PutFourDigitStringIntoBitMap[index, numberLeftX, y];
-- Display date: field.
stringIndex ←
FillScreenWithChars[intC.dateLeftX, y, intC.dateRightX, stringIndex, tocString];
UNTIL tocString[stringIndex] = opD.substringSeparator DO
stringIndex ← stringIndex + 1;
ENDLOOP;
stringIndex ← stringIndex + 1;
-- Display From: field.
stringIndex ←
FillScreenWithChars[intC.fromLeftX, y, intC.fromRightX, stringIndex, tocString];
UNTIL tocString[stringIndex] = opD.substringSeparator DO
stringIndex ← stringIndex + 1;
ENDLOOP;
stringIndex ← stringIndex + 1;
END
ELSE SkipToSubjectField[];
-- Display first line of Subject: field.
[stringIndex, firstFreeX] ←
FillScreenWithWords[subjectLeftX, y, rightMargin, stringIndex, tocString];
IF tnp.displayFormatted AND tocFixedPart.deleted THEN dsD.BlackenRectangle
[markLeftX, firstFreeX, y + dsD.lineHeight / 2, y + dsD.lineHeight / 2 + 1];
-- Record info for this line in the line descriptor block.
firstUnusedLine.linePair ← LinePair[index, nLines];
firstUnusedLine.state ← index;
firstUnusedLine ← firstUnusedLine.nextLine;
END;

-- Process lines after the first one.
UNTIL (stringIndex >= tocString.length)
OR (tnp.displayFormatted AND nLines = maxLinesPerTOCEntry) DO
nLines ← nLines + 1;
IF (firstUnusedLine = NIL) OR (firstUnusedLine = tnp.firstLineOffScreen)
OR (nLines <= suppressed) THEN
-- Do not display this line.
[stringIndex, ] ← PretendToFillScreenWithWords
[subjectExtensionLeftX, rightMargin, stringIndex, tocString]
ELSE BEGIN -- Display this line.
y ← firstUnusedLine.y;
dsD.ClearRectangle[subjectExtensionLeftX, rightMargin, y, y + dsD.lineHeight];
-- Display extra line of Subject: field.
[stringIndex, firstFreeX] ←
FillScreenWithWords[subjectExtensionLeftX, y, rightMargin, stringIndex, tocString];
IF tnp.displayFormatted AND tocFixedPart.deleted THEN dsD.BlackenRectangle
[subjectExtensionLeftX, firstFreeX, y + dsD.lineHeight / 2, y + dsD.lineHeight / 2 + 1];
-- Record info for this line in the line descriptor block.
firstUnusedLine.linePair ← LinePair[index, nLines];
firstUnusedLine.state ← index;
firstUnusedLine ← firstUnusedLine.nextLine;
END;
ENDLOOP;
END; -- of DisplayTOCEntry --


PutFourDigitStringIntoBitMap: PROCEDURE
[n: CARDINAL, startX: ScreenXCoord, y: ScreenYCoord] =
-- Places the four digit string for n into the bitmap. Blanks are allowed to take up a full
-- digit width on the screen.
BEGIN
s: STRING ← [4];
String.AppendDecimal[s, n];
[] ← dsD.PutStringInBitMap[startX + (4 - s.length) * digitWidth, y, s, plainFace];
END; -- of PutFourDigitStringIntoBitMap --


FillScreenWithChars: PROCEDURE [x: ScreenXCoord, y: ScreenYCoord, limX: ScreenXCoord,
startCharIndex: CARDINAL, s: STRING] RETURNS [firstUnusedCharIndex: CARDINAL] =
-- Fills the screen section [x..limX) with characters from s, breaking just before a character
-- begins.
BEGIN
curX: ScreenXCoord ← x;
char: CHARACTER;
charString: STRING ← [120];
i: CARDINAL ← 0;

FOR firstUnusedCharIndex IN [startCharIndex .. s.length) DO
IF (charString[i] ← char ← s[firstUnusedCharIndex]) = opD.substringSeparator THEN EXIT;
IF (curX ← dsD.GetCharRightX[char, curX]) > limX THEN EXIT;
i ← i + 1;
REPEAT
FINISHED => firstUnusedCharIndex ← s.length;
ENDLOOP;
charString.length ← firstUnusedCharIndex - startCharIndex;
[] ← dsD.PutStringInBitMap[x, y, charString, plainFace];
END; -- of FillScreenWithChars --


FillScreenWithWords: PUBLIC PROCEDURE [x: ScreenXCoord, y: ScreenYCoord,
limX: ScreenXCoord, startCharIndex: CARDINAL, s: STRING]
RETURNS [firstUnusedCharIndex: CARDINAL, firstEmptyX: ScreenXCoord] =
-- Fills the screen section [x..limX) with characters from s, breaking just before a word
-- begins. All white space characters remain on the same line as the word they follow.
BEGIN
curCharIndex: CARDINAL;
curX: ScreenXCoord ← x;
charString: STRING ← [120];

[firstUnusedCharIndex, firstEmptyX]
← PretendToFillScreenWithWords[x, limX, startCharIndex, s];
FOR curCharIndex IN [startCharIndex .. firstUnusedCharIndex) DO
charString[curCharIndex - startCharIndex] ← s[curCharIndex];
ENDLOOP;
charString.length ← firstUnusedCharIndex - startCharIndex;
[] ← dsD.PutStringInBitMap[x, y, charString, plainFace];
END; -- of FillScreenWithWords --


PretendToFillScreenWithWords: PROCEDURE [x: ScreenXCoord, limX: ScreenXCoord,
startCharIndex: CARDINAL, s: STRING] RETURNS [firstUnusedCharIndex: CARDINAL,
firstEmptyX: ScreenXCoord] =
-- Fills the screen section [x..limX) with characters from s, breaking just before a word
-- begins, at a substringSeparator character, or at the end of screen section in a word that
-- fills the entire section. All white space characters following a word are considered as
-- part of that word. The firstUnusedCharIndex returned may be the index of a
-- substringSeparator character.
BEGIN
state: {inWhite, inWord} ← inWord;
firstErasableX: ScreenXCoord ← x;
curCharIndex: CARDINAL ← startCharIndex;
curX: ScreenXCoord ← x;
newCurX: ScreenXCoord;
char: CHARACTER;

WhiteChar: PROCEDURE [c: CHARACTER] RETURNS [BOOLEAN] =
BEGIN
RETURN[(c = Ascii.SP) OR (c = Ascii.TAB)];
END; -- of WhiteChar --

firstUnusedCharIndex ← startCharIndex;
UNTIL curCharIndex >= s.length DO
char ← s[curCharIndex];
IF char = opD.substringSeparator THEN EXIT;
IF char = Ascii.CR OR char = Editor.nextCode THEN
{s[curCharIndex] ← Ascii.CR; curCharIndex ← curCharIndex + 1; EXIT};
IF WhiteChar[char] THEN state ← inWhite
ELSE IF state = inWhite THEN
-- transition from white space to nonwhite space --
{firstUnusedCharIndex ← curCharIndex; firstErasableX ← curX; state ← inWord};
IF (newCurX ← dsD.GetCharRightX[char, curX]) >= limX THEN
BEGIN -- character does not fit --
IF firstUnusedCharIndex # startCharIndex THEN GOTO backupCurrentWord
ELSE -- break in middle of long word -- EXIT;
END;
curCharIndex ← curCharIndex + 1;
curX ← newCurX;
REPEAT
backupCurrentWord => {firstEmptyX ← firstErasableX; RETURN};
ENDLOOP;
-- string s terminated, substringSeparator found, or break long word --
RETURN[curCharIndex, curX];
END; -- of PretendToFillScreenWithWords --


MapYToTOCIndex: PUBLIC PROCEDURE [y: ScreenYCoord, tnp: TOCTextNbrPtr]
RETURNS [vmD.TOCIndex] =
-- Returns the vmD.TOCIndex of the TOCEntry that occupies the line containing y.
BEGIN
line: LinePtr ← MapYToTOCLine[y, tnp];
RETURN[IF line.state = index THEN line.linePair.index ELSE line.linePair.index - 1];
END; -- of MapYToTOCIndex --


ThisIsAFirstLine: PUBLIC PROCEDURE [y: ScreenYCoord, tnp: TOCTextNbrPtr]
RETURNS [BOOLEAN] =
-- Returns TRUE if y is in a first line of a TOCEntry, FALSE otherwise.
BEGIN
line: LinePtr ← MapYToTOCLine[y, tnp];
RETURN[line.state = index AND line.linePair.lineNumber = 1];
END; -- of ThisIsAFirstLine --


MapYToTOCLine: PROCEDURE [y: ScreenYCoord, tnp: TOCTextNbrPtr] RETURNS [LinePtr] =
-- Returns the LinePtr for the line containing y.
BEGIN
nLines: CARDINAL;
line: LinePtr;
nLines ← LOOPHOLE[(y - tnp.topY), CARDINAL] / dsD.lineHeight;
line ← tnp.lines;
THROUGH [1 .. nLines] DO line ← line.nextLine; ENDLOOP;
RETURN[line];
END; -- of MapYToTOCLine --


MapTOCIndexToTopY: PUBLIC PROCEDURE [index: vmD.TOCIndex, tnp: TOCTextNbrPtr]
RETURNS [ScreenYCoord, BOOLEAN] =
-- Returns the first scanline for index. Returns FALSE if index is not currently displayed.
BEGIN
line: LinePtr;
line ← MapTOCIndexToTOCLine[index, tnp];
IF line = NIL THEN RETURN [tnp.topY, FALSE]
ELSE RETURN [line.y, TRUE];
END; -- of MapTOCIndexToTopY --


MapTOCIndexToTOCLine: PUBLIC PROCEDURE [index: vmD.TOCIndex, tnp: TOCTextNbrPtr,
line: LinePtr ← NIL] RETURNS [LinePtr] =
-- Returns the tocLine for index. Returns NIL if index is not currently displayed. Starts
-- search at line if not NIL.
BEGIN
IF line = NIL THEN line ← tnp.lines;
UNTIL line = tnp.firstLineOffScreen DO
IF line.linePair.index = index THEN RETURN[line];
line ← line.nextLine;
ENDLOOP;
RETURN[NIL];
END; -- of MapTOCIndexToTOCLine --


ScrollUpTOC: PUBLIC PROC [y: ScreenYCoord, tnp: TOCTextNbrPtr, key: CARDINAL] =
-- Computes number of lines to scroll up, moves area of screen up, refreshes bottom of TOC
-- with new text, and fixes up Line structures.
BEGIN
shiftNum: CARDINAL;
line, lastLine, newLastLine, newTopLine: LinePtr;
lineY: ScreenYCoord;
firstFree: vmD.TOCIndex;

IF ~tnp.haveToc THEN RETURN;
shiftNum ← MAX[(y - tnp.topY) / dsD.lineHeight, 1];

-- calculate possible smaller shiftNum --
line ← MapYToTOCLine[y, tnp];
IF line = tnp.lines THEN
IF line.state = end THEN RETURN ELSE line ← line.nextLine;
IF line.state = empty THEN BEGIN
shiftNum ← 0;
FOR line ← tnp.lines, line.nextLine UNTIL line.state = end DO
shiftNum ← shiftNum + 1;
ENDLOOP;
END;
IF shiftNum = 0 THEN RETURN;

dsD.SlideFullWidthRectangleVertically
[top: tnp.topY + shiftNum * dsD.lineHeight, bottom: tnp.bottomY, newTop: tnp.topY];
newLastLine ← tnp.lines;
THROUGH [1 .. shiftNum) DO
newLastLine ← newLastLine.nextLine;
ENDLOOP;
newTopLine ← newLastLine.nextLine;
lastLine ← tnp.firstLineOffScreen;
newLastLine.nextLine ← NIL;
lastLine.nextLine ← tnp.lines;
tnp.lines ← newTopLine;
tnp.firstLineOffScreen ← newLastLine;
lineY ← tnp.topY;
FOR line ← newTopLine, line.nextLine UNTIL line = NIL DO
line.y ← lineY;
lineY ← lineY + dsD.lineHeight;
ENDLOOP;
IF lastLine.state = empty THEN BEGIN
firstFree ← vmD.FirstFreeTOCIndex[tnp.toc, key];
FOR line ← lastLine.nextLine, line.nextLine UNTIL line = NIL DO
line.state ← empty;
line.linePair ← LinePair[firstFree, 1];
ENDLOOP;
END
ELSE
DisplayTOCTail[tnp, key, lastLine, lastLine.linePair.index, lastLine.linePair.lineNumber];
UpdateTOCThumbLine[tnp, key];
END; -- of ScrollUpTOC --


ScrollDownTOC: PUBLIC PROC [y: ScreenYCoord, tnp: TOCTextNbrPtr, key: CARDINAL] =
-- Computes number of lines to scroll down, moves area of screen down, refreshes top of
-- TOC with new text, and fixes up Line structures.
BEGIN
shiftNum, lineCount, nLines: CARDINAL;
line, lastLine, newLastLine, newTopLine: LinePtr;
lineY: ScreenYCoord;
i, lastInvisibleIndex, lastIndexToRefresh, newTopIndex: vmD.TOCIndex;
newTopLineNum: TOCLineNumber;
IF ~tnp.haveToc THEN RETURN;
shiftNum ← MAX[(y - tnp.topY) / dsD.lineHeight, 1];

-- calculate possible smaller shiftNum --
line ← MapYToTOCLine[y, tnp];
IF tnp.lines.state = end THEN BEGIN
lineCount ← 0;
lastInvisibleIndex ← vmD.FirstFreeTOCIndex[tnp.toc, key] - 1;
END
ELSE BEGIN
lineCount ← tnp.lines.linePair.lineNumber - 1;
lastInvisibleIndex ← tnp.lines.linePair.index - 1;
END;
lastIndexToRefresh ← IF lineCount = 0 THEN lastInvisibleIndex ELSE lastInvisibleIndex + 1;
FOR i ← lastInvisibleIndex, i-1 UNTIL (i=0) OR (lineCount >= shiftNum) DO
[, nLines] ← DisplayTOCEntry[tnp, key, i, 1, NIL];
lineCount ← lineCount + nLines;
ENDLOOP;
newTopIndex ← i + 1;
IF lineCount <= shiftNum THEN {shiftNum ← lineCount; newTopLineNum ← 1}
ELSE newTopLineNum ← lineCount - shiftNum + 1;
IF shiftNum = 0 THEN RETURN;
dsD.SlideFullWidthRectangleVertically
[top: tnp.topY, bottom: tnp.bottomY - shiftNum * dsD.lineHeight,
newTop: tnp.topY + shiftNum * dsD.lineHeight];
newLastLine ← tnp.lines;
THROUGH [0 .. LOOPHOLE[LOOPHOLE[tnp.bottomY - tnp.topY, CARDINAL] / dsD.lineHeight - shiftNum, CARDINAL]) DO
newLastLine ← newLastLine.nextLine;
ENDLOOP;
newTopLine ← newLastLine.nextLine;
lastLine ← tnp.firstLineOffScreen;
newLastLine.nextLine ← NIL;
lastLine.nextLine ← tnp.lines;
tnp.lines ← newTopLine;
tnp.firstLineOffScreen ← newLastLine;
lineY ← tnp.topY;
FOR line ← newTopLine, line.nextLine UNTIL line = NIL DO
line.y ← lineY;
lineY ← lineY + dsD.lineHeight;
ENDLOOP;
[line,] ← DisplayTOCEntry[tnp, key, newTopIndex, newTopLineNum - 1, newTopLine];
FOR i IN [newTopIndex + 1 .. lastIndexToRefresh] DO
[line,] ← DisplayTOCEntry[tnp, key, i, 0, line];
ENDLOOP;
tsD.ConsiderAll[tnp, key];
UpdateTOCThumbLine[tnp, key];
END; -- of ScrollDownTOC --


NextTOCEntry: PUBLIC PROC [tnp: TOCTextNbrPtr, key: CARDINAL, entry: vmD.TOCIndex]
RETURNS [exists: BOOLEAN, nextEntry: vmD.TOCIndex] =
-- Determines the next TOCEntry after entry according to the current TOC filter.
BEGIN
-- kludge for now with no filter.
nextEntry ← entry + 1;
exists ← nextEntry < vmD.FirstFreeTOCIndex[tnp.toc, key];
END; -- of NextTOCEntry --


ThumbTOC: PUBLIC PROCEDURE [tlnp: ThumbLineNbrPtr, np: NbrPtr, x: ScreenXCoord] =
-- Determines which TOCEntry will be the first entry on screen after thumbing and
-- displays the appropriate TOCEntries.
BEGIN
xRange: CARDINAL ← tlnp.rightX - tlnp.leftX;
savedCursor: dsD.CursorShape;
firstFree, newTopIndex: vmD.TOCIndex;
tnp: TOCTextNbrPtr ← WITH p: np SELECT FROM tocText => @p, ENDCASE => ERROR;
toc: vmD.TOCHandle ← tnp.toc;
key: CARDINAL ← 0;
keyExists: BOOLEAN = (intC.source.key # 0);

IF ~tnp.haveToc THEN RETURN;
IF keyExists THEN key ← intC.source.key
ELSE key ← vmD.WaitForLock[toc];
firstFree ← vmD.FirstFreeTOCIndex[toc, key] - 1;
IF firstFree = 0 THEN
{IF ~keyExists THEN vmD.UnlockTOC[toc, key]; RETURN};
[savedCursor, , ] ← dsD.GetCursor[];
dsD.ChangeCursor[hourGlass];
newTopIndex ← Inline.LongDiv[Inline.LongMult[firstFree, x - tlnp.leftX], xRange] + 1;
dsD.ClearRectangle[leftMargin, rightMargin, tnp.topY, tnp.bottomY];
DisplayTOCTail[tnp, key, tnp.lines, newTopIndex, 1];
UpdateTOCThumbLine[tnp, key];
IF ~keyExists THEN vmD.UnlockTOC[toc, key];
dsD.ChangeCursor[savedCursor];
END; -- of ThumbTOC --


MakeTOCIndexVisible: PUBLIC PROCEDURE [index: vmD.TOCIndex, key: CARDINAL] =
-- Makes the first line of index visible within the TOC window, including as much of the
-- entry as possible.
BEGIN
toc: TOCTextNbrPtr = intC.tocTextNbr;
firstIndex: vmD.TOCIndex ← toc.lines.linePair.index;
lastIndex: vmD.TOCIndex ← toc.firstLineOffScreen.linePair.index;
empty: BOOLEAN;
[empty, , ] ← DisplayedRange[index, index, toc];
SELECT TRUE FROM
(~empty OR toc.topY = toc.bottomY) => RETURN;
(index <= firstIndex AND index + 3 >= firstIndex) =>
UNTIL toc.lines.linePair = LinePair[index, 1] DO
ScrollDownTOC[toc.topY, toc, key];
ENDLOOP;
(index IN [lastIndex .. lastIndex + 3]) =>
UNTIL ~empty AND (toc.lines.linePair.index = index
OR toc.firstLineOffScreen.linePair.index # index) DO
ScrollUpTOC[toc.topY, toc, key];
[empty, , ] ← DisplayedRange[index, index, toc];
ENDLOOP;
ENDCASE =>
BEGIN
dsD.ClearRectangle[leftMargin, rightMargin, toc.topY, toc.bottomY];
DisplayTOCTail[toc, key, toc.lines, index, 1];
END;
END; -- of MakeTOCIndexVisible --


Consider: PUBLIC PROCEDURE
[lowIndex, highIndex: vmD.TOCIndex, tnp: TOCTextNbrPtr, key: CARDINAL] =
-- Paints considered marker on the first lines of all visible TOCEntries in the range
-- [lowIndex .. highIndex].
BEGIN
Considerer[lowIndex, highIndex, tnp, key, dsD.replace];
END; -- of Consider --


Deconsider: PUBLIC PROCEDURE
[lowIndex, highIndex: vmD.TOCIndex, tnp: TOCTextNbrPtr, key: CARDINAL] =
-- Removes selected marker from the first lines of all visible TOCEntries in the range
-- [lowIndex .. highIndex].
BEGIN
Considerer[lowIndex, highIndex, tnp, key, dsD.erase];
END; -- of Deconsider --


Considerer: PROCEDURE [lowIndex, highIndex: vmD.TOCIndex, tnp: TOCTextNbrPtr,
key: CARDINAL, function: dsD.BitBltFunction] =
-- Modifies selected marker from the first lines of all visible TOCEntries in the range
-- [lowIndex .. highIndex].
BEGIN
empty: BOOLEAN;
y: ScreenYCoord;
h, offset: CARDINAL;
lowC, highC, consideredIndex: vmD.TOCIndex;
IF key = 0 OR tnp.toc.key # key THEN exD.SysBug[];
[empty, lowC, highC] ← DisplayedRange[lowIndex, highIndex, tnp];
IF empty THEN RETURN;
[ , h] ← dsD.GetPictureParameters[triangle];
offset ← LOOPHOLE[(dsD.lineHeight - h), CARDINAL] / 2;
FOR consideredIndex IN [lowC .. highC] DO
[y, ] ← MapTOCIndexToTopY[consideredIndex, tnp];
dsD.PaintPicture[consideredLeftX, y + offset, triangle, function];
ENDLOOP;
END; -- of Considerer --


DisplayedRange: PROCEDURE [lowIndex, highIndex: vmD.TOCIndex, tnp: TOCTextNbrPtr]
RETURNS [empty: BOOLEAN, lowDisplayedInRange, highDisplayedInRange:
vmD.TOCIndex] =
-- Returns the range of those TOCEntries in [lowIndex..highIndex] whose first lines are
-- actually displayed on the screen.
BEGIN
line: LinePtr;
FOR line ← tnp.lines, line.nextLine DO
IF line = tnp.firstLineOffScreen OR line.state # index THEN RETURN[TRUE, 0, 0];
IF line.linePair.index IN [lowIndex .. highIndex] AND line.linePair.lineNumber = 1 THEN
EXIT;
ENDLOOP;
highDisplayedInRange ← lowDisplayedInRange ← line.linePair.index;
empty ← FALSE;
FOR line ← line, line.nextLine
UNTIL line = tnp.firstLineOffScreen OR line.state # index
OR line.linePair.index > highIndex DO
highDisplayedInRange ← line.linePair.index;
ENDLOOP;
END; -- of DisplayedRange --


RefreshTOCChange: PUBLIC PROCEDURE [toc: vmD.TOCHandle, key: CARDINAL,
index: vmD.TOCIndex, action: TOCChangeAction] =
-- Updates the TOC selection according to action, and repaints the TOC neighborhood if toc
-- is the one currently displayed there. A single TOC entry may be inserted at index or
-- the index’th entry may be deleted or replaced by a new single entry. The change must
-- have already been made in the virtual TOC.
BEGIN
OPEN tsD;
i: vmD.TOCIndex ← index;
line: LinePtr;
tnp: TOCTextNbrPtr = intC.tocTextNbr;

-- First, update TOC selection.
SELECT action FROM
delete => BEGIN
i ← index;
IF IsSelected[toc, key, index] THEN RemoveRange[toc, key, index, index + 1];
UNTIL (i ← NextSelectedEntry[toc, key, i]) = 0 DO
RemoveRange[toc, key, i, i + 1];
AddRange[toc, key, i - 1, i];
ENDLOOP;
END;
insert => BEGIN
FOR i ← LastSelectedEntry[toc, key], PrevSelectedEntry[toc, key, i]
UNTIL i = 0 OR i < index DO
RemoveRange[toc, key, i, i + 1];
AddRange[toc, key, i + 1, i + 2];
ENDLOOP;
END;
ENDCASE;
-- Next, update line blocks and screen.
SELECT TRUE FROM
tnp.toc # toc => NULL;
index < tnp.lines.linePair.index =>
-- action occurred before the displayed lines.
SELECT action FROM
insert, delete =>
BEGIN
delta: INTEGER ← IF action = insert THEN 1 ELSE -1;
FOR line ← tnp.lines, line.nextLine UNTIL line = NIL DO
line.linePair.index ← line.linePair.index + delta;
ENDLOOP;
END;
ENDCASE;
index <= tnp.firstLineOffScreen.linePair.index =>
BEGIN -- action occurred on the screen (or in trivial case of only in first line off screen.
line ← MapTOCIndexToTOCLine[index, tnp];
IF line = NIL THEN line ← tnp.firstLineOffScreen;
IF action = replace AND index < tnp.firstLineOffScreen.linePair.index
AND line.linePair.lineNumber = 1 THEN
BEGIN -- We are replacing an entry that is completely displayed on the screen.
oldLines: CARDINAL ← 1;
FOR ln: LinePtr ← line.nextLine, ln.nextLine UNTIL ln.linePair.index # index DO
oldLines ← oldLines + 1;
ENDLOOP;
IF oldLines = DisplayTOCEntry[tnp, key, index, 0, NIL].nLines THEN
BEGIN -- Simple case: no ripple, no changes in following toc entries.
dsD.ClearRectangle[leftMargin, rightMargin, line.y, line.y + dsD.lineHeight*oldLines];
[ , ] ← DisplayTOCEntry[tnp, key, index, 0, line];
IF tsD.IsSelected[toc, key, index] THEN Consider[index, index, tnp, key];
RETURN;
END;
END;
dsD.ClearRectangle[leftMargin, rightMargin, line.y, tnp.bottomY];
DisplayTOCTail[tnp, key, line, index, 1];
END;
ENDCASE; -- action occurred below screen; no fixup necessary.
END; -- of RefreshTOCChange --


END. -- of IntTOC --