nandi/frqpublic Fork 0
ec74784
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Text can be selected

One SelectionArea around the whole tree rather than a SelectableText per
label. Per-widget selection reaches only inside one widget, so a nick,
the time beside it and the two lines under it cannot be dragged across —
which is most of what anyone wants to copy out of a chat.

The risk of wrapping everything is a selection gesture that swallows the
taps underneath it, so there is a test that opens a room by pressing the
button that opens it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-19T10:14:41-07:00 Browse files
ec74784 parent: 6214c7e
modified flutter/lib/nim_renderer.dart +13 -1
@@ -115,7 +115,19 @@ class _NimAppState extends State<NimApp> {
115115 error: t.destructive,
116116 ),
117117 ),
118- home: Scaffold(backgroundColor: t.bg, body: SafeArea(child: _build(_tree))),
118+ // Everything inside one SelectionArea, so a message can be selected
119+ // and copied — and so can a nick, a timestamp, or a line of an error.
120+ // Per-widget `SelectableText` was the alternative and is worse: it
121+ // selects within one widget only, so a two-line answer and the name
122+ // above it cannot be dragged across, which is most of what anyone
123+ // wants to copy out of a chat.
124+ //
125+ // Taps still arrive: a selection starts on a drag, and the buttons,
126+ // faces and reaction pills under here keep their gestures.
127+ home: Scaffold(
128+ backgroundColor: t.bg,
129+ body: SafeArea(child: SelectionArea(child: _build(_tree))),
130+ ),
119131 );
120132
121133 // ---------------------------------------------------------------- helpers
@@ -115,7 +115,19 @@ class _NimAppState extends State<NimApp> {
115 error: t.destructive,115 error: t.destructive,
116 ),116 ),
117 ),117 ),
118- home: Scaffold(backgroundColor: t.bg, body: SafeArea(child: _build(_tree))),118+ // Everything inside one SelectionArea, so a message can be selected
119+ // and copied — and so can a nick, a timestamp, or a line of an error.
120+ // Per-widget `SelectableText` was the alternative and is worse: it
121+ // selects within one widget only, so a two-line answer and the name
122+ // above it cannot be dragged across, which is most of what anyone
123+ // wants to copy out of a chat.
124+ //
125+ // Taps still arrive: a selection starts on a drag, and the buttons,
126+ // faces and reaction pills under here keep their gestures.
127+ home: Scaffold(
128+ backgroundColor: t.bg,
129+ body: SafeArea(child: SelectionArea(child: _build(_tree))),
130+ ),
119 );131 );
120 132
121 // ---------------------------------------------------------------- helpers133 // ---------------------------------------------------------------- helpers
modified flutter/test/nim_layout_test.dart +26 -0
@@ -215,6 +215,32 @@ void main() {
215215 }
216216 });
217217
218+ group('selection', () {
219+ testWidgets('the whole tree sits in one SelectionArea', (tester) async {
220+ // One, not one per Text: a selection has to be draggable across the
221+ // nick, the time and the message, which is most of what anyone wants
222+ // to copy out of a chat.
223+ core.demoUi();
224+ await layOut(tester, sizes['desktop']!);
225+ expect(find.byType(SelectionArea), findsOneWidget);
226+ expect(find.text('hello there'), findsOneWidget);
227+ });
228+
229+ testWidgets('and taps still reach what is under it', (tester) async {
230+ // The risk with wrapping everything: a selection gesture that eats the
231+ // taps underneath. Opening a room from the list is the plainest one.
232+ core.demoUi();
233+ core.dispatch('screen.chats');
234+ await layOut(tester, sizes['desktop']!);
235+ // The row's "Open", not its name: the name is a label in this list.
236+ await tester.tap(find.text('Open').first);
237+ await tester.pump();
238+ expect(find.text('hello there'), findsWidgets,
239+ reason: 'tapping the room did not open it');
240+ expectLaidOut(tester, 'the room the tap opened');
241+ });
242+ });
243+
218244 group('emoji', () {
219245 // ✏️ is U+270F plus a variation selector asking for emoji presentation,
220246 // and DejaVu Sans claims U+270F — so ordinary fallback draws a monochrome
@@ -215,6 +215,32 @@ void main() {
215 }215 }
216 });216 });
217 217
218+ group('selection', () {
219+ testWidgets('the whole tree sits in one SelectionArea', (tester) async {
220+ // One, not one per Text: a selection has to be draggable across the
221+ // nick, the time and the message, which is most of what anyone wants
222+ // to copy out of a chat.
223+ core.demoUi();
224+ await layOut(tester, sizes['desktop']!);
225+ expect(find.byType(SelectionArea), findsOneWidget);
226+ expect(find.text('hello there'), findsOneWidget);
227+ });
228+
229+ testWidgets('and taps still reach what is under it', (tester) async {
230+ // The risk with wrapping everything: a selection gesture that eats the
231+ // taps underneath. Opening a room from the list is the plainest one.
232+ core.demoUi();
233+ core.dispatch('screen.chats');
234+ await layOut(tester, sizes['desktop']!);
235+ // The row's "Open", not its name: the name is a label in this list.
236+ await tester.tap(find.text('Open').first);
237+ await tester.pump();
238+ expect(find.text('hello there'), findsWidgets,
239+ reason: 'tapping the room did not open it');
240+ expectLaidOut(tester, 'the room the tap opened');
241+ });
242+ });
243+
218 group('emoji', () {244 group('emoji', () {
219 // ✏️ is U+270F plus a variation selector asking for emoji presentation,245 // ✏️ is U+270F plus a variation selector asking for emoji presentation,
220 // and DejaVu Sans claims U+270F — so ordinary fallback draws a monochrome246 // and DejaVu Sans claims U+270F — so ordinary fallback draws a monochrome