Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## 14.15.0
- **FEAT**(layers): Add `TextLayer.box` to lay a text out in a fixed box that clips or shrinks text that does not fit; `LayerInteractionConfigs.enableTextBoxResize` shows handles that resize the box without scaling the text, and `TextEditorState.setBoxEnabled` puts the edited text into a box.

## 14.14.3
- **PERF**(import-export): History steps that leave a paint layer's strokes unchanged share its points instead of each importing a copy, so opening a long history over a large drawing no longer takes hundreds of MB and several seconds. `PaintedModel.copy` now shares `offsets` with the original.

Expand Down
7 changes: 7 additions & 0 deletions example/lib/features/layer/layer_group_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import '/features/layer/layer_grouping_example.dart';
import '/features/layer/layer_rasterizer_example.dart';
import '/features/layer/layer_select_design_example.dart';
import '/features/layer/selectable_layer_example.dart';
import '/features/layer/text_box_example.dart';

/// A [StatefulWidget] that represents the AI group page in the application.
class LayerGroupPage extends StatefulWidget {
Expand Down Expand Up @@ -44,6 +45,12 @@ class _LayerGroupPageState extends State<LayerGroupPage> {
trailing: const Icon(Icons.chevron_right),
onTap: () => _openExample(const SelectableLayerExample()),
),
ListTile(
leading: const Icon(Icons.crop_free),
title: const Text('Resizable Text Boxes'),
trailing: const Icon(Icons.chevron_right),
onTap: () => _openExample(const TextBoxExample()),
),
ListTile(
leading: const Icon(Icons.image_outlined),
title: const Text('Export Layers as PNG'),
Expand Down
115 changes: 115 additions & 0 deletions example/lib/features/layer/text_box_example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Flutter imports:
import 'package:material_ui/material_ui.dart';

// Package imports:
import 'package:pro_image_editor/pro_image_editor.dart';

// Project imports:
import '/core/constants/example_constants.dart';
import '/core/mixin/example_helper.dart';

/// A widget that demonstrates text layers in a fixed, resizable box.
///
/// The editor opens with a boxed text layer already on the image. Drag the
/// handles on its edges to resize the box: the text wraps and shrinks to fit,
/// but its size does not change. The text editor gets a "Text box" chip
/// that puts new or edited text into a box, or takes it out of one.
///
/// Example usage:
/// ```dart
/// TextBoxExample();
/// ```
class TextBoxExample extends StatefulWidget {
/// Creates a new [TextBoxExample] widget.
const TextBoxExample({super.key});

@override
State<TextBoxExample> createState() => _TextBoxExampleState();
}

/// The state for the [TextBoxExample] widget.
class _TextBoxExampleState extends State<TextBoxExample>
with ExampleHelperState<TextBoxExample> {
@override
void initState() {
super.initState();
preCacheImage(assetPath: kImageEditorExampleAssetPath);
}

void _onAfterViewInit() {
editorKey.currentState!.addLayer(
TextLayer(
text: 'Drag the handles on the edges to resize this box. '
'The text wraps and shrinks to fit.',
box: const TextLayerBox(
size: Size(260, 120),
verticalAlign: TextLayerBoxVerticalAlign.center,
overflow: TextLayerBoxOverflow.shrink,
),
),
);
}

@override
Widget build(BuildContext context) {
if (!isPreCached) return const PrepareImageWidget();

return ProImageEditor.asset(
kImageEditorExampleAssetPath,
key: editorKey,
callbacks: ProImageEditorCallbacks(
onImageEditingStarted: onImageEditingStarted,
onImageEditingComplete: onImageEditingComplete,
onCloseEditor: (editorMode) => onCloseEditor(
editorMode: editorMode,
enablePop: !isDesktopMode(context),
),
mainEditorCallbacks: MainEditorCallbacks(
onAfterViewInit: _onAfterViewInit,
helperLines: HelperLinesCallbacks(onLineHit: vibrateLineHit),
),
),
configs: ProImageEditorConfigs(
designMode: platformDesignMode,
mainEditor: MainEditorConfigs(
enableCloseButton: !isDesktopMode(context),
),
layerInteraction: const LayerInteractionConfigs(
/// Shows the selection, and with it the box handles, on mobile too.
selectable: LayerInteractionSelectable.enabled,

/// Shows handles on the edges of a text layer with a box.
enableTextBoxResize: true,
),
textEditor: TextEditorConfigs(
widgets: TextEditorWidgets(
bodyItemsOverlay: (textEditor, rebuildStream) => [
ReactiveWidget(
stream: rebuildStream,
builder: (_) => _buildBoxToggle(textEditor),
),
],
),
),
),
);
}

/// A chip that puts the text into a box, or takes it out of one.
///
/// Turning it on fits the box around the text as it is, so the layer
/// looks the same until its box is resized.
Widget _buildBoxToggle(TextEditorState textEditor) {
return Positioned(
top: 12,
left: 12,
child: FilterChip(
avatar: const Icon(Icons.crop_free),
label: const Text('Text box'),
selected: textEditor.boxEnabled,
showCheckmark: false,
onSelected: textEditor.setBoxEnabled,
),
);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'utils/custom_widgets_typedef.dart';

export '/shared/widgets/layer/interaction_helper/text_layer_box_handles.dart';
export '/shared/widgets/layer/models/layer_item_interaction.dart';

/// A class that defines a layer of interaction for custom widgets,
Expand Down
12 changes: 12 additions & 0 deletions lib/core/models/editor_configs/layer_interaction_configs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class LayerInteractionConfigs {
this.enableLayerDragSelection = true,
this.enableMobilePinchScale = true,
this.enableMobilePinchRotate = true,
this.enableTextBoxResize = false,
this.mouseButtonPrimaryAction = MouseButtonAction.selectOrSpaceMove,
this.mouseButtonSecondaryAction = MouseButtonAction.pan,
this.mouseButtonMiddleAction = MouseButtonAction.pan,
Expand Down Expand Up @@ -63,6 +64,15 @@ class LayerInteractionConfigs {
/// If `false`, pinch rotation is disabled.
final bool enableMobilePinchRotate;

/// Whether the default selection overlay shows handles on the edges of a
/// text layer with a `TextLayer.box`, which resize the box without
/// scaling its text.
///
/// A custom `LayerInteractionWidgets.overlayChildBuilder` can show its own
/// handles with `TextLayerBoxHandles` and
/// `LayerItemInteractions.textBoxResizeDown`, whatever this says.
final bool enableTextBoxResize;

/// Determines whether the video controls should be hidden when the user
/// interacts with the editor.
final bool hideVideoControlsOnInteraction;
Expand Down Expand Up @@ -129,6 +139,7 @@ class LayerInteractionConfigs {
bool? hideToolbarOnInteraction,
bool? enableMobilePinchScale,
bool? enableMobilePinchRotate,
bool? enableTextBoxResize,
bool? hideVideoControlsOnInteraction,
bool? keepSelectionOnInteraction,
bool? enableKeyboardMultiSelection,
Expand All @@ -151,6 +162,7 @@ class LayerInteractionConfigs {
enableMobilePinchScale ?? this.enableMobilePinchScale,
enableMobilePinchRotate:
enableMobilePinchRotate ?? this.enableMobilePinchRotate,
enableTextBoxResize: enableTextBoxResize ?? this.enableTextBoxResize,
hideVideoControlsOnInteraction:
hideVideoControlsOnInteraction ?? this.hideVideoControlsOnInteraction,
keepSelectionOnInteraction:
Expand Down
1 change: 1 addition & 0 deletions lib/core/models/layers/layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export 'layer_mask.dart';
export 'paint_layer.dart';
export 'text_highlight.dart';
export 'text_layer.dart';
export 'text_layer_box.dart';
export 'widget_layer.dart';

/// Represents a layer with common properties for widgets.
Expand Down
22 changes: 22 additions & 0 deletions lib/core/models/layers/text_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class TextLayer extends Layer {
this.align = TextAlign.left,
this.fontScale = 1.0,
this.maxTextWidth,
this.box,
this.outlineWidth = 0,
this.outlineColor = _defaultOutlineColor,
List<TextHighlight>? highlights,
Expand Down Expand Up @@ -170,6 +171,11 @@ class TextLayer extends Layer {
text: map[keyConverter('text')] ?? '-',
fontScale: fontScale,
maxTextWidth: tryParseDouble(map[keyConverter('maxTextWidth')]),
box: map[keyConverter('box')] is Map
? TextLayerBox.fromMap(
Map<String, dynamic>.from(map[keyConverter('box')] as Map),
)
: null,
outlineWidth: safeParseDouble(map[keyConverter('outlineWidth')]),
outlineColor: outlineColor != null
? Color(safeParseInt(outlineColor))
Expand Down Expand Up @@ -253,6 +259,14 @@ class TextLayer extends Layer {
/// null, the text will not have a width constraint.
double? maxTextWidth;

/// A fixed box the text is laid out in, or `null` for a layer that grows
/// with its text.
///
/// The text wraps at the box width instead of [maxTextWidth], and the
/// background fills the box. Set it to `null` to remove the box;
/// [copyWith] cannot clear it.
TextLayerBox? box;

/// A custom text style for the text. Be careful the editor allow not to
/// import and export this style.
///
Expand Down Expand Up @@ -406,6 +420,7 @@ class TextLayer extends Layer {
'type': 'text',
if (maxTextWidth != null)
'maxTextWidth': maxTextWidth?.roundSmart(maxDecimalPlaces),
if (box != null) 'box': box!.toMap(maxDecimalPlaces: maxDecimalPlaces),
if (customSecondaryColor) 'customSecondaryColor': customSecondaryColor,
if (highlights.isNotEmpty)
'highlights': highlights.map((h) => h.toMap()).toList(),
Expand Down Expand Up @@ -479,6 +494,10 @@ class TextLayer extends Layer {
'decoration': textStyle?.decoration.toString(),
if (paintLayer.maxTextWidth != maxTextWidth)
'maxTextWidth': maxTextWidth?.roundSmart(maxDecimalPlaces),
// `null` is written too, so a removed box does not come back from the
// previous step.
if (paintLayer.box != box)
'box': box?.toMap(maxDecimalPlaces: maxDecimalPlaces),
// An empty list is written on purpose: the import merges each entry
// over the previous state, so a missing key would restore shadows the
// user removed.
Expand Down Expand Up @@ -522,6 +541,7 @@ class TextLayer extends Layer {
double? rotation,
double? scale,
double? maxTextWidth,
TextLayerBox? box,
double? outlineWidth,
Color? outlineColor,
List<TextHighlight>? highlights,
Expand Down Expand Up @@ -558,6 +578,7 @@ class TextLayer extends Layer {
align: align ?? this.align,
fontScale: fontScale ?? this.fontScale,
maxTextWidth: maxTextWidth ?? this.maxTextWidth,
box: box ?? this.box,
outlineWidth: outlineWidth ?? this.outlineWidth,
outlineColor: outlineColor ?? this.outlineColor,
highlights: highlights ?? this.highlights,
Expand Down Expand Up @@ -600,6 +621,7 @@ class TextLayer extends Layer {
..add(EnumProperty<TextAlign>('align', align))
..add(DoubleProperty('fontScale', fontScale))
..add(DoubleProperty('maxTextWidth', maxTextWidth))
..add(DiagnosticsProperty<TextLayerBox>('box', box))
..add(DoubleProperty('outlineWidth', outlineWidth))
..add(ColorProperty('outlineColor', outlineColor))
..add(DiagnosticsProperty<TextStyle>('textStyle', textStyle))
Expand Down
Loading
Loading