Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Need letterSpacing ,wordSpacing & text height features Like available in flutter TextStyle #1844

Open
1 task done
mrwebbeast opened this issue May 3, 2024 · 2 comments
Open
1 task done
Labels
enhancement New feature or request

Comments

@mrwebbeast
Copy link

Is there an existing issue for this?

Use case

These features like letterSpacing ,wordSpacing & text height are use full in many use cases just like google docs and i also requires some kind of features in my app ..

Thank You

Proposal

Need letterSpacing ,wordSpacing & text height features Like available in flutter TextStyle

const Text( 'Screens', style: TextStyle( color: primaryColor, fontSize: 22, fontWeight: FontWeight.w600, height: 2, letterSpacing: 5, wordSpacing: 2, ),

@mrwebbeast mrwebbeast added the enhancement New feature or request label May 3, 2024
@kairan77
Copy link

kairan77 commented May 6, 2024

This would result in breaking changes in terms of style configuration, I would argue against such a feature in the library.

My main reasons being:

  1. Now style is passed as a configuration to the editor fixed on each 'css class', making letterSpacing ,wordSpacing & text height configurable per fragment, would result in massive breaking change, it also makes dimension calculation much more difficult for rendering positions of objects such as inline embed accurately.
  2. Flutter quill is a productivity tool NOT a creation tool, it should optimize towards overall usage efficiency/productivity. Introducing productivity reduction features such as those mentioned (I would also include font size in this category) is against its design goal. Checkout apple/google's UI design guidelines, you soon realize given any specific device /screen resolution /a particular font-category ( such as heading1, heading2, body, quote etc), there is really ONLY ONE setting that is ever optimal for comfortable viewing, any deviation from this very specific optimal setting is ergonomically inefficient. These decisions should be made by coders at design time and not by users at runtime, allowing users to configure these settings in a productivity tool is wasting users' time.
  3. Quill can not aim to be a fully fledged creation tool because the underlying metadata (delta/mark down) format is not turing complete. Many features such as excel/word style tables can never be properly implemented without smuggling/encoding some sort of extensions (ie. xml, latex, or other turing complete metadata format) into the poor delta/markdown format.
  4. quill is like a hammer or chisel, you use it to chip away things, if you need a scissor for some fancy origami work, then it's probably better to find a scissor to use in the first place. makeshift scissors will never be good enough.

@CatHood0
Copy link

CatHood0 commented May 24, 2024

I can solve your problem to line spacing. I make a custom block attribute that makes this.

/// Attribute with issue

const String lineHeightKey = 'line-height';
const AttributeScope lineHeightScope = AttributeScope.block;

class LineHeightAttribute extends Attribute<String?> {
  const LineHeightAttribute({String? value = "1.0"})
      : super(
          lineHeightKey,
          lineHeightScope,
          value,
        );
}
///Custom quill toolbar button implementation

class QuillLineHeightButton extends QuillToolbarBaseValueButton<QuillLineHeightButtonOptions, QuillLineHeightButtonExtraOptions> {
  QuillLineHeightButton({
    required super.controller,
    @Deprecated('Please use the default display text from the options') this.defaultDisplayText,
    super.options = const QuillLineHeightButtonOptions(),
    super.key,
  })  : assert(options.rawItems?.isNotEmpty ?? true),
        assert(options.initialValue == null || (options.initialValue?.isNotEmpty ?? true));

  final String? defaultDisplayText;

  @override
  QuillLineHeightButtonState createState() => QuillLineHeightButtonState();
}

class QuillLineHeightButtonState extends QuillToolbarBaseValueButtonState<QuillLineHeightButton, QuillLineHeightButtonOptions,
    QuillLineHeightButtonExtraOptions, String> {
  Size? size;
  final MenuController _menuController = MenuController();

  List<String> get rawItemsMap {
    const List<String> spacings = ["1.0","1.15","1.5","2.0"];
    return spacings;
  }

  String get _defaultDisplayText {
    return options.initialValue ?? widget.options.defaultDisplayText ?? widget.defaultDisplayText ?? context.loc.fontSize;
  }

  @override
  String get currentStateValue {
    final Attribute<dynamic>? attribute = controller.getSelectionStyle().attributes[lineHeightKey];
    return attribute == null ? _defaultDisplayText : attribute.value ?? _defaultDisplayText;
  }

  @override
  String get defaultTooltip => context.loc.fontSize;

  void _onDropdownButtonPressed() {
    if (_menuController.isOpen) {
      _menuController.close();
    } else {
      _menuController.open();
    }
    afterButtonPressed?.call();
  }

  @override
  Widget build(BuildContext context) {
    size ??= MediaQuery.sizeOf(context);
    return MenuAnchor(
      controller: _menuController,
      menuChildren: rawItemsMap.map((String spacing) {
        return MenuItemButton(
          key: ValueKey<String>(spacing),
          onPressed: () {
            final String newValue = spacing;
            final attribute0 = currentValue == spacing ? const LineHeightAttribute(value: null) : LineHeightAttribute(value: newValue);
            controller.formatSelection(attribute0);
            setState(() {
              currentValue = newValue;
              options.onSelected?.call(newValue);
            });
          },
          child: SizedBox(
            height: 65,
            child: Row(
              children: [
                const Icon(Icons.format_line_spacing),
                const SizedBox(width: 5),
                Text.rich(
                  TextSpan(
                    children: [
                      TextSpan(
                          text: 'Spacing: ',
                          style: TextStyle(fontWeight: currentValue == spacing ? FontWeight.bold : FontWeight.w300)),
                      TextSpan(
                        text: spacing,
                        style: TextStyle(fontWeight: currentValue == spacing ? FontWeight.bold : FontWeight.w300),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        );
      }).toList(),
      child: Builder(
        builder: (BuildContext context) {
          final bool isMaterial3 = Theme.of(context).useMaterial3;
          if (!isMaterial3) {
            return RawMaterialButton(
              onPressed: _onDropdownButtonPressed,
              child: _buildContent(context),
            );
          }
          return QuillToolbarIconButton(
            tooltip: tooltip,
            isSelected: false,
            iconTheme: iconTheme,
            onPressed: _onDropdownButtonPressed,
            icon: _buildContent(context),
          );
        },
      ),
    );
  }

  Widget _buildContent(BuildContext context) {
    final bool hasFinalWidth = options.width != null;
    return Padding(
      padding: options.padding ?? const EdgeInsets.fromLTRB(10, 0, 0, 0),
      child: Row(
        mainAxisSize: !hasFinalWidth ? MainAxisSize.min : MainAxisSize.max,
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: <Widget>[
          UtilityWidgets.maybeWidget(
            enabled: hasFinalWidth,
            wrapper: (Widget child) => Expanded(child: child),
            child: Text(
              currentValue,
              overflow: options.labelOverflow,
              style: options.style ??
                  TextStyle(
                    fontSize: iconSize / 1.15,
                  ),
            ),
          ),
          Icon(
            Icons.arrow_drop_down,
            size: iconSize * iconButtonFactor,
          )
        ],
      ),
    );
  }
}

/// The [T] is the options for the button
/// The [E] is the extra options for the button
abstract class QuillToolbarBaseValueButton<T extends QuillToolbarBaseButtonOptions<T, E>, E extends QuillToolbarBaseButtonExtraOptions>
    extends StatefulWidget {
  const QuillToolbarBaseValueButton({required this.controller, required this.options, super.key});

  final T options;

  final QuillController controller;
}

/// The [W] is the widget that creates this State
/// The [V] is the type of the currentValue
abstract class QuillToolbarBaseValueButtonState<W extends QuillToolbarBaseValueButton<T, E>, T extends QuillToolbarBaseButtonOptions<T, E>,
    E extends QuillToolbarBaseButtonExtraOptions, V> extends State<W> {
  T get options => widget.options;

  QuillController get controller => widget.controller;

  late V currentValue;

  /// Callback to query the widget's state for the value to be assigned to currentState
  V get currentStateValue;

  @override
  void initState() {
    super.initState();
    controller.addListener(didChangeEditingValue);
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    currentValue = currentStateValue;
  }

  void didChangeEditingValue() {
    setState(() {
      currentValue = currentStateValue;
    });
  }

  @override
  void dispose() {
    controller.removeListener(didChangeEditingValue);
    super.dispose();
  }

  @override
  void didUpdateWidget(covariant W oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.controller != controller) {
      oldWidget.controller.removeListener(didChangeEditingValue);
      controller.addListener(didChangeEditingValue);
      currentValue = currentStateValue;
    }
  }

  String get defaultTooltip;

  String get tooltip {
    return options.tooltip ?? context.quillToolbarBaseButtonOptions?.tooltip ?? defaultTooltip;
  }

  double get iconSize {
    final double? baseFontSize = baseButtonExtraOptions?.iconSize;
    final double? iconSize = options.iconSize;
    return iconSize ?? baseFontSize ?? kDefaultIconSize;
  }

  double get iconButtonFactor {
    final double? baseIconFactor = baseButtonExtraOptions?.iconButtonFactor;
    final double? iconButtonFactor = options.iconButtonFactor;
    return iconButtonFactor ?? baseIconFactor ?? kDefaultIconButtonFactor;
  }

  QuillIconTheme? get iconTheme {
    return options.iconTheme ?? baseButtonExtraOptions?.iconTheme;
  }

  QuillToolbarBaseButtonOptions? get baseButtonExtraOptions {
    return context.quillToolbarBaseButtonOptions;
  }

  VoidCallback? get afterButtonPressed {
    return options.afterButtonPressed ?? baseButtonExtraOptions?.afterButtonPressed;
  }
}

class QuillLineHeightButtonExtraOptions extends QuillToolbarBaseButtonExtraOptions {
  const QuillLineHeightButtonExtraOptions({
    required super.controller,
    required this.currentValue,
    required this.defaultDisplayText,
    required super.context,
    required super.onPressed,
  });

  final String currentValue;
  final String defaultDisplayText;
}

@immutable
class QuillLineHeightButtonOptions extends QuillToolbarBaseButtonOptions<QuillLineHeightButtonOptions, QuillLineHeightButtonExtraOptions> {
  const QuillLineHeightButtonOptions({
    super.iconSize,
    super.iconButtonFactor,
    this.rawItems,
    this.onSelected,
    this.attribute = const LineHeightAttribute(value: "1.0"),
    super.afterButtonPressed,
    super.tooltip,
    this.padding,
    this.style,
    @Deprecated('No longer used') this.width,
    this.initialValue,
    this.labelOverflow = TextOverflow.visible,
    this.itemHeight,
    this.itemPadding,
    this.defaultItemColor = Colors.red,
    super.childBuilder,
    this.shape,
    this.defaultDisplayText,
  });

  final ButtonStyle? shape;

  final List<String>? rawItems;
  final ValueChanged<String>? onSelected;
  final LineHeightAttribute attribute;
  final EdgeInsetsGeometry? padding;
  final TextStyle? style;
  final double? width;
  final String? initialValue;
  final TextOverflow labelOverflow;
  @Deprecated('No longer used')
  final double? itemHeight;
  @Deprecated('No longer used')
  final EdgeInsets? itemPadding;
  final Color? defaultItemColor;
  final String? defaultDisplayText;

  QuillLineHeightButtonOptions copyWith({
    double? iconSize,
    double? iconButtonFactor,
    double? hoverElevation,
    double? highlightElevation,
    List<PopupMenuEntry<String>>? items,
    List<String>? rawItems,
    ValueChanged<String>? onSelected,
    LineHeightAttribute? attribute,
    EdgeInsetsGeometry? padding,
    TextStyle? style,
    double? width,
    String? initialValue,
    TextOverflow? labelOverflow,
    double? itemHeight,
    EdgeInsets? itemPadding,
    Color? defaultItemColor,
    VoidCallback? afterButtonPressed,
    String? tooltip,
    OutlinedBorder? shape,
    String? defaultDisplayText,
  }) {
    return QuillLineHeightButtonOptions(
      iconSize: iconSize ?? this.iconSize,
      iconButtonFactor: iconButtonFactor ?? this.iconButtonFactor,
      rawItems: rawItems ?? this.rawItems,
      onSelected: onSelected ?? this.onSelected,
      attribute: attribute ?? this.attribute,
      padding: padding ?? this.padding,
      style: style ?? this.style,
      // ignore: deprecated_member_use_from_same_package
      width: width ?? this.width,
      initialValue: initialValue ?? this.initialValue,
      labelOverflow: labelOverflow ?? this.labelOverflow,
      // ignore: deprecated_member_use_from_same_package
      itemHeight: itemHeight ?? this.itemHeight,
      // ignore: deprecated_member_use_from_same_package
      itemPadding: itemPadding ?? this.itemPadding,
      defaultItemColor: defaultItemColor ?? this.defaultItemColor,
      tooltip: tooltip ?? super.tooltip,
      afterButtonPressed: afterButtonPressed ?? super.afterButtonPressed,
      defaultDisplayText: defaultDisplayText ?? this.defaultDisplayText,
    );
  }
}

//And then just add to the QuillEditorConfigurations
customStyleBuilder: (Attribute<dynamic> attribute) {
          if (attribute.key.equals('line-height')) {
            return TextStyle(
              height: double.parse(attribute.value),
            );
          }
return TextStyle();
},

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants