Bläddra i källkod

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 veckor sedan
förälder
incheckning
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
   one you click comes to the front.
 - 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).
 - Multi-line editor widget (`VEditor`): insert/overwrite, arrows/Home/End/
   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.
 - Diffed redraw: only changed cells are written.
 - 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/
   arrows), push button, check box, radio buttons, list box (selection, wheel
   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 -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
 MODULE hello;
 IMPORT tv;
 VAR e : tv.Event; quit : BOOLEAN; st : tv.ViewPtr;
 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);
     tv.SetText(st, " Hello - press Esc");
     tv.AddView(tv.Desk(), st);
-    tv.Finish;
+    tv.Finish;                                (* draw + flush *)
+
     quit := FALSE;
     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
         END
     END;
-    tv.Done
+    tv.Done                                   (* restore the terminal *)
 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
+VAR w, t, b : tv.ViewPtr;
 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);
 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.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
 cmd := tv.HandleEvent(e);
@@ -79,13 +145,29 @@ ELSIF cmd = tv.cmOK THEN
 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
-CONST mQuit = 101; mAbout = 102;
+edView^.flags := edView^.flags + tv.vfExpand;
+```
+
+---
 
+## 7. Menus
+
+```modula2
+CONST mQuit = 101; mAbout = 102;
 tv.MenuBarInit;
 tv.MenuAdd("File");
 tv.MenuItemAdd("Open", 100);
@@ -95,109 +177,149 @@ tv.MenuAdd("Help");
 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
 VAR iName, cOn, cmd : INTEGER; s : ARRAY [0..127] OF CHAR;
 ...
 tv.Dialog("Options", 44, 12);
-iName := tv.DlgInput("Name:", "Modula-2");
+iName := tv.DlgInput("Name:", "Modula-2");   (* -> field index *)
 cOn   := tv.DlgCheck("Enabled", TRUE);
+tv.DlgRadio("Fast", TRUE);                    (* radios are exclusive *)
+tv.DlgRadio("Safe", FALSE);
 tv.DlgButton("OK", tv.cmOK);
 tv.DlgButton("Cancel", tv.cmCancel);
-cmd := tv.DlgRun;                 (* blocks until a button / Esc *)
+cmd := tv.DlgRun;                             (* blocks *)
 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;
 ```
 
-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
 VAR ed : tv.EditorData; edp : tv.EditorDataPtr; ev : tv.ViewPtr;
-...
 edp := CAST(tv.EditorDataPtr, ADR(ed));
 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;
-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^.flags := ev^.flags + tv.vfExpand;
 tv.EditorAttach(ev, edp);
 tv.AddView(win, ev);
 tv.FocusView(ev);
 ...
 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
 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;
+quit := FALSE;
 WHILE NOT quit DO
     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;
     IF tvg.Closed() THEN quit := TRUE END
 END;
 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))`.
 - 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;
     mPascal  = 111;
     mOberon  = 112;
+    mSize    = 120;
+    mMax     = 121;
 
 VAR
     ed : tv.EditorData;
@@ -29,6 +31,21 @@ VAR
     cmd : INTEGER;
     sender : tv.ViewPtr;
     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;
 BEGIN
@@ -66,6 +83,9 @@ BEGIN
     tv.MenuItemAdd("Save", mSave);
     tv.MenuSep;
     tv.MenuItemAdd("Quit", mQuit);
+    tv.MenuAdd("Window");
+    tv.MenuItemAdd("Size...", mSize);
+    tv.MenuItemAdd("Maximize", mMax);
     tv.MenuAdd("Syntax");
     tv.MenuItemAdd("Modula-2", mModula2);
     tv.MenuItemAdd("Pascal", mPascal);
@@ -83,6 +103,7 @@ BEGIN
     tv.EditorSetIndent(edp, 2);
 
     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.AddView(w, edView);
     tv.FocusView(edView);
@@ -111,6 +132,24 @@ BEGIN
                         tv.Message("Save", "Could not save the file.")
                     END;
                     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
                     tv.EditorSetLanguage(edp, tv.langModula2); Refresh
                 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;
     vfSelected = 2;
     vfFramed   = 4;
+    vfExpand   = 8;   (* grows when its window is resized *)
 
     MAXLINE  = 128;
     MAXVIEWS = 128;
@@ -175,6 +176,8 @@ PROCEDURE BringToFront (v : ViewPtr);
 PROCEDURE FocusView (v : ViewPtr);
 PROCEDURE FocusNext (backwards : BOOLEAN);
 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:
      MenuBarInit;

+ 56 - 6
tvision-m2/tv.mod

@@ -25,7 +25,7 @@ CONST
     H_DOUBLE = 2550H; V_DOUBLE = 2551H;
     TL_DOUBLE = 2554H; TR_DOUBLE = 2557H; BL_DOUBLE = 255AH; BR_DOUBLE = 255DH;
 
-    vfPressed = 8;   (* internal, transient *)
+    vfPressed = 16;  (* internal, transient *)
 
     MAXMENUS = 8;
     MAXMITEMS = 16;
@@ -66,7 +66,9 @@ VAR
     desk   : ViewPtr;
     modal  : ViewPtr;
 
-    dragWin : ViewPtr;
+    dragWin  : ViewPtr;
+    resizeWin : ViewPtr;
+    resOX, resOY : INTEGER;
     lastSender : ViewPtr;
     dragOX, dragOY : INTEGER;
 
@@ -223,7 +225,7 @@ BEGIN
     inLen := 0; inPos := 0;
     cursorX := 0; cursorY := 0; cursorShown := FALSE;
     clipN := 0;
-    modal := NIL; dragWin := NIL; lastSender := NIL;
+    modal := NIL; dragWin := NIL; resizeWin := NIL; lastSender := NIL;
     menuCount := 0; menuOpen := -1; menuSel := 0; menuShown := FALSE;
     winch := FALSE;
     dlg := NIL; dlgCount := 0;
@@ -918,6 +920,37 @@ BEGIN
     FocusView(nxt)
 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);
     VAR c : ViewPtr;
 BEGIN
@@ -955,7 +988,10 @@ BEGIN
     DrawText(v^.rect.x + 2, v^.rect.y, v^.text, title);
     (* close box *)
     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;
 
 PROCEDURE DrawInput(v : ViewPtr);
@@ -2184,6 +2220,13 @@ PROCEDURE HandleWindow(v : ViewPtr; VAR e : Event) : INTEGER;
     VAR w : ViewPtr; nx, ny : INTEGER;
 BEGIN
     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? *)
         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
@@ -2197,10 +2240,15 @@ BEGIN
             dragOY := e.my - v^.rect.y
         END
     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
           (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;
             ny := e.my - dragOY;
             (* keep the window's title corner on screen *)
@@ -2270,6 +2318,8 @@ BEGIN
            sideways stops as soon as the pointer leaves the frame) *)
         IF dragWin # NIL THEN
             cmd := HandleWindow(dragWin, e)
+        ELSIF resizeWin # NIL THEN
+            cmd := HandleWindow(resizeWin, e)
         ELSE
             cmd := DispatchMouse(root, e)
         END

BIN
tvision-m2/tv.o