package setup import ( "bubble/setup/ui" "bubble/types" 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 //Какое действие надо выполнить при нажатии } // NewModalModel создает новую модель модального окна на основе переданного сообщения func NewModalModel(msg types.ShowModalMsg, width, height int) *ModalModel { return &ModalModel{ modalType: msg.Type, title: msg.Title, message: msg.Message, buttons: msg.ModalButtons, focus: 0, width: width, height: height, onResult: msg.OnResult, } } func (m *ModalModel) Init() tea.Cmd { return nil } // Update отслеживает действия с модальным окном func (m *ModalModel) Update(msg tea.Msg) (tea.Model, 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 нужен для подтверждения кнопки. //Получаем значение кнопки, а после если у модального окна прописана функция, //то передаем значение кнопки в функцию модального окна. result := m.buttons[m.focus].Value var cmd tea.Cmd if m.onResult != nil { cmd = m.onResult(result) } //Возвращаем единую команду составленную из двух, //результата выполнения функции модального окна, //функцию возвращения сообщения о закрытии модального окна. return m, tea.Batch( cmd, func() tea.Msg { return types.CloseModalMsg{} }, ) } } 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) //В зависимости от типа модального окна разные цвета 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)) }