package ui import ( "github.com/gdamore/tcell/v2" "rickub.com/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 }