diff --git a/build.gradle b/build.gradle index d25e0b40..2fcf3af1 100644 --- a/build.gradle +++ b/build.gradle @@ -2,8 +2,8 @@ buildscript { ext.versions = [ targetSdk: 30, - compose: '1.0.0', - kotlin: '1.5.10', + compose: '1.0.1', + kotlin: '1.5.21', commonmark: '0.18.0' ] @@ -29,6 +29,10 @@ buildscript { composeOptions { kotlinCompilerExtensionVersion versions.compose } + + lintOptions { + disable "ComposableModifierFactory", "ModifierFactoryExtensionFunction", "ModifierFactoryReturnType", "ModifierFactoryUnreferencedReceiver" + } } ext.deps = [ @@ -58,6 +62,7 @@ buildscript { icons: "androidx.compose.material:material-icons-extended:${versions.compose}", test: "androidx.ui:ui-test:${versions.compose}", tooling: "androidx.compose.ui:ui-tooling:${versions.compose}", + tooling_preview: "androidx.compose.ui:ui-tooling-preview:${versions.compose}", tooling_data: "androidx.compose.ui:ui-tooling-data:${versions.compose}", ], kotlin: [ diff --git a/docs/richtext-commonmark.md b/docs/richtext-commonmark.md index 2e4a04c0..617ec93a 100644 --- a/docs/richtext-commonmark.md +++ b/docs/richtext-commonmark.md @@ -1,6 +1,6 @@ # Markdown -Library for rendering Markdown in Compose using Atlassian's [CommonMark](https://github.com/atlassian/commonmark-java) +Library for rendering Markdown in Compose using [CommonMark](https://github.com/commonmark/commonmark-java) library to parse, and `richtext-ui` to render. ## Gradle @@ -16,44 +16,48 @@ dependencies { The simplest way to render markdown is just pass a string to the [`Markdown`](../api/richtext-commonmark/com.zachklipp.richtext.markdown/-markdown.html) composable: +!!! warning + Markdown composable has recently changed to be an extension on [RichTextScope](../api/richtext-ui/com.zachklipp.richtext.ui/-rich-text-scope/index.html) instead + of being a separate entry composable. + ~~~kotlin -Markdown( - """ - # Demo - - Emphasis, aka italics, with *asterisks* or _underscores_. Strong emphasis, aka bold, with **asterisks** or __underscores__. Combined emphasis with **asterisks and _underscores_**. [Links with two blocks, text in square-brackets, destination is in parentheses.](https://www.example.com). Inline `code` has `back-ticks around` it. - - 1. First ordered list item - 2. Another item - * Unordered sub-list. - 3. And another item. - You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown). - - * Unordered list can use asterisks - - Or minuses - + Or pluses - --- - - ```javascript - var s = "code blocks use monospace font"; - alert(s); - ``` - - Markdown | Table | Extension - --- | --- | --- - *renders* | `beautiful images` | ![random image](https://picsum.photos/seed/picsum/400/400 "Text 1") - 1 | 2 | 3 - - > Blockquotes are very handy in email to emulate reply text. - > This line is part of the same quote. - """.trimIndent(), - Modifier.padding(16.dp) -) +RichText( + modifier = Modifier.padding(16.dp) +) { + Markdown( + """ + # Demo + + Emphasis, aka italics, with *asterisks* or _underscores_. Strong emphasis, aka bold, with **asterisks** or __underscores__. Combined emphasis with **asterisks and _underscores_**. [Links with two blocks, text in square-brackets, destination is in parentheses.](https://www.example.com). Inline `code` has `back-ticks around` it. + + 1. First ordered list item + 2. Another item + * Unordered sub-list. + 3. And another item. + You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown). + + * Unordered list can use asterisks + - Or minuses + + Or pluses + --- + + ```javascript + var s = "code blocks use monospace font"; + alert(s); + ``` + + Markdown | Table | Extension + --- | --- | --- + *renders* | `beautiful images` | ![random image](https://picsum.photos/seed/picsum/400/400 "Text 1") + 1 | 2 | 3 + + > Blockquotes are very handy in email to emulate reply text. + > This line is part of the same quote. + """.trimIndent() + ) +} ~~~ Which produces something like this: ![markdown demo](img/markdown-demo.png) - -The `Markdown` composable also takes an optional `RichTextStyle` which can be used to customize how -it's rendered. diff --git a/docs/richtext-ui-material.md b/docs/richtext-ui-material.md new file mode 100644 index 00000000..bdd550be --- /dev/null +++ b/docs/richtext-ui-material.md @@ -0,0 +1,75 @@ +# Richtext UI Material + +Library that makes RichText compatible with Material design in Compose. + +## Gradle + +```groovy +dependencies { + implementation "com.halilibo.compose-richtext:richtext-ui-material:${richtext_version}" +} +``` + +## Usage + +`RichText` composable already wraps around `BasicRichText` by using appropriate Material +attributes. You can quickly start by calling `RichText` without any further setup. + +```kotlin +RichText(modifier = Modifier.background(color = Color.White)) { + Heading(0, "Paragraphs") + Text("Simple paragraph.") + Text("Paragraph with\nmultiple lines.") + Text("Paragraph with really long line that should be getting wrapped.") + + Heading(0, "Lists") + Heading(1, "Unordered") + ListDemo(listType = Unordered) + Heading(1, "Ordered") + ListDemo(listType = Ordered) + + Heading(0, "Horizontal Line") + Text("Above line") + HorizontalRule() + Text("Below line") + + Heading(0, "Code Block") + CodeBlock( + """ + { + "Hello": "world!" + } + """.trimIndent() + ) + + Heading(0, "Block Quote") + BlockQuote { + Text("These paragraphs are quoted.") + Text("More text.") + BlockQuote { + Text("Nested block quote.") + } + } + + Heading(0, "Table") + Table(headerRow = { + cell { Text("Column 1") } + cell { Text("Column 2") } + }) { + row { + cell { Text("Hello") } + cell { + CodeBlock("Foo bar") + } + } + row { + cell { + BlockQuote { + Text("Stuff") + } + } + cell { Text("Hello world this is a really long line that is going to wrap hopefully") } + } + } +} +``` diff --git a/docs/richtext-ui.md b/docs/richtext-ui.md index 6b48bd6d..e8ac857f 100644 --- a/docs/richtext-ui.md +++ b/docs/richtext-ui.md @@ -3,6 +3,10 @@ A library of composables for formatting text using higher-level concepts than are supported by compose foundation, such as "bullet lists" and "headings". +RichText UI is a base library that is non-opinionated about higher level design requirements. +If you are already using `MaterialTheme` in your compose app, you can jump to [RichText UI Material](../richtext-ui-material/index.html) +for quick start. + ## Gradle ```groovy @@ -11,12 +15,34 @@ dependencies { } ``` +## [`BasicRichText`](../api/richtext-ui/com.zachklipp.richtext.ui/-basic-rich-text.html) + +Richtext UI does not depend on Material artifact of Compose. Design agnostic API allows anyone +to adopt RichText UI and its extensions like Markdown to their own design and typography systems. + +If you are planning to adopt RichText within your design system, please go ahead and check out [`RichText Material`](../richtext-ui-material/index.html) +for inspiration. + +## [`RichTextScope`](../api/richtext-ui/com.zachklipp.richtext.ui/-rich-text-scope/index.html) + +`RichTextScope` is a context wrapper around composables that integrate and play well within RichText +content. Scope carries information about the current `TextStyle` and `ContentColor` which enables anyone +to adopt `BasicRichText` by passing their own typography in the composition tree. `ProvideTextStyle` and +`ProvideContentColor` functions also serve to pass updated text styles from RichText context to outer +design system. + +RichTextScope also offers a `Default` implementation that sets up an internal design system to +easily test the library. + ## Example -Open the `Demo.kt` file in the `richtext-ui` module to play with this. +Open the `Demo.kt` file in the `sample` module to play with this. Although the mentioned demo +uses Material integrated version of `BasicRichText`, they share 99% of their API. ```kotlin -RichText(modifier = Modifier.background(color = Color.White)) { +RichTextScope.Default.BasicRichText( + modifier = Modifier.background(color = Color.White) +) { Heading(0, "Paragraphs") Text("Simple paragraph.") Text("Paragraph with\nmultiple lines.") diff --git a/gradle.properties b/gradle.properties index 4869e703..c7092dfa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,7 +22,7 @@ kotlin.code.style=official systemProp.org.gradle.internal.publish.checksums.insecure=true GROUP=com.halilibo.compose-richtext -VERSION_NAME=0.5.0 +VERSION_NAME=0.6.0 POM_DESCRIPTION=A collection of Compose libraries for advanced text formatting and alternative display types. diff --git a/mkdocs.yml b/mkdocs.yml index 9e32b9f2..33fa0eb8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -25,6 +25,7 @@ markdown_extensions: nav: - index.md + - richtext-ui-material.md - richtext-ui.md - richtext-commonmark.md - printing.md diff --git a/richtext-commonmark/api/richtext-commonmark.api b/richtext-commonmark/api/richtext-commonmark.api index 4aa56071..3811f251 100644 --- a/richtext-commonmark/api/richtext-commonmark.api +++ b/richtext-commonmark/api/richtext-commonmark.api @@ -15,6 +15,6 @@ public final class com/zachklipp/richtext/markdown/ComposableSingletons$Markdown } public final class com/zachklipp/richtext/markdown/MarkdownKt { - public static final fun Markdown (Ljava/lang/String;Landroidx/compose/ui/Modifier;Lcom/zachklipp/richtext/ui/RichTextStyle;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun Markdown (Lcom/zachklipp/richtext/ui/RichTextScope;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V } diff --git a/richtext-commonmark/src/main/java/com/zachklipp/richtext/markdown/Markdown.kt b/richtext-commonmark/src/main/java/com/zachklipp/richtext/markdown/Markdown.kt index 99118f94..5c396fbc 100644 --- a/richtext-commonmark/src/main/java/com/zachklipp/richtext/markdown/Markdown.kt +++ b/richtext-commonmark/src/main/java/com/zachklipp/richtext/markdown/Markdown.kt @@ -1,5 +1,6 @@ package com.zachklipp.richtext.markdown +import android.annotation.SuppressLint import android.os.Build import android.text.Html import android.widget.TextView @@ -10,7 +11,6 @@ import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.viewinterop.AndroidView import com.zachklipp.richtext.markdown.extensions.AstTableRoot @@ -20,9 +20,7 @@ import com.zachklipp.richtext.ui.FormattedList import com.zachklipp.richtext.ui.Heading import com.zachklipp.richtext.ui.HorizontalRule import com.zachklipp.richtext.ui.ListType -import com.zachklipp.richtext.ui.RichText import com.zachklipp.richtext.ui.RichTextScope -import com.zachklipp.richtext.ui.RichTextStyle import com.zachklipp.richtext.ui.string.InlineContent import com.zachklipp.richtext.ui.string.Text import com.zachklipp.richtext.ui.string.richTextString @@ -31,35 +29,27 @@ import org.commonmark.ext.gfm.tables.TablesExtension import org.commonmark.parser.Parser /** - * A composable that renders Markdown content using [RichText]. + * A composable that renders Markdown content using RichText. * * @param content Markdown text. No restriction on length. - * @param style [RichTextStyle] that will be used to style markdown rendering. * @param onLinkClicked A function to invoke when a link is clicked from rendered content. */ @Composable -public fun Markdown( +public fun RichTextScope.Markdown( content: String, - modifier: Modifier = Modifier, - style: RichTextStyle? = null, onLinkClicked: ((String) -> Unit)? = null ) { - RichText( - modifier = modifier, - style = style - ) { - // Can't use UriHandlerAmbient.current::openUri here, - // see https://issuetracker.google.com/issues/172366483 - val realLinkClickedHandler = onLinkClicked ?: LocalUriHandler.current.let { - remember { - { url -> it.openUri(url) } - } + // Can't use UriHandlerAmbient.current::openUri here, + // see https://issuetracker.google.com/issues/172366483 + val realLinkClickedHandler = onLinkClicked ?: LocalUriHandler.current.let { + remember { + { url -> it.openUri(url) } } + } - CompositionLocalProvider(LocalOnLinkClicked provides realLinkClickedHandler) { - val markdownAst = parsedMarkdownAst(text = content) - RecursiveRenderMarkdownAst(astNode = markdownAst) - } + CompositionLocalProvider(LocalOnLinkClicked provides realLinkClickedHandler) { + val markdownAst = parsedMarkdownAst(text = content) + RecursiveRenderMarkdownAst(astNode = markdownAst) } } @@ -71,7 +61,7 @@ public fun Markdown( * * This function basically receives a node from the tree, root or any node, and then * recursively travels along the nodes while spitting out or wrapping composables around - * the content. [RichText] API is highly compatible with this methodology. + * the content. RichText API is highly compatible with this method. * * However, there are multiple assumptions to increase predictability. Despite the fact * that every [AstNode] can have another [AstNode] as a child, it should not be that @@ -195,6 +185,7 @@ internal fun parsedMarkdownAst(text: String): AstNode? { * * @param node Root ASTNode whose children will be visited. */ +@SuppressLint("ComposableNaming") @Composable internal fun RichTextScope.visitChildren(node: AstNode?) { node?.childrenSequence()?.forEach { diff --git a/richtext-ui-material/api/richtext-ui-material.api b/richtext-ui-material/api/richtext-ui-material.api new file mode 100644 index 00000000..c81ff018 --- /dev/null +++ b/richtext-ui-material/api/richtext-ui-material.api @@ -0,0 +1,20 @@ +public final class com/zachklipp/richtext/ui/material/BuildConfig { + public static final field BUILD_TYPE Ljava/lang/String; + public static final field DEBUG Z + public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String; + public fun ()V +} + +public final class com/zachklipp/richtext/ui/material/ComposableSingletons$RichTextKt { + public static final field INSTANCE Lcom/zachklipp/richtext/ui/material/ComposableSingletons$RichTextKt; + public static field lambda-1 Lkotlin/jvm/functions/Function4; + public static field lambda-2 Lkotlin/jvm/functions/Function4; + public fun ()V + public final fun getLambda-1$richtext_ui_material_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-2$richtext_ui_material_release ()Lkotlin/jvm/functions/Function4; +} + +public final class com/zachklipp/richtext/ui/material/RichTextKt { + public static final fun RichText (Landroidx/compose/ui/Modifier;Lcom/zachklipp/richtext/ui/RichTextStyle;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V +} + diff --git a/richtext-ui-material/build.gradle b/richtext-ui-material/build.gradle new file mode 100644 index 00000000..227ede60 --- /dev/null +++ b/richtext-ui-material/build.gradle @@ -0,0 +1,16 @@ +plugins { + id('org.jetbrains.dokka') +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply from: rootProject.file('gradle/maven-publish.gradle') + +android rootProject.ext.defaultAndroidConfig + +dependencies { + compileOnly deps.compose.tooling + + api project(":richtext-ui") + implementation deps.compose.material +} diff --git a/richtext-ui-material/consumer-rules.pro b/richtext-ui-material/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/richtext-ui-material/gradle.properties b/richtext-ui-material/gradle.properties new file mode 100644 index 00000000..b970d057 --- /dev/null +++ b/richtext-ui-material/gradle.properties @@ -0,0 +1,4 @@ +POM_ARTIFACT_ID=richtext-ui-material +POM_NAME=Compose Richtext UI Material +POM_DESCRIPTION=An extension library for RichText UI to easily bind with Material apps. +POM_PACKAGING=aar diff --git a/richtext-ui-material/proguard-rules.pro b/richtext-ui-material/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/richtext-ui-material/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/richtext-ui-material/src/main/AndroidManifest.xml b/richtext-ui-material/src/main/AndroidManifest.xml new file mode 100644 index 00000000..eb92abdf --- /dev/null +++ b/richtext-ui-material/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/richtext-ui-material/src/main/java/com/zachklipp/richtext/ui/material/RichText.kt b/richtext-ui-material/src/main/java/com/zachklipp/richtext/ui/material/RichText.kt new file mode 100644 index 00000000..c78691cf --- /dev/null +++ b/richtext-ui-material/src/main/java/com/zachklipp/richtext/ui/material/RichText.kt @@ -0,0 +1,41 @@ +package com.zachklipp.richtext.ui.material + +import androidx.compose.material.LocalContentColor +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.ProvideTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import com.zachklipp.richtext.ui.BasicRichText +import com.zachklipp.richtext.ui.RichTextScope +import com.zachklipp.richtext.ui.RichTextStyle + +/** + * RichText implementation that integrates with Material design. + */ +@Composable +public fun RichText( + modifier: Modifier = Modifier, + style: RichTextStyle? = null, + children: @Composable RichTextScope.() -> Unit +) { + remember { + RichTextScope( + textStyle = { LocalTextStyle.current }, + contentColor = { LocalContentColor.current }, + ProvideTextStyle = { textStyle, content -> + ProvideTextStyle(textStyle, content) + }, + ProvideContentColor = { color, content -> + CompositionLocalProvider(LocalContentColor provides color) { + content() + } + } + ) + }.BasicRichText( + modifier = modifier, + style = style, + children = children + ) +} diff --git a/richtext-ui/api/richtext-ui.api b/richtext-ui/api/richtext-ui.api index cb496a36..1ed97fb2 100644 --- a/richtext-ui/api/richtext-ui.api +++ b/richtext-ui/api/richtext-ui.api @@ -1,5 +1,9 @@ +public final class com/zachklipp/richtext/ui/BasicRichTextKt { + public static final fun BasicRichText (Lcom/zachklipp/richtext/ui/RichTextScope;Landroidx/compose/ui/Modifier;Lcom/zachklipp/richtext/ui/RichTextStyle;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V +} + public abstract interface class com/zachklipp/richtext/ui/BlockQuoteGutter { - public abstract fun drawGutter (Landroidx/compose/runtime/Composer;I)V + public abstract fun gutterModifier (Lcom/zachklipp/richtext/ui/RichTextScope;Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/Modifier; } public final class com/zachklipp/richtext/ui/BlockQuoteGutter$BarGutter : com/zachklipp/richtext/ui/BlockQuoteGutter { @@ -11,12 +15,12 @@ public final class com/zachklipp/richtext/ui/BlockQuoteGutter$BarGutter : com/za public final fun component4 ()Lkotlin/jvm/functions/Function1; public final fun copy-mDbLjjM (JJJLkotlin/jvm/functions/Function1;)Lcom/zachklipp/richtext/ui/BlockQuoteGutter$BarGutter; public static synthetic fun copy-mDbLjjM$default (Lcom/zachklipp/richtext/ui/BlockQuoteGutter$BarGutter;JJJLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/zachklipp/richtext/ui/BlockQuoteGutter$BarGutter; - public fun drawGutter (Landroidx/compose/runtime/Composer;I)V public fun equals (Ljava/lang/Object;)Z public final fun getBarWidth-XSAIIZE ()J public final fun getColor ()Lkotlin/jvm/functions/Function1; public final fun getEndMargin-XSAIIZE ()J public final fun getStartMargin-XSAIIZE ()J + public fun gutterModifier (Lcom/zachklipp/richtext/ui/RichTextScope;Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/Modifier; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -74,6 +78,15 @@ public final class com/zachklipp/richtext/ui/ComposableSingletons$FormattedListK public final fun getLambda-1$richtext_ui_release ()Lkotlin/jvm/functions/Function4; } +public final class com/zachklipp/richtext/ui/ComposableSingletons$RichTextScopeKt { + public static final field INSTANCE Lcom/zachklipp/richtext/ui/ComposableSingletons$RichTextScopeKt; + public static field lambda-1 Lkotlin/jvm/functions/Function4; + public static field lambda-2 Lkotlin/jvm/functions/Function4; + public fun ()V + public final fun getLambda-1$richtext_ui_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-2$richtext_ui_release ()Lkotlin/jvm/functions/Function4; +} + public final class com/zachklipp/richtext/ui/ComposableSingletons$TableKt { public static final field INSTANCE Lcom/zachklipp/richtext/ui/ComposableSingletons$TableKt; public static field lambda-1 Lkotlin/jvm/functions/Function3; @@ -96,6 +109,9 @@ public final class com/zachklipp/richtext/ui/ComposableSingletons$TableKt { public final class com/zachklipp/richtext/ui/FormattedListKt { public static final fun FormattedList (Lcom/zachklipp/richtext/ui/RichTextScope;Lcom/zachklipp/richtext/ui/ListType;Ljava/util/List;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;I)V public static final fun FormattedList (Lcom/zachklipp/richtext/ui/RichTextScope;Lcom/zachklipp/richtext/ui/ListType;[Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;I)V + public static final fun painterUnorderedMarkers ([Landroidx/compose/ui/graphics/painter/Painter;)Lcom/zachklipp/richtext/ui/UnorderedMarkers; + public static final fun textOrderedMarkers (Lcom/zachklipp/richtext/ui/RichTextScope;[Lkotlin/jvm/functions/Function1;)Lcom/zachklipp/richtext/ui/OrderedMarkers; + public static final fun textUnorderedMarkers (Lcom/zachklipp/richtext/ui/RichTextScope;[Ljava/lang/String;)Lcom/zachklipp/richtext/ui/UnorderedMarkers; } public final class com/zachklipp/richtext/ui/HeadingKt { @@ -109,19 +125,19 @@ public final class com/zachklipp/richtext/ui/HorizontalRuleKt { public final class com/zachklipp/richtext/ui/ListStyle { public static final field Companion Lcom/zachklipp/richtext/ui/ListStyle$Companion; - public synthetic fun (Landroidx/compose/ui/unit/TextUnit;Landroidx/compose/ui/unit/TextUnit;Lcom/zachklipp/richtext/ui/OrderedMarkers;Lcom/zachklipp/richtext/ui/UnorderedMarkers;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Landroidx/compose/ui/unit/TextUnit;Landroidx/compose/ui/unit/TextUnit;Lcom/zachklipp/richtext/ui/OrderedMarkers;Lcom/zachklipp/richtext/ui/UnorderedMarkers;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Landroidx/compose/ui/unit/TextUnit;Landroidx/compose/ui/unit/TextUnit;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Landroidx/compose/ui/unit/TextUnit;Landroidx/compose/ui/unit/TextUnit;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1-U3a4LBI ()Landroidx/compose/ui/unit/TextUnit; public final fun component2-U3a4LBI ()Landroidx/compose/ui/unit/TextUnit; - public final fun component3 ()Lcom/zachklipp/richtext/ui/OrderedMarkers; - public final fun component4 ()Lcom/zachklipp/richtext/ui/UnorderedMarkers; - public final fun copy-iXsHpI0 (Landroidx/compose/ui/unit/TextUnit;Landroidx/compose/ui/unit/TextUnit;Lcom/zachklipp/richtext/ui/OrderedMarkers;Lcom/zachklipp/richtext/ui/UnorderedMarkers;)Lcom/zachklipp/richtext/ui/ListStyle; - public static synthetic fun copy-iXsHpI0$default (Lcom/zachklipp/richtext/ui/ListStyle;Landroidx/compose/ui/unit/TextUnit;Landroidx/compose/ui/unit/TextUnit;Lcom/zachklipp/richtext/ui/OrderedMarkers;Lcom/zachklipp/richtext/ui/UnorderedMarkers;ILjava/lang/Object;)Lcom/zachklipp/richtext/ui/ListStyle; + public final fun component3 ()Lkotlin/jvm/functions/Function1; + public final fun component4 ()Lkotlin/jvm/functions/Function1; + public final fun copy-iXsHpI0 (Landroidx/compose/ui/unit/TextUnit;Landroidx/compose/ui/unit/TextUnit;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lcom/zachklipp/richtext/ui/ListStyle; + public static synthetic fun copy-iXsHpI0$default (Lcom/zachklipp/richtext/ui/ListStyle;Landroidx/compose/ui/unit/TextUnit;Landroidx/compose/ui/unit/TextUnit;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/zachklipp/richtext/ui/ListStyle; public fun equals (Ljava/lang/Object;)Z public final fun getContentsIndent-U3a4LBI ()Landroidx/compose/ui/unit/TextUnit; public final fun getMarkerIndent-U3a4LBI ()Landroidx/compose/ui/unit/TextUnit; - public final fun getOrderedMarkers ()Lcom/zachklipp/richtext/ui/OrderedMarkers; - public final fun getUnorderedMarkers ()Lcom/zachklipp/richtext/ui/UnorderedMarkers; + public final fun getOrderedMarkers ()Lkotlin/jvm/functions/Function1; + public final fun getUnorderedMarkers ()Lkotlin/jvm/functions/Function1; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -144,15 +160,28 @@ public abstract interface class com/zachklipp/richtext/ui/OrderedMarkers { public final class com/zachklipp/richtext/ui/OrderedMarkers$Companion { public final fun invoke (Lkotlin/jvm/functions/Function4;)Lcom/zachklipp/richtext/ui/OrderedMarkers; - public final fun text ([Lkotlin/jvm/functions/Function1;)Lcom/zachklipp/richtext/ui/OrderedMarkers; } -public final class com/zachklipp/richtext/ui/RichTextKt { - public static final fun RichText (Landroidx/compose/ui/Modifier;Lcom/zachklipp/richtext/ui/RichTextStyle;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V +public final class com/zachklipp/richtext/ui/RichTextScope { + public static final field Companion Lcom/zachklipp/richtext/ui/RichTextScope$Companion; + public fun (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function4;)V + public final fun component1 ()Lkotlin/jvm/functions/Function2; + public final fun component2 ()Lkotlin/jvm/functions/Function4; + public final fun component3 ()Lkotlin/jvm/functions/Function2; + public final fun component4 ()Lkotlin/jvm/functions/Function4; + public final fun copy (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function4;)Lcom/zachklipp/richtext/ui/RichTextScope; + public static synthetic fun copy$default (Lcom/zachklipp/richtext/ui/RichTextScope;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function4;ILjava/lang/Object;)Lcom/zachklipp/richtext/ui/RichTextScope; + public fun equals (Ljava/lang/Object;)Z + public final fun getContentColor ()Lkotlin/jvm/functions/Function2; + public final fun getProvideContentColor ()Lkotlin/jvm/functions/Function4; + public final fun getProvideTextStyle ()Lkotlin/jvm/functions/Function4; + public final fun getTextStyle ()Lkotlin/jvm/functions/Function2; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } -public final class com/zachklipp/richtext/ui/RichTextScope { - public static final field INSTANCE Lcom/zachklipp/richtext/ui/RichTextScope; +public final class com/zachklipp/richtext/ui/RichTextScope$Companion { + public final fun getDefault ()Lcom/zachklipp/richtext/ui/RichTextScope; } public final class com/zachklipp/richtext/ui/RichTextScopeKt { @@ -236,15 +265,6 @@ public abstract interface class com/zachklipp/richtext/ui/UnorderedMarkers { public final class com/zachklipp/richtext/ui/UnorderedMarkers$Companion { public final fun invoke (Lkotlin/jvm/functions/Function3;)Lcom/zachklipp/richtext/ui/UnorderedMarkers; - public final fun painters ([Landroidx/compose/ui/graphics/painter/Painter;)Lcom/zachklipp/richtext/ui/UnorderedMarkers; - public final fun text ([Ljava/lang/String;)Lcom/zachklipp/richtext/ui/UnorderedMarkers; -} - -public final class com/zachklipp/richtext/ui/string/ComposableSingletons$TextKt { - public static final field INSTANCE Lcom/zachklipp/richtext/ui/string/ComposableSingletons$TextKt; - public static field lambda-1 Lkotlin/jvm/functions/Function3; - public fun ()V - public final fun getLambda-1$richtext_ui_release ()Lkotlin/jvm/functions/Function3; } public final class com/zachklipp/richtext/ui/string/InlineContent { diff --git a/richtext-ui/build.gradle b/richtext-ui/build.gradle index 886151d9..0a2f0ec8 100644 --- a/richtext-ui/build.gradle +++ b/richtext-ui/build.gradle @@ -9,11 +9,9 @@ apply from: rootProject.file('gradle/maven-publish.gradle') android rootProject.ext.defaultAndroidConfig dependencies { + compileOnly deps.compose.activity compileOnly deps.compose.tooling api deps.androidx.annotations api deps.compose.foundation - - // TODO Migrate off this. - implementation deps.compose.material } diff --git a/richtext-ui/src/main/java/com/zachklipp/richtext/ui/RichText.kt b/richtext-ui/src/main/java/com/zachklipp/richtext/ui/BasicRichText.kt similarity index 52% rename from richtext-ui/src/main/java/com/zachklipp/richtext/ui/RichText.kt rename to richtext-ui/src/main/java/com/zachklipp/richtext/ui/BasicRichText.kt index 6efb71a5..054a3e3d 100644 --- a/richtext-ui/src/main/java/com/zachklipp/richtext/ui/RichText.kt +++ b/richtext-ui/src/main/java/com/zachklipp/richtext/ui/BasicRichText.kt @@ -10,25 +10,25 @@ import androidx.compose.ui.platform.LocalDensity /** * Draws some rich text. Entry point to the compose-richtext library. + * + * Calling [BasicRichText] requires a [RichTextScope] instance as a receiving context. + * Please refer to [RichTextScope] for more information. */ @Composable -public fun RichText( +public fun RichTextScope.BasicRichText( modifier: Modifier = Modifier, style: RichTextStyle? = null, children: @Composable RichTextScope.() -> Unit ) { - with(RichTextScope) { - // Nested RichTexts should not continue list leveling from the parent. - RestartListLevel { - WithStyle(style) { - val resolvedStyle = currentRichTextStyle.resolveDefaults() - val blockSpacing = with(LocalDensity.current) { - resolvedStyle.paragraphSpacing!!.toDp() - } + RestartListLevel { + WithStyle(style) { + val resolvedStyle = currentRichTextStyle.resolveDefaults() + val blockSpacing = with(LocalDensity.current) { + resolvedStyle.paragraphSpacing!!.toDp() + } - Column(modifier = modifier, verticalArrangement = spacedBy(blockSpacing)) { - children() - } + Column(modifier = modifier, verticalArrangement = spacedBy(blockSpacing)) { + children() } } } diff --git a/richtext-ui/src/main/java/com/zachklipp/richtext/ui/BlockQuote.kt b/richtext-ui/src/main/java/com/zachklipp/richtext/ui/BlockQuote.kt index 2e88f934..546d9d8d 100644 --- a/richtext-ui/src/main/java/com/zachklipp/richtext/ui/BlockQuote.kt +++ b/richtext-ui/src/main/java/com/zachklipp/richtext/ui/BlockQuote.kt @@ -7,8 +7,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.LocalContentColor -import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable @@ -32,8 +30,7 @@ internal val DefaultBlockQuoteGutter = BarGutter() * [BarGutter] is provided as the reasonable default of a simple vertical line. */ public interface BlockQuoteGutter { - // TODO Make this return a modifier instead? - @Composable public fun drawGutter() + @Composable public fun RichTextScope.gutterModifier(): Modifier @Immutable public data class BarGutter( @@ -42,10 +39,11 @@ public interface BlockQuoteGutter { val endMargin: TextUnit = 6.sp, val color: (contentColor: Color) -> Color = { it.copy(alpha = .25f) } ) : BlockQuoteGutter { - @Composable override fun drawGutter() { + @Composable override fun RichTextScope.gutterModifier(): Modifier { with(LocalDensity.current) { - val color = color(LocalContentColor.current) - val modifier = remember(startMargin, endMargin, barWidth, color) { + val color = color(currentContentColor) + + return remember(startMargin, endMargin, barWidth, color) { // Padding must come before width. Modifier .padding( @@ -55,8 +53,6 @@ public interface BlockQuoteGutter { .width(barWidth.toDp()) .background(color, RoundedCornerShape(50)) } - - Box(modifier = modifier) } } } @@ -72,8 +68,8 @@ public interface BlockQuoteGutter { } Layout(content = { - gutter.drawGutter() - RichText( + Box(modifier = with(gutter) { gutterModifier() }) + BasicRichText( modifier = Modifier.padding(top = spacing, bottom = spacing), children = children ) @@ -123,7 +119,7 @@ public interface BlockQuoteGutter { ) { CompositionLocalProvider(LocalContentColor provides contentColor) { Box(Modifier.background(backgroundColor)) { - RichTextScope.BlockQuote { + RichTextScope.Default.BlockQuote { Text("Some text.") Text("Another paragraph.") BlockQuote { diff --git a/richtext-ui/src/main/java/com/zachklipp/richtext/ui/CodeBlock.kt b/richtext-ui/src/main/java/com/zachklipp/richtext/ui/CodeBlock.kt index eb50565a..95224a57 100644 --- a/richtext-ui/src/main/java/com/zachklipp/richtext/ui/CodeBlock.kt +++ b/richtext-ui/src/main/java/com/zachklipp/richtext/ui/CodeBlock.kt @@ -1,14 +1,8 @@ -@file:Suppress("RemoveEmptyParenthesesFromAnnotationEntry") - package com.zachklipp.richtext.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding -import androidx.compose.material.LocalContentColor -import androidx.compose.material.LocalTextStyle -import androidx.compose.material.ProvideTextStyle -import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable @@ -69,7 +63,7 @@ internal fun CodeBlockStyle.resolveDefaults() = CodeBlockStyle( */ @Composable public fun RichTextScope.CodeBlock(children: @Composable RichTextScope.() -> Unit) { val richTextStyle = currentRichTextStyle.resolveDefaults().codeBlockStyle!! - val textStyle = LocalTextStyle.current.merge(richTextStyle.textStyle) + val textStyle = currentTextStyle.merge(richTextStyle.textStyle) val background = Modifier.background(color = richTextStyle.background!!) val blockPadding = with(LocalDensity.current) { richTextStyle.padding!!.toDp() @@ -103,7 +97,7 @@ private fun CodeBlockPreview( CompositionLocalProvider(LocalContentColor provides contentColor) { Box(modifier = Modifier.background(color = backgroundColor)) { Box(modifier = Modifier.padding(24.dp)) { - RichTextScope.CodeBlock( + RichTextScope.Default.CodeBlock( """ data class Hello( val name: String diff --git a/richtext-ui/src/main/java/com/zachklipp/richtext/ui/FormattedList.kt b/richtext-ui/src/main/java/com/zachklipp/richtext/ui/FormattedList.kt index a0c0849b..1a008ac9 100644 --- a/richtext-ui/src/main/java/com/zachklipp/richtext/ui/FormattedList.kt +++ b/richtext-ui/src/main/java/com/zachklipp/richtext/ui/FormattedList.kt @@ -1,4 +1,4 @@ -@file:Suppress("RemoveEmptyParenthesesFromAnnotationEntry") +@file:Suppress("ComposableNaming") package com.zachklipp.richtext.ui @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.selection.DisableSelection -import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable @@ -54,15 +53,6 @@ public interface OrderedMarkers { ) public companion object { - /** - * Creates an [OrderedMarkers] that will cycle through the values in [markers] for each - * indentation level given the index. - */ - public fun text(vararg markers: (index: Int) -> String): OrderedMarkers = - OrderedMarkers { level, index -> - Text(markers[level % markers.size](index)) - } - /** * Creates an [OrderedMarkers] from an arbitrary composable given the indentation level and * the index. @@ -80,6 +70,17 @@ public interface OrderedMarkers { } } +/** + * Creates an [OrderedMarkers] that will cycle through the values in [markers] for each + * indentation level given the index. + */ +public fun RichTextScope.textOrderedMarkers( + vararg markers: (index: Int) -> String +): OrderedMarkers = + OrderedMarkers { level, index -> + Text(markers[level % markers.size](index)) + } + /** * Defines how to draw list markers for [FormattedList]s that are [Unordered]. * @@ -89,22 +90,6 @@ public interface UnorderedMarkers { @Composable public fun drawMarker(level: Int) public companion object { - /** - * Creates an [UnorderedMarkers] that will cycle through the values in [markers] for each - * indentation level. - */ - public fun text(vararg markers: String): UnorderedMarkers = UnorderedMarkers { - Text(markers[it % markers.size]) - } - - /** - * Creates an [UnorderedMarkers] that will cycle through the values in [painters] for each - * indentation level. - */ - public fun painters(vararg painters: Painter): UnorderedMarkers = UnorderedMarkers { - Box(Modifier.paint(painters[it % painters.size])) - } - /** * Creates an [UnorderedMarkers] from an arbitrary composable given the indentation level. */ @@ -115,6 +100,24 @@ public interface UnorderedMarkers { } } +/** + * Creates an [UnorderedMarkers] that will cycle through the values in [markers] for each + * indentation level. + */ +public fun @Composable RichTextScope.textUnorderedMarkers( + vararg markers: String +): UnorderedMarkers = UnorderedMarkers { + Text(markers[it % markers.size]) +} + +/** + * Creates an [UnorderedMarkers] that will cycle through the values in [painters] for each + * indentation level. + */ +public fun painterUnorderedMarkers(vararg painters: Painter): UnorderedMarkers = UnorderedMarkers { + Box(Modifier.paint(painters[it % painters.size])) +} + /** * Defines how [FormattedList]s should look. * @@ -125,8 +128,8 @@ public interface UnorderedMarkers { public data class ListStyle( val markerIndent: TextUnit? = null, val contentsIndent: TextUnit? = null, - val orderedMarkers: OrderedMarkers? = null, - val unorderedMarkers: UnorderedMarkers? = null + val orderedMarkers: (RichTextScope.() -> OrderedMarkers)? = null, + val unorderedMarkers: (RichTextScope.() -> UnorderedMarkers)? = null ) { public companion object { public val Default: ListStyle = ListStyle() @@ -135,19 +138,23 @@ public data class ListStyle( private val DefaultMarkerIndent = 8.sp private val DefaultContentsIndent = 4.sp -private val DefaultOrderedMarkers = OrderedMarkers.text( - { "${it + 1}." }, - { - ('a'..'z').drop(it % 26) - .first() + "." - }, - { "${it + 1})" }, - { - ('a'..'z').drop(it % 26) - .first() + ")" - } -) -private val DefaultUnorderedMarkers = UnorderedMarkers.text("•", "◦", "▸", "▹") +private val DefaultOrderedMarkers: RichTextScope.() -> OrderedMarkers = { + textOrderedMarkers( + { "${it + 1}." }, + { + ('a'..'z').drop(it % 26) + .first() + "." + }, + { "${it + 1})" }, + { + ('a'..'z').drop(it % 26) + .first() + ")" + } + ) +} +private val DefaultUnorderedMarkers: RichTextScope.() -> UnorderedMarkers = { + textUnorderedMarkers("•", "◦", "▸", "▹") +} internal fun ListStyle.resolveDefaults(): ListStyle = ListStyle( markerIndent = markerIndent ?: DefaultMarkerIndent, @@ -202,12 +209,12 @@ private val LocalListLevel = compositionLocalOf { 0 } prefixPadding = PaddingValues(start = markerIndent, end = contentsIndent), prefixForIndex = { index -> when (listType) { - Ordered -> listStyle.orderedMarkers!!.drawMarker(currentLevel, index) - Unordered -> listStyle.unorderedMarkers!!.drawMarker(currentLevel) + Ordered -> listStyle.orderedMarkers!!().drawMarker(currentLevel, index) + Unordered -> listStyle.unorderedMarkers!!().drawMarker(currentLevel) } }, itemForIndex = { index -> - RichText { + BasicRichText { CompositionLocalProvider(LocalListLevel provides currentLevel + 1) { drawItem(items[index]) } @@ -317,7 +324,7 @@ private val LocalListLevel = compositionLocalOf { 0 } ) { CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) { Box(Modifier.background(color = Color.White)) { - RichTextScope.FormattedList( + RichTextScope.Default.FormattedList( listType = listType, items = listOf( "Foo", @@ -335,11 +342,11 @@ private val LocalListLevel = compositionLocalOf { 0 } ) { (index, text) -> Text(text) if (index == 0) { - FormattedList(listType, @Composable() { + FormattedList(listType, @Composable { Text("indented $text") - FormattedList(listType, @Composable() { + FormattedList(listType, @Composable { Text("indented $text") - FormattedList(listType, @Composable() { + FormattedList(listType, @Composable { Text("indented $text") }) }) diff --git a/richtext-ui/src/main/java/com/zachklipp/richtext/ui/Heading.kt b/richtext-ui/src/main/java/com/zachklipp/richtext/ui/Heading.kt index 602b71f7..61979b87 100644 --- a/richtext-ui/src/main/java/com/zachklipp/richtext/ui/Heading.kt +++ b/richtext-ui/src/main/java/com/zachklipp/richtext/ui/Heading.kt @@ -6,10 +6,6 @@ import androidx.annotation.IntRange import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.material.LocalContentColor -import androidx.compose.material.LocalTextStyle -import androidx.compose.material.ProvideTextStyle -import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier @@ -91,21 +87,12 @@ internal val DefaultHeadingStyle: HeadingStyle = { level, textStyle -> ) { require(level >= 0) { "Level must be at least 0" } - val richTextStyle = currentRichTextStyle.resolveDefaults() - val headingStyleFunction = richTextStyle.headingStyle!! - - // According to [Text] composable's documentation: - // "Additionally, for [color], if [color] is not set, and [style] does not have a color, then - // [AmbientContentColor] will be used - this allows this [Text] or element containing this [Text] - // to adapt to different background colors and still maintain contrast and accessibility." - // However, [resolveDefaults] uses a static default color which is [Color.Black]. - // To fix this issue, we are specifying the text color according to [Text] documentation - // before calling [resolveDefaults]. - val incomingStyle = LocalTextStyle.current.let { - it.copy(color = it.color.takeOrElse { LocalContentColor.current }) + val incomingStyle = currentTextStyle.let { + it.copy(color = it.color.takeOrElse { currentContentColor }) } val currentTextStyle = resolveDefaults(incomingStyle, LocalLayoutDirection.current) + val headingStyleFunction = currentRichTextStyle.resolveDefaults().headingStyle!! val headingTextStyle = headingStyleFunction(level, currentTextStyle) val mergedTextStyle = currentTextStyle.merge(headingTextStyle) @@ -130,7 +117,7 @@ internal val DefaultHeadingStyle: HeadingStyle = { level, textStyle -> Box(Modifier.background(color = backgroundColor)) { Column { for (level in 0 until 10) { - RichTextScope.Heading(level, "Heading ${level + 1}") + RichTextScope.Default.Heading(level, "Heading ${level + 1}") } } } diff --git a/richtext-ui/src/main/java/com/zachklipp/richtext/ui/HorizontalRule.kt b/richtext-ui/src/main/java/com/zachklipp/richtext/ui/HorizontalRule.kt index cc9af5a0..0820ee01 100644 --- a/richtext-ui/src/main/java/com/zachklipp/richtext/ui/HorizontalRule.kt +++ b/richtext-ui/src/main/java/com/zachklipp/richtext/ui/HorizontalRule.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.material.LocalContentColor import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity @@ -15,7 +14,7 @@ import androidx.compose.ui.unit.dp * A simple horizontal line drawn with the current content color. */ @Composable public fun RichTextScope.HorizontalRule() { - val color = LocalContentColor.current.copy(alpha = .2f) + val color = currentContentColor.copy(alpha = .2f) val spacing = with(LocalDensity.current) { currentRichTextStyle.resolveDefaults().paragraphSpacing!!.toDp() } diff --git a/richtext-ui/src/main/java/com/zachklipp/richtext/ui/RichTextLocals.kt b/richtext-ui/src/main/java/com/zachklipp/richtext/ui/RichTextLocals.kt new file mode 100644 index 00000000..31a4ab11 --- /dev/null +++ b/richtext-ui/src/main/java/com/zachklipp/richtext/ui/RichTextLocals.kt @@ -0,0 +1,77 @@ +package com.zachklipp.richtext.ui + +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.takeOrElse +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow + +/** + * Carries the text style in Composition tree. [Heading], [CodeBlock], + * [BlockQuote] are designed to change the ongoing [TextStyle] in composition, + * so that their children can use the modified text style implicitly. + * + * LocalTextStyle also exists in Material package but this one is internal + * to RichText. + */ +internal val LocalTextStyle = compositionLocalOf { TextStyle.Default } + +/** + * Carries the content color in Composition tree. Default TextStyle + * does not have text color specified. It defaults to [Color.Black] + * in the "resolve chain" but Dark Mode is an exception. To also resolve + * for Dark Mode, content color should be passed to [RichTextScope]. + */ +internal val LocalContentColor = compositionLocalOf { Color.Black } + +/** + * The current [TextStyle]. + */ +internal val RichTextScope.currentTextStyle: TextStyle + @Composable get() = textStyle() + +/** + * The current content [Color]. + */ +internal val RichTextScope.currentContentColor: Color + @Composable get() = contentColor() + +/** + * Intended for preview composables. + * + * Instead of + * ``` + * BasicText("...", style = currentTextStyle) + * ``` + * + * We can write as follows + * ``` + * InternalBasicText("...") + * ``` + */ +@Composable +internal fun RichTextScope.Text( + text: String, + modifier: Modifier = Modifier, + onTextLayout: (TextLayoutResult) -> Unit = {}, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE +) { + val textColor = currentTextStyle.color.takeOrElse { currentContentColor } + val style = currentTextStyle.copy(color = textColor) + + BasicText( + text = text, + modifier = modifier, + style = style, + onTextLayout = onTextLayout, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines + ) +} \ No newline at end of file diff --git a/richtext-ui/src/main/java/com/zachklipp/richtext/ui/RichTextScope.kt b/richtext-ui/src/main/java/com/zachklipp/richtext/ui/RichTextScope.kt index 9c0e4df1..5eca71d3 100644 --- a/richtext-ui/src/main/java/com/zachklipp/richtext/ui/RichTextScope.kt +++ b/richtext-ui/src/main/java/com/zachklipp/richtext/ui/RichTextScope.kt @@ -3,14 +3,64 @@ package com.zachklipp.richtext.ui import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocal import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable +import androidx.compose.runtime.State +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle /** * Scope object for composables that can draw rich text. + * + * Even [BasicRichText] is called from a RichTextScope to point + * the fact that [RichTextScope] is a corner stone for this library. + * + * RichTextScope facilitates a context for RichText elements. It does not + * behave like a [State] or a [CompositionLocal]. Starting from [BasicRichText], + * this scope carries information that should not be passed down as a state. + * + * Developers who are looking to wrap [BasicRichText] within their own design + * should start from here. + * + * @param textStyle Returns the current text style. + * @param ProvideTextStyle RichText sometimes updates the current text style + * e.g. Heading, CodeBlock, and etc. New style should be passed to the outer + * theming to indicate that there is a need for update, so that children Text + * composables use the correct styling. + * @param contentColor Returns the current content color. + * @param ProvideContentColor Similar to [ProvideTextStyle], does the same job + * for content color. */ @Immutable -public object RichTextScope +public data class RichTextScope( + public val textStyle: @Composable () -> TextStyle, + public val ProvideTextStyle: @Composable (TextStyle, @Composable () -> Unit) -> Unit, + public val contentColor: @Composable () -> Color, + public val ProvideContentColor: @Composable (Color, @Composable () -> Unit) -> Unit +) { + public companion object { + /** + * BasicRichText also offers a default design system that developers + * can fallback into. This design system is initialized with [TextStyle.Default] + * and [Color.Black]. + */ + public val Default: RichTextScope = RichTextScope( + textStyle = { LocalTextStyle.current }, + ProvideTextStyle = { newTextStyle, content -> + CompositionLocalProvider(LocalTextStyle provides newTextStyle) { + content() + } + }, + contentColor = { LocalContentColor.current }, + ProvideContentColor = { newColor, content -> + CompositionLocalProvider(LocalContentColor provides newColor) { + content() + } + }, + ) + } +} /** * The current [RichTextStyle]. diff --git a/richtext-ui/src/main/java/com/zachklipp/richtext/ui/Table.kt b/richtext-ui/src/main/java/com/zachklipp/richtext/ui/Table.kt index 6aad76bc..1a66875b 100644 --- a/richtext-ui/src/main/java/com/zachklipp/richtext/ui/Table.kt +++ b/richtext-ui/src/main/java/com/zachklipp/richtext/ui/Table.kt @@ -4,9 +4,6 @@ package com.zachklipp.richtext.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.padding -import androidx.compose.material.LocalTextStyle -import androidx.compose.material.ProvideTextStyle -import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember @@ -108,7 +105,7 @@ public fun RichTextScope.Table( rows.maxByOrNull { it.cells.size }?.cells?.size ?: 0 ) } - val headerStyle = LocalTextStyle.current.merge(tableStyle.headerTextStyle) + val headerStyle = currentTextStyle.merge(tableStyle.headerTextStyle) val cellPadding = with(LocalDensity.current) { tableStyle.cellPadding!!.toDp() } @@ -124,7 +121,10 @@ public fun RichTextScope.Table( add(headerRow.cells.map<@Composable RichTextScope.() -> Unit, @Composable () -> Unit> { cell -> @Composable { ProvideTextStyle(headerStyle) { - RichText(modifier = cellModifier, children = cell) + BasicRichText( + modifier = cellModifier, + children = cell + ) } } }) @@ -134,7 +134,10 @@ public fun RichTextScope.Table( @Suppress("RemoveExplicitTypeArguments") row.cells.map<@Composable RichTextScope.() -> Unit, @Composable () -> Unit> { cell -> @Composable { - RichText(modifier = cellModifier, children = cell) + BasicRichText( + modifier = cellModifier, + children = cell + ) } } } @@ -199,7 +202,7 @@ private fun TablePreviewFixedWidth() { @Composable private fun TablePreviewContents(modifier: Modifier = Modifier) { - RichTextScope.Table( + RichTextScope.Default.Table( modifier = modifier .background(Color.White) .padding(4.dp), diff --git a/richtext-ui/src/main/java/com/zachklipp/richtext/ui/string/Text.kt b/richtext-ui/src/main/java/com/zachklipp/richtext/ui/string/Text.kt index ae00cecf..d3c6fcdc 100644 --- a/richtext-ui/src/main/java/com/zachklipp/richtext/ui/string/Text.kt +++ b/richtext-ui/src/main/java/com/zachklipp/richtext/ui/string/Text.kt @@ -1,8 +1,7 @@ package com.zachklipp.richtext.ui.string import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.material.LocalContentColor -import androidx.compose.material.Text +import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue @@ -17,9 +16,10 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Constraints -import com.zachklipp.richtext.ui.LocalRichTextStyle -import com.zachklipp.richtext.ui.RichText import com.zachklipp.richtext.ui.RichTextScope +import com.zachklipp.richtext.ui.currentContentColor +import com.zachklipp.richtext.ui.currentRichTextStyle +import com.zachklipp.richtext.ui.currentTextStyle import com.zachklipp.richtext.ui.string.RichTextString.Format import com.zachklipp.richtext.ui.string.RichTextString.Format.Bold import com.zachklipp.richtext.ui.string.RichTextString.Format.Link @@ -28,14 +28,12 @@ private const val ZERO_WIDTH_CHAR = "\u200B" @Preview(showBackground = true) @Composable private fun TextPreview() { - RichText { - Text(richTextString { - append("I'm ") - withFormat(Bold) { - append("bold!") - } - }) - } + RichTextScope.Default.Text(richTextString { + append("I'm ") + withFormat(Bold) { + append("bold!") + } + }) } /** @@ -50,8 +48,8 @@ public fun RichTextScope.Text( modifier: Modifier = Modifier, onTextLayout: (TextLayoutResult) -> Unit = {} ) { - val style = LocalRichTextStyle.current.stringStyle - val contentColor = LocalContentColor.current + val style = currentRichTextStyle.stringStyle + val contentColor = currentContentColor val annotated = remember(text, style, contentColor) { val resolvedStyle = (style ?: RichTextStringStyle.Default).resolveDefaults() text.toAnnotatedString(resolvedStyle, contentColor) @@ -93,12 +91,13 @@ public fun RichTextScope.Text( Layout( modifier = modifier.then(pressIndicator), content = { - Text( + BasicText( text = hack, onTextLayout = { result -> layoutResult.value = result onTextLayout(result) }, + style = currentTextStyle, inlineContent = inlineTextContents ) } diff --git a/sample/build.gradle b/sample/build.gradle index 964382d3..367decd7 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -7,6 +7,7 @@ dependencies { implementation project(':printing') implementation project(':richtext-commonmark') implementation project(':richtext-ui') + implementation project(':richtext-ui-material') implementation project(':slideshow') implementation deps.androidx.appcompat implementation deps.compose.activity diff --git a/sample/src/main/java/com/zachklipp/richtext/sample/Demo.kt b/sample/src/main/java/com/zachklipp/richtext/sample/Demo.kt index a6570fd2..b025ecff 100644 --- a/sample/src/main/java/com/zachklipp/richtext/sample/Demo.kt +++ b/sample/src/main/java/com/zachklipp/richtext/sample/Demo.kt @@ -22,10 +22,10 @@ import com.zachklipp.richtext.ui.HorizontalRule import com.zachklipp.richtext.ui.ListType import com.zachklipp.richtext.ui.ListType.Ordered import com.zachklipp.richtext.ui.ListType.Unordered -import com.zachklipp.richtext.ui.RichText import com.zachklipp.richtext.ui.RichTextScope import com.zachklipp.richtext.ui.RichTextStyle import com.zachklipp.richtext.ui.Table +import com.zachklipp.richtext.ui.material.RichText @Preview(widthDp = 300, heightDp = 1000) @Composable fun RichTextDemoOnWhite() { diff --git a/sample/src/main/java/com/zachklipp/richtext/sample/DocumentSample.kt b/sample/src/main/java/com/zachklipp/richtext/sample/DocumentSample.kt index 2d655a7c..5a42f724 100644 --- a/sample/src/main/java/com/zachklipp/richtext/sample/DocumentSample.kt +++ b/sample/src/main/java/com/zachklipp/richtext/sample/DocumentSample.kt @@ -54,7 +54,7 @@ import androidx.compose.ui.unit.offset import androidx.compose.ui.unit.sp import com.zachklipp.richtext.ui.FormattedList import com.zachklipp.richtext.ui.ListType.Unordered -import com.zachklipp.richtext.ui.RichText +import com.zachklipp.richtext.ui.material.RichText import com.zachklipp.richtext.ui.printing.Printable import com.zachklipp.richtext.ui.printing.PrintableController import com.zachklipp.richtext.ui.printing.hideWhenPrinting diff --git a/sample/src/main/java/com/zachklipp/richtext/sample/MarkdownSample.kt b/sample/src/main/java/com/zachklipp/richtext/sample/MarkdownSample.kt index 3b9543dd..2a5e20ad 100644 --- a/sample/src/main/java/com/zachklipp/richtext/sample/MarkdownSample.kt +++ b/sample/src/main/java/com/zachklipp/richtext/sample/MarkdownSample.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.zachklipp.richtext.markdown.Markdown import com.zachklipp.richtext.ui.RichTextStyle +import com.zachklipp.richtext.ui.material.RichText import com.zachklipp.richtext.ui.resolveDefaults @Preview @@ -69,14 +70,17 @@ import com.zachklipp.richtext.ui.resolveDefaults SelectionContainer { Column(Modifier.verticalScroll(rememberScrollState())) { - Markdown( - content = sampleMarkdown, + RichText( style = richTextStyle, modifier = Modifier.padding(8.dp), - onLinkClicked = { - Toast.makeText(context, it, Toast.LENGTH_SHORT).show() - } - ) + ) { + Markdown( + content = sampleMarkdown, + onLinkClicked = { + Toast.makeText(context, it, Toast.LENGTH_SHORT).show() + } + ) + } } } } diff --git a/sample/src/main/java/com/zachklipp/richtext/sample/SlideshowSample.kt b/sample/src/main/java/com/zachklipp/richtext/sample/SlideshowSample.kt index 65feac9b..114c3683 100644 --- a/sample/src/main/java/com/zachklipp/richtext/sample/SlideshowSample.kt +++ b/sample/src/main/java/com/zachklipp/richtext/sample/SlideshowSample.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import com.zachklipp.richtext.ui.FormattedList import com.zachklipp.richtext.ui.ListType.Ordered -import com.zachklipp.richtext.ui.RichText +import com.zachklipp.richtext.ui.material.RichText import com.zachklipp.richtext.ui.slideshow.BodySlide import com.zachklipp.richtext.ui.slideshow.NavigableContentContainer import com.zachklipp.richtext.ui.slideshow.SlideDivider diff --git a/sample/src/main/java/com/zachklipp/richtext/sample/TextDemo.kt b/sample/src/main/java/com/zachklipp/richtext/sample/TextDemo.kt index cb4dc5b3..b92b3921 100644 --- a/sample/src/main/java/com/zachklipp/richtext/sample/TextDemo.kt +++ b/sample/src/main/java/com/zachklipp/richtext/sample/TextDemo.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em import androidx.compose.ui.unit.sp -import com.zachklipp.richtext.ui.RichTextScope +import com.zachklipp.richtext.ui.material.RichText import com.zachklipp.richtext.ui.string.InlineContent import com.zachklipp.richtext.ui.string.RichTextString.Builder import com.zachklipp.richtext.ui.string.RichTextString.Format @@ -96,7 +96,9 @@ import kotlinx.coroutines.launch } } } - RichTextScope.Text(text) + RichText { + Text(text) + } } private val spinningCross = InlineContent { diff --git a/settings.gradle b/settings.gradle index eea2ec5b..9a79a437 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,6 +7,7 @@ pluginManagement { include ':printing' include ':richtext-ui' +include ':richtext-ui-material' include ':richtext-commonmark' include ':sample' include ':slideshow'