//go:build cgo package desktop import ( "strings" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" "github.com/kamelion/ratatoskr-go/internal/ui" ) // maxLogLen — обрезка буфера панели «Логи», чтобы не расти бесконечно. const maxLogLen = 200_000 // LogPanel — Fyne-реализация ui.LogPanel (спец 12.8, 12.9): поток системных // логов с фильтром по уровням (error/warning/info/debug). // // Записи хранятся в ui.LogBuffer (ограниченный буфер); лента показывает // только записи уровней, отмеченных в фильтре (по умолчанию отмечены все). // Фильтр — выпадающий список с чекбоксами: кнопка над лентой открывает // widget.PopUp с widget.Check по каждому уровню; изменение набора сразу // перерисовывает ленту — и по уже загруженным записям, и для новых. // // Состояние (buf/selected) доступается только с потока Fyne: Append приходит // из горутин Hub и заворачивается в fyne.Do, колбэки чекбоксов Fyne вызывает // сам на своём потоке. type LogPanel struct { label *widget.Label btn *widget.Button // кнопка-дропдаун фильтра уровней buf *ui.LogBuffer // буфер записей, старые вытесняются selected map[string]bool // уровень → показывать (по умолчанию все) } // NewLogPanel создаёт LogPanel со всеми включёнными уровнями. func NewLogPanel() *LogPanel { p := &LogPanel{ buf: ui.NewLogBuffer(maxLogLen), selected: make(map[string]bool, len(ui.LogLevels)), } for _, lvl := range ui.LogLevels { p.selected[lvl] = true } p.label = widget.NewLabel("") p.label.Wrapping = fyne.TextWrapWord p.btn = widget.NewButton(p.filterCaption(), p.showFilterPopUp) return p } // Widget возвращает контейнер панели для встраивания в окно (таб «Логи»): // строка фильтра сверху, лента лога в скролле снизу. func (p *LogPanel) Widget() fyne.CanvasObject { return container.NewBorder( container.NewHBox(p.btn), nil, nil, nil, container.NewScroll(p.label), ) } // Append добавляет запись лога (спец 12.9), с ограничением буфера. // Вызывается из горутин Hub → вся работа на потоке Fyne через fyne.Do. func (p *LogPanel) Append(level, text string) { fyne.Do(func() { lvl := ui.NormalizeLogLevel(level) dropped := p.buf.Append(lvl, text) // Перерисовка нужна, если запись видима или вытеснение задело ленту. if p.selected[lvl] || dropped { p.render() } }) } // render перестраивает текст ленты из буфера с учётом выбранного набора // уровней. Пустой набор → пустая лента (без ошибок). func (p *LogPanel) render() { p.label.SetText(p.buf.Render(p.selected)) } // setLevel включает/выключает показ уровня и сразу обновляет ленту. // Вызывается только с потока Fyne (колбэки чекбоксов). func (p *LogPanel) setLevel(level string, on bool) { if p.selected[level] == on { return } p.selected[level] = on p.btn.SetText(p.filterCaption()) p.render() } // filterCaption — подпись кнопки-дропдауна: текущий выбранный набор уровней. func (p *LogPanel) filterCaption() string { var chosen []string for _, lvl := range ui.LogLevels { if p.selected[lvl] { chosen = append(chosen, lvl) } } switch { case len(chosen) == len(ui.LogLevels): return "Уровни: все" case len(chosen) == 0: return "Уровни: нет" default: return "Уровни: " + strings.Join(chosen, "+") } } // showFilterPopUp открывает выпадающий список с чекбоксами уровней под // кнопкой фильтра. Список пересобирается при каждом открытии — чекбоксы // всегда отражают актуальное состояние фильтра. func (p *LogPanel) showFilterPopUp() { c := fyne.CurrentApp().Driver().CanvasForObject(p.btn) if c == nil { return // виджет ещё не встроен в окно } rows := make([]fyne.CanvasObject, 0, len(ui.LogLevels)) for _, lvl := range ui.LogLevels { lvl := lvl cb := widget.NewCheck(lvl, func(on bool) { p.setLevel(lvl, on) }) cb.SetChecked(p.selected[lvl]) rows = append(rows, cb) } pop := widget.NewPopUp(container.NewPadded(container.NewVBox(rows...)), c) pos := fyne.CurrentApp().Driver().AbsolutePositionForObject(p.btn) pop.ShowAtPosition(fyne.NewPos(pos.X, pos.Y+p.btn.Size().Height)) } // compile-time проверка реализации контракта. var _ ui.LogPanel = (*LogPanel)(nil)