Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions lib/src/cef_web_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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<void> setVisible(bool visible) => _channel
.invokeMethod('setVisible', {'sessionId': sessionId, 'visible': visible});
Future<void> 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
Expand Down
43 changes: 43 additions & 0 deletions test/cef_web_controller_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading