feat(cef): add Engine implementing port.Engine with adapters
Introduce Engine struct that implements port.Engine for the CEF backend. NewEngine initializes CEF, starts the message pump via glib.IdleAdd, and creates the GL loader, factory, and pool. Adapter types bridge concrete CEF types to port interfaces, with no-op stubs for Phase 1 subsystems (ContentInjector, SettingsApplier, FaviconDatabase).
- commit
- 10992b61b815caa224e9d069a31a3ea9022e5eb0
- author
- brice · · 2026-03-19 15:44 UTC
- committer
- brice · · 2026-03-28 04:35 UTC
- parent
- 440f59b
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +267 -0
A internal/infrastructure/cef/engine.go +100 -0
| @@ -0,0 +1,100 @@ | |||
| 1 | + | package cef | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | ||
| 6 | + | purecef "github.com/bnema/purego-cef/cef" | |
| 7 | + | "github.com/rs/zerolog" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/dumber/internal/application/port" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // Compile-time interface check. | |
| 13 | + | var _ port.Engine = (*Engine)(nil) | |
| 14 | + | ||
| 15 | + | // Engine implements port.Engine for the CEF browser backend. | |
| 16 | + | // It manages the CEF lifecycle and provides access to all engine subsystems. | |
| 17 | + | type Engine struct { | |
| 18 | + | ctx context.Context | |
| 19 | + | gl *glLoader | |
| 20 | + | factory *WebViewFactory | |
| 21 | + | pool *WebViewPool | |
| 22 | + | logger zerolog.Logger | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | // Factory returns the WebViewFactory for creating new WebView instances. | |
| 26 | + | func (e *Engine) Factory() port.WebViewFactory { | |
| 27 | + | return &webViewFactoryAdapter{factory: e.factory} | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | // Pool returns the WebViewPool for acquiring pre-warmed WebView instances. | |
| 31 | + | func (e *Engine) Pool() port.WebViewPool { | |
| 32 | + | return &webViewPoolAdapter{pool: e.pool, ctx: e.ctx} | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // ContentInjector returns a no-op injector (Phase 1 stub). | |
| 36 | + | func (e *Engine) ContentInjector() port.ContentInjector { | |
| 37 | + | return &noopContentInjector{} | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // SettingsApplier returns a no-op applier (Phase 1 stub). | |
| 41 | + | func (e *Engine) SettingsApplier() port.SettingsApplier { | |
| 42 | + | return &noopSettingsApplier{} | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | // FilterApplier returns nil (content filtering not yet supported for CEF). | |
| 46 | + | func (e *Engine) FilterApplier() port.FilterApplier { | |
| 47 | + | return nil | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | // FaviconDatabase returns a no-op database (Phase 1 stub). | |
| 51 | + | func (e *Engine) FaviconDatabase() port.FaviconDatabase { | |
| 52 | + | return &noopFaviconDatabase{} | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | // InternalSchemePath returns the URI scheme used for internal app resources. | |
| 56 | + | func (e *Engine) InternalSchemePath() string { | |
| 57 | + | return "cef://dumber/" | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | // Close releases all resources held by the engine. | |
| 61 | + | func (e *Engine) Close() error { | |
| 62 | + | e.pool.Close() | |
| 63 | + | purecef.Shutdown() | |
| 64 | + | return nil | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | // RegisterHandlers registers all WebUI message bridge handlers (Phase 1 stub). | |
| 68 | + | func (e *Engine) RegisterHandlers(_ context.Context, _ port.HandlerDependencies) error { | |
| 69 | + | return nil | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | // RegisterAccentHandlers registers accent/dead-key input handlers (Phase 1 stub). | |
| 73 | + | func (e *Engine) RegisterAccentHandlers(_ context.Context, _ port.AccentKeyHandler) error { | |
| 74 | + | return nil | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | // ConfigureDownloads sets up download handling (Phase 1 stub). | |
| 78 | + | func (e *Engine) ConfigureDownloads(_ context.Context, _ string, _ port.DownloadEventHandler, _ port.DownloadPreparer) error { | |
| 79 | + | return nil | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | // OnToolkitReady is called after the UI toolkit has initialized (Phase 1 stub). | |
| 83 | + | func (e *Engine) OnToolkitReady(_ context.Context) error { | |
| 84 | + | return nil | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | // UpdateAppearance updates default background color for new WebViews (Phase 1 stub). | |
| 88 | + | func (e *Engine) UpdateAppearance(_ context.Context, _, _, _, _ float64) error { | |
| 89 | + | return nil | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | // UpdateSettings applies runtime config changes to engine internals (Phase 1 stub). | |
| 93 | + | func (e *Engine) UpdateSettings(_ context.Context, _ port.EngineSettingsUpdate) error { | |
| 94 | + | return nil | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | // SetHandlerContext sets the base context for message handler dispatch. | |
| 98 | + | func (e *Engine) SetHandlerContext(ctx context.Context) { | |
| 99 | + | e.ctx = ctx | |
| 100 | + | } | |
| @@ -0,0 +1,100 @@ | |||||
| 1 | + | package cef | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | ||||
| 6 | + | purecef "github.com/bnema/purego-cef/cef" | |||
| 7 | + | "github.com/rs/zerolog" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // Compile-time interface check. | |||
| 13 | + | var _ port.Engine = (*Engine)(nil) | |||
| 14 | + | ||||
| 15 | + | // Engine implements port.Engine for the CEF browser backend. | |||
| 16 | + | // It manages the CEF lifecycle and provides access to all engine subsystems. | |||
| 17 | + | type Engine struct { | |||
| 18 | + | ctx context.Context | |||
| 19 | + | gl *glLoader | |||
| 20 | + | factory *WebViewFactory | |||
| 21 | + | pool *WebViewPool | |||
| 22 | + | logger zerolog.Logger | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | // Factory returns the WebViewFactory for creating new WebView instances. | |||
| 26 | + | func (e *Engine) Factory() port.WebViewFactory { | |||
| 27 | + | return &webViewFactoryAdapter{factory: e.factory} | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | // Pool returns the WebViewPool for acquiring pre-warmed WebView instances. | |||
| 31 | + | func (e *Engine) Pool() port.WebViewPool { | |||
| 32 | + | return &webViewPoolAdapter{pool: e.pool, ctx: e.ctx} | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // ContentInjector returns a no-op injector (Phase 1 stub). | |||
| 36 | + | func (e *Engine) ContentInjector() port.ContentInjector { | |||
| 37 | + | return &noopContentInjector{} | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | // SettingsApplier returns a no-op applier (Phase 1 stub). | |||
| 41 | + | func (e *Engine) SettingsApplier() port.SettingsApplier { | |||
| 42 | + | return &noopSettingsApplier{} | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | // FilterApplier returns nil (content filtering not yet supported for CEF). | |||
| 46 | + | func (e *Engine) FilterApplier() port.FilterApplier { | |||
| 47 | + | return nil | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | // FaviconDatabase returns a no-op database (Phase 1 stub). | |||
| 51 | + | func (e *Engine) FaviconDatabase() port.FaviconDatabase { | |||
| 52 | + | return &noopFaviconDatabase{} | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | // InternalSchemePath returns the URI scheme used for internal app resources. | |||
| 56 | + | func (e *Engine) InternalSchemePath() string { | |||
| 57 | + | return "cef://dumber/" | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | // Close releases all resources held by the engine. | |||
| 61 | + | func (e *Engine) Close() error { | |||
| 62 | + | e.pool.Close() | |||
| 63 | + | purecef.Shutdown() | |||
| 64 | + | return nil | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | // RegisterHandlers registers all WebUI message bridge handlers (Phase 1 stub). | |||
| 68 | + | func (e *Engine) RegisterHandlers(_ context.Context, _ port.HandlerDependencies) error { | |||
| 69 | + | return nil | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | // RegisterAccentHandlers registers accent/dead-key input handlers (Phase 1 stub). | |||
| 73 | + | func (e *Engine) RegisterAccentHandlers(_ context.Context, _ port.AccentKeyHandler) error { | |||
| 74 | + | return nil | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | // ConfigureDownloads sets up download handling (Phase 1 stub). | |||
| 78 | + | func (e *Engine) ConfigureDownloads(_ context.Context, _ string, _ port.DownloadEventHandler, _ port.DownloadPreparer) error { | |||
| 79 | + | return nil | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | // OnToolkitReady is called after the UI toolkit has initialized (Phase 1 stub). | |||
| 83 | + | func (e *Engine) OnToolkitReady(_ context.Context) error { | |||
| 84 | + | return nil | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | // UpdateAppearance updates default background color for new WebViews (Phase 1 stub). | |||
| 88 | + | func (e *Engine) UpdateAppearance(_ context.Context, _, _, _, _ float64) error { | |||
| 89 | + | return nil | |||
| 90 | + | } | |||
| 91 | + | ||||
| 92 | + | // UpdateSettings applies runtime config changes to engine internals (Phase 1 stub). | |||
| 93 | + | func (e *Engine) UpdateSettings(_ context.Context, _ port.EngineSettingsUpdate) error { | |||
| 94 | + | return nil | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | // SetHandlerContext sets the base context for message handler dispatch. | |||
| 98 | + | func (e *Engine) SetHandlerContext(ctx context.Context) { | |||
| 99 | + | e.ctx = ctx | |||
| 100 | + | } | |||
A internal/infrastructure/cef/engine_adapters.go +109 -0
| @@ -0,0 +1,109 @@ | |||
| 1 | + | package cef | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "fmt" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/dumber/internal/application/port" | |
| 8 | + | "github.com/bnema/dumber/internal/logging" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // --- WebViewFactory adapter --- | |
| 12 | + | ||
| 13 | + | // webViewFactoryAdapter bridges *WebViewFactory to port.WebViewFactory. | |
| 14 | + | // WebViewFactory.Create and CreateRelated return port.WebView directly, | |
| 15 | + | // so this adapter is thin but ensures the concrete type stays private. | |
| 16 | + | type webViewFactoryAdapter struct { | |
| 17 | + | factory *WebViewFactory | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | func (a *webViewFactoryAdapter) Create(ctx context.Context) (port.WebView, error) { | |
| 21 | + | wv, err := a.factory.Create(ctx) | |
| 22 | + | if err != nil { | |
| 23 | + | return nil, err | |
| 24 | + | } | |
| 25 | + | return wv, nil | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | func (a *webViewFactoryAdapter) CreateRelated(ctx context.Context, parentID port.WebViewID) (port.WebView, error) { | |
| 29 | + | wv, err := a.factory.CreateRelated(ctx, parentID) | |
| 30 | + | if err != nil { | |
| 31 | + | return nil, err | |
| 32 | + | } | |
| 33 | + | return wv, nil | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | // --- WebViewPool adapter --- | |
| 37 | + | ||
| 38 | + | // webViewPoolAdapter bridges *WebViewPool to port.WebViewPool. | |
| 39 | + | type webViewPoolAdapter struct { | |
| 40 | + | pool *WebViewPool | |
| 41 | + | ctx context.Context | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | func (a *webViewPoolAdapter) Acquire(ctx context.Context) (port.WebView, error) { | |
| 45 | + | wv, err := a.pool.Acquire(ctx) | |
| 46 | + | if err != nil { | |
| 47 | + | return nil, err | |
| 48 | + | } | |
| 49 | + | return wv, nil | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | func (a *webViewPoolAdapter) Release(wv port.WebView) { | |
| 53 | + | if wv == nil { | |
| 54 | + | return | |
| 55 | + | } | |
| 56 | + | cwv, ok := wv.(*WebView) | |
| 57 | + | if !ok { | |
| 58 | + | logging.FromContext(a.ctx).Warn(). | |
| 59 | + | Str("concrete_type", fmt.Sprintf("%T", wv)). | |
| 60 | + | Msg("webViewPoolAdapter.Release: unexpected type, cannot release to pool") | |
| 61 | + | return | |
| 62 | + | } | |
| 63 | + | a.pool.Release(cwv) | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | func (a *webViewPoolAdapter) Prewarm(count int) { | |
| 67 | + | a.pool.Prewarm(count) | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | func (a *webViewPoolAdapter) PrewarmAsync(ctx context.Context, count int) { | |
| 71 | + | a.pool.PrewarmAsync(ctx, count) | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | func (a *webViewPoolAdapter) Size() int { | |
| 75 | + | return a.pool.Size() | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | func (a *webViewPoolAdapter) Close() { | |
| 79 | + | a.pool.Close() | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | // --- No-op stubs for Phase 1 --- | |
| 83 | + | ||
| 84 | + | // noopContentInjector implements port.ContentInjector as a no-op. | |
| 85 | + | type noopContentInjector struct{} | |
| 86 | + | ||
| 87 | + | func (n *noopContentInjector) InjectThemeCSS(_ context.Context, _ string) error { | |
| 88 | + | return nil | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | func (n *noopContentInjector) InjectFindHighlightCSS(_ context.Context, _ string) error { | |
| 92 | + | return nil | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | func (n *noopContentInjector) RefreshScripts(_ context.Context, _ port.WebView) error { | |
| 96 | + | return nil | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | // noopSettingsApplier implements port.SettingsApplier as a no-op. | |
| 100 | + | type noopSettingsApplier struct{} | |
| 101 | + | ||
| 102 | + | func (n *noopSettingsApplier) ApplyToAll(_ context.Context, _ []port.WebView) {} | |
| 103 | + | ||
| 104 | + | // noopFaviconDatabase implements port.FaviconDatabase as a no-op. | |
| 105 | + | type noopFaviconDatabase struct{} | |
| 106 | + | ||
| 107 | + | func (n *noopFaviconDatabase) GetFaviconAsync(_ string, callback func(port.Texture)) { | |
| 108 | + | callback(nil) | |
| 109 | + | } | |
| @@ -0,0 +1,109 @@ | |||||
| 1 | + | package cef | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "fmt" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 8 | + | "github.com/bnema/dumber/internal/logging" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // --- WebViewFactory adapter --- | |||
| 12 | + | ||||
| 13 | + | // webViewFactoryAdapter bridges *WebViewFactory to port.WebViewFactory. | |||
| 14 | + | // WebViewFactory.Create and CreateRelated return port.WebView directly, | |||
| 15 | + | // so this adapter is thin but ensures the concrete type stays private. | |||
| 16 | + | type webViewFactoryAdapter struct { | |||
| 17 | + | factory *WebViewFactory | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | func (a *webViewFactoryAdapter) Create(ctx context.Context) (port.WebView, error) { | |||
| 21 | + | wv, err := a.factory.Create(ctx) | |||
| 22 | + | if err != nil { | |||
| 23 | + | return nil, err | |||
| 24 | + | } | |||
| 25 | + | return wv, nil | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | func (a *webViewFactoryAdapter) CreateRelated(ctx context.Context, parentID port.WebViewID) (port.WebView, error) { | |||
| 29 | + | wv, err := a.factory.CreateRelated(ctx, parentID) | |||
| 30 | + | if err != nil { | |||
| 31 | + | return nil, err | |||
| 32 | + | } | |||
| 33 | + | return wv, nil | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | // --- WebViewPool adapter --- | |||
| 37 | + | ||||
| 38 | + | // webViewPoolAdapter bridges *WebViewPool to port.WebViewPool. | |||
| 39 | + | type webViewPoolAdapter struct { | |||
| 40 | + | pool *WebViewPool | |||
| 41 | + | ctx context.Context | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | func (a *webViewPoolAdapter) Acquire(ctx context.Context) (port.WebView, error) { | |||
| 45 | + | wv, err := a.pool.Acquire(ctx) | |||
| 46 | + | if err != nil { | |||
| 47 | + | return nil, err | |||
| 48 | + | } | |||
| 49 | + | return wv, nil | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | func (a *webViewPoolAdapter) Release(wv port.WebView) { | |||
| 53 | + | if wv == nil { | |||
| 54 | + | return | |||
| 55 | + | } | |||
| 56 | + | cwv, ok := wv.(*WebView) | |||
| 57 | + | if !ok { | |||
| 58 | + | logging.FromContext(a.ctx).Warn(). | |||
| 59 | + | Str("concrete_type", fmt.Sprintf("%T", wv)). | |||
| 60 | + | Msg("webViewPoolAdapter.Release: unexpected type, cannot release to pool") | |||
| 61 | + | return | |||
| 62 | + | } | |||
| 63 | + | a.pool.Release(cwv) | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | func (a *webViewPoolAdapter) Prewarm(count int) { | |||
| 67 | + | a.pool.Prewarm(count) | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | func (a *webViewPoolAdapter) PrewarmAsync(ctx context.Context, count int) { | |||
| 71 | + | a.pool.PrewarmAsync(ctx, count) | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | func (a *webViewPoolAdapter) Size() int { | |||
| 75 | + | return a.pool.Size() | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | func (a *webViewPoolAdapter) Close() { | |||
| 79 | + | a.pool.Close() | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | // --- No-op stubs for Phase 1 --- | |||
| 83 | + | ||||
| 84 | + | // noopContentInjector implements port.ContentInjector as a no-op. | |||
| 85 | + | type noopContentInjector struct{} | |||
| 86 | + | ||||
| 87 | + | func (n *noopContentInjector) InjectThemeCSS(_ context.Context, _ string) error { | |||
| 88 | + | return nil | |||
| 89 | + | } | |||
| 90 | + | ||||
| 91 | + | func (n *noopContentInjector) InjectFindHighlightCSS(_ context.Context, _ string) error { | |||
| 92 | + | return nil | |||
| 93 | + | } | |||
| 94 | + | ||||
| 95 | + | func (n *noopContentInjector) RefreshScripts(_ context.Context, _ port.WebView) error { | |||
| 96 | + | return nil | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | // noopSettingsApplier implements port.SettingsApplier as a no-op. | |||
| 100 | + | type noopSettingsApplier struct{} | |||
| 101 | + | ||||
| 102 | + | func (n *noopSettingsApplier) ApplyToAll(_ context.Context, _ []port.WebView) {} | |||
| 103 | + | ||||
| 104 | + | // noopFaviconDatabase implements port.FaviconDatabase as a no-op. | |||
| 105 | + | type noopFaviconDatabase struct{} | |||
| 106 | + | ||||
| 107 | + | func (n *noopFaviconDatabase) GetFaviconAsync(_ string, callback func(port.Texture)) { | |||
| 108 | + | callback(nil) | |||
| 109 | + | } | |||
A internal/infrastructure/cef/engine_init.go +58 -0
| @@ -0,0 +1,58 @@ | |||
| 1 | + | package cef | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "fmt" | |
| 6 | + | ||
| 7 | + | purecef "github.com/bnema/purego-cef/cef" | |
| 8 | + | "github.com/jwijenbergh/puregotk/v4/glib" | |
| 9 | + | "github.com/rs/zerolog" | |
| 10 | + | ||
| 11 | + | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // NewEngine initializes the CEF runtime and returns a ready-to-use Engine. | |
| 15 | + | func NewEngine(ctx context.Context, cfg config.CEFEngineConfig, logger zerolog.Logger) (*Engine, error) { | |
| 16 | + | // 1. Initialize CEF. | |
| 17 | + | settings := purecef.DefaultSettings() | |
| 18 | + | if cfg.CEFDir != "" { | |
| 19 | + | settings.CEFDir = cfg.CEFDir | |
| 20 | + | } | |
| 21 | + | if cfg.LogSeverity != 0 { | |
| 22 | + | settings.LogSeverity = cfg.LogSeverity | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | purecef.MaybeExitSubprocess() | |
| 26 | + | ||
| 27 | + | if err := purecef.Init(settings); err != nil { | |
| 28 | + | return nil, fmt.Errorf("cef.Init: %w", err) | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | // 2. Pump CEF messages via the glib idle loop so CEF work executes on the | |
| 32 | + | // GTK main thread alongside normal UI processing. | |
| 33 | + | cb := glib.SourceFunc(func(_ uintptr) bool { | |
| 34 | + | purecef.DoMessageLoopWork() | |
| 35 | + | return true // keep firing | |
| 36 | + | }) | |
| 37 | + | glib.IdleAdd(&cb, 0) | |
| 38 | + | ||
| 39 | + | // 3. Load GL. | |
| 40 | + | gl, err := newGLLoader() | |
| 41 | + | if err != nil { | |
| 42 | + | purecef.Shutdown() | |
| 43 | + | return nil, fmt.Errorf("GL loader: %w", err) | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | // 4. Create factory + pool. | |
| 47 | + | scale := int32(1) // TODO: detect from GDK | |
| 48 | + | factory := newWebViewFactory(gl, scale) | |
| 49 | + | pool := newWebViewPool(factory) | |
| 50 | + | ||
| 51 | + | return &Engine{ | |
| 52 | + | ctx: ctx, | |
| 53 | + | gl: gl, | |
| 54 | + | factory: factory, | |
| 55 | + | pool: pool, | |
| 56 | + | logger: logger, | |
| 57 | + | }, nil | |
| 58 | + | } | |
| @@ -0,0 +1,58 @@ | |||||
| 1 | + | package cef | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "fmt" | |||
| 6 | + | ||||
| 7 | + | purecef "github.com/bnema/purego-cef/cef" | |||
| 8 | + | "github.com/jwijenbergh/puregotk/v4/glib" | |||
| 9 | + | "github.com/rs/zerolog" | |||
| 10 | + | ||||
| 11 | + | "github.com/bnema/dumber/internal/infrastructure/config" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // NewEngine initializes the CEF runtime and returns a ready-to-use Engine. | |||
| 15 | + | func NewEngine(ctx context.Context, cfg config.CEFEngineConfig, logger zerolog.Logger) (*Engine, error) { | |||
| 16 | + | // 1. Initialize CEF. | |||
| 17 | + | settings := purecef.DefaultSettings() | |||
| 18 | + | if cfg.CEFDir != "" { | |||
| 19 | + | settings.CEFDir = cfg.CEFDir | |||
| 20 | + | } | |||
| 21 | + | if cfg.LogSeverity != 0 { | |||
| 22 | + | settings.LogSeverity = cfg.LogSeverity | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | purecef.MaybeExitSubprocess() | |||
| 26 | + | ||||
| 27 | + | if err := purecef.Init(settings); err != nil { | |||
| 28 | + | return nil, fmt.Errorf("cef.Init: %w", err) | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | // 2. Pump CEF messages via the glib idle loop so CEF work executes on the | |||
| 32 | + | // GTK main thread alongside normal UI processing. | |||
| 33 | + | cb := glib.SourceFunc(func(_ uintptr) bool { | |||
| 34 | + | purecef.DoMessageLoopWork() | |||
| 35 | + | return true // keep firing | |||
| 36 | + | }) | |||
| 37 | + | glib.IdleAdd(&cb, 0) | |||
| 38 | + | ||||
| 39 | + | // 3. Load GL. | |||
| 40 | + | gl, err := newGLLoader() | |||
| 41 | + | if err != nil { | |||
| 42 | + | purecef.Shutdown() | |||
| 43 | + | return nil, fmt.Errorf("GL loader: %w", err) | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | // 4. Create factory + pool. | |||
| 47 | + | scale := int32(1) // TODO: detect from GDK | |||
| 48 | + | factory := newWebViewFactory(gl, scale) | |||
| 49 | + | pool := newWebViewPool(factory) | |||
| 50 | + | ||||
| 51 | + | return &Engine{ | |||
| 52 | + | ctx: ctx, | |||
| 53 | + | gl: gl, | |||
| 54 | + | factory: factory, | |||
| 55 | + | pool: pool, | |||
| 56 | + | logger: logger, | |||
| 57 | + | }, nil | |||
| 58 | + | } | |||