diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/decoration/EditorDecorationContainer.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/decoration/EditorDecorationContainer.kt index a6bf3dfc73..bb28700c96 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/decoration/EditorDecorationContainer.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/decoration/EditorDecorationContainer.kt @@ -2,9 +2,7 @@ package com.anytypeio.anytype.core_ui.features.editor.decoration import android.content.Context import android.util.AttributeSet -import android.view.View import android.widget.FrameLayout -import androidx.core.view.updateLayoutParams import com.anytypeio.anytype.core_ui.R import com.anytypeio.anytype.core_ui.extensions.veryLight import com.anytypeio.anytype.presentation.editor.editor.ThemeColor @@ -44,6 +42,8 @@ class EditorDecorationContainer @JvmOverloads constructor( .toInt() private val defaultGraphicContainerWidth = resources.getDimensionPixelSize(R.dimen.default_graphic_container_width) + private val defaultTextBottomExtraSpace = resources.getDimension(R.dimen.default_text_bottom_extra_space).toInt() + fun decorate( decorations: List, onApplyContentOffset: (OffsetLeft, OffsetBottom) -> Unit = { _, _ -> } @@ -96,12 +96,9 @@ class EditorDecorationContainer @JvmOverloads constructor( BlockView.Decoration.Style.Header.H3 -> { topMargin = defaultHeaderThreeExtraSpaceTop bottomOffset += defaultHeaderThreeExtraSpaceBottom - } - BlockView.Decoration.Style.Card -> { - } else -> { - // Do nothing + // TODO } } } diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/text/Bulleted.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/text/Bulleted.kt index 8291f379fe..15071c66ab 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/text/Bulleted.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/text/Bulleted.kt @@ -3,13 +3,19 @@ package com.anytypeio.anytype.core_ui.features.editor.holders.text import android.graphics.drawable.Drawable import android.text.Editable import android.view.View +import android.widget.FrameLayout import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.DrawableCompat import androidx.core.view.updateLayoutParams +import androidx.core.view.updatePadding +import androidx.recyclerview.widget.RecyclerView +import com.anytypeio.anytype.core_ui.BuildConfig import com.anytypeio.anytype.core_ui.R import com.anytypeio.anytype.core_ui.databinding.ItemBlockBulletedBinding import com.anytypeio.anytype.core_ui.extensions.dark import com.anytypeio.anytype.core_ui.features.editor.SupportNesting +import com.anytypeio.anytype.core_ui.features.editor.decoration.DecoratableViewHolder +import com.anytypeio.anytype.core_ui.features.editor.decoration.EditorDecorationContainer import com.anytypeio.anytype.core_ui.features.editor.marks import com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget import com.anytypeio.anytype.core_utils.ext.dimen @@ -22,11 +28,11 @@ import com.anytypeio.anytype.presentation.editor.editor.slash.SlashEvent class Bulleted( val binding: ItemBlockBulletedBinding, clicked: (ListenerType) -> Unit, -) : Text(binding.root, clicked), SupportNesting { +) : Text(binding.root, clicked), SupportNesting, DecoratableViewHolder { val indent: View = binding.bulletIndent val bullet = binding.bullet - private val container = binding.bulletBlockContainer + private val container = binding.graphicPlusTextContainer override val content: TextInputWidget = binding.bulletedListContent override val root: View = itemView @@ -36,6 +42,8 @@ class Bulleted( private val mentionUncheckedIcon: Drawable? private val mentionInitialsSize: Float + override val decoratableContainer: EditorDecorationContainer = binding.decorationContainer + init { setup() with(itemView.context) { @@ -47,6 +55,24 @@ class Bulleted( mentionCheckedIcon = ContextCompat.getDrawable(this, R.drawable.ic_task_1_text_16) mentionInitialsSize = resources.getDimension(R.dimen.mention_span_initials_size_default) } + applyDefaultOffsets() + } + + private fun applyDefaultOffsets() { + if (!BuildConfig.NESTED_DECORATION_ENABLED) { + binding.root.updatePadding( + left = dimen(R.dimen.default_document_item_padding_start), + right = dimen(R.dimen.default_document_item_padding_end) + ) + binding.root.updateLayoutParams { + topMargin = dimen(R.dimen.default_document_item_margin_top) + bottomMargin = dimen(R.dimen.default_document_item_margin_bottom) + } + binding.graphicPlusTextContainer.updatePadding( + left = dimen(R.dimen.default_document_content_padding_start), + right = dimen(R.dimen.default_document_content_padding_end), + ) + } } fun bind( @@ -104,10 +130,31 @@ class Bulleted( } override fun indentize(item: BlockView.Indentable) { - indent.updateLayoutParams { width = item.indent * dimen(R.dimen.indent) } + if (!BuildConfig.NESTED_DECORATION_ENABLED) { + indent.updateLayoutParams { width = item.indent * dimen(R.dimen.indent) } + } } override fun select(item: BlockView.Selectable) { container.isSelected = item.isSelected } + + override fun applyDecorations(decorations: List) { + if (BuildConfig.NESTED_DECORATION_ENABLED) { + decoratableContainer.decorate( + decorations = decorations + ) { offsetLeft, offsetBottom -> + binding.graphicPlusTextContainer.updateLayoutParams { + marginStart = dimen(R.dimen.default_indent) + offsetLeft + marginEnd = dimen(R.dimen.dp_8) + bottomMargin = offsetBottom + // TODO handle top and bottom offsets + } + } + } + } + + override fun onDecorationsChanged(decorations: List) { + applyDecorations(decorations = decorations) + } } \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/text/Checkbox.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/text/Checkbox.kt index f518e083aa..b6c7c2d73d 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/text/Checkbox.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/text/Checkbox.kt @@ -3,12 +3,18 @@ package com.anytypeio.anytype.core_ui.features.editor.holders.text import android.graphics.drawable.Drawable import android.text.Editable import android.view.View +import android.widget.FrameLayout import android.widget.ImageView import androidx.core.content.ContextCompat +import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding +import androidx.recyclerview.widget.RecyclerView +import com.anytypeio.anytype.core_ui.BuildConfig import com.anytypeio.anytype.core_ui.R import com.anytypeio.anytype.core_ui.databinding.ItemBlockCheckboxBinding import com.anytypeio.anytype.core_ui.features.editor.SupportNesting +import com.anytypeio.anytype.core_ui.features.editor.decoration.DecoratableViewHolder +import com.anytypeio.anytype.core_ui.features.editor.decoration.EditorDecorationContainer import com.anytypeio.anytype.core_ui.features.editor.marks import com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget import com.anytypeio.anytype.core_utils.ext.dimen @@ -20,12 +26,12 @@ import com.anytypeio.anytype.presentation.editor.editor.slash.SlashEvent class Checkbox( val binding: ItemBlockCheckboxBinding, clicked: (ListenerType) -> Unit, -) : Text(binding.root, clicked), SupportNesting { +) : Text(binding.root, clicked), SupportNesting, DecoratableViewHolder { var mode = BlockView.Mode.EDIT val checkbox: ImageView = binding.checkboxIcon - private val container = binding.checkboxBlockContentContainer + private val container = binding.graphicPlusTextContainer override val content: TextInputWidget = binding.checkboxContent override val root: View = itemView @@ -35,6 +41,8 @@ class Checkbox( private val mentionUncheckedIcon: Drawable? private val mentionInitialsSize: Float + override val decoratableContainer: EditorDecorationContainer = binding.decorationContainer + init { setup() with(itemView.context) { @@ -46,6 +54,24 @@ class Checkbox( mentionCheckedIcon = ContextCompat.getDrawable(this, R.drawable.ic_task_1_text_16) mentionInitialsSize = resources.getDimension(R.dimen.mention_span_initials_size_default) } + applyDefaultOffsets() + } + + private fun applyDefaultOffsets() { + if (!BuildConfig.NESTED_DECORATION_ENABLED) { + binding.root.updatePadding( + left = dimen(R.dimen.default_document_item_padding_start), + right = dimen(R.dimen.default_document_item_padding_end) + ) + binding.root.updateLayoutParams { + topMargin = dimen(R.dimen.default_document_item_margin_top) + bottomMargin = dimen(R.dimen.default_document_item_margin_bottom) + } + binding.graphicPlusTextContainer.updatePadding( + left = dimen(R.dimen.default_document_content_padding_start), + right = dimen(R.dimen.default_document_content_padding_end), + ) + } } fun bind( @@ -99,7 +125,9 @@ class Checkbox( override fun getMentionInitialsSize(): Float = mentionInitialsSize override fun indentize(item: BlockView.Indentable) { - checkbox.updatePadding(left = item.indent * dimen(R.dimen.indent)) + if (!BuildConfig.NESTED_DECORATION_ENABLED) { + checkbox.updatePadding(left = item.indent * dimen(R.dimen.indent)) + } } override fun enableEditMode() { @@ -115,4 +143,23 @@ class Checkbox( override fun select(item: BlockView.Selectable) { container.isSelected = item.isSelected } + + override fun applyDecorations(decorations: List) { + if (BuildConfig.NESTED_DECORATION_ENABLED) { + decoratableContainer.decorate( + decorations = decorations + ) { offsetLeft, offsetBottom -> + binding.graphicPlusTextContainer.updateLayoutParams { + marginStart = dimen(R.dimen.default_indent) + offsetLeft + marginEnd = dimen(R.dimen.dp_8) + bottomMargin = offsetBottom + // TODO handle top and bottom offsets + } + } + } + } + + override fun onDecorationsChanged(decorations: List) { + applyDecorations(decorations = decorations) + } } \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/text/Numbered.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/text/Numbered.kt index d0d8f0e18a..bc48c6cc12 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/text/Numbered.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/text/Numbered.kt @@ -4,14 +4,20 @@ import android.graphics.drawable.Drawable import android.text.Editable import android.view.Gravity import android.view.View +import android.widget.FrameLayout import android.widget.LinearLayout import androidx.core.content.ContextCompat import androidx.core.view.updateLayoutParams +import androidx.core.view.updatePadding +import androidx.recyclerview.widget.RecyclerView +import com.anytypeio.anytype.core_ui.BuildConfig import com.anytypeio.anytype.core_ui.R import com.anytypeio.anytype.core_ui.databinding.ItemBlockNumberedBinding import com.anytypeio.anytype.core_ui.extensions.setTextColor import com.anytypeio.anytype.core_ui.features.editor.BlockViewDiffUtil import com.anytypeio.anytype.core_ui.features.editor.SupportNesting +import com.anytypeio.anytype.core_ui.features.editor.decoration.DecoratableViewHolder +import com.anytypeio.anytype.core_ui.features.editor.decoration.EditorDecorationContainer import com.anytypeio.anytype.core_ui.features.editor.marks import com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget import com.anytypeio.anytype.core_utils.ext.addDot @@ -24,9 +30,9 @@ import com.anytypeio.anytype.presentation.editor.editor.slash.SlashEvent class Numbered( val binding: ItemBlockNumberedBinding, clicked: (ListenerType) -> Unit, -) : Text(binding.root, clicked), SupportNesting { +) : Text(binding.root, clicked), SupportNesting, DecoratableViewHolder { - private val container = binding.numberedBlockContentContainer + private val container = binding.graphicPlusTextContainer val number = binding.number override val content: TextInputWidget = binding.numberedListContent override val root: View = itemView @@ -37,6 +43,8 @@ class Numbered( private val mentionUncheckedIcon: Drawable? private val mentionInitialsSize: Float + override val decoratableContainer: EditorDecorationContainer = binding.decorationContainer + init { setup() with(itemView.context) { @@ -48,6 +56,24 @@ class Numbered( mentionCheckedIcon = ContextCompat.getDrawable(this, R.drawable.ic_task_1_text_16) mentionInitialsSize = resources.getDimension(R.dimen.mention_span_initials_size_default) } + applyDefaultOffsets() + } + + private fun applyDefaultOffsets() { + if (!BuildConfig.NESTED_DECORATION_ENABLED) { + binding.root.updatePadding( + left = dimen(R.dimen.default_document_item_padding_start), + right = dimen(R.dimen.default_document_item_padding_end) + ) + binding.root.updateLayoutParams { + topMargin = dimen(R.dimen.default_document_item_margin_top) + bottomMargin = dimen(R.dimen.default_document_item_margin_bottom) + } + binding.graphicPlusTextContainer.updatePadding( + left = dimen(R.dimen.default_document_content_padding_start), + right = dimen(R.dimen.default_document_content_padding_end), + ) + } } fun bind( @@ -127,17 +153,38 @@ class Numbered( } override fun indentize(item: BlockView.Indentable) { - number.updateLayoutParams { - setMargins( - item.indent * dimen(R.dimen.indent), - 0, - 0, - 0 - ) + if (!BuildConfig.NESTED_DECORATION_ENABLED) { + number.updateLayoutParams { + setMargins( + item.indent * dimen(R.dimen.indent), + 0, + 0, + 0 + ) + } } } override fun select(item: BlockView.Selectable) { container.isSelected = item.isSelected } + + override fun applyDecorations(decorations: List) { + if (BuildConfig.NESTED_DECORATION_ENABLED) { + decoratableContainer.decorate( + decorations = decorations + ) { offsetLeft, offsetBottom -> + binding.graphicPlusTextContainer.updateLayoutParams { + marginStart = dimen(R.dimen.default_indent) + offsetLeft + marginEnd = dimen(R.dimen.dp_8) + bottomMargin = offsetBottom + // TODO handle top and bottom offsets + } + } + } + } + + override fun onDecorationsChanged(decorations: List) { + applyDecorations(decorations = decorations) + } } \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/text/Toggle.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/text/Toggle.kt index 0f6f7fa4a0..072b464da9 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/text/Toggle.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/text/Toggle.kt @@ -3,12 +3,19 @@ package com.anytypeio.anytype.core_ui.features.editor.holders.text import android.graphics.drawable.Drawable import android.text.Editable import android.view.View +import android.widget.FrameLayout import androidx.core.content.ContextCompat import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import androidx.core.view.updatePadding +import androidx.recyclerview.widget.RecyclerView +import com.anytypeio.anytype.core_ui.BuildConfig import com.anytypeio.anytype.core_ui.R import com.anytypeio.anytype.core_ui.databinding.ItemBlockToggleBinding import com.anytypeio.anytype.core_ui.features.editor.BlockViewDiffUtil import com.anytypeio.anytype.core_ui.features.editor.SupportNesting +import com.anytypeio.anytype.core_ui.features.editor.decoration.DecoratableViewHolder +import com.anytypeio.anytype.core_ui.features.editor.decoration.EditorDecorationContainer import com.anytypeio.anytype.core_ui.features.editor.marks import com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget import com.anytypeio.anytype.core_utils.ext.dimen @@ -20,14 +27,14 @@ import com.anytypeio.anytype.presentation.editor.editor.slash.SlashEvent class Toggle( val binding: ItemBlockToggleBinding, clicked: (ListenerType) -> Unit, -) : Text(binding.root, clicked), SupportNesting { +) : Text(binding.root, clicked), SupportNesting, DecoratableViewHolder { private var mode = BlockView.Mode.EDIT val toggle = binding.toggle private val line = binding.guideline private val placeholder = binding.togglePlaceholder - private val container = binding.toolbarBlockContentContainer + private val container = binding.graphicPlusTextContainer override val content: TextInputWidget = binding.toggleContent override val root: View = itemView @@ -37,6 +44,8 @@ class Toggle( private val mentionUncheckedIcon: Drawable? private val mentionInitialsSize: Float + override val decoratableContainer: EditorDecorationContainer = binding.decorationContainer + init { setup() with(itemView.context) { @@ -48,6 +57,24 @@ class Toggle( mentionCheckedIcon = ContextCompat.getDrawable(this, R.drawable.ic_task_1_text_16) mentionInitialsSize = resources.getDimension(R.dimen.mention_span_initials_size_default) } + applyDefaultOffsets() + } + + private fun applyDefaultOffsets() { + if (!BuildConfig.NESTED_DECORATION_ENABLED) { + binding.root.updatePadding( + left = dimen(R.dimen.default_document_item_padding_start), + right = dimen(R.dimen.default_document_item_padding_end) + ) + binding.root.updateLayoutParams { + topMargin = dimen(R.dimen.default_document_item_margin_top) + bottomMargin = dimen(R.dimen.default_document_item_margin_bottom) + } + binding.graphicPlusTextContainer.updatePadding( + left = dimen(R.dimen.default_document_content_padding_start), + right = dimen(R.dimen.default_document_content_padding_end), + ) + } } fun bind( @@ -94,7 +121,9 @@ class Toggle( override fun getMentionInitialsSize(): Float = mentionInitialsSize override fun indentize(item: BlockView.Indentable) { - line.setGuidelineBegin(item.indent * dimen(R.dimen.indent)) + if (!BuildConfig.NESTED_DECORATION_ENABLED) { + line.setGuidelineBegin(item.indent * dimen(R.dimen.indent)) + } } override fun select(item: BlockView.Selectable) { @@ -148,6 +177,25 @@ class Toggle( } } + override fun applyDecorations(decorations: List) { + if (BuildConfig.NESTED_DECORATION_ENABLED) { + decoratableContainer.decorate( + decorations = decorations + ) { offsetLeft, offsetBottom -> + binding.graphicPlusTextContainer.updateLayoutParams { + marginStart = dimen(R.dimen.default_indent) + offsetLeft + marginEnd = dimen(R.dimen.dp_8) + bottomMargin = offsetBottom + // TODO handle top and bottom offsets + } + } + } + } + + override fun onDecorationsChanged(decorations: List) { + applyDecorations(decorations = decorations) + } + companion object { /** * Rotation value for a toggle icon for expanded state. diff --git a/core-ui/src/main/res/layout/item_block_bulleted.xml b/core-ui/src/main/res/layout/item_block_bulleted.xml index 50cf853e90..b54886437b 100644 --- a/core-ui/src/main/res/layout/item_block_bulleted.xml +++ b/core-ui/src/main/res/layout/item_block_bulleted.xml @@ -2,21 +2,20 @@ + android:layout_height="wrap_content"> + + @@ -36,7 +35,7 @@ diff --git a/core-ui/src/main/res/layout/item_block_checkbox.xml b/core-ui/src/main/res/layout/item_block_checkbox.xml index 72eb263e81..044be4f33b 100644 --- a/core-ui/src/main/res/layout/item_block_checkbox.xml +++ b/core-ui/src/main/res/layout/item_block_checkbox.xml @@ -1,19 +1,21 @@ + + diff --git a/core-ui/src/main/res/layout/item_block_numbered.xml b/core-ui/src/main/res/layout/item_block_numbered.xml index 2e8e1a16b9..6c5270b6a1 100644 --- a/core-ui/src/main/res/layout/item_block_numbered.xml +++ b/core-ui/src/main/res/layout/item_block_numbered.xml @@ -2,18 +2,20 @@ + android:layout_height="wrap_content"> + + @@ -32,7 +34,7 @@ diff --git a/core-ui/src/main/res/layout/item_block_toggle.xml b/core-ui/src/main/res/layout/item_block_toggle.xml index b4ac7890d7..1dc614792f 100644 --- a/core-ui/src/main/res/layout/item_block_toggle.xml +++ b/core-ui/src/main/res/layout/item_block_toggle.xml @@ -2,18 +2,20 @@ + + 24dp 4dp 8dp + 2dp \ No newline at end of file diff --git a/core-ui/src/test/java/com/anytypeio/anytype/core_ui/BlockViewStubs.kt b/core-ui/src/test/java/com/anytypeio/anytype/core_ui/BlockViewStubs.kt index b552b0f2b9..970d42a6fa 100644 --- a/core-ui/src/test/java/com/anytypeio/anytype/core_ui/BlockViewStubs.kt +++ b/core-ui/src/test/java/com/anytypeio/anytype/core_ui/BlockViewStubs.kt @@ -37,4 +37,140 @@ fun StubParagraphView( ghostEditorSelection = ghostSelection, cursor = cursor, alignment = alignment +) + +fun StubNumberedView( + id: Id = MockDataFactory.randomString(), + text: String = MockDataFactory.randomString(), + marks: List = emptyList(), + isFocused: Boolean = MockDataFactory.randomBoolean(), + isSelected: Boolean = MockDataFactory.randomBoolean(), + color: String? = null, + indent: Indent = 0, + searchFields: List = emptyList(), + backgroundColor: String? = null, + mode: BlockView.Mode = BlockView.Mode.EDIT, + decorations: List = emptyList(), + ghostSelection: IntRange? = null, + cursor: Int? = null, + alignment: Alignment? = null, + number: Int = 1 +) : BlockView.Text.Numbered = BlockView.Text.Numbered( + id = id, + text = text, + marks = marks, + isFocused = isFocused, + isSelected = isSelected, + color = color, + indent = indent, + searchFields = searchFields, + backgroundColor = backgroundColor, + mode = mode, + decorations = decorations, + ghostEditorSelection = ghostSelection, + cursor = cursor, + alignment = alignment, + number = number +) + +fun StubBulletedView( + id: Id = MockDataFactory.randomString(), + text: String = MockDataFactory.randomString(), + marks: List = emptyList(), + isFocused: Boolean = MockDataFactory.randomBoolean(), + isSelected: Boolean = MockDataFactory.randomBoolean(), + color: String? = null, + indent: Indent = 0, + searchFields: List = emptyList(), + backgroundColor: String? = null, + mode: BlockView.Mode = BlockView.Mode.EDIT, + decorations: List = emptyList(), + ghostSelection: IntRange? = null, + cursor: Int? = null, + alignment: Alignment? = null, +) : BlockView.Text.Bulleted = BlockView.Text.Bulleted( + id = id, + text = text, + marks = marks, + isFocused = isFocused, + isSelected = isSelected, + color = color, + indent = indent, + searchFields = searchFields, + backgroundColor = backgroundColor, + mode = mode, + decorations = decorations, + ghostEditorSelection = ghostSelection, + cursor = cursor, + alignment = alignment +) + +fun StubCheckboxView( + id: Id = MockDataFactory.randomString(), + text: String = MockDataFactory.randomString(), + marks: List = emptyList(), + isFocused: Boolean = MockDataFactory.randomBoolean(), + isSelected: Boolean = MockDataFactory.randomBoolean(), + color: String? = null, + indent: Indent = 0, + searchFields: List = emptyList(), + backgroundColor: String? = null, + mode: BlockView.Mode = BlockView.Mode.EDIT, + decorations: List = emptyList(), + ghostSelection: IntRange? = null, + cursor: Int? = null, + alignment: Alignment? = null, + isChecked: Boolean = false +) : BlockView.Text.Checkbox = BlockView.Text.Checkbox( + id = id, + text = text, + marks = marks, + isFocused = isFocused, + isSelected = isSelected, + color = color, + indent = indent, + searchFields = searchFields, + backgroundColor = backgroundColor, + mode = mode, + decorations = decorations, + ghostEditorSelection = ghostSelection, + cursor = cursor, + alignment = alignment, + isChecked = isChecked +) + +fun StubToggleView( + id: Id = MockDataFactory.randomString(), + text: String = MockDataFactory.randomString(), + marks: List = emptyList(), + isFocused: Boolean = MockDataFactory.randomBoolean(), + isSelected: Boolean = MockDataFactory.randomBoolean(), + color: String? = null, + indent: Indent = 0, + searchFields: List = emptyList(), + backgroundColor: String? = null, + mode: BlockView.Mode = BlockView.Mode.EDIT, + decorations: List = emptyList(), + ghostSelection: IntRange? = null, + cursor: Int? = null, + alignment: Alignment? = null, + isEmpty: Boolean = false, + toggled: Boolean = false +) : BlockView.Text.Toggle = BlockView.Text.Toggle( + id = id, + text = text, + marks = marks, + isFocused = isFocused, + isSelected = isSelected, + color = color, + indent = indent, + searchFields = searchFields, + backgroundColor = backgroundColor, + mode = mode, + decorations = decorations, + ghostEditorSelection = ghostSelection, + cursor = cursor, + alignment = alignment, + isEmpty = isEmpty, + toggled = toggled ) \ No newline at end of file diff --git a/core-ui/src/test/java/com/anytypeio/anytype/core_ui/uitests/editor/EditorNestedDecorationListBlockTest.kt b/core-ui/src/test/java/com/anytypeio/anytype/core_ui/uitests/editor/EditorNestedDecorationListBlockTest.kt new file mode 100644 index 0000000000..16e4c8a984 --- /dev/null +++ b/core-ui/src/test/java/com/anytypeio/anytype/core_ui/uitests/editor/EditorNestedDecorationListBlockTest.kt @@ -0,0 +1,294 @@ +package com.anytypeio.anytype.core_ui.uitests.editor + +import android.content.Context +import android.os.Build +import androidx.fragment.app.Fragment +import androidx.fragment.app.testing.FragmentScenario +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.ViewInteraction +import com.anytypeio.anytype.core_ui.BuildConfig +import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.StubBulletedView +import com.anytypeio.anytype.core_ui.StubCheckboxView +import com.anytypeio.anytype.core_ui.StubNumberedView +import com.anytypeio.anytype.core_ui.StubToggleView +import com.anytypeio.anytype.core_ui.extensions.veryLight +import com.anytypeio.anytype.core_ui.uitests.givenAdapter +import com.anytypeio.anytype.presentation.editor.editor.ThemeColor +import com.anytypeio.anytype.presentation.editor.editor.model.BlockView +import com.anytypeio.anytype.test_utils.TestFragment +import com.anytypeio.anytype.test_utils.utils.checkHasChildViewCount +import com.anytypeio.anytype.test_utils.utils.checkHasMarginStart +import com.anytypeio.anytype.test_utils.utils.checkHasViewGroupChildWithBackground +import com.anytypeio.anytype.test_utils.utils.checkHasViewGroupChildWithMarginLeft +import com.anytypeio.anytype.test_utils.utils.checkIsDisplayed +import com.anytypeio.anytype.test_utils.utils.onItemView +import com.anytypeio.anytype.test_utils.utils.rVMatcher +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config( + manifest = Config.NONE, + sdk = [Build.VERSION_CODES.P], + instrumentedPackages = [ "androidx.loader.content" ] +) +class EditorNestedDecorationListBlockTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + private lateinit var scenario: FragmentScenario + + @Before + fun setUp() { + context.setTheme(R.style.Theme_MaterialComponents) + scenario = launchFragmentInContainer() + } + + /** + * Block with background + * ...Numbered block with background (rendered block) + */ + @Test + fun `numbered should have two backgrounds with indentation - when current block of another block`() { + if (!BuildConfig.NESTED_DECORATION_ENABLED) return + scenario.onFragment { + + // SETUP + + val bg1 = ThemeColor.YELLOW + val bg2 = ThemeColor.ORANGE + + val numbered = StubNumberedView( + indent = 1, + decorations = listOf( + BlockView.Decoration( + background = bg1 + ), + BlockView.Decoration( + background = bg2 + ) + ), + backgroundColor = bg2.code + ) + + val recycler = givenRecycler(it) + val adapter = givenAdapter(listOf(numbered)) + recycler.adapter = adapter + + val rvMatcher = com.anytypeio.anytype.test_utils.R.id.recycler.rVMatcher() + + // TESTING + + val decorationContainerView = rvMatcher.onItemView(0, R.id.decorationContainer) + val graphicPlusTextContainerView = rvMatcher.onItemView(0, R.id.graphicPlusTextContainer) + + // Checking our decorations + + `first child view should have its background and zero margin, second child view should have its background and one-indent margin`( + decorationContainerView, bg1, bg2 + ) + + // Checking content left indentation + + graphicPlusTextContainerView.checkHasMarginStart( + marginStart = context.resources.getDimension(R.dimen.default_indent).toInt() * 2 + ) + } + } + + /** + * Block with background + * ...Bulleted block with background (rendered block) + */ + @Test + fun `bulleted should have two backgrounds with indentation - when current block of another block`() { + if (!BuildConfig.NESTED_DECORATION_ENABLED) return + scenario.onFragment { + + // SETUP + + val bg1 = ThemeColor.YELLOW + val bg2 = ThemeColor.ORANGE + + val bulleted = StubBulletedView( + indent = 1, + decorations = listOf( + BlockView.Decoration( + background = bg1 + ), + BlockView.Decoration( + background = bg2 + ) + ), + backgroundColor = bg2.code + ) + + val recycler = givenRecycler(it) + val adapter = givenAdapter(listOf(bulleted)) + recycler.adapter = adapter + + val rvMatcher = com.anytypeio.anytype.test_utils.R.id.recycler.rVMatcher() + + // TESTING + + val decorationContainerView = rvMatcher.onItemView(0, R.id.decorationContainer) + val graphicPlusTextContainerView = rvMatcher.onItemView(0, R.id.graphicPlusTextContainer) + + // Checking our decorations + + `first child view should have its background and zero margin, second child view should have its background and one-indent margin`( + decorationContainerView, bg1, bg2 + ) + + // Checking content left indentation + + graphicPlusTextContainerView.checkHasMarginStart( + marginStart = context.resources.getDimension(R.dimen.default_indent).toInt() * 2 + ) + } + } + + /** + * Block with background + * ...Checkbox block with background (rendered block) + */ + @Test + fun `checkbox should have two backgrounds with indentation - when current block of another block`() { + if (!BuildConfig.NESTED_DECORATION_ENABLED) return + scenario.onFragment { + + // SETUP + + val bg1 = ThemeColor.YELLOW + val bg2 = ThemeColor.ORANGE + + val bulleted = StubCheckboxView( + indent = 1, + decorations = listOf( + BlockView.Decoration( + background = bg1 + ), + BlockView.Decoration( + background = bg2 + ) + ), + backgroundColor = bg2.code + ) + + val recycler = givenRecycler(it) + val adapter = givenAdapter(listOf(bulleted)) + recycler.adapter = adapter + + val rvMatcher = com.anytypeio.anytype.test_utils.R.id.recycler.rVMatcher() + + // TESTING + + val decorationContainerView = rvMatcher.onItemView(0, R.id.decorationContainer) + val graphicPlusTextContainerView = rvMatcher.onItemView(0, R.id.graphicPlusTextContainer) + + // Checking our decorations + + `first child view should have its background and zero margin, second child view should have its background and one-indent margin`( + decorationContainerView, bg1, bg2 + ) + + // Checking content left indentation + + graphicPlusTextContainerView.checkHasMarginStart( + marginStart = context.resources.getDimension(R.dimen.default_indent).toInt() * 2 + ) + } + } + + /** + * Block with background + * ...Toggle block with background (rendered block) + */ + @Test + fun `toggle should have two backgrounds with indentation - when current block of another block`() { + if (!BuildConfig.NESTED_DECORATION_ENABLED) return + scenario.onFragment { + + // SETUP + + val bg1 = ThemeColor.YELLOW + val bg2 = ThemeColor.ORANGE + + val bulleted = StubToggleView( + indent = 1, + decorations = listOf( + BlockView.Decoration( + background = bg1 + ), + BlockView.Decoration( + background = bg2 + ) + ), + backgroundColor = bg2.code + ) + + val recycler = givenRecycler(it) + val adapter = givenAdapter(listOf(bulleted)) + recycler.adapter = adapter + + val rvMatcher = com.anytypeio.anytype.test_utils.R.id.recycler.rVMatcher() + + // TESTING + + val decorationContainerView = rvMatcher.onItemView(0, R.id.decorationContainer) + val graphicPlusTextContainerView = rvMatcher.onItemView(0, R.id.graphicPlusTextContainer) + + // Checking our decorations + + `first child view should have its background and zero margin, second child view should have its background and one-indent margin`( + decorationContainerView, bg1, bg2 + ) + + // Checking content left indentation + + graphicPlusTextContainerView.checkHasMarginStart( + marginStart = context.resources.getDimension(R.dimen.default_indent).toInt() * 2 + ) + } + } + + private fun `first child view should have its background and zero margin, second child view should have its background and one-indent margin`( + decorationContainerView: ViewInteraction, + bg1: ThemeColor, + bg2: ThemeColor + ) { + decorationContainerView.checkIsDisplayed() + decorationContainerView.checkHasChildViewCount(2) + + decorationContainerView.checkHasViewGroupChildWithBackground( + pos = 0, + background = context.resources.veryLight(bg1, 0), + ) + decorationContainerView.checkHasViewGroupChildWithMarginLeft( + pos = 0, + margin = 0 + ) + + decorationContainerView.checkHasViewGroupChildWithBackground( + pos = 1, + background = context.resources.veryLight(bg2, 0), + ) + decorationContainerView.checkHasViewGroupChildWithMarginLeft( + pos = 1, + margin = context.resources.getDimension(R.dimen.default_indent).toInt() + ) + } + + private fun givenRecycler(fr: Fragment): RecyclerView { + val root = checkNotNull(fr.view) + return root.findViewById(com.anytypeio.anytype.test_utils.R.id.recycler).apply { + layoutManager = LinearLayoutManager(context) + } + } +} \ No newline at end of file diff --git a/test/android-utils/src/main/java/com/anytypeio/anytype/test_utils/utils/EspressoExt.kt b/test/android-utils/src/main/java/com/anytypeio/anytype/test_utils/utils/EspressoExt.kt index 61c61c20fe..90d46cffc4 100644 --- a/test/android-utils/src/main/java/com/anytypeio/anytype/test_utils/utils/EspressoExt.kt +++ b/test/android-utils/src/main/java/com/anytypeio/anytype/test_utils/utils/EspressoExt.kt @@ -16,8 +16,11 @@ import androidx.test.espresso.matcher.ViewMatchers.withInputType import com.anytypeio.anytype.test_utils.utils.TestUtils.withRecyclerView import com.anytypeio.anytype.test_utils.utils.espresso.HasChildViewWithText import com.anytypeio.anytype.test_utils.utils.espresso.HasViewGroupChildViewWithText +import com.anytypeio.anytype.test_utils.utils.espresso.HasViewGroupChildWithBackground +import com.anytypeio.anytype.test_utils.utils.espresso.HasViewGroupChildWithMarginLeft import com.anytypeio.anytype.test_utils.utils.espresso.WithBackgroundColor import com.anytypeio.anytype.test_utils.utils.espresso.WithChildViewCount +import com.anytypeio.anytype.test_utils.utils.espresso.WithMarginStart import com.anytypeio.anytype.test_utils.utils.espresso.WithTextColor import com.anytypeio.anytype.test_utils.utils.espresso.WithoutBackgroundColor import org.hamcrest.Matchers.not @@ -83,6 +86,16 @@ fun ViewInteraction.checkHasNoBackground() { check(matches(WithoutBackgroundColor())) } +fun ViewInteraction.checkHasMarginStart(marginStart: Int) { + check( + matches( + WithMarginStart( + marginStartExpected = marginStart + ) + ) + ) +} + fun ViewInteraction.checkHasChildViewCount(count: Int) : ViewInteraction { return check(matches(WithChildViewCount(count))) } @@ -104,6 +117,34 @@ fun ViewInteraction.checkHasChildViewWithText( return check(matches(HasChildViewWithText(pos, text, target))) } +fun ViewInteraction.checkHasViewGroupChildWithBackground( + pos: Int, + background: Int +) : ViewInteraction { + return check( + matches( + HasViewGroupChildWithBackground( + pos = pos, + background = background + ) + ) + ) +} + +fun ViewInteraction.checkHasViewGroupChildWithMarginLeft( + pos: Int, + margin: Int +) : ViewInteraction { + return check( + matches( + HasViewGroupChildWithMarginLeft( + pos = pos, + margin = margin + ) + ) + ) +} + fun ViewInteraction.checkHasViewGroupChildWithText( pos: Int, text: String diff --git a/test/android-utils/src/main/java/com/anytypeio/anytype/test_utils/utils/espresso/ViewMatchers.kt b/test/android-utils/src/main/java/com/anytypeio/anytype/test_utils/utils/espresso/ViewMatchers.kt index 86ef4f6f98..ce7b0ec50f 100644 --- a/test/android-utils/src/main/java/com/anytypeio/anytype/test_utils/utils/espresso/ViewMatchers.kt +++ b/test/android-utils/src/main/java/com/anytypeio/anytype/test_utils/utils/espresso/ViewMatchers.kt @@ -8,6 +8,7 @@ import android.widget.TextView import androidx.annotation.ColorInt import androidx.annotation.ColorRes import androidx.core.content.ContextCompat +import androidx.core.view.marginStart import androidx.recyclerview.widget.RecyclerView import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction @@ -22,8 +23,7 @@ import org.hamcrest.TypeSafeMatcher class WithTextColor( @ColorInt private val expectedColor: Int -) : - BoundedMatcher(TextView::class.java) { +) : BoundedMatcher(TextView::class.java) { override fun matchesSafely(item: TextView) = item.currentTextColor == expectedColor override fun describeTo(description: Description) { description.appendText("with text color:") @@ -64,8 +64,24 @@ class WithChildViewCount(private val expectedCount: Int) : } } -class HasViewGroupChildViewWithText(private val pos: Int, val text: String) : - BoundedMatcher(ViewGroup::class.java) { +class WithMarginStart( + private val marginStartExpected: Int +) : BoundedMatcher(View::class.java) { + override fun describeTo(description: Description) { + description.appendText("with margin start:") + description.appendValue(marginStartExpected) + } + + override fun matchesSafely(item: View): Boolean { + val actual = item.marginStart + return actual == marginStartExpected + } +} + +class HasViewGroupChildViewWithText( + private val pos: Int, + val text: String +) : BoundedMatcher(ViewGroup::class.java) { private var actual: String? = null @@ -84,6 +100,48 @@ class HasViewGroupChildViewWithText(private val pos: Int, val text: String) : } } +class HasViewGroupChildWithBackground( + private val pos: Int, + private val background: Int +) : BoundedMatcher(ViewGroup::class.java) { + + private var actual: Int? = null + + override fun matchesSafely(item: ViewGroup): Boolean { + val child = item.getChildAt(pos) + checkNotNull(child) { throw IllegalStateException("No view child at position: $pos") } + actual = (child.background as ColorDrawable).color + return actual == background + } + + override fun describeTo(description: Description) { + if (actual != null) { + description.appendText("Should have background [${background}] at position: $pos but was: [$actual]"); + } + } +} + +class HasViewGroupChildWithMarginLeft( + private val pos: Int, + private val margin: Int +) : BoundedMatcher(ViewGroup::class.java) { + + private var actual: Int? = null + + override fun matchesSafely(item: ViewGroup): Boolean { + val child = item.getChildAt(pos) + checkNotNull(child) { throw IllegalStateException("No view child at position: $pos") } + actual = child.marginStart + return actual == margin + } + + override fun describeTo(description: Description) { + if (actual != null) { + description.appendText("Should have margin [${margin}] at position: $pos but was: [$actual]"); + } + } +} + class HasChildViewWithText(private val pos: Int, val text: String, val target: Int) : BoundedMatcher(RecyclerView::class.java) {