turbo-editors/turbo-corepublic Fork 0
28d59854361aeda8541d853093e732126f3d7bff
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

🛟 Updated. 28d5985 · on 28d59854361aeda8541d853093e732126f3d7bff · k33g · 16h ago
desktop.go · 216 lines · 5.6 KBGo Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
package ui

import (
	"github.com/gdamore/tcell/v2"

	"codeberg.org/turbo-editors/turbo-core/theme"
)

// desktopPattern is the character Turbo Vision tiled its backdrop with.
const desktopPattern = '░'

// Desktop is the backdrop and the windows on it.
//
// Windows are kept in back-to-front order, so the last one is the active one:
// drawing walks the slice forwards and event routing walks it backwards, which
// is all the z-ordering a desktop needs.
type Desktop struct {
	Box
	windows []*Window
}

// NewDesktop returns an empty desktop.
func NewDesktop() *Desktop { return &Desktop{} }

// Windows returns the windows, back to front. The slice is the desktop's own;
// do not reorder it.
func (d *Desktop) Windows() []*Window { return d.windows }

// Count returns how many windows are open.
func (d *Desktop) Count() int { return len(d.windows) }

// Active returns the front window, or nil when the desktop is empty.
func (d *Desktop) Active() *Window {
	if len(d.windows) == 0 {
		return nil
	}
	return d.windows[len(d.windows)-1]
}

// Add puts a window on top of the desktop and makes it active.
//
//	desktop.Add(ui.NewWindow("main.go", view))
func (d *Desktop) Add(w *Window) {
	// The desktop is what a window is maximised into, so it is the desktop that
	// knows how — and a window that has not been added to one draws no maximise
	// box rather than one that would do nothing.
	w.OnMaximize = func() { d.ToggleMaximize(w) }

	d.windows = append(d.windows, w)
	d.refresh()
}

// Remove takes a window off the desktop and reports whether it was there.
func (d *Desktop) Remove(w *Window) bool {
	for i, candidate := range d.windows {
		if candidate == w {
			d.windows = append(d.windows[:i], d.windows[i+1:]...)
			d.refresh()
			return true
		}
	}
	return false
}

// Focus brings a window to the front and reports whether it was on the desktop
// to begin with.
func (d *Desktop) Focus(w *Window) bool {
	if !d.Remove(w) {
		return false
	}
	d.Add(w)
	return true
}

// FocusNumber brings window number n to the front, counting from one, and
// reports whether there is one. It is what Alt-1 … Alt-9 do.
func (d *Desktop) FocusNumber(n int) bool {
	if n < 1 || n > len(d.windows) {
		return false
	}
	return d.Focus(d.windows[n-1])
}

// Next cycles to the window behind the active one, as F6 does.
func (d *Desktop) Next() {
	if len(d.windows) < 2 {
		return
	}
	// The window at the back becomes the front one, and everything else slides
	// back a place.
	d.windows = append(d.windows[len(d.windows)-1:], d.windows[:len(d.windows)-1]...)
	d.refresh()
}

// SetBounds places the desktop and takes its windows with it.
//
// A window follows the desktop edges its grow mode names, so one that filled
// the terminal still fills it after the terminal is resized. None may end up
// larger than the desktop, whatever its grow mode.
//
// This is called on every turn of the event loop, and does nothing at all when
// the desktop has not actually changed size.
func (d *Desktop) SetBounds(r Rect) {
	previous := d.Bounds()
	d.Box.SetBounds(r)

	if previous == r {
		return
	}
	for _, w := range d.windows {
		w.followDesktop(previous, r)
	}
}

// Tile lays every window out side by side in a grid, none overlapping.
func (d *Desktop) Tile() {
	if len(d.windows) == 0 {
		return
	}

	columns := 1
	for columns*columns < len(d.windows) {
		columns++
	}
	rows := (len(d.windows) + columns - 1) / columns

	cellWidth := max(d.Bounds().W/columns, MinWindowWidth)
	cellHeight := max(d.Bounds().H/rows, MinWindowHeight)

	for i, w := range d.windows {
		w.place(Rect{
			X: d.Bounds().X + (i%columns)*cellWidth,
			Y: d.Bounds().Y + (i/columns)*cellHeight,
			W: cellWidth,
			H: cellHeight,
		})
	}
}

// cascadeStep is how far each cascaded window is offset from the one behind.
const cascadeStep = 2

// Cascade stacks the windows with their title bars all visible.
func (d *Desktop) Cascade() {
	area := d.Bounds()
	for i, w := range d.windows {
		offset := i * cascadeStep
		w.place(Rect{
			X: area.X + offset,
			Y: area.Y + offset,
			W: max(area.W-offset-cascadeStep, MinWindowWidth),
			H: max(area.H-offset-cascadeStep, MinWindowHeight),
		})
	}
}

// ToggleMaximize gives one window the whole desktop, or puts it back where it
// was if it already has it.
//
//	desktop.ToggleMaximize(desktop.Active())
func (d *Desktop) ToggleMaximize(w *Window) {
	if w.Maximized() {
		w.Restore()
		return
	}
	w.Maximize(d.Bounds())
}

// refresh renumbers the windows and marks the front one active, which must
// happen after any change to the stack.
func (d *Desktop) refresh() {
	for i, w := range d.windows {
		w.SetNumber(i + 1)
		w.SetActive(false)
	}
	if active := d.Active(); active != nil {
		active.SetActive(true)
	}
}

// Draw paints the backdrop and then every window from back to front.
func (d *Desktop) Draw(p *Painter, th *theme.Theme) {
	p.Fill(p.Size(), desktopPattern, th.Style(theme.KeyDesktop))
	for _, w := range d.windows {
		w.Draw(p, th)
	}
}

// HandleKey gives the key to the active window.
func (d *Desktop) HandleKey(ev *tcell.EventKey) bool {
	active := d.Active()
	return active != nil && active.HandleKey(ev)
}

// HandleMouse routes a click to the front-most window it lands on, bringing
// that window forward first.
//
// A window in the middle of being dragged keeps receiving events wherever the
// pointer goes, or a fast drag would escape the window and strand it.
func (d *Desktop) HandleMouse(ev *tcell.EventMouse) bool {
	if active := d.Active(); active != nil && active.Dragging() {
		return active.HandleMouse(ev)
	}

	for i := len(d.windows) - 1; i >= 0; i-- {
		w := d.windows[i]
		if !w.hit(ev) {
			continue
		}
		if ev.Buttons() == tcell.Button1 {
			d.Focus(w)
		}
		return w.HandleMouse(ev)
	}
	return false
}