diff --git a/web/repertory/lib/widgets/app_dropdown.dart b/web/repertory/lib/widgets/app_dropdown.dart new file mode 100644 index 00000000..80e000da --- /dev/null +++ b/web/repertory/lib/widgets/app_dropdown.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import 'package:repertory/constants.dart' as constants; +import 'package:repertory/helpers.dart'; + +class AppDropdownFormField extends StatelessWidget { + const AppDropdownFormField({ + super.key, + required this.values, + required this.labelOf, + this.value, + this.onChanged, + this.validator, + this.labelText, + this.prefixIcon, + this.enabled = true, + this.constrainToIntrinsic = false, + this.widthMultiplier = 1.0, + this.maxWidth, + this.isExpanded = false, + this.dropdownColor, + this.textStyle, + this.contentPadding, + this.fillColor, + }); + + final List values; + final String Function(T value) labelOf; + final T? value; + final ValueChanged? onChanged; + final FormFieldValidator? validator; + final String? labelText; + final IconData? prefixIcon; + final bool enabled; + final bool constrainToIntrinsic; + final double widthMultiplier; + final double? maxWidth; + final bool isExpanded; + final Color? dropdownColor; + final TextStyle? textStyle; + final EdgeInsetsGeometry? contentPadding; + + final Color? fillColor; + + double _measureTextWidth( + BuildContext context, + String text, + TextStyle? style, + ) { + final tp = TextPainter( + text: TextSpan(text: text, style: style), + maxLines: 1, + textDirection: Directionality.of(context), + )..layout(); + return tp.width; + } + + double? _computedMaxWidth(BuildContext context) { + if (!constrainToIntrinsic) return maxWidth; + + final theme = Theme.of(context); + final scheme = theme.colorScheme; + final effectiveStyle = + textStyle ?? + theme.textTheme.bodyMedium?.copyWith( + color: scheme.onSurface.withValues(alpha: 0.96), + ); + + final longest = values.isEmpty + ? '' + : values + .map((v) => labelOf(v)) + .reduce((a, b) => a.length >= b.length ? a : b); + + final labelW = _measureTextWidth(context, longest, effectiveStyle); + + final prefixW = prefixIcon == null ? 0.0 : 48.0; + const arrowW = 32.0; + final pad = contentPadding ?? const EdgeInsets.all(constants.paddingSmall); + final padW = (pad is EdgeInsets) + ? (pad.left + pad.right) + : constants.paddingSmall * 2; + + final base = labelW + prefixW + arrowW + padW; + + final cap = + maxWidth ?? (MediaQuery.of(context).size.width - constants.padding * 2); + return (base * widthMultiplier).clamp(0.0, cap); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scheme = theme.colorScheme; + + final effectiveFill = + fillColor ?? scheme.primary.withValues(alpha: constants.primaryAlpha); + + final effectiveTextStyle = + textStyle ?? + theme.textTheme.bodyMedium?.copyWith( + color: scheme.onSurface.withValues(alpha: 0.96), + ); + + final items = values.map((v) { + return DropdownMenuItem( + value: v, + child: Text( + labelOf(v), + style: effectiveTextStyle, + overflow: TextOverflow.ellipsis, + ), + ); + }).toList(); + + final field = DropdownButtonFormField( + decoration: createCommonDecoration(scheme, labelText ?? ""), + dropdownColor: dropdownColor ?? effectiveFill, + iconEnabledColor: scheme.onSurface.withValues(alpha: 0.90), + initialValue: value, + isExpanded: isExpanded, + items: items, + onChanged: enabled ? onChanged : null, + style: effectiveTextStyle, + validator: validator, + ); + + final maxW = _computedMaxWidth(context); + final wrapped = maxW == null + ? field + : ConstrainedBox( + constraints: BoxConstraints(maxWidth: maxW), + child: field, + ); + + return Align( + alignment: Alignment.centerLeft, + heightFactor: 1.0, + child: wrapped, + ); + } +}