ui.go

  1package model
  2
  3import (
  4	"image"
  5
  6	"github.com/charmbracelet/bubbles/v2/key"
  7	tea "github.com/charmbracelet/bubbletea/v2"
  8	"github.com/charmbracelet/crush/internal/app"
  9	"github.com/charmbracelet/crush/internal/ui/common"
 10	"github.com/charmbracelet/crush/internal/ui/dialog"
 11	"github.com/charmbracelet/lipgloss/v2"
 12	uv "github.com/charmbracelet/ultraviolet"
 13)
 14
 15type uiState uint8
 16
 17const (
 18	uiStateMain uiState = iota
 19)
 20
 21type UI struct {
 22	app *app.App
 23	com *common.Common
 24
 25	width, height int
 26	state         uiState
 27
 28	keyMap KeyMap
 29
 30	dialog *dialog.Overlay
 31}
 32
 33func New(com *common.Common, app *app.App) *UI {
 34	return &UI{
 35		app:    app,
 36		com:    com,
 37		dialog: dialog.NewOverlay(),
 38		keyMap: DefaultKeyMap(),
 39	}
 40}
 41
 42func (m *UI) Init() tea.Cmd {
 43	return nil
 44}
 45
 46func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 47	var cmds []tea.Cmd
 48	switch msg := msg.(type) {
 49	case tea.WindowSizeMsg:
 50		m.width = msg.Width
 51		m.height = msg.Height
 52	case tea.KeyPressMsg:
 53		switch m.state {
 54		case uiStateMain:
 55			switch {
 56			case key.Matches(msg, m.keyMap.Quit):
 57				quitDialog := dialog.NewQuit(m.com)
 58				if !m.dialog.ContainsDialog(quitDialog.ID()) {
 59					m.dialog.AddDialog(quitDialog)
 60					return m, nil
 61				}
 62			}
 63		}
 64	}
 65
 66	updatedDialog, cmd := m.dialog.Update(msg)
 67	m.dialog = updatedDialog
 68	if cmd != nil {
 69		cmds = append(cmds, cmd)
 70	}
 71
 72	return m, tea.Batch(cmds...)
 73}
 74
 75func (m *UI) View() tea.View {
 76	var v tea.View
 77	v.AltScreen = true
 78
 79	// The screen area we're working with
 80	area := image.Rect(0, 0, m.width, m.height)
 81	layers := []*lipgloss.Layer{}
 82
 83	if dialogView := m.dialog.View(); dialogView != "" {
 84		dialogWidth, dialogHeight := lipgloss.Width(dialogView), lipgloss.Height(dialogView)
 85		dialogArea := centerRect(area, dialogWidth, dialogHeight)
 86		layers = append(layers,
 87			lipgloss.NewLayer(dialogView).
 88				X(dialogArea.Min.X).
 89				Y(dialogArea.Min.Y),
 90		)
 91	}
 92
 93	mainRect, sideRect := uv.SplitHorizontal(area, uv.Fixed(area.Dx()-40))
 94	mainRect, footRect := uv.SplitVertical(mainRect, uv.Fixed(area.Dy()-7))
 95
 96	layers = append(layers, lipgloss.NewLayer(
 97		lipgloss.NewStyle().Width(mainRect.Dx()).
 98			Height(mainRect.Dy()).
 99			Border(lipgloss.NormalBorder()).
100			Render(" Main View "),
101	).X(mainRect.Min.X).Y(mainRect.Min.Y),
102		lipgloss.NewLayer(
103			lipgloss.NewStyle().Width(sideRect.Dx()).
104				Height(sideRect.Dy()).
105				Border(lipgloss.NormalBorder()).
106				Render(" Side View "),
107		).X(sideRect.Min.X).Y(sideRect.Min.Y),
108		lipgloss.NewLayer(
109			lipgloss.NewStyle().Width(footRect.Dx()).
110				Height(footRect.Dy()).
111				Border(lipgloss.NormalBorder()).
112				Render(" Footer View "),
113		).X(footRect.Min.X).Y(footRect.Min.Y),
114	)
115
116	v.Layer = lipgloss.NewCanvas(layers...)
117
118	return v
119}
120
121// centerRect returns a new [Rectangle] centered within the given area with the
122// specified width and height.
123func centerRect(area uv.Rectangle, width, height int) uv.Rectangle {
124	centerX := area.Min.X + area.Dx()/2
125	centerY := area.Min.Y + area.Dy()/2
126	minX := centerX - width/2
127	minY := centerY - height/2
128	maxX := minX + width
129	maxY := minY + height
130	return image.Rect(minX, minY, maxX, maxY)
131}