Files
prevara/window.go
2025-11-30 16:45:22 +00:00

232 lines
5.3 KiB
Go

package gel
import (
"runtime"
"time"
"github.com/p9c/p9/pkg/opts/binary"
"github.com/p9c/p9/pkg/opts/meta"
"github.com/p9c/p9/pkg/gel/clipboard"
"github.com/p9c/p9/pkg/gel/fonts/p9fonts"
"github.com/p9c/p9/pkg/qu"
uberatomic "go.uber.org/atomic"
"github.com/p9c/p9/pkg/gel/gio/app"
"github.com/p9c/p9/pkg/gel/gio/io/event"
l "github.com/p9c/p9/pkg/gel/gio/layout"
"github.com/p9c/p9/pkg/gel/gio/op"
"github.com/p9c/p9/pkg/gel/gio/unit"
)
type CallbackQueue chan func() error
func NewCallbackQueue(bufSize int) CallbackQueue {
return make(CallbackQueue, bufSize)
}
type scaledConfig struct {
Scale float32
}
func (s *scaledConfig) Now() time.Time {
return time.Now()
}
// Window is the main container for a gel application. It embeds Theme for styling
// and app.Window for platform integration. Window provides factory methods for
// creating widgets and handles the main event loop, clipboard operations, and
// overlay rendering.
type Window struct {
*Theme
*app.Window
opts []app.Option
scale *scaledConfig
Width *uberatomic.Int32 // stores the width at the beginning of render
Height *uberatomic.Int32
ops op.Ops
Runner CallbackQueue
overlay []*func(gtx l.Context)
ClipboardWriteReqs chan string
ClipboardReadReqs chan func(string)
ClipboardContent string
clipboardReadReady qu.C
clipboardReadResponse chan string
}
func (w *Window) PushOverlay(overlay *func(gtx l.Context)) {
w.overlay = append(w.overlay, overlay)
}
func (w *Window) PopOverlay(overlay *func(gtx l.Context)) {
if len(w.overlay) == 0 {
return
}
index := -1
for i := range w.overlay {
if overlay == w.overlay[i] {
index = i
break
}
}
if index != -1 {
if index == len(w.overlay)-1 {
w.overlay = w.overlay[:index]
} else if index == 0 {
w.overlay = w.overlay[1:]
} else {
w.overlay = append(w.overlay[:index], w.overlay[index+1:]...)
}
}
}
func (w *Window) Overlay(gtx l.Context) {
for _, overlay := range w.overlay {
(*overlay)(gtx)
}
}
// NewWindowP9 creates a new window
func NewWindowP9(quit chan struct{}) (out *Window) {
out = &Window{
scale: &scaledConfig{1},
Runner: NewCallbackQueue(32),
Width: uberatomic.NewInt32(0),
Height: uberatomic.NewInt32(0),
ClipboardWriteReqs: make(chan string, 1),
ClipboardReadReqs: make(chan func(string), 32),
clipboardReadReady: qu.Ts(1),
clipboardReadResponse: make(chan string,1),
}
out.Theme = NewTheme(
binary.New(meta.Data{}, false, nil),
p9fonts.Collection(), quit,
)
out.Theme.WidgetPool = out.NewPool()
clipboard.Start()
return
}
// NewWindow creates a new window
func NewWindow(th *Theme) (out *Window) {
out = &Window{
Theme: th,
scale: &scaledConfig{1},
}
return
}
// Title sets the title of the window
func (w *Window) Title(title string) (out *Window) {
w.opts = append(w.opts, app.Title(title))
return w
}
// Size sets the dimensions of the window
func (w *Window) Size(width, height float32) (out *Window) {
w.opts = append(
w.opts,
app.Size(unit.Dp(float32(w.TextSize)*width), unit.Dp(float32(w.TextSize)*height)),
)
return w
}
// Scale sets the scale factor for rendering
func (w *Window) Scale(s float32) *Window {
w.scale = &scaledConfig{s}
return w
}
// Open sets the window options and initialise the node.window
func (w *Window) Open() (out *Window) {
if w.scale == nil {
w.Scale(1)
}
if w.opts != nil {
w.Window = new(app.Window)
w.Window.Option(w.opts...)
w.opts = nil
}
return w
}
func (w *Window) Run(frame func(ctx l.Context) l.Dimensions, destroy func(), quit qu.C,) (e error) {
// Start background tasks goroutine
go func() {
ticker := time.NewTicker(time.Second)
for {
select {
case content := <-w.ClipboardWriteReqs:
// Clipboard write is handled through commands in frame events
w.ClipboardContent = content
w.Invalidate()
case fn := <-w.ClipboardReadReqs:
go func() {
// Request clipboard read - response comes via clipboard event
w.Invalidate()
fn(<-w.clipboardReadResponse)
}()
case <-ticker.C:
w.TextSize = GetScaledTextSize(unit.Sp(16))
w.Invalidate()
case fn := <-w.Runner:
if e := fn(); E.Chk(e) {
return
}
case <-quit.Wait():
return
}
}
}()
// Main event loop using blocking Event() method
runner := func() {
for {
ev := w.Window.Event()
if e = w.processEvents(ev, frame, destroy); E.Chk(e) {
return
}
// Check for quit signal
select {
case <-quit.Wait():
return
default:
}
}
}
switch runtime.GOOS {
case "ios", "android":
go runner()
default:
runner()
}
return
}
// WriteClipboard writes content to the system clipboard
func (w *Window) WriteClipboard(content string) {
w.ClipboardWriteReqs <- content
}
func (w *Window) processEvents(e event.Event, frame func(ctx l.Context) l.Dimensions, destroy func()) error {
switch ev := e.(type) {
case app.DestroyEvent:
D.Ln("received destroy event", ev.Err)
destroy()
return ev.Err
case app.FrameEvent:
ops := op.Ops{}
c := app.NewContext(&ops, ev)
// update dimensions for responsive sizing widgets
w.Width.Store(int32(c.Constraints.Max.X))
w.Height.Store(int32(c.Constraints.Max.Y))
frame(c)
w.Overlay(c)
ev.Frame(c.Ops)
}
return nil
}