feat(cef): implement Phase 1 engine features and fix shortcut propagation
- Zoom: SetZoomLevel/GetZoomLevel with linear-to-logarithmic conversion - Background color: JS injection at runtime + ARGB packed color at creation - HiDPI: detect scale from GDK primary monitor, implement GetScreenInfo - Middle-click: intercept button 2 on hovered links → OnLinkMiddleClick - Audio state: implement AudioHandler for IsPlayingAudio tracking - Fix keyboard shortcuts (Ctrl+L, Ctrl+F, etc.) by returning false for Ctrl/Alt modified key events so GTK's ShortcutController can process them
- commit
- b59184cca681be642af179dc5962eab63734db6d
- author
- brice · · 2026-03-20 22:38 UTC
- committer
- brice · · 2026-03-28 04:37 UTC
- parent
- a5e0073
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +201 -26
M internal/infrastructure/cef/engine.go +5 -2
| @@ -119,8 +119,11 @@ func (e *Engine) OnToolkitReady(_ context.Context) error { | |||
| 119 | 119 | return nil | |
| 120 | 120 | } | |
| 121 | 121 | ||
| 122 | - | // UpdateAppearance updates default background color for new WebViews (Phase 1 stub). | |
| 123 | - | func (e *Engine) UpdateAppearance(_ context.Context, _, _, _, _ float64) error { | |
| 122 | + | // UpdateAppearance updates the default background color for new WebViews. | |
| 123 | + | func (e *Engine) UpdateAppearance(_ context.Context, r, g, b, alpha float64) error { | |
| 124 | + | if e.factory != nil { | |
| 125 | + | e.factory.setDefaultBackgroundColor(r, g, b, alpha) | |
| 126 | + | } | |
| 124 | 127 | return nil | |
| 125 | 128 | } | |
| 126 | 129 | ||
| @@ -119,8 +119,11 @@ func (e *Engine) OnToolkitReady(_ context.Context) error { | |||||
| 119 | return nil | 119 | return nil | ||
| 120 | } | 120 | } | ||
| 121 | 121 | ||||
| 122 | - | // UpdateAppearance updates default background color for new WebViews (Phase 1 stub). | 122 | + | // UpdateAppearance updates the default background color for new WebViews. |
| 123 | - | func (e *Engine) UpdateAppearance(_ context.Context, _, _, _, _ float64) error { | 123 | + | func (e *Engine) UpdateAppearance(_ context.Context, r, g, b, alpha float64) error { |
| 124 | + | if e.factory != nil { | |||
| 125 | + | e.factory.setDefaultBackgroundColor(r, g, b, alpha) | |||
| 126 | + | } | |||
| 124 | return nil | 127 | return nil | ||
| 125 | } | 128 | } | ||
| 126 | 129 | ||||
M internal/infrastructure/cef/engine_init.go +18 -2
| @@ -10,6 +10,7 @@ import ( | |||
| 10 | 10 | "syscall" | |
| 11 | 11 | ||
| 12 | 12 | purecef "github.com/bnema/purego-cef/cef" | |
| 13 | + | "github.com/bnema/puregotk/v4/gdk" | |
| 13 | 14 | "github.com/rs/zerolog" | |
| 14 | 15 | ||
| 15 | 16 | "github.com/bnema/dumber/internal/infrastructure/config" | |
| @@ -102,8 +103,23 @@ func NewEngine(ctx context.Context, cfg config.CEFEngineConfig) (*Engine, error) | |||
| 102 | 103 | return nil, fmt.Errorf("GL loader: %w", err) | |
| 103 | 104 | } | |
| 104 | 105 | ||
| 105 | - | // 4. Create factory + pool and wire them into the engine. | |
| 106 | - | scale := int32(1) // TODO: detect from GDK | |
| 106 | + | // 4. Detect HiDPI scale from the primary monitor. | |
| 107 | + | scale := int32(1) | |
| 108 | + | if display := gdk.DisplayGetDefault(); display != nil { | |
| 109 | + | if monitors := display.GetMonitors(); monitors != nil { | |
| 110 | + | obj := monitors.GetObject(0) | |
| 111 | + | if obj != nil { | |
| 112 | + | mon := &gdk.Monitor{} | |
| 113 | + | mon.SetGoPointer(obj.GoPointer()) | |
| 114 | + | if s := mon.GetScaleFactor(); s > 0 { | |
| 115 | + | scale = int32(s) | |
| 116 | + | logger.Info().Int32("scale", scale).Msg("cef: detected HiDPI scale from monitor") | |
| 117 | + | } | |
| 118 | + | } | |
| 119 | + | } | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | // 5. Create factory + pool and wire them into the engine. | |
| 107 | 123 | factory := newWebViewFactory(eng, gl, scale) | |
| 108 | 124 | pool := newWebViewPool(factory) | |
| 109 | 125 | ||
| @@ -10,6 +10,7 @@ import ( | |||||
| 10 | "syscall" | 10 | "syscall" | ||
| 11 | 11 | ||||
| 12 | purecef "github.com/bnema/purego-cef/cef" | 12 | purecef "github.com/bnema/purego-cef/cef" | ||
| 13 | + | "github.com/bnema/puregotk/v4/gdk" | |||
| 13 | "github.com/rs/zerolog" | 14 | "github.com/rs/zerolog" | ||
| 14 | 15 | ||||
| 15 | "github.com/bnema/dumber/internal/infrastructure/config" | 16 | "github.com/bnema/dumber/internal/infrastructure/config" | ||
| @@ -102,8 +103,23 @@ func NewEngine(ctx context.Context, cfg config.CEFEngineConfig) (*Engine, error) | |||||
| 102 | return nil, fmt.Errorf("GL loader: %w", err) | 103 | return nil, fmt.Errorf("GL loader: %w", err) | ||
| 103 | } | 104 | } | ||
| 104 | 105 | ||||
| 105 | - | // 4. Create factory + pool and wire them into the engine. | 106 | + | // 4. Detect HiDPI scale from the primary monitor. |
| 106 | - | scale := int32(1) // TODO: detect from GDK | 107 | + | scale := int32(1) |
| 108 | + | if display := gdk.DisplayGetDefault(); display != nil { | |||
| 109 | + | if monitors := display.GetMonitors(); monitors != nil { | |||
| 110 | + | obj := monitors.GetObject(0) | |||
| 111 | + | if obj != nil { | |||
| 112 | + | mon := &gdk.Monitor{} | |||
| 113 | + | mon.SetGoPointer(obj.GoPointer()) | |||
| 114 | + | if s := mon.GetScaleFactor(); s > 0 { | |||
| 115 | + | scale = int32(s) | |||
| 116 | + | logger.Info().Int32("scale", scale).Msg("cef: detected HiDPI scale from monitor") | |||
| 117 | + | } | |||
| 118 | + | } | |||
| 119 | + | } | |||
| 120 | + | } | |||
| 121 | + | ||||
| 122 | + | // 5. Create factory + pool and wire them into the engine. | |||
| 107 | factory := newWebViewFactory(eng, gl, scale) | 123 | factory := newWebViewFactory(eng, gl, scale) | ||
| 108 | pool := newWebViewPool(factory) | 124 | pool := newWebViewPool(factory) | ||
| 109 | 125 | ||||
M internal/infrastructure/cef/factory.go +30 -4
| @@ -17,10 +17,11 @@ var _ port.WebViewFactory = (*WebViewFactory)(nil) | |||
| 17 | 17 | // gets a unique ID, its own renderPipeline and inputBridge, and an | |
| 18 | 18 | // asynchronously-created CEF browser (via BrowserHostCreateBrowser). | |
| 19 | 19 | type WebViewFactory struct { | |
| 20 | - | engine *Engine | |
| 21 | - | gl *glLoader | |
| 22 | - | nextID atomic.Uint64 | |
| 23 | - | scale int32 | |
| 20 | + | engine *Engine | |
| 21 | + | gl *glLoader | |
| 22 | + | nextID atomic.Uint64 | |
| 23 | + | scale int32 | |
| 24 | + | bgColor atomic.Uint32 // packed ARGB for BrowserSettings.BackgroundColor | |
| 24 | 25 | } | |
| 25 | 26 | ||
| 26 | 27 | // newWebViewFactory returns a factory that will create WebViews using the | |
| @@ -36,6 +37,13 @@ func newWebViewFactory(engine *Engine, gl *glLoader, scale int32) *WebViewFactor | |||
| 36 | 37 | } | |
| 37 | 38 | } | |
| 38 | 39 | ||
| 40 | + | // setDefaultBackgroundColor stores a packed ARGB color for new browser creation. | |
| 41 | + | func (f *WebViewFactory) setDefaultBackgroundColor(r, g, b, a float64) { | |
| 42 | + | const s = colorScale | |
| 43 | + | argb := uint32(a*s)<<24 | uint32(r*s)<<16 | uint32(g*s)<<8 | uint32(b*s) | |
| 44 | + | f.bgColor.Store(argb) | |
| 45 | + | } | |
| 46 | + | ||
| 39 | 47 | // Create builds a new CEF off-screen browser WebView. The browser is created | |
| 40 | 48 | // asynchronously; the returned WebView is usable immediately but navigation | |
| 41 | 49 | // will fail with errNoBrowser until OnAfterCreated fires. | |
| @@ -58,6 +66,21 @@ func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) { | |||
| 58 | 66 | input.attachTo(pipeline.glArea) | |
| 59 | 67 | wv.input = input | |
| 60 | 68 | ||
| 69 | + | // Wire middle-click → new tab using the cached hover URI. | |
| 70 | + | input.onMiddleClick = func(_ string) { | |
| 71 | + | wv.mu.RLock() | |
| 72 | + | hoverURI := wv.lastHoverURI | |
| 73 | + | cb := wv.callbacks | |
| 74 | + | wv.mu.RUnlock() | |
| 75 | + | if hoverURI == "" || cb == nil || cb.OnLinkMiddleClick == nil { | |
| 76 | + | return | |
| 77 | + | } | |
| 78 | + | uri := hoverURI | |
| 79 | + | wv.runOnGTK(func() { | |
| 80 | + | cb.OnLinkMiddleClick(uri) | |
| 81 | + | }) | |
| 82 | + | } | |
| 83 | + | ||
| 61 | 84 | // Build a CEF client backed by our handlerSet. | |
| 62 | 85 | // Store on WebView to prevent GC collection before CEF AddRef's it. | |
| 63 | 86 | client := purecef.NewClient(wv.handlers) | |
| @@ -70,6 +93,9 @@ func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) { | |||
| 70 | 93 | // Configure BrowserSettings. | |
| 71 | 94 | settings := purecef.DefaultBrowserSettings() | |
| 72 | 95 | settings.WindowlessFrameRate = 60 | |
| 96 | + | if bg := f.bgColor.Load(); bg != 0 { | |
| 97 | + | settings.BackgroundColor = bg | |
| 98 | + | } | |
| 73 | 99 | ||
| 74 | 100 | // Defer browser creation until the GL area has a non-zero size. | |
| 75 | 101 | // CEF requires GetViewRect to return a non-empty rect, but the GL area | |
| @@ -17,10 +17,11 @@ var _ port.WebViewFactory = (*WebViewFactory)(nil) | |||||
| 17 | // gets a unique ID, its own renderPipeline and inputBridge, and an | 17 | // gets a unique ID, its own renderPipeline and inputBridge, and an | ||
| 18 | // asynchronously-created CEF browser (via BrowserHostCreateBrowser). | 18 | // asynchronously-created CEF browser (via BrowserHostCreateBrowser). | ||
| 19 | type WebViewFactory struct { | 19 | type WebViewFactory struct { | ||
| 20 | - | engine *Engine | 20 | + | engine *Engine |
| 21 | - | gl *glLoader | 21 | + | gl *glLoader |
| 22 | - | nextID atomic.Uint64 | 22 | + | nextID atomic.Uint64 |
| 23 | - | scale int32 | 23 | + | scale int32 |
| 24 | + | bgColor atomic.Uint32 // packed ARGB for BrowserSettings.BackgroundColor | |||
| 24 | } | 25 | } | ||
| 25 | 26 | ||||
| 26 | // newWebViewFactory returns a factory that will create WebViews using the | 27 | // newWebViewFactory returns a factory that will create WebViews using the | ||
| @@ -36,6 +37,13 @@ func newWebViewFactory(engine *Engine, gl *glLoader, scale int32) *WebViewFactor | |||||
| 36 | } | 37 | } | ||
| 37 | } | 38 | } | ||
| 38 | 39 | ||||
| 40 | + | // setDefaultBackgroundColor stores a packed ARGB color for new browser creation. | |||
| 41 | + | func (f *WebViewFactory) setDefaultBackgroundColor(r, g, b, a float64) { | |||
| 42 | + | const s = colorScale | |||
| 43 | + | argb := uint32(a*s)<<24 | uint32(r*s)<<16 | uint32(g*s)<<8 | uint32(b*s) | |||
| 44 | + | f.bgColor.Store(argb) | |||
| 45 | + | } | |||
| 46 | + | ||||
| 39 | // Create builds a new CEF off-screen browser WebView. The browser is created | 47 | // Create builds a new CEF off-screen browser WebView. The browser is created | ||
| 40 | // asynchronously; the returned WebView is usable immediately but navigation | 48 | // asynchronously; the returned WebView is usable immediately but navigation | ||
| 41 | // will fail with errNoBrowser until OnAfterCreated fires. | 49 | // will fail with errNoBrowser until OnAfterCreated fires. | ||
| @@ -58,6 +66,21 @@ func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) { | |||||
| 58 | input.attachTo(pipeline.glArea) | 66 | input.attachTo(pipeline.glArea) | ||
| 59 | wv.input = input | 67 | wv.input = input | ||
| 60 | 68 | ||||
| 69 | + | // Wire middle-click → new tab using the cached hover URI. | |||
| 70 | + | input.onMiddleClick = func(_ string) { | |||
| 71 | + | wv.mu.RLock() | |||
| 72 | + | hoverURI := wv.lastHoverURI | |||
| 73 | + | cb := wv.callbacks | |||
| 74 | + | wv.mu.RUnlock() | |||
| 75 | + | if hoverURI == "" || cb == nil || cb.OnLinkMiddleClick == nil { | |||
| 76 | + | return | |||
| 77 | + | } | |||
| 78 | + | uri := hoverURI | |||
| 79 | + | wv.runOnGTK(func() { | |||
| 80 | + | cb.OnLinkMiddleClick(uri) | |||
| 81 | + | }) | |||
| 82 | + | } | |||
| 83 | + | ||||
| 61 | // Build a CEF client backed by our handlerSet. | 84 | // Build a CEF client backed by our handlerSet. | ||
| 62 | // Store on WebView to prevent GC collection before CEF AddRef's it. | 85 | // Store on WebView to prevent GC collection before CEF AddRef's it. | ||
| 63 | client := purecef.NewClient(wv.handlers) | 86 | client := purecef.NewClient(wv.handlers) | ||
| @@ -70,6 +93,9 @@ func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) { | |||||
| 70 | // Configure BrowserSettings. | 93 | // Configure BrowserSettings. | ||
| 71 | settings := purecef.DefaultBrowserSettings() | 94 | settings := purecef.DefaultBrowserSettings() | ||
| 72 | settings.WindowlessFrameRate = 60 | 95 | settings.WindowlessFrameRate = 60 | ||
| 96 | + | if bg := f.bgColor.Load(); bg != 0 { | |||
| 97 | + | settings.BackgroundColor = bg | |||
| 98 | + | } | |||
| 73 | 99 | ||||
| 74 | // Defer browser creation until the GL area has a non-zero size. | 100 | // Defer browser creation until the GL area has a non-zero size. | ||
| 75 | // CEF requires GetViewRect to return a non-empty rect, but the GL area | 101 | // CEF requires GetViewRect to return a non-empty rect, but the GL area | ||
M internal/infrastructure/cef/input_bridge.go +21 -1
| @@ -51,6 +51,10 @@ type inputBridge struct { | |||
| 51 | 51 | imContext *gtk.IMContextSimple | |
| 52 | 52 | // Prevent GC from collecting signal callbacks. | |
| 53 | 53 | commitCb func(gtk.IMContext, string) | |
| 54 | + | ||
| 55 | + | // onMiddleClick is called when button 2 (middle) is pressed on a link. | |
| 56 | + | // The callback receives the hovered URI. Set by the factory. | |
| 57 | + | onMiddleClick func(uri string) | |
| 54 | 58 | } | |
| 55 | 59 | ||
| 56 | 60 | // newInputBridge creates an input bridge with the given HiDPI scale factor. | |
| @@ -154,7 +158,16 @@ func (ib *inputBridge) attachFocusAndKeyboard(glArea *gtk.GLArea) { | |||
| 154 | 158 | } | |
| 155 | 159 | ||
| 156 | 160 | keyPressCb := func(_ gtk.EventControllerKey, keyval, keycode uint, state gdk.ModifierType) bool { | |
| 157 | - | ib.onKeyPress(keyval, keycode, uint(state)) | |
| 161 | + | mods := uint(state) | |
| 162 | + | ib.onKeyPress(keyval, keycode, mods) | |
| 163 | + | ||
| 164 | + | // Let Ctrl/Alt modified key combos propagate to the window's | |
| 165 | + | // ShortcutController so that app shortcuts (Ctrl+L, Ctrl+F, …) | |
| 166 | + | // still fire. Only consume unmodified / Shift-only keys that | |
| 167 | + | // are text input destined for CEF. | |
| 168 | + | if mods&uint(gdk.ControlMaskValue) != 0 || mods&uint(gdk.AltMaskValue) != 0 { | |
| 169 | + | return false | |
| 170 | + | } | |
| 158 | 171 | return true | |
| 159 | 172 | } | |
| 160 | 173 | key.ConnectKeyPressed(&keyPressCb) | |
| @@ -198,11 +211,18 @@ func (ib *inputBridge) onMouseMove(x, y float64, mods uint, leave bool) { | |||
| 198 | 211 | func (ib *inputBridge) onMousePress(x, y float64, button, mods uint, clickCount int) { | |
| 199 | 212 | ib.mu.Lock() | |
| 200 | 213 | host := ib.host | |
| 214 | + | middleCB := ib.onMiddleClick | |
| 201 | 215 | ib.mu.Unlock() | |
| 202 | 216 | if host == nil { | |
| 203 | 217 | return | |
| 204 | 218 | } | |
| 205 | 219 | ||
| 220 | + | // Middle-click on a link opens in a new tab instead of sending to CEF. | |
| 221 | + | if button == 2 && middleCB != nil { | |
| 222 | + | middleCB("") | |
| 223 | + | return | |
| 224 | + | } | |
| 225 | + | ||
| 206 | 226 | evt := buildMouseEvent(x, y, mods, ib.scale) | |
| 207 | 227 | btn := translateMouseButton(button) | |
| 208 | 228 | host.SendMouseClickEvent(&evt, btn, 0, int32(clickCount)) | |
| @@ -51,6 +51,10 @@ type inputBridge struct { | |||||
| 51 | imContext *gtk.IMContextSimple | 51 | imContext *gtk.IMContextSimple | ||
| 52 | // Prevent GC from collecting signal callbacks. | 52 | // Prevent GC from collecting signal callbacks. | ||
| 53 | commitCb func(gtk.IMContext, string) | 53 | commitCb func(gtk.IMContext, string) | ||
| 54 | + | ||||
| 55 | + | // onMiddleClick is called when button 2 (middle) is pressed on a link. | |||
| 56 | + | // The callback receives the hovered URI. Set by the factory. | |||
| 57 | + | onMiddleClick func(uri string) | |||
| 54 | } | 58 | } | ||
| 55 | 59 | ||||
| 56 | // newInputBridge creates an input bridge with the given HiDPI scale factor. | 60 | // newInputBridge creates an input bridge with the given HiDPI scale factor. | ||
| @@ -154,7 +158,16 @@ func (ib *inputBridge) attachFocusAndKeyboard(glArea *gtk.GLArea) { | |||||
| 154 | } | 158 | } | ||
| 155 | 159 | ||||
| 156 | keyPressCb := func(_ gtk.EventControllerKey, keyval, keycode uint, state gdk.ModifierType) bool { | 160 | keyPressCb := func(_ gtk.EventControllerKey, keyval, keycode uint, state gdk.ModifierType) bool { | ||
| 157 | - | ib.onKeyPress(keyval, keycode, uint(state)) | 161 | + | mods := uint(state) |
| 162 | + | ib.onKeyPress(keyval, keycode, mods) | |||
| 163 | + | ||||
| 164 | + | // Let Ctrl/Alt modified key combos propagate to the window's | |||
| 165 | + | // ShortcutController so that app shortcuts (Ctrl+L, Ctrl+F, …) | |||
| 166 | + | // still fire. Only consume unmodified / Shift-only keys that | |||
| 167 | + | // are text input destined for CEF. | |||
| 168 | + | if mods&uint(gdk.ControlMaskValue) != 0 || mods&uint(gdk.AltMaskValue) != 0 { | |||
| 169 | + | return false | |||
| 170 | + | } | |||
| 158 | return true | 171 | return true | ||
| 159 | } | 172 | } | ||
| 160 | key.ConnectKeyPressed(&keyPressCb) | 173 | key.ConnectKeyPressed(&keyPressCb) | ||
| @@ -198,11 +211,18 @@ func (ib *inputBridge) onMouseMove(x, y float64, mods uint, leave bool) { | |||||
| 198 | func (ib *inputBridge) onMousePress(x, y float64, button, mods uint, clickCount int) { | 211 | func (ib *inputBridge) onMousePress(x, y float64, button, mods uint, clickCount int) { | ||
| 199 | ib.mu.Lock() | 212 | ib.mu.Lock() | ||
| 200 | host := ib.host | 213 | host := ib.host | ||
| 214 | + | middleCB := ib.onMiddleClick | |||
| 201 | ib.mu.Unlock() | 215 | ib.mu.Unlock() | ||
| 202 | if host == nil { | 216 | if host == nil { | ||
| 203 | return | 217 | return | ||
| 204 | } | 218 | } | ||
| 205 | 219 | ||||
| 220 | + | // Middle-click on a link opens in a new tab instead of sending to CEF. | |||
| 221 | + | if button == 2 && middleCB != nil { | |||
| 222 | + | middleCB("") | |||
| 223 | + | return | |||
| 224 | + | } | |||
| 225 | + | ||||
| 206 | evt := buildMouseEvent(x, y, mods, ib.scale) | 226 | evt := buildMouseEvent(x, y, mods, ib.scale) | ||
| 207 | btn := translateMouseButton(button) | 227 | btn := translateMouseButton(button) | ||
| 208 | host.SendMouseClickEvent(&evt, btn, 0, int32(clickCount)) | 228 | host.SendMouseClickEvent(&evt, btn, 0, int32(clickCount)) | ||
M internal/infrastructure/cef/webview.go +78 -14
| @@ -4,6 +4,8 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "encoding/base64" | |
| 6 | 6 | "errors" | |
| 7 | + | "fmt" | |
| 8 | + | "math" | |
| 7 | 9 | "sync" | |
| 8 | 10 | "sync/atomic" | |
| 9 | 11 | "unsafe" | |
| @@ -61,10 +63,15 @@ type WebView struct { | |||
| 61 | 63 | canGoFwd bool | |
| 62 | 64 | isLoading bool | |
| 63 | 65 | ||
| 66 | + | // Last known hover URI for middle-click → new tab. | |
| 67 | + | lastHoverURI string | |
| 68 | + | ||
| 64 | 69 | // Atomic state. | |
| 65 | - | destroyed atomic.Bool | |
| 66 | - | fullscreen atomic.Bool | |
| 67 | - | generation atomic.Uint64 | |
| 70 | + | destroyed atomic.Bool | |
| 71 | + | fullscreen atomic.Bool | |
| 72 | + | generation atomic.Uint64 | |
| 73 | + | audioPlaying atomic.Bool | |
| 74 | + | zoomFactor atomic.Value // float64, initialized to 1.0 | |
| 68 | 75 | } | |
| 69 | 76 | ||
| 70 | 77 | // pendingBrowserCreate holds the parameters needed to create a CEF browser, | |
| @@ -254,7 +261,7 @@ func (wv *WebView) State() port.WebViewState { | |||
| 254 | 261 | Progress: wv.progress, | |
| 255 | 262 | CanGoBack: wv.canGoBack, | |
| 256 | 263 | CanGoFwd: wv.canGoFwd, | |
| 257 | - | ZoomLevel: 1.0, | |
| 264 | + | ZoomLevel: wv.GetZoomLevel(), | |
| 258 | 265 | } | |
| 259 | 266 | } | |
| 260 | 267 | ||
| @@ -263,9 +270,9 @@ func (wv *WebView) IsFullscreen() bool { | |||
| 263 | 270 | return wv.fullscreen.Load() | |
| 264 | 271 | } | |
| 265 | 272 | ||
| 266 | - | // IsPlayingAudio returns false (Phase 1 stub). | |
| 273 | + | // IsPlayingAudio returns true if any audio stream is active. | |
| 267 | 274 | func (wv *WebView) IsPlayingAudio() bool { | |
| 268 | - | return false | |
| 275 | + | return wv.audioPlaying.Load() | |
| 269 | 276 | } | |
| 270 | 277 | ||
| 271 | 278 | // Generation returns a monotonic counter incremented on pool reuse. | |
| @@ -279,16 +286,43 @@ func (wv *WebView) Favicon() port.Texture { | |||
| 279 | 286 | } | |
| 280 | 287 | ||
| 281 | 288 | // --------------------------------------------------------------------------- | |
| 282 | - | // Zoom (Phase 1 stubs) | |
| 289 | + | // Zoom | |
| 283 | 290 | // --------------------------------------------------------------------------- | |
| 284 | 291 | ||
| 285 | - | // GetZoomLevel returns 1.0 (Phase 1 stub). | |
| 292 | + | // chromiumZoomBase is the base used by Chromium's logarithmic zoom level. | |
| 293 | + | // CEF zoom level = log(factor) / log(1.2), where factor is the linear multiplier. | |
| 294 | + | const chromiumZoomBase = 1.2 | |
| 295 | + | ||
| 296 | + | // cefZoomFromFactor converts a linear zoom factor (1.0 = 100%) to CEF's | |
| 297 | + | // logarithmic zoom level. | |
| 298 | + | func cefZoomFromFactor(factor float64) float64 { | |
| 299 | + | if factor <= 0 { | |
| 300 | + | return 0 | |
| 301 | + | } | |
| 302 | + | return math.Log(factor) / math.Log(chromiumZoomBase) | |
| 303 | + | } | |
| 304 | + | ||
| 305 | + | // GetZoomLevel returns the current linear zoom factor. | |
| 286 | 306 | func (wv *WebView) GetZoomLevel() float64 { | |
| 307 | + | if v := wv.zoomFactor.Load(); v != nil { | |
| 308 | + | return v.(float64) | |
| 309 | + | } | |
| 287 | 310 | return 1.0 | |
| 288 | 311 | } | |
| 289 | 312 | ||
| 290 | - | // SetZoomLevel is a no-op (Phase 1 stub). | |
| 291 | - | func (wv *WebView) SetZoomLevel(_ context.Context, _ float64) error { | |
| 313 | + | // SetZoomLevel sets the zoom level using a linear factor (1.0 = 100%). | |
| 314 | + | func (wv *WebView) SetZoomLevel(_ context.Context, factor float64) error { | |
| 315 | + | if wv.destroyed.Load() { | |
| 316 | + | return errDestroyed | |
| 317 | + | } | |
| 318 | + | wv.mu.RLock() | |
| 319 | + | host := wv.host | |
| 320 | + | wv.mu.RUnlock() | |
| 321 | + | if host == nil { | |
| 322 | + | return errNoBrowser | |
| 323 | + | } | |
| 324 | + | host.SetZoomLevel(cefZoomFromFactor(factor)) | |
| 325 | + | wv.zoomFactor.Store(factor) | |
| 292 | 326 | return nil | |
| 293 | 327 | } | |
| 294 | 328 | ||
| @@ -330,11 +364,23 @@ func (wv *WebView) RunJavaScript(_ context.Context, script string) { | |||
| 330 | 364 | browser.GetMainFrame().ExecuteJavaScript(script, "", 0) | |
| 331 | 365 | } | |
| 332 | 366 | ||
| 333 | - | // SetBackgroundColor is a no-op in Phase 1. | |
| 334 | - | func (wv *WebView) SetBackgroundColor(_, _, _, _ float64) {} | |
| 367 | + | // colorScale converts a 0.0–1.0 color component to an 8-bit integer. | |
| 368 | + | const colorScale = 255 | |
| 335 | 369 | ||
| 336 | - | // ResetBackgroundToDefault is a no-op in Phase 1. | |
| 337 | - | func (wv *WebView) ResetBackgroundToDefault() {} | |
| 370 | + | // SetBackgroundColor sets the background via JS injection (CEF has no runtime API). | |
| 371 | + | func (wv *WebView) SetBackgroundColor(r, g, b, a float64) { | |
| 372 | + | script := fmt.Sprintf( | |
| 373 | + | `document.documentElement.style.backgroundColor='rgba(%d,%d,%d,%.2f)'`, | |
| 374 | + | int(r*colorScale), int(g*colorScale), int(b*colorScale), a, | |
| 375 | + | ) | |
| 376 | + | wv.RunJavaScript(context.Background(), script) | |
| 377 | + | } | |
| 378 | + | ||
| 379 | + | // ResetBackgroundToDefault clears the injected background color. | |
| 380 | + | func (wv *WebView) ResetBackgroundToDefault() { | |
| 381 | + | wv.RunJavaScript(context.Background(), | |
| 382 | + | `document.documentElement.style.backgroundColor=''`) | |
| 383 | + | } | |
| 338 | 384 | ||
| 339 | 385 | // --------------------------------------------------------------------------- | |
| 340 | 386 | // Lifecycle | |
| @@ -424,6 +470,24 @@ func (wv *WebView) updateLoadState(loading, back, fwd bool) { | |||
| 424 | 470 | wv.mu.Unlock() | |
| 425 | 471 | } | |
| 426 | 472 | ||
| 473 | + | func (wv *WebView) setAudioPlaying(playing bool) { | |
| 474 | + | wv.audioPlaying.Store(playing) | |
| 475 | + | wv.mu.RLock() | |
| 476 | + | cb := wv.callbacks | |
| 477 | + | wv.mu.RUnlock() | |
| 478 | + | if cb != nil && cb.OnAudioStateChanged != nil { | |
| 479 | + | wv.runOnGTK(func() { | |
| 480 | + | cb.OnAudioStateChanged(playing) | |
| 481 | + | }) | |
| 482 | + | } | |
| 483 | + | } | |
| 484 | + | ||
| 485 | + | func (wv *WebView) updateHoverURI(uri string) { | |
| 486 | + | wv.mu.Lock() | |
| 487 | + | wv.lastHoverURI = uri | |
| 488 | + | wv.mu.Unlock() | |
| 489 | + | } | |
| 490 | + | ||
| 427 | 491 | func (wv *WebView) runOnGTK(fn func()) { | |
| 428 | 492 | if fn == nil { | |
| 429 | 493 | return | |
| @@ -4,6 +4,8 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "encoding/base64" | 5 | "encoding/base64" | ||
| 6 | "errors" | 6 | "errors" | ||
| 7 | + | "fmt" | |||
| 8 | + | "math" | |||
| 7 | "sync" | 9 | "sync" | ||
| 8 | "sync/atomic" | 10 | "sync/atomic" | ||
| 9 | "unsafe" | 11 | "unsafe" | ||
| @@ -61,10 +63,15 @@ type WebView struct { | |||||
| 61 | canGoFwd bool | 63 | canGoFwd bool | ||
| 62 | isLoading bool | 64 | isLoading bool | ||
| 63 | 65 | ||||
| 66 | + | // Last known hover URI for middle-click → new tab. | |||
| 67 | + | lastHoverURI string | |||
| 68 | + | ||||
| 64 | // Atomic state. | 69 | // Atomic state. | ||
| 65 | - | destroyed atomic.Bool | 70 | + | destroyed atomic.Bool |
| 66 | - | fullscreen atomic.Bool | 71 | + | fullscreen atomic.Bool |
| 67 | - | generation atomic.Uint64 | 72 | + | generation atomic.Uint64 |
| 73 | + | audioPlaying atomic.Bool | |||
| 74 | + | zoomFactor atomic.Value // float64, initialized to 1.0 | |||
| 68 | } | 75 | } | ||
| 69 | 76 | ||||
| 70 | // pendingBrowserCreate holds the parameters needed to create a CEF browser, | 77 | // pendingBrowserCreate holds the parameters needed to create a CEF browser, | ||
| @@ -254,7 +261,7 @@ func (wv *WebView) State() port.WebViewState { | |||||
| 254 | Progress: wv.progress, | 261 | Progress: wv.progress, | ||
| 255 | CanGoBack: wv.canGoBack, | 262 | CanGoBack: wv.canGoBack, | ||
| 256 | CanGoFwd: wv.canGoFwd, | 263 | CanGoFwd: wv.canGoFwd, | ||
| 257 | - | ZoomLevel: 1.0, | 264 | + | ZoomLevel: wv.GetZoomLevel(), |
| 258 | } | 265 | } | ||
| 259 | } | 266 | } | ||
| 260 | 267 | ||||
| @@ -263,9 +270,9 @@ func (wv *WebView) IsFullscreen() bool { | |||||
| 263 | return wv.fullscreen.Load() | 270 | return wv.fullscreen.Load() | ||
| 264 | } | 271 | } | ||
| 265 | 272 | ||||
| 266 | - | // IsPlayingAudio returns false (Phase 1 stub). | 273 | + | // IsPlayingAudio returns true if any audio stream is active. |
| 267 | func (wv *WebView) IsPlayingAudio() bool { | 274 | func (wv *WebView) IsPlayingAudio() bool { | ||
| 268 | - | return false | 275 | + | return wv.audioPlaying.Load() |
| 269 | } | 276 | } | ||
| 270 | 277 | ||||
| 271 | // Generation returns a monotonic counter incremented on pool reuse. | 278 | // Generation returns a monotonic counter incremented on pool reuse. | ||
| @@ -279,16 +286,43 @@ func (wv *WebView) Favicon() port.Texture { | |||||
| 279 | } | 286 | } | ||
| 280 | 287 | ||||
| 281 | // --------------------------------------------------------------------------- | 288 | // --------------------------------------------------------------------------- | ||
| 282 | - | // Zoom (Phase 1 stubs) | 289 | + | // Zoom |
| 283 | // --------------------------------------------------------------------------- | 290 | // --------------------------------------------------------------------------- | ||
| 284 | 291 | ||||
| 285 | - | // GetZoomLevel returns 1.0 (Phase 1 stub). | 292 | + | // chromiumZoomBase is the base used by Chromium's logarithmic zoom level. |
| 293 | + | // CEF zoom level = log(factor) / log(1.2), where factor is the linear multiplier. | |||
| 294 | + | const chromiumZoomBase = 1.2 | |||
| 295 | + | ||||
| 296 | + | // cefZoomFromFactor converts a linear zoom factor (1.0 = 100%) to CEF's | |||
| 297 | + | // logarithmic zoom level. | |||
| 298 | + | func cefZoomFromFactor(factor float64) float64 { | |||
| 299 | + | if factor <= 0 { | |||
| 300 | + | return 0 | |||
| 301 | + | } | |||
| 302 | + | return math.Log(factor) / math.Log(chromiumZoomBase) | |||
| 303 | + | } | |||
| 304 | + | ||||
| 305 | + | // GetZoomLevel returns the current linear zoom factor. | |||
| 286 | func (wv *WebView) GetZoomLevel() float64 { | 306 | func (wv *WebView) GetZoomLevel() float64 { | ||
| 307 | + | if v := wv.zoomFactor.Load(); v != nil { | |||
| 308 | + | return v.(float64) | |||
| 309 | + | } | |||
| 287 | return 1.0 | 310 | return 1.0 | ||
| 288 | } | 311 | } | ||
| 289 | 312 | ||||
| 290 | - | // SetZoomLevel is a no-op (Phase 1 stub). | 313 | + | // SetZoomLevel sets the zoom level using a linear factor (1.0 = 100%). |
| 291 | - | func (wv *WebView) SetZoomLevel(_ context.Context, _ float64) error { | 314 | + | func (wv *WebView) SetZoomLevel(_ context.Context, factor float64) error { |
| 315 | + | if wv.destroyed.Load() { | |||
| 316 | + | return errDestroyed | |||
| 317 | + | } | |||
| 318 | + | wv.mu.RLock() | |||
| 319 | + | host := wv.host | |||
| 320 | + | wv.mu.RUnlock() | |||
| 321 | + | if host == nil { | |||
| 322 | + | return errNoBrowser | |||
| 323 | + | } | |||
| 324 | + | host.SetZoomLevel(cefZoomFromFactor(factor)) | |||
| 325 | + | wv.zoomFactor.Store(factor) | |||
| 292 | return nil | 326 | return nil | ||
| 293 | } | 327 | } | ||
| 294 | 328 | ||||
| @@ -330,11 +364,23 @@ func (wv *WebView) RunJavaScript(_ context.Context, script string) { | |||||
| 330 | browser.GetMainFrame().ExecuteJavaScript(script, "", 0) | 364 | browser.GetMainFrame().ExecuteJavaScript(script, "", 0) | ||
| 331 | } | 365 | } | ||
| 332 | 366 | ||||
| 333 | - | // SetBackgroundColor is a no-op in Phase 1. | 367 | + | // colorScale converts a 0.0–1.0 color component to an 8-bit integer. |
| 334 | - | func (wv *WebView) SetBackgroundColor(_, _, _, _ float64) {} | 368 | + | const colorScale = 255 |
| 335 | 369 | ||||
| 336 | - | // ResetBackgroundToDefault is a no-op in Phase 1. | 370 | + | // SetBackgroundColor sets the background via JS injection (CEF has no runtime API). |
| 337 | - | func (wv *WebView) ResetBackgroundToDefault() {} | 371 | + | func (wv *WebView) SetBackgroundColor(r, g, b, a float64) { |
| 372 | + | script := fmt.Sprintf( | |||
| 373 | + | `document.documentElement.style.backgroundColor='rgba(%d,%d,%d,%.2f)'`, | |||
| 374 | + | int(r*colorScale), int(g*colorScale), int(b*colorScale), a, | |||
| 375 | + | ) | |||
| 376 | + | wv.RunJavaScript(context.Background(), script) | |||
| 377 | + | } | |||
| 378 | + | ||||
| 379 | + | // ResetBackgroundToDefault clears the injected background color. | |||
| 380 | + | func (wv *WebView) ResetBackgroundToDefault() { | |||
| 381 | + | wv.RunJavaScript(context.Background(), | |||
| 382 | + | `document.documentElement.style.backgroundColor=''`) | |||
| 383 | + | } | |||
| 338 | 384 | ||||
| 339 | // --------------------------------------------------------------------------- | 385 | // --------------------------------------------------------------------------- | ||
| 340 | // Lifecycle | 386 | // Lifecycle | ||
| @@ -424,6 +470,24 @@ func (wv *WebView) updateLoadState(loading, back, fwd bool) { | |||||
| 424 | wv.mu.Unlock() | 470 | wv.mu.Unlock() | ||
| 425 | } | 471 | } | ||
| 426 | 472 | ||||
| 473 | + | func (wv *WebView) setAudioPlaying(playing bool) { | |||
| 474 | + | wv.audioPlaying.Store(playing) | |||
| 475 | + | wv.mu.RLock() | |||
| 476 | + | cb := wv.callbacks | |||
| 477 | + | wv.mu.RUnlock() | |||
| 478 | + | if cb != nil && cb.OnAudioStateChanged != nil { | |||
| 479 | + | wv.runOnGTK(func() { | |||
| 480 | + | cb.OnAudioStateChanged(playing) | |||
| 481 | + | }) | |||
| 482 | + | } | |||
| 483 | + | } | |||
| 484 | + | ||||
| 485 | + | func (wv *WebView) updateHoverURI(uri string) { | |||
| 486 | + | wv.mu.Lock() | |||
| 487 | + | wv.lastHoverURI = uri | |||
| 488 | + | wv.mu.Unlock() | |||
| 489 | + | } | |||
| 490 | + | ||||
| 427 | func (wv *WebView) runOnGTK(fn func()) { | 491 | func (wv *WebView) runOnGTK(fn func()) { | ||
| 428 | if fn == nil { | 492 | if fn == nil { | ||
| 429 | return | 493 | return | ||
M internal/infrastructure/cef/webview_handlers.go +49 -3
| @@ -24,13 +24,14 @@ var ( | |||
| 24 | 24 | _ purecef.LoadHandler = (*handlerSet)(nil) | |
| 25 | 25 | _ purecef.LifeSpanHandler = (*handlerSet)(nil) | |
| 26 | 26 | _ purecef.RequestHandler = (*handlerSet)(nil) | |
| 27 | + | _ purecef.AudioHandler = (*handlerSet)(nil) | |
| 27 | 28 | ) | |
| 28 | 29 | ||
| 29 | 30 | // =========================================================================== | |
| 30 | 31 | // Client | |
| 31 | 32 | // =========================================================================== | |
| 32 | 33 | ||
| 33 | - | func (h *handlerSet) GetAudioHandler() purecef.AudioHandler { return nil } | |
| 34 | + | func (h *handlerSet) GetAudioHandler() purecef.AudioHandler { return h } | |
| 34 | 35 | func (h *handlerSet) GetCommandHandler() purecef.CommandHandler { return nil } | |
| 35 | 36 | func (h *handlerSet) GetContextMenuHandler() purecef.ContextMenuHandler { return nil } | |
| 36 | 37 | func (h *handlerSet) GetDialogHandler() purecef.DialogHandler { return nil } | |
| @@ -92,7 +93,27 @@ func (h *handlerSet) GetScreenPoint(_ purecef.Browser, _, _ int32, _, _ unsafe.P | |||
| 92 | 93 | return 0 | |
| 93 | 94 | } | |
| 94 | 95 | ||
| 95 | - | func (h *handlerSet) GetScreenInfo(_ purecef.Browser, _ *purecef.ScreenInfo) int32 { return 0 } | |
| 96 | + | func (h *handlerSet) GetScreenInfo(_ purecef.Browser, info *purecef.ScreenInfo) int32 { | |
| 97 | + | if info == nil { | |
| 98 | + | return 0 | |
| 99 | + | } | |
| 100 | + | h.wv.pipeline.mu.Lock() | |
| 101 | + | w := h.wv.pipeline.width | |
| 102 | + | ht := h.wv.pipeline.height | |
| 103 | + | s := h.wv.pipeline.scale | |
| 104 | + | h.wv.pipeline.mu.Unlock() | |
| 105 | + | ||
| 106 | + | info.Size = unsafe.Sizeof(*info) | |
| 107 | + | info.DeviceScaleFactor = float32(s) | |
| 108 | + | info.Depth = 24 | |
| 109 | + | info.DepthPerComponent = 8 | |
| 110 | + | info.Rect.X = 0 | |
| 111 | + | info.Rect.Y = 0 | |
| 112 | + | info.Rect.Width = w | |
| 113 | + | info.Rect.Height = ht | |
| 114 | + | info.AvailableRect = info.Rect | |
| 115 | + | return 1 | |
| 116 | + | } | |
| 96 | 117 | ||
| 97 | 118 | func (h *handlerSet) OnPopupShow(_ purecef.Browser, _ int32) {} | |
| 98 | 119 | ||
| @@ -199,8 +220,11 @@ func (h *handlerSet) OnFullscreenModeChange(_ purecef.Browser, fullscreen int32) | |||
| 199 | 220 | ||
| 200 | 221 | func (h *handlerSet) OnTooltip(_ purecef.Browser, _ uintptr) int32 { return 0 } | |
| 201 | 222 | ||
| 202 | - | // OnStatusMessage fires the OnLinkHover callback with the status text. | |
| 223 | + | // OnStatusMessage fires the OnLinkHover callback with the status text | |
| 224 | + | // and caches the hover URI for middle-click interception. | |
| 203 | 225 | func (h *handlerSet) OnStatusMessage(_ purecef.Browser, value string) { | |
| 226 | + | h.wv.updateHoverURI(value) | |
| 227 | + | ||
| 204 | 228 | h.wv.mu.RLock() | |
| 205 | 229 | cb := h.wv.callbacks | |
| 206 | 230 | h.wv.mu.RUnlock() | |
| @@ -473,6 +497,28 @@ func (h *handlerSet) OnRenderProcessTerminated(_ purecef.Browser, status purecef | |||
| 473 | 497 | ||
| 474 | 498 | func (h *handlerSet) OnDocumentAvailableInMainFrame(_ purecef.Browser) {} | |
| 475 | 499 | ||
| 500 | + | // =========================================================================== | |
| 501 | + | // AudioHandler (5 methods) | |
| 502 | + | // =========================================================================== | |
| 503 | + | ||
| 504 | + | func (h *handlerSet) GetAudioParameters(_ purecef.Browser, _ *purecef.AudioParameters) int32 { | |
| 505 | + | return 1 // proceed with defaults | |
| 506 | + | } | |
| 507 | + | ||
| 508 | + | func (h *handlerSet) OnAudioStreamStarted(_ purecef.Browser, _ *purecef.AudioParameters, _ int32) { | |
| 509 | + | h.wv.setAudioPlaying(true) | |
| 510 | + | } | |
| 511 | + | ||
| 512 | + | func (h *handlerSet) OnAudioStreamPacket(_ purecef.Browser, _ unsafe.Pointer, _ int32, _ int64) {} | |
| 513 | + | ||
| 514 | + | func (h *handlerSet) OnAudioStreamStopped(_ purecef.Browser) { | |
| 515 | + | h.wv.setAudioPlaying(false) | |
| 516 | + | } | |
| 517 | + | ||
| 518 | + | func (h *handlerSet) OnAudioStreamError(_ purecef.Browser, _ string) { | |
| 519 | + | h.wv.setAudioPlaying(false) | |
| 520 | + | } | |
| 521 | + | ||
| 476 | 522 | // cefCursorNames maps CEF cursor types to GDK/CSS cursor names. | |
| 477 | 523 | var cefCursorNames = map[purecef.CursorType]string{ | |
| 478 | 524 | purecef.CursorTypeCtPointer: "default", | |
| @@ -24,13 +24,14 @@ var ( | |||||
| 24 | _ purecef.LoadHandler = (*handlerSet)(nil) | 24 | _ purecef.LoadHandler = (*handlerSet)(nil) | ||
| 25 | _ purecef.LifeSpanHandler = (*handlerSet)(nil) | 25 | _ purecef.LifeSpanHandler = (*handlerSet)(nil) | ||
| 26 | _ purecef.RequestHandler = (*handlerSet)(nil) | 26 | _ purecef.RequestHandler = (*handlerSet)(nil) | ||
| 27 | + | _ purecef.AudioHandler = (*handlerSet)(nil) | |||
| 27 | ) | 28 | ) | ||
| 28 | 29 | ||||
| 29 | // =========================================================================== | 30 | // =========================================================================== | ||
| 30 | // Client | 31 | // Client | ||
| 31 | // =========================================================================== | 32 | // =========================================================================== | ||
| 32 | 33 | ||||
| 33 | - | func (h *handlerSet) GetAudioHandler() purecef.AudioHandler { return nil } | 34 | + | func (h *handlerSet) GetAudioHandler() purecef.AudioHandler { return h } |
| 34 | func (h *handlerSet) GetCommandHandler() purecef.CommandHandler { return nil } | 35 | func (h *handlerSet) GetCommandHandler() purecef.CommandHandler { return nil } | ||
| 35 | func (h *handlerSet) GetContextMenuHandler() purecef.ContextMenuHandler { return nil } | 36 | func (h *handlerSet) GetContextMenuHandler() purecef.ContextMenuHandler { return nil } | ||
| 36 | func (h *handlerSet) GetDialogHandler() purecef.DialogHandler { return nil } | 37 | func (h *handlerSet) GetDialogHandler() purecef.DialogHandler { return nil } | ||
| @@ -92,7 +93,27 @@ func (h *handlerSet) GetScreenPoint(_ purecef.Browser, _, _ int32, _, _ unsafe.P | |||||
| 92 | return 0 | 93 | return 0 | ||
| 93 | } | 94 | } | ||
| 94 | 95 | ||||
| 95 | - | func (h *handlerSet) GetScreenInfo(_ purecef.Browser, _ *purecef.ScreenInfo) int32 { return 0 } | 96 | + | func (h *handlerSet) GetScreenInfo(_ purecef.Browser, info *purecef.ScreenInfo) int32 { |
| 97 | + | if info == nil { | |||
| 98 | + | return 0 | |||
| 99 | + | } | |||
| 100 | + | h.wv.pipeline.mu.Lock() | |||
| 101 | + | w := h.wv.pipeline.width | |||
| 102 | + | ht := h.wv.pipeline.height | |||
| 103 | + | s := h.wv.pipeline.scale | |||
| 104 | + | h.wv.pipeline.mu.Unlock() | |||
| 105 | + | ||||
| 106 | + | info.Size = unsafe.Sizeof(*info) | |||
| 107 | + | info.DeviceScaleFactor = float32(s) | |||
| 108 | + | info.Depth = 24 | |||
| 109 | + | info.DepthPerComponent = 8 | |||
| 110 | + | info.Rect.X = 0 | |||
| 111 | + | info.Rect.Y = 0 | |||
| 112 | + | info.Rect.Width = w | |||
| 113 | + | info.Rect.Height = ht | |||
| 114 | + | info.AvailableRect = info.Rect | |||
| 115 | + | return 1 | |||
| 116 | + | } | |||
| 96 | 117 | ||||
| 97 | func (h *handlerSet) OnPopupShow(_ purecef.Browser, _ int32) {} | 118 | func (h *handlerSet) OnPopupShow(_ purecef.Browser, _ int32) {} | ||
| 98 | 119 | ||||
| @@ -199,8 +220,11 @@ func (h *handlerSet) OnFullscreenModeChange(_ purecef.Browser, fullscreen int32) | |||||
| 199 | 220 | ||||
| 200 | func (h *handlerSet) OnTooltip(_ purecef.Browser, _ uintptr) int32 { return 0 } | 221 | func (h *handlerSet) OnTooltip(_ purecef.Browser, _ uintptr) int32 { return 0 } | ||
| 201 | 222 | ||||
| 202 | - | // OnStatusMessage fires the OnLinkHover callback with the status text. | 223 | + | // OnStatusMessage fires the OnLinkHover callback with the status text |
| 224 | + | // and caches the hover URI for middle-click interception. | |||
| 203 | func (h *handlerSet) OnStatusMessage(_ purecef.Browser, value string) { | 225 | func (h *handlerSet) OnStatusMessage(_ purecef.Browser, value string) { | ||
| 226 | + | h.wv.updateHoverURI(value) | |||
| 227 | + | ||||
| 204 | h.wv.mu.RLock() | 228 | h.wv.mu.RLock() | ||
| 205 | cb := h.wv.callbacks | 229 | cb := h.wv.callbacks | ||
| 206 | h.wv.mu.RUnlock() | 230 | h.wv.mu.RUnlock() | ||
| @@ -473,6 +497,28 @@ func (h *handlerSet) OnRenderProcessTerminated(_ purecef.Browser, status purecef | |||||
| 473 | 497 | ||||
| 474 | func (h *handlerSet) OnDocumentAvailableInMainFrame(_ purecef.Browser) {} | 498 | func (h *handlerSet) OnDocumentAvailableInMainFrame(_ purecef.Browser) {} | ||
| 475 | 499 | ||||
| 500 | + | // =========================================================================== | |||
| 501 | + | // AudioHandler (5 methods) | |||
| 502 | + | // =========================================================================== | |||
| 503 | + | ||||
| 504 | + | func (h *handlerSet) GetAudioParameters(_ purecef.Browser, _ *purecef.AudioParameters) int32 { | |||
| 505 | + | return 1 // proceed with defaults | |||
| 506 | + | } | |||
| 507 | + | ||||
| 508 | + | func (h *handlerSet) OnAudioStreamStarted(_ purecef.Browser, _ *purecef.AudioParameters, _ int32) { | |||
| 509 | + | h.wv.setAudioPlaying(true) | |||
| 510 | + | } | |||
| 511 | + | ||||
| 512 | + | func (h *handlerSet) OnAudioStreamPacket(_ purecef.Browser, _ unsafe.Pointer, _ int32, _ int64) {} | |||
| 513 | + | ||||
| 514 | + | func (h *handlerSet) OnAudioStreamStopped(_ purecef.Browser) { | |||
| 515 | + | h.wv.setAudioPlaying(false) | |||
| 516 | + | } | |||
| 517 | + | ||||
| 518 | + | func (h *handlerSet) OnAudioStreamError(_ purecef.Browser, _ string) { | |||
| 519 | + | h.wv.setAudioPlaying(false) | |||
| 520 | + | } | |||
| 521 | + | ||||
| 476 | // cefCursorNames maps CEF cursor types to GDK/CSS cursor names. | 522 | // cefCursorNames maps CEF cursor types to GDK/CSS cursor names. | ||
| 477 | var cefCursorNames = map[purecef.CursorType]string{ | 523 | var cefCursorNames = map[purecef.CursorType]string{ | ||
| 478 | purecef.CursorTypeCtPointer: "default", | 524 | purecef.CursorTypeCtPointer: "default", | ||