diff --git a/lib/src/cef_web_controller.dart b/lib/src/cef_web_controller.dart index d2e34cf..81f762e 100644 --- a/lib/src/cef_web_controller.dart +++ b/lib/src/cef_web_controller.dart @@ -30,6 +30,16 @@ class CefWebController { static int _counter = 0; bool _disposed = false; + // Last visibility the consumer requested, and whether it was ever set + // explicitly. Re-asserted to the native side once the browser binds (see + // [_createSession]) so a `setVisible` that raced or was lost during the create + // burst can't leave the slot latched hidden — a permanently blank on-screen + // tile. Only re-asserted when explicitly set: a never-set slot keeps CEF's + // default-shown, so a fresh/recovered OFF-screen slot isn't briefly forced + // visible (which would pump it at 60fps until its cull edge lands). + bool _lastVisible = true; + bool _visibilityExplicitlySet = false; + /// Stable id for this session, echoed in every host message. final String sessionId; @@ -520,6 +530,19 @@ class CefWebController { _channel.invokeMethod( 'addJavaScriptChannel', {'sessionId': sessionId, 'name': name}); } + // Re-assert the last-known visibility now the browser is bound — but only if + // the consumer ever set it. A setVisible that landed before the browser bound + // (recorded as intent only) or was lost / mis-ordered under the create burst + // would otherwise leave the slot latched hidden — a permanently blank + // on-screen tile. Idempotent when already in sync (the native hidden->visible + // edge repaints; a matching state is a no-op). Gated on the explicit-set flag + // so a fresh/recovered slot that was never told a visibility keeps CEF's + // default-shown instead of being force-repainted. Pairs with the consumer-side + // direct drive in the agent_ui tile. + if (_visibilityExplicitlySet) { + _channel.invokeMethod( + 'setVisible', {'sessionId': sessionId, 'visible': _lastVisible}); + } return textureId; } @@ -783,8 +806,12 @@ class CefWebController { /// teardown. Use it to stop an off-screen view from burning GPU while keeping /// its DOM, scroll position, and JS state intact; call `setVisible(true)` to /// resume (CEF repaints the current frame). Visibility defaults to shown. - Future setVisible(bool visible) => _channel - .invokeMethod('setVisible', {'sessionId': sessionId, 'visible': visible}); + Future setVisible(bool visible) { + _lastVisible = visible; + _visibilityExplicitlySet = true; + return _channel.invokeMethod( + 'setVisible', {'sessionId': sessionId, 'visible': visible}); + } /// Start (or advance) a find-in-page search for [text]. Results arrive on /// [onFindResult]. Pass `findNext: true` to move to the next/previous match of diff --git a/test/cef_web_controller_test.dart b/test/cef_web_controller_test.dart index e145047..79e6f95 100644 --- a/test/cef_web_controller_test.dart +++ b/test/cef_web_controller_test.dart @@ -41,6 +41,49 @@ void main() { expect(args['width'], 100); }); + test('create re-asserts last-known visibility only when explicitly set', + () async { + // Explicit setVisible(false) before create: the browser binds, then the + // controller replays the recorded visibility so a create-burst race can't + // leave the slot latched in the wrong state. Carries the explicit value. + final hidden = CefWebController(sessionId: 'vis-false'); + await hidden.setVisible(false); + log.clear(); + await hidden.create(url: 'about:blank', width: 1, height: 1); + final reHidden = log.where((m) => + m.method == 'setVisible' && + (m.arguments as Map)['sessionId'] == 'vis-false'); + expect(reHidden, isNotEmpty, + reason: 'an explicitly-set visibility must be re-asserted after bind'); + expect((reHidden.last.arguments as Map)['visible'], false); + + // Explicit setVisible(true) before create: replays true — the on-screen wedge + // heal (a visible edge that raced the bind is re-applied so the tile paints). + final shown = CefWebController(sessionId: 'vis-true'); + await shown.setVisible(true); + log.clear(); + await shown.create(url: 'about:blank', width: 1, height: 1); + final reShown = log.where((m) => + m.method == 'setVisible' && + (m.arguments as Map)['sessionId'] == 'vis-true'); + expect(reShown, isNotEmpty); + expect((reShown.last.arguments as Map)['visible'], true); + + // Never set: NO re-assert. A fresh/recovered slot that was never told a + // visibility keeps CEF's default-shown rather than being force-repainted, so + // an off-screen tile isn't briefly pumped at 60fps on create/recover. + final never = CefWebController(sessionId: 'vis-never'); + log.clear(); + await never.create(url: 'about:blank', width: 1, height: 1); + expect( + log.where((m) => + m.method == 'setVisible' && + (m.arguments as Map)['sessionId'] == 'vis-never'), + isEmpty, + reason: 'a never-set visibility must not be re-asserted (off-screen guard)', + ); + }); + test('create forwards allowedSchemes as a lowercased CSV', () async { final c = CefWebController(sessionId: 's-allow'); await c.create(