-- CGTextImpl.mesa
-- Last changed by Doug Wyatt, October 4, 1982 11:34 am
-- Last changed by Paul Rovner, August 10, 1983 11:32 am

DIRECTORY
Basics USING [LongMult],
CGArea USING [Ref],
CGClipper USING [Data, IsAllBoxes, Node, Ref],
CGColor USING [GetStipple],
CGContext USING [GenBox, Ref],
CGDevice USING [Ref],
CGFont USING [defaultFont, LoadFont, Ref, Rep, WidthArray],
CGMatrix USING [Assign, Concat, GetTrans, Id, IsTrivial, Map, New, Ref,
SetTrans, Translate],
CGPrivate USING [Context],
CGSource USING [Ref, Rep],
CGStorage USING [pZone, qZone],
Graphics USING [Warning],
GraphicsOps USING [],
GraphicsBasic USING [black, Box, Vec, white],
ConvertUnsafe USING [AppendRope],
Rope USING [Cat, Map, MaxLen, ROPE],
PrincOps USING [BBTableSpace, BitBltTablePtr],
PrincOpsUtils USING [AlignedBBTable, BITBLT],
Real USING [RoundI, RoundLI],
SpecialReal USING [Small],
StrikeFormat USING [nullWidthEntry, WidthEntry, WTable, XTable];

CGTextImpl: CEDAR PROGRAM
IMPORTS Basics, CGClipper, CGColor, CGContext, CGFont, CGMatrix, CGStorage, Graphics,
ConvertUnsafe, Rope, PrincOpsUtils, Real, SpecialReal
EXPORTS CGContext, CGPrivate, Graphics, GraphicsBasic, GraphicsOps = {
OPEN StrikeFormat, GraphicsBasic;

FontObject: PUBLIC TYPE = CGFont.Rep; -- export to GraphicsBasic
TextDataRep: PUBLIC TYPE = DataRep; -- export to CGContext

Context: TYPE = CGPrivate.Context;
ContextData: TYPE = CGContext.Ref;

Data: TYPE = REF DataRep;
DataRep: TYPE = RECORD [
font: CGFont.Ref, -- default font
lastid: CGMatrix.Id, -- id of last matrix used to form map
map: CGMatrix.Ref, -- font (or bitmap) to device matrix
mask: CGSource.Ref -- source info for font
];

Text: TYPE = REF READONLY TEXT;

dataZone: ZONE = CGStorage.qZone;
textZone: ZONE = CGStorage.pZone;
srcZone: ZONE = CGStorage.qZone;

GetData: PROC[self: ContextData] RETURNS[Data] = INLINE {
data: Data ← self.textdata;
RETURN[IF data=NIL THEN MakeData[self] ELSE data] };

MakeData: PROC[self: ContextData] RETURNS[Data] = {
data: Data ← dataZone.NEW[DataRep ← [font: NIL, lastid: 0, map: NIL, mask: NIL]];
data.font ← CGFont.defaultFont;
data.map ← CGMatrix.New[];
data.mask ← srcZone.NEW[CGSource.Rep ← [type: array, mode: transparent,
fat: FALSE, bps: 0, color: GraphicsBasic.black, xbase: NIL, xrast: 0, Get: NIL]];
self.textdata ← data;
RETURN[data];
};

ActionProc: TYPE = PROC[CHAR] RETURNS[BOOL];
-- this is SAFE, and matches Rope.ActionType

DrawChar: PUBLIC PROC[self: Context, char: CHARACTER, font: CGFont.Ref] = {
CharMap: PROC[action: ActionProc] = { [] ← action[char] };
self.procs.DrawChars[self,font,CharMap];
};

DrawString: PUBLIC UNSAFE PROC[self: Context, string: LONG STRING,
start: NAT ← 0, len: NATLAST[NAT], font: CGFont.Ref] = UNCHECKED {
StringMap: SAFE PROC[action: ActionProc] = CHECKED {
FOR i: CARDINAL IN[start..start+len) DO c: CHARACTER;
TRUSTED { c ← string[i] }; IF action[c] THEN EXIT ENDLOOP };
-- Be sure substring limits are in range
IF string=NIL THEN RETURN;
start ← MIN[start,string.length]; len ← MIN[len,string.length-start];
self.procs.DrawChars[self,font,StringMap];
};

DrawText: PUBLIC PROC[self: Context, text: Text,
start: NAT ← 0, len: NATLAST[NAT], font: CGFont.Ref] = {
TextMap: PROC[action: ActionProc] = {
FOR i: CARDINAL IN[start..start+len) DO
IF action[text[i]] THEN EXIT ENDLOOP };
-- Be sure substring limits are in range
IF text=NIL THEN RETURN;
start ← MIN[start,text.length]; len ← MIN[len,text.length-start];
self.procs.DrawChars[self,font,TextMap];
};

DrawRope: PUBLIC PROC[self: Context, rope: Rope.ROPE,
start: INT ← 0, len: INT ← Rope.MaxLen, font: CGFont.Ref] = {
RopeMap: PROC[action: ActionProc] = { [] ← Rope.Map[rope,start,len,action] };
IF rope=NIL THEN RETURN;
self.procs.DrawChars[self,font,RopeMap];
};

DrawTextFromProc: PUBLIC PROC[self: Context,
map: PROC[ActionProc], font: CGFont.Ref] = {
self.procs.DrawChars[self, font, map];
};



-- DrawChars must do the following for each character:
-- Get character information from strike font:
-- xl,xr,yt,yb bounding box of character in strike
-- xo,yo position of character origin in strike
-- xw,yw width vector for character
-- Let T be a copy of the current transformation
-- Translate T to the rounded current position
-- Concatenate: M ← [1,0,0,-1,-xo,yo] * T
-- Use M to transform the box [xl,yt,xr,yb]
-- Clip and display the resulting shape
-- Use M to transform the relative vector [xw,yw]
-- Move the current position by this amount
--
-- Fortunately, much of the above computation can be taken out of the loop.
-- For all characters in a strike font, yt,yb,yo and yw(=0) are fixed
-- Only the translation components of the matrices change
-- Most of the time, M is a simple translation
--

-- Easiest case: cp is in integer range, msd is a translation, clipper is a box

DrawChars: PUBLIC PROC[self: Context,
font: CGFont.Ref, map: PROC[ActionProc]] = TRUSTED {
ctx: ContextData ← NARROW[self.data];
data: Data ← GetData[ctx];
-- information from strike font
f: CGFont.Ref ← IF font=NIL THEN data.font ELSE font;
kern: BOOLEAN = f.kerned;
min: CHARACTER = f.min;
max: CHARACTER = f.max;
offset: INTEGER = f.ox;
xtable: LONG POINTER TO XTable = f.xtable;
wtable: LONG POINTER TO WTable = f.wtable;
width: REF CGFont.WidthArray ← f.width;
-- information from display context
T: CGMatrix.Ref ← ctx.matrix;
clipper: CGClipper.Ref ← ctx.clipper;
cp: Vec ← ctx.cp;
src: CGSource.Ref ← ctx.src;

M: CGMatrix.Ref ← data.map;
bxmin,bxmax,bymin,bymax: INTEGER ← 0; -- bounding box for string
x: INTEGER ← 0; -- current x position
wid: INTEGER ← 0; -- width of string
len: INT ← 0; -- number of chars

-- Determine the font-to-device transformation matrix
-- Compute it if necessary, then cache it for the next time
IF data.lastid#T.id THEN {
CGMatrix.Assign[M,T];
IF ctx.yUp THEN CGMatrix.Concat[M,1,0,0,-1];
data.lastid ← T.id };

{
-- Compute the bounding box for the string (and count the characters)
BoxAction: SAFE PROC[char: CHARACTER] RETURNS[BOOLEAN] = TRUSTED {
xl,xr,xmin,xmax: INTEGER;
-- get character information from font
IF kern THEN { i: NAT; w: WidthEntry ← nullWidthEntry;
IF char IN[min..max] THEN w ← wtable[i ← char-min];
IF w=nullWidthEntry THEN w ← wtable[i ← max-min+1];
xl ← xtable[i]; xr ← xtable[i+1]; xmin ← x + (offset + w.offset) }
ELSE { i: NATIF char IN[min..max] THEN char-min ELSE max-min+1;
xl ← xtable[i]; xr ← xtable[i+1]; xmin ← x };
xmax ← xmin + (xr - xl);
IF len=0 THEN { bxmin ← xmin; bxmax ← xmax } -- update x part of bounding box
ELSE { IF xmin<bxmin THEN bxmin ← xmin; IF xmax>bxmax THEN bxmax ← xmax };
x ← x + width[char]; -- advance position
len ← len + 1;
RETURN[FALSE];
};
map[BoxAction];
wid ← x;
IF len>0 THEN { bymin ← -(f.oy+f.dy); bymax ← bymin + f.dy };
};

-- Test for the easy (and, we hope, common) case
IF ctx.haveRaster
AND CGMatrix.IsTrivial[M]
AND CGClipper.IsAllBoxes[clipper]
AND (cp.y+MIN[bymin,0])>FIRST[INTEGER]
AND (cp.y+MAX[0,bymax])<LAST[INTEGER]
AND (cp.x+MIN[bxmin,0])>FIRST[INTEGER]
AND (cp.x+MAX[0,bxmax,x])<LAST[INTEGER]
THEN {
cpx: INTEGER ← Real.RoundI[cp.x];
cpy: INTEGER ← Real.RoundI[cp.y];
data: CGClipper.Data ← clipper.data;
sbase: LONG POINTER ← f.bitmap;
srast: CARDINAL ← f.raster;
dbase: LONG POINTER ← ctx.dbase;
drast: CARDINAL ← ctx.drast;
bbspace: PrincOps.BBTableSpace;
bb: PrincOps.BitBltTablePtr ← PrincOpsUtils.AlignedBBTable[@bbspace];
color: CARDINAL ← 52525B;
IF src.color=GraphicsBasic.black THEN color ← 177777B
ELSE IF src.color=GraphicsBasic.white THEN color ← 0
ELSE IF src.color.tag=stipple THEN color ← CGColor.GetStipple[src.color];
bb^ ← [dst: , src: , width: , height: ,
dstBpl: drast*16, srcDesc: [srcBpl[srast*16]],
flags: [disjoint: TRUE, gray: FALSE, srcFunc: null, dstFunc: or]
];
IF src.mode=invert THEN { bb.flags.dstFunc ← xor }
ELSE IF color=0 THEN { bb.flags.srcFunc ← complement; bb.flags.dstFunc ← and };
bxmin ← bxmin + cpx; bxmax ← bxmax + cpx;
bymin ← bymin + cpy; bymax ← bymax + cpy;
FOR i: NAT IN[0..clipper.size) DO
node: CGClipper.Node ← data[i];
cxmin: INTEGER ← Real.RoundI[node.xmin];
cxmax: INTEGER ← Real.RoundI[node.xmax];
cymin: INTEGER ← Real.RoundI[node.ymin];
cymax: INTEGER ← Real.RoundI[node.ymax];
IF bymax<=cymin THEN EXIT; -- clipper is sorted in increasing ymin
IF bymin<cymax AND bxmax>cxmin AND bxmin<cxmax THEN {
ymin: INTEGER ← bymin;
ymax: INTEGER ← bymax;
yb: INTEGER ← 0;
dline,sline: LONG POINTERNIL;
IF ymin<cymin THEN { yb ← cymin-ymin; ymin ← cymin };
IF ymax>cymax THEN { ymax ← cymax };
dline ← dbase + Basics.LongMult[ymin,drast];
sline ← sbase + Basics.LongMult[yb,srast];
bb.height ← ymax - ymin;
x ← cpx;
{
EasyDrawAction: SAFE PROC[char: CHARACTER] RETURNS[BOOLEAN] = TRUSTED {
xl,xr,xmin,xmax: INTEGER;
IF kern THEN { i: NAT; w: WidthEntry ← nullWidthEntry;
IF char IN[min..max] THEN w ← wtable[i ← char-min];
IF w=nullWidthEntry THEN w ← wtable[i ← max-min+1];
xl ← xtable[i]; xr ← xtable[i+1]; xmin ← x + (offset + w.offset) }
ELSE { i: NATIF char IN[min..max] THEN char-min ELSE max-min+1;
xl ← xtable[i]; xr ← xtable[i+1]; xmin ← x };
xmax ← xmin + (xr - xl);
IF xmax>cxmin AND xmin<cxmax THEN {
WB: TYPE = MACHINE DEPENDENT RECORD[w: [0..7777B], b: [0..17B]];
IF xmin<cxmin THEN { xl ← xl + (cxmin-xmin); xmin ← cxmin };
IF xmax>cxmax THEN { xmax ← cxmax };
bb.dst ← [word: dline + LOOPHOLE[xmin,WB].w, bit: LOOPHOLE[xmin,WB].b];
bb.src ← [word: sline + LOOPHOLE[xl,WB].w, bit: LOOPHOLE[xl,WB].b];
bb.width ← xmax-xmin;
PrincOpsUtils.BITBLT[bb];
};
x ← x + width[char]; -- advance position
RETURN[FALSE];
};
map[EasyDrawAction];
};
};
ENDLOOP;
IF ctx.boxing THEN {
-- *** fix up bounding box here ***
};
cp.x ← cp.x + wid;
}
ELSE { -- have to do it the hard way
device: CGDevice.Ref ← ctx.device;
mask: CGSource.Ref ← data.mask;
yo: REAL ← f.oy+f.dy;
box: Box;
mask.xbase ← f.bitmap; mask.xrast ← f.raster;
mask.color ← src.color;
mask.mode ← (IF src.mode=invert THEN invert ELSE transparent);
box.ymin ← 0; box.ymax ← f.dy;
-- Round cp to nearest device grid point
IF SpecialReal.Small[cp.x] THEN cp.x ← Real.RoundLI[cp.x];
IF SpecialReal.Small[cp.y] THEN cp.y ← Real.RoundLI[cp.y];
CGMatrix.SetTrans[M,cp];
{
HardDrawAction: SAFE PROC[char: CHARACTER] RETURNS[BOOLEAN] = TRUSTED {
xl,xr,xo,xw: INTEGER;
area: CGArea.Ref ← NIL;

-- get character information from font
IF kern THEN { i: NAT; w: WidthEntry ← nullWidthEntry;
IF char IN[min..max] THEN w ← wtable[i ← char-min];
IF w=nullWidthEntry THEN w ← wtable[i ← max-min+1];
xl ← xtable[i]; xr ← xtable[i+1]; xo ← xl - (offset + w.offset) }
ELSE { i: NATIF char IN[min..max] THEN char-min ELSE max-min+1;
xl ← xtable[i]; xr ← xtable[i+1]; xo ← xl };
xw ← width[char]; -- width from width table

CGMatrix.Translate[M,-xo,-yo];
box.xmin ← xl; box.xmax ← xr;
area ← CGContext.GenBox[ctx,box,M];
device.Show[device,area,mask,M];

-- move current position by character width
CGMatrix.SetTrans[M,cp];
CGMatrix.Translate[M,xw,0];
cp ← CGMatrix.GetTrans[M];
RETURN[FALSE];
};
map[HardDrawAction];
};
IF ctx.boxing THEN {
-- *** fix up bounding box here ***
};
};
ctx.cp ← cp; -- update current position
};

DefaultFont: PUBLIC PROC RETURNS[CGFont.Ref] = { RETURN[CGFont.defaultFont] };
-- for GraphicsOps

MakeFont: PUBLIC PROC[name: Rope.ROPE] RETURNS[CGFont.Ref] = {
IsDot: PROC[c: CHARACTER] RETURNS[BOOLEAN] = { RETURN[c='.] };
string: STRING ← [100];
font: CGFont.Ref;
IF NOT Rope.Map[base: name, action: IsDot] THEN name ← Rope.Cat[name,".ks"];
TRUSTED { ConvertUnsafe.AppendRope[string,name]; font ← CGFont.LoadFont[string] };
IF font=NIL THEN { SIGNAL Graphics.Warning[fontNotFound]; font ← CGFont.defaultFont }
ELSE font.fileName ← name;
RETURN[font];
};

CharBox: PUBLIC PROC[font: CGFont.Ref, char: CHARACTER]
RETURNS[xmin,ymin,xmax,ymax: REAL] = {
kern: BOOLEAN = font.kerned;
min: CHARACTER = font.min;
max: CHARACTER = font.max;
xtable: LONG POINTER TO XTable = font.xtable;
wtable: LONG POINTER TO WTable = font.wtable;
bymin: INTEGER ← font.oy;
bymax: INTEGER ← bymin + font.dy;
xl,xr,xo: INTEGER;

IF kern THEN { i: NAT; w: WidthEntry ← nullWidthEntry;
IF char IN[min..max] THEN { i ← char-min; TRUSTED {w ← wtable[i]} };
IF w=nullWidthEntry THEN { i ← max-min+1; TRUSTED {w ← wtable[i]} };
TRUSTED {xl ← xtable[i]; xr ← xtable[i+1]}; xo ← xl - (font.ox + w.offset) }
ELSE { i: NATIF char IN[min..max] THEN char-min ELSE max-min+1;
TRUSTED {xl ← xtable[i]; xr ← xtable[i+1]}; xo ← xl };
RETURN[xmin: xl - xo, xmax: xr - xo, ymin: bymin, ymax: bymax];
};

TextBox: PUBLIC PROC[font: CGFont.Ref, text: Text,
start: NAT ← 0, len: NATLAST[NAT]]
RETURNS[xmin,ymin,xmax,ymax: REAL] = {
TextMap: PROC[action: ActionProc] = {
FOR i: CARDINAL IN[start..start+len) DO
IF action[text[i]] THEN EXIT ENDLOOP };
box: Box;
-- Be sure substring limits are in range
IF text=NIL THEN start ← len ← 0
ELSE { start ← MIN[start,text.length]; len ← MIN[len,text.length-start] };
box ← CharsBox[font,TextMap];
RETURN[xmin: box.xmin, ymin: box.ymin, xmax: box.xmax, ymax: box.ymax];
};

RopeBox: PUBLIC PROC[font: CGFont.Ref, rope: Rope.ROPE,
start: INT ← 0, len: INTLAST[INT]]
RETURNS[xmin,ymin,xmax,ymax: REAL] = {
RopeMap: PROC[action: ActionProc] = { [] ← Rope.Map[rope,start,len,action] };
box: Box;
box ← CharsBox[font,RopeMap];
RETURN[xmin: box.xmin, ymin: box.ymin, xmax: box.xmax, ymax: box.ymax];
};

CharsBox: PROC[font: CGFont.Ref, map: PROC[ActionProc]] RETURNS[Box] = {
kern: BOOLEAN = font.kerned;
min: CHARACTER = font.min;
max: CHARACTER = font.max;
offset: INTEGER = font.ox;
xtable: LONG POINTER TO XTable = font.xtable;
wtable: LONG POINTER TO WTable = font.wtable;
width: REF CGFont.WidthArray ← font.width;
cpx: INTEGER ← 0;
bxmin,bxmax: INTEGER ← 0;
bymin: INTEGER ← font.oy;
bymax: INTEGER ← bymin + font.dy;
first: BOOLEANTRUE;

{
Action: PROC[char: CHARACTER] RETURNS[BOOLEAN] = {
xl,xr,xo,xw,tx,cxmin,cxmax: INTEGER;

-- get character information from font
IF kern THEN { i: NAT; w: WidthEntry ← nullWidthEntry;
IF char IN[min..max] THEN { i ← char-min; TRUSTED {w ← wtable[i]} };
IF w=nullWidthEntry THEN { i ← max-min+1; TRUSTED {w ← wtable[i]} };
TRUSTED {xl ← xtable[i]; xr ← xtable[i+1]}; xo ← xl - (offset + w.offset) }
ELSE { i: NATIF char IN[min..max] THEN char-min ELSE max-min+1;
TRUSTED {xl ← xtable[i]; xr ← xtable[i+1]}; xo ← xl };
xw ← width[char]; -- width from width table
tx ← cpx - xo; -- x translation
cxmin ← xl + tx; cxmax ← xr + tx; -- x bounds

-- update x components of bounding box
IF first THEN { bxmin ← cxmin; bxmax ← cxmax; first ← FALSE }
ELSE { IF cxmin<bxmin THEN bxmin ← cxmin; IF cxmax>bxmax THEN bxmax ← cxmax };

-- move current position by character width
cpx ← cpx + xw;
RETURN[FALSE];
};
map[Action];
};
IF first THEN RETURN[[0,0,0,0]]
ELSE RETURN[[xmin: bxmin, xmax: bxmax, ymin: bymin, ymax: bymax]];
};

CharWidth: PUBLIC PROC[font: CGFont.Ref, char: CHARACTER]
RETURNS[xw,yw: REAL] = {
RETURN[font.width[char],0];
};

TextWidth: PUBLIC PROC[font: CGFont.Ref, text: Text,
start: NAT ← 0, len: NATLAST[NAT]]
RETURNS[xw,yw: REAL] = {
TextMap: PROC[action: ActionProc] = {
FOR i: CARDINAL IN[start..start+len) DO
IF action[text[i]] THEN EXIT ENDLOOP };
w: Vec;
-- Be sure substring limits are in range
IF text=NIL THEN start ← len ← 0
ELSE { start ← MIN[start,text.length]; len ← MIN[len,text.length-start] };
w ← CharsWidth[font,TextMap];
RETURN[xw: w.x, yw: w.y];
};

RopeWidth: PUBLIC PROC[font: CGFont.Ref, rope: Rope.ROPE,
start: INT ← 0, len: INTLAST[INT]]
RETURNS[xw,yw: REAL] = {
RopeMap: PROC[action: ActionProc] = { [] ← Rope.Map[rope,start,len,action] };
w: Vec;
w ← CharsWidth[font,RopeMap];
RETURN[xw: w.x, yw: w.y];
};

CharsWidth: PROC[font: CGFont.Ref, map: PROC[ActionProc]] RETURNS[Vec] = {
width: REF CGFont.WidthArray ← font.width;
w: INTEGER ← 0;
Action: PROC[char: CHARACTER] RETURNS[BOOLEAN] = {
w ← w + width[char]; RETURN[FALSE] };
map[Action];
RETURN[[w,0]];
};

FontBox: PUBLIC PROC[font: CGFont.Ref]
RETURNS[xmin,ymin,xmax,ymax: REAL] = {
RETURN[xmin: font.ox, xmax: font.ox+font.dx,
ymin: font.oy, ymax: font.oy+font.dy];
};

SetDefaultFont: PUBLIC PROC[self: Context, font: CGFont.Ref] = {
ctx: ContextData ← NARROW[self.data];
data: Data ← GetData[ctx];
data.font ← font;
};

GetDefaultFont: PUBLIC PROC[self: Context] RETURNS[CGFont.Ref] = {
ctx: ContextData ← NARROW[self.data];
data: Data ← GetData[ctx];
RETURN[data.font];
};

}.