@apollohg/react-native-prose-editor 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +12 -7
  2. package/android/build.gradle +7 -2
  3. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +289 -2
  4. package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +51 -1
  5. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
  6. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +16 -3
  7. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +82 -1
  8. package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +403 -45
  9. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +246 -0
  10. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +841 -155
  11. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +125 -8
  12. package/{src/EditorTheme.ts → dist/EditorTheme.d.ts} +12 -52
  13. package/dist/EditorTheme.js +29 -0
  14. package/dist/EditorToolbar.d.ts +129 -0
  15. package/dist/EditorToolbar.js +394 -0
  16. package/dist/NativeEditorBridge.d.ts +242 -0
  17. package/dist/NativeEditorBridge.js +647 -0
  18. package/dist/NativeRichTextEditor.d.ts +142 -0
  19. package/dist/NativeRichTextEditor.js +649 -0
  20. package/dist/YjsCollaboration.d.ts +83 -0
  21. package/dist/YjsCollaboration.js +585 -0
  22. package/dist/addons.d.ts +70 -0
  23. package/dist/addons.js +77 -0
  24. package/dist/index.d.ts +8 -0
  25. package/dist/index.js +26 -0
  26. package/dist/schemas.d.ts +35 -0
  27. package/{src/schemas.ts → dist/schemas.js} +62 -27
  28. package/dist/useNativeEditor.d.ts +40 -0
  29. package/dist/useNativeEditor.js +117 -0
  30. package/ios/EditorAddons.swift +26 -3
  31. package/ios/EditorCore.xcframework/Info.plist +5 -5
  32. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  33. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  34. package/ios/EditorLayoutManager.swift +236 -0
  35. package/ios/EditorTheme.swift +51 -1
  36. package/ios/Generated_editor_core.swift +270 -2
  37. package/ios/NativeEditorExpoView.swift +612 -45
  38. package/ios/NativeEditorModule.swift +81 -0
  39. package/ios/PositionBridge.swift +22 -0
  40. package/ios/RenderBridge.swift +427 -39
  41. package/ios/RichTextEditorView.swift +1342 -18
  42. package/ios/editor_coreFFI/editor_coreFFI.h +209 -0
  43. package/package.json +80 -64
  44. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  45. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  46. package/rust/android/x86_64/libeditor_core.so +0 -0
  47. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +404 -4
  48. package/src/EditorToolbar.tsx +0 -620
  49. package/src/NativeEditorBridge.ts +0 -607
  50. package/src/NativeRichTextEditor.tsx +0 -951
  51. package/src/addons.ts +0 -158
  52. package/src/index.ts +0 -63
  53. package/src/useNativeEditor.ts +0 -173
@@ -1,17 +1,24 @@
1
1
  package com.apollohg.editor
2
2
 
3
3
  import android.content.Context
4
+ import android.os.Build
4
5
  import android.graphics.Color
5
6
  import android.graphics.Typeface
6
7
  import android.graphics.drawable.GradientDrawable
8
+ import android.util.TypedValue
7
9
  import android.view.Gravity
8
10
  import android.view.View
11
+ import android.view.ViewOutlineProvider
9
12
  import android.widget.HorizontalScrollView
10
13
  import android.widget.LinearLayout
11
14
  import androidx.appcompat.widget.AppCompatButton
12
15
  import androidx.appcompat.widget.AppCompatTextView
16
+ import androidx.appcompat.content.res.AppCompatResources
13
17
  import androidx.core.view.setPadding
18
+ import com.google.android.material.R as MaterialR
19
+ import com.google.android.material.color.DynamicColors
14
20
  import org.json.JSONObject
21
+ import kotlin.math.roundToInt
15
22
 
16
23
  internal data class NativeToolbarState(
17
24
  val marks: Map<String, Boolean>,
@@ -91,6 +98,9 @@ internal enum class ToolbarDefaultIconId {
91
98
  italic,
92
99
  underline,
93
100
  strike,
101
+ link,
102
+ image,
103
+ blockquote,
94
104
  bulletList,
95
105
  orderedList,
96
106
  indentList,
@@ -103,6 +113,7 @@ internal enum class ToolbarDefaultIconId {
103
113
 
104
114
  internal enum class ToolbarItemKind {
105
115
  mark,
116
+ blockquote,
106
117
  list,
107
118
  command,
108
119
  node,
@@ -122,6 +133,9 @@ internal data class NativeToolbarIcon(
122
133
  ToolbarDefaultIconId.italic to "I",
123
134
  ToolbarDefaultIconId.underline to "U",
124
135
  ToolbarDefaultIconId.strike to "S",
136
+ ToolbarDefaultIconId.link to "🔗",
137
+ ToolbarDefaultIconId.image to "🖼",
138
+ ToolbarDefaultIconId.blockquote to "❝",
125
139
  ToolbarDefaultIconId.bulletList to "•≡",
126
140
  ToolbarDefaultIconId.orderedList to "1.",
127
141
  ToolbarDefaultIconId.indentList to "→",
@@ -136,6 +150,9 @@ internal data class NativeToolbarIcon(
136
150
  ToolbarDefaultIconId.italic to "format-italic",
137
151
  ToolbarDefaultIconId.underline to "format-underlined",
138
152
  ToolbarDefaultIconId.strike to "strikethrough-s",
153
+ ToolbarDefaultIconId.link to "link",
154
+ ToolbarDefaultIconId.image to "image",
155
+ ToolbarDefaultIconId.blockquote to "format-quote",
139
156
  ToolbarDefaultIconId.bulletList to "format-list-bulleted",
140
157
  ToolbarDefaultIconId.orderedList to "format-list-numbered",
141
158
  ToolbarDefaultIconId.indentList to "format-indent-increase",
@@ -279,6 +296,7 @@ internal data class NativeToolbarItem(
279
296
  NativeToolbarItem(ToolbarItemKind.mark, label = "Italic", icon = NativeToolbarIcon(defaultId = ToolbarDefaultIconId.italic), mark = "italic"),
280
297
  NativeToolbarItem(ToolbarItemKind.mark, label = "Underline", icon = NativeToolbarIcon(defaultId = ToolbarDefaultIconId.underline), mark = "underline"),
281
298
  NativeToolbarItem(ToolbarItemKind.mark, label = "Strikethrough", icon = NativeToolbarIcon(defaultId = ToolbarDefaultIconId.strike), mark = "strike"),
299
+ NativeToolbarItem(ToolbarItemKind.blockquote, label = "Blockquote", icon = NativeToolbarIcon(defaultId = ToolbarDefaultIconId.blockquote)),
282
300
  NativeToolbarItem(ToolbarItemKind.separator),
283
301
  NativeToolbarItem(ToolbarItemKind.list, label = "Bullet List", icon = NativeToolbarIcon(defaultId = ToolbarDefaultIconId.bulletList), listType = ToolbarListType.bulletList),
284
302
  NativeToolbarItem(ToolbarItemKind.list, label = "Ordered List", icon = NativeToolbarIcon(defaultId = ToolbarDefaultIconId.orderedList), listType = ToolbarListType.orderedList),
@@ -313,6 +331,11 @@ internal data class NativeToolbarItem(
313
331
  val label = rawItem.optNullableString("label") ?: continue
314
332
  parsed.add(NativeToolbarItem(type, key, label, icon, mark = mark))
315
333
  }
334
+ ToolbarItemKind.blockquote -> {
335
+ val icon = NativeToolbarIcon.fromJson(rawItem.optJSONObject("icon")) ?: continue
336
+ val label = rawItem.optNullableString("label") ?: continue
337
+ parsed.add(NativeToolbarItem(type, key, label, icon))
338
+ }
316
339
  ToolbarItemKind.list -> {
317
340
  val icon = NativeToolbarIcon.fromJson(rawItem.optJSONObject("icon")) ?: continue
318
341
  val listType = runCatching {
@@ -358,6 +381,16 @@ internal data class NativeToolbarItem(
358
381
  }
359
382
 
360
383
  internal class EditorKeyboardToolbarView(context: Context) : HorizontalScrollView(context) {
384
+ private companion object {
385
+ private const val NATIVE_CONTAINER_HEIGHT_DP = 64
386
+ private const val NATIVE_CONTAINER_HORIZONTAL_PADDING_DP = 16
387
+ private const val NATIVE_CONTAINER_VERTICAL_PADDING_DP = 12
388
+ private const val NATIVE_BUTTON_SIZE_DP = 40
389
+ private const val NATIVE_BUTTON_ICON_SIZE_SP = 24f
390
+ private const val NATIVE_ITEM_SPACING_DP = 8
391
+ private const val NATIVE_GROUP_SPACING_DP = 12
392
+ }
393
+
361
394
  private data class ButtonBinding(
362
395
  val item: NativeToolbarItem,
363
396
  val button: AppCompatButton
@@ -366,6 +399,7 @@ internal class EditorKeyboardToolbarView(context: Context) : HorizontalScrollVie
366
399
  var onPressItem: ((NativeToolbarItem) -> Unit)? = null
367
400
  var onSelectMentionSuggestion: ((NativeMentionSuggestion) -> Unit)? = null
368
401
 
402
+ private val themedContext: Context = DynamicColors.wrapContextIfAvailable(context)
369
403
  private val contentRow = LinearLayout(context)
370
404
  private var theme: EditorToolbarTheme? = null
371
405
  private var mentionTheme: EditorMentionTheme? = null
@@ -375,11 +409,18 @@ internal class EditorKeyboardToolbarView(context: Context) : HorizontalScrollVie
375
409
  private val bindings = mutableListOf<ButtonBinding>()
376
410
  private val separators = mutableListOf<View>()
377
411
  private val mentionChips = mutableListOf<MentionSuggestionChipView>()
412
+ private val buttonBackgroundColors = mutableMapOf<AppCompatButton, Int>()
378
413
  private val density = resources.displayMetrics.density
414
+ internal var appliedAppearance: EditorToolbarAppearance = EditorToolbarAppearance.CUSTOM
415
+ private set
379
416
  internal var appliedChromeCornerRadiusPx: Float = 0f
380
417
  private set
381
418
  internal var appliedChromeStrokeWidthPx: Int = 0
382
419
  private set
420
+ internal var appliedChromeElevationPx: Float = 0f
421
+ private set
422
+ internal var appliedChromeColor: Int = Color.TRANSPARENT
423
+ private set
383
424
  internal var appliedButtonCornerRadiusPx: Float = 0f
384
425
  private set
385
426
  val isShowingMentionSuggestions: Boolean
@@ -390,13 +431,17 @@ internal class EditorKeyboardToolbarView(context: Context) : HorizontalScrollVie
390
431
  overScrollMode = OVER_SCROLL_NEVER
391
432
  setBackgroundColor(Color.TRANSPARENT)
392
433
  clipToPadding = false
434
+ clipChildren = false
435
+ isFillViewport = true
393
436
 
394
437
  contentRow.orientation = LinearLayout.HORIZONTAL
395
- contentRow.gravity = Gravity.CENTER_VERTICAL
438
+ contentRow.gravity = Gravity.START or Gravity.CENTER_VERTICAL
396
439
  contentRow.setPadding(dp(12))
440
+ contentRow.clipToPadding = false
441
+ contentRow.clipChildren = false
397
442
  addView(
398
443
  contentRow,
399
- LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
444
+ LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
400
445
  )
401
446
  rebuildContent()
402
447
  }
@@ -412,7 +457,7 @@ internal class EditorKeyboardToolbarView(context: Context) : HorizontalScrollVie
412
457
  this.theme = theme
413
458
  updateChrome()
414
459
  separators.forEach { separator ->
415
- separator.setBackgroundColor(theme?.separatorColor ?: Color.parseColor("#E5E5EA"))
460
+ separator.setBackgroundColor(resolveSeparatorColor())
416
461
  }
417
462
  bindings.forEach { binding ->
418
463
  updateButtonAppearance(
@@ -422,14 +467,14 @@ internal class EditorKeyboardToolbarView(context: Context) : HorizontalScrollVie
422
467
  )
423
468
  }
424
469
  mentionChips.forEach { chip ->
425
- chip.applyTheme(mentionTheme)
470
+ chip.applyTheme(mentionTheme, theme?.appearance ?: EditorToolbarAppearance.CUSTOM)
426
471
  }
427
472
  }
428
473
 
429
474
  fun applyMentionTheme(theme: EditorMentionTheme?) {
430
475
  mentionTheme = theme
431
476
  mentionChips.forEach { chip ->
432
- chip.applyTheme(theme)
477
+ chip.applyTheme(theme, this.theme?.appearance ?: EditorToolbarAppearance.CUSTOM)
433
478
  }
434
479
  }
435
480
 
@@ -454,6 +499,18 @@ internal class EditorKeyboardToolbarView(context: Context) : HorizontalScrollVie
454
499
  mentionChips.getOrNull(index)?.performClick()
455
500
  }
456
501
 
502
+ internal fun buttonAtForTesting(index: Int): AppCompatButton? =
503
+ bindings.getOrNull(index)?.button
504
+
505
+ internal fun buttonBackgroundColorAtForTesting(index: Int): Int? =
506
+ bindings.getOrNull(index)?.button?.let { buttonBackgroundColors[it] }
507
+
508
+ internal fun mentionChipAtForTesting(index: Int): MentionSuggestionChipView? =
509
+ mentionChips.getOrNull(index)
510
+
511
+ internal fun separatorAtForTesting(index: Int): View? =
512
+ separators.getOrNull(index)
513
+
457
514
  private fun rebuildContent() {
458
515
  bindings.clear()
459
516
  separators.clear()
@@ -475,40 +532,36 @@ internal class EditorKeyboardToolbarView(context: Context) : HorizontalScrollVie
475
532
  for (item in compactItems(items)) {
476
533
  if (item.type == ToolbarItemKind.separator) {
477
534
  val separator = View(context)
478
- val params = LinearLayout.LayoutParams(dp(1), dp(22))
479
- params.marginStart = dp(6)
480
- params.marginEnd = dp(6)
481
- separator.layoutParams = params
482
- separator.setBackgroundColor(theme?.separatorColor ?: Color.parseColor("#E5E5EA"))
535
+ configureSeparator(separator)
483
536
  separators.add(separator)
484
537
  contentRow.addView(separator)
485
538
  continue
486
539
  }
487
540
 
488
- val button = AppCompatButton(context).apply {
489
- val resolvedIcon = item.icon?.resolveForAndroid(context)
541
+ val button = AppCompatButton(themedContext).apply {
542
+ val resolvedIcon = item.icon?.resolveForAndroid(themedContext)
490
543
  ?: NativeToolbarResolvedIcon("?")
491
544
  text = resolvedIcon.text
492
545
  typeface = resolvedIcon.typeface ?: Typeface.DEFAULT
493
- textSize = 16f
494
- minWidth = dp(36)
495
- minimumWidth = dp(36)
496
- minHeight = dp(36)
497
- minimumHeight = dp(36)
498
546
  gravity = Gravity.CENTER
499
- setPadding(dp(10), dp(8), dp(10), dp(8))
500
547
  background = GradientDrawable()
501
548
  isAllCaps = false
502
549
  includeFontPadding = false
503
550
  contentDescription = item.label
504
551
  setOnClickListener { onPressItem?.invoke(item) }
552
+ elevation = 0f
553
+ translationZ = 0f
554
+ stateListAnimator = null
555
+ }
556
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
557
+ button.foreground = resolveDrawableAttr(android.R.attr.selectableItemBackgroundBorderless)
505
558
  }
506
559
  val params = LinearLayout.LayoutParams(
507
560
  LinearLayout.LayoutParams.WRAP_CONTENT,
508
561
  LinearLayout.LayoutParams.WRAP_CONTENT
509
562
  )
510
- params.marginEnd = dp(6)
511
563
  button.layoutParams = params
564
+ applyButtonLayout(button, appearance = theme?.appearance ?: EditorToolbarAppearance.CUSTOM)
512
565
  bindings.add(ButtonBinding(item, button))
513
566
  contentRow.addView(button)
514
567
  }
@@ -517,7 +570,7 @@ internal class EditorKeyboardToolbarView(context: Context) : HorizontalScrollVie
517
570
  private fun rebuildMentionSuggestions() {
518
571
  for (suggestion in mentionSuggestions) {
519
572
  val chip = MentionSuggestionChipView(context, suggestion).apply {
520
- applyTheme(mentionTheme)
573
+ applyTheme(mentionTheme, theme?.appearance ?: EditorToolbarAppearance.CUSTOM)
521
574
  setOnClickListener { onSelectMentionSuggestion?.invoke(suggestion) }
522
575
  }
523
576
  val params = LinearLayout.LayoutParams(
@@ -542,41 +595,135 @@ internal class EditorKeyboardToolbarView(context: Context) : HorizontalScrollVie
542
595
  }
543
596
 
544
597
  private fun updateChrome() {
545
- val cornerRadiusPx = (theme?.borderRadius ?: 0f) * density
546
- val strokeWidthPx = ((theme?.borderWidth ?: 1f) * density).toInt().coerceAtLeast(1)
598
+ val appearance = theme?.appearance ?: EditorToolbarAppearance.CUSTOM
599
+ val cornerRadiusPx = (theme?.resolvedBorderRadius() ?: 0f) * density
600
+ val strokeWidthPx = if (appearance == EditorToolbarAppearance.NATIVE) {
601
+ 0
602
+ } else {
603
+ ((theme?.resolvedBorderWidth() ?: 1f) * density).roundToInt().coerceAtLeast(1)
604
+ }
547
605
  val drawable = GradientDrawable().apply {
548
606
  shape = GradientDrawable.RECTANGLE
549
607
  cornerRadius = cornerRadiusPx
550
- setColor(theme?.backgroundColor ?: Color.WHITE)
551
- setStroke(strokeWidthPx, theme?.borderColor ?: Color.parseColor("#E5E5EA"))
608
+ setColor(
609
+ if (appearance == EditorToolbarAppearance.NATIVE) {
610
+ resolveColorAttr(
611
+ MaterialR.attr.colorSurfaceContainer,
612
+ MaterialR.attr.colorSurfaceContainerLow,
613
+ MaterialR.attr.colorSurface,
614
+ android.R.attr.colorBackground
615
+ )
616
+ } else {
617
+ theme?.backgroundColor ?: resolveColorAttr(
618
+ MaterialR.attr.colorSurface,
619
+ android.R.attr.colorBackground
620
+ )
621
+ }
622
+ )
623
+ if (strokeWidthPx > 0) {
624
+ setStroke(strokeWidthPx, theme?.borderColor ?: resolveSeparatorColor())
625
+ }
552
626
  }
627
+ appliedAppearance = appearance
553
628
  appliedChromeCornerRadiusPx = cornerRadiusPx
554
629
  appliedChromeStrokeWidthPx = strokeWidthPx
630
+ appliedChromeElevationPx = 0f
631
+ appliedChromeColor = if (appearance == EditorToolbarAppearance.NATIVE) {
632
+ resolveColorAttr(
633
+ MaterialR.attr.colorSurfaceContainer,
634
+ MaterialR.attr.colorSurfaceContainerLow,
635
+ MaterialR.attr.colorSurface,
636
+ android.R.attr.colorBackground
637
+ )
638
+ } else {
639
+ theme?.backgroundColor ?: resolveColorAttr(
640
+ MaterialR.attr.colorSurface,
641
+ android.R.attr.colorBackground
642
+ )
643
+ }
555
644
  background = drawable
556
- elevation = 0f
645
+ outlineProvider = ViewOutlineProvider.BACKGROUND
646
+ clipToOutline = cornerRadiusPx > 0f
647
+ elevation = appliedChromeElevationPx
648
+ updateContainerLayout(appearance)
649
+ separators.forEach(::configureSeparator)
557
650
  }
558
651
 
559
652
  private fun updateButtonAppearance(button: AppCompatButton, enabled: Boolean, active: Boolean) {
560
- val textColor = when {
561
- !enabled -> theme?.buttonDisabledColor ?: Color.parseColor("#C7C7CC")
562
- active -> theme?.buttonActiveColor ?: Color.parseColor("#007AFF")
563
- else -> theme?.buttonColor ?: Color.parseColor("#666666")
653
+ val appearance = theme?.appearance ?: EditorToolbarAppearance.CUSTOM
654
+ applyButtonLayout(button, appearance)
655
+ val textColor = if (appearance == EditorToolbarAppearance.NATIVE) {
656
+ when {
657
+ !enabled -> withAlpha(
658
+ resolveColorAttr(
659
+ MaterialR.attr.colorOnSurface,
660
+ android.R.attr.textColorPrimary
661
+ ),
662
+ 0.38f
663
+ )
664
+ active -> resolveColorAttr(
665
+ MaterialR.attr.colorOnSecondaryContainer,
666
+ MaterialR.attr.colorOnPrimaryContainer,
667
+ MaterialR.attr.colorOnSurface,
668
+ android.R.attr.textColorPrimary
669
+ )
670
+ else -> resolveColorAttr(
671
+ MaterialR.attr.colorOnSurfaceVariant,
672
+ MaterialR.attr.colorOnSurface,
673
+ android.R.attr.textColorSecondary
674
+ )
675
+ }
676
+ } else {
677
+ when {
678
+ !enabled -> theme?.buttonDisabledColor ?: withAlpha(
679
+ resolveColorAttr(MaterialR.attr.colorOnSurface, android.R.attr.textColorPrimary),
680
+ 0.38f
681
+ )
682
+ active -> theme?.buttonActiveColor ?: resolveColorAttr(
683
+ MaterialR.attr.colorPrimary,
684
+ android.R.attr.textColorPrimary
685
+ )
686
+ else -> theme?.buttonColor ?: resolveColorAttr(
687
+ MaterialR.attr.colorOnSurfaceVariant,
688
+ MaterialR.attr.colorOnSurface,
689
+ android.R.attr.textColorSecondary
690
+ )
691
+ }
564
692
  }
565
- val backgroundColor = if (active) {
566
- theme?.buttonActiveBackgroundColor ?: Color.parseColor("#1F007AFF")
693
+ val backgroundColor = if (appearance == EditorToolbarAppearance.NATIVE) {
694
+ if (active) {
695
+ resolveColorAttr(
696
+ MaterialR.attr.colorSecondaryContainer,
697
+ MaterialR.attr.colorPrimaryContainer,
698
+ MaterialR.attr.colorSurfaceVariant,
699
+ android.R.attr.colorAccent
700
+ )
701
+ } else {
702
+ Color.TRANSPARENT
703
+ }
704
+ } else if (active) {
705
+ theme?.buttonActiveBackgroundColor ?: resolveColorAttr(
706
+ MaterialR.attr.colorPrimaryContainer,
707
+ MaterialR.attr.colorSecondaryContainer,
708
+ MaterialR.attr.colorSurfaceVariant,
709
+ android.R.attr.colorAccent
710
+ )
567
711
  } else {
568
712
  Color.TRANSPARENT
569
713
  }
570
- val buttonCornerRadiusPx = (theme?.buttonBorderRadius ?: 6f) * density
714
+ val buttonCornerRadiusPx = (theme?.resolvedButtonBorderRadius() ?: 6f) * density
571
715
  val drawable = GradientDrawable().apply {
572
716
  shape = GradientDrawable.RECTANGLE
573
717
  cornerRadius = buttonCornerRadiusPx
574
718
  setColor(backgroundColor)
575
719
  }
576
720
  appliedButtonCornerRadiusPx = buttonCornerRadiusPx
721
+ buttonBackgroundColors[button] = backgroundColor
577
722
  button.background = drawable
578
723
  button.setTextColor(textColor)
579
- button.alpha = if (enabled) 1f else 0.7f
724
+ button.alpha = if (enabled || appearance == EditorToolbarAppearance.NATIVE) 1f else 0.7f
725
+ button.refreshDrawableState()
726
+ button.invalidate()
580
727
  }
581
728
 
582
729
  private fun buttonState(
@@ -589,6 +736,10 @@ internal class EditorKeyboardToolbarView(context: Context) : HorizontalScrollVie
589
736
  val mark = item.mark.orEmpty()
590
737
  Pair(state.allowedMarks.contains(mark), state.marks[mark] == true)
591
738
  }
739
+ ToolbarItemKind.blockquote -> Pair(
740
+ state.commands["toggleBlockquote"] == true,
741
+ state.nodes["blockquote"] == true
742
+ )
592
743
  ToolbarItemKind.list -> when (item.listType) {
593
744
  ToolbarListType.bulletList -> Pair(
594
745
  state.commands["wrapBulletList"] == true,
@@ -617,15 +768,133 @@ internal class EditorKeyboardToolbarView(context: Context) : HorizontalScrollVie
617
768
  }
618
769
 
619
770
  private fun dp(value: Int): Int = (value * density).toInt()
771
+
772
+ private fun resolveColorAttr(vararg attrs: Int): Int =
773
+ resolveColorAttrOrNull(*attrs) ?: Color.TRANSPARENT
774
+
775
+ private fun resolveColorAttrOrNull(vararg attrs: Int): Int? {
776
+ val typedValue = TypedValue()
777
+ for (attr in attrs) {
778
+ if (!themedContext.theme.resolveAttribute(attr, typedValue, true)) {
779
+ continue
780
+ }
781
+ if (typedValue.resourceId != 0) {
782
+ AppCompatResources.getColorStateList(themedContext, typedValue.resourceId)
783
+ ?.defaultColor
784
+ ?.let { return it }
785
+ } else if (typedValue.type in TypedValue.TYPE_FIRST_COLOR_INT..TypedValue.TYPE_LAST_COLOR_INT) {
786
+ return typedValue.data
787
+ }
788
+ }
789
+ return null
790
+ }
791
+
792
+ private fun resolveDrawableAttr(attr: Int) =
793
+ TypedValue().let { typedValue ->
794
+ if (!themedContext.theme.resolveAttribute(attr, typedValue, true) || typedValue.resourceId == 0) {
795
+ null
796
+ } else {
797
+ AppCompatResources.getDrawable(themedContext, typedValue.resourceId)
798
+ }
799
+ }
800
+
801
+ private fun resolveSeparatorColor(): Int =
802
+ theme?.separatorColor
803
+ ?: theme?.borderColor
804
+ ?: resolveColorAttr(
805
+ MaterialR.attr.colorOutlineVariant,
806
+ MaterialR.attr.colorOutline,
807
+ android.R.attr.textColorHint
808
+ )
809
+
810
+ private fun updateContainerLayout(appearance: EditorToolbarAppearance) {
811
+ val isNative = appearance == EditorToolbarAppearance.NATIVE
812
+ val horizontalPadding = dp(
813
+ if (isNative) {
814
+ NATIVE_CONTAINER_HORIZONTAL_PADDING_DP
815
+ } else {
816
+ 12
817
+ }
818
+ )
819
+ val verticalPadding = dp(
820
+ if (isNative) {
821
+ NATIVE_CONTAINER_VERTICAL_PADDING_DP
822
+ } else {
823
+ 12
824
+ }
825
+ )
826
+ contentRow.setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding)
827
+ contentRow.gravity = if (isNative) {
828
+ Gravity.START or Gravity.CENTER_VERTICAL
829
+ } else {
830
+ Gravity.CENTER_VERTICAL
831
+ }
832
+ contentRow.minimumHeight = dp(
833
+ if (isNative) {
834
+ NATIVE_CONTAINER_HEIGHT_DP
835
+ } else {
836
+ 0
837
+ }
838
+ )
839
+ }
840
+
841
+ private fun applyButtonLayout(button: AppCompatButton, appearance: EditorToolbarAppearance) {
842
+ val isNative = appearance == EditorToolbarAppearance.NATIVE
843
+ val sizePx = dp(if (isNative) NATIVE_BUTTON_SIZE_DP else 36)
844
+ button.textSize = if (isNative) NATIVE_BUTTON_ICON_SIZE_SP else 16f
845
+ button.minWidth = sizePx
846
+ button.minimumWidth = sizePx
847
+ button.minHeight = sizePx
848
+ button.minimumHeight = sizePx
849
+ button.setPadding(
850
+ if (isNative) 0 else dp(10),
851
+ if (isNative) 0 else dp(8),
852
+ if (isNative) 0 else dp(10),
853
+ if (isNative) 0 else dp(8)
854
+ )
855
+ (button.layoutParams as? LinearLayout.LayoutParams)?.let { params ->
856
+ params.marginEnd = dp(if (isNative) NATIVE_ITEM_SPACING_DP else 6)
857
+ button.layoutParams = params
858
+ }
859
+ }
860
+
861
+ private fun configureSeparator(separator: View) {
862
+ val appearance = theme?.appearance ?: EditorToolbarAppearance.CUSTOM
863
+ val params = if (appearance == EditorToolbarAppearance.NATIVE) {
864
+ LinearLayout.LayoutParams(dp(1), dp(24)).apply {
865
+ marginStart = dp(NATIVE_GROUP_SPACING_DP / 2)
866
+ marginEnd = dp(NATIVE_GROUP_SPACING_DP / 2)
867
+ }
868
+ } else {
869
+ LinearLayout.LayoutParams(dp(1), dp(22)).apply {
870
+ marginStart = dp(6)
871
+ marginEnd = dp(6)
872
+ }
873
+ }
874
+ separator.layoutParams = params
875
+ separator.setBackgroundColor(
876
+ if (appearance == EditorToolbarAppearance.NATIVE) {
877
+ withAlpha(resolveSeparatorColor(), 0.6f)
878
+ } else {
879
+ resolveSeparatorColor()
880
+ }
881
+ )
882
+ }
620
883
  }
621
884
 
622
- private class MentionSuggestionChipView(
885
+ private fun withAlpha(color: Int, alphaFraction: Float): Int {
886
+ val alpha = (alphaFraction.coerceIn(0f, 1f) * 255).roundToInt()
887
+ return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))
888
+ }
889
+
890
+ internal class MentionSuggestionChipView(
623
891
  context: Context,
624
892
  val suggestion: NativeMentionSuggestion
625
893
  ) : LinearLayout(context) {
626
894
  private val titleView = AppCompatTextView(context)
627
895
  private val subtitleView = AppCompatTextView(context)
628
896
  private var theme: EditorMentionTheme? = null
897
+ private var toolbarAppearance: EditorToolbarAppearance = EditorToolbarAppearance.CUSTOM
629
898
  private val density = resources.displayMetrics.density
630
899
 
631
900
  init {
@@ -671,15 +940,34 @@ private class MentionSuggestionChipView(
671
940
  applyTheme(null)
672
941
  }
673
942
 
674
- fun applyTheme(theme: EditorMentionTheme?) {
943
+ fun applyTheme(
944
+ theme: EditorMentionTheme?,
945
+ toolbarAppearance: EditorToolbarAppearance = EditorToolbarAppearance.CUSTOM
946
+ ) {
675
947
  this.theme = theme
948
+ this.toolbarAppearance = toolbarAppearance
676
949
  val hasSubtitle = !suggestion.subtitle.isNullOrBlank()
677
950
  subtitleView.visibility = if (hasSubtitle) View.VISIBLE else View.GONE
678
951
  background = GradientDrawable().apply {
679
952
  shape = GradientDrawable.RECTANGLE
680
- cornerRadius = (theme?.borderRadius ?: 12f) * density
681
- setColor(theme?.backgroundColor ?: Color.parseColor("#F2F2F7"))
682
- val strokeWidth = ((theme?.borderWidth ?: 0f) * density).toInt()
953
+ cornerRadius = (if (toolbarAppearance == EditorToolbarAppearance.NATIVE) 20f else (theme?.borderRadius ?: 12f)) * density
954
+ setColor(
955
+ if (toolbarAppearance == EditorToolbarAppearance.NATIVE) {
956
+ Color.TRANSPARENT
957
+ } else {
958
+ theme?.backgroundColor ?: resolveColorAttr(
959
+ MaterialR.attr.colorSurfaceContainerLow,
960
+ MaterialR.attr.colorSurfaceVariant,
961
+ MaterialR.attr.colorSurface,
962
+ android.R.attr.colorBackground
963
+ )
964
+ }
965
+ )
966
+ val strokeWidth = if (toolbarAppearance == EditorToolbarAppearance.NATIVE) {
967
+ 0
968
+ } else {
969
+ ((theme?.borderWidth ?: 0f) * density).toInt()
970
+ }
683
971
  if (strokeWidth > 0) {
684
972
  setStroke(strokeWidth, theme?.borderColor ?: Color.TRANSPARENT)
685
973
  }
@@ -689,23 +977,93 @@ private class MentionSuggestionChipView(
689
977
 
690
978
  private fun updateAppearance(highlighted: Boolean) {
691
979
  val backgroundDrawable = background as? GradientDrawable
692
- val backgroundColor = if (highlighted) {
693
- theme?.optionHighlightedBackgroundColor ?: Color.parseColor("#1F007AFF")
980
+ val backgroundColor = if (toolbarAppearance == EditorToolbarAppearance.NATIVE) {
981
+ if (highlighted) {
982
+ resolveColorAttr(
983
+ MaterialR.attr.colorSecondaryContainer,
984
+ MaterialR.attr.colorPrimaryContainer,
985
+ MaterialR.attr.colorSurfaceVariant,
986
+ android.R.attr.colorAccent
987
+ )
988
+ } else {
989
+ Color.TRANSPARENT
990
+ }
991
+ } else if (highlighted) {
992
+ theme?.optionHighlightedBackgroundColor ?: resolveColorAttr(
993
+ MaterialR.attr.colorSecondaryContainer,
994
+ MaterialR.attr.colorPrimaryContainer,
995
+ MaterialR.attr.colorSurfaceVariant,
996
+ android.R.attr.colorAccent
997
+ )
694
998
  } else {
695
- theme?.backgroundColor ?: Color.parseColor("#F2F2F7")
999
+ theme?.backgroundColor ?: resolveColorAttr(
1000
+ MaterialR.attr.colorSurfaceContainerLow,
1001
+ MaterialR.attr.colorSurfaceVariant,
1002
+ MaterialR.attr.colorSurface,
1003
+ android.R.attr.colorBackground
1004
+ )
696
1005
  }
697
1006
  backgroundDrawable?.setColor(backgroundColor)
698
1007
  titleView.setTextColor(
699
- if (highlighted) {
700
- theme?.optionHighlightedTextColor ?: theme?.optionTextColor ?: Color.BLACK
1008
+ if (toolbarAppearance == EditorToolbarAppearance.NATIVE && !highlighted) {
1009
+ resolveColorAttr(
1010
+ MaterialR.attr.colorOnSurface,
1011
+ android.R.attr.textColorPrimary
1012
+ )
1013
+ } else if (highlighted) {
1014
+ theme?.optionHighlightedTextColor
1015
+ ?: theme?.optionTextColor
1016
+ ?: resolveColorAttr(
1017
+ MaterialR.attr.colorOnSecondaryContainer,
1018
+ MaterialR.attr.colorOnPrimaryContainer,
1019
+ MaterialR.attr.colorOnSurface,
1020
+ android.R.attr.textColorPrimary
1021
+ )
701
1022
  } else {
702
- theme?.optionTextColor ?: theme?.textColor ?: Color.BLACK
1023
+ theme?.optionTextColor
1024
+ ?: theme?.textColor
1025
+ ?: resolveColorAttr(
1026
+ MaterialR.attr.colorOnSurface,
1027
+ android.R.attr.textColorPrimary
1028
+ )
1029
+ }
1030
+ )
1031
+ subtitleView.setTextColor(
1032
+ if (toolbarAppearance == EditorToolbarAppearance.NATIVE) {
1033
+ resolveColorAttr(
1034
+ MaterialR.attr.colorOnSurfaceVariant,
1035
+ android.R.attr.textColorSecondary
1036
+ )
1037
+ } else {
1038
+ theme?.optionSecondaryTextColor ?: resolveColorAttr(
1039
+ MaterialR.attr.colorOnSurfaceVariant,
1040
+ android.R.attr.textColorSecondary
1041
+ )
703
1042
  }
704
1043
  )
705
- subtitleView.setTextColor(theme?.optionSecondaryTextColor ?: Color.DKGRAY)
706
1044
  }
707
1045
 
1046
+ fun usesNativeAppearanceForTesting(): Boolean =
1047
+ toolbarAppearance == EditorToolbarAppearance.NATIVE
1048
+
708
1049
  private fun dp(value: Int): Int = (value * density).toInt()
1050
+
1051
+ private fun resolveColorAttr(vararg attrs: Int): Int {
1052
+ val typedValue = TypedValue()
1053
+ for (attr in attrs) {
1054
+ if (!context.theme.resolveAttribute(attr, typedValue, true)) {
1055
+ continue
1056
+ }
1057
+ if (typedValue.resourceId != 0) {
1058
+ AppCompatResources.getColorStateList(context, typedValue.resourceId)
1059
+ ?.defaultColor
1060
+ ?.let { return it }
1061
+ } else if (typedValue.type in TypedValue.TYPE_FIRST_COLOR_INT..TypedValue.TYPE_LAST_COLOR_INT) {
1062
+ return typedValue.data
1063
+ }
1064
+ }
1065
+ return Color.TRANSPARENT
1066
+ }
709
1067
  }
710
1068
 
711
1069
  private fun JSONObject.optNullableString(key: String): String? {