Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add NesRunningTextLines #116

Merged
merged 1 commit into from
Jan 4, 2024
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# 0.13.0
- feat: `NesContainer.paintBuilder` to override the one set on the theme.
- fix: `NesRunningText` was not disposing its controller.
- feat: add `NesRunningTextLines`.

# 0.12.1
- fix: theme lerp causing error on null access.
Expand Down
13 changes: 13 additions & 0 deletions example/lib/gallery/sections/running_text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ class RunningTextSection extends StatelessWidget {
const NesRunningText(
text: 'This is a simple running text.',
),
const SizedBox(height: 32),
Text(
'Running text multiline',
style: theme.textTheme.displayMedium,
),
const SizedBox(height: 16),
const NesRunningTextLines(
texts: [
'Welcome back adventurer!',
'Hope you gathered all your gear!',
'The journey is about to begin!',
],
),
],
);
}
Expand Down
6 changes: 6 additions & 0 deletions lib/src/widgets/nes_running_text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ class _NesRunningTextState extends State<NesRunningText>
}
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
void didUpdateWidget(covariant NesRunningText oldWidget) {
super.didUpdateWidget(oldWidget);
Expand Down
118 changes: 118 additions & 0 deletions lib/src/widgets/nes_running_text_lines.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import 'package:nes_ui/nes_ui.dart';

/// {@template nes_running_text_lines}
/// A widget that displays running text, one line after the other.
/// {@endtemplate}
class NesRunningTextLines extends StatefulWidget {
/// {@macro nes_running_text_lines}
const NesRunningTextLines({
required this.texts,
this.speed = .08,
this.textStyle,
this.onEnd,
this.running = true,
this.linesAlignment = MainAxisAlignment.start,
super.key,
});

/// The texts to display.
final List<String> texts;

/// The speed of the text, in seconds.
final double speed;

/// The style of the text. When omitted, it uses the theme's bodyMedium style.
final TextStyle? textStyle;

/// Called when the text has reached the end.
final VoidCallback? onEnd;

/// Whether the text is running.
final bool running;

/// The alignment of the lines.
final MainAxisAlignment linesAlignment;

@override
State<NesRunningTextLines> createState() => _NesRunningTextLinesState();
}

class _NesRunningTextLinesState extends State<NesRunningTextLines> {
late Map<int, String> _lines;

@override
void initState() {
super.initState();

_initLines();

if (widget.running) {
_start();
}
}

@override
void didUpdateWidget(NesRunningTextLines oldWidget) {
super.didUpdateWidget(oldWidget);

if (widget.running != oldWidget.running) {
if (widget.running) {
_start();
}
}

if (widget.texts != oldWidget.texts) {
_initLines();
if (widget.running) {
_start();
}
}
}

void _start() {
setState(() {
_lines[0] = widget.texts[0];
});
}

void _initLines() {
_lines = {
for (var i = 0; i < widget.texts.length; i++) i: '',
};
}

void _next(int index) {
if (index < widget.texts.length - 1) {
setState(() {
_lines[index + 1] = widget.texts[index + 1];
});
} else {
widget.onEnd?.call();
}
}

@override
Widget build(BuildContext context) {
final texts = _lines.values;
return Column(
mainAxisAlignment: widget.linesAlignment,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (var i = 0; i < texts.length; i++)
if (_lines[i]?.isNotEmpty ?? false)
NesRunningText(
text: _lines[i] ?? '',
speed: widget.speed,
textStyle: widget.textStyle,
onEnd: () {
_next(i);
},
running: widget.running,
)
else
const Text(''),
],
);
}
}
1 change: 1 addition & 0 deletions lib/src/widgets/widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export 'nes_key_icon.dart';
export 'nes_loading_indicator.dart';
export 'nes_pressabled.dart';
export 'nes_running_text.dart';
export 'nes_running_text_lines.dart';
export 'nes_scrollbar.dart';
export 'nes_selection_list.dart';
export 'nes_single_child_scroll_view.dart';
Expand Down
129 changes: 129 additions & 0 deletions test/src/widgets/nes_running_text_lines_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:nes_ui/nes_ui.dart';

extension PumpNessRunningTextLines on WidgetTester {
Future<void> pumpRunningTextLines({
required List<String> texts,
bool running = true,
VoidCallback? onEnd,
Widget? child,
}) async {
return pumpWidget(
MaterialApp(
theme: flutterNesTheme(),
home: NesRunningTextLines(
texts: texts,
running: running,
onEnd: onEnd,
),
),
);
}
}

void main() {
group('NesButton', () {
testWidgets('renders', (tester) async {
await tester.pumpRunningTextLines(
texts: ['The Child', 'The Mandalorian'],
);

await tester.pumpAndSettle();
expect(find.text('The Child'), findsOneWidget);
expect(find.text('The Mandalorian'), findsOneWidget);
});

testWidgets('calls onEnd when finishes', (tester) async {
var called = false;
await tester.pumpRunningTextLines(
texts: ['The Child', 'The Mandalorian'],
onEnd: () => called = true,
);

await tester.pumpAndSettle();
expect(find.text('The Child'), findsOneWidget);
expect(find.text('The Mandalorian'), findsOneWidget);
expect(called, isTrue);
});

testWidgets('does not starts if playing is false', (tester) async {
await tester.pumpRunningTextLines(
texts: ['The Child', 'The Mandalorian'],
running: false,
);

await tester.pumpAndSettle();
expect(find.text('The Child'), findsNothing);
});

testWidgets('plays the text once running changes to true', (tester) async {
var running = false;
await tester.pumpWidget(
MaterialApp(
theme: flutterNesTheme(),
home: StatefulBuilder(
builder: (context, setState) {
return Column(
children: [
NesRunningTextLines(
texts: const ['The Child', 'The Mandalorian'],
running: running,
),
ElevatedButton(
onPressed: () => setState(() => running = true),
child: const Text('Start'),
),
],
);
},
),
),
);

await tester.pumpAndSettle();
expect(find.text('The Child'), findsNothing);
expect(find.text('The Mandalorian'), findsNothing);

await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
expect(find.text('The Child'), findsOneWidget);
expect(find.text('The Mandalorian'), findsOneWidget);
});

testWidgets('plays the new text when received a new one', (tester) async {
var texts = ['The Child', 'The Mandalorian'];
await tester.pumpWidget(
MaterialApp(
theme: flutterNesTheme(),
home: StatefulBuilder(
builder: (context, setState) {
return Column(
children: [
NesRunningTextLines(
texts: texts,
),
ElevatedButton(
onPressed: () => setState(
() => texts = ['Ramona', 'Salazar'],
),
child: const Text('Start'),
),
],
);
},
),
),
);

await tester.pumpAndSettle();
expect(find.text('The Child'), findsOneWidget);
expect(find.text('The Mandalorian'), findsOneWidget);

await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
expect(find.text('Ramona'), findsOneWidget);
expect(find.text('Salazar'), findsOneWidget);
});
});
}
41 changes: 41 additions & 0 deletions widgetbook/lib/widgetbook/use_cases/running_texts.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// ignore_for_file: public_member_api_docs

import 'package:flutter/material.dart';
import 'package:nes_ui/nes_ui.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;

@widgetbook.UseCase(
name: 'default',
type: NesRunningText,
)
Widget normal(BuildContext context) {
return const Center(
child: NesContainer(
width: 400,
height: 200,
child: NesRunningText(
text: 'Get ready!',
),
),
);
}

@widgetbook.UseCase(
name: 'default',
type: NesRunningTextLines,
)
Widget lines(BuildContext context) {
return const Center(
child: NesContainer(
width: 600,
height: 160,
child: NesRunningTextLines(
texts: [
'Welcome back adventurer!',
'Hope you gathered all your gear!',
'The journey is about to begin!',
],
),
),
);
}
25 changes: 20 additions & 5 deletions widgetbook/lib/widgetbook/widgetbook.directories.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
import 'package:widgetbook/widgetbook.dart' as _i1;
import 'package:widgetbook_app/widgetbook/use_cases/buttons.dart' as _i2;
import 'package:widgetbook_app/widgetbook/use_cases/checkboxes.dart' as _i3;
import 'package:widgetbook_app/widgetbook/use_cases/containers.dart' as _i4;
import 'package:widgetbook_app/widgetbook/use_cases/containers.dart' as _i5;
import 'package:widgetbook_app/widgetbook/use_cases/running_texts.dart' as _i4;

final directories = <_i1.WidgetbookNode>[
_i1.WidgetbookFolder(
Expand Down Expand Up @@ -50,6 +51,20 @@ final directories = <_i1.WidgetbookNode>[
builder: _i3.checkbox,
),
),
_i1.WidgetbookLeafComponent(
name: 'NesRunningText',
useCase: _i1.WidgetbookUseCase(
name: 'default',
builder: _i4.normal,
),
),
_i1.WidgetbookLeafComponent(
name: 'NesRunningTextLines',
useCase: _i1.WidgetbookUseCase(
name: 'default',
builder: _i4.lines,
),
),
_i1.WidgetbookFolder(
name: 'containers',
children: [
Expand All @@ -58,19 +73,19 @@ final directories = <_i1.WidgetbookNode>[
useCases: [
_i1.WidgetbookUseCase(
name: 'default',
builder: _i4.normal,
builder: _i5.normal,
),
_i1.WidgetbookUseCase(
name: 'with corner inner square painter',
builder: _i4.cornerInnerSquare,
builder: _i5.cornerInnerSquare,
),
_i1.WidgetbookUseCase(
name: 'with label',
builder: _i4.label,
builder: _i5.label,
),
_i1.WidgetbookUseCase(
name: 'with square corner painter',
builder: _i4.squareConer,
builder: _i5.squareConer,
),
],
)
Expand Down