1package ui
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "strings"
8
9 "github.com/bnema/dumber/internal/bootstrap"
10 "github.com/bnema/dumber/internal/logging"
11 "github.com/bnema/dumber/internal/ui/component"
12 "github.com/bnema/dumber/internal/ui/layout"
13 "github.com/bnema/puregotk/v4/gdk"
14 "github.com/bnema/puregotk/v4/gio"
15 "github.com/bnema/puregotk/v4/glib"
16 "github.com/bnema/puregotk/v4/gtk"
17 "github.com/bnema/puregotk/v4/layershell"
18)
19
20var layerShellLibraryAvailable = func() bool {
21 if bootstrap.LayerShellPreloadPresent(bootstrap.CurrentEnvMap()) {
22 return true
23 }
24
25 return bootstrap.LayerShellLibraryPath() != ""
26}
27
28const (
29 standaloneOmniboxNamespace = AppID + ".omnibox"
30 standaloneOmniboxTitle = "Dumber Omnibox"
31 standaloneOmniboxWidth = 1200
32 standaloneOmniboxHeight = 200
33 standaloneOmniboxWindowCSS = "standalone-omnibox-window"
34)
35
36type StandaloneOmniboxWindowConfig struct {
37 Namespace string
38 Title string
39 Width int
40 Height int
41}
42
43type StandaloneOmniboxRuntime struct {
44 OmniboxCfg component.OmniboxConfig
45 ApplyTheme func(display *gdk.Display)
46}
47
48type standaloneOmniboxHostLayoutConfig struct {
49 UseDefaultSize bool
50 OmniboxSize component.ModalSizeConfig
51}
52
53var (
54 layerShellIsSupported = layershell.IsSupported
55 layerShellInitForWindow = func(window *gtk.ApplicationWindow) {
56 layershell.InitForWindow(&window.Window)
57 }
58 layerShellSetLayer = func(window *gtk.ApplicationWindow, layer layershell.Layer) {
59 layershell.SetLayer(&window.Window, layer)
60 }
61 layerShellSetExclusiveZone = func(window *gtk.ApplicationWindow, zone int) {
62 layershell.SetExclusiveZone(&window.Window, zone)
63 }
64 layerShellSetNamespace = func(window *gtk.ApplicationWindow, namespace *string) {
65 layershell.SetNamespace(&window.Window, namespace)
66 }
67 layerShellSetKeyboardMode = func(window *gtk.ApplicationWindow, mode layershell.KeyboardMode) {
68 layershell.SetKeyboardMode(&window.Window, mode)
69 }
70 buildStandaloneOmniboxHostFn = buildStandaloneOmniboxHost
71 newStandaloneOmniboxWindow = gtk.NewApplicationWindow
72 configureStandaloneOmniboxWindowFn = configureStandaloneOmniboxWindow
73 presentStandaloneOmniboxWindow = func(window *gtk.ApplicationWindow) {
74 window.Present()
75 }
76 logStandaloneOmniboxHostAllocationFn = logStandaloneOmniboxHostAllocation
77 showStandaloneOmniboxFn = func(ctx context.Context, omnibox *component.Omnibox) {
78 omnibox.Show(ctx, "")
79 }
80)
81
82type standaloneOmniboxActivationRetention struct {
83 window *gtk.ApplicationWindow
84 host *gtk.Widget
85 omnibox *component.Omnibox
86}
87
88func (r *standaloneOmniboxActivationRetention) retain(
89 window *gtk.ApplicationWindow,
90 host *gtk.Widget,
91 omnibox *component.Omnibox,
92) {
93 if r == nil {
94 return
95 }
96 r.window = window
97 r.host = host
98 r.omnibox = omnibox
99}
100
101func (r *standaloneOmniboxActivationRetention) release() {
102 if r == nil {
103 return
104 }
105 r.window = nil
106 r.host = nil
107 r.omnibox = nil
108}
109
110func DefaultStandaloneOmniboxWindowConfig() StandaloneOmniboxWindowConfig {
111 return StandaloneOmniboxWindowConfig{
112 Namespace: standaloneOmniboxNamespace,
113 Title: standaloneOmniboxTitle,
114 Width: standaloneOmniboxWidth,
115 Height: standaloneOmniboxHeight,
116 }
117}
118
119func standaloneOmniboxArgv(args []string) []string {
120 if len(args) == 0 {
121 return []string{"dumber-omnibox"}
122 }
123
124 filtered := []string{args[0]}
125 subcommandSkipped := false
126 for i := 1; i < len(args); i++ {
127 arg := args[i]
128 if arg == "omnibox" && !subcommandSkipped {
129 subcommandSkipped = true
130 continue
131 }
132 if strings.HasPrefix(arg, "-test.") {
133 if !strings.Contains(arg, "=") && goTestFlagConsumesNextArg(arg) && i+1 < len(args) {
134 i++
135 }
136 continue
137 }
138 filtered = append(filtered, arg)
139 }
140
141 return filtered
142}
143
144func goTestFlagConsumesNextArg(arg string) bool {
145 switch arg {
146 case "-test.bench", "-test.blockprofile", "-test.count", "-test.coverprofile",
147 "-test.cpu", "-test.cpuprofile", "-test.fuzz", "-test.fuzzcachedir",
148 "-test.fuzzminimizetime", "-test.fuzztime", "-test.list", "-test.memprofile",
149 "-test.memprofilerate", "-test.mutexprofile", "-test.mutexprofilefraction",
150 "-test.outputdir", "-test.parallel", "-test.run", "-test.shuffle",
151 "-test.skip", "-test.timeout", "-test.trace":
152 return true
153 default:
154 return false
155 }
156}
157
158func wrapStandaloneOmniboxNavigate(original func(context.Context, string) error, quit func()) func(context.Context, string) error {
159 return func(ctx context.Context, url string) error {
160 if original == nil {
161 return fmt.Errorf("standalone omnibox navigate handler is not configured")
162 }
163 if err := original(ctx, url); err != nil {
164 return err
165 }
166 if quit != nil {
167 quit()
168 }
169 return nil
170 }
171}
172
173func standaloneOmniboxCloseHandler(quit func()) func() {
174 return func() {
175 if quit != nil {
176 quit()
177 }
178 }
179}
180
181func standaloneOmniboxWindowDecorated(layerShellApplied bool) bool {
182 return !layerShellApplied
183}
184
185func standaloneOmniboxWindowResizable(layerShellApplied bool) bool {
186 return !layerShellApplied
187}
188
189func standaloneOmniboxHostLayout(layerShellApplied bool) standaloneOmniboxHostLayoutConfig {
190 return standaloneOmniboxHostLayoutConfig{
191 UseDefaultSize: !layerShellApplied,
192 OmniboxSize: standaloneOmniboxSizeConfig(),
193 }
194}
195
196func attachStandaloneOmniboxHostWidget(host layout.OverlayWidget, omniboxWidget layout.Widget) {
197 if host == nil {
198 return
199 }
200
201 host.SetChild(omniboxWidget)
202}
203
204func standaloneOmniboxSizeConfig() component.ModalSizeConfig {
205 return component.ResolveModalSizeConfig(component.ModalSizeConfig{
206 FixedWidth: component.OmniboxSizeDefaults.MaxWidth,
207 FixedTopMargin: 0,
208 UseFixedTopMargin: true,
209 }, component.OmniboxSizeDefaults)
210}
211
212func tryInitLayerShell(window *gtk.ApplicationWindow, cfg StandaloneOmniboxWindowConfig) bool {
213 if window == nil {
214 return false
215 }
216 if !safeLayerShellSupported() {
217 return false
218 }
219
220 layerShellInitForWindow(window)
221 layerShellSetLayer(window, layershell.LayerOverlayValue)
222 layerShellSetExclusiveZone(window, 0)
223 layerShellSetNamespace(window, &cfg.Namespace)
224 layerShellSetKeyboardMode(window, layershell.KeyboardModeExclusiveValue)
225 return true
226}
227
228func safeLayerShellSupported() (supported bool) {
229 defer func() {
230 if recover() != nil {
231 supported = false
232 }
233 }()
234
235 if bootstrap.LayerShellPreloadPresent(bootstrap.CurrentEnvMap()) || layerShellLibraryAvailable() {
236 return layerShellIsSupported()
237 }
238
239 return false
240}
241
242func configureStandaloneOmniboxWindow(window *gtk.ApplicationWindow, windowCfg StandaloneOmniboxWindowConfig, child *gtk.Widget) {
243 if window == nil {
244 return
245 }
246
247 title := windowCfg.Title
248 window.SetTitle(&title)
249 layerShellApplied := tryInitLayerShell(window, windowCfg)
250 hostLayout := standaloneOmniboxHostLayout(layerShellApplied)
251 if hostLayout.UseDefaultSize {
252 window.SetDefaultSize(windowCfg.Width, windowCfg.Height)
253 }
254 if layerShellApplied {
255 window.AddCssClass(standaloneOmniboxWindowCSS)
256 }
257 window.SetDecorated(standaloneOmniboxWindowDecorated(layerShellApplied))
258 window.SetResizable(standaloneOmniboxWindowResizable(layerShellApplied))
259 window.SetChild(child)
260}
261
262func buildStandaloneOmniboxHost(
263 ctx context.Context,
264 runtimeCfg *StandaloneOmniboxRuntime,
265 gtkApp *gtk.Application,
266) (*gtk.Widget, *component.Omnibox) {
267 factory := layout.NewGtkWidgetFactory()
268 overlay := factory.NewOverlay()
269 if overlay == nil {
270 logging.FromContext(ctx).Error().Msg("failed to create standalone omnibox host widgets")
271 return nil, nil
272 }
273
274 cfg := runtimeCfg.OmniboxCfg
275 cfg.OnNavigate = wrapStandaloneOmniboxNavigate(cfg.OnNavigate, gtkApp.Quit)
276 cfg.SizeConfig = standaloneOmniboxSizeConfig()
277
278 omnibox := component.NewOmnibox(ctx, cfg)
279 if omnibox == nil {
280 logging.FromContext(ctx).Error().Msg("failed to create standalone omnibox")
281 return nil, nil
282 }
283
284 overlay.SetVisible(true)
285 omnibox.SetParentOverlay(overlay)
286 omnibox.SetOnClose(standaloneOmniboxCloseHandler(gtkApp.Quit))
287
288 omniboxWidget := omnibox.WidgetAsLayout(factory)
289 if omniboxWidget == nil {
290 logging.FromContext(ctx).Error().Msg("failed to wrap standalone omnibox widget")
291 return nil, nil
292 }
293
294 attachStandaloneOmniboxHostWidget(overlay, omniboxWidget)
295 logging.FromContext(ctx).Debug().Msg("standalone omnibox host configured with primary child")
296 return overlay.GtkWidget(), omnibox
297}
298
299func logStandaloneOmniboxHostAllocation(ctx context.Context, host *gtk.Widget) {
300 if host == nil {
301 return
302 }
303
304 var cb glib.SourceFunc = func(uintptr) bool {
305 logging.FromContext(ctx).Debug().
306 Int("width", host.GetAllocatedWidth()).
307 Int("height", host.GetAllocatedHeight()).
308 Msg("standalone omnibox host allocation")
309 return false
310 }
311 glib.IdleAdd(&cb, 0)
312}
313
314func activateStandaloneOmnibox(
315 ctx context.Context,
316 runtimeCfg *StandaloneOmniboxRuntime,
317 gtkApp *gtk.Application,
318 windowCfg StandaloneOmniboxWindowConfig,
319 retention *standaloneOmniboxActivationRetention,
320) {
321 child, omnibox := buildStandaloneOmniboxHostFn(ctx, runtimeCfg, gtkApp)
322 if child == nil || omnibox == nil {
323 gtkApp.Quit()
324 return
325 }
326
327 window := newStandaloneOmniboxWindow(gtkApp)
328 if window == nil {
329 logging.FromContext(ctx).Error().Msg("failed to create standalone omnibox window")
330 gtkApp.Quit()
331 return
332 }
333
334 retention.retain(window, child, omnibox)
335 configureStandaloneOmniboxWindowFn(window, windowCfg, child)
336 if runtimeCfg.ApplyTheme != nil {
337 if display := window.GetDisplay(); display != nil {
338 runtimeCfg.ApplyTheme(display)
339 }
340 }
341 presentStandaloneOmniboxWindow(window)
342 logStandaloneOmniboxHostAllocationFn(ctx, child)
343 showStandaloneOmniboxFn(ctx, omnibox)
344}
345
346func RunStandaloneOmnibox(ctx context.Context, runtimeCfg *StandaloneOmniboxRuntime) int {
347 if runtimeCfg == nil {
348 logging.FromContext(ctx).Error().Msg("standalone omnibox runtime not configured")
349 return 1
350 }
351
352 EnsureAdwaitaInitialized()
353 windowCfg := DefaultStandaloneOmniboxWindowConfig()
354 appID := windowCfg.Namespace
355 gtkApp := gtk.NewApplication(&appID, gtkApplicationFlags())
356 if gtkApp == nil {
357 logging.FromContext(ctx).Error().Msg("failed to create standalone omnibox application")
358 return 1
359 }
360 defer gtkApp.Unref()
361 retention := &standaloneOmniboxActivationRetention{}
362
363 activateCb := func(_ gio.Application) {
364 activateStandaloneOmnibox(ctx, runtimeCfg, gtkApp, windowCfg, retention)
365 }
366 gtkApp.ConnectActivate(&activateCb)
367
368 argv := standaloneOmniboxArgv(os.Args)
369 code := gtkApp.Run(len(argv), argv)
370 retention.release()
371 return code
372}