| 🛟 Updated. 28d5985 k33g 18h ago | 1 | package ui |
| 2 | |
| 3 | import ( |
| 4 | "github.com/gdamore/tcell/v2" |
| 5 | |
| 📦 Turbo Core f3ade8d k33g 11h ago | 6 | "rickub.com/turbo-editors/turbo-core/theme" |
| 🛟 Updated. 28d5985 k33g 18h ago | 7 | ) |
| 8 | |
| 9 | // desktopPattern is the character Turbo Vision tiled its backdrop with. |
| 10 | const desktopPattern = '░' |
| 11 | |
| 12 | // Desktop is the backdrop and the windows on it. |
| 13 | // |
| 14 | // Windows are kept in back-to-front order, so the last one is the active one: |
| 15 | // drawing walks the slice forwards and event routing walks it backwards, which |
| 16 | // is all the z-ordering a desktop needs. |
| 17 | type Desktop struct { |
| 18 | Box |
| 19 | windows []*Window |
| 20 | } |
| 21 | |
| 22 | // NewDesktop returns an empty desktop. |
| 23 | func NewDesktop() *Desktop { return &Desktop{} } |
| 24 | |
| 25 | // Windows returns the windows, back to front. The slice is the desktop's own; |
| 26 | // do not reorder it. |
| 27 | func (d *Desktop) Windows() []*Window { return d.windows } |
| 28 | |
| 29 | // Count returns how many windows are open. |
| 30 | func (d *Desktop) Count() int { return len(d.windows) } |
| 31 | |
| 32 | // Active returns the front window, or nil when the desktop is empty. |
| 33 | func (d *Desktop) Active() *Window { |
| 34 | if len(d.windows) == 0 { |
| 35 | return nil |
| 36 | } |
| 37 | return d.windows[len(d.windows)-1] |
| 38 | } |
| 39 | |
| 40 | // Add puts a window on top of the desktop and makes it active. |
| 41 | // |
| 42 | // desktop.Add(ui.NewWindow("main.go", view)) |
| 43 | func (d *Desktop) Add(w *Window) { |
| 44 | // The desktop is what a window is maximised into, so it is the desktop that |
| 45 | // knows how — and a window that has not been added to one draws no maximise |
| 46 | // box rather than one that would do nothing. |
| 47 | w.OnMaximize = func() { d.ToggleMaximize(w) } |
| 48 | |
| 49 | d.windows = append(d.windows, w) |
| 50 | d.refresh() |
| 51 | } |
| 52 | |
| 53 | // Remove takes a window off the desktop and reports whether it was there. |
| 54 | func (d *Desktop) Remove(w *Window) bool { |
| 55 | for i, candidate := range d.windows { |
| 56 | if candidate == w { |
| 57 | d.windows = append(d.windows[:i], d.windows[i+1:]...) |
| 58 | d.refresh() |
| 59 | return true |
| 60 | } |
| 61 | } |
| 62 | return false |
| 63 | } |
| 64 | |
| 65 | // Focus brings a window to the front and reports whether it was on the desktop |
| 66 | // to begin with. |
| 67 | func (d *Desktop) Focus(w *Window) bool { |
| 68 | if !d.Remove(w) { |
| 69 | return false |
| 70 | } |
| 71 | d.Add(w) |
| 72 | return true |
| 73 | } |
| 74 | |
| 75 | // FocusNumber brings window number n to the front, counting from one, and |
| 76 | // reports whether there is one. It is what Alt-1 … Alt-9 do. |
| 77 | func (d *Desktop) FocusNumber(n int) bool { |
| 78 | if n < 1 || n > len(d.windows) { |
| 79 | return false |
| 80 | } |
| 81 | return d.Focus(d.windows[n-1]) |
| 82 | } |
| 83 | |
| 84 | // Next cycles to the window behind the active one, as F6 does. |
| 85 | func (d *Desktop) Next() { |
| 86 | if len(d.windows) < 2 { |
| 87 | return |
| 88 | } |
| 89 | // The window at the back becomes the front one, and everything else slides |
| 90 | // back a place. |
| 91 | d.windows = append(d.windows[len(d.windows)-1:], d.windows[:len(d.windows)-1]...) |
| 92 | d.refresh() |
| 93 | } |
| 94 | |
| 95 | // SetBounds places the desktop and takes its windows with it. |
| 96 | // |
| 97 | // A window follows the desktop edges its grow mode names, so one that filled |
| 98 | // the terminal still fills it after the terminal is resized. None may end up |
| 99 | // larger than the desktop, whatever its grow mode. |
| 100 | // |
| 101 | // This is called on every turn of the event loop, and does nothing at all when |
| 102 | // the desktop has not actually changed size. |
| 103 | func (d *Desktop) SetBounds(r Rect) { |
| 104 | previous := d.Bounds() |
| 105 | d.Box.SetBounds(r) |
| 106 | |
| 107 | if previous == r { |
| 108 | return |
| 109 | } |
| 110 | for _, w := range d.windows { |
| 111 | w.followDesktop(previous, r) |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | // Tile lays every window out side by side in a grid, none overlapping. |
| 116 | func (d *Desktop) Tile() { |
| 117 | if len(d.windows) == 0 { |
| 118 | return |
| 119 | } |
| 120 | |
| 121 | columns := 1 |
| 122 | for columns*columns < len(d.windows) { |
| 123 | columns++ |
| 124 | } |
| 125 | rows := (len(d.windows) + columns - 1) / columns |
| 126 | |
| 127 | cellWidth := max(d.Bounds().W/columns, MinWindowWidth) |
| 128 | cellHeight := max(d.Bounds().H/rows, MinWindowHeight) |
| 129 | |
| 130 | for i, w := range d.windows { |
| 131 | w.place(Rect{ |
| 132 | X: d.Bounds().X + (i%columns)*cellWidth, |
| 133 | Y: d.Bounds().Y + (i/columns)*cellHeight, |
| 134 | W: cellWidth, |
| 135 | H: cellHeight, |
| 136 | }) |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | // cascadeStep is how far each cascaded window is offset from the one behind. |
| 141 | const cascadeStep = 2 |
| 142 | |
| 143 | // Cascade stacks the windows with their title bars all visible. |
| 144 | func (d *Desktop) Cascade() { |
| 145 | area := d.Bounds() |
| 146 | for i, w := range d.windows { |
| 147 | offset := i * cascadeStep |
| 148 | w.place(Rect{ |
| 149 | X: area.X + offset, |
| 150 | Y: area.Y + offset, |
| 151 | W: max(area.W-offset-cascadeStep, MinWindowWidth), |
| 152 | H: max(area.H-offset-cascadeStep, MinWindowHeight), |
| 153 | }) |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | // ToggleMaximize gives one window the whole desktop, or puts it back where it |
| 158 | // was if it already has it. |
| 159 | // |
| 160 | // desktop.ToggleMaximize(desktop.Active()) |
| 161 | func (d *Desktop) ToggleMaximize(w *Window) { |
| 162 | if w.Maximized() { |
| 163 | w.Restore() |
| 164 | return |
| 165 | } |
| 166 | w.Maximize(d.Bounds()) |
| 167 | } |
| 168 | |
| 169 | // refresh renumbers the windows and marks the front one active, which must |
| 170 | // happen after any change to the stack. |
| 171 | func (d *Desktop) refresh() { |
| 172 | for i, w := range d.windows { |
| 173 | w.SetNumber(i + 1) |
| 174 | w.SetActive(false) |
| 175 | } |
| 176 | if active := d.Active(); active != nil { |
| 177 | active.SetActive(true) |
| 178 | } |
| 179 | } |
| 180 | |
| 181 | // Draw paints the backdrop and then every window from back to front. |
| 182 | func (d *Desktop) Draw(p *Painter, th *theme.Theme) { |
| 183 | p.Fill(p.Size(), desktopPattern, th.Style(theme.KeyDesktop)) |
| 184 | for _, w := range d.windows { |
| 185 | w.Draw(p, th) |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | // HandleKey gives the key to the active window. |
| 190 | func (d *Desktop) HandleKey(ev *tcell.EventKey) bool { |
| 191 | active := d.Active() |
| 192 | return active != nil && active.HandleKey(ev) |
| 193 | } |
| 194 | |
| 195 | // HandleMouse routes a click to the front-most window it lands on, bringing |
| 196 | // that window forward first. |
| 197 | // |
| 198 | // A window in the middle of being dragged keeps receiving events wherever the |
| 199 | // pointer goes, or a fast drag would escape the window and strand it. |
| 200 | func (d *Desktop) HandleMouse(ev *tcell.EventMouse) bool { |
| 201 | if active := d.Active(); active != nil && active.Dragging() { |
| 202 | return active.HandleMouse(ev) |
| 203 | } |
| 204 | |
| 205 | for i := len(d.windows) - 1; i >= 0; i-- { |
| 206 | w := d.windows[i] |
| 207 | if !w.hit(ev) { |
| 208 | continue |
| 209 | } |
| 210 | if ev.Buttons() == tcell.Button1 { |
| 211 | d.Focus(w) |
| 212 | } |
| 213 | return w.HandleMouse(ev) |
| 214 | } |
| 215 | return false |
| 216 | } |