GoSetup-bubble/setup/modal.go

171 lines
7.4 KiB
Go
Raw Normal View History

package setup
import (
"bubble/setup/ui"
"bubble/types"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
// ModalModel структура описывающая модель отрисовки модального окна
type ModalModel struct {
modalType types.ModalType //Тип модального окна
title string //Заголовок модального окна
message string //Сообщение модального окна
buttons []types.ModalButton //Кнопки модального окна
focus int //Фокус для отслеживания с каким объектом надо работать
width int //Ширина окна
height int //Высота окна
onResult func(result any) tea.Cmd //Какое действие надо выполнить при нажатии
spinner spinner.Model //Для демонстрации прогресса выполнения действий
}
// NewModalModel создает новую модель модального окна на основе переданного сообщения
func NewModalModel(msg types.ShowModalMsg, width, height int) *ModalModel {
m := &ModalModel{
modalType: msg.Type,
title: msg.Title,
message: msg.Message,
buttons: msg.ModalButtons,
focus: 0,
width: width,
height: height,
onResult: msg.OnResult,
}
if msg.Type == types.ModalLoading {
s := spinner.New()
s.Spinner = spinner.Line
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
m.spinner = s
}
return m
}
func (m *ModalModel) Init() tea.Cmd {
if m.modalType == types.ModalLoading {
return m.spinner.Tick
}
return nil
}
// Update отслеживает действия с модальным окном
func (m *ModalModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
//Отслеживаем и записываем новые размеры окна
m.width = msg.Width
m.height = msg.Height
return m, nil
case tea.KeyMsg:
//Отслеживаем нажатие клавиш модального окна
switch {
case key.Matches(msg, types.MainKeys.Up) || key.Matches(msg, types.MainKeys.Left):
//Обработчик нажатия клавиш перехода на прошлую кнопку.
//Уменьшаем фокус, если фокус стал меньше 0, значит он вышел за пределы кнопок,
//переводим его на последнюю кнопку.
m.focus--
if m.focus < 0 {
m.focus = len(m.buttons) - 1
}
case key.Matches(msg, types.MainKeys.Down) || key.Matches(msg, types.MainKeys.Right):
//Обработчик нажатия клавиш перехода на следующую кнопку.
//Увеличиваем фокус, если фокус стал равен количество кнопок, значит он вышел за их пределы,
//переводим его на первую позицию, то есть на 0
m.focus++
if m.focus >= len(m.buttons) {
m.focus = 0
}
case key.Matches(msg, types.MainKeys.Confirm):
//Обработчик клавиши подтверждения выбранной кнопки.
//Если у модального окна есть кнопки.
//Получаем значение нажатой кнопки, а после если у модального окна прописана функция,
//то передаем значение кнопки в функцию модального окна.
if len(m.buttons) > 0 {
result := m.buttons[m.focus].Value
if m.onResult != nil {
cmd = m.onResult(result)
}
//Возвращаем единую команду составленную из двух,
//результата выполнения функции модального окна,
//функцию возвращения сообщения о закрытии модального окна.
return m, tea.Sequence(cmd, func() tea.Msg { return types.CloseModalMsg{} })
}
}
}
//Если это модальное окно загрузки, то обновляем счетчик
if m.modalType == types.ModalLoading {
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
}
return m, nil
}
// View формирует строку для вывода модального окна и её контента
func (m *ModalModel) View() string {
//Данные о размерах контейнера
containerWidth, containerHeight, _, _ := ui.ContentSizes(m.width, m.height)
//Модальное окно не должно занимать все пространство, поэтому получаем минимально допустимые размеры
modalWidth := min(containerWidth, ui.ModalMaxWidth)
modalHeight := min(containerHeight, ui.ModalMaxHeight)
//Формируем просто сообщение и кнопки с учетом фокуса
message := lipgloss.NewStyle().Render(m.message)
var buttons []string
for i, button := range m.buttons {
style := ui.ModalButtonNormal
if i == m.focus {
style = ui.ModalButtonFocused
}
buttons = append(buttons, style.Render(button.Text))
}
//Строка кнопок
buttonRow := lipgloss.JoinHorizontal(lipgloss.Center, buttons...)
//Тело контента модального окна
modalBody := lipgloss.JoinVertical(lipgloss.Center, message, "", buttonRow)
if m.modalType == types.ModalLoading {
modalBody = lipgloss.JoinVertical(lipgloss.Center, m.spinner.View(), message)
}
//В зависимости от типа модального окна разные цвета
var borderColor lipgloss.Color
var modalTitleStyle lipgloss.Style
switch m.modalType {
case types.ModalError:
borderColor = ui.ColorError
modalTitleStyle = ui.ModalTitleError
case types.ModalWarning, types.ModalConfirm:
borderColor = ui.ColorWarning
modalTitleStyle = ui.ModalTitleWarning
default:
borderColor = ui.ColorAccent
modalTitleStyle = ui.ModalTitleDefault
}
//Формируем модальный контейнер с контентом внутри и указанным цветом границ.
modalView := ui.ModalContainer.
Width(modalWidth).
Height(modalHeight).
Align(lipgloss.Center).
Border(lipgloss.DoubleBorder()).
BorderForeground(borderColor).
Render(modalBody)
symbolToRepeat := "*"
//Если у модального окна есть заголовок, то в modalView записываем новое значение,
//ставя заголовок сверху модального контейнера
if m.title != "" {
//repeatCount := modalWidth - utf8.RuneCountInString(m.title)
//if repeatCount < 0 {
// repeatCount = 0
//}
//symbolFill := strings.Repeat(symbolToRepeat, repeatCount/2)
//modalView = lipgloss.JoinVertical(lipgloss.Center, modalTitleStyle.Render(symbolFill+m.title+symbolFill), modalView)
modalView = lipgloss.JoinVertical(lipgloss.Center, modalTitleStyle.Render(m.title), modalView)
}
return lipgloss.Place(m.width, m.height,
lipgloss.Center, lipgloss.Center,
modalView,
lipgloss.WithWhitespaceChars(symbolToRepeat),
lipgloss.WithWhitespaceForeground(ui.ColorGrey))
}