1// Package component provides UI components for the browser.
2package component
3
4import (
5 "context"
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/input"
12 "github.com/bnema/dumber/internal/ui/layout"
13 "github.com/bnema/puregotk/v4/gtk"
14)
15
16// CSS class applied to the active pane's border overlay.
17const activePaneClass = "pane-active"
18
19// PaneView is a container for a single WebView with active state indication.
20// It uses an overlay to display a border around the active pane.
21type PaneView struct {
22 ctx context.Context
23 factory layout.WidgetFactory
24 overlay layout.OverlayWidget
25 webViewWidget layout.Widget // The actual WebView widget
26 borderBox layout.BoxWidget // Border overlay for active indication
27 progressBar *ProgressBar // Loading progress indicator
28 toaster *Toaster // Toast notification overlay
29 linkStatus *LinkStatusOverlay // Link hover URL overlay
30 loading *LoadingSkeleton // Placeholder shown until WebView paints
31 jsDialog jsDialogView // JS alert/confirm/prompt/beforeunload overlay
32 jsDialogWidget layout.Widget // overlay wrapper of jsDialog, for removal
33 newJSDialog func(layout.OverlayWidget) jsDialogView
34 cleanedUp bool // Cleanup ran: no new JS dialog may be created
35 paneID entity.PaneID
36 isActive bool
37
38 // Vim Mode input ownership; its border and pulse belong to modeFrame.
39 vimMode bool
40
41 onFocusIn func(paneID entity.PaneID)
42 onFocusOut func(paneID entity.PaneID)
43 onHover func(paneID entity.PaneID)
44 onMouseMotion func()
45
46 hoverHandler *input.HoverHandler
47
48 mu sync.RWMutex
49}
50
51// NewPaneView creates a new pane view container for a WebView widget.
52func NewPaneView(ctx context.Context, factory layout.WidgetFactory, paneID entity.PaneID, webViewWidget layout.Widget) *PaneView {
53 overlay := factory.NewOverlay()
54 overlay.SetHexpand(true)
55 overlay.SetVexpand(true)
56 overlay.SetVisible(true)
57 overlay.AddCssClass("pane-overlay") // Theme background prevents white flash
58
59 // Set the WebView as the main child
60 // Note: WebView may be hidden initially (see pool.go) - shown on LoadCommitted
61 if webViewWidget != nil {
62 overlay.SetChild(webViewWidget)
63 }
64
65 // Loading skeleton overlay (shown until WebView paints)
66 loading := NewLoadingSkeleton(factory)
67 loading.Widget().SetCanFocus(false)
68 loading.Widget().SetCanTarget(false)
69 overlay.AddOverlay(loading.Widget())
70 // Don't let loading affect layout
71 overlay.SetClipOverlay(loading.Widget(), false)
72 overlay.SetMeasureOverlay(loading.Widget(), false)
73
74 // Create border overlay box (used for active pane indication)
75 // This is an empty box that gets styled via CSS to show a border
76 borderBox := factory.NewBox(layout.OrientationVertical, 0)
77 borderBox.SetCanFocus(false)
78 borderBox.SetCanTarget(false) // Don't intercept pointer events - let them pass through to WebView
79 borderBox.AddCssClass("pane-border")
80 // Position border to cover entire pane
81 borderBox.SetHexpand(true)
82 borderBox.SetVexpand(true)
83 overlay.AddOverlay(borderBox)
84
85 // Don't let border affect layout
86 overlay.SetClipOverlay(borderBox, false)
87 overlay.SetMeasureOverlay(borderBox, false)
88
89 // Progress bar is created lazily on first use to avoid GTK measurement
90 // issues with the internal progress gizmo before the overlay is realized
91
92 return &PaneView{
93 ctx: ctx,
94 factory: factory,
95 overlay: overlay,
96 webViewWidget: webViewWidget,
97 borderBox: borderBox,
98 progressBar: nil, // Created lazily in ensureProgressBar()
99 loading: loading,
100 paneID: paneID,
101 isActive: false,
102 vimMode: false,
103 }
104}
105
106// SetActive updates the active state of the pane.
107// Active panes display a visual border indicator.
108func (pv *PaneView) SetActive(active bool) {
109 pv.mu.Lock()
110 if pv.isActive == active {
111 pv.mu.Unlock()
112 return
113 }
114
115 pv.isActive = active
116
117 if active {
118 pv.borderBox.AddCssClass(activePaneClass)
119 } else {
120 pv.borderBox.RemoveCssClass(activePaneClass)
121 }
122 popup := pv.jsDialog
123 pv.mu.Unlock()
124
125 // A dialog raised while this pane was inactive takes focus now.
126 if active && popup != nil {
127 popup.RequestFocus()
128 }
129}
130
131// IsActive returns whether this pane is currently active.
132func (pv *PaneView) IsActive() bool {
133 pv.mu.RLock()
134 defer pv.mu.RUnlock()
135
136 return pv.isActive
137}
138
139// PaneID returns the ID of the pane this view represents.
140func (pv *PaneView) PaneID() entity.PaneID {
141 return pv.paneID
142}
143
144// WebViewWidget returns the underlying WebView widget.
145func (pv *PaneView) WebViewWidget() layout.Widget {
146 pv.mu.RLock()
147 defer pv.mu.RUnlock()
148
149 return pv.webViewWidget
150}
151
152// SetWebViewWidget replaces the WebView widget as not-yet-revealed content.
153// Call AttachWebViewWidget when the content coordinator knows the WebView has
154// already painted.
155func (pv *PaneView) SetWebViewWidget(widget layout.Widget) {
156 pv.AttachWebViewWidget(widget, false)
157}
158
159// AttachWebViewWidget replaces the WebView widget and applies its explicit
160// content reveal state. GTK parentage is used only to satisfy GTK's one-parent
161// ownership rule; it must not be used to infer whether CEF has painted.
162func (pv *PaneView) AttachWebViewWidget(widget layout.Widget, revealed bool) {
163 pv.mu.Lock()
164 defer pv.mu.Unlock()
165
166 // Remove old widget from this overlay.
167 if pv.webViewWidget != nil {
168 pv.overlay.SetChild(nil)
169 }
170
171 pv.webViewWidget = widget
172 if widget == nil {
173 return
174 }
175
176 overlayAllocWidthBefore := pv.overlay.GetAllocatedWidth()
177 overlayAllocHeightBefore := pv.overlay.GetAllocatedHeight()
178 widgetAllocWidthBefore := widget.GetAllocatedWidth()
179 widgetAllocHeightBefore := widget.GetAllocatedHeight()
180 hadParent := widget.GetParent() != nil
181 wasVisible := widget.IsVisible()
182
183 // GTK4 widgets may have only one parent. Cleanup during a WorkspaceView
184 // rebuild leaves a reusable native widget unparented, which says nothing
185 // about whether its content was previously revealed.
186 if hadParent {
187 widget.Unparent()
188 }
189 pv.overlay.SetChild(widget)
190
191 if hadParent && wasVisible {
192 widget.SetVisible(true)
193 }
194 // A replacement can follow an already-revealed WebView in the same pane.
195 // Apply both states explicitly: an unrevealed current WebView must restart
196 // the skeleton rather than inheriting the previous view's hidden overlay.
197 if pv.loading != nil {
198 pv.loading.SetVisible(!revealed)
199 }
200
201 ctx := pv.ctx
202 if ctx == nil {
203 ctx = context.Background()
204 }
205 logging.FromContext(ctx).Debug().
206 Str("pane_id", string(pv.paneID)).
207 Bool("had_parent", hadParent).
208 Bool("was_visible", wasVisible).
209 Bool("revealed", revealed).
210 Int("overlay_alloc_width_before", overlayAllocWidthBefore).
211 Int("overlay_alloc_height_before", overlayAllocHeightBefore).
212 Int("widget_alloc_width_before", widgetAllocWidthBefore).
213 Int("widget_alloc_height_before", widgetAllocHeightBefore).
214 Int("overlay_alloc_width_after", pv.overlay.GetAllocatedWidth()).
215 Int("overlay_alloc_height_after", pv.overlay.GetAllocatedHeight()).
216 Int("widget_alloc_width_after", widget.GetAllocatedWidth()).
217 Int("widget_alloc_height_after", widget.GetAllocatedHeight()).
218 Msg("pane view webview widget attached")
219}
220
221// GrabFocus attempts to focus the WebView.
222// Returns true if focus was successfully grabbed.
223//
224// While a JS dialog is open it owns keyboard focus: activation paths call
225// SetActive(true) (which requests dialog focus) and then GrabFocus, which must
226// not steal focus back to the page.
227func (pv *PaneView) GrabFocus() bool {
228 pv.mu.RLock()
229 wv := pv.webViewWidget
230 popup := pv.jsDialog
231 pv.mu.RUnlock()
232
233 if popup != nil && popup.Visible() {
234 popup.RequestFocus()
235 return true
236 }
237
238 if wv == nil {
239 return false
240 }
241 return wv.GrabFocus()
242}
243
244// HasFocus returns whether the WebView currently has focus.
245func (pv *PaneView) HasFocus() bool {
246 pv.mu.RLock()
247 wv := pv.webViewWidget
248 pv.mu.RUnlock()
249
250 if wv == nil {
251 return false
252 }
253 return wv.HasFocus()
254}
255
256// SetOnFocusIn sets the callback for when the pane gains focus.
257func (pv *PaneView) SetOnFocusIn(fn func(paneID entity.PaneID)) {
258 pv.mu.Lock()
259 defer pv.mu.Unlock()
260
261 pv.onFocusIn = fn
262}
263
264// SetOnFocusOut sets the callback for when the pane loses focus.
265func (pv *PaneView) SetOnFocusOut(fn func(paneID entity.PaneID)) {
266 pv.mu.Lock()
267 defer pv.mu.Unlock()
268
269 pv.onFocusOut = fn
270}
271
272// SetOnHover sets the callback for when the pane is hovered.
273func (pv *PaneView) SetOnHover(fn func(paneID entity.PaneID)) {
274 pv.mu.Lock()
275 defer pv.mu.Unlock()
276
277 pv.onHover = fn
278}
279
280// SetOnMouseMotion sets a callback fired on intentional mouse movement within the pane.
281// Used to detect real user interaction vs synthetic GTK enter events from widget rearrangement.
282func (pv *PaneView) SetOnMouseMotion(fn func()) {
283 pv.mu.Lock()
284 defer pv.mu.Unlock()
285
286 pv.onMouseMotion = fn
287}
288
289// AttachHoverHandler creates and attaches a hover handler for focus-follows-mouse behavior.
290func (pv *PaneView) AttachHoverHandler(ctx context.Context) {
291 pv.mu.Lock()
292 defer pv.mu.Unlock()
293
294 // Create hover handler
295 pv.hoverHandler = input.NewHoverHandler(ctx, pv.paneID)
296
297 // Wire up hover enter callback
298 pv.hoverHandler.SetOnEnter(func(paneID entity.PaneID) {
299 pv.mu.RLock()
300 callback := pv.onHover
301 pv.mu.RUnlock()
302
303 if callback != nil {
304 callback(paneID)
305 }
306 })
307
308 // Wire up mouse motion callback (intentional movement detection)
309 pv.hoverHandler.SetOnMotion(func() {
310 pv.mu.RLock()
311 callback := pv.onMouseMotion
312 pv.mu.RUnlock()
313
314 if callback != nil {
315 callback()
316 }
317 })
318
319 // Attach to overlay widget
320 gtkWidget := pv.overlay.GtkWidget()
321 if gtkWidget != nil {
322 pv.hoverHandler.AttachTo(gtkWidget)
323 }
324}
325
326// CancelPendingHover cancels any pending hover focus timer.
327// Called when keyboard navigation occurs to prevent hover from overriding keyboard focus.
328func (pv *PaneView) CancelPendingHover() {
329 pv.mu.RLock()
330 handler := pv.hoverHandler
331 pv.mu.RUnlock()
332
333 if handler != nil {
334 handler.Cancel()
335 }
336}
337
338// Widget returns the underlying overlay widget for embedding in containers.
339func (pv *PaneView) Widget() layout.Widget {
340 return pv.overlay
341}
342
343// Overlay returns the underlying overlay widget for direct access.
344func (pv *PaneView) Overlay() layout.OverlayWidget {
345 return pv.overlay
346}
347
348// Show makes the pane visible.
349func (pv *PaneView) Show() {
350 pv.overlay.Show()
351}
352
353// Hide makes the pane invisible.
354func (pv *PaneView) Hide() {
355 pv.overlay.Hide()
356}
357
358// SetVisible sets the visibility of the pane.
359func (pv *PaneView) SetVisible(visible bool) {
360 pv.overlay.SetVisible(visible)
361}
362
363// IsVisible returns whether the pane is visible.
364func (pv *PaneView) IsVisible() bool {
365 return pv.overlay.IsVisible()
366}
367
368// AddCssClass adds a CSS class to the overlay.
369func (pv *PaneView) AddCssClass(class string) {
370 pv.overlay.AddCssClass(class)
371}
372
373// RemoveCssClass removes a CSS class from the overlay.
374func (pv *PaneView) RemoveCssClass(class string) {
375 pv.overlay.RemoveCssClass(class)
376}
377
378// AddOverlayWidget adds a widget as an overlay on this pane.
379// The widget will appear above the WebView and border.
380func (pv *PaneView) AddOverlayWidget(widget layout.Widget) {
381 pv.overlay.AddOverlay(widget)
382 pv.overlay.SetClipOverlay(widget, false)
383 pv.overlay.SetMeasureOverlay(widget, false)
384}
385
386// RemoveOverlayWidget removes an overlay widget from this pane.
387func (pv *PaneView) RemoveOverlayWidget(widget layout.Widget) {
388 pv.overlay.RemoveOverlay(widget)
389}
390
391// GetContentDimensions returns the pane's allocated width and height.
392func (pv *PaneView) GetContentDimensions() (width, height int) {
393 return pv.overlay.GetAllocatedWidth(), pv.overlay.GetAllocatedHeight()
394}
395
396// ensureProgressBar creates the progress bar lazily on first use.
397// This avoids GTK measurement issues with the internal progress gizmo
398// that can occur when the widget is added before the overlay is realized.
399// Must be called with write lock held.
400func (pv *PaneView) ensureProgressBar() *ProgressBar {
401 if pv.progressBar != nil {
402 return pv.progressBar
403 }
404
405 ctx := pv.ctx
406 if ctx == nil {
407 ctx = context.Background()
408 }
409 logging.FromContext(ctx).
410 Debug().
411 Str("pane_id", string(pv.paneID)).
412 Msg("creating progress bar")
413
414 pb := NewProgressBar(pv.ctx, pv.factory)
415 pv.overlay.AddOverlay(pb.Widget())
416 pv.overlay.SetClipOverlay(pb.Widget(), false)
417 pv.overlay.SetMeasureOverlay(pb.Widget(), false)
418 pv.progressBar = pb
419 return pb
420}
421
422// SetLoadProgress updates the progress bar with the current load progress.
423// progress should be between 0.0 and 1.0.
424func (pv *PaneView) SetLoadProgress(progress float64) {
425 pv.mu.Lock()
426 pb := pv.ensureProgressBar()
427 ctx := pv.ctx
428 if ctx == nil {
429 ctx = context.Background()
430 }
431 logging.FromContext(ctx).
432 Trace().
433 Str("pane_id", string(pv.paneID)).
434 Float64("progress", progress).
435 Msg("setting load progress")
436 pv.mu.Unlock()
437
438 pb.SetProgress(progress)
439}
440
441// SetLoading shows or hides the progress bar.
442func (pv *PaneView) SetLoading(loading bool) {
443 pv.mu.Lock()
444 hadProgressBar := pv.progressBar != nil
445 pb := pv.ensureProgressBar()
446 ctx := pv.ctx
447 if ctx == nil {
448 ctx = context.Background()
449 }
450 logging.FromContext(ctx).
451 Debug().
452 Str("pane_id", string(pv.paneID)).
453 Bool("loading", loading).
454 Bool("progress_bar_existed", hadProgressBar).
455 Msg("setting loading state")
456 pv.mu.Unlock()
457
458 if loading {
459 pb.Show()
460 } else {
461 pb.HideWithReason("loading_false")
462 }
463}
464
465// HideLoadingSkeleton hides the loading skeleton overlay (if present).
466func (pv *PaneView) HideLoadingSkeleton() {
467 pv.mu.Lock()
468 loading := pv.loading
469 pv.mu.Unlock()
470
471 if loading != nil {
472 loading.SetVisible(false)
473 }
474}
475
476// ensureToaster creates the toaster lazily on first use.
477// Must be called with write lock held.
478func (pv *PaneView) ensureToaster() *Toaster {
479 if pv.toaster != nil {
480 return pv.toaster
481 }
482
483 t := NewToaster(pv.factory)
484 pv.overlay.AddOverlay(t.Widget())
485 pv.overlay.SetClipOverlay(t.Widget(), false)
486 pv.overlay.SetMeasureOverlay(t.Widget(), false)
487 pv.toaster = t
488 return t
489}
490
491// ShowToast displays a toast notification with the given message and level.
492// If a toast is already visible, updates the text and resets the dismiss timer.
493func (pv *PaneView) ShowToast(ctx context.Context, message string, level ToastLevel) {
494 pv.mu.Lock()
495 t := pv.ensureToaster()
496 pv.mu.Unlock()
497
498 t.Show(ctx, message, level)
499}
500
501// ShowZoomToast displays a zoom level toast notification.
502// Formats the zoom percentage with a % suffix.
503func (pv *PaneView) ShowZoomToast(ctx context.Context, zoomPercent int) {
504 pv.mu.Lock()
505 t := pv.ensureToaster()
506 pv.mu.Unlock()
507
508 t.ShowZoom(ctx, zoomPercent)
509}
510
511// ensureLinkStatus creates the link status overlay lazily on first use.
512// Must be called with write lock held.
513func (pv *PaneView) ensureLinkStatus() *LinkStatusOverlay {
514 if pv.linkStatus != nil {
515 return pv.linkStatus
516 }
517
518 ls := NewLinkStatusOverlay(pv.factory)
519 pv.overlay.AddOverlay(ls.Widget())
520 pv.overlay.SetClipOverlay(ls.Widget(), false)
521 pv.overlay.SetMeasureOverlay(ls.Widget(), false)
522 pv.linkStatus = ls
523 return ls
524}
525
526// ShowLinkStatus displays the link status overlay with the given URI.
527// If uri is empty, hides the overlay instead.
528func (pv *PaneView) ShowLinkStatus(uri string) {
529 pv.mu.Lock()
530 ls := pv.ensureLinkStatus()
531 pv.mu.Unlock()
532
533 ls.Show(uri)
534}
535
536// HideLinkStatus hides the link status overlay.
537func (pv *PaneView) HideLinkStatus() {
538 pv.mu.Lock()
539 ls := pv.linkStatus
540 pv.mu.Unlock()
541
542 if ls != nil {
543 ls.Hide()
544 }
545}
546
547// SetVimMode tracks the pane owning Vim navigation; modeFrame owns the border.
548func (pv *PaneView) SetVimMode(active bool) {
549 pv.mu.Lock()
550 defer pv.mu.Unlock()
551
552 if pv.vimMode == active {
553 return
554 }
555
556 pv.vimMode = active
557}
558
559// IsVimMode returns whether Vim mode is currently active on this pane.
560func (pv *PaneView) IsVimMode() bool {
561 pv.mu.RLock()
562 defer pv.mu.RUnlock()
563
564 return pv.vimMode
565}
566
567// Cleanup removes the WebView widget from the overlay and clears references.
568// This must be called before destroying the WebView to ensure proper GTK cleanup.
569// After calling Cleanup, the PaneView should not be reused.
570//
571// An open JS dialog is answered with cancel ("Stay" for beforeunload). That is
572// an accepted trade-off: WorkspaceView.Rebuild (split/close/consume/reattach)
573// cleans up views whose WebView survives, so such a dialog is canceled rather
574// than carried over to the rebuilt view. Canceling is the safe default.
575func (pv *PaneView) Cleanup() {
576 pv.mu.Lock()
577 pv.cleanedUp = true
578 // The WebView may outlive this view (workspace rebuild), so an open JS
579 // dialog must be answered, not dropped. The hidden callback re-locks
580 // pv.mu, so detach it here and dismiss after unlocking.
581 popup, popupWidget := pv.jsDialog, pv.jsDialogWidget
582 if popup != nil {
583 popup.SetOnHidden(nil)
584 }
585 pv.jsDialog, pv.jsDialogWidget = nil, nil
586 pv.cleanupLocked()
587 pv.mu.Unlock()
588
589 if popup != nil {
590 popup.Dismiss(false)
591 popup.Destroy()
592 if popupWidget != nil {
593 pv.overlay.RemoveOverlay(popupWidget)
594 }
595 }
596}
597
598// cleanupLocked releases everything except the JS dialog. Caller holds pv.mu.
599func (pv *PaneView) cleanupLocked() {
600 // Clear callbacks to prevent use-after-free
601 pv.onFocusIn = nil
602 pv.onFocusOut = nil
603 pv.onHover = nil
604 pv.onMouseMotion = nil
605
606 // Detach hover handler if present
607 if pv.hoverHandler != nil {
608 pv.hoverHandler.Detach()
609 pv.hoverHandler = nil
610 }
611
612 // Remove WebView from overlay (unparents it from GTK hierarchy)
613 if pv.webViewWidget != nil {
614 pv.overlay.SetChild(nil)
615 pv.webViewWidget = nil
616 }
617
618 // Clear other overlay children
619 if pv.loading != nil {
620 pv.overlay.RemoveOverlay(pv.loading.Widget())
621 pv.loading = nil
622 }
623 if pv.progressBar != nil {
624 pv.overlay.RemoveOverlay(pv.progressBar.Widget())
625 pv.progressBar = nil
626 }
627 if pv.toaster != nil {
628 pv.overlay.RemoveOverlay(pv.toaster.Widget())
629 pv.toaster = nil
630 }
631 if pv.linkStatus != nil {
632 pv.linkStatus.Cleanup() // Cancel pending timers before removal
633 pv.overlay.RemoveOverlay(pv.linkStatus.Widget())
634 pv.linkStatus = nil
635 }
636}
637
638// jsDialogView is the JS dialog popup as seen by PaneView (a seam for tests).
639type jsDialogView interface {
640 Widget() *gtk.Widget
641 Show(req port.JSDialogRequest, respond func(ok bool, input string), focus bool, uiScale float64)
642 Hide()
643 Dismiss(ok bool)
644 RequestFocus()
645 Visible() bool
646 SetOnHidden(fn func(hadFocus bool))
647 Destroy()
648}
649
650func newJSDialogPopupView(parent layout.OverlayWidget) jsDialogView {
651 if p := NewJSDialogPopup(parent); p != nil {
652 return p
653 }
654 return nil
655}
656
657// ensureJSDialog creates the JS dialog popup lazily on first use.
658// Must be called with write lock held.
659func (pv *PaneView) ensureJSDialog() jsDialogView {
660 if pv.jsDialog != nil {
661 return pv.jsDialog
662 }
663 create := pv.newJSDialog
664 if create == nil {
665 create = newJSDialogPopupView
666 }
667 popup := create(pv.overlay)
668 if popup == nil {
669 return nil
670 }
671 w := pv.factory.WrapWidget(popup.Widget())
672 pv.overlay.AddOverlay(w)
673 // Clip so the scrim stays inside the pane; it must not affect layout.
674 pv.overlay.SetClipOverlay(w, true)
675 pv.overlay.SetMeasureOverlay(w, false)
676 popup.SetOnHidden(func(hadFocus bool) {
677 // Give focus back to the page only if the dialog had taken it.
678 if !hadFocus {
679 return
680 }
681 pv.mu.RLock()
682 wvw := pv.webViewWidget
683 pv.mu.RUnlock()
684 if wvw != nil {
685 wvw.GrabFocus()
686 }
687 })
688 pv.jsDialog = popup
689 pv.jsDialogWidget = w
690 return popup
691}
692
693// ShowJSDialog displays a JavaScript dialog scoped to this pane. The dialog
694// lives in the pane's overlay, so it never blocks other panes; if the pane is
695// in a background tab it becomes visible when that tab is shown, and it only
696// takes keyboard focus while the pane is active (otherwise when it becomes
697// active). respond is invoked once with the user's answer. Returns false if the
698// popup is unavailable.
699func (pv *PaneView) ShowJSDialog(req port.JSDialogRequest, respond func(ok bool, input string), uiScale float64) bool {
700 pv.mu.Lock()
701 if pv.cleanedUp {
702 pv.mu.Unlock()
703 return false
704 }
705 popup := pv.ensureJSDialog()
706 focus := pv.isActive
707 pv.mu.Unlock()
708 if popup == nil {
709 return false
710 }
711 popup.Show(req, respond, focus, uiScale)
712 return true
713}
714
715// HideJSDialog closes any JS dialog without answering it (engine-canceled).
716func (pv *PaneView) HideJSDialog() {
717 pv.mu.RLock()
718 popup := pv.jsDialog
719 pv.mu.RUnlock()
720 if popup != nil {
721 popup.Hide()
722 }
723}