diff --git a/lib/cubit/canvas_cubit.dart b/lib/cubit/canvas_cubit.dart index 0f02ba0c..925f54e1 100644 --- a/lib/cubit/canvas_cubit.dart +++ b/lib/cubit/canvas_cubit.dart @@ -32,6 +32,116 @@ class CanvasCubit extends Cubit { } } + // Toggle text shadow on/off + void toggleTextShadow(int index) { + if (index < 0 || index >= state.textItems.length) return; + + final updatedItems = List.from(state.textItems); + updatedItems[index] = updatedItems[index].copyWith( + hasShadow: !updatedItems[index].hasShadow, + ); + _updateState(textItems: updatedItems); + } + +// Change shadow color + void changeShadowColor(int index, Color color) { + if (index < 0 || index >= state.textItems.length) return; + + final updatedItems = List.from(state.textItems); + updatedItems[index] = updatedItems[index].copyWith( + hasShadow: true, // Automatically enable shadow when changing color + shadowColor: color, + ); + _updateState(textItems: updatedItems); + } + +// Change shadow blur radius + void changeShadowBlur(int index, double blurRadius) { + if (index < 0 || index >= state.textItems.length) return; + + final updatedItems = List.from(state.textItems); + updatedItems[index] = updatedItems[index].copyWith( + hasShadow: true, + shadowBlurRadius: blurRadius.clamp(0.0, 50.0), + ); + _updateState(textItems: updatedItems); + } + +// Change shadow offset + void changeShadowOffset(int index, Offset offset) { + if (index < 0 || index >= state.textItems.length) return; + + final updatedItems = List.from(state.textItems); + updatedItems[index] = updatedItems[index].copyWith( + hasShadow: true, + shadowOffset: offset, + ); + _updateState(textItems: updatedItems); + } + +// Apply preset shadow styles + void applyShadowPreset(int index, ShadowPreset preset) { + if (index < 0 || index >= state.textItems.length) return; + + final updatedItems = List.from(state.textItems); + + switch (preset) { + case ShadowPreset.soft: + updatedItems[index] = updatedItems[index].copyWith( + hasShadow: true, + shadowColor: Colors.black.withValues(alpha: 0.3), + shadowBlurRadius: 8.0, + shadowOffset: const Offset(2.0, 2.0), + ); + break; + case ShadowPreset.medium: + updatedItems[index] = updatedItems[index].copyWith( + hasShadow: true, + shadowColor: Colors.black.withValues(alpha: 0.5), + shadowBlurRadius: 6.0, + shadowOffset: const Offset(3.0, 3.0), + ); + break; + case ShadowPreset.hard: + updatedItems[index] = updatedItems[index].copyWith( + hasShadow: true, + shadowColor: Colors.black.withValues(alpha: 0.7), + shadowBlurRadius: 2.0, + shadowOffset: const Offset(4.0, 4.0), + ); + break; + case ShadowPreset.glow: + updatedItems[index] = updatedItems[index].copyWith( + hasShadow: true, + shadowColor: Colors.white.withValues(alpha: 0.8), + shadowBlurRadius: 15.0, + shadowOffset: Offset.zero, + ); + break; + case ShadowPreset.coloredGlow: + // Use the text color for the glow + final textColor = updatedItems[index].color; + updatedItems[index] = updatedItems[index].copyWith( + hasShadow: true, + shadowColor: textColor.withValues(alpha: 0.6), + shadowBlurRadius: 12.0, + shadowOffset: Offset.zero, + ); + break; + case ShadowPreset.outline: + updatedItems[index] = updatedItems[index].copyWith( + hasShadow: true, + shadowColor: Colors.black, + shadowBlurRadius: 1.0, + shadowOffset: const Offset(1.0, 1.0), + ); + break; + } + + _updateState(textItems: updatedItems); + CustomSnackbar.showSuccess('${preset.name} shadow applied'); + } + // Add to your CanvasCubit class void fillCanvas(Color fillColor) { // Create a special "fill" path that covers the entire canvas @@ -116,9 +226,13 @@ class CanvasCubit extends Cubit { fontWeight: FontWeight.normal, fontFamily: 'Roboto', isUnderlined: false, - isHighlighted: false, // Add this line - highlightColor: null, // Add this line + isHighlighted: false, + highlightColor: null, color: ColorConstants.uiWhite, + hasShadow: false, // Reset shadow + shadowColor: Colors.black, + shadowBlurRadius: 4.0, + shadowOffset: const Offset(2.0, 2.0), ); _updateState(textItems: updatedItems); } @@ -909,3 +1023,13 @@ class CanvasCubit extends Cubit { CustomSnackbar.showInfo('Last stroke undone'); } } + +// Enum for shadow presets +enum ShadowPreset { + soft, + medium, + hard, + glow, + coloredGlow, + outline, +} diff --git a/lib/models/text_item_model.dart b/lib/models/text_item_model.dart index 1557e22e..6962cf7e 100644 --- a/lib/models/text_item_model.dart +++ b/lib/models/text_item_model.dart @@ -13,7 +13,13 @@ class TextItem { final Color color; final bool isHighlighted; final Color? highlightColor; - final TextAlign textAlign; + final TextAlign textAlign; + + // Shadow properties + final bool hasShadow; + final Color shadowColor; + final double shadowBlurRadius; + final Offset shadowOffset; TextItem({ required this.text, @@ -27,7 +33,11 @@ class TextItem { required this.color, this.isHighlighted = false, this.highlightColor, - this.textAlign = TextAlign.left, + this.textAlign = TextAlign.left, + this.hasShadow = false, + this.shadowColor = Colors.black, + this.shadowBlurRadius = 4.0, + this.shadowOffset = const Offset(2.0, 2.0), }); TextItem copyWith({ @@ -42,7 +52,11 @@ class TextItem { Color? color, bool? isHighlighted, Color? highlightColor, - TextAlign? textAlign, + TextAlign? textAlign, + bool? hasShadow, + Color? shadowColor, + double? shadowBlurRadius, + Offset? shadowOffset, }) { return TextItem( text: text ?? this.text, @@ -56,7 +70,11 @@ class TextItem { color: color ?? this.color, isHighlighted: isHighlighted ?? this.isHighlighted, highlightColor: highlightColor ?? this.highlightColor, - textAlign: textAlign ?? this.textAlign, + textAlign: textAlign ?? this.textAlign, + hasShadow: hasShadow ?? this.hasShadow, + shadowColor: shadowColor ?? this.shadowColor, + shadowBlurRadius: shadowBlurRadius ?? this.shadowBlurRadius, + shadowOffset: shadowOffset ?? this.shadowOffset, ); } @@ -69,9 +87,64 @@ class TextItem { final backgroundColor = isHighlighted && highlightColor != null ? 'background-color: #${highlightColor!.toARGB32().toRadixString(16).padLeft(8, '0').substring(2)}; ' : ''; + + final textShadow = hasShadow + ? 'text-shadow: ${shadowOffset.dx}px ${shadowOffset.dy}px ${shadowBlurRadius}px #${shadowColor.toARGB32().toRadixString(16).padLeft(8, '0').substring(2)}; ' + : ''; final style = - 'font-size: ${fontSize}px; font-family: $fontFamily; color: $cssColor; font-weight: $cssFontWeight; font-style: $cssFontStyle; $textDecoration$backgroundColor'; + 'font-size: ${fontSize}px; font-family: $fontFamily; color: $cssColor; font-weight: $cssFontWeight; font-style: $cssFontStyle; $textDecoration$backgroundColor$textShadow'; return '$text'; } -} + + // Convert to map for saving + Map toJson() { + return { + 'text': text, + 'x': x, + 'y': y, + 'fontSize': fontSize, + 'fontWeight': fontWeight.index, + 'fontStyle': fontStyle.index, + 'color': color.toARGB32(), + 'fontFamily': fontFamily, + 'isUnderlined': isUnderlined, + 'textAlign': textAlign.index, + 'isHighlighted': isHighlighted, + 'highlightColor': highlightColor?.toARGB32(), + 'hasShadow': hasShadow, + 'shadowColor': shadowColor.toARGB32(), + 'shadowBlurRadius': shadowBlurRadius, + 'shadowOffsetDx': shadowOffset.dx, + 'shadowOffsetDy': shadowOffset.dy, + }; + } + + // Create from map when loading + factory TextItem.fromJson(Map json) { + return TextItem( + text: json['text'], + x: json['x'].toDouble(), + y: json['y'].toDouble(), + fontSize: json['fontSize'].toDouble(), + fontWeight: FontWeight.values[json['fontWeight']], + fontStyle: FontStyle.values[json['fontStyle']], + color: Color(json['color']), + fontFamily: json['fontFamily'], + isUnderlined: json['isUnderlined'] ?? false, + textAlign: TextAlign.values[json['textAlign'] ?? 0], + isHighlighted: json['isHighlighted'] ?? false, + highlightColor: json['highlightColor'] != null + ? Color(json['highlightColor']) + : null, + hasShadow: json['hasShadow'] ?? false, + shadowColor: json['shadowColor'] != null + ? Color(json['shadowColor']) + : Colors.black, + shadowBlurRadius: json['shadowBlurRadius']?.toDouble() ?? 4.0, + shadowOffset: json['shadowOffsetDx'] != null && json['shadowOffsetDy'] != null + ? Offset(json['shadowOffsetDx'].toDouble(), json['shadowOffsetDy'].toDouble()) + : const Offset(2.0, 2.0), + ); + } +} \ No newline at end of file diff --git a/lib/ui/widgets/editable_text_widget.dart b/lib/ui/widgets/editable_text_widget.dart index 4a053aef..5ef7e2ed 100644 --- a/lib/ui/widgets/editable_text_widget.dart +++ b/lib/ui/widgets/editable_text_widget.dart @@ -39,29 +39,39 @@ class EditableTextWidget extends StatelessWidget { context.read().editText(index, result); } }, - // 4. onDoubleTap handler is now gone - child:SizedBox( + child: SizedBox( width: MediaQuery.of(context).size.width * 0.9, child: Text( - textItem.text, - textAlign: textItem.textAlign, - style: GoogleFonts.getFont( - textItem.fontFamily, - fontStyle: textItem.fontStyle, - fontWeight: textItem.fontWeight, - fontSize: textItem.fontSize, - decoration: textItem.isUnderlined - ? TextDecoration.underline - : TextDecoration.none, - color: textItem.color, - backgroundColor: - textItem.isHighlighted && textItem.highlightColor != null - ? textItem.highlightColor - : (isSelected - ? ColorConstants.highlightYellow.withAlpha((0.3 * 255).toInt()) - : null), + textItem.text, + textAlign: textItem.textAlign, + style: GoogleFonts.getFont( + textItem.fontFamily, + fontStyle: textItem.fontStyle, + fontWeight: textItem.fontWeight, + fontSize: textItem.fontSize, + decoration: textItem.isUnderlined + ? TextDecoration.underline + : TextDecoration.none, + color: textItem.color, + backgroundColor: + textItem.isHighlighted && textItem.highlightColor != null + ? textItem.highlightColor + : (isSelected + ? ColorConstants.highlightYellow + .withValues(alpha: 0.3) + : null), + shadows: textItem.hasShadow + ? [ + Shadow( + color: textItem.shadowColor, + offset: textItem.shadowOffset, + blurRadius: textItem.shadowBlurRadius, + ), + ] + : null, + ), ), - ),) + ), ); } } @@ -108,8 +118,8 @@ class EditTextDialog extends StatelessWidget { ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), - borderSide: - const BorderSide(color: ColorConstants.dialogPurple, width: 2), + borderSide: const BorderSide( + color: ColorConstants.dialogPurple, width: 2), ), ), ), @@ -120,7 +130,7 @@ class EditTextDialog extends StatelessWidget { children: [ TextButton( onPressed: () => Navigator.pop(context, '_delete_'), - child: Text( + child: const Text( 'Remove', style: TextStyle(color: ColorConstants.gray600), ), @@ -137,7 +147,8 @@ class EditTextDialog extends StatelessWidget { } }, style: ElevatedButton.styleFrom( - backgroundColor: ColorConstants.dialogPurple.withAlpha((0.2 * 255).toInt()), + backgroundColor: + ColorConstants.dialogPurple.withValues(alpha: 0.2), foregroundColor: ColorConstants.dialogPurple, elevation: 0, padding: const EdgeInsets.symmetric( @@ -157,4 +168,4 @@ class EditTextDialog extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/lib/ui/widgets/font_controls.dart b/lib/ui/widgets/font_controls.dart index 759610e3..17266331 100644 --- a/lib/ui/widgets/font_controls.dart +++ b/lib/ui/widgets/font_controls.dart @@ -7,6 +7,7 @@ import '../../constants/font_family_list.dart'; import '../../constants/color_constants.dart'; import '../../cubit/canvas_cubit.dart'; import '../../cubit/canvas_state.dart'; +import '../widgets/shadows_controls.dart'; class FontControls extends StatelessWidget { const FontControls({super.key}); @@ -36,7 +37,9 @@ class FontControls extends StatelessWidget { const SizedBox(width: 25), _buildAlignmentControls(context), const SizedBox(width: 25), - _buildHighlightControls(context), + _buildHighlightControls(context), + const SizedBox(width: 25), + const ShadowControls(), const SizedBox(width: 25), _buildFontFamilyControls(context), const SizedBox(width: 25), @@ -75,8 +78,7 @@ class FontControls extends StatelessWidget { ), child: _buildStyleButton( icon: Icons.copy, - isSelected: - false, // This button is never in a "selected" state. + isSelected: false, onPressed: isDisabled ? null : () { @@ -114,7 +116,8 @@ class FontControls extends StatelessWidget { selectedIndex == null || selectedIndex >= state.textItems.length; final textItem = !isDisabled ? state.textItems[selectedIndex] : null; final isHighlighted = textItem?.isHighlighted ?? false; - final currentHighlightColor = textItem?.highlightColor ?? ColorConstants.highlightYellow; + final currentHighlightColor = + textItem?.highlightColor ?? ColorConstants.highlightYellow; return Row( mainAxisSize: MainAxisSize.min, @@ -324,98 +327,95 @@ class FontControls extends StatelessWidget { }, ); } - -Widget _buildAlignmentControls(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) { - // Only rebuild if selection changed or textAlign changed - final prevIndex = previous.selectedTextItemIndex; - final currIndex = current.selectedTextItemIndex; - - if (prevIndex != currIndex) return true; - - if (currIndex != null && - currIndex < previous.textItems.length && - currIndex < current.textItems.length) { - final prevItem = previous.textItems[currIndex]; - final currItem = current.textItems[currIndex]; - return prevItem.textAlign != currItem.textAlign; - } - - return false; - }, - builder: (context, state) { - final selectedIndex = state.selectedTextItemIndex; - final isDisabled = - selectedIndex == null || selectedIndex >= state.textItems.length; - - final textItem = !isDisabled ? state.textItems[selectedIndex] : null; - final currentAlign = textItem?.textAlign ?? TextAlign.left; - - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - 'Align', - style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14), - ), - const SizedBox(width: 12), - Container( - padding: const EdgeInsets.symmetric(horizontal: 4), - decoration: BoxDecoration( - color: ColorConstants.gray100, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: ColorConstants.gray300), + + Widget _buildAlignmentControls(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) { + // Only rebuild if selection changed or textAlign changed + final prevIndex = previous.selectedTextItemIndex; + final currIndex = current.selectedTextItemIndex; + + if (prevIndex != currIndex) return true; + + if (currIndex != null && + currIndex < previous.textItems.length && + currIndex < current.textItems.length) { + final prevItem = previous.textItems[currIndex]; + final currItem = current.textItems[currIndex]; + return prevItem.textAlign != currItem.textAlign; + } + + return false; + }, + builder: (context, state) { + final selectedIndex = state.selectedTextItemIndex; + final isDisabled = + selectedIndex == null || selectedIndex >= state.textItems.length; + + final textItem = !isDisabled ? state.textItems[selectedIndex] : null; + final currentAlign = textItem?.textAlign ?? TextAlign.left; + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Align', + style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14), ), - child: Row( - children: [ - _buildStyleButton( - icon: Icons.format_align_left, - isSelected: currentAlign == TextAlign.left, - onPressed: isDisabled - ? null - : () => context - .read() - .changeTextAlignment(selectedIndex, TextAlign.left), - ), - const SizedBox(width: 8), - _buildStyleButton( - icon: Icons.format_align_center, - isSelected: currentAlign == TextAlign.center, - onPressed: isDisabled - ? null - : () => context - .read() - .changeTextAlignment(selectedIndex, TextAlign.center), - ), - const SizedBox(width: 8), - _buildStyleButton( - icon: Icons.format_align_right, - isSelected: currentAlign == TextAlign.right, - onPressed: isDisabled - ? null - : () => context - .read() - .changeTextAlignment(selectedIndex, TextAlign.right), - ), - const SizedBox(width: 8), - _buildStyleButton( - icon: Icons.format_align_justify, - isSelected: currentAlign == TextAlign.justify, - onPressed: isDisabled - ? null - : () => context - .read() - .changeTextAlignment(selectedIndex, TextAlign.justify), - ), - ], + const SizedBox(width: 12), + Container( + padding: const EdgeInsets.symmetric(horizontal: 4), + decoration: BoxDecoration( + color: ColorConstants.gray100, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: ColorConstants.gray300), + ), + child: Row( + children: [ + _buildStyleButton( + icon: Icons.format_align_left, + isSelected: currentAlign == TextAlign.left, + onPressed: isDisabled + ? null + : () => context + .read() + .changeTextAlignment(selectedIndex, TextAlign.left), + ), + const SizedBox(width: 8), + _buildStyleButton( + icon: Icons.format_align_center, + isSelected: currentAlign == TextAlign.center, + onPressed: isDisabled + ? null + : () => context.read().changeTextAlignment( + selectedIndex, TextAlign.center), + ), + const SizedBox(width: 8), + _buildStyleButton( + icon: Icons.format_align_right, + isSelected: currentAlign == TextAlign.right, + onPressed: isDisabled + ? null + : () => context.read().changeTextAlignment( + selectedIndex, TextAlign.right), + ), + const SizedBox(width: 8), + _buildStyleButton( + icon: Icons.format_align_justify, + isSelected: currentAlign == TextAlign.justify, + onPressed: isDisabled + ? null + : () => context.read().changeTextAlignment( + selectedIndex, TextAlign.justify), + ), + ], + ), ), - ), - ], - ); - }, - ); -} + ], + ); + }, + ); + } Widget _buildFontFamilyControls(BuildContext context) { return BlocBuilder( @@ -556,7 +556,8 @@ Widget _buildAlignmentControls(BuildContext context) { }), //More Colors Button ^_^ IconButton( - icon: const Icon(Icons.more_horiz, color: ColorConstants.gray600), + icon: const Icon(Icons.more_horiz, + color: ColorConstants.gray600), tooltip: 'More colors', onPressed: isDisabled ? null @@ -731,7 +732,7 @@ Widget _buildAlignmentControls(BuildContext context) { }) { return Material( color: isSelected - ? ColorConstants.uiBlueAccent.withAlpha((0.2 * 255).toInt()) + ? ColorConstants.uiBlueAccent.withValues(alpha: 0.2) : ColorConstants.transparent, borderRadius: BorderRadius.circular(4), child: InkWell( @@ -742,7 +743,9 @@ Widget _buildAlignmentControls(BuildContext context) { child: Icon( icon, size: 20, - color: isSelected ? ColorConstants.uiBlueAccent : ColorConstants.gray700, + color: isSelected + ? ColorConstants.uiBlueAccent + : ColorConstants.gray700, ), ), ), @@ -823,7 +826,8 @@ class _FontPickerContentState extends State<_FontPickerContent> { style: GoogleFonts.getFont(font, fontSize: 16), ), trailing: isSelected - ? const Icon(Icons.check, color: ColorConstants.dialogButtonBlue) + ? const Icon(Icons.check, + color: ColorConstants.dialogButtonBlue) : null, onTap: () { context diff --git a/lib/ui/widgets/shadows_controls.dart b/lib/ui/widgets/shadows_controls.dart new file mode 100644 index 00000000..d85041e7 --- /dev/null +++ b/lib/ui/widgets/shadows_controls.dart @@ -0,0 +1,507 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flex_color_picker/flex_color_picker.dart'; +import '../../cubit/canvas_cubit.dart'; +import '../../cubit/canvas_state.dart'; +import '../../constants/color_constants.dart'; + +class ShadowControls extends StatelessWidget { + const ShadowControls({super.key}); + + // Shadow color palette + static const shadowColors = [ + Colors.black, + Color(0xFF424242), + Color(0xFF757575), + Color(0xFF1976D2), + Color(0xFFD32F2F), + Color(0xFF388E3C), + ]; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) { + if (previous.selectedTextItemIndex != current.selectedTextItemIndex) { + return true; + } + if (current.selectedTextItemIndex != null) { + final pItem = previous.textItems[current.selectedTextItemIndex!]; + final cItem = current.textItems[current.selectedTextItemIndex!]; + return pItem.hasShadow != cItem.hasShadow || + pItem.shadowColor != cItem.shadowColor || + pItem.shadowBlurRadius != cItem.shadowBlurRadius || + pItem.shadowOffset != cItem.shadowOffset; + } + return false; + }, + builder: (context, state) { + final selectedIndex = state.selectedTextItemIndex; + final isDisabled = + selectedIndex == null || selectedIndex >= state.textItems.length; + final textItem = !isDisabled ? state.textItems[selectedIndex] : null; + final hasShadow = textItem?.hasShadow ?? false; + final shadowColor = textItem?.shadowColor ?? Colors.black; + final shadowBlur = textItem?.shadowBlurRadius ?? 4.0; + final shadowOffset = textItem?.shadowOffset ?? const Offset(2.0, 2.0); + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Shadow', + style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14)), + const SizedBox(width: 12), + Container( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), + decoration: BoxDecoration( + color: ColorConstants.gray100, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: ColorConstants.gray300), + ), + child: Row( + children: [ + // Toggle shadow on/off + _buildStyleButton( + icon: Icons.blur_on, + isSelected: hasShadow, + onPressed: isDisabled + ? null + : () => context + .read() + .toggleTextShadow(selectedIndex), + ), + + if (hasShadow) ...[ + const SizedBox(width: 8), + // Shadow color swatches + ...shadowColors.map((color) { + final isSelected = shadowColor == color; + return GestureDetector( + onTap: isDisabled + ? null + : () => context + .read() + .changeShadowColor(selectedIndex, color), + child: Container( + width: 24, + height: 24, + margin: const EdgeInsets.symmetric(horizontal: 2), + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + border: Border.all( + color: isSelected + ? ColorConstants.gray800 + : ColorConstants.gray400, + width: isSelected ? 2 : 1, + ), + ), + ), + ); + }), + const SizedBox(width: 4), + // More colors button + _buildStyleButton( + icon: Icons.colorize, + isSelected: false, + tooltip: 'More colors', + onPressed: isDisabled + ? null + : () => _showColorPicker( + context, + selectedIndex, + shadowColor, + ), + ), + const SizedBox(width: 8), + // Shadow presets button + _buildStyleButton( + icon: Icons.auto_awesome, + isSelected: false, + tooltip: 'Shadow presets', + onPressed: isDisabled + ? null + : () => + _showShadowPresetsDialog(context, selectedIndex), + ), + const SizedBox(width: 8), + // Advanced settings button + _buildStyleButton( + icon: Icons.tune, + isSelected: false, + tooltip: 'Shadow settings', + onPressed: isDisabled + ? null + : () => _showShadowSettingsSheet( + context, + selectedIndex, + shadowColor, + shadowBlur, + shadowOffset, + ), + ), + ], + ], + ), + ), + ], + ); + }, + ); + } + + Widget _buildStyleButton({ + required IconData icon, + required bool isSelected, + required VoidCallback? onPressed, + String? tooltip, + }) { + return Tooltip( + message: tooltip ?? '', + child: Material( + color: isSelected + ? ColorConstants.gray800.withValues(alpha: 0.2) + : ColorConstants.transparent, + borderRadius: BorderRadius.circular(4), + child: InkWell( + borderRadius: BorderRadius.circular(4), + onTap: onPressed, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + icon, + size: 20, + color: isSelected + ? ColorConstants.gray800 + : ColorConstants.gray700, + ), + ), + ), + ), + ); + } + + void _showColorPicker( + BuildContext context, + int selectedIndex, + Color currentColor, + ) async { + final pickedColor = await showColorPickerDialog( + context, + currentColor, + title: const Text('Pick shadow color'), + showColorCode: true, + showRecentColors: false, + backgroundColor: ColorConstants.uiWhite, + ); + if (context.mounted) { + context.read().changeShadowColor(selectedIndex, pickedColor); + } + } + + void _showShadowPresetsDialog(BuildContext context, int selectedIndex) { + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + title: const Row( + children: [ + Icon(Icons.auto_awesome, color: ColorConstants.gray800), + SizedBox(width: 8), + Text('Shadow Presets'), + ], + ), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildPresetTile( + context, + selectedIndex, + ShadowPreset.soft, + 'Soft Shadow', + 'Light, subtle shadow', + Icons.wb_cloudy, + ), + _buildPresetTile( + context, + selectedIndex, + ShadowPreset.medium, + 'Medium Shadow', + 'Balanced shadow effect', + Icons.filter_drama, + ), + _buildPresetTile( + context, + selectedIndex, + ShadowPreset.hard, + 'Hard Shadow', + 'Strong, defined shadow', + Icons.wb_sunny, + ), + _buildPresetTile( + context, + selectedIndex, + ShadowPreset.glow, + 'White Glow', + 'Soft white glow effect', + Icons.lightbulb_outline, + ), + _buildPresetTile( + context, + selectedIndex, + ShadowPreset.coloredGlow, + 'Colored Glow', + 'Glow matching text color', + Icons.color_lens, + ), + _buildPresetTile( + context, + selectedIndex, + ShadowPreset.outline, + 'Outline', + 'Sharp text outline', + Icons.border_outer, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + style: TextButton.styleFrom( + foregroundColor: ColorConstants.gray800, + ), + child: const Text('Close'), + ), + ], + ), + ); + } + + Widget _buildPresetTile( + BuildContext context, + int selectedIndex, + ShadowPreset preset, + String title, + String subtitle, + IconData icon, + ) { + return Container( + margin: const EdgeInsets.only(bottom: 8), + decoration: BoxDecoration( + border: Border.all(color: ColorConstants.gray300), + borderRadius: BorderRadius.circular(8), + ), + child: ListTile( + leading: Icon(icon, color: ColorConstants.gray800), + title: Text(title, style: const TextStyle(fontWeight: FontWeight.w600)), + subtitle: Text(subtitle, style: const TextStyle(fontSize: 12)), + onTap: () { + context.read().applyShadowPreset(selectedIndex, preset); + Navigator.pop(context); + }, + ), + ); + } + + void _showShadowSettingsSheet( + BuildContext context, + int selectedIndex, + Color currentColor, + double currentBlur, + Offset currentOffset, + ) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: ColorConstants.dialogWhite, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (sheetContext) => BlocProvider.value( + value: context.read(), + child: BlocBuilder( + builder: (builderContext, state) { + final textItem = state.selectedTextItemIndex != null && + state.selectedTextItemIndex! < state.textItems.length + ? state.textItems[state.selectedTextItemIndex!] + : null; + + final shadowBlur = textItem?.shadowBlurRadius ?? currentBlur; + final shadowOffset = textItem?.shadowOffset ?? currentOffset; + + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(sheetContext).viewInsets.bottom, + top: 16, + left: 16, + right: 16, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Shadow Settings', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(sheetContext), + ), + ], + ), + const SizedBox(height: 20), + + // Blur radius slider + const Text('Blur Radius', + style: TextStyle(fontWeight: FontWeight.w600)), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: Slider( + value: shadowBlur, + min: 0, + max: 50, + divisions: 50, + label: shadowBlur.toStringAsFixed(1), + activeColor: ColorConstants.gray800, + onChanged: (value) { + builderContext + .read() + .changeShadowBlur(selectedIndex, value); + }, + ), + ), + SizedBox( + width: 50, + child: Text( + shadowBlur.toStringAsFixed(1), + style: const TextStyle(fontWeight: FontWeight.w500), + textAlign: TextAlign.center, + ), + ), + ], + ), + const SizedBox(height: 20), + + // Offset controls + const Text('Shadow Offset', + style: TextStyle(fontWeight: FontWeight.w600)), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Horizontal', + style: TextStyle(fontSize: 12)), + Row( + children: [ + Expanded( + child: Slider( + value: shadowOffset.dx, + min: -20, + max: 20, + divisions: 40, + label: shadowOffset.dx.toStringAsFixed(1), + activeColor: ColorConstants.gray800, + onChanged: (value) { + builderContext + .read() + .changeShadowOffset(selectedIndex, + Offset(value, shadowOffset.dy)); + }, + ), + ), + SizedBox( + width: 40, + child: Text( + shadowOffset.dx.toStringAsFixed(1), + style: const TextStyle(fontSize: 12), + textAlign: TextAlign.center, + ), + ), + ], + ), + ], + ), + ), + ], + ), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Vertical', style: TextStyle(fontSize: 12)), + Row( + children: [ + Expanded( + child: Slider( + value: shadowOffset.dy, + min: -20, + max: 20, + divisions: 40, + label: shadowOffset.dy.toStringAsFixed(1), + activeColor: ColorConstants.gray800, + onChanged: (value) { + builderContext + .read() + .changeShadowOffset(selectedIndex, + Offset(shadowOffset.dx, value)); + }, + ), + ), + SizedBox( + width: 40, + child: Text( + shadowOffset.dy.toStringAsFixed(1), + style: const TextStyle(fontSize: 12), + textAlign: TextAlign.center, + ), + ), + ], + ), + ], + ), + ), + ], + ), + const SizedBox(height: 20), + + // Close button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => Navigator.pop(sheetContext), + style: ElevatedButton.styleFrom( + backgroundColor: ColorConstants.gray800, + foregroundColor: ColorConstants.dialogWhite, + padding: const EdgeInsets.symmetric(vertical: 16), + ), + child: const Text('Done'), + ), + ), + const SizedBox(height: 16), + ], + ), + ); + }, + ), + ), + ); + } +} \ No newline at end of file