230 lines
7.0 KiB
Go
230 lines
7.0 KiB
Go
package main
|
|
|
|
import (
|
|
"image"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gioui.org/app"
|
|
"gioui.org/layout"
|
|
"gioui.org/op"
|
|
"gioui.org/op/clip"
|
|
"gioui.org/op/paint"
|
|
"gioui.org/unit"
|
|
"gioui.org/widget"
|
|
"gioui.org/widget/material"
|
|
|
|
"loov.dev/lensm/internal/gui"
|
|
"loov.dev/lensm/internal/syntax"
|
|
)
|
|
|
|
func (ui *FileUI) openSettingsWindow() {
|
|
if ui.settingsWindowOpen || ui.settingsEvents == nil {
|
|
return
|
|
}
|
|
ui.settingsWindowOpen = true
|
|
events, acks, exited := ui.settingsEvents, ui.settingsAcks, ui.exited
|
|
ui.Windows.Open("lensm settings", image.Pt(520, 360), func(w *app.Window) error {
|
|
// Only pump events here: the settings window is laid out on the
|
|
// main event loop, because layout reads and mutates state shared
|
|
// with the main window (Settings, MCP, Config, widget state) and
|
|
// shapes text through the Theme's Shaper, which is not safe for
|
|
// concurrent use.
|
|
for {
|
|
ev := w.Event()
|
|
select {
|
|
case events <- ev:
|
|
case <-exited:
|
|
return nil
|
|
}
|
|
select {
|
|
case <-acks:
|
|
case <-exited:
|
|
return nil
|
|
}
|
|
if e, ok := ev.(app.DestroyEvent); ok {
|
|
return e.Err
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func (ui *FileUI) layoutSettingsWindow(gtx layout.Context) layout.Dimensions {
|
|
colors := ui.Theme.Colors
|
|
paint.FillShape(gtx.Ops, colors.Background, clip.Rect{Max: gtx.Constraints.Max}.Op())
|
|
ui.handleSettingsActions(gtx)
|
|
|
|
return layout.UniformInset(14).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
title := material.H6(ui.Theme.Theme, "Settings")
|
|
return layout.Inset{Bottom: 12}.Layout(gtx, title.Layout)
|
|
}),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
return ui.layoutSettingsSection(gtx, "Visual", []layout.FlexChild{
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
|
layout.Rigid(material.Switch(ui.Theme.Theme, &ui.Dark, "Dark theme").Layout),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
label := ui.Theme.Label("Dark theme", 1)
|
|
return layout.Inset{Left: 6}.Layout(gtx, label.Layout)
|
|
}),
|
|
)
|
|
}),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
check := material.CheckBox(ui.Theme.Theme, &ui.ShowNativeAsm, "Native asm")
|
|
return check.Layout(gtx)
|
|
}),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
check := material.CheckBox(ui.Theme.Theme, &ui.ShowAsmHelp, "Show instruction help")
|
|
return check.Layout(gtx)
|
|
}),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
return ui.layoutSyntaxSelector(gtx, colors)
|
|
}),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
return ui.layoutLabeledEditor(gtx, "Text size", &ui.TextSizeEditor)
|
|
}),
|
|
})
|
|
}),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
return layout.Inset{Top: 14}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
return ui.layoutSettingsSection(gtx, "MCP", []layout.FlexChild{
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
status := "Stopped"
|
|
if ui.MCP != nil {
|
|
status = "Running: " + ui.MCP.URL()
|
|
}
|
|
label := ui.Theme.Muted(status, 1)
|
|
return layout.Inset{Top: 6, Bottom: 6}.Layout(gtx, label.Layout)
|
|
}),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
if ui.MCP != nil {
|
|
gtx = gtx.Disabled()
|
|
}
|
|
button := material.Button(ui.Theme.Theme, &ui.StartMCP, "Start MCP")
|
|
return button.Layout(gtx)
|
|
}),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
if ui.MCP == nil {
|
|
gtx = gtx.Disabled()
|
|
}
|
|
button := material.Button(ui.Theme.Theme, &ui.StopMCP, "Stop MCP")
|
|
return layout.Inset{Left: 8}.Layout(gtx, button.Layout)
|
|
}),
|
|
)
|
|
}),
|
|
})
|
|
})
|
|
}),
|
|
)
|
|
})
|
|
}
|
|
|
|
func (ui *FileUI) layoutSettingsSection(gtx layout.Context, title string, children []layout.FlexChild) layout.Dimensions {
|
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
|
append([]layout.FlexChild{
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
label := ui.Theme.Muted(title, 0.9)
|
|
return layout.Inset{Bottom: 6}.Layout(gtx, label.Layout)
|
|
}),
|
|
}, children...)...,
|
|
)
|
|
}
|
|
|
|
func (ui *FileUI) layoutLabeledEditor(gtx layout.Context, labelText string, editor *widget.Editor) layout.Dimensions {
|
|
return layout.Inset{Top: 6}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
label := ui.Theme.Label(labelText, 1)
|
|
return layout.Inset{Right: 8}.Layout(gtx, label.Layout)
|
|
}),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
gtx.Constraints = layout.Exact(image.Pt(gtx.Metric.Dp(80), gtx.Metric.Dp(34)))
|
|
return gui.FocusBorder(ui.Theme.Theme, gtx.Focused(editor)).Layout(gtx,
|
|
material.Editor(ui.Theme.Theme, editor, "").Layout)
|
|
}),
|
|
)
|
|
})
|
|
}
|
|
|
|
func (ui *FileUI) handleSettingsActions(gtx layout.Context) {
|
|
changedVisual := false
|
|
for ui.Dark.Update(gtx) {
|
|
ui.Theme.SetDark(ui.Dark.Value)
|
|
changedVisual = true
|
|
}
|
|
for ui.ShowNativeAsm.Update(gtx) {
|
|
changedVisual = true
|
|
}
|
|
for ui.ShowAsmHelp.Update(gtx) {
|
|
changedVisual = true
|
|
}
|
|
for ui.SyntaxStyle.Update(gtx) {
|
|
ui.SyntaxStyle.Value = syntax.NormalizeStyle(ui.SyntaxStyle.Value)
|
|
changedVisual = true
|
|
}
|
|
if ui.updatePositiveIntEditor(gtx, &ui.TextSizeEditor, func(value int) {
|
|
ui.Theme.TextSize = unit.Sp(value)
|
|
}) {
|
|
changedVisual = true
|
|
}
|
|
if changedVisual {
|
|
ui.saveVisualSettings()
|
|
gtx.Execute(op.InvalidateCmd{})
|
|
ui.invalidateMain()
|
|
}
|
|
|
|
for ui.StartMCP.Clicked(gtx) {
|
|
ui.startMCP()
|
|
gtx.Execute(op.InvalidateCmd{})
|
|
}
|
|
for ui.StopMCP.Clicked(gtx) {
|
|
ui.stopMCP()
|
|
gtx.Execute(op.InvalidateCmd{})
|
|
}
|
|
}
|
|
|
|
func (ui *FileUI) updatePositiveIntEditor(gtx layout.Context, editor *widget.Editor, apply func(int)) bool {
|
|
changed := false
|
|
for {
|
|
ev, ok := editor.Update(gtx)
|
|
if !ok {
|
|
break
|
|
}
|
|
switch ev.(type) {
|
|
case widget.ChangeEvent, widget.SubmitEvent:
|
|
changed = true
|
|
}
|
|
}
|
|
if !changed {
|
|
return false
|
|
}
|
|
value, err := strconv.Atoi(strings.TrimSpace(editor.Text()))
|
|
if err != nil || value <= 0 {
|
|
return false
|
|
}
|
|
apply(value)
|
|
return true
|
|
}
|
|
|
|
func (ui *FileUI) saveVisualSettings() {
|
|
settings := ui.Settings
|
|
settings.Dark = ui.Dark.Value
|
|
settings.ShowNativeAsm = ui.ShowNativeAsm.Value
|
|
settings.ShowAsmHelp = ui.ShowAsmHelp.Value
|
|
settings.SyntaxStyle = syntax.NormalizeStyle(ui.SyntaxStyle.Value)
|
|
if value, err := strconv.Atoi(strings.TrimSpace(ui.TextSizeEditor.Text())); err == nil && value > 0 {
|
|
settings.TextSize = value
|
|
}
|
|
ui.saveSettings(settings)
|
|
}
|
|
|
|
func (ui *FileUI) saveSettings(settings AppSettings) {
|
|
ui.Settings = settings
|
|
ui.sessionDirty = true
|
|
ui.scheduleFlush()
|
|
}
|