diff --git a/integration_test/windows_textview_shot_test.dart b/integration_test/windows_textview_shot_test.dart index d1f7638..b7b55f9 100644 --- a/integration_test/windows_textview_shot_test.dart +++ b/integration_test/windows_textview_shot_test.dart @@ -21,6 +21,8 @@ void main() { ('ascii', List.generate(3000, (i) => String.fromCharCode(0x61 + i % 26)).join()), ('mixed', List.generate(3000, (i) => i.isEven ? '한' : 'x').join()), ('korean', '가나다라마바사아자차카타파하' * 200), + // 28 지적 (10/19): 이모지 (두 단위 글자) 가 든 긴 줄 - 재는 조각이 글자 가운데서 끊기면 UTF-16 오류 + ('emoji', List.generate(3000, (i) => 'abc😀').join()), ]) { testWidgets('글 뷰어 긴 줄 $name ${w}x$h', (t) async { t.view.physicalSize = Size(w.toDouble(), h.toDouble()); @@ -44,6 +46,7 @@ void main() { final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png)); Directory(out).createSync(recursive: true); File(p.join(out, 'textview_${name}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List()); + expect(t.takeException(), isNull, reason: '그리는 중 오류 없음 ($name $w)'); await t.pumpWidget(const SizedBox()); }); } diff --git a/lib/ui/text_viewer.dart b/lib/ui/text_viewer.dart index c3c6d98..df1e1b0 100644 --- a/lib/ui/text_viewer.dart +++ b/lib/ui/text_viewer.dart @@ -122,6 +122,7 @@ class _TextViewerPageState extends State { /// 넓은 글자 (한글 · 한자 · 가나 · 전각) 하나가 고정폭 글자 몇 개 폭인지 (관리자 지시 10/19: 넓은 글자가 섞인 줄도 줄 끝이 비지 않게) double _wideRatio(BuildContext context) { + // l10n-skip: 폭을 재는 넓은 글자 (화면 글이 아님) final m = _measure(context, 'M'), w = _measure(context, '가'); return m == null || w == null ? 2 : w / m; } @@ -158,7 +159,10 @@ class _TextViewerPageState extends State { _rows = viewerRows(_text, perRow, wide: _wideRatio(context), cut: (line, from) { var size = perRow * 8; while (true) { - final end = (from + size).clamp(0, line.length); + var end = (from + size).clamp(0, line.length); + // 28 지적 (10/19): 재는 조각이 이모지 같은 두 단위 글자의 가운데에서 끝나면 반쪽 글자라 TextSpan 이 + // "not well-formed UTF-16" 으로 실패했다 (화면은 정상이나 오류가 쏟아지고 더 큰 조각을 다시 잼) - 글자 앞에서 자른다 + if (end < line.length && end > from + 1 && (line.codeUnitAt(end) & 0xFC00) == 0xDC00) end--; tp.text = TextSpan(text: line.substring(from, end), style: style); tp.layout(maxWidth: room); final m = tp.computeLineMetrics(); diff --git a/test/xplore_tools_test.dart b/test/xplore_tools_test.dart index ee0ee57..3cefb02 100644 --- a/test/xplore_tools_test.dart +++ b/test/xplore_tools_test.dart @@ -143,6 +143,27 @@ void main() { expect(lines, 4, reason: '네 줄 분량으로 나눔 (다섯째 반쪽 줄 없음)'); }); + testWidgets('h 글 뷰어 (28 지적 10/19): 이모지가 든 긴 줄 - 재는 조각이 두 단위 글자 가운데서 끊기지 않음 (UTF-16 오류 없음)', (t) async { + final line = List.generate(3000, (i) => 'abc😀').join(); + final f = File(p.join(tmp.path, 'emoji.txt'))..writeAsStringSync('$line\nend'); + for (final w in [412.0, 960.0]) { + t.view.physicalSize = Size(w, 900); + t.view.devicePixelRatio = 1; + await t.pumpWidget(MaterialApp(home: TextViewerPage(key: ValueKey(w), path: f.path))); + for (var i = 0; i < 40 && find.textContaining('abc😀abc').evaluate().isEmpty; i++) { + await t.runAsync(() => Future.delayed(const Duration(milliseconds: 30))); + await t.pump(); + } + expect(t.takeException(), isNull, reason: '폭 $w'); + final texts = t.widgetList(find.textContaining('abc😀')).map((x) => x.data!).toList(); + expect(texts, isNotEmpty); + for (final s in texts) { + expect((s.codeUnitAt(s.length - 1) & 0xFC00) == 0xD800, isFalse, reason: '조각이 반쪽 이모지로 끝나지 않음'); + } + } + t.view.reset(); + }); + testWidgets('g 격자 보기: 그림 · 동영상만, 누르면 열기', (t) async { final png = File(p.join(tmp.path, 'a.png'))..writeAsBytesSync([0x89, 0x50]); final opened = [];