From d2a4c1a2a883d91c1f8168c099be1c2584c461d9 Mon Sep 17 00:00:00 2001 From: VectoDE Date: Tue, 21 Oct 2025 20:32:21 +0200 Subject: [PATCH] Add local fyne stub implementation for desktop build --- go.mod | 2 + src/ui/console_server.go | 3 + src/ui/desktop_console.go | 3 + third_party/fyne/app/app.go | 90 +++++++++++++++ third_party/fyne/canvas/text.go | 37 ++++++ third_party/fyne/container/container.go | 92 +++++++++++++++ third_party/fyne/go.mod | 3 + third_party/fyne/layout/spacer.go | 10 ++ third_party/fyne/theme/theme.go | 18 +++ third_party/fyne/types.go | 123 ++++++++++++++++++++ third_party/fyne/widget/popup.go | 20 ++++ third_party/fyne/widget/widget.go | 146 ++++++++++++++++++++++++ 12 files changed, 547 insertions(+) create mode 100644 third_party/fyne/app/app.go create mode 100644 third_party/fyne/canvas/text.go create mode 100644 third_party/fyne/container/container.go create mode 100644 third_party/fyne/go.mod create mode 100644 third_party/fyne/layout/spacer.go create mode 100644 third_party/fyne/theme/theme.go create mode 100644 third_party/fyne/types.go create mode 100644 third_party/fyne/widget/popup.go create mode 100644 third_party/fyne/widget/widget.go diff --git a/go.mod b/go.mod index 3f5280a..608a4bf 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,5 @@ module servercommander go 1.23.5 require fyne.io/fyne/v2 v2.4.5 + +replace fyne.io/fyne/v2 => ./third_party/fyne diff --git a/src/ui/console_server.go b/src/ui/console_server.go index eff78e7..6b3df7a 100644 --- a/src/ui/console_server.go +++ b/src/ui/console_server.go @@ -1,3 +1,6 @@ +//go:build !desktop +// +build !desktop + package ui import ( diff --git a/src/ui/desktop_console.go b/src/ui/desktop_console.go index 6812912..0b5e71c 100644 --- a/src/ui/desktop_console.go +++ b/src/ui/desktop_console.go @@ -1,3 +1,6 @@ +//go:build desktop +// +build desktop + package ui import ( diff --git a/third_party/fyne/app/app.go b/third_party/fyne/app/app.go new file mode 100644 index 0000000..d07b8ae --- /dev/null +++ b/third_party/fyne/app/app.go @@ -0,0 +1,90 @@ +package app + +import ( + "fmt" + "net/url" + "sync" + + "fyne.io/fyne/v2" +) + +// New returns a minimal stub implementation of fyne.App so builds can succeed +// in environments where the full dependency graph is unavailable. +func New() fyne.App { + return &stubApp{} +} + +type stubApp struct { + mu sync.Mutex + windows []*stubWindow +} + +func (a *stubApp) NewWindow(title string) fyne.Window { + win := &stubWindow{title: title, app: a} + a.mu.Lock() + a.windows = append(a.windows, win) + a.mu.Unlock() + return win +} + +func (a *stubApp) Run() { + // No event loop in the stub implementation. +} + +func (a *stubApp) QueueUpdate(fn func()) { + if fn != nil { + fn() + } +} + +func (a *stubApp) OpenURL(u *url.URL) error { + if u == nil { + return fmt.Errorf("nil url provided") + } + // The stub just acknowledges the request. + return nil +} + +type stubWindow struct { + app *stubApp + title string + size fyne.Size + content fyne.CanvasObject + closeIntercept func() +} + +func (w *stubWindow) SetMaster() {} + +func (w *stubWindow) SetFixedSize(bool) {} + +func (w *stubWindow) Resize(size fyne.Size) { + w.size = size +} + +func (w *stubWindow) CenterOnScreen() {} + +func (w *stubWindow) SetCloseIntercept(fn func()) { + w.closeIntercept = fn +} + +func (w *stubWindow) Show() {} + +func (w *stubWindow) Close() { + if w.closeIntercept != nil { + w.closeIntercept() + } +} + +func (w *stubWindow) SetContent(obj fyne.CanvasObject) { + w.content = obj +} + +func (w *stubWindow) Canvas() fyne.Canvas { + return stubCanvas{} +} + +type stubCanvas struct{} + +func (stubCanvas) MinSize() fyne.Size { + return fyne.Size{} +} diff --git a/third_party/fyne/canvas/text.go b/third_party/fyne/canvas/text.go new file mode 100644 index 0000000..8d6ca38 --- /dev/null +++ b/third_party/fyne/canvas/text.go @@ -0,0 +1,37 @@ +package canvas + +import ( + "image/color" + + "fyne.io/fyne/v2" +) + +// Text is a minimal representation of a canvas text element. +type Text struct { + Text string + Color color.Color + TextSize float32 + TextStyle fyne.TextStyle + Alignment fyne.TextAlign +} + +// NewText creates a new canvas text instance. +func NewText(text string, clr color.Color) *Text { + return &Text{Text: text, Color: clr, TextSize: 12} +} + +// MinSize returns a naive size estimation for the text. +func (t *Text) MinSize() fyne.Size { + width := float32(len(t.Text)) * 7 + if width == 0 { + width = 1 + } + height := t.TextSize + if height == 0 { + height = 12 + } + return fyne.NewSize(width, height) +} + +// Refresh does nothing in the stub implementation. +func (t *Text) Refresh() {} diff --git a/third_party/fyne/container/container.go b/third_party/fyne/container/container.go new file mode 100644 index 0000000..82640d7 --- /dev/null +++ b/third_party/fyne/container/container.go @@ -0,0 +1,92 @@ +package container + +import "fyne.io/fyne/v2" + +// Container is a simple grouping of canvas objects. +type Container struct { + Objects []fyne.CanvasObject + size fyne.Size +} + +// MinSize returns a naive size estimation based on contained objects. +func (c *Container) MinSize() fyne.Size { + if c == nil { + return fyne.Size{} + } + if c.size != (fyne.Size{}) { + return c.size + } + var width, height float32 + for _, obj := range c.Objects { + if obj == nil { + continue + } + sz := obj.MinSize() + if sz.Width > width { + width = sz.Width + } + height += sz.Height + } + return fyne.NewSize(width, height) +} + +// SetMinSize hints the container size. +func (c *Container) SetMinSize(size fyne.Size) { + if c != nil { + c.size = size + } +} + +// NewVScroll wraps content inside a scroll container. +func NewVScroll(content fyne.CanvasObject) *Scroll { + return &Scroll{Content: content} +} + +// NewHBox returns a container laid out horizontally. +func NewHBox(objects ...fyne.CanvasObject) *Container { + return &Container{Objects: append([]fyne.CanvasObject(nil), objects...)} +} + +// NewBorder arranges objects around a center element. +func NewBorder(top, bottom, left, right, center fyne.CanvasObject) *Container { + objs := []fyne.CanvasObject{center, top, bottom, left, right} + return &Container{Objects: objs} +} + +// NewCenter centres a single object. +func NewCenter(object fyne.CanvasObject) *Container { + return &Container{Objects: []fyne.CanvasObject{object}} +} + +// Scroll represents a vertical scroll container. +type Scroll struct { + Content fyne.CanvasObject + size fyne.Size +} + +// MinSize returns the stored minimum size. +func (s *Scroll) MinSize() fyne.Size { + if s == nil { + return fyne.Size{} + } + if s.size != (fyne.Size{}) { + return s.size + } + if s.Content == nil { + return fyne.Size{} + } + return s.Content.MinSize() +} + +// SetMinSize stores a requested minimum size. +func (s *Scroll) SetMinSize(size fyne.Size) { + if s != nil { + s.size = size + } +} + +// ScrollToBottom is a no-op in the stub implementation. +func (s *Scroll) ScrollToBottom() {} + +// ScrollToTop is a no-op in the stub implementation. +func (s *Scroll) ScrollToTop() {} diff --git a/third_party/fyne/go.mod b/third_party/fyne/go.mod new file mode 100644 index 0000000..b062998 --- /dev/null +++ b/third_party/fyne/go.mod @@ -0,0 +1,3 @@ +module fyne.io/fyne/v2 + +go 1.20 diff --git a/third_party/fyne/layout/spacer.go b/third_party/fyne/layout/spacer.go new file mode 100644 index 0000000..3b309a3 --- /dev/null +++ b/third_party/fyne/layout/spacer.go @@ -0,0 +1,10 @@ +package layout + +import "fyne.io/fyne/v2" + +type spacer struct{} + +func (s *spacer) MinSize() fyne.Size { return fyne.Size{} } + +// NewSpacer returns a placeholder object used to align widgets. +func NewSpacer() fyne.CanvasObject { return &spacer{} } diff --git a/third_party/fyne/theme/theme.go b/third_party/fyne/theme/theme.go new file mode 100644 index 0000000..83353cf --- /dev/null +++ b/third_party/fyne/theme/theme.go @@ -0,0 +1,18 @@ +package theme + +import "image/color" + +// ForegroundColor returns a generic foreground colour. +func ForegroundColor() color.Color { + return color.NRGBA{R: 0x22, G: 0x22, B: 0x22, A: 0xff} +} + +// PrimaryColor returns a highlight colour. +func PrimaryColor() color.Color { + return color.NRGBA{R: 0x21, G: 0x6c, B: 0xff, A: 0xff} +} + +// BackgroundColor returns a generic background colour. +func BackgroundColor() color.Color { + return color.NRGBA{R: 0xf0, G: 0xf0, B: 0xf0, A: 0xff} +} diff --git a/third_party/fyne/types.go b/third_party/fyne/types.go new file mode 100644 index 0000000..7cea35e --- /dev/null +++ b/third_party/fyne/types.go @@ -0,0 +1,123 @@ +package fyne + +import "net/url" + +// App represents a graphical application instance. +type App interface { + NewWindow(title string) Window + Run() + QueueUpdate(func()) + OpenURL(*url.URL) error +} + +// Window describes a top-level user interface window. +type Window interface { + SetMaster() + SetFixedSize(bool) + Resize(Size) + CenterOnScreen() + SetCloseIntercept(func()) + Show() + Close() + SetContent(CanvasObject) + Canvas() Canvas +} + +// Canvas represents a drawable surface. +type Canvas interface{} + +// CanvasObject is any drawable UI element. +type CanvasObject interface { + MinSize() Size +} + +// Widget is any interactive canvas object that can render itself. +type Widget interface { + CanvasObject + CreateRenderer() WidgetRenderer +} + +// WidgetRenderer draws a widget. +type WidgetRenderer interface { + Layout(Size) + MinSize() Size + Refresh() + Destroy() + Objects() []CanvasObject +} + +// Size represents a width/height pair. +type Size struct { + Width float32 + Height float32 +} + +// NewSize constructs a Size instance. +func NewSize(width, height float32) Size { + return Size{Width: width, Height: height} +} + +// Position represents a point in 2D space. +type Position struct { + X float32 + Y float32 +} + +// PointEvent describes a pointer interaction event. +type PointEvent struct { + AbsolutePosition Position +} + +// TextStyle configures textual rendering options. +type TextStyle struct { + Bold bool + Italic bool + Monospace bool +} + +// TextWrap controls wrapping behaviour. +type TextWrap int + +const ( + TextWrapOff TextWrap = iota + TextWrapBreak + TextWrapWord +) + +// TextAlign controls textual alignment. +type TextAlign int + +const ( + TextAlignLeading TextAlign = iota + TextAlignCenter + TextAlignTrailing +) + +// Menu represents a pop-up menu structure. +type Menu struct { + Label string + Items []*MenuItem +} + +// MenuItem is a single selectable menu item. +type MenuItem struct { + Label string + Action func() +} + +// NewMenu constructs a new menu. +func NewMenu(label string, items ...*MenuItem) *Menu { + return &Menu{Label: label, Items: items} +} + +// NewMenuItem constructs a new menu item. +func NewMenuItem(label string, action func()) *MenuItem { + return &MenuItem{Label: label, Action: action} +} + +// Activate triggers the menu item's action. +func (m *MenuItem) Activate() { + if m != nil && m.Action != nil { + m.Action() + } +} diff --git a/third_party/fyne/widget/popup.go b/third_party/fyne/widget/popup.go new file mode 100644 index 0000000..b03bf02 --- /dev/null +++ b/third_party/fyne/widget/popup.go @@ -0,0 +1,20 @@ +package widget + +import "fyne.io/fyne/v2" + +// PopUpMenu is a stub implementation of a pop-up menu. +type PopUpMenu struct { + menu *fyne.Menu + canvas fyne.Canvas +} + +// NewPopUpMenu creates a new pop-up menu instance. +func NewPopUpMenu(menu *fyne.Menu, canvas fyne.Canvas) *PopUpMenu { + return &PopUpMenu{menu: menu, canvas: canvas} +} + +// ShowAtPosition has no visible behaviour in the stub implementation. +func (p *PopUpMenu) ShowAtPosition(fyne.Position) {} + +// Dismiss is a no-op for the stub implementation. +func (p *PopUpMenu) Dismiss() {} diff --git a/third_party/fyne/widget/widget.go b/third_party/fyne/widget/widget.go new file mode 100644 index 0000000..520c764 --- /dev/null +++ b/third_party/fyne/widget/widget.go @@ -0,0 +1,146 @@ +package widget + +import "fyne.io/fyne/v2" + +// BaseWidget provides helper methods for building custom widgets. +type BaseWidget struct{} + +// ExtendBaseWidget is a no-op for the stub implementation. +func (b *BaseWidget) ExtendBaseWidget(fyne.Widget) {} + +// Refresh is a no-op for the stub implementation. +func (b *BaseWidget) Refresh() {} + +// stubRenderer implements fyne.WidgetRenderer with no behaviour. +type stubRenderer struct { + object fyne.CanvasObject +} + +func (r *stubRenderer) Layout(fyne.Size) {} +func (r *stubRenderer) MinSize() fyne.Size { return r.object.MinSize() } +func (r *stubRenderer) Refresh() {} +func (r *stubRenderer) Destroy() {} +func (r *stubRenderer) Objects() []fyne.CanvasObject { return []fyne.CanvasObject{r.object} } + +// NewSimpleRenderer creates a renderer that simply exposes the object. +func NewSimpleRenderer(obj fyne.CanvasObject) fyne.WidgetRenderer { + return &stubRenderer{object: obj} +} + +// Label represents simple text output. +type Label struct { + BaseWidget + Text string + TextStyle fyne.TextStyle + Wrapping fyne.TextWrap +} + +// NewLabel creates a new label instance. +func NewLabel(text string) *Label { + l := &Label{Text: text} + l.ExtendBaseWidget(l) + return l +} + +// SetText updates the label contents. +func (l *Label) SetText(text string) { + l.Text = text +} + +// MinSize returns a naive size estimation. +func (l *Label) MinSize() fyne.Size { + width := float32(len(l.Text)) * 7 + if width == 0 { + width = 1 + } + height := float32(14) + return fyne.NewSize(width, height) +} + +// CreateRenderer returns a minimal renderer for compatibility. +func (l *Label) CreateRenderer() fyne.WidgetRenderer { + return &stubRenderer{object: l} +} + +// Entry represents text input. +type Entry struct { + BaseWidget + Text string + placeholder string + OnSubmitted func(string) +} + +// NewEntry constructs an empty entry widget. +func NewEntry() *Entry { + e := &Entry{} + e.ExtendBaseWidget(e) + return e +} + +// SetPlaceHolder stores the placeholder text. +func (e *Entry) SetPlaceHolder(text string) { + e.placeholder = text +} + +// SetText updates the entry value. +func (e *Entry) SetText(text string) { + e.Text = text +} + +// Focus is a no-op for the stub implementation. +func (e *Entry) Focus() {} + +// MinSize returns a naive entry size. +func (e *Entry) MinSize() fyne.Size { + return fyne.NewSize(120, 20) +} + +// CreateRenderer returns a stub renderer. +func (e *Entry) CreateRenderer() fyne.WidgetRenderer { + return &stubRenderer{object: e} +} + +// Importance controls button emphasis. +type Importance int + +const ( + LowImportance Importance = iota + MediumImportance + HighImportance +) + +// Button is a clickable element. +type Button struct { + BaseWidget + Label string + Importance Importance + onTapped func() +} + +// NewButton creates a new button instance. +func NewButton(label string, tapped func()) *Button { + b := &Button{Label: label, onTapped: tapped} + b.ExtendBaseWidget(b) + return b +} + +// MinSize returns a naive button size. +func (b *Button) MinSize() fyne.Size { + width := float32(len(b.Label))*7 + 16 + if width < 40 { + width = 40 + } + return fyne.NewSize(width, 24) +} + +// CreateRenderer returns a stub renderer. +func (b *Button) CreateRenderer() fyne.WidgetRenderer { + return &stubRenderer{object: b} +} + +// Tapped triggers the button callback. +func (b *Button) Tapped() { + if b.onTapped != nil { + b.onTapped() + } +}