Line data Source code
1 : // Copyright 2014 The Flutter Authors.
2 : // Copyright 2021 Suragch.
3 : // All rights reserved.
4 : // Use of this source code is governed by a BSD-style license that can be
5 : // found in the LICENSE file.
6 :
7 : import 'dart:math' as math;
8 :
9 : import 'package:flutter/foundation.dart' show listEquals;
10 : import 'package:flutter/material.dart' show Icons, Material, MaterialType, IconButton;
11 : import 'package:flutter/rendering.dart';
12 : import 'package:flutter/widgets.dart';
13 :
14 : import 'mongol_text_selection_toolbar_layout_delegate.dart';
15 :
16 : // Minimal padding from all edges of the selection toolbar to all edges of the
17 : // viewport.
18 : const double _kToolbarScreenPadding = 8.0;
19 : const double _kToolbarWidth = 44.0;
20 :
21 : /// A fully-functional Material-style text selection toolbar.
22 : ///
23 : /// Tries to position itself to the left of [anchorLeft], but if it doesn't fit,
24 : /// then it positions itself to the right of [anchorRight].
25 : ///
26 : /// If any children don't fit in the menu, an overflow menu will automatically
27 : /// be created.
28 : ///
29 : /// See also:
30 : ///
31 : /// * [MongolTextSelectionControls.buildToolbar], where this is used by default to
32 : /// build an Android-style toolbar.
33 : class MongolTextSelectionToolbar extends StatelessWidget {
34 : /// Creates an instance of MongolTextSelectionToolbar.
35 1 : const MongolTextSelectionToolbar({
36 : Key? key,
37 : required this.anchorLeft,
38 : required this.anchorRight,
39 : this.toolbarBuilder = _defaultToolbarBuilder,
40 : required this.children,
41 2 : }) : assert(children.length > 0),
42 1 : super(key: key);
43 :
44 : /// The focal point to the left of which the toolbar attempts to position
45 : /// itself.
46 : ///
47 : /// If there is not enough room to the left before reaching the left of the
48 : /// screen, then the toolbar will position itself to the right of
49 : /// [anchorRight].
50 : final Offset anchorLeft;
51 :
52 : /// The focal point to the right of which the toolbar attempts to position
53 : /// itself, if it doesn't fit to the left of [anchorLeft].
54 : final Offset anchorRight;
55 :
56 : /// The children that will be displayed in the text selection toolbar.
57 : ///
58 : /// Typically these are buttons.
59 : ///
60 : /// Must not be empty.
61 : ///
62 : /// See also:
63 : /// * [MongolTextSelectionToolbarButton], which builds a toolbar button.
64 : final List<Widget> children;
65 :
66 : /// Builds the toolbar container.
67 : ///
68 : /// Useful for customizing the high-level background of the toolbar. The given
69 : /// child Widget will contain all of the [children].
70 : final ToolbarBuilder toolbarBuilder;
71 :
72 : // Build the default text selection menu toolbar.
73 1 : static Widget _defaultToolbarBuilder(BuildContext context, Widget child) {
74 1 : return _TextSelectionToolbarContainer(
75 : child: child,
76 : );
77 : }
78 :
79 1 : @override
80 : Widget build(BuildContext context) {
81 : final paddingLeft =
82 4 : MediaQuery.of(context).padding.left + _kToolbarScreenPadding;
83 3 : final availableWidth = anchorLeft.dx - paddingLeft;
84 1 : final fitsLeft = _kToolbarWidth <= availableWidth;
85 1 : final localAdjustment = Offset(paddingLeft, _kToolbarScreenPadding);
86 :
87 1 : return Padding(
88 1 : padding: EdgeInsets.fromLTRB(
89 : paddingLeft,
90 : _kToolbarScreenPadding,
91 : _kToolbarScreenPadding,
92 : _kToolbarScreenPadding,
93 : ),
94 1 : child: Stack(
95 1 : children: <Widget>[
96 1 : CustomSingleChildLayout(
97 1 : delegate: MongolTextSelectionToolbarLayoutDelegate(
98 2 : anchorLeft: anchorLeft - localAdjustment,
99 2 : anchorRight: anchorRight - localAdjustment,
100 : fitsLeft: fitsLeft,
101 : ),
102 1 : child: _TextSelectionToolbarOverflowable(
103 : isLeft: fitsLeft,
104 1 : toolbarBuilder: toolbarBuilder,
105 1 : children: children,
106 : ),
107 : ),
108 : ],
109 : ),
110 : );
111 : }
112 : }
113 :
114 : // A toolbar containing the given children. If they overflow the height
115 : // available, then the overflowing children will be displayed in an overflow
116 : // menu.
117 : class _TextSelectionToolbarOverflowable extends StatefulWidget {
118 1 : const _TextSelectionToolbarOverflowable({
119 : Key? key,
120 : required this.isLeft,
121 : required this.toolbarBuilder,
122 : required this.children,
123 2 : }) : assert(children.length > 0),
124 1 : super(key: key);
125 :
126 : final List<Widget> children;
127 :
128 : // When true, the toolbar fits to the left of its anchor and will be
129 : // positioned there.
130 : final bool isLeft;
131 :
132 : // Builds the toolbar that will be populated with the children and fit inside
133 : // of the layout that adjusts to overflow.
134 : final ToolbarBuilder toolbarBuilder;
135 :
136 1 : @override
137 : _TextSelectionToolbarOverflowableState createState() =>
138 1 : _TextSelectionToolbarOverflowableState();
139 : }
140 :
141 : class _TextSelectionToolbarOverflowableState
142 : extends State<_TextSelectionToolbarOverflowable>
143 : with TickerProviderStateMixin {
144 : // Whether or not the overflow menu is open. When it is closed, the menu
145 : // items that don't overflow are shown. When it is open, only the overflowing
146 : // menu items are shown.
147 : bool _overflowOpen = false;
148 :
149 : // The key for _TextSelectionToolbarTrailingEdgeAlign.
150 : UniqueKey _containerKey = UniqueKey();
151 :
152 : // Close the menu and reset layout calculations, as in when the menu has
153 : // changed and saved values are no longer relevant. This should be called in
154 : // setState or another context where a rebuild is happening.
155 0 : void _reset() {
156 : // Change _TextSelectionToolbarTrailingEdgeAlign's key when the menu changes in
157 : // order to cause it to rebuild. This lets it recalculate its
158 : // saved height for the new set of children, and it prevents AnimatedSize
159 : // from animating the size change.
160 0 : _containerKey = UniqueKey();
161 : // If the menu items change, make sure the overflow menu is closed. This
162 : // prevents getting into a broken state where _overflowOpen is true when
163 : // there are not enough children to cause overflow.
164 0 : _overflowOpen = false;
165 : }
166 :
167 0 : @override
168 : void didUpdateWidget(_TextSelectionToolbarOverflowable oldWidget) {
169 0 : super.didUpdateWidget(oldWidget);
170 : // If the children are changing at all, the current page should be reset.
171 0 : if (!listEquals(widget.children, oldWidget.children)) {
172 0 : _reset();
173 : }
174 : }
175 :
176 1 : @override
177 : Widget build(BuildContext context) {
178 1 : return _TextSelectionToolbarTrailingEdgeAlign(
179 1 : key: _containerKey,
180 1 : overflowOpen: _overflowOpen,
181 1 : child: AnimatedSize(
182 : vsync: this,
183 : // This duration was eyeballed on a Pixel 2 emulator running Android
184 : // API 28.
185 : duration: const Duration(milliseconds: 140),
186 3 : child: widget.toolbarBuilder(
187 : context,
188 1 : _TextSelectionToolbarItemsLayout(
189 2 : isLeft: widget.isLeft,
190 1 : overflowOpen: _overflowOpen,
191 1 : children: <Widget>[
192 1 : _TextSelectionToolbarOverflowButton(
193 : icon:
194 2 : Icon(_overflowOpen ? Icons.arrow_back : Icons.more_horiz),
195 0 : onPressed: () {
196 0 : setState(() {
197 0 : _overflowOpen = !_overflowOpen;
198 : });
199 : },
200 : ),
201 2 : ...widget.children,
202 : ],
203 : )),
204 : ),
205 : );
206 : }
207 : }
208 :
209 : // When the overflow menu is open, it tries to align its trailing edge to the
210 : // trailing edge of the closed menu. This widget handles this effect by
211 : // measuring and maintaining the height of the closed menu and aligning the child
212 : // to that side.
213 : class _TextSelectionToolbarTrailingEdgeAlign
214 : extends SingleChildRenderObjectWidget {
215 1 : const _TextSelectionToolbarTrailingEdgeAlign({
216 : Key? key,
217 : required Widget child,
218 : required this.overflowOpen,
219 1 : }) : super(key: key, child: child);
220 :
221 : final bool overflowOpen;
222 :
223 1 : @override
224 : _TextSelectionToolbarTrailingEdgeAlignRenderBox createRenderObject(
225 : BuildContext context) {
226 1 : return _TextSelectionToolbarTrailingEdgeAlignRenderBox(
227 1 : overflowOpen: overflowOpen,
228 : );
229 : }
230 :
231 0 : @override
232 : void updateRenderObject(BuildContext context,
233 : _TextSelectionToolbarTrailingEdgeAlignRenderBox renderObject) {
234 0 : renderObject.overflowOpen = overflowOpen;
235 : }
236 : }
237 :
238 : class _TextSelectionToolbarTrailingEdgeAlignRenderBox extends RenderProxyBox {
239 1 : _TextSelectionToolbarTrailingEdgeAlignRenderBox({
240 : required bool overflowOpen,
241 : }) : _overflowOpen = overflowOpen,
242 1 : super();
243 :
244 : // The height of the menu when it was closed. This is used to achieve the
245 : // behavior where the open menu aligns its trailing edge to the closed menu's
246 : // trailing edge.
247 : double? _closedHeight;
248 :
249 : bool _overflowOpen;
250 2 : bool get overflowOpen => _overflowOpen;
251 0 : set overflowOpen(bool value) {
252 0 : if (value == overflowOpen) {
253 : return;
254 : }
255 0 : _overflowOpen = value;
256 0 : markNeedsLayout();
257 : }
258 :
259 1 : @override
260 : void performLayout() {
261 4 : child!.layout(constraints.loosen(), parentUsesSize: true);
262 :
263 : // Save the height when the menu is closed. If the menu changes, this height
264 : // is invalid, so it's important that this RenderBox be recreated in that
265 : // case. Currently, this is achieved by providing a new key to
266 : // _TextSelectionToolbarTrailingEdgeAlign.
267 2 : if (!overflowOpen && _closedHeight == null) {
268 4 : _closedHeight = child!.size.height;
269 : }
270 :
271 4 : size = constraints.constrain(Size(
272 3 : child!.size.width,
273 : // If the open menu is higher than the closed menu, just use its own height
274 : // and don't worry about aligning the trailing edges.
275 : // _closedHeight is used even when the menu is closed to allow it to
276 : // animate its size while keeping the same edge alignment.
277 6 : _closedHeight == null || child!.size.height > _closedHeight!
278 0 : ? child!.size.height
279 1 : : _closedHeight!,
280 : ));
281 :
282 : // Set the offset in the parent data such that the child will be aligned to
283 : // the trailing edge.
284 2 : final childParentData = child!.parentData! as ToolbarItemsParentData;
285 2 : childParentData.offset = Offset(
286 : 0.0,
287 6 : size.height - child!.size.height,
288 : );
289 : }
290 :
291 : // Paint at the offset set in the parent data.
292 1 : @override
293 : void paint(PaintingContext context, Offset offset) {
294 2 : final childParentData = child!.parentData! as ToolbarItemsParentData;
295 4 : context.paintChild(child!, childParentData.offset + offset);
296 : }
297 :
298 : // Include the parent data offset in the hit test.
299 0 : @override
300 : bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
301 : // The x, y parameters have the top left of the node's box as the origin.
302 0 : final childParentData = child!.parentData! as ToolbarItemsParentData;
303 0 : return result.addWithPaintOffset(
304 0 : offset: childParentData.offset,
305 : position: position,
306 0 : hitTest: (BoxHitTestResult result, Offset transformed) {
307 0 : assert(transformed == position - childParentData.offset);
308 0 : return child!.hitTest(result, position: transformed);
309 : },
310 : );
311 : }
312 :
313 1 : @override
314 : void setupParentData(RenderBox child) {
315 2 : if (child.parentData is! ToolbarItemsParentData) {
316 2 : child.parentData = ToolbarItemsParentData();
317 : }
318 : }
319 :
320 1 : @override
321 : void applyPaintTransform(RenderObject child, Matrix4 transform) {
322 1 : final childParentData = child.parentData! as ToolbarItemsParentData;
323 5 : transform.translate(childParentData.offset.dx, childParentData.offset.dy);
324 1 : super.applyPaintTransform(child, transform);
325 : }
326 : }
327 :
328 : // Renders the menu items in the correct positions in the menu and its overflow
329 : // submenu based on calculating which item would first overflow.
330 : class _TextSelectionToolbarItemsLayout extends MultiChildRenderObjectWidget {
331 1 : _TextSelectionToolbarItemsLayout({
332 : Key? key,
333 : required this.isLeft,
334 : required this.overflowOpen,
335 : required List<Widget> children,
336 1 : }) : super(key: key, children: children);
337 :
338 : final bool isLeft;
339 : final bool overflowOpen;
340 :
341 1 : @override
342 : _RenderTextSelectionToolbarItemsLayout createRenderObject(
343 : BuildContext context) {
344 1 : return _RenderTextSelectionToolbarItemsLayout(
345 1 : isLeft: isLeft,
346 1 : overflowOpen: overflowOpen,
347 : );
348 : }
349 :
350 0 : @override
351 : void updateRenderObject(BuildContext context,
352 : _RenderTextSelectionToolbarItemsLayout renderObject) {
353 : renderObject
354 0 : ..isLeft = isLeft
355 0 : ..overflowOpen = overflowOpen;
356 : }
357 :
358 1 : @override
359 : _TextSelectionToolbarItemsLayoutElement createElement() =>
360 1 : _TextSelectionToolbarItemsLayoutElement(this);
361 : }
362 :
363 : class _TextSelectionToolbarItemsLayoutElement
364 : extends MultiChildRenderObjectElement {
365 1 : _TextSelectionToolbarItemsLayoutElement(
366 : MultiChildRenderObjectWidget widget,
367 1 : ) : super(widget);
368 :
369 1 : static bool _shouldPaint(Element child) {
370 2 : return (child.renderObject!.parentData! as ToolbarItemsParentData)
371 1 : .shouldPaint;
372 : }
373 :
374 1 : @override
375 : void debugVisitOnstageChildren(ElementVisitor visitor) {
376 3 : children.where(_shouldPaint).forEach(visitor);
377 : }
378 : }
379 :
380 : class _RenderTextSelectionToolbarItemsLayout extends RenderBox
381 : with ContainerRenderObjectMixin<RenderBox, ToolbarItemsParentData> {
382 1 : _RenderTextSelectionToolbarItemsLayout({
383 : required bool isLeft,
384 : required bool overflowOpen,
385 : }) : _isLeft = isLeft,
386 : _overflowOpen = overflowOpen,
387 1 : super();
388 :
389 : // The index of the last item that doesn't overflow.
390 : int _lastIndexThatFits = -1;
391 :
392 : bool _isLeft;
393 0 : bool get isLeft => _isLeft;
394 0 : set isLeft(bool value) {
395 0 : if (value == isLeft) {
396 : return;
397 : }
398 0 : _isLeft = value;
399 0 : markNeedsLayout();
400 : }
401 :
402 : bool _overflowOpen;
403 2 : bool get overflowOpen => _overflowOpen;
404 0 : set overflowOpen(bool value) {
405 0 : if (value == overflowOpen) {
406 : return;
407 : }
408 0 : _overflowOpen = value;
409 0 : markNeedsLayout();
410 : }
411 :
412 : // Layout the necessary children, and figure out where the children first
413 : // overflow, if at all.
414 1 : void _layoutChildren() {
415 : // When overflow is not open, the toolbar is always a specific width.
416 1 : final sizedConstraints = _overflowOpen
417 0 : ? constraints
418 2 : : BoxConstraints.loose(Size(
419 : _kToolbarWidth,
420 2 : constraints.maxHeight,
421 : ));
422 :
423 1 : var i = -1;
424 : var height = 0.0;
425 2 : visitChildren((RenderObject renderObjectChild) {
426 1 : i++;
427 :
428 : // No need to layout children inside the overflow menu when it's closed.
429 : // The opposite is not true. It is necessary to layout the children that
430 : // don't overflow when the overflow menu is open in order to calculate
431 : // _lastIndexThatFits.
432 3 : if (_lastIndexThatFits != -1 && !overflowOpen) {
433 : return;
434 : }
435 :
436 : final child = renderObjectChild as RenderBox;
437 2 : child.layout(sizedConstraints.loosen(), parentUsesSize: true);
438 3 : height += child.size.height;
439 :
440 2 : if (height > sizedConstraints.maxHeight && _lastIndexThatFits == -1) {
441 0 : _lastIndexThatFits = i - 1;
442 : }
443 : });
444 :
445 : // If the last child overflows, but only because of the height of the
446 : // overflow button, then just show it and hide the overflow button.
447 1 : final navButton = firstChild!;
448 3 : if (_lastIndexThatFits != -1 &&
449 0 : _lastIndexThatFits == childCount - 2 &&
450 0 : height - navButton.size.height <= sizedConstraints.maxHeight) {
451 0 : _lastIndexThatFits = -1;
452 : }
453 : }
454 :
455 : // Returns true when the child should be painted, false otherwise.
456 1 : bool _shouldPaintChild(RenderObject renderObjectChild, int index) {
457 : // Paint the navButton when there is overflow.
458 2 : if (renderObjectChild == firstChild) {
459 3 : return _lastIndexThatFits != -1;
460 : }
461 :
462 : // If there is no overflow, all children besides the navButton are painted.
463 3 : if (_lastIndexThatFits == -1) {
464 : return true;
465 : }
466 :
467 : // When there is overflow, paint if the child is in the part of the menu
468 : // that is currently open. Overflowing children are painted when the
469 : // overflow menu is open, and the children that fit are painted when the
470 : // overflow menu is closed.
471 0 : return (index > _lastIndexThatFits) == overflowOpen;
472 : }
473 :
474 : // Decide which children will be painted, set their shouldPaint, and set the
475 : // offset that painted children will be placed at.
476 1 : void _placeChildren() {
477 1 : var i = -1;
478 : var nextSize = const Size(0.0, 0.0);
479 : var fitHeight = 0.0;
480 1 : final navButton = firstChild!;
481 1 : var overflowWidth = overflowOpen && !isLeft ? navButton.size.width : 0.0;
482 2 : visitChildren((RenderObject renderObjectChild) {
483 1 : i++;
484 :
485 : final child = renderObjectChild as RenderBox;
486 1 : final childParentData = child.parentData! as ToolbarItemsParentData;
487 :
488 : // Handle placing the navigation button after iterating all children.
489 1 : if (renderObjectChild == navButton) {
490 : return;
491 : }
492 :
493 : // There is no need to place children that won't be painted.
494 1 : if (!_shouldPaintChild(renderObjectChild, i)) {
495 0 : childParentData.shouldPaint = false;
496 : return;
497 : }
498 1 : childParentData.shouldPaint = true;
499 :
500 1 : if (!overflowOpen) {
501 2 : childParentData.offset = Offset(0.0, fitHeight);
502 3 : fitHeight += child.size.height;
503 1 : nextSize = Size(
504 4 : math.max(child.size.width, nextSize.width),
505 : fitHeight,
506 : );
507 : } else {
508 0 : childParentData.offset = Offset(overflowWidth, 0.0);
509 0 : overflowWidth += child.size.width;
510 0 : nextSize = Size(
511 : overflowWidth,
512 0 : math.max(child.size.height, nextSize.height),
513 : );
514 : }
515 : });
516 :
517 : // Place the navigation button if needed.
518 1 : final navButtonParentData = navButton.parentData! as ToolbarItemsParentData;
519 2 : if (_shouldPaintChild(firstChild!, 0)) {
520 0 : navButtonParentData.shouldPaint = true;
521 0 : if (overflowOpen) {
522 0 : navButtonParentData.offset =
523 0 : isLeft ? Offset(overflowWidth, 0.0) : Offset.zero;
524 0 : nextSize = Size(
525 0 : isLeft ? nextSize.width + navButton.size.width : nextSize.width,
526 0 : nextSize.height,
527 : );
528 : } else {
529 0 : navButtonParentData.offset = Offset(0.0, fitHeight);
530 : nextSize =
531 0 : Size(nextSize.width, nextSize.height + navButton.size.height);
532 : }
533 : } else {
534 1 : navButtonParentData.shouldPaint = false;
535 : }
536 :
537 1 : size = nextSize;
538 : }
539 :
540 1 : @override
541 : void performLayout() {
542 2 : _lastIndexThatFits = -1;
543 1 : if (firstChild == null) {
544 0 : size = constraints.smallest;
545 : return;
546 : }
547 :
548 1 : _layoutChildren();
549 1 : _placeChildren();
550 : }
551 :
552 1 : @override
553 : void paint(PaintingContext context, Offset offset) {
554 2 : visitChildren((RenderObject renderObjectChild) {
555 : final child = renderObjectChild as RenderBox;
556 1 : final childParentData = child.parentData! as ToolbarItemsParentData;
557 1 : if (!childParentData.shouldPaint) {
558 : return;
559 : }
560 :
561 3 : context.paintChild(child, childParentData.offset + offset);
562 : });
563 : }
564 :
565 1 : @override
566 : void setupParentData(RenderBox child) {
567 2 : if (child.parentData is! ToolbarItemsParentData) {
568 2 : child.parentData = ToolbarItemsParentData();
569 : }
570 : }
571 :
572 0 : @override
573 : bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
574 : // The x, y parameters have the top left of the node's box as the origin.
575 0 : var child = lastChild;
576 : while (child != null) {
577 0 : final childParentData = child.parentData! as ToolbarItemsParentData;
578 :
579 : // Don't hit test children aren't shown.
580 0 : if (!childParentData.shouldPaint) {
581 0 : child = childParentData.previousSibling;
582 : continue;
583 : }
584 :
585 0 : final isHit = result.addWithPaintOffset(
586 0 : offset: childParentData.offset,
587 : position: position,
588 0 : hitTest: (BoxHitTestResult result, Offset transformed) {
589 0 : assert(transformed == position - childParentData.offset);
590 0 : return child!.hitTest(result, position: transformed);
591 : },
592 : );
593 : if (isHit) {
594 : return true;
595 : }
596 0 : child = childParentData.previousSibling;
597 : }
598 : return false;
599 : }
600 :
601 : // Visit only the children that should be painted.
602 1 : @override
603 : void visitChildrenForSemantics(RenderObjectVisitor visitor) {
604 2 : visitChildren((RenderObject renderObjectChild) {
605 : final child = renderObjectChild as RenderBox;
606 : final childParentData =
607 1 : child.parentData! as ToolbarItemsParentData;
608 1 : if (childParentData.shouldPaint) {
609 1 : visitor(renderObjectChild);
610 : }
611 : });
612 : }
613 : }
614 :
615 : // The Material-styled toolbar outline. Fill it with any widgets you want. No
616 : // overflow ability.
617 : class _TextSelectionToolbarContainer extends StatelessWidget {
618 1 : const _TextSelectionToolbarContainer({
619 : Key? key,
620 : required this.child,
621 1 : }) : super(key: key);
622 :
623 : final Widget child;
624 :
625 1 : @override
626 : Widget build(BuildContext context) {
627 1 : return Material(
628 : // This value was eyeballed to match the native text selection menu on
629 : // a Pixel 2 running Android 10.
630 : borderRadius: const BorderRadius.all(Radius.circular(7.0)),
631 : clipBehavior: Clip.antiAlias,
632 : elevation: 1.0,
633 : type: MaterialType.card,
634 1 : child: child,
635 : );
636 : }
637 : }
638 :
639 : // A button styled like a Material native Android text selection overflow menu
640 : // forward and back controls.
641 : class _TextSelectionToolbarOverflowButton extends StatelessWidget {
642 1 : const _TextSelectionToolbarOverflowButton({
643 : Key? key,
644 : required this.icon,
645 : this.onPressed,
646 : this.tooltip,
647 1 : }) : super(key: key);
648 :
649 : final Icon icon;
650 : final VoidCallback? onPressed;
651 : final String? tooltip;
652 :
653 1 : @override
654 : Widget build(BuildContext context) {
655 1 : return Material(
656 : type: MaterialType.card,
657 : color: const Color(0x00000000),
658 1 : child: IconButton(
659 1 : icon: icon,
660 1 : onPressed: onPressed,
661 1 : tooltip: tooltip,
662 : ),
663 : );
664 : }
665 : }
|