| 🛟 Updated. 28d5985 k33g 18h ago | 1 | // Package ui is the widget framework the editor is built from: a desktop |
| 2 | // holding movable windows, a menu bar with drop-down menus, modal dialogs, |
| 3 | // buttons, input lines and list boxes — the Turbo Vision furniture, drawn on |
| 4 | // tcell. |
| 5 | // |
| 6 | // Nothing here knows about Go source, buffers or language servers. Widgets |
| 7 | // draw through a clipping Painter and answer key and mouse events; what they |
| 8 | // mean is decided by the package above. |
| 9 | package ui |
| 10 | |
| 11 | // Rect is a rectangle of terminal cells: its top-left corner and its size. |
| 12 | // |
| 13 | // A rectangle with a zero or negative width or height is empty and covers no |
| 14 | // cell at all, which is what an off-screen or fully clipped widget reduces to. |
| 15 | type Rect struct { |
| 16 | X, Y, W, H int |
| 17 | } |
| 18 | |
| 19 | // Right returns the column just past the rectangle's right edge. |
| 20 | func (r Rect) Right() int { return r.X + r.W } |
| 21 | |
| 22 | // Bottom returns the row just past the rectangle's bottom edge. |
| 23 | func (r Rect) Bottom() int { return r.Y + r.H } |
| 24 | |
| 25 | // IsEmpty reports whether the rectangle covers no cell. |
| 26 | func (r Rect) IsEmpty() bool { return r.W <= 0 || r.H <= 0 } |
| 27 | |
| 28 | // Contains reports whether the cell at (x, y) is inside the rectangle. |
| 29 | func (r Rect) Contains(x, y int) bool { |
| 30 | return x >= r.X && x < r.Right() && y >= r.Y && y < r.Bottom() |
| 31 | } |
| 32 | |
| 33 | // Intersect returns the rectangle covered by both r and other, which is empty |
| 34 | // when they do not overlap. |
| 35 | func (r Rect) Intersect(other Rect) Rect { |
| 36 | x := max(r.X, other.X) |
| 37 | y := max(r.Y, other.Y) |
| 38 | right := min(r.Right(), other.Right()) |
| 39 | bottom := min(r.Bottom(), other.Bottom()) |
| 40 | |
| 41 | if right <= x || bottom <= y { |
| 42 | return Rect{X: x, Y: y} |
| 43 | } |
| 44 | return Rect{X: x, Y: y, W: right - x, H: bottom - y} |
| 45 | } |
| 46 | |
| 47 | // Inset returns the rectangle shrunk by dx on the left and right and by dy on |
| 48 | // the top and bottom. It is how a widget moves inside its own frame. |
| 49 | // |
| 50 | // body := window.Bounds().Inset(1, 1) |
| 51 | func (r Rect) Inset(dx, dy int) Rect { |
| 52 | return Rect{X: r.X + dx, Y: r.Y + dy, W: r.W - 2*dx, H: r.H - 2*dy} |
| 53 | } |
| 54 | |
| 55 | // Move returns the rectangle shifted by (dx, dy), keeping its size. |
| 56 | func (r Rect) Move(dx, dy int) Rect { |
| 57 | return Rect{X: r.X + dx, Y: r.Y + dy, W: r.W, H: r.H} |
| 58 | } |
| 59 | |
| 60 | // WithSize returns the rectangle with a different size and the same corner. |
| 61 | func (r Rect) WithSize(w, h int) Rect { |
| 62 | return Rect{X: r.X, Y: r.Y, W: w, H: h} |
| 63 | } |
| 64 | |
| 65 | // CenteredIn returns a rectangle of the same size placed in the middle of |
| 66 | // outer, which is where a dialog opens. |
| 67 | func (r Rect) CenteredIn(outer Rect) Rect { |
| 68 | return Rect{ |
| 69 | X: outer.X + (outer.W-r.W)/2, |
| 70 | Y: outer.Y + (outer.H-r.H)/2, |
| 71 | W: r.W, |
| 72 | H: r.H, |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | // ClampInto returns the rectangle moved — never resized — so that it lies |
| 77 | // inside outer, as far as its size allows. A window dragged past the edge of |
| 78 | // the screen comes back this way. |
| 79 | func (r Rect) ClampInto(outer Rect) Rect { |
| 80 | out := r |
| 81 | out.X = min(out.X, outer.Right()-out.W) |
| 82 | out.Y = min(out.Y, outer.Bottom()-out.H) |
| 83 | out.X = max(out.X, outer.X) |
| 84 | out.Y = max(out.Y, outer.Y) |
| 85 | return out |
| 86 | } |