refactor: quick pills
This commit is contained in:
@@ -9,6 +9,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
|
|
||||||
import 'dart:io' show Platform;
|
import 'dart:io' show Platform;
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:math' as math;
|
||||||
import '../providers/chat_providers.dart';
|
import '../providers/chat_providers.dart';
|
||||||
import '../../tools/widgets/unified_tools_modal.dart';
|
import '../../tools/widgets/unified_tools_modal.dart';
|
||||||
import '../../tools/providers/tools_providers.dart';
|
import '../../tools/providers/tools_providers.dart';
|
||||||
@@ -597,83 +598,123 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
iconSize: IconSize.large + 2.0,
|
iconSize: IconSize.large + 2.0,
|
||||||
),
|
),
|
||||||
const SizedBox(width: Spacing.xs),
|
const SizedBox(width: Spacing.xs),
|
||||||
// Quick pills: no scroll, clip text within fixed max width
|
// Quick pills: expand to full text when space allows
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
child: LayoutBuilder(
|
||||||
children: [
|
builder: (context, constraints) {
|
||||||
Expanded(
|
final double total = constraints.maxWidth;
|
||||||
flex: 2,
|
const double toolsWidth = TouchTarget.comfortable;
|
||||||
child: _buildPillButton(
|
const double gapBeforeTools = Spacing.xs;
|
||||||
|
final double gapBetweenPills = imageGenAvailable ? Spacing.xs : 0;
|
||||||
|
|
||||||
|
final double availableForPills =
|
||||||
|
math.max(0.0, total - toolsWidth - gapBeforeTools);
|
||||||
|
|
||||||
|
// Measure natural widths (text + horizontal padding)
|
||||||
|
final textStyle = AppTypography.labelStyle;
|
||||||
|
const double horizontalPadding = Spacing.md * 2;
|
||||||
|
|
||||||
|
String webLabel = AppLocalizations.of(context)!.web;
|
||||||
|
final webTp = TextPainter(
|
||||||
|
text: TextSpan(text: webLabel, style: textStyle),
|
||||||
|
maxLines: 1,
|
||||||
|
textDirection: Directionality.of(context),
|
||||||
|
)..layout();
|
||||||
|
final double webNatural = webTp.width + horizontalPadding;
|
||||||
|
|
||||||
|
double imageNatural = 0;
|
||||||
|
if (imageGenAvailable) {
|
||||||
|
final imgLabel = AppLocalizations.of(context)!.imageGen;
|
||||||
|
final imgTp = TextPainter(
|
||||||
|
text: TextSpan(text: imgLabel, style: textStyle),
|
||||||
|
maxLines: 1,
|
||||||
|
textDirection: Directionality.of(context),
|
||||||
|
)..layout();
|
||||||
|
imageNatural = imgTp.width + horizontalPadding;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> rowChildren = [];
|
||||||
|
|
||||||
|
Widget webPill = _buildPillButton(
|
||||||
icon: Platform.isIOS
|
icon: Platform.isIOS
|
||||||
? CupertinoIcons.search
|
? CupertinoIcons.search
|
||||||
: Icons.search,
|
: Icons.search,
|
||||||
label: AppLocalizations.of(
|
label: webLabel,
|
||||||
context,
|
|
||||||
)!.web,
|
|
||||||
isActive: webSearchEnabled,
|
isActive: webSearchEnabled,
|
||||||
onTap:
|
onTap: widget.enabled && !_isRecording
|
||||||
widget.enabled &&
|
|
||||||
!_isRecording
|
|
||||||
? () {
|
? () {
|
||||||
ref
|
ref.read(
|
||||||
.read(
|
webSearchEnabledProvider.notifier,
|
||||||
webSearchEnabledProvider
|
).state = !webSearchEnabled;
|
||||||
.notifier,
|
|
||||||
)
|
|
||||||
.state =
|
|
||||||
!webSearchEnabled;
|
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
),
|
);
|
||||||
),
|
|
||||||
if (imageGenAvailable) ...[
|
if (!imageGenAvailable) {
|
||||||
const SizedBox(width: Spacing.xs),
|
if (webNatural <= availableForPills) {
|
||||||
Expanded(
|
rowChildren.add(webPill);
|
||||||
flex: 3,
|
} else {
|
||||||
child: _buildPillButton(
|
rowChildren.add(Flexible(fit: FlexFit.loose, child: webPill));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Widget imagePill = _buildPillButton(
|
||||||
icon: Platform.isIOS
|
icon: Platform.isIOS
|
||||||
? CupertinoIcons.photo
|
? CupertinoIcons.photo
|
||||||
: Icons.image,
|
: Icons.image,
|
||||||
label: AppLocalizations.of(
|
label: AppLocalizations.of(context)!.imageGen,
|
||||||
context,
|
|
||||||
)!.imageGen,
|
|
||||||
isActive: imageGenEnabled,
|
isActive: imageGenEnabled,
|
||||||
onTap:
|
onTap: widget.enabled && !_isRecording
|
||||||
widget.enabled &&
|
|
||||||
!_isRecording
|
|
||||||
? () {
|
? () {
|
||||||
ref
|
ref.read(
|
||||||
.read(
|
imageGenerationEnabledProvider.notifier,
|
||||||
imageGenerationEnabledProvider
|
).state = !imageGenEnabled;
|
||||||
.notifier,
|
|
||||||
)
|
|
||||||
.state =
|
|
||||||
!imageGenEnabled;
|
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
),
|
);
|
||||||
),
|
|
||||||
],
|
final double combined = webNatural + gapBetweenPills + imageNatural;
|
||||||
const SizedBox(width: Spacing.xs),
|
if (combined <= availableForPills) {
|
||||||
_buildRoundButton(
|
// Both fit naturally
|
||||||
|
rowChildren..add(webPill)..add(const SizedBox(width: Spacing.xs))..add(imagePill);
|
||||||
|
} else if (webNatural < availableForPills) {
|
||||||
|
// Keep web natural, let image take remaining
|
||||||
|
rowChildren
|
||||||
|
..add(webPill)
|
||||||
|
..add(const SizedBox(width: Spacing.xs))
|
||||||
|
..add(Flexible(fit: FlexFit.loose, child: imagePill));
|
||||||
|
} else if (imageNatural < availableForPills) {
|
||||||
|
// Keep image natural, let web take remaining
|
||||||
|
rowChildren
|
||||||
|
..add(Flexible(fit: FlexFit.loose, child: webPill))
|
||||||
|
..add(const SizedBox(width: Spacing.xs))
|
||||||
|
..add(imagePill);
|
||||||
|
} else {
|
||||||
|
// Both too large: apportion space proportional to their natural widths
|
||||||
|
final int webFlex = math.max(1, webNatural.round());
|
||||||
|
final int imgFlex = math.max(1, imageNatural.round());
|
||||||
|
rowChildren
|
||||||
|
..add(Flexible(fit: FlexFit.loose, flex: webFlex, child: webPill))
|
||||||
|
..add(const SizedBox(width: Spacing.xs))
|
||||||
|
..add(Flexible(fit: FlexFit.loose, flex: imgFlex, child: imagePill));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append tools button at the end
|
||||||
|
rowChildren
|
||||||
|
..add(const SizedBox(width: Spacing.xs))
|
||||||
|
..add(_buildRoundButton(
|
||||||
icon: Icons.more_horiz,
|
icon: Icons.more_horiz,
|
||||||
onTap:
|
onTap: widget.enabled && !_isRecording
|
||||||
widget.enabled && !_isRecording
|
|
||||||
? _showUnifiedToolsModal
|
? _showUnifiedToolsModal
|
||||||
: null,
|
: null,
|
||||||
tooltip: AppLocalizations.of(
|
tooltip: AppLocalizations.of(context)!.tools,
|
||||||
context,
|
isActive: ref.watch(selectedToolIdsProvider).isNotEmpty ||
|
||||||
)!.tools,
|
|
||||||
isActive:
|
|
||||||
ref
|
|
||||||
.watch(
|
|
||||||
selectedToolIdsProvider,
|
|
||||||
)
|
|
||||||
.isNotEmpty ||
|
|
||||||
webSearchEnabled ||
|
webSearchEnabled ||
|
||||||
imageGenEnabled,
|
imageGenEnabled,
|
||||||
),
|
));
|
||||||
],
|
|
||||||
|
return Row(children: rowChildren);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: Spacing.xs),
|
const SizedBox(width: Spacing.xs),
|
||||||
@@ -1014,9 +1055,34 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
onTap();
|
onTap();
|
||||||
},
|
},
|
||||||
child: Container(
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final textStyle = AppTypography.labelStyle.copyWith(
|
||||||
|
color: isActive
|
||||||
|
? context.conduitTheme.buttonPrimary
|
||||||
|
: context.conduitTheme.textPrimary,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Measure natural single-line text width
|
||||||
|
final textPainter = TextPainter(
|
||||||
|
text: TextSpan(text: label, style: textStyle),
|
||||||
|
maxLines: 1,
|
||||||
|
textDirection: Directionality.of(context),
|
||||||
|
)..layout();
|
||||||
|
|
||||||
|
const double horizontalPadding = Spacing.md * 2;
|
||||||
|
final double naturalWidth = textPainter.width + horizontalPadding;
|
||||||
|
final double maxAllowed = constraints.maxWidth.isFinite
|
||||||
|
? constraints.maxWidth
|
||||||
|
: naturalWidth;
|
||||||
|
final double finalWidth = math.min(naturalWidth, maxAllowed);
|
||||||
|
final bool needsClamp = naturalWidth > maxAllowed;
|
||||||
|
|
||||||
|
final double innerTextWidth = math.max(0.0, finalWidth - horizontalPadding);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: finalWidth,
|
||||||
height: TouchTarget.comfortable, // exact height match
|
height: TouchTarget.comfortable, // exact height match
|
||||||
alignment: Alignment.center,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
|
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
// Subtle primary tint when active for clearer affordance
|
// Subtle primary tint when active for clearer affordance
|
||||||
@@ -1030,18 +1096,29 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
boxShadow: ConduitShadows.button,
|
boxShadow: ConduitShadows.button,
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
|
child: needsClamp
|
||||||
|
? SizedBox(
|
||||||
|
width: innerTextWidth,
|
||||||
child: Text(
|
child: Text(
|
||||||
label,
|
label,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
style: AppTypography.labelStyle.copyWith(
|
textAlign: TextAlign.center,
|
||||||
color: isActive
|
style: textStyle,
|
||||||
? context.conduitTheme.buttonPrimary
|
),
|
||||||
: context.conduitTheme.textPrimary,
|
)
|
||||||
),
|
: Text(
|
||||||
|
label,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
softWrap: false,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: textStyle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user