refactor: chat input bar to make it more compact
This commit is contained in:
@@ -66,14 +66,6 @@ class _MicButton extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final Color borderColor = isRecording
|
|
||||||
? context.conduitTheme.buttonPrimary
|
|
||||||
: context.conduitTheme.cardBorder;
|
|
||||||
final Color bgColor = isRecording
|
|
||||||
? context.conduitTheme.buttonPrimary.withValues(
|
|
||||||
alpha: Alpha.buttonHover,
|
|
||||||
)
|
|
||||||
: context.conduitTheme.cardBackground;
|
|
||||||
final Color iconColor = isRecording
|
final Color iconColor = isRecording
|
||||||
? context.conduitTheme.buttonPrimaryText
|
? context.conduitTheme.buttonPrimaryText
|
||||||
: context.conduitTheme.textPrimary.withValues(alpha: Alpha.strong);
|
: context.conduitTheme.textPrimary.withValues(alpha: Alpha.strong);
|
||||||
@@ -82,26 +74,18 @@ class _MicButton extends StatelessWidget {
|
|||||||
message: tooltip,
|
message: tooltip,
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
shape: RoundedRectangleBorder(
|
shape: const CircleBorder(),
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.round),
|
|
||||||
side: BorderSide(color: borderColor, width: BorderWidth.regular),
|
|
||||||
),
|
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.round),
|
customBorder: const CircleBorder(),
|
||||||
onTap: onTap == null
|
onTap: onTap == null
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
onTap!();
|
onTap!();
|
||||||
},
|
},
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
width: TouchTarget.comfortable,
|
width: TouchTarget.minimum,
|
||||||
height: TouchTarget.comfortable,
|
height: TouchTarget.minimum,
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: bgColor,
|
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.round),
|
|
||||||
boxShadow: ConduitShadows.button,
|
|
||||||
),
|
|
||||||
child: Center(
|
child: Center(
|
||||||
child: isRecording
|
child: isRecording
|
||||||
? _WaveformBars(intensity: intensity, color: iconColor)
|
? _WaveformBars(intensity: intensity, color: iconColor)
|
||||||
@@ -379,8 +363,9 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!mounted || _isDeactivated) return;
|
if (!mounted || _isDeactivated) return;
|
||||||
_controller.text = incoming;
|
_controller.text = incoming;
|
||||||
_controller.selection =
|
_controller.selection = TextSelection.collapsed(
|
||||||
TextSelection.collapsed(offset: incoming.length);
|
offset: incoming.length,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
ref.read(prefilledInputTextProvider.notifier).state = null;
|
ref.read(prefilledInputTextProvider.notifier).state = null;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@@ -398,9 +383,12 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
final webSearchEnabled = ref.watch(webSearchEnabledProvider);
|
final webSearchEnabled = ref.watch(webSearchEnabledProvider);
|
||||||
final imageGenEnabled = ref.watch(imageGenerationEnabledProvider);
|
final imageGenEnabled = ref.watch(imageGenerationEnabledProvider);
|
||||||
final imageGenAvailable = ref.watch(imageGenerationAvailableProvider);
|
final imageGenAvailable = ref.watch(imageGenerationAvailableProvider);
|
||||||
final selectedQuickPills =
|
final selectedQuickPills = ref.watch(
|
||||||
ref.watch(appSettingsProvider.select((s) => s.quickPills));
|
appSettingsProvider.select((s) => s.quickPills),
|
||||||
final sendOnEnter = ref.watch(appSettingsProvider.select((s) => s.sendOnEnter));
|
);
|
||||||
|
final sendOnEnter = ref.watch(
|
||||||
|
appSettingsProvider.select((s) => s.sendOnEnter),
|
||||||
|
);
|
||||||
final toolsAsync = ref.watch(toolsListProvider);
|
final toolsAsync = ref.watch(toolsListProvider);
|
||||||
final List<Tool> availableTools = toolsAsync.maybeWhen<List<Tool>>(
|
final List<Tool> availableTools = toolsAsync.maybeWhen<List<Tool>>(
|
||||||
data: (t) => t,
|
data: (t) => t,
|
||||||
@@ -426,6 +414,27 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final bool showPlaceholder =
|
||||||
|
!_hasText && !_focusNode.hasFocus && !_isRecording;
|
||||||
|
final Brightness brightness = Theme.of(context).brightness;
|
||||||
|
final Color outlineColor = (_focusNode.hasFocus || _hasText)
|
||||||
|
? context.conduitTheme.inputBorderFocused.withValues(alpha: 0.6)
|
||||||
|
: context.conduitTheme.inputBorder.withValues(alpha: 0.7);
|
||||||
|
final Color glowColor = context.conduitTheme.inputBackground.withValues(
|
||||||
|
alpha: brightness == Brightness.dark ? 0.2 : 0.12,
|
||||||
|
);
|
||||||
|
final Color composerSurface = context.conduitTheme.inputBackground;
|
||||||
|
final Color placeholderColor = context.conduitTheme.inputPlaceholder;
|
||||||
|
final Color badgeBackground = showPlaceholder
|
||||||
|
? placeholderColor.withValues(alpha: 0.12)
|
||||||
|
: composerSurface.withValues(alpha: 0.3);
|
||||||
|
final Color badgeBorder = showPlaceholder
|
||||||
|
? Colors.transparent
|
||||||
|
: outlineColor.withValues(alpha: 0.35);
|
||||||
|
final Color badgeIconColor = showPlaceholder
|
||||||
|
? placeholderColor
|
||||||
|
: context.conduitTheme.textPrimary.withValues(alpha: 0.75);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
// Transparent wrapper so rounded corners are visible against page background
|
// Transparent wrapper so rounded corners are visible against page background
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
@@ -442,25 +451,21 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: context.conduitTheme.inputBackground,
|
color: context.conduitTheme.inputBackground,
|
||||||
borderRadius: const BorderRadius.vertical(
|
borderRadius: BorderRadius.circular(AppBorderRadius.bottomSheet),
|
||||||
top: Radius.circular(AppBorderRadius.xl),
|
|
||||||
bottom: Radius.circular(0),
|
|
||||||
),
|
|
||||||
border: Border(
|
border: Border(
|
||||||
top: BorderSide(
|
top: BorderSide(
|
||||||
color: context.conduitTheme.dividerColor,
|
color: outlineColor.withValues(alpha: 0.5),
|
||||||
width: BorderWidth.regular,
|
|
||||||
),
|
|
||||||
left: BorderSide(
|
|
||||||
color: context.conduitTheme.dividerColor,
|
|
||||||
width: BorderWidth.regular,
|
|
||||||
),
|
|
||||||
right: BorderSide(
|
|
||||||
color: context.conduitTheme.dividerColor,
|
|
||||||
width: BorderWidth.regular,
|
width: BorderWidth.regular,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
boxShadow: ConduitShadows.input,
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: glowColor,
|
||||||
|
blurRadius: 24,
|
||||||
|
spreadRadius: -16,
|
||||||
|
offset: const Offset(0, -4),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
@@ -488,48 +493,40 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// Collapsed/Expanded top row: text input with left/right buttons in collapsed
|
// Modern header row inspired by the Gemini surface
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.fromLTRB(
|
||||||
left: Spacing.inputPadding,
|
Spacing.sm,
|
||||||
right: Spacing.inputPadding,
|
Spacing.sm,
|
||||||
top: Spacing.inputPadding,
|
Spacing.sm,
|
||||||
bottom: Spacing.inputPadding,
|
Spacing.sm,
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: composerSurface,
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
AppBorderRadius.large,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Spacing.md,
|
||||||
|
vertical: Spacing.xs,
|
||||||
),
|
),
|
||||||
child: GestureDetector(
|
|
||||||
// Defer taps to the TextField so it can gain focus immediately.
|
|
||||||
// This prevents the first tap from being consumed by the wrapper,
|
|
||||||
// which previously opened the keyboard without focusing the field.
|
|
||||||
behavior: HitTestBehavior.deferToChild,
|
|
||||||
onTap: () {
|
|
||||||
if (!_isExpanded && widget.enabled) {
|
|
||||||
_pendingFocusAfterExpand = true;
|
|
||||||
_setExpanded(true);
|
|
||||||
// Defer focus until AnimatedSize finishes changing layout
|
|
||||||
// to avoid IME/client race conditions.
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (!_isExpanded) ...[
|
|
||||||
_buildRoundButton(
|
|
||||||
icon: Icons.add,
|
|
||||||
onTap: widget.enabled
|
|
||||||
? _showAttachmentOptions
|
|
||||||
: null,
|
|
||||||
tooltip: AppLocalizations.of(
|
|
||||||
context,
|
|
||||||
)!.addAttachment,
|
|
||||||
showBackground: false,
|
|
||||||
iconSize: IconSize.large + 2.0,
|
|
||||||
),
|
|
||||||
const SizedBox(width: Spacing.xs),
|
|
||||||
] else ...[
|
|
||||||
SizedBox(width: Spacing.xs),
|
|
||||||
],
|
|
||||||
// Text input expands to fill
|
|
||||||
Expanded(
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: () {
|
||||||
|
if (!widget.enabled) return;
|
||||||
|
if (!_isExpanded) {
|
||||||
|
_pendingFocusAfterExpand = true;
|
||||||
|
_setExpanded(true);
|
||||||
|
} else {
|
||||||
|
_ensureFocusedIfEnabled();
|
||||||
|
}
|
||||||
|
},
|
||||||
child: Semantics(
|
child: Semantics(
|
||||||
textField: true,
|
textField: true,
|
||||||
label: AppLocalizations.of(
|
label: AppLocalizations.of(
|
||||||
@@ -541,24 +538,43 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
child: Shortcuts(
|
child: Shortcuts(
|
||||||
shortcuts: () {
|
shortcuts: () {
|
||||||
final map = <LogicalKeySet, Intent>{
|
final map = <LogicalKeySet, Intent>{
|
||||||
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.enter): const _SendMessageIntent(),
|
LogicalKeySet(
|
||||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.enter): const _SendMessageIntent(),
|
LogicalKeyboardKey.meta,
|
||||||
|
LogicalKeyboardKey.enter,
|
||||||
|
): const _SendMessageIntent(),
|
||||||
|
LogicalKeySet(
|
||||||
|
LogicalKeyboardKey.control,
|
||||||
|
LogicalKeyboardKey.enter,
|
||||||
|
): const _SendMessageIntent(),
|
||||||
};
|
};
|
||||||
if (sendOnEnter) {
|
if (sendOnEnter) {
|
||||||
map[LogicalKeySet(LogicalKeyboardKey.enter)] = const _SendMessageIntent();
|
map[LogicalKeySet(
|
||||||
map[LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.enter)] = const _InsertNewlineIntent();
|
LogicalKeyboardKey.enter,
|
||||||
|
)] =
|
||||||
|
const _SendMessageIntent();
|
||||||
|
map[LogicalKeySet(
|
||||||
|
LogicalKeyboardKey.shift,
|
||||||
|
LogicalKeyboardKey.enter,
|
||||||
|
)] =
|
||||||
|
const _InsertNewlineIntent();
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}(),
|
}(),
|
||||||
child: Actions(
|
child: Actions(
|
||||||
actions: <Type, Action<Intent>>{
|
actions: <Type, Action<Intent>>{
|
||||||
_SendMessageIntent: CallbackAction<_SendMessageIntent>(
|
_SendMessageIntent:
|
||||||
|
CallbackAction<
|
||||||
|
_SendMessageIntent
|
||||||
|
>(
|
||||||
onInvoke: (intent) {
|
onInvoke: (intent) {
|
||||||
_sendMessage();
|
_sendMessage();
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_InsertNewlineIntent: CallbackAction<_InsertNewlineIntent>(
|
_InsertNewlineIntent:
|
||||||
|
CallbackAction<
|
||||||
|
_InsertNewlineIntent
|
||||||
|
>(
|
||||||
onInvoke: (intent) {
|
onInvoke: (intent) {
|
||||||
_insertNewline();
|
_insertNewline();
|
||||||
return null;
|
return null;
|
||||||
@@ -571,18 +587,26 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
enabled: widget.enabled,
|
enabled: widget.enabled,
|
||||||
autofocus: false,
|
autofocus: false,
|
||||||
maxLines: _isExpanded ? null : 1,
|
maxLines: _isExpanded ? null : 1,
|
||||||
keyboardType: TextInputType.multiline,
|
keyboardType:
|
||||||
|
TextInputType.multiline,
|
||||||
textCapitalization:
|
textCapitalization:
|
||||||
TextCapitalization.sentences,
|
TextCapitalization.sentences,
|
||||||
textInputAction: sendOnEnter
|
textInputAction: sendOnEnter
|
||||||
? TextInputAction.send
|
? TextInputAction.send
|
||||||
: TextInputAction.newline,
|
: TextInputAction.newline,
|
||||||
showCursor: true,
|
showCursor: true,
|
||||||
scrollPadding: const EdgeInsets.only(bottom: 80),
|
scrollPadding:
|
||||||
keyboardAppearance: Theme.of(context).brightness,
|
const EdgeInsets.only(
|
||||||
cursorColor:
|
bottom: 80,
|
||||||
context.conduitTheme.inputText,
|
),
|
||||||
style: AppTypography.chatMessageStyle
|
keyboardAppearance: Theme.of(
|
||||||
|
context,
|
||||||
|
).brightness,
|
||||||
|
cursorColor: context
|
||||||
|
.conduitTheme
|
||||||
|
.inputText,
|
||||||
|
style: AppTypography
|
||||||
|
.chatMessageStyle
|
||||||
.copyWith(
|
.copyWith(
|
||||||
color: _isRecording
|
color: _isRecording
|
||||||
? context
|
? context
|
||||||
@@ -603,10 +627,9 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
context,
|
context,
|
||||||
)!.messageHintText,
|
)!.messageHintText,
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
color: context
|
color: placeholderColor,
|
||||||
.conduitTheme
|
fontSize:
|
||||||
.inputPlaceholder,
|
AppTypography.bodyLarge,
|
||||||
fontSize: AppTypography.bodyLarge,
|
|
||||||
fontWeight: _isRecording
|
fontWeight: _isRecording
|
||||||
? FontWeight.w500
|
? FontWeight.w500
|
||||||
: FontWeight.w400,
|
: FontWeight.w400,
|
||||||
@@ -614,39 +637,39 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
? FontStyle.italic
|
? FontStyle.italic
|
||||||
: FontStyle.normal,
|
: FontStyle.normal,
|
||||||
),
|
),
|
||||||
// Ensure the text field background matches its parent container
|
|
||||||
// and does not use the global InputDecorationTheme fill
|
|
||||||
filled: false,
|
filled: false,
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
enabledBorder: InputBorder.none,
|
enabledBorder: InputBorder.none,
|
||||||
focusedBorder: InputBorder.none,
|
focusedBorder: InputBorder.none,
|
||||||
errorBorder: InputBorder.none,
|
errorBorder: InputBorder.none,
|
||||||
disabledBorder: InputBorder.none,
|
disabledBorder:
|
||||||
|
InputBorder.none,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
alignLabelWithHint: true,
|
alignLabelWithHint: true,
|
||||||
),
|
),
|
||||||
// Send on Enter when enabled; otherwise keep newline behavior
|
|
||||||
onSubmitted: (_) {
|
onSubmitted: (_) {
|
||||||
if (sendOnEnter) _sendMessage();
|
if (sendOnEnter) {
|
||||||
|
_sendMessage();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (!widget.enabled) return;
|
if (!widget.enabled) return;
|
||||||
if (!_isExpanded) {
|
if (!_isExpanded) {
|
||||||
_pendingFocusAfterExpand = true;
|
_pendingFocusAfterExpand =
|
||||||
|
true;
|
||||||
_setExpanded(true);
|
_setExpanded(true);
|
||||||
// Fallback in case animation is skipped
|
|
||||||
WidgetsBinding.instance
|
WidgetsBinding.instance
|
||||||
.addPostFrameCallback((_) {
|
.addPostFrameCallback((
|
||||||
|
_,
|
||||||
|
) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (_pendingFocusAfterExpand) {
|
if (_pendingFocusAfterExpand) {
|
||||||
_ensureFocusedIfEnabled();
|
_ensureFocusedIfEnabled();
|
||||||
// Focus alone should bring up IME.
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
_ensureFocusedIfEnabled();
|
_ensureFocusedIfEnabled();
|
||||||
// Focus alone should bring up IME.
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -654,27 +677,36 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
if (!_isExpanded) ...[
|
if (!_isExpanded) ...[
|
||||||
const SizedBox(width: Spacing.sm),
|
const SizedBox(width: Spacing.sm),
|
||||||
// Primary action button (Send/Stop) when collapsed
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (voiceAvailable) ...[
|
||||||
|
_buildVoiceButton(voiceAvailable),
|
||||||
|
const SizedBox(width: Spacing.xs),
|
||||||
|
],
|
||||||
_buildPrimaryButton(
|
_buildPrimaryButton(
|
||||||
_hasText,
|
_hasText,
|
||||||
isGenerating,
|
isGenerating,
|
||||||
stopGeneration,
|
stopGeneration,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Expanded bottom row with additional options
|
// Expanded bottom row with additional options
|
||||||
if (_isExpanded) ...[
|
if (_isExpanded) ...[
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
left: Spacing.inputPadding,
|
left: Spacing.inputPadding,
|
||||||
right: Spacing.inputPadding,
|
right: Spacing.inputPadding,
|
||||||
bottom: Spacing.inputPadding,
|
top: Spacing.xs,
|
||||||
|
bottom: Spacing.sm,
|
||||||
),
|
),
|
||||||
child: FadeTransition(
|
child: FadeTransition(
|
||||||
opacity: _expandController,
|
opacity: _expandController,
|
||||||
@@ -696,64 +728,112 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final double total = constraints.maxWidth;
|
final double total =
|
||||||
final bool showImage = imageGenAvailable && showImagePillPref;
|
constraints.maxWidth;
|
||||||
|
final bool showImage =
|
||||||
|
imageGenAvailable &&
|
||||||
|
showImagePillPref;
|
||||||
final bool showWeb = showWebPill;
|
final bool showWeb = showWebPill;
|
||||||
// Tools button is always shown
|
// Tools button is always shown
|
||||||
final double toolsWidth = TouchTarget.comfortable;
|
final double toolsWidth =
|
||||||
final double gapBeforeTools = Spacing.xs;
|
TouchTarget.minimum;
|
||||||
|
final double gapBeforeTools =
|
||||||
|
Spacing.xs;
|
||||||
|
|
||||||
final double availableForPills =
|
final double availableForPills = math
|
||||||
math.max(0.0, total - toolsWidth - gapBeforeTools);
|
.max(
|
||||||
|
0.0,
|
||||||
|
total -
|
||||||
|
toolsWidth -
|
||||||
|
gapBeforeTools,
|
||||||
|
);
|
||||||
|
|
||||||
// Compose selected pill entries in order
|
// Compose selected pill entries in order
|
||||||
final List<Map<String, dynamic>> entries = [];
|
final List<Map<String, dynamic>>
|
||||||
final textStyle = AppTypography.labelStyle;
|
entries = [];
|
||||||
const double horizontalPadding = Spacing.md * 2;
|
final textStyle =
|
||||||
|
AppTypography.labelStyle;
|
||||||
|
const double horizontalPadding =
|
||||||
|
Spacing.md * 2;
|
||||||
|
|
||||||
for (final id in selectedQuickPills) {
|
for (final id in selectedQuickPills) {
|
||||||
if (id == 'web' && showWeb) {
|
if (id == 'web' && showWeb) {
|
||||||
final lbl = AppLocalizations.of(context)!.web;
|
final lbl = AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
)!.web;
|
||||||
final tp = TextPainter(
|
final tp = TextPainter(
|
||||||
text: TextSpan(text: lbl, style: textStyle),
|
text: TextSpan(
|
||||||
|
text: lbl,
|
||||||
|
style: textStyle,
|
||||||
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
textDirection: Directionality.of(context),
|
textDirection:
|
||||||
|
Directionality.of(context),
|
||||||
)..layout();
|
)..layout();
|
||||||
entries.add({
|
entries.add({
|
||||||
'id': id,
|
'id': id,
|
||||||
'label': lbl,
|
'label': lbl,
|
||||||
'width': tp.width + horizontalPadding,
|
'width':
|
||||||
|
tp.width +
|
||||||
|
horizontalPadding,
|
||||||
'widgetBuilder': () => _buildPillButton(
|
'widgetBuilder': () => _buildPillButton(
|
||||||
icon: Platform.isIOS ? CupertinoIcons.search : Icons.search,
|
icon: Platform.isIOS
|
||||||
|
? CupertinoIcons.search
|
||||||
|
: Icons.search,
|
||||||
label: lbl,
|
label: lbl,
|
||||||
isActive: webSearchEnabled,
|
isActive: webSearchEnabled,
|
||||||
onTap: widget.enabled && !_isRecording
|
onTap:
|
||||||
|
widget.enabled &&
|
||||||
|
!_isRecording
|
||||||
? () {
|
? () {
|
||||||
ref.read(webSearchEnabledProvider.notifier).state = !webSearchEnabled;
|
ref
|
||||||
|
.read(
|
||||||
|
webSearchEnabledProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.state =
|
||||||
|
!webSearchEnabled;
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
} else if (id == 'image' && showImage) {
|
} else if (id == 'image' &&
|
||||||
final lbl = AppLocalizations.of(context)!.imageGen;
|
showImage) {
|
||||||
|
final lbl = AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
)!.imageGen;
|
||||||
final tp = TextPainter(
|
final tp = TextPainter(
|
||||||
text: TextSpan(text: lbl, style: textStyle),
|
text: TextSpan(
|
||||||
|
text: lbl,
|
||||||
|
style: textStyle,
|
||||||
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
textDirection: Directionality.of(context),
|
textDirection:
|
||||||
|
Directionality.of(context),
|
||||||
)..layout();
|
)..layout();
|
||||||
entries.add({
|
entries.add({
|
||||||
'id': id,
|
'id': id,
|
||||||
'label': lbl,
|
'label': lbl,
|
||||||
'width': tp.width + horizontalPadding,
|
'width':
|
||||||
|
tp.width +
|
||||||
|
horizontalPadding,
|
||||||
'widgetBuilder': () => _buildPillButton(
|
'widgetBuilder': () => _buildPillButton(
|
||||||
icon: Platform.isIOS ? CupertinoIcons.photo : Icons.image,
|
icon: Platform.isIOS
|
||||||
|
? CupertinoIcons.photo
|
||||||
|
: Icons.image,
|
||||||
label: lbl,
|
label: lbl,
|
||||||
isActive: imageGenEnabled,
|
isActive: imageGenEnabled,
|
||||||
onTap: widget.enabled && !_isRecording
|
onTap:
|
||||||
|
widget.enabled &&
|
||||||
|
!_isRecording
|
||||||
? () {
|
? () {
|
||||||
ref
|
ref
|
||||||
.read(imageGenerationEnabledProvider.notifier)
|
.read(
|
||||||
.state = !imageGenEnabled;
|
imageGenerationEnabledProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.state =
|
||||||
|
!imageGenEnabled;
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@@ -762,35 +842,68 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
// Tool ID from server
|
// Tool ID from server
|
||||||
Tool? tool;
|
Tool? tool;
|
||||||
for (final t in availableTools) {
|
for (final t in availableTools) {
|
||||||
if (t.id == id) { tool = t; break; }
|
if (t.id == id) {
|
||||||
|
tool = t;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (tool != null) {
|
if (tool != null) {
|
||||||
final lbl = tool.name;
|
final lbl = tool.name;
|
||||||
final tp = TextPainter(
|
final tp = TextPainter(
|
||||||
text: TextSpan(text: lbl, style: textStyle),
|
text: TextSpan(
|
||||||
|
text: lbl,
|
||||||
|
style: textStyle,
|
||||||
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
textDirection: Directionality.of(context),
|
textDirection:
|
||||||
|
Directionality.of(
|
||||||
|
context,
|
||||||
|
),
|
||||||
)..layout();
|
)..layout();
|
||||||
final selectedIds = ref.watch(selectedToolIdsProvider);
|
final selectedIds = ref.watch(
|
||||||
final isActive = selectedIds.contains(id);
|
selectedToolIdsProvider,
|
||||||
|
);
|
||||||
|
final isActive = selectedIds
|
||||||
|
.contains(id);
|
||||||
entries.add({
|
entries.add({
|
||||||
'id': id,
|
'id': id,
|
||||||
'label': lbl,
|
'label': lbl,
|
||||||
'width': tp.width + horizontalPadding,
|
'width':
|
||||||
|
tp.width +
|
||||||
|
horizontalPadding,
|
||||||
'widgetBuilder': () => _buildPillButton(
|
'widgetBuilder': () => _buildPillButton(
|
||||||
icon: Icons.extension,
|
icon: Platform.isIOS
|
||||||
|
? CupertinoIcons.wrench
|
||||||
|
: Icons.build,
|
||||||
label: lbl,
|
label: lbl,
|
||||||
isActive: isActive,
|
isActive: isActive,
|
||||||
onTap: widget.enabled && !_isRecording
|
onTap:
|
||||||
|
widget.enabled &&
|
||||||
|
!_isRecording
|
||||||
? () {
|
? () {
|
||||||
final current = List<String>.from(
|
final current =
|
||||||
ref.read(selectedToolIdsProvider));
|
List<
|
||||||
if (current.contains(id)) {
|
String
|
||||||
current.remove(id);
|
>.from(
|
||||||
|
ref.read(
|
||||||
|
selectedToolIdsProvider,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (current
|
||||||
|
.contains(id)) {
|
||||||
|
current.remove(
|
||||||
|
id,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
current.add(id);
|
current.add(id);
|
||||||
}
|
}
|
||||||
ref.read(selectedToolIdsProvider.notifier).state = current;
|
ref
|
||||||
|
.read(
|
||||||
|
selectedToolIdsProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.state =
|
||||||
|
current;
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@@ -805,12 +918,18 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
// no quick pills, will just show tools later
|
// no quick pills, will just show tools later
|
||||||
} else if (entries.length == 1) {
|
} else if (entries.length == 1) {
|
||||||
final e = entries.first;
|
final e = entries.first;
|
||||||
final pill = e['widgetBuilder']() as Widget;
|
final pill =
|
||||||
|
e['widgetBuilder']() as Widget;
|
||||||
final w = (e['width'] as double);
|
final w = (e['width'] as double);
|
||||||
if (w <= availableForPills) {
|
if (w <= availableForPills) {
|
||||||
rowChildren.add(pill);
|
rowChildren.add(pill);
|
||||||
} else {
|
} else {
|
||||||
rowChildren.add(Flexible(fit: FlexFit.loose, child: pill));
|
rowChildren.add(
|
||||||
|
Flexible(
|
||||||
|
fit: FlexFit.loose,
|
||||||
|
child: pill,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// up to 2 based on settings enforcement; if more, take first 2
|
// up to 2 based on settings enforcement; if more, take first 2
|
||||||
@@ -818,126 +937,122 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
final e2 = entries[1];
|
final e2 = entries[1];
|
||||||
final w1 = (e1['width'] as double);
|
final w1 = (e1['width'] as double);
|
||||||
final w2 = (e2['width'] as double);
|
final w2 = (e2['width'] as double);
|
||||||
const double gapBetweenPills = Spacing.xs;
|
const double gapBetweenPills =
|
||||||
final combined = w1 + gapBetweenPills + w2;
|
Spacing.xs;
|
||||||
final pill1 = e1['widgetBuilder']() as Widget;
|
final combined =
|
||||||
final pill2 = e2['widgetBuilder']() as Widget;
|
w1 + gapBetweenPills + w2;
|
||||||
|
final pill1 =
|
||||||
|
e1['widgetBuilder']() as Widget;
|
||||||
|
final pill2 =
|
||||||
|
e2['widgetBuilder']() as Widget;
|
||||||
|
|
||||||
if (combined <= availableForPills) {
|
if (combined <= availableForPills) {
|
||||||
rowChildren
|
rowChildren
|
||||||
..add(pill1)
|
..add(pill1)
|
||||||
..add(const SizedBox(width: Spacing.xs))
|
..add(
|
||||||
|
const SizedBox(
|
||||||
|
width: Spacing.xs,
|
||||||
|
),
|
||||||
|
)
|
||||||
..add(pill2);
|
..add(pill2);
|
||||||
} else if (w1 < availableForPills) {
|
} else if (w1 < availableForPills) {
|
||||||
rowChildren
|
rowChildren
|
||||||
..add(pill1)
|
..add(pill1)
|
||||||
..add(const SizedBox(width: Spacing.xs))
|
..add(
|
||||||
..add(Flexible(fit: FlexFit.loose, child: pill2));
|
const SizedBox(
|
||||||
|
width: Spacing.xs,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..add(
|
||||||
|
Flexible(
|
||||||
|
fit: FlexFit.loose,
|
||||||
|
child: pill2,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else if (w2 < availableForPills) {
|
} else if (w2 < availableForPills) {
|
||||||
rowChildren
|
rowChildren
|
||||||
..add(Flexible(fit: FlexFit.loose, child: pill1))
|
..add(
|
||||||
..add(const SizedBox(width: Spacing.xs))
|
Flexible(
|
||||||
|
fit: FlexFit.loose,
|
||||||
|
child: pill1,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..add(
|
||||||
|
const SizedBox(
|
||||||
|
width: Spacing.xs,
|
||||||
|
),
|
||||||
|
)
|
||||||
..add(pill2);
|
..add(pill2);
|
||||||
} else {
|
} else {
|
||||||
final int f1 = math.max(1, w1.round());
|
final int f1 = math.max(
|
||||||
final int f2 = math.max(1, w2.round());
|
1,
|
||||||
|
w1.round(),
|
||||||
|
);
|
||||||
|
final int f2 = math.max(
|
||||||
|
1,
|
||||||
|
w2.round(),
|
||||||
|
);
|
||||||
rowChildren
|
rowChildren
|
||||||
..add(Flexible(fit: FlexFit.loose, flex: f1, child: pill1))
|
..add(
|
||||||
..add(const SizedBox(width: Spacing.xs))
|
Flexible(
|
||||||
..add(Flexible(fit: FlexFit.loose, flex: f2, child: pill2));
|
fit: FlexFit.loose,
|
||||||
|
flex: f1,
|
||||||
|
child: pill1,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..add(
|
||||||
|
const SizedBox(
|
||||||
|
width: Spacing.xs,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..add(
|
||||||
|
Flexible(
|
||||||
|
fit: FlexFit.loose,
|
||||||
|
flex: f2,
|
||||||
|
child: pill2,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append tools button at the end (always visible)
|
// Append tools button at the end (always visible)
|
||||||
rowChildren
|
|
||||||
..add(const SizedBox(width: Spacing.xs))
|
rowChildren..add(
|
||||||
..add(_buildRoundButton(
|
_buildIconButton(
|
||||||
icon: Icons.more_horiz,
|
icon: Platform.isIOS
|
||||||
onTap: widget.enabled && !_isRecording
|
? CupertinoIcons.wrench
|
||||||
|
: Icons.build,
|
||||||
|
onTap:
|
||||||
|
widget.enabled &&
|
||||||
|
!_isRecording
|
||||||
? _showUnifiedToolsModal
|
? _showUnifiedToolsModal
|
||||||
: null,
|
: null,
|
||||||
tooltip: AppLocalizations.of(context)!.tools,
|
tooltip: AppLocalizations.of(
|
||||||
isActive: ref.watch(selectedToolIdsProvider).isNotEmpty ||
|
context,
|
||||||
|
)!.tools,
|
||||||
|
isActive:
|
||||||
|
ref
|
||||||
|
.watch(
|
||||||
|
selectedToolIdsProvider,
|
||||||
|
)
|
||||||
|
.isNotEmpty ||
|
||||||
webSearchEnabled ||
|
webSearchEnabled ||
|
||||||
imageGenEnabled,
|
imageGenEnabled,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return Row(children: rowChildren);
|
return Row(children: rowChildren);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: Spacing.xs),
|
const SizedBox(width: Spacing.sm),
|
||||||
// Mic + Send cluster pinned to the right
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// Microphone button: inline voice input toggle with animated intensity ring
|
if (voiceAvailable) ...[
|
||||||
Builder(
|
_buildVoiceButton(voiceAvailable),
|
||||||
builder: (context) {
|
|
||||||
const double buttonSize =
|
|
||||||
TouchTarget.comfortable;
|
|
||||||
final double t = _isRecording
|
|
||||||
? (_intensity.clamp(0, 10) /
|
|
||||||
10.0)
|
|
||||||
: 0.0;
|
|
||||||
final double ringMaxExtra = 16.0;
|
|
||||||
final double ringSize =
|
|
||||||
buttonSize + (ringMaxExtra * t);
|
|
||||||
final double ringOpacity =
|
|
||||||
0.15 + (0.35 * t);
|
|
||||||
|
|
||||||
return SizedBox(
|
|
||||||
width: buttonSize,
|
|
||||||
height: buttonSize,
|
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
children: [
|
|
||||||
AnimatedContainer(
|
|
||||||
duration: const Duration(
|
|
||||||
milliseconds: 120,
|
|
||||||
),
|
|
||||||
width: ringSize,
|
|
||||||
height: ringSize,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: context
|
|
||||||
.conduitTheme
|
|
||||||
.buttonPrimary
|
|
||||||
.withValues(
|
|
||||||
alpha: ringOpacity,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Transform.scale(
|
|
||||||
scale: _isRecording
|
|
||||||
? 1.0 +
|
|
||||||
(_intensity.clamp(
|
|
||||||
0,
|
|
||||||
10,
|
|
||||||
) /
|
|
||||||
200)
|
|
||||||
: 1.0,
|
|
||||||
child: _MicButton(
|
|
||||||
isRecording: _isRecording,
|
|
||||||
intensity: _intensity,
|
|
||||||
onTap:
|
|
||||||
(widget.enabled &&
|
|
||||||
voiceAvailable)
|
|
||||||
? _toggleVoice
|
|
||||||
: null,
|
|
||||||
tooltip:
|
|
||||||
AppLocalizations.of(
|
|
||||||
context,
|
|
||||||
)!.voiceInput,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(width: Spacing.xs),
|
const SizedBox(width: Spacing.xs),
|
||||||
// Primary action button (Send/Stop) when expanded
|
],
|
||||||
_buildPrimaryButton(
|
_buildPrimaryButton(
|
||||||
_hasText,
|
_hasText,
|
||||||
isGenerating,
|
isGenerating,
|
||||||
@@ -975,7 +1090,6 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
tooltip: 'Test On-Device STT',
|
tooltip: 'Test On-Device STT',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
// removed duplicate send button; now only in right cluster
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -994,13 +1108,90 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildVoiceButton(bool voiceAvailable) {
|
||||||
|
if (!voiceAvailable) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return Builder(
|
||||||
|
builder: (context) {
|
||||||
|
const double buttonSize = TouchTarget.minimum;
|
||||||
|
final double t = _isRecording ? (_intensity.clamp(0, 10) / 10.0) : 0.0;
|
||||||
|
final double ringMaxExtra = 16.0;
|
||||||
|
final double ringSize = buttonSize + (ringMaxExtra * t);
|
||||||
|
final double ringOpacity = _isRecording ? 0.15 + (0.35 * t) : 0.0;
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: buttonSize,
|
||||||
|
height: buttonSize,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 120),
|
||||||
|
width: ringSize,
|
||||||
|
height: ringSize,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: context.conduitTheme.buttonPrimary.withValues(
|
||||||
|
alpha: ringOpacity,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Transform.scale(
|
||||||
|
scale: _isRecording
|
||||||
|
? 1.0 + (_intensity.clamp(0, 10) / 200)
|
||||||
|
: 1.0,
|
||||||
|
child: _MicButton(
|
||||||
|
isRecording: _isRecording,
|
||||||
|
intensity: _intensity,
|
||||||
|
onTap: (widget.enabled && voiceAvailable)
|
||||||
|
? _toggleVoice
|
||||||
|
: null,
|
||||||
|
tooltip: AppLocalizations.of(context)!.voiceInput,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildIconButton({
|
||||||
|
required IconData icon,
|
||||||
|
required VoidCallback? onTap,
|
||||||
|
required String tooltip,
|
||||||
|
bool isActive = false,
|
||||||
|
}) {
|
||||||
|
final Color iconColor = widget.enabled
|
||||||
|
? (isActive
|
||||||
|
? context.conduitTheme.buttonPrimary
|
||||||
|
: context.conduitTheme.textPrimary.withValues(
|
||||||
|
alpha: Alpha.strong,
|
||||||
|
))
|
||||||
|
: context.conduitTheme.textPrimary.withValues(alpha: Alpha.disabled);
|
||||||
|
return Tooltip(
|
||||||
|
message: tooltip,
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: onTap,
|
||||||
|
padding: const EdgeInsets.all(Spacing.xs),
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: TouchTarget.minimum,
|
||||||
|
minHeight: TouchTarget.minimum,
|
||||||
|
),
|
||||||
|
splashRadius: TouchTarget.minimum / 2,
|
||||||
|
icon: Icon(icon, color: iconColor, size: IconSize.medium),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildPrimaryButton(
|
Widget _buildPrimaryButton(
|
||||||
bool hasText,
|
bool hasText,
|
||||||
bool isGenerating,
|
bool isGenerating,
|
||||||
void Function() stopGeneration,
|
void Function() stopGeneration,
|
||||||
) {
|
) {
|
||||||
// Spec: 48px touch target, circular radius, md icon size
|
// Compact 44px touch target, circular radius, md icon size
|
||||||
const double buttonSize = TouchTarget.comfortable; // 48.0
|
const double buttonSize = TouchTarget.minimum; // 44.0
|
||||||
const double radius = AppBorderRadius.round; // big to ensure circle
|
const double radius = AppBorderRadius.round; // big to ensure circle
|
||||||
|
|
||||||
final enabled = !isGenerating && hasText && widget.enabled;
|
final enabled = !isGenerating && hasText && widget.enabled;
|
||||||
@@ -1147,8 +1338,8 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
onTap();
|
onTap();
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
width: TouchTarget.comfortable,
|
width: TouchTarget.minimum,
|
||||||
height: TouchTarget.comfortable,
|
height: TouchTarget.minimum,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isActive
|
color: isActive
|
||||||
? context.conduitTheme.buttonPrimary
|
? context.conduitTheme.buttonPrimary
|
||||||
@@ -1227,7 +1418,10 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
final double finalWidth = math.min(naturalWidth, maxAllowed);
|
final double finalWidth = math.min(naturalWidth, maxAllowed);
|
||||||
final bool needsClamp = naturalWidth > maxAllowed;
|
final bool needsClamp = naturalWidth > maxAllowed;
|
||||||
|
|
||||||
final double innerTextWidth = math.max(0.0, finalWidth - horizontalPadding);
|
final double innerTextWidth = math.max(
|
||||||
|
0.0,
|
||||||
|
finalWidth - horizontalPadding,
|
||||||
|
);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: finalWidth,
|
width: finalWidth,
|
||||||
|
|||||||
Reference in New Issue
Block a user