MODULE TextFrames0;  (*HM 17-Dec-1991*)
IMPORT OS, Viewers0, Texts0;

CONST
  EOL = 0DX; DEL = 7FX;
  scrollW = 12;  (*width of scroll bar*)

TYPE
  Line = POINTER TO LineDesc;
  LineDesc = RECORD
    len, asc, dsc, wid: INTEGER;  (*length, ascender, descender, width*)
    eol: BOOLEAN;  (*TRUE if line is terminated with EOL*)
    next: Line
  END;
  Position* = RECORD  (*position of a character c on the screen*)
    x-, y-, dx-: INTEGER;  (*(x,y) = left point on base line; dx = width of c*)
    org-, pos-: LONGINT; (*origin of line containing c; text position of c*)
    L: Line  (*line containing c*)
  END;
  Frame* = POINTER TO FrameDesc;
  FrameDesc* = RECORD (Viewers0.FrameDesc)
    text*: Texts0.Text;
    org-: LONGINT;  (*index of first character in the frame*)
    caret-: Position; (*caret visible of caret.pos >= 0*)
    selBeg-, selEnd-: Position; (*selection; visible if selBeg.pos >= 0*)
    selTime: LONGINT;  (*time stamp of selection*)
    lsp, margin: INTEGER;  (*space between lines; space between frame border and text*)
    lines: Line  (*list of lines in frame*)
  END;
  SelectionMsg = RECORD (OS.Message) f: Frame END;

VAR
  cmdFrame-: Frame;  (*frame from which most recent command was invoked*)
  cmdPos-: LONGINT;  (*text position after most recent command*)


(*auxiliary procedures*)

PROCEDURE GetMetric (at: Texts0.Attribute; ch: CHAR; VAR dx, x, y, asc, dsc:
INTEGER; VAR pat: OS.Pattern);
  VAR w, h: INTEGER;
BEGIN
  IF at.elem = NIL THEN OS.GetCharMetric(at.fnt, ch, dx, x, y, w, h, pat);
    asc := at.fnt.maxY; dsc := -at.fnt.minY
  ELSE dx := at.elem.w; x := 0; y := 0; dsc := at.elem.dsc; asc := at.elem.h - dsc
  END
END GetMetric;

PROCEDURE MeasureLine (t: Texts0.Text; VAR L: Line);
  VAR ch: CHAR; dx, x, y, asc, dsc: INTEGER; pat: OS.Pattern;
BEGIN L.len := 0; L.asc := 0; L.dsc := 0; L.wid := 0; ch := " ";
  WHILE (ch # EOL) & (t.pos < t.len) DO t.Read(ch); INC(L.len);
    GetMetric(t.attr, ch, dx, x, y, asc, dsc, pat);
    INC(L.wid, dx);
    IF asc > L.asc THEN L.asc := asc END;
    IF dsc > L.dsc THEN L.dsc := dsc END
  END;
  L.eol := ch = EOL
END MeasureLine;

PROCEDURE DrawLine (t: Texts0.Text; len, left, right, base: INTEGER);
  VAR ch: CHAR; dx, x, y, w, h: INTEGER; pat: OS.Pattern;
BEGIN
  WHILE len > 0 DO t.Read(ch); DEC(len);
    IF t.attr.elem = NIL THEN OS.GetCharMetric(t.attr.fnt, ch, dx, x, y, w, h, pat);
      IF left + dx < right THEN OS.DrawPattern(pat, left + x, base + y) END
    ELSE dx := t.attr.elem.w;
      IF left + dx < right THEN t.attr.elem.Draw(left, base) END
    END;
    INC(left, dx)
  END
END DrawLine;

(*methods of class Frame*)

PROCEDURE (f: Frame) FlipCaret;
BEGIN OS.DrawPattern(OS.Caret, f.caret.x, f.caret.y - 10)
END FlipCaret;

PROCEDURE (f: Frame) FlipSelection (a, b: Position);
  VAR x, y: INTEGER; L: Line;
BEGIN L := a.L; x := a.x; y := a.y - L.dsc;
  WHILE L # b.L DO OS.InvertBlock(x, y, f.x + f.w - x, L.asc + L.dsc);
    L := L.next; x := f.x + scrollW + f.margin; y := y - f.lsp - L.asc - L.dsc
  END;
  OS.InvertBlock(x, y, b.x - x, L.asc + L.dsc)
END FlipSelection;

PROCEDURE (f: Frame) RedrawFrom (top: INTEGER);
  VAR t: Texts0.Text; L, L0: Line; y: INTEGER; org: LONGINT;
BEGIN
  (*find first line to be redrawn*)
  y := f.y + f.h - f.margin; org := f.org; L0 := f.lines; L := L0.next;
  WHILE (L # f.lines) & (y - L.asc - L.dsc >= top) DO DEC(y, L.asc + L.dsc + f.lsp); org := org + L.len; L0 := L; L := L.next END;
  IF y > top THEN top := y END;
  OS.FadeCursor; OS.EraseBlock(f.x, f.y, f.w, top - f.y);
  (*draw scroll bar*)
  IF f.margin > 0 THEN OS.InvertBlock(f.x + scrollW, f.y, 1, top - f.y) END;
  (*redraw lines and build new line descriptors; L0 is last valid line descriptor*)
  t := f.text;
  LOOP NEW(L);
    t.SetPos(org); MeasureLine(t, L); IF (L.len = 0) OR (y - L.asc - L.dsc < f.y + f.margin) THEN EXIT END;
    t.SetPos(org); DrawLine(t, L.len, f.x + scrollW + f.margin, f.x + f.w - f.margin, y - L.asc); org := org + L.len;
    DEC(y, L.asc + L.dsc + f.lsp); L0.next := L; L0 := L; IF t.pos >= t.len THEN EXIT END
  END;
  L0.next := f.lines
END RedrawFrom;

PROCEDURE (f: Frame) GetPointPos (x0, y0: INTEGER; VAR p: Position);
  VAR t: Texts0.Text; ch: CHAR; L: Line; dx, x, y, asc, dsc: INTEGER; pat: OS.Pattern;
BEGIN
  (*find line containing y0*)
  L := f.lines.next; p.y := f.y + f.h - f.margin; p.org := f.org;
  WHILE (L # f.lines) & (y0 < p.y - L.asc - L.dsc - f.lsp) & L.eol DO
    DEC(p.y, L.asc + L.dsc + f.lsp); p.org := p.org + L.len; L := L.next
  END;
  DEC(p.y, L.asc);
  (*find character containing x0*)
  p.x := f.x + scrollW + f.margin; p.L := L; p.pos := p.org; t := f.text; t.SetPos(p.pos);
  LOOP IF t.pos >= t.len THEN p.dx := 0; EXIT END;
    t.Read(ch); GetMetric(t.attr, ch, dx, x, y, asc, dsc, pat);
    IF (ch = EOL) OR (p.x + dx > x0) THEN p.dx := dx; EXIT ELSE INC(p.pos); INC(p.x, dx) END;
  END
END GetPointPos;

PROCEDURE (f: Frame) GetCharPos (pos: LONGINT; VAR p: Position);
  VAR t: Texts0.Text; ch: CHAR; L: Line; dx, x, y, asc, dsc: INTEGER; pat: OS.Pattern;
i: LONGINT;
BEGIN
  (*find line containing pos*)
  L := f.lines.next; p.y := f.y + f.h - f.margin; p.org := f.org; p.pos := pos;
  WHILE (L # f.lines) & (pos >= p.org + L.len) & L.eol DO p.org := p.org + L.len; DEC(p.y, L.asc + L.dsc + f.lsp); L := L.next END;
  DEC(p.y, L.asc); p.L := L;
  (*find character at pos*)
  p.x := f.x + scrollW + f.margin; t := f.text; t.SetPos(p.org);
  FOR i := 1 TO p.pos - p.org DO
    t.Read(ch); GetMetric(t.attr, ch, dx, x, y, asc, dsc, pat);
    INC(p.x, dx)
  END;
  IF t.pos >= t.len THEN p.dx := 0 ELSE t.Read(ch); GetMetric(t.attr, ch, p.dx, x, y, asc, dsc, pat) END
END GetCharPos;

PROCEDURE (f: Frame) CallCommand;
  VAR x, y, i: INTEGER; buttons: SET; p: Position; t: Texts0.Text; ch: CHAR; cmd: ARRAY 64 OF CHAR;
BEGIN REPEAT OS.GetMouse(buttons, x, y) UNTIL buttons = {};
  f.GetPointPos(x, y, p); t := f.text; t.SetPos(p.org); t.Read(ch);
  REPEAT
    WHILE (t.pos < t.len) & (ch # EOL) & ((CAP(ch) < "A") OR (CAP(ch) > "Z")) DO t.Read(ch) END;
    i := 0;
    WHILE (CAP(ch) >= "A") & (CAP(ch) <= "Z") OR (ch >= "0") & (ch <= "9") OR (ch = ".") DO
      cmd[i] := ch; INC(i); t.Read(ch)
    END;
    cmd[i] := 0X;
  UNTIL (t.pos >= t.len) OR (ch = EOL) OR (t.pos > p.pos);
  cmdFrame := f; cmdPos := t.pos; OS.Call(cmd)
END CallCommand;

PROCEDURE (f: Frame) RemoveCaret*;
BEGIN
  IF f.caret.pos >= 0 THEN f.FlipCaret; f.caret.pos := -1 END
END RemoveCaret;

PROCEDURE (f: Frame) SetCaret* (pos: LONGINT);
  VAR p: Position;
BEGIN
  IF pos < 0 THEN pos := 0 ELSIF pos > f.text.len THEN pos := f.text.len END;
  f.SetFocus; f.GetCharPos(pos, p);
  IF p.x < f.x + f.w - f.margin THEN f.caret := p; f.FlipCaret END
END SetCaret;

PROCEDURE (f: Frame) RemoveSelection*;
BEGIN IF f.selBeg.pos >= 0 THEN f.FlipSelection(f.selBeg, f.selEnd); f.selBeg.pos := -1 END
END RemoveSelection;

PROCEDURE (f: Frame) SetSelection* (from, to: LONGINT);
BEGIN f.RemoveSelection;
  f.GetCharPos(from, f.selBeg); f.GetCharPos(to, f.selEnd); f.FlipSelection(f.selBeg, f.selEnd); f.selTime := OS.Time()
END SetSelection;

PROCEDURE (f: Frame) Defocus*;
BEGIN f.RemoveCaret; f.Defocus^
END Defocus;

PROCEDURE (f: Frame) Neutralize*;
BEGIN f.RemoveCaret; f.RemoveSelection
END Neutralize;

PROCEDURE (f: Frame) Draw*;
BEGIN f.RedrawFrom(f.y + f.h)
END Draw;

PROCEDURE (f: Frame) Modify* (dy: INTEGER);
  VAR y: INTEGER;
BEGIN y := f.y; f.Modify^ (dy);
  IF y > f.y THEN f.RedrawFrom(y) ELSE f.RedrawFrom(f.y) END
END Modify;

PROCEDURE (f: Frame) HandleMouse* (x, y: INTEGER; buttons: SET);
  VAR p: Position; b: SET; t: Texts0.Text; ch: CHAR; f1: Frame;
BEGIN f.HandleMouse^ (x, y, buttons);
  t := f.text;
  IF (x < f.x + scrollW) & (buttons # {}) THEN (*handle click in scroll bar*)
    REPEAT OS.GetMouse(b, x, y); buttons := buttons + b UNTIL b = {};
    f.Neutralize;
    IF OS.left IN buttons THEN f.GetPointPos(x, y, p); f.org := p.org
    ELSIF OS.right IN buttons THEN f.org := 0
    ELSIF OS.middle IN buttons THEN t.SetPos((f.y + f.h - y) * f.text.len DIV f.h);
      REPEAT t.Read(ch) UNTIL (ch = EOL) OR (t.pos >= t.len);
      f.org := t.pos
    END;
    f.RedrawFrom(f.y + f.h)
  ELSE (*handle click in text area*)
    f.GetPointPos(x, y, p);
    IF OS.left IN buttons THEN
      IF p.pos # f.caret.pos THEN f.SetCaret(p.pos) END
    ELSIF OS.middle IN buttons THEN t.SetPos(p.pos); t.Read(ch);
      IF t.attr.elem = NIL THEN f.CallCommand ELSE t.attr.elem.HandleMouse(f, x, y) END
    ELSIF OS.right IN buttons THEN f.RemoveSelection;
      f.selBeg := p; f.selEnd := p; f.selTime := OS.Time();
      LOOP OS.GetMouse(b, x, y); buttons := buttons + b; IF b = {} THEN EXIT END;
        OS.DrawCursor(x, y); f.GetPointPos(x, y, p);
        IF p.pos < f.selBeg.pos THEN p := f.selBeg END; (*dont expand selection to the left*)
        IF p.pos < t.len THEN INC(p.pos); INC(p.x, p.dx) END;
        IF p.pos # f.selEnd.pos THEN
          IF p.pos > f.selEnd.pos THEN f.FlipSelection(f.selEnd, p) ELSE f.FlipSelection(p, f.selEnd) END;
          f.selEnd := p
        END
      END;
      (*handle interclick*)
      IF OS.left IN buttons THEN t.Delete(f.selBeg.pos, f.selEnd.pos)
      ELSIF (OS.middle IN buttons) & (Viewers0.focus # NIL) & (Viewers0.focus IS Frame) THEN
        f1 := Viewers0.focus(Frame);
        IF f1.caret.pos >= 0 THEN f1.text.Insert(f1.caret.pos, t, f.selBeg.pos, f.selEnd.pos) END
      END
    END
  END
END HandleMouse;

PROCEDURE (f: Frame) HandleKey* (ch: CHAR);
  VAR pos: LONGINT;
BEGIN pos := f.caret.pos;
  IF pos >= 0 THEN
    IF ch = DEL THEN
      IF pos > 0 THEN f.text.Delete(pos - 1, pos); f.SetCaret(pos - 1) END 
    ELSE f.text.SetPos(pos); f.text.Write(ch); f.SetCaret(pos + 1)
    END
  END
END HandleKey;

PROCEDURE (f: Frame) Handle* (VAR m: OS.Message);
  VAR t: Texts0.Text; ch: CHAR; VAR dx, x, y, asc, dsc: INTEGER; pat: OS.Pattern; p: Position;
BEGIN
  t := f.text;
  WITH
    m: Texts0.NotifyInsMsg DO
      IF m.t = t THEN
        IF m.beg < f.org THEN f.org := f.org + (m.end - m.beg)
        ELSE
          f.Neutralize; OS.FadeCursor;
          f.GetCharPos(m.beg, p);
          t.SetPos(m.beg); t.Read(ch); GetMetric(t.attr, ch, dx, x, y, asc, dsc, pat);
          IF (m.end = m.beg+1) & (ch # EOL) & (p.L # f.lines) & (asc+dsc <= p.L.asc+p.L.dsc) THEN
            IF p.x + dx <= f.x + f.w - f.margin THEN
              OS.CopyBlock(p.x, p.y-p.L.dsc, f.x+f.w-f.margin-dx-p.x, p.L.asc+p.L.dsc,
                p.x+dx, p.y-p.L.dsc);
              OS.EraseBlock(p.x, p.y-p.L.dsc, dx, p.L.asc + p.L.dsc);
              IF t.attr.elem = NIL THEN OS.DrawPattern(pat, p.x + x, p.y + y)
              ELSE t.attr.elem.Draw(p.x, p.y)
              END
            ELSE OS.EraseBlock(p.x, p.y-p.L.dsc, f.x+f.w-p.x, p.L.asc+p.L.dsc)
            END;
            INC(p.L.len); INC(p.L.wid, dx)
          ELSE f.RedrawFrom(p.y + p.L.asc)
          END
        END
      END
  | m: Texts0.NotifyDelMsg DO
      IF m.t = t THEN
        IF m.end <= f.org THEN f.org := f.org - (m.end - m.beg)
        ELSE
          f.Neutralize;
          IF m.beg < f.org THEN f.org := m.beg; f.RedrawFrom(f.y + f.h)
          ELSE f.GetCharPos(m.beg, p); f.RedrawFrom(p.y + p.L.asc)
          END
        END
      END
  | m: Texts0.NotifyReplMsg DO
      IF (m.t = t) & (m.end > f.org) THEN
        f.Neutralize;
        IF m.beg < f.org THEN m.beg := f.org END;
        f.GetCharPos(m.beg, p); f.RedrawFrom(p.y + p.L.asc)
      END
  | m: SelectionMsg DO
      IF (f.selBeg.pos >= 0) & ((m.f = NIL) OR (m.f.selTime < f.selTime)) THEN m.f := f END
  ELSE
  END
END Handle;

PROCEDURE New* (t: Texts0.Text): Frame;
  VAR f: Frame; fnt: OS.Font;
BEGIN NEW(f); f.text := t;
  f.org := 0; f.caret.pos := -1; f.selBeg.pos := -1; f.lsp := 2; f.margin := 5;
  NEW(f.lines); f.lines.next := f.lines; fnt := OS.DefaultFont(); f.lines.asc := fnt.maxY; f.lines.dsc := -fnt.minY; f.lines.len := 0;
  RETURN f
END New;

PROCEDURE NewMenu* (name, menu: ARRAY OF CHAR): Frame;
  VAR t: Texts0.Text; f: Frame; i: INTEGER;
BEGIN NEW(t); t.Clear;
  i := 0; WHILE name[i] # 0X DO t.Write(name[i]); INC(i) END;
  t.Write(" "); t.Write("|"); t.Write(" ");
  i := 0; WHILE menu[i] # 0X DO t.Write(menu[i]); INC(i) END;
  f := New(t); f.margin := 0; RETURN f
END NewMenu;

PROCEDURE (f: Frame) Copy* (): Viewers0.Frame;
  VAR f1: Frame;
BEGIN f1 := New(f.text); f1.margin := f.margin; RETURN f1
END Copy;

PROCEDURE GetSelection* (VAR f: Frame);
  VAR m: SelectionMsg;
BEGIN m.f := NIL; Viewers0.Broadcast(m); f := m.f
END GetSelection;

END TextFrames0.
