1package ui
2
3import (
4 "context"
5 "fmt"
6 "sync"
7
8 "github.com/bnema/dumber/internal/application/port"
9 "github.com/bnema/dumber/internal/domain/entity"
10 "github.com/bnema/dumber/internal/logging"
11 "github.com/bnema/dumber/internal/ui/coordinator/content"
12 "github.com/bnema/dumber/internal/ui/layout"
13 "github.com/bnema/dumber/internal/ui/window"
14 "github.com/bnema/puregotk/v4/gtk"
15)
16
17type nativePopupWindow struct {
18 popupID port.WebViewID
19 parentPaneID entity.PaneID
20 parentWindowID string
21 popupWindow *window.PopupWindow
22 webView port.WebView
23 closeOnce sync.Once
24}
25
26type nativePopupDestroyer interface {
27 Destroy()
28}
29
30var newPopupWindow = window.NewPopup
31
32func destroyFailedNativePopupSetup(popupShell nativePopupDestroyer, wv port.WebView) {
33 if wv != nil && !wv.IsDestroyed() {
34 wv.Destroy()
35 }
36 if popupShell != nil {
37 popupShell.Destroy()
38 }
39}
40
41func prepareNativePopupContentWidget(widget layout.Widget) (*gtk.Widget, error) {
42 if widget == nil {
43 return nil, fmt.Errorf("failed to wrap native popup webview widget")
44 }
45 gtkWidget := widget.GtkWidget()
46 if gtkWidget == nil {
47 return nil, fmt.Errorf("failed to wrap native popup webview widget")
48 }
49 widget.SetHexpand(true)
50 widget.SetVexpand(true)
51 return gtkWidget, nil
52}
53
54func (a *App) ensureNativePopupWindows() {
55 if a.nativePopupWindows == nil {
56 a.nativePopupWindows = make(map[port.WebViewID]*nativePopupWindow)
57 }
58}
59
60func (a *App) openNativePopupWindow(ctx context.Context, input content.NativePopupInput) error {
61 if a == nil || a.gtkApp == nil {
62 return fmt.Errorf("gtk application not available for native popup")
63 }
64 if input.PopupWebView == nil {
65 return fmt.Errorf("native popup webview is nil")
66 }
67
68 popupShell, err := newPopupWindow(ctx, a.gtkApp)
69 if err != nil {
70 destroyFailedNativePopupSetup(nil, input.PopupWebView)
71 return err
72 }
73 if a.contentCoord == nil {
74 destroyFailedNativePopupSetup(popupShell, input.PopupWebView)
75 return fmt.Errorf("content coordinator not available for native popup")
76 }
77 widget := a.contentCoord.WrapWidget(ctx, input.PopupWebView)
78 gtkWidget, err := prepareNativePopupContentWidget(widget)
79 if err != nil {
80 destroyFailedNativePopupSetup(popupShell, input.PopupWebView)
81 return err
82 }
83 popupShell.SetContent(gtkWidget)
84
85 parentWindowID := ""
86 if bw := a.browserWindowForAnyPane(input.ParentPaneID); bw != nil {
87 parentWindowID = bw.id
88 }
89
90 popupID := input.PopupWebView.ID()
91 state := &nativePopupWindow{
92 popupID: popupID,
93 parentPaneID: input.ParentPaneID,
94 parentWindowID: parentWindowID,
95 popupWindow: popupShell,
96 webView: input.PopupWebView,
97 }
98 popupShell.SetTitle("Dumber")
99
100 a.ensureNativePopupWindows()
101 a.nativePopupWindows[popupID] = state
102
103 if popupShell.Window() != nil {
104 closeRequestCb := func(_ gtk.Window) bool {
105 a.releaseNativePopupWindow(popupID, false, false)
106 return false
107 }
108 popupShell.Window().ConnectCloseRequest(&closeRequestCb)
109 }
110
111 if aborter, ok := input.PopupWebView.(port.NativePopupHostAbortCapable); ok {
112 aborter.SetNativePopupHostAbort(func() {
113 a.dispatchOnMainThread(func() {
114 a.releaseNativePopupWindow(popupID, true, false)
115 })
116 })
117 }
118 if lifecycle, ok := input.PopupWebView.(port.PopupLifecycleCapable); ok {
119 lifecycle.SetOnReadyToShow(func() {
120 a.dispatchOnMainThread(func() {
121 a.showNativePopupWindow(popupID)
122 })
123 })
124 lifecycle.SetOnClose(func() {
125 a.dispatchOnMainThread(func() {
126 a.releaseNativePopupWindow(popupID, true, false)
127 })
128 })
129 } else {
130 popupShell.Show()
131 }
132 if oauthWV, ok := input.PopupWebView.(port.OAuthCallbackCapable); ok {
133 oauthWV.AddCloseCallback(func() {
134 a.dispatchOnMainThread(func() {
135 a.releaseNativePopupWindow(popupID, true, false)
136 })
137 })
138 }
139 if a.contentCoord != nil && input.ObserveOAuthAutoClose {
140 a.contentCoord.ObserveNativePopupAuth(ctx, input)
141 }
142
143 logging.FromContext(ctx).Info().
144 Uint64("popup_id", uint64(popupID)).
145 Str("target_uri", input.TargetURI).
146 Str("parent_pane_id", string(input.ParentPaneID)).
147 Msg("native popup host created")
148 return nil
149}
150
151func (a *App) showNativePopupWindow(popupID port.WebViewID) {
152 if a == nil || a.nativePopupWindows == nil {
153 return
154 }
155 if state := a.nativePopupWindows[popupID]; state != nil && state.popupWindow != nil {
156 state.popupWindow.Show()
157 }
158}
159
160func (a *App) releaseNativePopupWindow(popupID port.WebViewID, closeWindow, destroyWindow bool) {
161 if a == nil || a.nativePopupWindows == nil {
162 return
163 }
164 state := a.nativePopupWindows[popupID]
165 if state == nil {
166 return
167 }
168 state.closeOnce.Do(func() {
169 delete(a.nativePopupWindows, popupID)
170 if state.webView != nil && !state.webView.IsDestroyed() {
171 state.webView.Destroy()
172 }
173 if state.popupWindow != nil {
174 switch {
175 case destroyWindow:
176 state.popupWindow.Destroy()
177 case closeWindow:
178 state.popupWindow.Close()
179 }
180 }
181 })
182}