1 /*
<lambda>null2  * Copyright 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.graphics.shapes.testcompose
18 
19 import android.content.Intent
20 import android.os.Bundle
21 import androidx.activity.compose.setContent
22 import androidx.compose.animation.animateContentSize
23 import androidx.compose.animation.core.animateFloatAsState
24 import androidx.compose.animation.core.tween
25 import androidx.compose.foundation.layout.Arrangement
26 import androidx.compose.foundation.layout.Column
27 import androidx.compose.foundation.layout.Row
28 import androidx.compose.foundation.layout.Spacer
29 import androidx.compose.foundation.layout.fillMaxHeight
30 import androidx.compose.foundation.layout.fillMaxWidth
31 import androidx.compose.foundation.layout.height
32 import androidx.compose.foundation.layout.padding
33 import androidx.compose.foundation.layout.size
34 import androidx.compose.foundation.layout.width
35 import androidx.compose.foundation.rememberScrollState
36 import androidx.compose.foundation.selection.selectableGroup
37 import androidx.compose.foundation.shape.RoundedCornerShape
38 import androidx.compose.foundation.verticalScroll
39 import androidx.compose.material.icons.Icons
40 import androidx.compose.material.icons.automirrored.filled.ArrowBack
41 import androidx.compose.material.icons.filled.Add
42 import androidx.compose.material.icons.filled.Check
43 import androidx.compose.material.icons.filled.Clear
44 import androidx.compose.material.icons.filled.Edit
45 import androidx.compose.material.icons.filled.Info
46 import androidx.compose.material.icons.filled.KeyboardArrowDown
47 import androidx.compose.material.icons.filled.Share
48 import androidx.compose.material.icons.filled.Warning
49 import androidx.compose.material3.AlertDialog
50 import androidx.compose.material3.Button
51 import androidx.compose.material3.Card
52 import androidx.compose.material3.ExperimentalMaterial3Api
53 import androidx.compose.material3.HorizontalDivider
54 import androidx.compose.material3.Icon
55 import androidx.compose.material3.IconButton
56 import androidx.compose.material3.MaterialTheme
57 import androidx.compose.material3.OutlinedButton
58 import androidx.compose.material3.RadioButton
59 import androidx.compose.material3.Scaffold
60 import androidx.compose.material3.SegmentedButton
61 import androidx.compose.material3.SegmentedButtonDefaults
62 import androidx.compose.material3.SingleChoiceSegmentedButtonRow
63 import androidx.compose.material3.Text
64 import androidx.compose.material3.TextButton
65 import androidx.compose.material3.TextField
66 import androidx.compose.material3.TopAppBar
67 import androidx.compose.runtime.Composable
68 import androidx.compose.runtime.MutableState
69 import androidx.compose.runtime.getValue
70 import androidx.compose.runtime.mutableIntStateOf
71 import androidx.compose.runtime.mutableStateOf
72 import androidx.compose.runtime.remember
73 import androidx.compose.runtime.setValue
74 import androidx.compose.runtime.snapshots.SnapshotStateList
75 import androidx.compose.runtime.toMutableStateList
76 import androidx.compose.ui.Alignment
77 import androidx.compose.ui.Modifier
78 import androidx.compose.ui.draw.rotate
79 import androidx.compose.ui.text.AnnotatedString
80 import androidx.compose.ui.text.ParagraphStyle
81 import androidx.compose.ui.text.SpanStyle
82 import androidx.compose.ui.text.buildAnnotatedString
83 import androidx.compose.ui.text.font.FontWeight
84 import androidx.compose.ui.text.input.TextFieldValue
85 import androidx.compose.ui.text.style.TextOverflow
86 import androidx.compose.ui.text.withStyle
87 import androidx.compose.ui.unit.dp
88 import androidx.fragment.app.FragmentActivity
89 import androidx.graphics.shapes.Feature
90 import androidx.graphics.shapes.FeatureSerializer
91 import androidx.graphics.shapes.RoundedPolygon
92 import androidx.graphics.shapes.SvgPathParser
93 
94 class ShapeEditor : FragmentActivity() {
95     override fun onCreate(savedInstanceState: Bundle?) {
96         super.onCreate(savedInstanceState)
97         setContent(parent = null) { MaterialTheme { PolygonEditor(this) } }
98     }
99 }
100 
101 @Composable
PolygonEditornull102 private fun PolygonEditor(activity: FragmentActivity) {
103     val shapeParams = remember { materialShapes().map { mutableStateOf(it) }.toMutableStateList() }
104 
105     var selectedStartShape by remember { mutableIntStateOf(5) }
106     var selectedEndShape by remember { mutableIntStateOf(16) }
107     var selectedIndex by remember { mutableIntStateOf(0) }
108     val selectedShape = if (selectedIndex == 0) selectedStartShape else selectedEndShape
109     val currentScreenType: MutableState<ScreenTypes> = remember { mutableStateOf(ScreenTypes.HOME) }
110 
111     val changeSelected = { shapeIndex: Int ->
112         if (selectedIndex == 0) selectedStartShape = shapeIndex else selectedEndShape = shapeIndex
113     }
114 
115     val screens: Map<ScreenTypes, @Composable () -> Unit> =
116         mapOf(
117             ScreenTypes.HOME to
118                 {
119                     HomeScreen(
120                         shapeParams,
121                         selectedStartShape,
122                         selectedEndShape,
123                         selectedShape,
124                         selectedIndex,
125                         onSelectedSwitch = { index -> selectedIndex = index },
126                         onShapeSwitch = changeSelected,
127                         onEditClick = { currentScreenType.value = ScreenTypes.EDIT },
128                         onAboutClick = { currentScreenType.value = ScreenTypes.ABOUT },
129                         onExportClick = {
130                             exportShape(activity, shapeParams[selectedShape].value.serialized())
131                         },
132                         onAddShapeClick = {
133                             shapeParams.add(mutableStateOf(ShapeParameters("Custom")))
134                             changeSelected(shapeParams.lastIndex)
135                             // Give this shape a chance to be visualized, as the visualization
136                             // will initialize/sync properties like the feature overlay and we do
137                             // not
138                             // want a prompt for a 'save changes?' when users go back immediately
139                             shapeParams.last().value.genShape()
140                             currentScreenType.value = ScreenTypes.EDIT
141                         },
142                         onImportShapeClick = { parseFunction: () -> List<Feature> ->
143                             val features: List<Feature> = parseFunction()
144                             shapeParams.add(
145                                 mutableStateOf(
146                                     CustomShapeParameters("Custom") {
147                                         RoundedPolygon(features).normalized()
148                                     }
149                                 )
150                             )
151                             changeSelected(shapeParams.lastIndex)
152                         }
153                     )
154                 },
155             ScreenTypes.EDIT to
156                 {
157                     EditScreen(
158                         shapeParams[selectedShape].value,
159                         onBackClick = { currentScreenType.value = ScreenTypes.HOME },
160                         onSave = { newParams: ShapeParameters ->
161                             shapeParams[selectedShape].value = newParams
162                             currentScreenType.value = ScreenTypes.HOME
163                         }
164                     )
165                 },
166             ScreenTypes.ABOUT to
167                 {
168                     AboutScreen(onBackClick = { currentScreenType.value = ScreenTypes.HOME })
169                 }
170         )
171 
172     screens[currentScreenType.value]?.let { it() }
173 }
174 
175 @Composable
EditScreennull176 private fun EditScreen(
177     parameters: ShapeParameters,
178     onBackClick: () -> Unit,
179     onSave: (ShapeParameters) -> Unit
180 ) {
181     var showSaveMessage by remember { mutableStateOf(false) }
182     val copyToEdit = remember { parameters.copy() }
183     var selectedViewModeIndex by remember { mutableIntStateOf(0) }
184 
185     if (showSaveMessage) {
186         SaveConfirmationDialog(
187             onDismissRequest = {
188                 showSaveMessage = false
189                 onBackClick()
190             },
191             onConfirmation = { onSave(copyToEdit) }
192         )
193     }
194 
195     Scaffold(
196         topBar = {
197             EditScreenHeader(
198                 selectedViewModeIndex,
199                 onBackClick = {
200                     if (!copyToEdit.equals(parameters)) showSaveMessage = true else onBackClick()
201                 },
202                 onModeSwitch = { index -> selectedViewModeIndex = index }
203             )
204         },
205         bottomBar = { EditScreenFooter(onCancel = onBackClick, onSave = { onSave(copyToEdit) }) }
206     ) { innerPadding ->
207         Column(
208             modifier = Modifier.padding(innerPadding),
209             horizontalAlignment = Alignment.CenterHorizontally,
210             verticalArrangement = Arrangement.spacedBy(3.dp),
211         ) {
212             if (selectedViewModeIndex == 0) {
213                 ParametricEditor(copyToEdit)
214             } else {
215                 FeatureEditor(copyToEdit)
216             }
217         }
218     }
219 }
220 
221 @OptIn(ExperimentalMaterial3Api::class)
222 @Composable
EditScreenHeadernull223 private fun EditScreenHeader(
224     selectedIndex: Int,
225     onBackClick: () -> Unit,
226     onModeSwitch: (Int) -> Unit
227 ) {
228     val options = listOf("Parametric", "Features")
229 
230     TopAppBar(
231         title = { Text("Shape Edit") },
232         actions = {
233             SingleChoiceSegmentedButtonRow {
234                 options.forEachIndexed { index, label ->
235                     SegmentedButton(
236                         shape =
237                             SegmentedButtonDefaults.itemShape(index = index, count = options.size),
238                         onClick = { onModeSwitch(index) },
239                         selected = index == selectedIndex,
240                         icon = {}
241                     ) {
242                         Text(label)
243                     }
244                 }
245             }
246         },
247         navigationIcon = {
248             IconButton(onClick = onBackClick) {
249                 Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Go back to Home")
250             }
251         }
252     )
253 }
254 
255 @Composable
EditScreenFooternull256 private fun EditScreenFooter(onCancel: () -> Unit, onSave: () -> Unit) {
257     Row(
258         Modifier.padding(horizontal = 20.dp).fillMaxWidth(),
259         horizontalArrangement = Arrangement.Center
260     ) {
261         OutlinedButton(onClick = onCancel, modifier = Modifier.weight(1f)) {
262             Icon(
263                 Icons.Default.Clear,
264                 contentDescription = "Cancel",
265                 modifier = Modifier.padding(horizontal = 6.dp)
266             )
267             Text("Cancel")
268         }
269         Spacer(Modifier.width(12.dp))
270         Button(onClick = onSave, modifier = Modifier.weight(1f)) {
271             Icon(
272                 Icons.Default.Check,
273                 contentDescription = "Save",
274                 modifier = Modifier.padding(horizontal = 6.dp)
275             )
276             Text("Save")
277         }
278     }
279 }
280 
281 @Composable
HomeScreennull282 private fun HomeScreen(
283     shapeParams: SnapshotStateList<MutableState<ShapeParameters>>,
284     selectedStartShape: Int,
285     selectedEndShape: Int,
286     selectedShape: Int,
287     selectedIndex: Int,
288     onSelectedSwitch: (Int) -> Unit,
289     onShapeSwitch: (Int) -> Unit,
290     onEditClick: () -> Unit,
291     onAboutClick: () -> Unit,
292     onExportClick: () -> Unit,
293     onAddShapeClick: () -> Unit,
294     onImportShapeClick: (() -> List<Feature>) -> Unit
295 ) {
296     val shapes =
297         remember(shapeParams.size) { shapeParams.map { sp -> sp.value.genShape().normalized() } }
298 
299     var showImportMessage by remember { mutableStateOf(false) }
300 
301     Scaffold(
302         topBar = {
303             HomeScreenHeader(
304                 selectedIndex,
305                 shapes[selectedStartShape],
306                 shapes[selectedEndShape],
307                 onSelectedSwitch
308             )
309         },
310         bottomBar = { HomeScreenFooter(onAboutClick, onExportClick) }
311     ) { innerPadding ->
312         Column(
313             modifier = Modifier.padding(innerPadding),
314             horizontalAlignment = Alignment.CenterHorizontally,
315             verticalArrangement = Arrangement.spacedBy(3.dp),
316         ) {
317             if (showImportMessage) {
318                 NewShapeDialog(
319                     { showImportMessage = false },
320                     {
321                         onAddShapeClick()
322                         showImportMessage = false
323                     },
324                     {
325                         onImportShapeClick(it)
326                         showImportMessage = false
327                     }
328                 )
329             }
330 
331             ShapesGallery(
332                 shapes,
333                 selectedShape,
334                 if (selectedIndex == 0) selectedEndShape else selectedStartShape,
335                 Modifier.fillMaxHeight(0.35f).verticalScroll(rememberScrollState()),
336                 onShapeSwitch
337             )
338 
339             EditButtonRow(onEditClick) { showImportMessage = true }
340 
341             Spacer(Modifier.height(12.dp))
342 
343             HorizontalDivider(thickness = 2.dp)
344 
345             AnimatedMorphView(shapes, selectedStartShape, selectedEndShape)
346         }
347     }
348 }
349 
350 @OptIn(ExperimentalMaterial3Api::class)
351 @Composable
HomeScreenHeadernull352 private fun HomeScreenHeader(
353     selectedIndex: Int,
354     selectedStartShape: RoundedPolygon,
355     selectedEndShape: RoundedPolygon,
356     onSwitch: (Int) -> Unit
357 ) {
358     val options =
359         listOf(
360             "From",
361             "To",
362         )
363 
364     TopAppBar(
365         title = { Text("Shape Selection") },
366         actions = {
367             SingleChoiceSegmentedButtonRow {
368                 options.forEachIndexed { index, label ->
369                     SegmentedButton(
370                         shape =
371                             SegmentedButtonDefaults.itemShape(index = index, count = options.size),
372                         onClick = { onSwitch(index) },
373                         selected = index == selectedIndex,
374                         icon = {},
375                         label = {
376                             Row(horizontalArrangement = Arrangement.SpaceEvenly) {
377                                 Text(label)
378                                 Spacer(Modifier.size(10.dp))
379                                 if (index == 0) PolygonView(selectedStartShape)
380                                 else PolygonView(selectedEndShape)
381                             }
382                         }
383                     )
384                 }
385             }
386         }
387     )
388 }
389 
390 @Composable
HomeScreenFooternull391 private fun HomeScreenFooter(onAboutClick: () -> Unit, onExportClick: () -> Unit) {
392     Row(
393         Modifier.padding(horizontal = 20.dp).fillMaxWidth(),
394         horizontalArrangement = Arrangement.Center
395     ) {
396         OutlinedButton(onClick = onAboutClick, modifier = Modifier.weight(1f)) {
397             Icon(
398                 Icons.Default.Info,
399                 contentDescription = "Help",
400                 modifier = Modifier.padding(horizontal = 6.dp)
401             )
402             Text("Usage FAQ")
403         }
404         Spacer(Modifier.width(12.dp))
405         Button(onClick = onExportClick, modifier = Modifier.weight(1f)) {
406             Icon(
407                 Icons.Default.Share,
408                 contentDescription = "Export",
409                 modifier = Modifier.padding(horizontal = 6.dp)
410             )
411             Text("Export")
412         }
413     }
414 }
415 
416 @OptIn(ExperimentalMaterial3Api::class)
417 @Composable
AboutScreennull418 fun AboutScreen(onBackClick: () -> Unit) {
419     val padding = 10.dp
420     Scaffold(
421         topBar = {
422             TopAppBar(
423                 title = { Text("Help") },
424                 navigationIcon = {
425                     IconButton(onClick = onBackClick) {
426                         Icon(
427                             Icons.AutoMirrored.Default.ArrowBack,
428                             contentDescription = "Go back to Home"
429                         )
430                     }
431                 }
432             )
433         },
434     ) { innerPadding ->
435         Column(
436             modifier = Modifier.padding(30.dp).verticalScroll(rememberScrollState()),
437             horizontalAlignment = Alignment.CenterHorizontally,
438             verticalArrangement = Arrangement.spacedBy(3.dp),
439         ) {
440             Spacer(Modifier.height(innerPadding.calculateTopPadding()))
441 
442             ExpandableQuestionCard(
443                 "What are Shape Features and why should I care about them?",
444                 AnnotatedString(
445                     "Features create special target points on a shape that the morph algorithm uses to guide the shape's transformation. The algorithm will match Features of the same type (except 'None') between shapes. You can see and edit these points by going to Edit > Features. By understanding and manipulating Features, you can control how your shapes change during a morph."
446                 ),
447                 Modifier.padding(vertical = padding)
448             )
449 
450             ExpandableQuestionCard(
451                 "How can I make my Morph symmetrical?",
452                 SYMMETRIC_SHAPE_ANSWER,
453                 Modifier.padding(vertical = padding)
454             )
455 
456             ExpandableQuestionCard(
457                 "How can I add Shapes from Figma / Google Icons / other ?",
458                 CUSTOM_SHAPE_ANSWER,
459                 Modifier.padding(vertical = padding)
460             )
461 
462             ExpandableQuestionCard(
463                 "I finished editing my Shape and want to maintain the changes in my code. What do I have to do?",
464                 AnnotatedString(
465                     "To preserve your edited shape for future use, export it by clicking the 'Export' button on the Home Screen. This will generate a code snippet representing your shape. Copy this code and integrate it into your project's relevant file to load and use the shape within your project."
466                 ),
467                 Modifier.padding(vertical = padding)
468             )
469         }
470     }
471 }
472 
473 @Composable
EditButtonRownull474 private fun EditButtonRow(onEditClick: () -> Unit, onImportShapeClick: () -> Unit) {
475     Row(
476         Modifier.padding(horizontal = 20.dp).fillMaxWidth(),
477         horizontalArrangement = Arrangement.Center
478     ) {
479         OutlinedButton(onClick = onEditClick, modifier = Modifier.weight(1f)) {
480             Icon(
481                 Icons.Default.Edit,
482                 contentDescription = "Edit Shape",
483                 modifier = Modifier.padding(horizontal = 6.dp)
484             )
485             Text("Edit Shape")
486         }
487         Spacer(Modifier.width(12.dp))
488         Button(onClick = onImportShapeClick, modifier = Modifier.weight(1f)) {
489             Icon(
490                 Icons.Default.Add,
491                 contentDescription = "New Shape",
492                 modifier = Modifier.padding(horizontal = 6.dp)
493             )
494             Text("New Shape")
495         }
496     }
497 }
498 
499 @Composable
NewShapeDialognull500 private fun NewShapeDialog(
501     onDismissRequest: () -> Unit,
502     onAddShape: () -> Unit,
503     onImportShape: (() -> List<Feature>) -> Unit
504 ) {
505     var wantsSvgImport by remember { mutableStateOf(true) }
506     val text = remember { mutableStateOf(TextFieldValue("")) }
507     var selectedButton by remember { mutableIntStateOf(0) }
508 
509     AlertDialog(
510         onDismissRequest = onDismissRequest,
511         title = { Text("Add Shape") },
512         text = {
513             Column(
514                 modifier = Modifier.selectableGroup(),
515                 verticalArrangement = Arrangement.SpaceEvenly
516             ) {
517                 Text("How do create the new Shape?")
518 
519                 LabelledRadioButton(
520                     selectedButton == 0,
521                     "Parametric Shape",
522                 ) {
523                     selectedButton = 0
524                 }
525 
526                 LabelledRadioButton(
527                     selectedButton == 1,
528                     "SVG Path Import",
529                 ) {
530                     selectedButton = 1
531                     wantsSvgImport = true
532                 }
533 
534                 LabelledRadioButton(
535                     selectedButton == 2,
536                     "Serialized Shape Features",
537                 ) {
538                     selectedButton = 2
539                     wantsSvgImport = false
540                 }
541 
542                 ImportTextField(text, wantsSvgImport, selectedButton != 0)
543             }
544         },
545         icon = { Icon(Icons.Default.Add, "Add Shape") },
546         confirmButton = {
547             TextButton(
548                 onClick = {
549                     when (selectedButton) {
550                         0 -> onAddShape()
551                         1 -> {
552                             onImportShape { SvgPathParser.parseFeatures(text.value.text) }
553                         }
554                         2 -> onImportShape { FeatureSerializer.parse(text.value.text) }
555                     }
556                 },
557             ) {
558                 Text("Confirm")
559             }
560         },
561         dismissButton = { TextButton(onClick = onDismissRequest) { Text("Cancel") } }
562     )
563 }
564 
565 @Composable
SaveConfirmationDialognull566 private fun SaveConfirmationDialog(
567     onDismissRequest: () -> Unit,
568     onConfirmation: () -> Unit,
569 ) {
570     AlertDialog(
571         onDismissRequest = onDismissRequest,
572         title = { Text("Discard Changes?") },
573         text = { Text("Any changes made to the shape will be lost.") },
574         icon = { Icon(Icons.Default.Warning, "Discard Changes?") },
575         confirmButton = {
576             TextButton(
577                 onClick = onConfirmation,
578             ) {
579                 Text("Save")
580             }
581         },
582         dismissButton = { TextButton(onClick = onDismissRequest) { Text("Discard") } }
583     )
584 }
585 
586 @Composable
ExpandableQuestionCardnull587 private fun ExpandableQuestionCard(
588     title: String,
589     content: AnnotatedString,
590     modifier: Modifier = Modifier,
591 ) {
592     var isExpanded by remember { mutableStateOf(false) }
593 
594     val rotation by animateFloatAsState(targetValue = if (isExpanded) 180f else 0f)
595 
596     Card(
597         modifier =
598             modifier.fillMaxWidth().animateContentSize(animationSpec = tween(durationMillis = 250)),
599         shape = RoundedCornerShape(16.dp),
600         onClick = { isExpanded = !isExpanded }
601     ) {
602         Column(modifier = Modifier.fillMaxWidth().padding(20.dp)) {
603             Row(verticalAlignment = Alignment.CenterVertically) {
604                 Text(
605                     modifier = Modifier.weight(6f),
606                     text = title,
607                     fontSize = MaterialTheme.typography.titleLarge.fontSize,
608                     fontWeight = FontWeight.Bold,
609                     overflow = TextOverflow.Ellipsis
610                 )
611                 IconButton(
612                     modifier = Modifier.weight(1f).rotate(rotation),
613                     onClick = { isExpanded = !isExpanded }
614                 ) {
615                     Icon(
616                         imageVector = Icons.Default.KeyboardArrowDown,
617                         contentDescription = "Expand Content"
618                     )
619                 }
620             }
621             if (isExpanded) {
622                 Text(
623                     text = content,
624                     fontSize = MaterialTheme.typography.titleSmall.fontSize,
625                     fontWeight = FontWeight.Normal,
626                     overflow = TextOverflow.Ellipsis
627                 )
628             }
629         }
630     }
631 }
632 
633 @Composable
ImportTextFieldnull634 fun ImportTextField(text: MutableState<TextFieldValue>, importsSvgPath: Boolean, enabled: Boolean) {
635     var hasBeenEdited by remember { mutableStateOf(false) }
636 
637     if (!hasBeenEdited) {
638         text.value = TextFieldValue(if (importsSvgPath) TRIANGE_SVG_PATH else SERIALIZED_TRIANGLE)
639     }
640 
641     TextField(
642         value = text.value,
643         onValueChange = {
644             text.value = it
645             hasBeenEdited = true
646         },
647         enabled = enabled,
648         minLines = 10,
649         maxLines = 20,
650         label = {
651             Text(
652                 if (importsSvgPath) "Svg Path ('d' attribute in svg files)"
653                 else "Serialized Shape Features String"
654             )
655         },
656     )
657 }
658 
659 @Composable
LabelledRadioButtonnull660 fun LabelledRadioButton(selected: Boolean, label: String, onClick: () -> Unit) {
661     Row(verticalAlignment = Alignment.CenterVertically) {
662         RadioButton(
663             selected = selected,
664             onClick = onClick,
665             enabled = true,
666         )
667         Text(label)
668     }
669 }
670 
AnnotatedStringnull671 private fun AnnotatedString.Builder.addParagraph(header: String, content: String) {
672     appendLine()
673     withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { append(header) }
674     withStyle(ParagraphStyle()) { append(content) }
675 }
676 
exportShapenull677 private fun exportShape(activity: FragmentActivity, serialization: String) {
678     println(serialization)
679     val sendIntent: Intent =
680         Intent().apply {
681             action = Intent.ACTION_SEND
682             putExtra(Intent.EXTRA_TEXT, serialization)
683             type = "text/plain"
684         }
685     val shareIntent = Intent.createChooser(sendIntent, null)
686     activity.startActivity(shareIntent)
687 }
688 
689 private enum class ScreenTypes {
690     HOME,
691     EDIT,
692     ABOUT,
693 }
694 
<lambda>null695 private val CUSTOM_SHAPE_ANSWER = buildAnnotatedString {
696     append("Here's a general approach to import shapes from various sources into your design tool:")
697     appendLine()
698     addParagraph(
699         "1. Export the Shape",
700         "Figma: Export the shape as an SVG file.\nGoogle Icons: Download the icon in SVG format.\nOther Sources: Export the shape in a vector format like SVG or PDF."
701     )
702     addParagraph(
703         "2. Obtain the Path Data",
704         "Open the SVG file in a text editor and locate the '<path>' element. Copy the value of the 'd' attribute. This attribute contains the path data, which is a sequence of commands that define the shape's geometry."
705     )
706     withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { append("Example (triangle)") }
707     withStyle(ParagraphStyle()) { append(TRIANGE_SVG_PATH) }
708 
709     addParagraph(
710         "3. Import the Path Data",
711         "In the app, click the 'Import' button. It will open an field that you can paste the path data into. Finally, your shape will be added to the gallery and is ready for editing."
712     )
713 }
714 
<lambda>null715 private val SYMMETRIC_SHAPE_ANSWER = buildAnnotatedString {
716     append(
717         "To achieve symmetry in your Morph, focus on the Features of your shapes. These are the special points that guide the morphing process."
718     )
719     appendLine()
720     addParagraph(
721         "1. Identify Symmetrical Areas",
722         "Determine the parts of your shapes that you want to be symmetrical."
723     )
724     addParagraph(
725         "2. Match Feature Types",
726         "Ensure that the Features corresponding to these symmetrical areas have the same type (e.g., both inward or outward indentation)."
727     )
728     addParagraph(
729         "3. Align Feature Points",
730         "Check if the anchor points of these Features align. If not, you may need to split your shapes to create additional Features and fine-tune the alignment."
731     )
732     appendLine()
733     append(
734         "By carefully aligning and matching Features, you can create smooth and symmetrical Morphs."
735     )
736 }
737 
738 private const val TRIANGE_SVG_PATH = "M 0 0 0.5 1 1 0 Z"
739 private const val SERIALIZED_TRIANGLE =
740     "V1n0.5,1.5,0.167,0.83,-0.167,0.167,-0.5,-0.5x-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5n-0.5,-0.5,0.167,-0.5,0.83,-0.5,1.5,-0.5x1.5,-0.5,1.5,-0.5,1.5,-0.5,1.5,-0.5n1.5,-0.5,1.167,0.167,0.83,0.83,0.5,1.5x0.5,1.5,0.5,1.5,0.5,1.5,0.5,1.5"
741