Repository navigation
[macOS] With semantics enabled, the first composing keystroke into an empty TextField loses its composition and the field stops repainting #194199
Description
Steps to reproduce
flutter create --platforms=macos repro, replacelib/main.dartwith the code sample, and add the Swift snippet tomacos/Runner/MainFlutterWindow.swift. The snippet's channel postsNSApplicationDidChangeAccessibilityEnhancedUserInterfaceNotificationwithAXEnhancedUserInterface = YES. AppKit posts the same notification when another process sets that attribute on the app throughAXUIElementSetAttributeValue, as assistive apps do. The engine enables semantics when it arrives (FlutterEngine.mm,onAccessibilityStatusChanged:). Setting the attribute from another process has the same effect on a release build.flutter run -d macos, with the ABC keyboard layout.- Click the first field and type ⌥E, then
e, thenx. It readséx. - Click "Enable semantics, as an assistive app does".
- Click the second field and type ⌥E, then
e, thenx.
Expected results
The second field behaves like the first: ⌥E shows the accent as marked text, e turns it into é, and the field reads éx.
Actual results
- The second field shows an underlined
´and stops repainting; even the caret stops blinking. No frame runs until something unrelated schedules one, such as hovering the button or the window resigning key. - Its controller ends up holding
ex: the pending accent is discarded, soénever forms. - With an input method that composes with marked text (tested with Sogou Pinyin 6.25.3), typing
ceshileaves the field at an underlinedc. The input method commitscon its own and starts a new composition ate, so choosing a candidate yieldscfollowed by the candidate foreshi. - Typing into the field again, after it has been edited once, works.
- With
FLTEnableMergedPlatformUIThreadset tofalse, the field repaints normally, but the first composition is still lost.
Mechanism, from the logs below and from method swizzling on FlutterTextInputPlugin and FlutterTextField:
- The first keystroke arrives as marked text (
setMarkedText:), and the framework applies composing0..1. - The next frame's
flushSemanticssends the field's new value.Shell::OnEngineUpdateSemanticsusesRunNowOrPostTask, so with merged threadsAccessibilityBridgeMachandles the update synchronously inside the frame. ItsVALUE_CHANGEDbranch calls[FlutterTextField startEditing], which callsselectText:.FlutterTextInputPluginresigns first responder and becomes the field editor of theNSTextFieldwhile the composition is still pending, so the composition ends. The ABC dead key is discarded; Sogou Pinyin commits its letter withinsertText:. - The plugin reports the change through
updateEditState.Shell::OnPlatformViewDispatchPlatformMessageusesRunNowAndFlushMessages, so the framework'sTextInputhandler runs immediately, still inside the frame (SchedulerPhase.persistentCallbacks).EditableTextapplies the value and marks itself dirty.ensureVisualUpdatedoes not schedule a frame in that phase, and layout and paint have already run. Every latersetStateandmarkNeedsPaintreturns early because the dirty flags are already set, sohasScheduledFramestaysfalse.
The same code is on master: AccessibilityBridgeMac.mm calls startEditing synchronously from VALUE_CHANGED and DOCUMENT_SELECTION_CHANGED, with no marked-text check.
Code sample
lib/main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
SchedulerBinding.instance.addPersistentFrameCallback((_) => debugPrint('REPRO frame'));
runApp(const MaterialApp(home: Repro()));
}
class Repro extends StatefulWidget {
const Repro({super.key});
@override
State<Repro> createState() => _ReproState();
}
class _ReproState extends State<Repro> {
final _first = TextEditingController();
final _second = TextEditingController();
@override
void initState() {
super.initState();
for (final (name, controller) in [('first', _first), ('second', _second)]) {
controller.addListener(() {
final scheduler = SchedulerBinding.instance;
debugPrint('REPRO $name "${controller.text}" composing ${controller.value.composing} '
'phase ${scheduler.schedulerPhase}');
scheduleMicrotask(() => debugPrint('REPRO then hasScheduledFrame ${scheduler.hasScheduledFrame}'));
});
}
}
@override
void dispose() {
_first.dispose();
_second.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(controller: _first, decoration: const InputDecoration(labelText: 'First field')),
const SizedBox(height: 24),
FilledButton(
onPressed: () => const MethodChannel('repro').invokeMethod<void>('enableSemantics'),
child: const Text('Enable semantics, as an assistive app does'),
),
const SizedBox(height: 24),
TextField(controller: _second, decoration: const InputDecoration(labelText: 'Second field')),
],
),
),
);
}
}macos/Runner/MainFlutterWindow.swift, after RegisterGeneratedPlugins
// Posts what AppKit posts when an assistive app sets AXEnhancedUserInterface on this app.
FlutterMethodChannel(name: "repro", binaryMessenger: flutterViewController.engine.binaryMessenger)
.setMethodCallHandler { _, result in
NotificationCenter.default.post(
name: NSNotification.Name("NSApplicationDidChangeAccessibilityEnhancedUserInterfaceNotification"),
object: NSApp, userInfo: ["AXEnhancedUserInterface": true])
result(nil)
}Screenshots or Video
Screenshots / Video demonstration
The sample above, with the ABC keyboard layout. The first field gives éx. After the button, the second field sticks on ´ with a frozen caret, and moving the pointer over the button repaints it as ex.
flutter-sample-deadkey.mp4
Logs
Logs
ABC layout, merged threads (default). The first field, before semantics is on, then the second field after it:
REPRO first "´" composing TextRange(start: 0, end: 1) phase SchedulerPhase.idle
REPRO then hasScheduledFrame true
REPRO first "é" composing TextRange(start: -1, end: -1) phase SchedulerPhase.idle
REPRO then hasScheduledFrame true
REPRO first "éx" composing TextRange(start: -1, end: -1) phase SchedulerPhase.idle
REPRO then hasScheduledFrame true
...
REPRO second "´" composing TextRange(start: 0, end: 1) phase SchedulerPhase.idle
REPRO then hasScheduledFrame true
REPRO second "" composing TextRange(start: -1, end: -1) phase SchedulerPhase.persistentCallbacks
REPRO frame
REPRO then hasScheduledFrame false
REPRO second "e" composing TextRange(start: -1, end: -1) phase SchedulerPhase.idle
REPRO then hasScheduledFrame false
REPRO second "ex" composing TextRange(start: -1, end: -1) phase SchedulerPhase.idle
REPRO then hasScheduledFrame false
No REPRO frame follows until the pointer moves over the button.
Sogou Pinyin, typing ceshi, merged threads (default), from a single-field version of the sample:
REPRO value "c" composing TextRange(start: 0, end: 1) phase SchedulerPhase.idle
REPRO then hasScheduledFrame true
REPRO value "c" composing TextRange(start: -1, end: -1) phase SchedulerPhase.persistentCallbacks
REPRO frame
REPRO then hasScheduledFrame false
REPRO value "ce" composing TextRange(start: 1, end: 2) phase SchedulerPhase.idle
REPRO then hasScheduledFrame false
...
REPRO value "ce'shi" composing TextRange(start: 1, end: 6) phase SchedulerPhase.idle
REPRO then hasScheduledFrame false
Sogou Pinyin with FLTEnableMergedPlatformUIThread set to false, from the single-field version:
REPRO value "c" composing TextRange(start: 0, end: 1) phase SchedulerPhase.idle
REPRO then hasScheduledFrame true
REPRO frame
REPRO value "c" composing TextRange(start: -1, end: -1) phase SchedulerPhase.idle
REPRO then hasScheduledFrame true
REPRO value "ce" composing TextRange(start: 1, end: 2) phase SchedulerPhase.idle
REPRO then hasScheduledFrame true
REPRO frame
...
Native calls on the first keystroke, Sogou Pinyin, from our app (swizzled FlutterTextInputPlugin and FlutterTextField):
setMarkedText "c" superview=FlutterViewWrapper
FlutterTextField startEditing (currentEditor == nil)
plugin resignFirstResponder
insertText "c" plugin not first responder
plugin becomeFirstResponder superview=_NSKeyboardFocusClipView
plugin setClient: <FlutterTextField>
setMarkedText "e" … "e'shi"
Flutter Doctor output
Doctor output
[✓] Flutter (3.47.7, on macOS 26.4 25E246 darwin-arm64)
• Framework revision abaf9c5237, 2026-10-08
• Engine revision deb287481e (hash d8a2d6c768c334d53e16277f837970ffa1e2de81)
• Dart version 3.13.5
[✓] Xcode - develop for iOS and macOS (Xcode 26.6)
• Build 17F113
Activity
suryaKommana2662 commentedon Oct 13, 2026
@agrapine Thanks for the report. I followed the provided reproduction steps on macOS, but I could not reproduce the issue locally. After enabling semantics, the second text field displayed éx as expected when typing Option+E, followed by e and x.
Screen.Recording.2026-10-13.at.2.03.24.PM.mov
Flutter Doctor Output
agrapine commentedon Oct 13, 2026
Thanks for trying it, @suryaKommana2662.
I ran it again with the stock 3.47.7 SDK (prebuilt engine
deb287481e) on macOS 26.4 (25E246), and it still reproduces, both windowed and full screen:In your recording the underlined
´in the second field turns intoé, so the composition survived on your machine. Could you rerun with this change to the listener'sdebugPrintinlib/main.dartand paste theREPROlines from theflutter runconsole?semantics false, the button didn't turn semantics on. AMissingPluginExceptionin the console means theMainFlutterWindow.swiftsnippet isn't in the build.semantics trueand no line showsSchedulerPhase.persistentCallbacks, the composition survives on macOS 27.0 (26A428). I can only test 26.4 here, so that would narrow the bug to earlier macOS versions.