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
? CupertinoIcons.search
: Icons.search,
label: webLabel,
isActive: webSearchEnabled,
onTap: widget.enabled && !_isRecording
? () {
ref.read(
webSearchEnabledProvider.notifier,
).state = !webSearchEnabled;
}
: null,
);
if (!imageGenAvailable) {
if (webNatural <= availableForPills) {
rowChildren.add(webPill);
} else {
rowChildren.add(Flexible(fit: FlexFit.loose, child: webPill));
}
} else {
Widget imagePill = _buildPillButton(
icon: Platform.isIOS icon: Platform.isIOS
? CupertinoIcons.search ? CupertinoIcons.photo
: Icons.search, : Icons.image,
label: AppLocalizations.of( label: AppLocalizations.of(context)!.imageGen,
context, isActive: imageGenEnabled,
)!.web, onTap: widget.enabled && !_isRecording
isActive: webSearchEnabled,
onTap:
widget.enabled &&
!_isRecording
? () { ? () {
ref ref.read(
.read( imageGenerationEnabledProvider.notifier,
webSearchEnabledProvider ).state = !imageGenEnabled;
.notifier,
)
.state =
!webSearchEnabled;
} }
: null, : null,
), );
),
if (imageGenAvailable) ...[ final double combined = webNatural + gapBetweenPills + imageNatural;
const SizedBox(width: Spacing.xs), if (combined <= availableForPills) {
Expanded( // Both fit naturally
flex: 3, rowChildren..add(webPill)..add(const SizedBox(width: Spacing.xs))..add(imagePill);
child: _buildPillButton( } else if (webNatural < availableForPills) {
icon: Platform.isIOS // Keep web natural, let image take remaining
? CupertinoIcons.photo rowChildren
: Icons.image, ..add(webPill)
label: AppLocalizations.of( ..add(const SizedBox(width: Spacing.xs))
context, ..add(Flexible(fit: FlexFit.loose, child: imagePill));
)!.imageGen, } else if (imageNatural < availableForPills) {
isActive: imageGenEnabled, // Keep image natural, let web take remaining
onTap: rowChildren
widget.enabled && ..add(Flexible(fit: FlexFit.loose, child: webPill))
!_isRecording ..add(const SizedBox(width: Spacing.xs))
? () { ..add(imagePill);
ref } else {
.read( // Both too large: apportion space proportional to their natural widths
imageGenerationEnabledProvider final int webFlex = math.max(1, webNatural.round());
.notifier, final int imgFlex = math.max(1, imageNatural.round());
) rowChildren
.state = ..add(Flexible(fit: FlexFit.loose, flex: webFlex, child: webPill))
!imageGenEnabled; ..add(const SizedBox(width: Spacing.xs))
} ..add(Flexible(fit: FlexFit.loose, flex: imgFlex, child: imagePill));
: null, }
), }
),
], // Append tools button at the end
const SizedBox(width: Spacing.xs), rowChildren
_buildRoundButton( ..add(const SizedBox(width: Spacing.xs))
icon: Icons.more_horiz, ..add(_buildRoundButton(
onTap: icon: Icons.more_horiz,
widget.enabled && !_isRecording onTap: widget.enabled && !_isRecording
? _showUnifiedToolsModal ? _showUnifiedToolsModal
: null, : null,
tooltip: AppLocalizations.of( tooltip: AppLocalizations.of(context)!.tools,
context, isActive: ref.watch(selectedToolIdsProvider).isNotEmpty ||
)!.tools, webSearchEnabled ||
isActive: imageGenEnabled,
ref ));
.watch(
selectedToolIdsProvider, return Row(children: rowChildren);
) },
.isNotEmpty ||
webSearchEnabled ||
imageGenEnabled,
),
],
), ),
), ),
const SizedBox(width: Spacing.xs), const SizedBox(width: Spacing.xs),
@@ -1014,34 +1055,70 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
HapticFeedback.selectionClick(); HapticFeedback.selectionClick();
onTap(); onTap();
}, },
child: Container( child: LayoutBuilder(
height: TouchTarget.comfortable, // exact height match builder: (context, constraints) {
alignment: Alignment.center, final textStyle = AppTypography.labelStyle.copyWith(
padding: const EdgeInsets.symmetric(horizontal: Spacing.md), color: isActive
decoration: BoxDecoration( ? context.conduitTheme.buttonPrimary
// Subtle primary tint when active for clearer affordance : context.conduitTheme.textPrimary,
color: isActive );
? context.conduitTheme.buttonPrimary.withValues(
alpha: Alpha.buttonHover + 0.04, // Measure natural single-line text width
) final textPainter = TextPainter(
: context.conduitTheme.cardBackground, text: TextSpan(text: label, style: textStyle),
borderRadius: BorderRadius.circular(AppBorderRadius.xl),
// No elevation to match modal chips
boxShadow: ConduitShadows.button,
),
child: Center(
child: Text(
label,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, textDirection: Directionality.of(context),
softWrap: false, )..layout();
style: AppTypography.labelStyle.copyWith(
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
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
decoration: BoxDecoration(
// Subtle primary tint when active for clearer affordance
color: isActive color: isActive
? context.conduitTheme.buttonPrimary ? context.conduitTheme.buttonPrimary.withValues(
: context.conduitTheme.textPrimary, alpha: Alpha.buttonHover + 0.04,
)
: context.conduitTheme.cardBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.xl),
// No elevation to match modal chips
boxShadow: ConduitShadows.button,
), ),
), child: Center(
), child: needsClamp
? SizedBox(
width: innerTextWidth,
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: false,
textAlign: TextAlign.center,
style: textStyle,
),
)
: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: false,
textAlign: TextAlign.center,
style: textStyle,
),
),
);
},
), ),
), ),
); );