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 }