GoSetup-bubble/setup/modal.go

169 lines
7.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package setup
import (
"bubble/setup/ui"
"bubble/types"
"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 msg.String() {
case "left", "shift+tab":
//Обработчик нажатия клавиш left и shift+tab нужен для перевода фокуса на прошлую кнопку.
//Уменьшаем фокус, если фокус стал меньше 0, значит он вышел за пределы кнопок,
//переводим его на последнюю кнопку.
m.focus--
if m.focus < 0 {
m.focus = len(m.buttons) - 1
}
case "right", "tab":
//Обработчик нажатия клавиш right и tab нужен для перевода фокуса на следующую кнопку.
//Увеличиваем фокус, если фокус стал равен количество кнопок, значит он вышел за их пределы,
//переводим его на первую позицию, то есть на 0
m.focus++
if m.focus >= len(m.buttons) {
m.focus = 0
}
case "enter":
//Обработчик клавиши enter нужен для подтверждения кнопки.
//Если у модального окна есть кнопки.
//Получаем значение нажатой кнопки, а после если у модального окна прописана функция,
//то передаем значение кнопки в функцию модального окна.
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))
}