feat/ui-refactor #9
@@ -157,6 +157,27 @@
|
|||||||
приложение работает headless (UI=nil). Реальное окно полноценно проверяется
|
приложение работает headless (UI=nil). Реальное окно полноценно проверяется
|
||||||
на машине с MinGW-w64/gcc (`go build -ldflags ...`), здесь — только typecheck.
|
на машине с MinGW-w64/gcc (`go build -ldflags ...`), здесь — только typecheck.
|
||||||
|
|
||||||
|
### 12.8. Структура UI-кода: компоненты-виджеты (рефакторинг)
|
||||||
|
|
||||||
|
`internal/ui` (без Fyne) — контракты + Nil-реализации + юнит-тесты. Fyne-код —
|
||||||
|
только в `internal/ui/desktop/*`, один файл — одна панель. `desktop/window.go`
|
||||||
|
становится чистой компоновкой.
|
||||||
|
|
||||||
|
Каждый компонент:
|
||||||
|
- **интерфейс** в `internal/ui/<panel>.go` (контракт для тестов/headless);
|
||||||
|
- **Nil-реализация** в том же файле (headless/тесты);
|
||||||
|
- **Fyne-реализация** в `internal/ui/desktop/<panel>.go`.
|
||||||
|
|
||||||
|
### 12.9. Контракт виджета «Логи» (LogPanel)
|
||||||
|
|
||||||
|
Панель «Логи» — поток системных логов (сырые строки из `events.LogLine`).
|
||||||
|
|
||||||
|
- `Append(text)` — добавить строку лога; при превышении лимита старые строки
|
||||||
|
отбрасываются (буфер ограничен, не расти бесконечно).
|
||||||
|
- Контейнер виджета — отдаётся окну для встраивания в таб «Логи» (Scroll).
|
||||||
|
- Обновление содержимого — на потоке Fyne (внутри панели `fyne.Do`), не из
|
||||||
|
горутины Hub.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Открытые пункты (TODO)
|
## Открытые пункты (TODO)
|
||||||
|
|||||||
49
internal/ui/desktop/log_panel.go
Normal file
49
internal/ui/desktop/log_panel.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
//go:build cgo
|
||||||
|
|
||||||
|
package desktop
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
|
||||||
|
"github.com/kamelion/ratatoskr-go/internal/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogPanel — Fyne-реализация ui.LogPanel (спец 12.8, 12.9): поток системных логов.
|
||||||
|
//
|
||||||
|
// Внутри — widget.Label с ограниченным буфером (maxLogLen); Append обрезает
|
||||||
|
// строку до лимита. Вызовы Append приходят из горутин Hub, поэтому обновление
|
||||||
|
// виджета выполняется через fyne.Do (на потоке Fyne).
|
||||||
|
type LogPanel struct {
|
||||||
|
label *widget.Label
|
||||||
|
}
|
||||||
|
|
||||||
|
// maxLogLen — обрезка буфера панели «Логи», чтобы не расти бесконечно.
|
||||||
|
const maxLogLen = 200_000
|
||||||
|
|
||||||
|
// NewLogPanel создаёт LogPanel.
|
||||||
|
func NewLogPanel() *LogPanel {
|
||||||
|
l := widget.NewLabel("")
|
||||||
|
l.Wrapping = fyne.TextWrapWord
|
||||||
|
return &LogPanel{label: l}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Widget возвращает контейнер панели для встраивания в окно (таб «Логи»).
|
||||||
|
func (p *LogPanel) Widget() fyne.CanvasObject {
|
||||||
|
return container.NewScroll(p.label)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append добавляет строку лога (спец 12.9), с ограничением буфера.
|
||||||
|
func (p *LogPanel) Append(text string) {
|
||||||
|
fyne.Do(func() {
|
||||||
|
s := p.label.Text + text + "\n"
|
||||||
|
if len(s) > maxLogLen {
|
||||||
|
s = s[len(s)-maxLogLen:]
|
||||||
|
}
|
||||||
|
p.label.SetText(s)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// compile-time проверка реализации контракта.
|
||||||
|
var _ ui.LogPanel = (*LogPanel)(nil)
|
||||||
@@ -26,7 +26,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
maxLogLen = 200_000 // обрезка буферов панелей, чтобы не расти бесконечно
|
bufLimit = 200_000 // обрезка буферов панелей окна (conv/state), чтобы не расти бесконечно
|
||||||
selectedPref = "task.selected"
|
selectedPref = "task.selected"
|
||||||
splitHPref = "split.h"
|
splitHPref = "split.h"
|
||||||
splitVPref = "split.v"
|
splitVPref = "split.v"
|
||||||
@@ -52,8 +52,8 @@ type Window struct {
|
|||||||
statusLbl *widget.Label
|
statusLbl *widget.Label
|
||||||
detailLbl *widget.Label
|
detailLbl *widget.Label
|
||||||
convLbl *widget.Label
|
convLbl *widget.Label
|
||||||
logsLbl *widget.Label
|
|
||||||
stateLbl *widget.Label
|
stateLbl *widget.Label
|
||||||
|
logs *LogPanel
|
||||||
input *widget.Entry
|
input *widget.Entry
|
||||||
|
|
||||||
hsplit *container.Split
|
hsplit *container.Split
|
||||||
@@ -97,8 +97,7 @@ func (w *Window) build() {
|
|||||||
w.convLbl = widget.NewLabel("Выберите задачу.")
|
w.convLbl = widget.NewLabel("Выберите задачу.")
|
||||||
w.convLbl.Wrapping = fyne.TextWrapWord
|
w.convLbl.Wrapping = fyne.TextWrapWord
|
||||||
|
|
||||||
w.logsLbl = widget.NewLabel("")
|
w.logs = NewLogPanel()
|
||||||
w.logsLbl.Wrapping = fyne.TextWrapWord
|
|
||||||
|
|
||||||
w.stateLbl = widget.NewLabel("")
|
w.stateLbl = widget.NewLabel("")
|
||||||
w.stateLbl.Wrapping = fyne.TextWrapWord
|
w.stateLbl.Wrapping = fyne.TextWrapWord
|
||||||
@@ -149,7 +148,7 @@ func (w *Window) build() {
|
|||||||
|
|
||||||
// Нижняя панель: вкладки «Логи» + «Состояние».
|
// Нижняя панель: вкладки «Логи» + «Состояние».
|
||||||
bottomTabs := container.NewAppTabs(
|
bottomTabs := container.NewAppTabs(
|
||||||
container.NewTabItem("Логи", container.NewScroll(w.logsLbl)),
|
container.NewTabItem("Логи", w.logs.Widget()),
|
||||||
container.NewTabItem("Состояние", container.NewScroll(w.stateLbl)),
|
container.NewTabItem("Состояние", container.NewScroll(w.stateLbl)),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -303,26 +302,22 @@ func (w *Window) refreshList() {
|
|||||||
// appendConv добавляет строку в диалог (буфер ограничен).
|
// appendConv добавляет строку в диалог (буфер ограничен).
|
||||||
func (w *Window) appendConv(text string) {
|
func (w *Window) appendConv(text string) {
|
||||||
s := w.convLbl.Text + text + "\n\n"
|
s := w.convLbl.Text + text + "\n\n"
|
||||||
if len(s) > maxLogLen {
|
if len(s) > bufLimit {
|
||||||
s = s[len(s)-maxLogLen:]
|
s = s[len(s)-bufLimit:]
|
||||||
}
|
}
|
||||||
w.convLbl.SetText(s)
|
w.convLbl.SetText(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// appendLog добавляет строку в панель «Логи».
|
// appendLog добавляет строку в панель «Логи» (делегирует в LogPanel).
|
||||||
func (w *Window) appendLog(text string) {
|
func (w *Window) appendLog(text string) {
|
||||||
s := w.logsLbl.Text + text + "\n"
|
w.logs.Append(text)
|
||||||
if len(s) > maxLogLen {
|
|
||||||
s = s[len(s)-maxLogLen:]
|
|
||||||
}
|
|
||||||
w.logsLbl.SetText(s)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// appendState добавляет строку в панель «Состояние».
|
// appendState добавляет строку в панель «Состояние».
|
||||||
func (w *Window) appendState(text string) {
|
func (w *Window) appendState(text string) {
|
||||||
s := w.stateLbl.Text + text + "\n"
|
s := w.stateLbl.Text + text + "\n"
|
||||||
if len(s) > maxLogLen {
|
if len(s) > bufLimit {
|
||||||
s = s[len(s)-maxLogLen:]
|
s = s[len(s)-bufLimit:]
|
||||||
}
|
}
|
||||||
w.stateLbl.SetText(s)
|
w.stateLbl.SetText(s)
|
||||||
}
|
}
|
||||||
@@ -450,7 +445,7 @@ func (w *Window) OnAgentActivity(e events.AgentActivity) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *Window) OnLog(e events.LogLine) {
|
func (w *Window) OnLog(e events.LogLine) {
|
||||||
fyne.Do(func() { w.appendLog(e.Text) })
|
w.appendLog(e.Text)
|
||||||
}
|
}
|
||||||
|
|
||||||
func taskStatusLine(e events.TaskStatusChanged) string {
|
func taskStatusLine(e events.TaskStatusChanged) string {
|
||||||
|
|||||||
29
internal/ui/log_panel.go
Normal file
29
internal/ui/log_panel.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Package ui — контроллер и представления десктопного интерфейса (Fyne).
|
||||||
|
//
|
||||||
|
// Слой обмена с ядром: однонаправленный поток (спец 12.2–12.5).
|
||||||
|
// - Core мутирует состояние; UI только читает снимки и реагирует на события.
|
||||||
|
// - Действия UI = команды (CreateTask/ApproveTask/...), которые зовут Core.
|
||||||
|
// - Подписчик шины (Controller) получает события и перекладывает их в View
|
||||||
|
// (реализация Fyne) через fyne.Do — никаких прямых вызовов Fyne из core.
|
||||||
|
//
|
||||||
|
// Контракты компонентов-виджетов (спец 12.8): интерфейс + Nil-реализация.
|
||||||
|
package ui
|
||||||
|
|
||||||
|
// LogPanel — панель «Логи» (спец 12.9): поток системных логов (сырые строки).
|
||||||
|
//
|
||||||
|
// Содержит строки лога в буфере ограниченного размера; при превышении лимита
|
||||||
|
// старые строки отбрасываются. Контейнер виджета отдаётся окну для встраивания
|
||||||
|
// в таб «Логи» — реализация (Fyne) сама владеет своим виджетом.
|
||||||
|
type LogPanel interface {
|
||||||
|
// Append добавляет строку лога. Реализация обязана перекладывать работу
|
||||||
|
// на поток Fyne (fyne.Do / fyne.DoAndWait), т.к. вызывается из горутин Hub.
|
||||||
|
Append(text string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NilLogPanel — no-op реализация LogPanel для headless-режима и тестов.
|
||||||
|
type NilLogPanel struct{}
|
||||||
|
|
||||||
|
// NewNilLogPanel создаёт NilLogPanel.
|
||||||
|
func NewNilLogPanel() *NilLogPanel { return &NilLogPanel{} }
|
||||||
|
|
||||||
|
func (NilLogPanel) Append(string) {}
|
||||||
12
internal/ui/log_panel_test.go
Normal file
12
internal/ui/log_panel_test.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// NilLogPanel — no-op контракт (спец 12.8): Append ничего не делает, безопасно
|
||||||
|
// вызывается из горутин Hub и из тестов без Fyne.
|
||||||
|
func TestNilLogPanelAppendNoop(t *testing.T) {
|
||||||
|
p := NewNilLogPanel()
|
||||||
|
p.Append("line 1")
|
||||||
|
p.Append("")
|
||||||
|
p.Append("line 3")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user