refactor: quick pills

This commit is contained in:
cogwheel0
2025-09-07 13:52:09 +05:30
parent c0902c1ad1
commit 9cb835861a

View File

@@ -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,
), ),
), ),
);
},
), ),
), ),
); );