Forráskód Böngészése

Add resizable windows and a complete tutorial to tvision-m2

Resizing:
- Windows have a bottom-right resize handle (drawn with the block glyph);
  dragging it resizes the window, routed to the window even when the pointer
  leaves it (like title dragging).
- New WindowResize(w, ww, hh): clamps to a minimum and the screen, and grows
  or shrinks any direct child flagged with the new vfExpand flag by the same
  delta, so an editor child follows its window.
- examples/edit gained a Window menu: "Size..." (a dialog with Width/Height
  fields) and "Maximize"; the editor child is flagged vfExpand.

Docs:
- TUTORIAL.md rewritten as a complete 12-section guide (build, event loop,
  views, widgets, focus/commands, mouse, menus, dialogs, editor, graphics
  backend, the edit example, GNU Modula-2 pitfalls).
- README.md/SUMMARY.md updated.
Eric Streit 4 hete
szülő
commit
c3d0ec2633

+ 2 - 1
tvision-m2/README.md

@@ -46,7 +46,8 @@ sibling project:
   clipped to each window; overlapping windows are ordered (z-order) and the
   clipped to each window; overlapping windows are ordered (z-order) and the
   one you click comes to the front.
   one you click comes to the front.
 - Windows: double-line frame, title, ` X ` close box, draggable by the title
 - Windows: double-line frame, title, ` X ` close box, draggable by the title
-  bar with the mouse.
+  bar with the mouse and resizable by the bottom-right `■` handle
+  (`WindowResize`; children flagged `vfExpand` grow with the window).
 - Menu bar + drop-down menus (mouse and keyboard).
 - Menu bar + drop-down menus (mouse and keyboard).
 - Multi-line editor widget (`VEditor`): insert/overwrite, arrows/Home/End/
 - Multi-line editor widget (`VEditor`): insert/overwrite, arrows/Home/End/
   PgUp/PgDn, Enter/Backspace/Delete, Tab indent, mouse positioning, wheel
   PgUp/PgDn, Enter/Backspace/Delete, Tab indent, mouse positioning, wheel

+ 3 - 2
tvision-m2/SUMMARY.md

@@ -29,8 +29,9 @@ from the captured terminal stream).
 - 16-colour fg/bg, explicit blink on/off, UTF-8 box drawing, clipping stack.
 - 16-colour fg/bg, explicit blink on/off, UTF-8 box drawing, clipping stack.
 - Diffed redraw: only changed cells are written.
 - Diffed redraw: only changed cells are written.
 - View tree: groups and windows own children; per-window clipping; z-order
 - View tree: groups and windows own children; per-window clipping; z-order
-  with click-to-front; windows draggable by the title row (any direction) and
-  clamped so the title stays on screen; ` X ` close box.
+  with click-to-front; windows draggable by the title row (any direction),
+  resizable by the bottom-right handle (`WindowResize`), clamped so the
+  title stays on screen; ` X ` close box.
 - Widgets: static text, frame, input line (insert/backspace/delete/Home/End/
 - Widgets: static text, frame, input line (insert/backspace/delete/Home/End/
   arrows), push button, check box, radio buttons, list box (selection, wheel
   arrows), push button, check box, radio buttons, list box (selection, wheel
   scroll) and vertical scroll bar.
   scroll) and vertical scroll bar.

+ 211 - 89
tvision-m2/TUTORIAL.md

@@ -1,74 +1,140 @@
-# tvision-m2 - tutorial
+# tvision-m2 - a complete tutorial
 
 
-Build the library once:
+This is a step-by-step guide to writing applications with **tvision-m2**, a
+Turbo-Vision / TVision style character-cell UI toolkit written in ISO GNU
+Modula-2 (`gm2 -fiso`).
+
+You can run the same program in a **terminal** (ANSI backend) or in a
+**graphics window** (tigr backend); the toolkit code is identical, only the
+`Init`/`ReadEvent`/`Finish` calls change.
+
+The full reference is in `API.md`; this document teaches by example.
+
+---
+
+## 1. Building the library
+
+From the `tvision-m2/` directory:
 
 
-    cd tvision-m2
     gm2 -fiso -c tvtty.def
     gm2 -fiso -c tvtty.def
     gm2 -fiso -I. -c tv.mod
     gm2 -fiso -I. -c tv.mod
+    gm2 -fiso -I. -c tvfont.mod          # only needed for the graphics backend
+    gm2 -fiso -I. -I../modula2 -c tvg.mod # idem
+
+or simply `make`.  To build a program:
+
+    gm2 -fiso -I. tv.o myapp.mod -o myapp            # terminal
+    gm2 -fiso -I. tv.o tvg.o tvfont.o ../modula2/tigr.o \
+         ../modula2/helper.o myapp.mod -o myapp -lGL -lX11   # graphics
 
 
-Each program is then:
+Run it from a real terminal (or a graphical session for the tigr backend).
 
 
-    gm2 -fiso -I. tv.o myapp.mod -o myapp
-    ./myapp          # run from a real terminal
+---
 
 
-## 1. The smallest program
+## 2. The program skeleton
 
 
-An empty blue desktop with a status line; `Esc` quits.
+Every application has the same shape: initialise, build the views, then loop
+reading events and redrawing.
 
 
 ```modula2
 ```modula2
 MODULE hello;
 MODULE hello;
 IMPORT tv;
 IMPORT tv;
 VAR e : tv.Event; quit : BOOLEAN; st : tv.ViewPtr;
 VAR e : tv.Event; quit : BOOLEAN; st : tv.ViewPtr;
 BEGIN
 BEGIN
-    IF NOT tv.Init() THEN HALT END;
+    IF NOT tv.Init() THEN HALT END;          (* raw mode + alternate screen *)
     st := tv.NewView(tv.VText, 0, tv.Rows()-1, tv.Cols(), 1);
     st := tv.NewView(tv.VText, 0, tv.Rows()-1, tv.Cols(), 1);
     tv.SetText(st, " Hello - press Esc");
     tv.SetText(st, " Hello - press Esc");
     tv.AddView(tv.Desk(), st);
     tv.AddView(tv.Desk(), st);
-    tv.Finish;
+    tv.Finish;                                (* draw + flush *)
+
     quit := FALSE;
     quit := FALSE;
     WHILE NOT quit DO
     WHILE NOT quit DO
-        IF tv.Resized() THEN tv.Finish END;
-        IF tv.ReadEvent(e) THEN
-            IF (e.kind = tv.evKey) AND (e.key = tv.kEsc) THEN quit := TRUE END;
+        IF tv.Resized() THEN tv.Finish END;   (* SIGWINCH / size change *)
+        IF tv.ReadEvent(e) THEN               (* waits ~0.1 s *)
+            IF (e.kind = tv.evKey) AND (e.key = tv.kEsc) THEN
+                quit := TRUE
+            ELSE
+                IF tv.HandleEvent(e) = tv.cmClose THEN quit := TRUE END
+            END;
             IF NOT quit THEN tv.Finish END
             IF NOT quit THEN tv.Finish END
         END
         END
     END;
     END;
-    tv.Done
+    tv.Done                                   (* restore the terminal *)
 END hello.
 END hello.
 ```
 ```
 
 
-The shape of every program: `Init`; add views to `Desk()`; loop calling
-`Resized` and `ReadEvent`; redraw with `Finish`; `Done` on exit.
+`tv.HandleEvent(e)` dispatches the event to the menu bar, the modal dialog
+(if any) or the desktop, and returns a **command** (`cmNone`, `cmClose`, or
+the tag of the control that fired).  `tv.Sender()` tells you which view
+produced it.
+
+---
 
 
-## 2. A window with widgets
+## 3. Views and the view tree
 
 
-Views use absolute screen coordinates.  `NewView(kind, x, y, w, h)` creates
-one; `AddView(parent, child)` attaches it.
+Everything on screen is a **view**.  `NewView(kind, x, y, w, h)` creates one
+at absolute screen coordinates; `AddView(parent, child)` attaches it.  The
+desktop (`tv.Desk()`) is the root; put your top-level windows in it.
 
 
 ```modula2
 ```modula2
+VAR w, t, b : tv.ViewPtr;
 w := tv.NewView(tv.VWindow, 3, 2, 50, 12);
 w := tv.NewView(tv.VWindow, 3, 2, 50, 12);
-tv.SetText(w, "Editor");                     (* window title *)
+tv.SetText(w, "My window");
+tv.AddView(tv.Desk(), w);
 
 
 t := tv.NewView(tv.VText, 5, 4, 20, 1);
 t := tv.NewView(tv.VText, 5, 4, 20, 1);
 tv.SetText(t, "Name:"); tv.AddView(w, t);
 tv.SetText(t, "Name:"); tv.AddView(w, t);
 
 
-input := tv.NewView(tv.VInput, 12, 4, 34, 1);
-tv.SetText(input, "Modula-2"); tv.AddView(w, input);
-
-b := tv.NewView(tv.VButton, 12, 10, 8, 1);
+b := tv.NewView(tv.VButton, 5, 9, 10, 1);
 tv.SetText(b, "OK"); tv.SetTag(b, tv.cmOK); tv.AddView(w, b);
 tv.SetText(b, "OK"); tv.SetTag(b, tv.cmOK); tv.AddView(w, b);
-
-tv.AddView(tv.Desk(), w);
-tv.FocusView(input);
 ```
 ```
 
 
-A `VWindow` clips its children, can be dragged by its title row and closed
-with the ` X ` box.  `Tab`/arrows move focus; inputs are edited with the
-usual keys.
+- **Windows** draw a double-line frame and a title, clip their children and
+  can be **dragged** by the title row and **resized** by the `■` handle in
+  the bottom-right corner.  ` X ` closes them (HandleEvent returns
+  `cmClose`).
+- Overlapping windows have a **z-order**: clicking one brings it to the
+  front (`BringToFront`).
+- Children are drawn after their parent; a window clips them
+  (`PushClipRect` internally).
 
 
-## 3. Reacting to commands
+Useful calls: `BringToFront(v)`, `MoveViewBy(v,dx,dy)`, `DelView(v)`,
+`WindowResize(w, ww, hh)`.
 
 
-`HandleEvent` returns `cmNone`, `cmClose`, or the tag you gave a control:
+---
+
+## 4. The widget set
+
+| kind | what it is | key fields |
+|---|---|---|
+| `VText` | static text | `text` |
+| `VFrame` | single/double box | `rect` |
+| `VInput` | one-line edit field | `text`, `value` |
+| `VButton` | push button | `text`, `tag` |
+| `VCheck` | check box | `text`, `flags` (`vfSelected`) |
+| `VRadio` | radio button (self-exclusive) | `text`, `flags` |
+| `VList` | list box | `list`, `value` |
+| `VScroll` | vertical scroll bar | `value`, `minv`, `maxv` |
+| `VEditor` | multi-line editor | `ed` |
+
+Field access uses `^` because views are pointers: `v^.text`, `v^.flags`,
+`v^.value`.  Flags are bits: `vfDisabled`, `vfSelected`, `vfFramed`,
+`vfExpand`.
+
+Radio buttons are automatically exclusive among their siblings, so you do not
+manage the group yourself.
+
+---
+
+## 5. Focus and commands
+
+`Tab` / `Shift-Tab` (and up/down) move the keyboard focus through the
+focusable widgets (input, button, check, radio, list, scroll, editor).
+`FocusView(v)` sets it programmatically.  The focused input gets the
+hardware cursor.
+
+Every control can carry a **command tag** (`SetTag`).  When it is activated,
+`HandleEvent` returns that tag:
 
 
 ```modula2
 ```modula2
 cmd := tv.HandleEvent(e);
 cmd := tv.HandleEvent(e);
@@ -79,13 +145,29 @@ ELSIF cmd = tv.cmOK THEN
 END;
 END;
 ```
 ```
 
 
-Use custom tags >= 100 to distinguish your own buttons.
+Use the built-ins `cmOK`, `cmCancel`, `cmYes`, `cmNo`, `cmClose`, or any
+value >= 100 for your own commands.
+
+---
 
 
-## 4. Menus
+## 6. Mouse
+
+Mouse handling is automatic: click a control to focus/press it, drag a
+window title to move it, drag the `■` corner to resize, click list rows and
+use the wheel to scroll.  You only call `HandleEvent`.
+
+To make a child grow/shrink with its window, set `vfExpand`:
 
 
 ```modula2
 ```modula2
-CONST mQuit = 101; mAbout = 102;
+edView^.flags := edView^.flags + tv.vfExpand;
+```
+
+---
 
 
+## 7. Menus
+
+```modula2
+CONST mQuit = 101; mAbout = 102;
 tv.MenuBarInit;
 tv.MenuBarInit;
 tv.MenuAdd("File");
 tv.MenuAdd("File");
 tv.MenuItemAdd("Open", 100);
 tv.MenuItemAdd("Open", 100);
@@ -95,109 +177,149 @@ tv.MenuAdd("Help");
 tv.MenuItemAdd("About", mAbout);
 tv.MenuItemAdd("About", mAbout);
 ```
 ```
 
 
-The bar appears on row 0.  Clicking a title opens the drop-down; `F10`,
-arrows, Enter and Esc work too.  Selecting an item makes `HandleEvent`
-return its tag:
+The bar appears on row 0.  Click a title (or press `F10`) to open the
+drop-down; Left/Right switch menus, Up/Down move, Enter/Space or a click
+selects, Esc or a click outside closes.  Selecting an item makes
+`HandleEvent` return its tag (`Sender` is `NIL`).
 
 
-```modula2
-IF cmd = mQuit THEN quit := TRUE
-ELSIF cmd = mAbout THEN
-    IF tv.MessageBox("About", "my app", tv.mbOK) = tv.cmOK THEN END
-END;
-```
+---
+
+## 8. Dialogs and message boxes
+
+`MessageBox(title, text, kind)` with `mbOK`, `mbOKCancel` or `mbYesNo`
+returns `cmOK`/`cmCancel`/`cmYes`/`cmNo`.  `Message(t, x)` is the OK
+shorthand.
 
 
-## 5. Dialogs
+For a form use the dialog builder:
 
 
 ```modula2
 ```modula2
 VAR iName, cOn, cmd : INTEGER; s : ARRAY [0..127] OF CHAR;
 VAR iName, cOn, cmd : INTEGER; s : ARRAY [0..127] OF CHAR;
 ...
 ...
 tv.Dialog("Options", 44, 12);
 tv.Dialog("Options", 44, 12);
-iName := tv.DlgInput("Name:", "Modula-2");
+iName := tv.DlgInput("Name:", "Modula-2");   (* -> field index *)
 cOn   := tv.DlgCheck("Enabled", TRUE);
 cOn   := tv.DlgCheck("Enabled", TRUE);
+tv.DlgRadio("Fast", TRUE);                    (* radios are exclusive *)
+tv.DlgRadio("Safe", FALSE);
 tv.DlgButton("OK", tv.cmOK);
 tv.DlgButton("OK", tv.cmOK);
 tv.DlgButton("Cancel", tv.cmCancel);
 tv.DlgButton("Cancel", tv.cmCancel);
-cmd := tv.DlgRun;                 (* blocks until a button / Esc *)
+cmd := tv.DlgRun;                             (* blocks *)
 IF cmd = tv.cmOK THEN
 IF cmd = tv.cmOK THEN
-    tv.DlgTextAt(iName, s);       (* read the field *)
-    IF tv.DlgValue(cOn) = 1 THEN tv.Message("Info", "Enabled") END
+    tv.DlgTextAt(iName, s);                   (* read an input *)
+    IF tv.DlgValue(cOn) = 1 THEN ... END      (* read a check/radio *)
 END;
 END;
 ```
 ```
 
 
-Radios added with `DlgRadio` are automatically exclusive.  For simple
-"notice" dialogs use `Message` / `MessageBox`.
+`DlgRun` returns the pressed button's tag, `cmCancel` on Esc, `cmClose`
+from the close box.
 
 
-## 6. Lists and mouse
+---
 
 
-`VList` shows a caller-owned `ListData`:
-
-```modula2
-VAR data : tv.ListData;
-data.items[0] := "Alpha"; data.items[1] := "Bravo"; data.count := 2;
-list := tv.NewView(tv.VList, 5, 4, 20, 6);
-list^.list := CAST(tv.ListDataPtr, ADR(data));
-tv.AddView(w, list);
-```
+## 9. The editor
 
 
-The mouse works automatically: click to focus/press buttons, drag window
-titles, click list rows and scroll with the wheel.  You only need
-`HandleEvent`; the toolkit does the hit-testing.
-
-## 7. A text editor
+Declare one `EditorData` buffer (a few tens of KB) and attach it to a
+`VEditor` view:
 
 
 ```modula2
 ```modula2
 VAR ed : tv.EditorData; edp : tv.EditorDataPtr; ev : tv.ViewPtr;
 VAR ed : tv.EditorData; edp : tv.EditorDataPtr; ev : tv.ViewPtr;
-...
 edp := CAST(tv.EditorDataPtr, ADR(ed));
 edp := CAST(tv.EditorDataPtr, ADR(ed));
 IF NOT tv.EditorLoad(edp, "notes.txt") THEN
 IF NOT tv.EditorLoad(edp, "notes.txt") THEN
-    tv.EditorInit(edp); tv.EditorAddLine(edp, "Type here...")
+    tv.EditorInit(edp);
+    tv.EditorAddLine(edp, "MODULE hello;")
 END;
 END;
-tv.EditorSetLanguage(edp, tv.langModula2);   (* or langPascal / langOberon *)
-tv.EditorSetIndent(edp, 2);                  (* auto-indent: 2 spaces per level; 0 = off *)
+tv.EditorSetLanguage(edp, tv.langModula2);   (* or langPascal / langOberon / langNone *)
+tv.EditorSetIndent(edp, 2);                  (* auto-indent step, 0 = off *)
+
 ev := tv.NewView(tv.VEditor, 2, 2, 60, 20);
 ev := tv.NewView(tv.VEditor, 2, 2, 60, 20);
+ev^.flags := ev^.flags + tv.vfExpand;
 tv.EditorAttach(ev, edp);
 tv.EditorAttach(ev, edp);
 tv.AddView(win, ev);
 tv.AddView(win, ev);
 tv.FocusView(ev);
 tv.FocusView(ev);
 ...
 ...
 IF tv.EditorSave(edp, "notes.txt") THEN ... END;
 IF tv.EditorSave(edp, "notes.txt") THEN ... END;
+IF tv.EditorDirty(edp) THEN ... END;         (* modified since load/save *)
 ```
 ```
 
 
-See `examples/edit.mod` for a full-screen editor with a File menu.
+The editor supports:
+
+- insert/overwrite (`Ins`), Enter, Backspace/Delete with line join,
+  arrows/Home/End/PgUp/PgDn, Tab (indent to a 4-column stop);
+- mouse: click to place the cursor, wheel to scroll;
+- a Ln/Col/Insert/Modified status row at the bottom;
+- **syntax highlighting** for Modula-2, Pascal and Oberon (keywords,
+  strings, numbers, `(* *)` / `{ }` / `//` comments);
+- **keyword up-casing** for Modula-2 and Oberon (`begin` -> `BEGIN`);
+- **auto-indentation**: Enter copies the leading spaces and adds a level
+  after `BEGIN`/`THEN`/`DO`/...; a line starting with `END`/`UNTIL`/`ELSE`
+  is dedented.
+
+See `examples/edit.mod` for a full-screen editor with File (New/Save/Quit),
+Window (Size/Maximize) and Syntax menus.
+
+---
 
 
-## 8. Running in a graphics window (tigr)
+## 10. Running in a graphics window
 
 
-`tvg` is a drop-in replacement for the terminal loop:
+Replace the terminal loop with the tigr backend; the rest of the program is
+unchanged.
 
 
 ```modula2
 ```modula2
 IMPORT tv, tvg;
 IMPORT tv, tvg;
 ...
 ...
-IF NOT tvg.InitGfx("my app", 90, 30) THEN HALT END;
-tv.MenuBarInit; ... build views on tv.Desk() ...
+IF NOT tvg.InitGfx("my app", 90, 30) THEN HALT END;  (* title, cols, rows *)
+... build views on tv.Desk() exactly as before ...
 tvg.Finish;
 tvg.Finish;
+quit := FALSE;
 WHILE NOT quit DO
 WHILE NOT quit DO
     IF tvg.ReadEvent(e) THEN
     IF tvg.ReadEvent(e) THEN
-        cmd := tv.HandleEvent(e);          (* same events, same commands *)
-        IF (e.kind = tv.evKey) AND (e.key = tv.kEsc) THEN quit := TRUE END;
-        tvg.Finish
+        IF (e.kind = tv.evKey) AND (e.key = tv.kEsc) THEN quit := TRUE
+        ELSE
+            cmd := tv.HandleEvent(e);
+            ...
+        END;
+        IF NOT quit THEN tvg.Finish END
     END;
     END;
     IF tvg.Closed() THEN quit := TRUE END
     IF tvg.Closed() THEN quit := TRUE END
 END;
 END;
 tvg.Done
 tvg.Done
 ```
 ```
 
 
-Build it with `make tvg.o` then `make -C examples gfx`; see `examples/gfx.mod`.
-The toolkit code (windows, widgets, menus, dialogs) is identical.
+`tvg` renders the cell buffer with a bundled 8x16 monospaced VGA font and
+maps tigr keyboard/mouse to the same `tv.Event`, so menus, dialogs, the
+editor and mouse dragging all work unchanged.  Build it as shown in §1; see
+`examples/gfx.mod`.
 
 
-## 9. More examples
+---
 
 
-See `examples/` (`empty`, `window`, `dialog`, `menus`) and build them with
-`make`.  See `API.md` for the full reference.
+## 11. Walk-through: `examples/edit.mod`
 
 
-## Pitfalls (GNU Modula-2)
+A minimal but complete editor:
 
 
-- Pointer fields need `^` (`v^.rect`).
-- Value-returning no-arg procedures need `()` in a `.def`
-  (`Cols() : INTEGER`).
+1. `tv.Init`, then a menu bar (`File`, `Window`, `Syntax`).
+2. A full-screen `VWindow` filling the desktop minus the menu and status
+   rows.
+3. A `VEditor` child sized to the window interior, flagged `vfExpand`, with
+   an `EditorData` loaded from `tvision-edit.txt` (or a sample).
+4. A `VText` status bar on the last row.
+5. The event loop dispatches keys/mouse; menu commands call `EditorSave`,
+   `EditorInit`, `WindowResize` or switch the language; the window redraws
+   with `tv.Finish`.
+
+Run it from a terminal:
+
+    cd examples && make && ./edit
+
+---
+
+## 12. GNU Modula-2 pitfalls
+
+- Pointer fields need `^`: `v^.rect`, not `v.rect`.
+- Value-returning no-argument procedures need `()` in a `.def`
+  (`PROCEDURE Cols() : INTEGER;`).
 - `INTEGER` compared with `HIGH(array)` needs `VAL(INTEGER, HIGH(a))`.
 - `INTEGER` compared with `HIGH(array)` needs `VAL(INTEGER, HIGH(a))`.
 - A `BEGIN ... END` block cannot be a `CASE` branch - use a helper.
 - A `BEGIN ... END` block cannot be a `CASE` branch - use a helper.
-- `Present` is diffed; if you draw outside the loop (e.g. before `Init`),
-  the cells do not exist yet.
+- `EXIT` is only allowed inside a `LOOP`, not `WHILE`/`REPEAT`.
+- Cast an address to a view/buffer pointer with `CAST(tv.EditorDataPtr, ADR(x))`.
+
+That is everything you need to build a Turbo-Vision style application.
+Have a look at `examples/` for complete programs and `API.md` for the full
+reference.

BIN
tvision-m2/demo


BIN
tvision-m2/examples/dialog


BIN
tvision-m2/examples/edit


+ 39 - 0
tvision-m2/examples/edit.mod

@@ -19,6 +19,8 @@ CONST
     mModula2 = 110;
     mModula2 = 110;
     mPascal  = 111;
     mPascal  = 111;
     mOberon  = 112;
     mOberon  = 112;
+    mSize    = 120;
+    mMax     = 121;
 
 
 VAR
 VAR
     ed : tv.EditorData;
     ed : tv.EditorData;
@@ -29,6 +31,21 @@ VAR
     cmd : INTEGER;
     cmd : INTEGER;
     sender : tv.ViewPtr;
     sender : tv.ViewPtr;
     status : ARRAY [0..95] OF CHAR;
     status : ARRAY [0..95] OF CHAR;
+    s1, s2, tmp : ARRAY [0..31] OF CHAR;
+    iW, iH, nw, nh : INTEGER;
+
+PROCEDURE ToInt (s : ARRAY OF CHAR) : INTEGER;
+    VAR i, n : INTEGER; neg : BOOLEAN;
+BEGIN
+    i := 0; n := 0; neg := FALSE;
+    IF s[0] = '-' THEN neg := TRUE; i := 1 END;
+    WHILE (i <= VAL(INTEGER, HIGH(s))) AND (s[i] >= '0') AND (s[i] <= '9') DO
+        n := n * 10 + VAL(INTEGER, ORD(s[i]) - ORD('0'));
+        i := i + 1
+    END;
+    IF neg THEN n := 0 - n END;
+    RETURN n
+END ToInt;
 
 
 PROCEDURE Sample;
 PROCEDURE Sample;
 BEGIN
 BEGIN
@@ -66,6 +83,9 @@ BEGIN
     tv.MenuItemAdd("Save", mSave);
     tv.MenuItemAdd("Save", mSave);
     tv.MenuSep;
     tv.MenuSep;
     tv.MenuItemAdd("Quit", mQuit);
     tv.MenuItemAdd("Quit", mQuit);
+    tv.MenuAdd("Window");
+    tv.MenuItemAdd("Size...", mSize);
+    tv.MenuItemAdd("Maximize", mMax);
     tv.MenuAdd("Syntax");
     tv.MenuAdd("Syntax");
     tv.MenuItemAdd("Modula-2", mModula2);
     tv.MenuItemAdd("Modula-2", mModula2);
     tv.MenuItemAdd("Pascal", mPascal);
     tv.MenuItemAdd("Pascal", mPascal);
@@ -83,6 +103,7 @@ BEGIN
     tv.EditorSetIndent(edp, 2);
     tv.EditorSetIndent(edp, 2);
 
 
     edView := tv.NewView(tv.VEditor, 2, 2, tv.Cols() - 4, tv.Rows() - 5);
     edView := tv.NewView(tv.VEditor, 2, 2, tv.Cols() - 4, tv.Rows() - 5);
+    edView^.flags := edView^.flags + tv.vfExpand;   (* grow with the window *)
     tv.EditorAttach(edView, CAST(tv.EditorDataPtr, ADR(ed)));
     tv.EditorAttach(edView, CAST(tv.EditorDataPtr, ADR(ed)));
     tv.AddView(w, edView);
     tv.AddView(w, edView);
     tv.FocusView(edView);
     tv.FocusView(edView);
@@ -111,6 +132,24 @@ BEGIN
                         tv.Message("Save", "Could not save the file.")
                         tv.Message("Save", "Could not save the file.")
                     END;
                     END;
                     Refresh
                     Refresh
+                ELSIF cmd = mSize THEN
+                    tv.IntToText(s1, w^.rect.w, 0);
+                    tv.IntToText(s2, w^.rect.h, 0);
+                    tv.Dialog("Window size", 34, 9);
+                    iW := tv.DlgInput("Width:", s1);
+                    iH := tv.DlgInput("Height:", s2);
+                    tv.DlgButton("OK", tv.cmOK);
+                    tv.DlgButton("Cancel", tv.cmCancel);
+                    IF tv.DlgRun() = tv.cmOK THEN
+                        tv.DlgTextAt(iW, s1);
+                        tv.DlgTextAt(iH, s2);
+                        nw := ToInt(s1); nh := ToInt(s2);
+                        tv.WindowResize(w, nw, nh);
+                        Refresh
+                    END
+                ELSIF cmd = mMax THEN
+                    tv.WindowResize(w, tv.Cols() - 2, tv.Rows() - 3);
+                    Refresh
                 ELSIF cmd = mModula2 THEN
                 ELSIF cmd = mModula2 THEN
                     tv.EditorSetLanguage(edp, tv.langModula2); Refresh
                     tv.EditorSetLanguage(edp, tv.langModula2); Refresh
                 ELSIF cmd = mPascal THEN
                 ELSIF cmd = mPascal THEN

BIN
tvision-m2/examples/empty


BIN
tvision-m2/examples/gfx


BIN
tvision-m2/examples/menus


BIN
tvision-m2/examples/window


+ 3 - 0
tvision-m2/tv.def

@@ -53,6 +53,7 @@ CONST
     vfDisabled = 1;
     vfDisabled = 1;
     vfSelected = 2;
     vfSelected = 2;
     vfFramed   = 4;
     vfFramed   = 4;
+    vfExpand   = 8;   (* grows when its window is resized *)
 
 
     MAXLINE  = 128;
     MAXLINE  = 128;
     MAXVIEWS = 128;
     MAXVIEWS = 128;
@@ -175,6 +176,8 @@ PROCEDURE BringToFront (v : ViewPtr);
 PROCEDURE FocusView (v : ViewPtr);
 PROCEDURE FocusView (v : ViewPtr);
 PROCEDURE FocusNext (backwards : BOOLEAN);
 PROCEDURE FocusNext (backwards : BOOLEAN);
 PROCEDURE MoveViewBy (v : ViewPtr; dx, dy : INTEGER);
 PROCEDURE MoveViewBy (v : ViewPtr; dx, dy : INTEGER);
+(* Resize a window; direct children flagged vfExpand grow by the same delta. *)
+PROCEDURE WindowResize (w : ViewPtr; ww, hh : INTEGER);
 
 
 (* Menu bar + drop-down menus.  Typical use:
 (* Menu bar + drop-down menus.  Typical use:
      MenuBarInit;
      MenuBarInit;

+ 56 - 6
tvision-m2/tv.mod

@@ -25,7 +25,7 @@ CONST
     H_DOUBLE = 2550H; V_DOUBLE = 2551H;
     H_DOUBLE = 2550H; V_DOUBLE = 2551H;
     TL_DOUBLE = 2554H; TR_DOUBLE = 2557H; BL_DOUBLE = 255AH; BR_DOUBLE = 255DH;
     TL_DOUBLE = 2554H; TR_DOUBLE = 2557H; BL_DOUBLE = 255AH; BR_DOUBLE = 255DH;
 
 
-    vfPressed = 8;   (* internal, transient *)
+    vfPressed = 16;  (* internal, transient *)
 
 
     MAXMENUS = 8;
     MAXMENUS = 8;
     MAXMITEMS = 16;
     MAXMITEMS = 16;
@@ -66,7 +66,9 @@ VAR
     desk   : ViewPtr;
     desk   : ViewPtr;
     modal  : ViewPtr;
     modal  : ViewPtr;
 
 
-    dragWin : ViewPtr;
+    dragWin  : ViewPtr;
+    resizeWin : ViewPtr;
+    resOX, resOY : INTEGER;
     lastSender : ViewPtr;
     lastSender : ViewPtr;
     dragOX, dragOY : INTEGER;
     dragOX, dragOY : INTEGER;
 
 
@@ -223,7 +225,7 @@ BEGIN
     inLen := 0; inPos := 0;
     inLen := 0; inPos := 0;
     cursorX := 0; cursorY := 0; cursorShown := FALSE;
     cursorX := 0; cursorY := 0; cursorShown := FALSE;
     clipN := 0;
     clipN := 0;
-    modal := NIL; dragWin := NIL; lastSender := NIL;
+    modal := NIL; dragWin := NIL; resizeWin := NIL; lastSender := NIL;
     menuCount := 0; menuOpen := -1; menuSel := 0; menuShown := FALSE;
     menuCount := 0; menuOpen := -1; menuSel := 0; menuShown := FALSE;
     winch := FALSE;
     winch := FALSE;
     dlg := NIL; dlgCount := 0;
     dlg := NIL; dlgCount := 0;
@@ -918,6 +920,37 @@ BEGIN
     FocusView(nxt)
     FocusView(nxt)
 END FocusNext;
 END FocusNext;
 
 
+PROCEDURE WindowResize (w : ViewPtr; ww, hh : INTEGER);
+    VAR c : ViewPtr; dw, dh, minw, minh : INTEGER;
+BEGIN
+    IF w = NIL THEN RETURN END;
+    minw := 12; minh := 4;
+    IF ww < minw THEN ww := minw END;
+    IF hh < minh THEN hh := minh END;
+    IF w^.rect.x + ww > cols THEN ww := cols - w^.rect.x END;
+    IF w^.rect.y + hh > rows THEN hh := rows - w^.rect.y END;
+    IF ww < minw THEN ww := minw END;
+    IF hh < minh THEN hh := minh END;
+    dw := ww - w^.rect.w;
+    dh := hh - w^.rect.h;
+    w^.rect.w := ww;
+    w^.rect.h := hh;
+    c := w^.first;
+    WHILE c # NIL DO
+        IF (c^.flags DIV vfExpand) MOD 2 = 1 THEN
+            c^.rect.w := c^.rect.w + dw;
+            c^.rect.h := c^.rect.h + dh
+        END;
+        c := c^.next
+    END
+END WindowResize;
+
+PROCEDURE InResizeHandle (v : ViewPtr; mx, my : INTEGER) : BOOLEAN;
+BEGIN
+    RETURN (my >= v^.rect.y + v^.rect.h - 2) AND (my < v^.rect.y + v^.rect.h) AND
+           (mx >= v^.rect.x + v^.rect.w - 3) AND (mx < v^.rect.x + v^.rect.w)
+END InResizeHandle;
+
 PROCEDURE MoveViewBy (v : ViewPtr; dx, dy : INTEGER);
 PROCEDURE MoveViewBy (v : ViewPtr; dx, dy : INTEGER);
     VAR c : ViewPtr;
     VAR c : ViewPtr;
 BEGIN
 BEGIN
@@ -955,7 +988,10 @@ BEGIN
     DrawText(v^.rect.x + 2, v^.rect.y, v^.text, title);
     DrawText(v^.rect.x + 2, v^.rect.y, v^.text, title);
     (* close box *)
     (* close box *)
     x2 := v^.rect.x + v^.rect.w - 3;
     x2 := v^.rect.x + v^.rect.w - 3;
-    DrawText(x2, v^.rect.y, " X ", A(White, Red, FALSE))
+    DrawText(x2, v^.rect.y, " X ", A(White, Red, FALSE));
+    (* resize handle in the bottom-right corner *)
+    DrawCh(v^.rect.x + v^.rect.w - 1, v^.rect.y + v^.rect.h - 1,
+           25A0H, A(White, Blue, FALSE))
 END DrawWindow;
 END DrawWindow;
 
 
 PROCEDURE DrawInput(v : ViewPtr);
 PROCEDURE DrawInput(v : ViewPtr);
@@ -2184,6 +2220,13 @@ PROCEDURE HandleWindow(v : ViewPtr; VAR e : Event) : INTEGER;
     VAR w : ViewPtr; nx, ny : INTEGER;
     VAR w : ViewPtr; nx, ny : INTEGER;
 BEGIN
 BEGIN
     IF (e.kind = evMouse) AND e.mpressed THEN
     IF (e.kind = evMouse) AND e.mpressed THEN
+        (* resize handle (bottom-right) ? *)
+        IF InResizeHandle(v, e.mx, e.my) THEN
+            resizeWin := v;
+            resOX := e.mx - (v^.rect.x + v^.rect.w - 1);
+            resOY := e.my - (v^.rect.y + v^.rect.h - 1);
+            RETURN cmNone
+        END;
         (* close box? *)
         (* close box? *)
         IF (e.my = v^.rect.y) AND
         IF (e.my = v^.rect.y) AND
            (e.mx >= v^.rect.x + v^.rect.w - 3) AND (e.mx < v^.rect.x + v^.rect.w) THEN
            (e.mx >= v^.rect.x + v^.rect.w - 3) AND (e.mx < v^.rect.x + v^.rect.w) THEN
@@ -2197,10 +2240,15 @@ BEGIN
             dragOY := e.my - v^.rect.y
             dragOY := e.my - v^.rect.y
         END
         END
     ELSIF (e.kind = evMouse) AND e.mreleased THEN
     ELSIF (e.kind = evMouse) AND e.mreleased THEN
-        dragWin := NIL
+        dragWin := NIL;
+        resizeWin := NIL
     ELSIF (e.kind = evMouse) AND (e.mbtn > 0) AND (NOT e.mpressed) AND
     ELSIF (e.kind = evMouse) AND (e.mbtn > 0) AND (NOT e.mpressed) AND
           (NOT e.mreleased) THEN
           (NOT e.mreleased) THEN
-        IF dragWin # NIL THEN
+        IF resizeWin # NIL THEN
+            WindowResize(resizeWin,
+                         e.mx - resOX - resizeWin^.rect.x + 1,
+                         e.my - resOY - resizeWin^.rect.y + 1)
+        ELSIF dragWin # NIL THEN
             nx := e.mx - dragOX;
             nx := e.mx - dragOX;
             ny := e.my - dragOY;
             ny := e.my - dragOY;
             (* keep the window's title corner on screen *)
             (* keep the window's title corner on screen *)
@@ -2270,6 +2318,8 @@ BEGIN
            sideways stops as soon as the pointer leaves the frame) *)
            sideways stops as soon as the pointer leaves the frame) *)
         IF dragWin # NIL THEN
         IF dragWin # NIL THEN
             cmd := HandleWindow(dragWin, e)
             cmd := HandleWindow(dragWin, e)
+        ELSIF resizeWin # NIL THEN
+            cmd := HandleWindow(resizeWin, e)
         ELSE
         ELSE
             cmd := DispatchMouse(root, e)
             cmd := DispatchMouse(root, e)
         END
         END

BIN
tvision-m2/tv.o