From e19b471e57a6577f5cfa8cf31243dcf181a6febb Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Tue, 14 Apr 2026 02:15:44 +0200 Subject: [PATCH 01/28] create custom bottom sheet (without all this scaffold stuff and without the assumption that the sheet must be a Surface) --- .../ui/common/bottom_sheet/BottomSheet.kt | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/bottom_sheet/BottomSheet.kt diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/bottom_sheet/BottomSheet.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/bottom_sheet/BottomSheet.kt new file mode 100644 index 0000000000..8b24214b0b --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/bottom_sheet/BottomSheet.kt @@ -0,0 +1,175 @@ +package de.westnordost.streetcomplete.ui.common.bottom_sheet + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.AnchoredDraggableDefaults +import androidx.compose.foundation.gestures.AnchoredDraggableState +import androidx.compose.foundation.gestures.DraggableAnchors +import androidx.compose.foundation.gestures.FlingBehavior +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.ScrollScope +import androidx.compose.foundation.gestures.anchoredDraggable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.datasource.LoremIpsum +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.ui.common.bottom_sheet.BottomSheetState.* +import de.westnordost.streetcomplete.ui.ktx.toPx +import kotlin.jvm.JvmName +import kotlin.math.max + +enum class BottomSheetState { Collapsed, Expanded } + +/** A simple bottom sheet, i.e. a Box that can be pulled up from below by dragging it up. Handles + * nested vertical scrolling properly. */ +@Composable +fun BottomSheet( + modifier: Modifier = Modifier, + initialState: BottomSheetState = Collapsed, + peekHeight: Dp = 64.dp, + content: @Composable () -> Unit, +) { + val state = rememberSaveable(saver = AnchoredDraggableState.Saver()) { + AnchoredDraggableState(initialState) + } + val flingBehavior = AnchoredDraggableDefaults.flingBehavior(state) + val nestedScrollConnection = remember(state, flingBehavior) { + ConsumeNestedScrollConnection( + state = state, + flingBehavior = flingBehavior, + orientation = Orientation.Vertical + ) + } + val peekHeightPx = peekHeight.toPx() + + // outer box into which the sheet can slide into + BoxWithConstraints( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.BottomCenter, + ) { + // inner box, i.e. the bottom sheet + Box( + modifier = Modifier + .fillMaxWidth() + .onSizeChanged { size -> + state.updateAnchors(DraggableAnchors { + Collapsed at max(size.height - peekHeightPx, 0f) + Expanded at 0f + }) + } + // while offset hasn't been initialized yet, it should not be visible, to avoid + // flickering + .alpha(if (state.offset.isNaN()) 0f else 1f) + .offset { IntOffset(0, state.offset.toInt()) } + .nestedScroll(nestedScrollConnection) + .anchoredDraggable( + state = state, + orientation = Orientation.Vertical, + flingBehavior = flingBehavior + ), + content = { content() } + ) + } +} + +private fun ConsumeNestedScrollConnection( + state: AnchoredDraggableState<*>, + flingBehavior: FlingBehavior, + orientation: Orientation, +): NestedScrollConnection = object : NestedScrollConnection { + + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + val delta = available.toFloat() + return if (delta < 0 && source == NestedScrollSource.UserInput) { + state.dispatchRawDelta(delta).toOffset() + } else { + Offset.Zero + } + } + + override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { + return if (source == NestedScrollSource.UserInput) { + state.dispatchRawDelta(available.toFloat()).toOffset() + } else { + Offset.Zero + } + } + + override suspend fun onPreFling(available: Velocity): Velocity { + val toFling = available.toFloat() + val currentOffset = state.requireOffset() + return if (toFling < 0 && currentOffset > state.anchors.minPosition()) { + // since we go to the anchor with tween settling, consume all for the best UX + available + } else { + Velocity.Zero + } + } + + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + state.anchoredDrag { + val scrollFlingScope = object : ScrollScope { + override fun scrollBy(pixels: Float): Float { + dragTo(state.offset + pixels) + return pixels + } + } + with(flingBehavior) { scrollFlingScope.performFling(consumed.toFloat()) } + } + return available + } + + private fun Float.toOffset() = Offset( + x = if (orientation == Orientation.Horizontal) this else 0f, + y = if (orientation == Orientation.Vertical) this else 0f, + ) + + @JvmName("velocityToFloat") + private fun Velocity.toFloat() = if (orientation == Orientation.Horizontal) x else y + + @JvmName("offsetToFloat") + private fun Offset.toFloat(): Float = if (orientation == Orientation.Horizontal) x else y +} + +@Preview +@Composable +private fun BottomSheetPreview() { + BottomSheet { + Column(Modifier + .fillMaxWidth() + .height(300.dp) + .background(Color.Green) + ) { + Box(Modifier.fillMaxWidth().height(50.dp).background(Color.Blue)) + Text( + text = LoremIpsum(1000).values.joinToString(" "), + modifier = Modifier.verticalScroll(state = rememberScrollState()) + ) + } + } +} From 2e2c7006a9bbc98a6cb266bf64bb1ba8accea0e2 Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Tue, 14 Apr 2026 02:17:40 +0200 Subject: [PATCH 02/28] convenience composable for SpeechBubbleShape --- .../quests/note_comments/NoteCommentItem.kt | 26 +++------ .../ui/common/speech_bubble/SpeechBubble.kt | 55 +++++++++++++++++++ .../{ => speech_bubble}/SpeechBubbleShape.kt | 4 +- 3 files changed, 64 insertions(+), 21 deletions(-) create mode 100644 app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubble.kt rename app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/{ => speech_bubble}/SpeechBubbleShape.kt (98%) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/note_comments/NoteCommentItem.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/note_comments/NoteCommentItem.kt index 4b4aa8a0aa..e1d49d04bd 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/note_comments/NoteCommentItem.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/note_comments/NoteCommentItem.kt @@ -24,11 +24,12 @@ import androidx.compose.ui.text.TextLinkStyles import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.withLink +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import de.westnordost.streetcomplete.data.osmnotes.NoteComment import de.westnordost.streetcomplete.resources.* -import de.westnordost.streetcomplete.ui.common.SpeechBubbleArrowDirection -import de.westnordost.streetcomplete.ui.common.SpeechBubbleShape +import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubble +import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubbleArrowDirection import de.westnordost.streetcomplete.ui.util.annotateLinks import de.westnordost.streetcomplete.ui.util.formatAnnotated import de.westnordost.streetcomplete.util.ktx.toLocalDateTime @@ -47,6 +48,7 @@ fun NoteCommentItem( noteComment: NoteComment, avatarPainter: Painter?, modifier: Modifier = Modifier, + elevation: Dp = 16.dp, textLinkStyles: TextLinkStyles? = null ) { val annotatedUserName = buildAnnotatedString { @@ -83,9 +85,8 @@ fun NoteCommentItem( Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { Surface( modifier = Modifier.size(50.dp), - elevation = 16.dp, + elevation = elevation, shape = CircleShape, - border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f)) ) { Image( painter = avatarPainter ?: painterResource(Res.drawable.avatar_osm_anonymous), @@ -93,23 +94,12 @@ fun NoteCommentItem( ) } - val speechBubbleShape = SpeechBubbleShape( + SpeechBubble( arrowDirection = SpeechBubbleArrowDirection.Start, - cornerRadius = 16.dp, - arrowSize = 10.dp, - ) - Surface( - elevation = 16.dp, - shape = speechBubbleShape, - border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f)) + elevation = elevation, ) { SelectionContainer { - Column( - modifier = Modifier - .padding(speechBubbleShape.contentPadding) - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { Text(annotatedCommentText) Divider() Text( diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubble.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubble.kt new file mode 100644 index 0000000000..264ebd7206 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubble.kt @@ -0,0 +1,55 @@ +package de.westnordost.streetcomplete.ui.common.speech_bubble + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** Surface in the shape of a speech bubble with a border stroke and a default inner padding by + * default. */ +@Composable +fun SpeechBubble( + modifier: Modifier = Modifier, + cornerRadius: Dp = 16.dp, + arrowSize: Dp = 12.dp, + arrowDirection: SpeechBubbleArrowDirection = SpeechBubbleArrowDirection.Bottom, + arrowPlacementBias: Float = 0f, + color: Color = MaterialTheme.colors.surface, + contentColor: Color = contentColorFor(color), + border: BorderStroke? = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f)), + elevation: Dp = 0.dp, + contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 12.dp), + content: @Composable () -> Unit, +) { + val shape = SpeechBubbleShape( + cornerRadius = cornerRadius, + arrowSize = arrowSize, + arrowDirection = arrowDirection, + arrowPlacementBias = arrowPlacementBias, + ) + Surface( + modifier = modifier, + shape = shape, + color = color, + contentColor = contentColor, + border = border, + elevation = elevation, + ) { + Box(modifier = Modifier + .clip(shape) + .padding(shape.contentPadding) + .padding(contentPadding) + ) { + content() + } + } +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/SpeechBubbleShape.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubbleShape.kt similarity index 98% rename from app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/SpeechBubbleShape.kt rename to app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubbleShape.kt index b4b4bbd7c3..43bc4fcbeb 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/SpeechBubbleShape.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubbleShape.kt @@ -1,4 +1,4 @@ -package de.westnordost.streetcomplete.ui.common +package de.westnordost.streetcomplete.ui.common.speech_bubble import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -7,8 +7,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CornerSize -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.Slider import androidx.compose.material.Text From 824e6ec3b02893096e6e9db7c4c70e98a13ffe6b Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Tue, 14 Apr 2026 02:18:35 +0200 Subject: [PATCH 03/28] move --- .../westnordost/streetcomplete/quests/AbstractQuestForm.kt | 5 +++-- .../streetcomplete/quests/LeaveNoteInsteadFragment.kt | 1 + .../screens/main/bottom_sheet/CreateNoteFragment.kt | 2 +- .../{quests => ui/common/quest}/QuestHeader.kt | 6 +----- 4 files changed, 6 insertions(+), 8 deletions(-) rename app/src/commonMain/kotlin/de/westnordost/streetcomplete/{quests => ui/common/quest}/QuestHeader.kt (94%) diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/AbstractQuestForm.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/AbstractQuestForm.kt index 40429331c6..4a110cce19 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/AbstractQuestForm.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/AbstractQuestForm.kt @@ -27,6 +27,7 @@ import de.westnordost.streetcomplete.databinding.ButtonPanelButtonBinding import de.westnordost.streetcomplete.databinding.FragmentQuestAnswerBinding import de.westnordost.streetcomplete.screens.main.bottom_sheet.AbstractBottomSheetFragment import de.westnordost.streetcomplete.screens.main.bottom_sheet.IsMapOrientationAware +import de.westnordost.streetcomplete.ui.common.quest.QuestHeader import de.westnordost.streetcomplete.ui.util.content import de.westnordost.streetcomplete.util.FragmentViewBindingPropertyDelegate import de.westnordost.streetcomplete.util.ktx.popIn @@ -137,7 +138,7 @@ abstract class AbstractQuestForm : protected open fun getHintImages(): List = questType.hintImages @Composable - protected open fun ContentBeforeSpeechbubbleContent() {} + protected open fun ContentBeforeSpeechBubbleContent() {} override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -152,7 +153,7 @@ abstract class AbstractQuestForm : } } binding.contentBeforeSpeechbubbleContent.content { - ContentBeforeSpeechbubbleContent() + ContentBeforeSpeechBubbleContent() } binding.okButton.setOnClickListener { diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/LeaveNoteInsteadFragment.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/LeaveNoteInsteadFragment.kt index c268df450a..478fb9bf52 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/LeaveNoteInsteadFragment.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/LeaveNoteInsteadFragment.kt @@ -26,6 +26,7 @@ import de.westnordost.streetcomplete.databinding.FragmentQuestAnswerBinding import de.westnordost.streetcomplete.quests.note_comments.NoteForm import de.westnordost.streetcomplete.resources.* import de.westnordost.streetcomplete.screens.main.bottom_sheet.AbstractCreateNoteFragment +import de.westnordost.streetcomplete.ui.common.quest.QuestHeader import de.westnordost.streetcomplete.ui.theme.titleLarge import de.westnordost.streetcomplete.ui.util.content import de.westnordost.streetcomplete.util.ktx.viewLifecycleScope diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/bottom_sheet/CreateNoteFragment.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/bottom_sheet/CreateNoteFragment.kt index 9c474bd104..11bfddee13 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/bottom_sheet/CreateNoteFragment.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/bottom_sheet/CreateNoteFragment.kt @@ -26,9 +26,9 @@ import de.westnordost.streetcomplete.data.osmnotes.edits.NoteEditAction import de.westnordost.streetcomplete.data.osmnotes.edits.NoteEditsController import de.westnordost.streetcomplete.data.osmtracks.Trackpoint import de.westnordost.streetcomplete.databinding.FragmentCreateNoteBinding -import de.westnordost.streetcomplete.quests.QuestHeader import de.westnordost.streetcomplete.quests.note_comments.NoteForm import de.westnordost.streetcomplete.resources.* +import de.westnordost.streetcomplete.ui.common.quest.QuestHeader import de.westnordost.streetcomplete.ui.util.content import de.westnordost.streetcomplete.util.ktx.getLocationInWindow import de.westnordost.streetcomplete.util.ktx.viewLifecycleScope diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/QuestHeader.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestHeader.kt similarity index 94% rename from app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/QuestHeader.kt rename to app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestHeader.kt index 54d9dcdd34..097cc094bd 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/QuestHeader.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestHeader.kt @@ -1,8 +1,7 @@ -package de.westnordost.streetcomplete.quests +package de.westnordost.streetcomplete.ui.common.quest import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -27,11 +26,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties import de.westnordost.streetcomplete.ui.common.InfoFilledIcon import de.westnordost.streetcomplete.ui.common.InfoOutlineIcon import de.westnordost.streetcomplete.ui.ktx.fadingHorizontalScrollEdges From 9ac83a8475a18061f66c0631529d5e77d953cc7a Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Tue, 14 Apr 2026 02:19:08 +0200 Subject: [PATCH 04/28] use default text link styles --- .../quests/note_discussion/NoteDiscussionForm.kt | 13 +++---------- .../streetcomplete/ui/theme/Typography.kt | 16 ++++++++++++++++ .../westnordost/streetcomplete/ui/util/Html.kt | 11 ++--------- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/note_discussion/NoteDiscussionForm.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/note_discussion/NoteDiscussionForm.kt index 9926e28544..2e69d893bf 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/note_discussion/NoteDiscussionForm.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/note_discussion/NoteDiscussionForm.kt @@ -37,6 +37,7 @@ import de.westnordost.streetcomplete.quests.AnswerItem import de.westnordost.streetcomplete.quests.note_comments.NoteCommentItem import de.westnordost.streetcomplete.quests.note_comments.NoteForm import de.westnordost.streetcomplete.screens.main.bottom_sheet.AbstractCreateNoteFragment +import de.westnordost.streetcomplete.ui.theme.defaultTextLinkStyles import de.westnordost.streetcomplete.ui.util.content import de.westnordost.streetcomplete.ui.util.rememberSerializable import de.westnordost.streetcomplete.util.image.loadImageBitmap @@ -126,16 +127,8 @@ class NoteDiscussionForm : AbstractQuestForm(), TakePhotoFragment.Listener { } @Composable - override fun ContentBeforeSpeechbubbleContent() { - val textLinkStyles = TextLinkStyles( - style = SpanStyle( - color = MaterialTheme.colors.primary, - textDecoration = TextDecoration.Underline - ), - focusedStyle = SpanStyle( - color = MaterialTheme.colors.secondary, - ) - ) + override fun ContentBeforeSpeechBubbleContent() { + val textLinkStyles = MaterialTheme.typography.defaultTextLinkStyles() ProvideTextStyle(MaterialTheme.typography.body2) { Column( verticalArrangement = Arrangement.spacedBy(8.dp), diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Typography.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Typography.kt index fd0d88f814..ee1d1549c5 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Typography.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Typography.kt @@ -1,8 +1,13 @@ package de.westnordost.streetcomplete.ui.theme +import androidx.compose.material.MaterialTheme import androidx.compose.material.Typography +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration private val material2 = Typography() @@ -30,3 +35,14 @@ val Typography.headlineSmall get() = h5 val Typography.titleLarge get() = h6 val Typography.titleMedium get() = subtitle1 val Typography.titleSmall get() = subtitle2 + +@Composable +fun Typography.defaultTextLinkStyles() = TextLinkStyles( + style = SpanStyle( + color = MaterialTheme.colors.primary, + textDecoration = TextDecoration.Underline + ), + focusedStyle = SpanStyle( + color = MaterialTheme.colors.secondary, + ) +) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/util/Html.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/util/Html.kt index 7fb43ae3f6..ce5c392a77 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/util/Html.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/util/Html.kt @@ -21,21 +21,14 @@ import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.sp import de.westnordost.streetcomplete.ui.ktx.pxToSp +import de.westnordost.streetcomplete.ui.theme.defaultTextLinkStyles import de.westnordost.streetcomplete.util.html.HtmlElementNode import de.westnordost.streetcomplete.util.html.HtmlNode import de.westnordost.streetcomplete.util.html.HtmlTextNode @Composable fun List.toAnnotatedString( - textLinkStyles: TextLinkStyles = TextLinkStyles( - style = SpanStyle( - color = MaterialTheme.colors.primary, - textDecoration = TextDecoration.Underline - ), - focusedStyle = SpanStyle( - color = MaterialTheme.colors.secondary, - ) - ) + textLinkStyles: TextLinkStyles = MaterialTheme.typography.defaultTextLinkStyles() ): AnnotatedString { val textStyle = LocalTextStyle.current val textMeasurer = rememberTextMeasurer() From caa11d711567481c37f9ca1fb0280347950e326d Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Tue, 14 Apr 2026 02:19:43 +0200 Subject: [PATCH 05/28] copy new implementation from M3 --- .../ui/common/VerticalDivider.kt | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/VerticalDivider.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/VerticalDivider.kt index f7dbbf159b..b384ee5252 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/VerticalDivider.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/VerticalDivider.kt @@ -1,14 +1,13 @@ package de.westnordost.streetcomplete.ui.common -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.width import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -19,13 +18,16 @@ fun VerticalDivider( color: Color = MaterialTheme.colors.onSurface.copy(alpha = DividerAlpha), thickness: Dp = 1.dp ) { - val targetThickness = - if (thickness == Dp.Hairline) { - (1f / LocalDensity.current.density).dp - } else { - thickness - } - Box(modifier.fillMaxHeight().width(targetThickness).background(color = color)) + Canvas(modifier.fillMaxHeight().width(thickness)) { + val width = thickness.toPx() + val offset = width / 2f + drawLine( + color = color, + strokeWidth = width, + start = Offset(offset, 0f), + end = Offset(offset, size.height), + ) + } } private const val DividerAlpha = 0.12f From 2a887b9454e9753644aae4a724a8f61a9142423c Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Tue, 14 Apr 2026 02:20:16 +0200 Subject: [PATCH 06/28] add QuestAnswerButtonBar --- .../bottom_sheet/move_node/MoveNodeForm.kt | 7 +- .../streetcomplete/ui/common/ButtonBar.kt | 25 ----- .../ui/common/quest/QuestAnswerButtonBar.kt | 97 +++++++++++++++++++ 3 files changed, 99 insertions(+), 30 deletions(-) delete mode 100644 app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/ButtonBar.kt create mode 100644 app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerButtonBar.kt diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/bottom_sheet/move_node/MoveNodeForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/bottom_sheet/move_node/MoveNodeForm.kt index 88ebd4971d..a2576f35dc 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/bottom_sheet/move_node/MoveNodeForm.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/bottom_sheet/move_node/MoveNodeForm.kt @@ -13,7 +13,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.unit.dp import de.westnordost.streetcomplete.resources.* -import de.westnordost.streetcomplete.ui.common.ButtonBar import de.westnordost.streetcomplete.ui.theme.titleLarge import org.jetbrains.compose.resources.stringResource @@ -60,10 +59,8 @@ fun MoveNodeForm( Divider() // button panel - ButtonBar { - TextButton(onClick = onClickCancel) { - Text(stringResource(Res.string.cancel)) - } + Column(Modifier.fillMaxWidth()) { + TextButton(onClickCancel) { Text(stringResource(Res.string.cancel)) } } } } diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/ButtonBar.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/ButtonBar.kt deleted file mode 100644 index b71b1a286b..0000000000 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/ButtonBar.kt +++ /dev/null @@ -1,25 +0,0 @@ -package de.westnordost.streetcomplete.ui.common - -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier - -/** Horizontal button bar for bottom sheets. Add [HorizontalDivider] between buttons as needed. */ -@Composable -fun ButtonBar( - modifier: Modifier = Modifier, - content: @Composable RowScope.() -> Unit, -) { - Row( - modifier = modifier - .fillMaxWidth() - .height(IntrinsicSize.Min), - verticalAlignment = Alignment.CenterVertically, - content = content, - ) -} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerButtonBar.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerButtonBar.kt new file mode 100644 index 0000000000..4d6e8e6627 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerButtonBar.kt @@ -0,0 +1,97 @@ +package de.westnordost.streetcomplete.ui.common.quest + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material.DropdownMenu +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.resources.* +import de.westnordost.streetcomplete.ui.common.DropdownMenuItem +import de.westnordost.streetcomplete.ui.common.VerticalDivider +import org.jetbrains.compose.resources.stringResource + +@Immutable +data class Answer(val text: String, val action: () -> Unit) + +/** Horizontal button bar for bottom sheets that can be multi-line if it does not all fit in one + * line and places subtle dividers in-between the [answers]. Also, optionally [otherAnswers] will + * be shown in a dropdown button aligned to the start of the bar. */ +@Composable +fun QuestAnswerButtonBar( + modifier: Modifier = Modifier, + answers: List = emptyList(), + otherAnswers: List = emptyList(), +) { + FlowRow( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(4.dp, alignment = Alignment.End), + itemVerticalAlignment = Alignment.CenterVertically, + ) { + if (otherAnswers.isNotEmpty()) { + OtherAnswersTextButton(answers = otherAnswers) + Spacer(Modifier.weight(1f)) + } + for ((index, item) in answers.withIndex()) { + if (otherAnswers.isNotEmpty() || index != 0) { + VerticalDivider(Modifier.height(24.dp)) + } + TextButton(onClick = item.action) { Text(item.text) } + } + } +} + +@Composable +private fun OtherAnswersTextButton( + answers: List, + modifier: Modifier = Modifier, +) { + var expanded by rememberSaveable { mutableStateOf(false) } + + Box(modifier) { + TextButton(onClick = { expanded = true }) { + Text(stringResource(Res.string.quest_generic_otherAnswers2)) + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + for (answer in answers) { + DropdownMenuItem(onClick = { expanded = false; answer.action() }) { + Text(answer.text) + } + } + } + } +} + + +@Preview +@Composable +private fun QuestAnswerButtonBarPreview() { + QuestAnswerButtonBar( + answers = listOf( + Answer("No") {}, + Answer("Perhaps") {}, + Answer("Depends how you define \"No\"") {}, + Answer("Yes") {}, + ), + otherAnswers = listOf( + Answer("Depends how you define \"Yes\"") {}, + Answer("Can't say") {} + ) + ) +} From 592b81191b359b5bcef7e9ac81b4b01a88ca23b4 Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Tue, 14 Apr 2026 02:20:46 +0200 Subject: [PATCH 07/28] add QuestForm --- .../ui/common/quest/QuestAnswerBubble.kt | 62 ++++++ .../ui/common/quest/QuestForm.kt | 189 ++++++++++++++++++ 2 files changed, 251 insertions(+) create mode 100644 app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerBubble.kt create mode 100644 app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerBubble.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerBubble.kt new file mode 100644 index 0000000000..f733ecd4fd --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerBubble.kt @@ -0,0 +1,62 @@ +package de.westnordost.streetcomplete.ui.common.quest + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Divider +import androidx.compose.material.MaterialTheme +import androidx.compose.material.ProvideTextStyle +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubble +import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubbleArrowDirection + +/** Speech bubble for the quest answer, i.e. content and/or button bar answers */ +@Composable +fun QuestAnswerBubble( + modifier: Modifier = Modifier, + elevation: Dp = 0.dp, + answers: List = emptyList(), + otherAnswers: List = emptyList(), + contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 12.dp), + content: @Composable (BoxScope.() -> Unit)? = null +) { + Surface( + modifier = modifier, + shape = RoundedCornerShape(16.dp), + elevation = elevation, + border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f)), + ) { + ProvideTextStyle(MaterialTheme.typography.body1) { + Column { + if (content != null) { + Box( + modifier = Modifier.fillMaxWidth().padding(contentPadding), + contentAlignment = Alignment.Center + ) { + content() + } + } + if (content != null && (answers.isNotEmpty() || otherAnswers.isNotEmpty())) { + Divider() + } + if (answers.isNotEmpty() || otherAnswers.isNotEmpty()) { + QuestAnswerButtonBar( + modifier = Modifier.padding(horizontal = 8.dp), + answers = answers, + otherAnswers = otherAnswers + ) + } + } + } + } +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt new file mode 100644 index 0000000000..bbdff58a19 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt @@ -0,0 +1,189 @@ +package de.westnordost.streetcomplete.ui.common.quest + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.BottomSheetScaffold +import androidx.compose.material.Button +import androidx.compose.material.ContentAlpha +import androidx.compose.material.DropdownMenu +import androidx.compose.material.FabPosition +import androidx.compose.material.FloatingActionButton +import androidx.compose.material.Icon +import androidx.compose.material.LocalContentAlpha +import androidx.compose.material.MaterialTheme +import androidx.compose.material.MaterialTheme.colors +import androidx.compose.material.ProvideTextStyle +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.TopAppBar +import androidx.compose.material.rememberBottomSheetScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocal +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.datasource.LoremIpsum +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.data.osm.edits.EditType +import de.westnordost.streetcomplete.data.quest.QuestType +import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement +import de.westnordost.streetcomplete.resources.Res +import de.westnordost.streetcomplete.resources.ic_arrow_drop_down_24 +import de.westnordost.streetcomplete.resources.note_for_object +import de.westnordost.streetcomplete.resources.quest_generic_otherAnswers2 +import de.westnordost.streetcomplete.resources.quest_maxweight_title +import de.westnordost.streetcomplete.resources.quest_streetName_hint +import de.westnordost.streetcomplete.resources.quest_streetName_title +import de.westnordost.streetcomplete.screens.main.messages.LoremIpsumLines +import de.westnordost.streetcomplete.ui.common.DropdownMenuItem +import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubble +import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubbleArrowDirection +import de.westnordost.streetcomplete.ui.theme.defaultTextLinkStyles +import de.westnordost.streetcomplete.ui.theme.titleSmall +import de.westnordost.streetcomplete.ui.util.annotateLinks +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource + +/** A generic quest form, with a [title], [subtitle], [hintText] and [hintImages] in the + * header speech bubble, then an optional [note] by another mapper shown below as another speech + * bubble, then finally the speech bubble containing the center-aligned [content] padded with a + * [contentPadding] (if there is any content) and below a row oftext buttons showing different + * [answers]. At the very start of the text button row, there's a text button labeled "Uh…" that, + * when tapped, opens a dropdown menu containing [otherAnswers]. */ +@Composable +fun QuestForm( + questType: QuestType, + modifier: Modifier = Modifier, + title: String = stringResource(questType.title), + subtitle: AnnotatedString? = null, + hintText: String? = questType.hint?.let { stringResource(it) }, + hintImages: List = questType.hintImages, + note: String? = null, + answers: List = emptyList(), + otherAnswers: List = emptyList(), + contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 12.dp), + content: @Composable (BoxScope.() -> Unit)? = null +) { + val elevation = 16.dp + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + SpeechBubble( + elevation = elevation, + arrowDirection = SpeechBubbleArrowDirection.Top, + arrowPlacementBias = 0.1f, + modifier = Modifier.padding(horizontal = 4.dp) + ) { + QuestHeader( + title = title, + subtitle = subtitle, + hintText = hintText, + hintImages = hintImages, + modifier = Modifier.fillMaxWidth(), + ) + } + + if (note != null) { + NoteBubble( + text = note, + elevation = elevation, + modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp) + ) + } + + QuestAnswerBubble( + modifier = Modifier.fillMaxWidth(), + elevation = elevation, + answers = answers, + otherAnswers = otherAnswers, + contentPadding = contentPadding, + content = content, + ) + } +} + +@Composable +private fun NoteBubble( + text: String, + modifier: Modifier = Modifier, + elevation: Dp = 0.dp, +) { + Surface( + modifier = modifier, + shape = RoundedCornerShape(16.dp), + elevation = elevation, + ) { + Column(Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) { + Text( + text = stringResource(Res.string.note_for_object), + style = MaterialTheme.typography.titleSmall + ) + SelectionContainer { + Text( + text = text.annotateLinks(MaterialTheme.typography.defaultTextLinkStyles()), + style = MaterialTheme.typography.body2, + modifier = Modifier.alpha(ContentAlpha.medium) + ) + } + } + } +} + +@Preview +@Composable +private fun QuestFormPreview() { + QuestForm( + questType = object : QuestType { + override val icon = 0 + override val title = Res.string.quest_streetName_title + override val wikiLink = null + override val achievements = emptyList() + override val hint = Res.string.quest_streetName_hint + }, + subtitle = AnnotatedString("Tertiary Road"), + note = "unpaved", + answers = listOf( + Answer("Yes") {}, + Answer("No") {}, + Answer("Perhaps") {}, + ), + otherAnswers = listOf( + Answer("Can't say") {}, + Answer("Can say") {}, + ) + ) { + Text("Some content") + } +} From 079c3fc003ecbfac57412f773f87e51936f83c42 Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Tue, 14 Apr 2026 17:32:15 +0200 Subject: [PATCH 08/28] use PaddingValues.Zero --- .../screens/settings/presets/EditTypePresetsScreen.kt | 2 +- .../screens/settings/quest_selection/QuestSelectionList.kt | 2 +- .../screens/user/edits/EditTypeStatisticsColumn.kt | 2 +- .../screens/user/achievements/LazyAchievementsGrid.kt | 2 +- .../screens/user/edits/CountryStatisticsColumn.kt | 2 +- .../streetcomplete/screens/user/links/LazyLinksColumn.kt | 6 +++--- .../westnordost/streetcomplete/ui/common/AutoFitFontSize.kt | 2 +- .../westnordost/streetcomplete/ui/common/StepperButton.kt | 4 ++-- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/settings/presets/EditTypePresetsScreen.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/settings/presets/EditTypePresetsScreen.kt index e5cf9fd6e7..faee9af68c 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/settings/presets/EditTypePresetsScreen.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/settings/presets/EditTypePresetsScreen.kt @@ -100,7 +100,7 @@ import org.jetbrains.compose.resources.stringResource private fun EditTypePresetsList( viewModel: EditTypePresetsViewModel, modifier: Modifier = Modifier, - contentPadding: PaddingValues = PaddingValues(0.dp) + contentPadding: PaddingValues = PaddingValues.Zero ) { val presets by viewModel.presets.collectAsState() diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/settings/quest_selection/QuestSelectionList.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/settings/quest_selection/QuestSelectionList.kt index 69f060c52f..abccc03fde 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/settings/quest_selection/QuestSelectionList.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/settings/quest_selection/QuestSelectionList.kt @@ -45,7 +45,7 @@ fun QuestSelectionList( onSelect: (questType: QuestType, selected: Boolean) -> Unit, onReorder: (questType: QuestType, toAfter: QuestType) -> Unit, modifier: Modifier = Modifier, - contentPadding: PaddingValues = PaddingValues(0.dp), + contentPadding: PaddingValues = PaddingValues.Zero, ) { var showEnableQuestDialog by remember { mutableStateOf(null) } diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/user/edits/EditTypeStatisticsColumn.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/user/edits/EditTypeStatisticsColumn.kt index 9e60237b48..314a51957b 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/user/edits/EditTypeStatisticsColumn.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/user/edits/EditTypeStatisticsColumn.kt @@ -26,7 +26,7 @@ import de.westnordost.streetcomplete.ui.theme.GrassGreen fun EditTypeStatisticsColumn( statistics: List, modifier: Modifier = Modifier, - contentPadding: PaddingValues = PaddingValues(0.dp), + contentPadding: PaddingValues = PaddingValues.Zero, ) { var showInfo by remember { mutableStateOf(null) } diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/user/achievements/LazyAchievementsGrid.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/user/achievements/LazyAchievementsGrid.kt index 8428f0872f..fd0d347297 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/user/achievements/LazyAchievementsGrid.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/user/achievements/LazyAchievementsGrid.kt @@ -21,7 +21,7 @@ fun LazyAchievementsGrid( achievements: List>, onClickAchievement: (achievement: Achievement, level: Int) -> Unit, modifier: Modifier = Modifier, - contentPadding: PaddingValues = PaddingValues(0.dp) + contentPadding: PaddingValues = PaddingValues.Zero ) { LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 144.dp), diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/user/edits/CountryStatisticsColumn.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/user/edits/CountryStatisticsColumn.kt index 5fa2eef59c..a4f8de5477 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/user/edits/CountryStatisticsColumn.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/user/edits/CountryStatisticsColumn.kt @@ -29,7 +29,7 @@ fun CountryStatisticsColumn( flagAlignments: FlagAlignments, isCurrentWeek: Boolean, modifier: Modifier = Modifier, - contentPadding: PaddingValues = PaddingValues(0.dp), + contentPadding: PaddingValues = PaddingValues.Zero, ) { var showInfo by remember { mutableStateOf(null) } diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/user/links/LazyLinksColumn.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/user/links/LazyLinksColumn.kt index bd7bcdddab..fb79786c46 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/user/links/LazyLinksColumn.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/user/links/LazyLinksColumn.kt @@ -19,7 +19,7 @@ import androidx.compose.ui.tooling.preview.Preview fun LazyGroupedLinksColumn( allLinks: List, modifier: Modifier = Modifier, - contentPadding: PaddingValues = PaddingValues(0.dp) + contentPadding: PaddingValues = PaddingValues.Zero ) { val groupedLinks = remember(allLinks) { allLinks.groupBy { it.category }.map { (k, v) -> k to v } @@ -46,7 +46,7 @@ fun LazyGroupedLinksColumn( fun LazyLinksColumn( links: List, modifier: Modifier = Modifier, - contentPadding: PaddingValues = PaddingValues(0.dp) + contentPadding: PaddingValues = PaddingValues.Zero ) { LazyLinksGrid(modifier, contentPadding = contentPadding) { items(links) { link -> @@ -58,7 +58,7 @@ fun LazyLinksColumn( @Composable private fun LazyLinksGrid( modifier: Modifier = Modifier, - contentPadding: PaddingValues = PaddingValues(0.dp), + contentPadding: PaddingValues = PaddingValues.Zero, content: LazyGridScope.() -> Unit ) { LazyVerticalGrid( diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/AutoFitFontSize.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/AutoFitFontSize.kt index 7c1bf9cf74..4709cb960d 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/AutoFitFontSize.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/AutoFitFontSize.kt @@ -16,7 +16,7 @@ fun AutoFitFontSize( value: String, modifier: Modifier = Modifier, maxLines: Int = 1, - contentPadding: PaddingValues = PaddingValues(0.dp), + contentPadding: PaddingValues = PaddingValues.Zero, content: @Composable () -> Unit, ) { val textStyle = LocalTextStyle.current diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/StepperButton.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/StepperButton.kt index fc9d8bdab5..79a2dae201 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/StepperButton.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/StepperButton.kt @@ -56,7 +56,7 @@ fun StepperButton( onClick = onIncrease, style = style, enabled = increaseEnabled, - contentPadding = PaddingValues(0.dp), + contentPadding = PaddingValues.Zero, content = increaseContent ) Divider() @@ -65,7 +65,7 @@ fun StepperButton( onClick = onDecrease, style = style, enabled = decreaseEnabled, - contentPadding = PaddingValues(0.dp), + contentPadding = PaddingValues.Zero, content = decreaseContent ) } From 3c3ce22108e79523564de8bec18cacec4ff3bc47 Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Tue, 14 Apr 2026 17:33:04 +0200 Subject: [PATCH 09/28] make FloatingOkButton enablebe (necessary for overlays) --- .../{ic_check_48.xml => ic_check_32.xml} | 4 +- .../ui/common/FloatingActionButton.kt | 67 +++++++++++++++++++ .../ui/common/FloatingOkButton.kt | 14 ++-- 3 files changed, 73 insertions(+), 12 deletions(-) rename app/src/commonMain/composeResources/drawable/{ic_check_48.xml => ic_check_32.xml} (83%) create mode 100644 app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/FloatingActionButton.kt diff --git a/app/src/commonMain/composeResources/drawable/ic_check_48.xml b/app/src/commonMain/composeResources/drawable/ic_check_32.xml similarity index 83% rename from app/src/commonMain/composeResources/drawable/ic_check_48.xml rename to app/src/commonMain/composeResources/drawable/ic_check_32.xml index 7328e3761b..ad0346dc13 100644 --- a/app/src/commonMain/composeResources/drawable/ic_check_48.xml +++ b/app/src/commonMain/composeResources/drawable/ic_check_32.xml @@ -1,6 +1,6 @@ Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + interactionSource: MutableInteractionSource? = null, + shape: Shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)), + backgroundColor: Color = MaterialTheme.colors.secondary, + contentColor: Color = contentColorFor(backgroundColor), + elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), + content: @Composable () -> Unit, +) { + @Suppress("NAME_SHADOWING") + val interactionSource = interactionSource ?: remember { MutableInteractionSource() } + Surface( + onClick = onClick, + modifier = modifier.semantics { role = Role.Button }, + enabled = enabled, + shape = shape, + color = backgroundColor, + contentColor = contentColor, + elevation = elevation.elevation(interactionSource).value, + interactionSource = interactionSource, + ) { + CompositionLocalProvider(LocalContentAlpha provides contentColor.alpha) { + ProvideTextStyle(MaterialTheme.typography.button) { + Box( + modifier = Modifier.defaultMinSize(minWidth = FabSize, minHeight = FabSize), + contentAlignment = Alignment.Center, + ) { + content() + } + } + } + } +} + +private val FabSize = 56.dp diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/FloatingOkButton.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/FloatingOkButton.kt index 22fa6d68c6..16c844a623 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/FloatingOkButton.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/FloatingOkButton.kt @@ -6,14 +6,10 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.FloatingActionButton import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import de.westnordost.streetcomplete.resources.* import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @@ -21,9 +17,10 @@ import org.jetbrains.compose.resources.stringResource /** Floating OK (check) button with animated pop-in/pop-out*/ @Composable fun FloatingOkButton( - visible: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, + visible: Boolean = true, + enabled: Boolean = true, ) { AnimatedVisibility( visible = visible, @@ -33,14 +30,11 @@ fun FloatingOkButton( ) { FloatingActionButton( onClick = onClick, - shape = CircleShape, - backgroundColor = MaterialTheme.colors.secondary, - modifier = Modifier.size(72.dp), + enabled = enabled ) { Icon( - painter = painterResource(Res.drawable.ic_check_48), + painter = painterResource(Res.drawable.ic_check_32), contentDescription = stringResource(Res.string.ok), - tint = MaterialTheme.colors.onSecondary, ) } } From ffa3fe065987bd248175a2b68bc1cc9bfeafeb70 Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Tue, 14 Apr 2026 17:33:39 +0200 Subject: [PATCH 10/28] add comments etc --- .../ui/common/bottom_sheet/BottomSheet.kt | 38 ++++--------------- 1 file changed, 8 insertions(+), 30 deletions(-) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/bottom_sheet/BottomSheet.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/bottom_sheet/BottomSheet.kt index 8b24214b0b..bc73c4cfa8 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/bottom_sheet/BottomSheet.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/bottom_sheet/BottomSheet.kt @@ -1,6 +1,5 @@ package de.westnordost.streetcomplete.ui.common.bottom_sheet -import androidx.compose.foundation.background import androidx.compose.foundation.gestures.AnchoredDraggableDefaults import androidx.compose.foundation.gestures.AnchoredDraggableState import androidx.compose.foundation.gestures.DraggableAnchors @@ -10,35 +9,26 @@ import androidx.compose.foundation.gestures.ScrollScope import androidx.compose.foundation.gestures.anchoredDraggable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.datasource.LoremIpsum import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp -import de.westnordost.streetcomplete.ui.common.bottom_sheet.BottomSheetState.* +import de.westnordost.streetcomplete.ui.common.bottom_sheet.BottomSheetState.Collapsed +import de.westnordost.streetcomplete.ui.common.bottom_sheet.BottomSheetState.Expanded import de.westnordost.streetcomplete.ui.ktx.toPx import kotlin.jvm.JvmName import kotlin.math.max @@ -86,6 +76,9 @@ fun BottomSheet( // flickering .alpha(if (state.offset.isNaN()) 0f else 1f) .offset { IntOffset(0, state.offset.toInt()) } + // necessary because drag events are usually dispatched to nested scroll views + // first, we need to steal back control of it so we can first expand the sheet up + // via drag before allowing to scroll in the nested view .nestedScroll(nestedScrollConnection) .anchoredDraggable( state = state, @@ -132,6 +125,9 @@ private fun ConsumeNestedScrollConnection( } override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + // this API is batshit crazy. Do I understand it? No. I got it from the example at + // https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/AnchoredDraggableSample.kt;l=416-432;drc=7440f70755e3735dbd8f04718d12dfeec7584dc8 + // pointed at from the comment of the now deprecated `state.settle(velocity)` API. state.anchoredDrag { val scrollFlingScope = object : ScrollScope { override fun scrollBy(pixels: Float): Float { @@ -155,21 +151,3 @@ private fun ConsumeNestedScrollConnection( @JvmName("offsetToFloat") private fun Offset.toFloat(): Float = if (orientation == Orientation.Horizontal) x else y } - -@Preview -@Composable -private fun BottomSheetPreview() { - BottomSheet { - Column(Modifier - .fillMaxWidth() - .height(300.dp) - .background(Color.Green) - ) { - Box(Modifier.fillMaxWidth().height(50.dp).background(Color.Blue)) - Text( - text = LoremIpsum(1000).values.joinToString(" "), - modifier = Modifier.verticalScroll(state = rememberScrollState()) - ) - } - } -} From bb97631587b021b50586b72f0b03ebe7d72ae4b4 Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Tue, 14 Apr 2026 17:34:11 +0200 Subject: [PATCH 11/28] use WindowInfo in Dimensions.kt --- .../screens/main/controls/Crosshair.kt | 9 ++-- .../streetcomplete/ui/ktx/WindowInfo.kt | 6 +++ .../streetcomplete/ui/theme/Dimensions.kt | 44 ++++++++++++------- 3 files changed, 38 insertions(+), 21 deletions(-) create mode 100644 app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/ktx/WindowInfo.kt diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/Crosshair.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/Crosshair.kt index 04b2e6b1b7..6ce28015ec 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/Crosshair.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/Crosshair.kt @@ -1,6 +1,6 @@ package de.westnordost.streetcomplete.screens.main.controls -import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.ContentAlpha @@ -10,22 +10,23 @@ import androidx.compose.material.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalWindowInfo import de.westnordost.streetcomplete.resources.* import de.westnordost.streetcomplete.ui.theme.AppTheme -import de.westnordost.streetcomplete.ui.theme.getOpenQuestFormMapPadding import org.jetbrains.compose.resources.painterResource import androidx.compose.ui.tooling.preview.Preview +import de.westnordost.streetcomplete.ui.theme.Dimensions /** A crosshair at the position at which a new POI should be created */ @Composable fun Crosshair(modifier: Modifier = Modifier) { - BoxWithConstraints(modifier.fillMaxSize()) { + Box(modifier) { Icon( painter = painterResource(Res.drawable.crosshair), contentDescription = null, modifier = Modifier .align(Alignment.Center) - .padding(getOpenQuestFormMapPadding(maxWidth, maxHeight)), + .padding(Dimensions.getOpenQuestFormMapPadding(LocalWindowInfo.current)), tint = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium) ) } diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/ktx/WindowInfo.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/ktx/WindowInfo.kt new file mode 100644 index 0000000000..78906512d8 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/ktx/WindowInfo.kt @@ -0,0 +1,6 @@ +package de.westnordost.streetcomplete.ui.ktx + +import androidx.compose.ui.platform.WindowInfo + +val WindowInfo.isLandscape: Boolean get() = + containerSize.width > containerSize.height diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Dimensions.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Dimensions.kt index 268c13f9a6..ffb66d6af9 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Dimensions.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Dimensions.kt @@ -1,25 +1,35 @@ package de.westnordost.streetcomplete.ui.theme import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.platform.WindowInfo import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.ui.ktx.isLandscape -/** Padding on the map due to an open quest form */ -fun getOpenQuestFormMapPadding(totalWidth: Dp, totalHeight: Dp): PaddingValues { - val isLandscape = totalWidth > totalHeight - return PaddingValues.Absolute( - left = if (isLandscape) getMaxQuestFormWidth(totalWidth) else 0.dp, - top = 0.dp, - right = 0.dp, - bottom = if (isLandscape) 0.dp else 320.dp - ) -} +object Dimensions { + val QuestFormPeekHeight = 400.dp -fun getMaxQuestFormWidth(totalWidth: Dp): Dp = - if (totalWidth >= 820.dp) 480.dp - else if (totalWidth >= 600.dp) 360.dp - else 480.dp + fun getMaxQuestFormWidth(windowInfo: WindowInfo): Dp = + if (windowInfo.isLandscape) { + // in landscape mode, the quest form is by default expanded, so it already takes up + // more/all vertical space. Especially on smaller screens, the width thus must be + // somewhat limited so that the map is still visible + if (windowInfo.containerDpSize.width > 820.dp) 480.dp + else 360.dp + } + else { + // in portrait mode, it may stretch very wide + 480.dp + } -fun getQuestFormPeekHeight(isLandscape: Boolean): Dp = - if (isLandscape) 540.dp - else 400.dp + /** Padding on the map due to an open quest form */ + fun getOpenQuestFormMapPadding(windowInfo: WindowInfo): PaddingValues { + val isLandscape = windowInfo.isLandscape + return PaddingValues.Absolute( + left = if (isLandscape) getMaxQuestFormWidth(windowInfo) else 0.dp, + top = 0.dp, + right = 0.dp, + bottom = if (isLandscape) 0.dp else 320.dp + ) + } +} From 2ae3a53ee71c80de624c83f6dbe8474ce3696917 Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Tue, 14 Apr 2026 17:35:15 +0200 Subject: [PATCH 12/28] use BottomSheet in QuestForm, make scrollable, add floating OK button --- .../ui/common/quest/QuestAnswerBubble.kt | 9 +- .../ui/common/quest/QuestForm.kt | 175 ++++++++++-------- 2 files changed, 102 insertions(+), 82 deletions(-) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerBubble.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerBubble.kt index f733ecd4fd..0ef953d753 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerBubble.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerBubble.kt @@ -7,7 +7,9 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Divider import androidx.compose.material.MaterialTheme import androidx.compose.material.ProvideTextStyle @@ -19,6 +21,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubble import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubbleArrowDirection +import de.westnordost.streetcomplete.ui.ktx.fadingVerticalScrollEdges /** Speech bubble for the quest answer, i.e. content and/or button bar answers */ @Composable @@ -30,6 +33,7 @@ fun QuestAnswerBubble( contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 12.dp), content: @Composable (BoxScope.() -> Unit)? = null ) { + val scrollState = rememberScrollState() Surface( modifier = modifier, shape = RoundedCornerShape(16.dp), @@ -37,7 +41,10 @@ fun QuestAnswerBubble( border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f)), ) { ProvideTextStyle(MaterialTheme.typography.body1) { - Column { + Column(Modifier + .fadingVerticalScrollEdges(scrollState, 32.dp) + .verticalScroll(scrollState), + ) { if (content != null) { Box( modifier = Modifier.fillMaxWidth().padding(contentPadding), diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt index bbdff58a19..56d454c425 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt @@ -1,80 +1,65 @@ package de.westnordost.streetcomplete.ui.common.quest +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material.BottomSheetScaffold -import androidx.compose.material.Button import androidx.compose.material.ContentAlpha -import androidx.compose.material.DropdownMenu -import androidx.compose.material.FabPosition -import androidx.compose.material.FloatingActionButton -import androidx.compose.material.Icon -import androidx.compose.material.LocalContentAlpha import androidx.compose.material.MaterialTheme -import androidx.compose.material.MaterialTheme.colors -import androidx.compose.material.ProvideTextStyle import androidx.compose.material.Surface import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.TopAppBar -import androidx.compose.material.rememberBottomSheetScaffoldState import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocal -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.datasource.LoremIpsum import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import de.westnordost.streetcomplete.data.osm.edits.EditType import de.westnordost.streetcomplete.data.quest.QuestType import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement -import de.westnordost.streetcomplete.resources.Res -import de.westnordost.streetcomplete.resources.ic_arrow_drop_down_24 -import de.westnordost.streetcomplete.resources.note_for_object -import de.westnordost.streetcomplete.resources.quest_generic_otherAnswers2 -import de.westnordost.streetcomplete.resources.quest_maxweight_title -import de.westnordost.streetcomplete.resources.quest_streetName_hint -import de.westnordost.streetcomplete.resources.quest_streetName_title -import de.westnordost.streetcomplete.screens.main.messages.LoremIpsumLines -import de.westnordost.streetcomplete.ui.common.DropdownMenuItem +import de.westnordost.streetcomplete.resources.* +import de.westnordost.streetcomplete.ui.common.FloatingOkButton +import de.westnordost.streetcomplete.ui.common.bottom_sheet.BottomSheet +import de.westnordost.streetcomplete.ui.common.bottom_sheet.BottomSheetState import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubble import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubbleArrowDirection +import de.westnordost.streetcomplete.ui.ktx.isLandscape +import de.westnordost.streetcomplete.ui.theme.Dimensions import de.westnordost.streetcomplete.ui.theme.defaultTextLinkStyles import de.westnordost.streetcomplete.ui.theme.titleSmall import de.westnordost.streetcomplete.ui.util.annotateLinks -import kotlinx.coroutines.launch import org.jetbrains.compose.resources.DrawableResource -import org.jetbrains.compose.resources.StringResource -import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource +/** A quest form can **either** have an OK button for confirmation **or** a list of button answers + * at-a-time */ +sealed interface QuestAnswer + +data class Answers(val answers: List) : QuestAnswer { + // convenience constructors + constructor() : this(emptyList()) + constructor(vararg answers: Answer) : this(answers.toList()) +} +data class Confirm( + val isVisible: Boolean, + val onClick: () -> Unit, +) : QuestAnswer + + /** A generic quest form, with a [title], [subtitle], [hintText] and [hintImages] in the * header speech bubble, then an optional [note] by another mapper shown below as another speech * bubble, then finally the speech bubble containing the center-aligned [content] padded with a @@ -84,53 +69,82 @@ import org.jetbrains.compose.resources.stringResource @Composable fun QuestForm( questType: QuestType, + answers: QuestAnswer, modifier: Modifier = Modifier, title: String = stringResource(questType.title), subtitle: AnnotatedString? = null, hintText: String? = questType.hint?.let { stringResource(it) }, hintImages: List = questType.hintImages, note: String? = null, - answers: List = emptyList(), - otherAnswers: List = emptyList(), - contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 12.dp), + otherAnswers: Answers = Answers(), + contentPadding: PaddingValues = PaddingValues(horizontal = 24.dp, vertical = 12.dp), content: @Composable (BoxScope.() -> Unit)? = null ) { - val elevation = 16.dp - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(4.dp) + val windowInfo = LocalWindowInfo.current + + val initialState = + if (LocalWindowInfo.current.isLandscape) BottomSheetState.Expanded + else BottomSheetState.Collapsed + + val elevation = 4.dp + + Box(modifier = modifier.sizeIn(maxWidth = Dimensions.getMaxQuestFormWidth(windowInfo)) ) { - SpeechBubble( - elevation = elevation, - arrowDirection = SpeechBubbleArrowDirection.Top, - arrowPlacementBias = 0.1f, - modifier = Modifier.padding(horizontal = 4.dp) + BottomSheet( + initialState = initialState, + peekHeight = Dimensions.QuestFormPeekHeight ) { - QuestHeader( - title = title, - subtitle = subtitle, - hintText = hintText, - hintImages = hintImages, - modifier = Modifier.fillMaxWidth(), - ) - } + Column( + modifier = Modifier + .fillMaxWidth() + .safeDrawingPadding(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + SpeechBubble( + elevation = elevation, + arrowDirection = SpeechBubbleArrowDirection.Top, + arrowPlacementBias = 0.1f, + modifier = Modifier.padding(horizontal = 8.dp) + ) { + QuestHeader( + title = title, + subtitle = subtitle, + hintText = hintText, + hintImages = hintImages, + modifier = Modifier.fillMaxWidth(), + ) + } + + if (note != null) { + NoteBubble( + text = note, + elevation = elevation, + modifier = Modifier + .padding(horizontal = 8.dp) + .fillMaxWidth() + ) + } - if (note != null) { - NoteBubble( - text = note, - elevation = elevation, - modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp) + QuestAnswerBubble( + modifier = Modifier.fillMaxWidth(), + elevation = elevation, + answers = (answers as? Answers)?.answers ?: emptyList(), + otherAnswers = otherAnswers.answers, + contentPadding = contentPadding, + content = content, + ) + } + } + if (answers is Confirm) { + FloatingOkButton( + visible = answers.isVisible, + onClick = answers.onClick, + modifier = Modifier + .align(Alignment.BottomEnd) + .safeDrawingPadding() + .padding(8.dp) ) } - - QuestAnswerBubble( - modifier = Modifier.fillMaxWidth(), - elevation = elevation, - answers = answers, - otherAnswers = otherAnswers, - contentPadding = contentPadding, - content = content, - ) } } @@ -144,6 +158,7 @@ private fun NoteBubble( modifier = modifier, shape = RoundedCornerShape(16.dp), elevation = elevation, + border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f)), ) { Column(Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) { Text( @@ -164,6 +179,7 @@ private fun NoteBubble( @Preview @Composable private fun QuestFormPreview() { + Box(Modifier.fillMaxSize().background(Color.Green)) { QuestForm( questType = object : QuestType { override val icon = 0 @@ -174,16 +190,13 @@ private fun QuestFormPreview() { }, subtitle = AnnotatedString("Tertiary Road"), note = "unpaved", - answers = listOf( - Answer("Yes") {}, - Answer("No") {}, - Answer("Perhaps") {}, - ), - otherAnswers = listOf( + answers = Confirm(true, onClick = {}), + otherAnswers = Answers( Answer("Can't say") {}, Answer("Can say") {}, ) ) { - Text("Some content") + Text(LoremIpsum(500).values.joinToString(" ")) + } } } From dbc611384d42dca6562182a597aba1d65137d183 Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Tue, 14 Apr 2026 17:44:48 +0200 Subject: [PATCH 13/28] always use CompositionLocalProvider with LocalContentAlpha for hint text coloring for consistency --- .../screens/main/edithistory/EditDetails.kt | 13 ++++++++----- .../overlay_selection/OverlaySelectionRow.kt | 15 +++++++++------ .../settings/quest_selection/QuestSelectionRow.kt | 15 +++++++++------ .../quests/max_speed/MaxSpeedForm.kt | 13 ++++++++----- .../quests/max_speed/RoadTypeSelect.kt | 13 ++++++++----- .../main/bottom_sheet/move_node/MoveNodeForm.kt | 13 ++++++++----- .../streetcomplete/ui/common/quest/QuestForm.kt | 15 +++++++++------ 7 files changed, 59 insertions(+), 38 deletions(-) diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/edithistory/EditDetails.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/edithistory/EditDetails.kt index c96a5365ef..c1bc9bf816 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/edithistory/EditDetails.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/edithistory/EditDetails.kt @@ -7,10 +7,12 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.ContentAlpha import androidx.compose.material.Divider +import androidx.compose.material.LocalContentAlpha import androidx.compose.material.LocalContentColor import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -45,11 +47,12 @@ fun EditDetails( modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp) ) { - Text( - text = dateTimeFormatter.format(createdTime), - style = MaterialTheme.typography.body2, - color = LocalContentColor.current.copy(alpha = ContentAlpha.medium), - ) + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { + Text( + text = dateTimeFormatter.format(createdTime), + style = MaterialTheme.typography.body2, + ) + } Row( horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/settings/overlay_selection/OverlaySelectionRow.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/settings/overlay_selection/OverlaySelectionRow.kt index f46854feaf..dad24414fb 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/settings/overlay_selection/OverlaySelectionRow.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/settings/overlay_selection/OverlaySelectionRow.kt @@ -13,10 +13,12 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.Checkbox import androidx.compose.material.ContentAlpha +import androidx.compose.material.LocalContentAlpha import androidx.compose.material.LocalContentColor import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -73,10 +75,11 @@ fun OverlaySelectionRow( @Composable private fun DisabledHint(text: String) { - Text( - text = text, - style = MaterialTheme.typography.body2, - fontStyle = FontStyle.Italic, - color = LocalContentColor.current.copy(alpha = ContentAlpha.medium), - ) + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { + Text( + text = text, + style = MaterialTheme.typography.body2, + fontStyle = FontStyle.Italic, + ) + } } diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/settings/quest_selection/QuestSelectionRow.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/settings/quest_selection/QuestSelectionRow.kt index b95669c8ab..4671dce523 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/settings/quest_selection/QuestSelectionRow.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/settings/quest_selection/QuestSelectionRow.kt @@ -15,10 +15,12 @@ import androidx.compose.foundation.layout.width import androidx.compose.material.Checkbox import androidx.compose.material.ContentAlpha import androidx.compose.material.Icon +import androidx.compose.material.LocalContentAlpha import androidx.compose.material.LocalContentColor import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -92,12 +94,13 @@ fun QuestSelectionRow( @Composable private fun DisabledHint(text: String) { - Text( - text = text, - style = MaterialTheme.typography.body2, - fontStyle = FontStyle.Italic, - color = LocalContentColor.current.copy(alpha = ContentAlpha.medium), - ) + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { + Text( + text = text, + style = MaterialTheme.typography.body2, + fontStyle = FontStyle.Italic, + ) + } } @Preview diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/max_speed/MaxSpeedForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/max_speed/MaxSpeedForm.kt index 12074d8e21..d6b33c4e04 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/max_speed/MaxSpeedForm.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/max_speed/MaxSpeedForm.kt @@ -6,10 +6,12 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.ContentAlpha +import androidx.compose.material.LocalContentAlpha import androidx.compose.material.LocalContentColor import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -66,11 +68,12 @@ fun MaxSpeedForm( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(4.dp) ) { - Text( - text = stringResource(Res.string.quest_maxspeed_type_description), - color = LocalContentColor.current.copy(alpha = ContentAlpha.medium), - style = MaterialTheme.typography.body2, - ) + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { + Text( + text = stringResource(Res.string.quest_maxspeed_type_description), + style = MaterialTheme.typography.body2, + ) + } DropdownButton( items = selectableMaxSpeedTypes, onSelectedItem = { onAnswer(it) }, diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/max_speed/RoadTypeSelect.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/max_speed/RoadTypeSelect.kt index 955c62aa30..c4f5c00548 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/max_speed/RoadTypeSelect.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/max_speed/RoadTypeSelect.kt @@ -2,10 +2,12 @@ package de.westnordost.streetcomplete.quests.max_speed import androidx.compose.foundation.layout.Column import androidx.compose.material.ContentAlpha +import androidx.compose.material.LocalContentAlpha import androidx.compose.material.LocalContentColor import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -33,11 +35,12 @@ fun RoadTypeSelect( horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier, ) { - Text( - text = stringResource(Res.string.quest_maxspeed_answer_roadtype_description), - color = LocalContentColor.current.copy(alpha = ContentAlpha.medium), - style = MaterialTheme.typography.body2, - ) + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { + Text( + text = stringResource(Res.string.quest_maxspeed_answer_roadtype_description), + style = MaterialTheme.typography.body2, + ) + } ItemSelectGrid( columns = SimpleGridCells.Fixed(cells), items = selectable, diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/bottom_sheet/move_node/MoveNodeForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/bottom_sheet/move_node/MoveNodeForm.kt index a2576f35dc..14dff59b08 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/bottom_sheet/move_node/MoveNodeForm.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/bottom_sheet/move_node/MoveNodeForm.kt @@ -5,10 +5,12 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.ContentAlpha import androidx.compose.material.Divider +import androidx.compose.material.LocalContentAlpha import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.unit.dp @@ -50,11 +52,12 @@ fun MoveNodeForm( style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(bottom = 8.dp) ) - Text( - text = stringResource(Res.string.move_node_description), - style = MaterialTheme.typography.body2, - modifier = Modifier.alpha(ContentAlpha.medium) - ) + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { + Text( + text = stringResource(Res.string.move_node_description), + style = MaterialTheme.typography.body2, + ) + } Divider() diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt index 56d454c425..f45c0d5cac 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt @@ -15,10 +15,12 @@ import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.ContentAlpha +import androidx.compose.material.LocalContentAlpha import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -165,12 +167,13 @@ private fun NoteBubble( text = stringResource(Res.string.note_for_object), style = MaterialTheme.typography.titleSmall ) - SelectionContainer { - Text( - text = text.annotateLinks(MaterialTheme.typography.defaultTextLinkStyles()), - style = MaterialTheme.typography.body2, - modifier = Modifier.alpha(ContentAlpha.medium) - ) + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { + SelectionContainer { + Text( + text = text.annotateLinks(MaterialTheme.typography.defaultTextLinkStyles()), + style = MaterialTheme.typography.body2, + ) + } } } } From 18ebf603801bedd154c144a7e0fc75ac4cb6eb14 Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Tue, 14 Apr 2026 17:48:11 +0200 Subject: [PATCH 14/28] put NoteBubble into own file --- .../ui/common/quest/NoteBubble.kt | 52 +++++++++++++++++++ .../ui/common/quest/QuestForm.kt | 39 +++----------- 2 files changed, 59 insertions(+), 32 deletions(-) create mode 100644 app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/NoteBubble.kt diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/NoteBubble.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/NoteBubble.kt new file mode 100644 index 0000000000..e125ada00b --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/NoteBubble.kt @@ -0,0 +1,52 @@ +package de.westnordost.streetcomplete.ui.common.quest + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.ContentAlpha +import androidx.compose.material.LocalContentAlpha +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.resources.* +import de.westnordost.streetcomplete.ui.theme.defaultTextLinkStyles +import de.westnordost.streetcomplete.ui.theme.titleSmall +import de.westnordost.streetcomplete.ui.util.annotateLinks +import org.jetbrains.compose.resources.stringResource + +/** Speech bubble (without arrow) that contains a note another user left for this object */ +@Composable +fun NoteBubble( + text: String, + modifier: Modifier = Modifier, + elevation: Dp = 0.dp, +) { + Surface( + modifier = modifier, + shape = RoundedCornerShape(16.dp), + elevation = elevation, + border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f)), + ) { + Column(Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) { + Text( + text = stringResource(Res.string.note_for_object), + style = MaterialTheme.typography.titleSmall + ) + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { + SelectionContainer { + Text( + text = text.annotateLinks(MaterialTheme.typography.defaultTextLinkStyles()), + style = MaterialTheme.typography.body2, + ) + } + } + } + } +} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt index f45c0d5cac..6c91b77331 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt @@ -65,7 +65,7 @@ data class Confirm( /** A generic quest form, with a [title], [subtitle], [hintText] and [hintImages] in the * header speech bubble, then an optional [note] by another mapper shown below as another speech * bubble, then finally the speech bubble containing the center-aligned [content] padded with a - * [contentPadding] (if there is any content) and below a row oftext buttons showing different + * [contentPadding] (if there is any content) and below a row of text buttons showing different * [answers]. At the very start of the text button row, there's a text button labeled "Uh…" that, * when tapped, opens a dropdown menu containing [otherAnswers]. */ @Composable @@ -150,35 +150,6 @@ fun QuestForm( } } -@Composable -private fun NoteBubble( - text: String, - modifier: Modifier = Modifier, - elevation: Dp = 0.dp, -) { - Surface( - modifier = modifier, - shape = RoundedCornerShape(16.dp), - elevation = elevation, - border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f)), - ) { - Column(Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) { - Text( - text = stringResource(Res.string.note_for_object), - style = MaterialTheme.typography.titleSmall - ) - CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { - SelectionContainer { - Text( - text = text.annotateLinks(MaterialTheme.typography.defaultTextLinkStyles()), - style = MaterialTheme.typography.body2, - ) - } - } - } - } -} - @Preview @Composable private fun QuestFormPreview() { @@ -193,13 +164,17 @@ private fun QuestFormPreview() { }, subtitle = AnnotatedString("Tertiary Road"), note = "unpaved", - answers = Confirm(true, onClick = {}), + answers = Answers( + Answer("Yes") {}, + Answer("Maybe") {}, + Answer("No") {}, + ), otherAnswers = Answers( Answer("Can't say") {}, Answer("Can say") {}, ) ) { - Text(LoremIpsum(500).values.joinToString(" ")) + Text(LoremIpsum(200).values.joinToString(" ")) } } } From 590a7d5d38ef205bb4156139e786481838fce384 Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Tue, 14 Apr 2026 17:51:19 +0200 Subject: [PATCH 15/28] comments --- .../streetcomplete/ui/common/quest/QuestForm.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt index 6c91b77331..992b34fcbf 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt @@ -66,8 +66,9 @@ data class Confirm( * header speech bubble, then an optional [note] by another mapper shown below as another speech * bubble, then finally the speech bubble containing the center-aligned [content] padded with a * [contentPadding] (if there is any content) and below a row of text buttons showing different - * [answers]. At the very start of the text button row, there's a text button labeled "Uh…" that, - * when tapped, opens a dropdown menu containing [otherAnswers]. */ + * [answers] (defined from start to end). At the very start of the text button row, there's a text + * button labeled "Uh…" that, when tapped, opens a dropdown menu containing [otherAnswers] + * (defined from start to bottom). */ @Composable fun QuestForm( questType: QuestType, @@ -153,7 +154,6 @@ fun QuestForm( @Preview @Composable private fun QuestFormPreview() { - Box(Modifier.fillMaxSize().background(Color.Green)) { QuestForm( questType = object : QuestType { override val icon = 0 @@ -165,9 +165,9 @@ private fun QuestFormPreview() { subtitle = AnnotatedString("Tertiary Road"), note = "unpaved", answers = Answers( - Answer("Yes") {}, - Answer("Maybe") {}, Answer("No") {}, + Answer("Maybe") {}, + Answer("Yes") {}, ), otherAnswers = Answers( Answer("Can't say") {}, @@ -176,5 +176,4 @@ private fun QuestFormPreview() { ) { Text(LoremIpsum(200).values.joinToString(" ")) } - } } From 631b21ec2681bfc73d2df9f6603c5b502f16349d Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Tue, 14 Apr 2026 22:04:44 +0200 Subject: [PATCH 16/28] fix nested scrolling and flinging --- .../ui/common/bottom_sheet/BottomSheet.kt | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/bottom_sheet/BottomSheet.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/bottom_sheet/BottomSheet.kt index bc73c4cfa8..d350b9bce6 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/bottom_sheet/BottomSheet.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/bottom_sheet/BottomSheet.kt @@ -49,7 +49,7 @@ fun BottomSheet( } val flingBehavior = AnchoredDraggableDefaults.flingBehavior(state) val nestedScrollConnection = remember(state, flingBehavior) { - ConsumeNestedScrollConnection( + ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection( state = state, flingBehavior = flingBehavior, orientation = Orientation.Vertical @@ -90,7 +90,7 @@ fun BottomSheet( } } -private fun ConsumeNestedScrollConnection( +private fun ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection( state: AnchoredDraggableState<*>, flingBehavior: FlingBehavior, orientation: Orientation, @@ -118,6 +118,15 @@ private fun ConsumeNestedScrollConnection( val currentOffset = state.requireOffset() return if (toFling < 0 && currentOffset > state.anchors.minPosition()) { // since we go to the anchor with tween settling, consume all for the best UX + state.anchoredDrag { + val scrollFlingScope = object : ScrollScope { + override fun scrollBy(pixels: Float): Float { + dragTo(state.offset + pixels) + return pixels + } + } + with(flingBehavior) { scrollFlingScope.performFling(toFling) } + } available } else { Velocity.Zero @@ -125,7 +134,7 @@ private fun ConsumeNestedScrollConnection( } override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { - // this API is batshit crazy. Do I understand it? No. I got it from the example at + // this API is crazy. Do I understand it? No. I got it from the example at // https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/AnchoredDraggableSample.kt;l=416-432;drc=7440f70755e3735dbd8f04718d12dfeec7584dc8 // pointed at from the comment of the now deprecated `state.settle(velocity)` API. state.anchoredDrag { @@ -135,7 +144,7 @@ private fun ConsumeNestedScrollConnection( return pixels } } - with(flingBehavior) { scrollFlingScope.performFling(consumed.toFloat()) } + with(flingBehavior) { scrollFlingScope.performFling(available.toFloat()) } } return available } From 154b0eea4625e49a795498d15e523683b98ff1bb Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Wed, 15 Apr 2026 00:29:52 +0200 Subject: [PATCH 17/28] add constant for speech bubble corner radius --- .../streetcomplete/ui/common/quest/NoteBubble.kt | 3 ++- .../streetcomplete/ui/common/quest/QuestAnswerBubble.kt | 3 ++- .../streetcomplete/ui/common/quest/QuestForm.kt | 3 +-- .../streetcomplete/ui/common/speech_bubble/SpeechBubble.kt | 5 +++-- .../de/westnordost/streetcomplete/ui/theme/Shapes.kt | 7 +++++-- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/NoteBubble.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/NoteBubble.kt index e125ada00b..91e509b8d5 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/NoteBubble.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/NoteBubble.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import de.westnordost.streetcomplete.resources.* import de.westnordost.streetcomplete.ui.theme.defaultTextLinkStyles +import de.westnordost.streetcomplete.ui.theme.speechBubbleCornerRadius import de.westnordost.streetcomplete.ui.theme.titleSmall import de.westnordost.streetcomplete.ui.util.annotateLinks import org.jetbrains.compose.resources.stringResource @@ -30,7 +31,7 @@ fun NoteBubble( ) { Surface( modifier = modifier, - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(MaterialTheme.shapes.speechBubbleCornerRadius), elevation = elevation, border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f)), ) { diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerBubble.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerBubble.kt index 0ef953d753..a55bfd8a86 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerBubble.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerBubble.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubble import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubbleArrowDirection import de.westnordost.streetcomplete.ui.ktx.fadingVerticalScrollEdges +import de.westnordost.streetcomplete.ui.theme.speechBubbleCornerRadius /** Speech bubble for the quest answer, i.e. content and/or button bar answers */ @Composable @@ -36,7 +37,7 @@ fun QuestAnswerBubble( val scrollState = rememberScrollState() Surface( modifier = modifier, - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(MaterialTheme.shapes.speechBubbleCornerRadius), elevation = elevation, border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f)), ) { diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt index 992b34fcbf..d6273af805 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt @@ -91,8 +91,7 @@ fun QuestForm( val elevation = 4.dp - Box(modifier = modifier.sizeIn(maxWidth = Dimensions.getMaxQuestFormWidth(windowInfo)) - ) { + Box(modifier = modifier.sizeIn(maxWidth = Dimensions.getMaxQuestFormWidth(windowInfo))) { BottomSheet( initialState = initialState, peekHeight = Dimensions.QuestFormPeekHeight diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubble.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubble.kt index 264ebd7206..3c7ae64bc6 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubble.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubble.kt @@ -13,14 +13,15 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.ui.theme.speechBubbleCornerRadius /** Surface in the shape of a speech bubble with a border stroke and a default inner padding by * default. */ @Composable fun SpeechBubble( modifier: Modifier = Modifier, - cornerRadius: Dp = 16.dp, - arrowSize: Dp = 12.dp, + cornerRadius: Dp = MaterialTheme.shapes.speechBubbleCornerRadius, + arrowSize: Dp = cornerRadius * 0.75f, arrowDirection: SpeechBubbleArrowDirection = SpeechBubbleArrowDirection.Bottom, arrowPlacementBias: Float = 0f, color: Color = MaterialTheme.colors.surface, diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Shapes.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Shapes.kt index 1fce497268..8158e9b1b8 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Shapes.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Shapes.kt @@ -2,10 +2,13 @@ package de.westnordost.streetcomplete.ui.theme import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Shapes +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp val Shapes = Shapes( // theme is more speech-bubbly than default - medium = RoundedCornerShape(10.dp), - large = RoundedCornerShape(10.dp) + medium = RoundedCornerShape(12.dp), + large = RoundedCornerShape(12.dp) ) + +val Shapes.speechBubbleCornerRadius: Dp get() = 16.dp From f3228e588832a0f0015f4d17f13222ed17bd9261 Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Wed, 15 Apr 2026 14:41:49 +0200 Subject: [PATCH 18/28] FloatingActionButton: properly implement greying out when disabled -_- --- .../ui/common/FloatingActionButton.kt | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/FloatingActionButton.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/FloatingActionButton.kt index 80a6b91e09..95ee46285b 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/FloatingActionButton.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/FloatingActionButton.kt @@ -1,23 +1,24 @@ package de.westnordost.streetcomplete.ui.common +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.shape.CornerSize +import androidx.compose.material.ButtonColors +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.ButtonElevation import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.FloatingActionButtonDefaults -import androidx.compose.material.FloatingActionButtonElevation import androidx.compose.material.LocalContentAlpha import androidx.compose.material.MaterialTheme import androidx.compose.material.ProvideTextStyle import androidx.compose.material.Surface -import androidx.compose.material.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.role @@ -33,29 +34,31 @@ fun FloatingActionButton( modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource? = null, + elevation: ButtonElevation? = FloatingActionButtonDefaults.elevation(), shape: Shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)), - backgroundColor: Color = MaterialTheme.colors.secondary, - contentColor: Color = contentColorFor(backgroundColor), - elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), + border: BorderStroke? = null, + colors: ButtonColors = FloatingActionButtonDefaults.buttonColors(), content: @Composable () -> Unit, ) { @Suppress("NAME_SHADOWING") val interactionSource = interactionSource ?: remember { MutableInteractionSource() } + val contentColor by colors.contentColor(enabled) Surface( onClick = onClick, modifier = modifier.semantics { role = Role.Button }, enabled = enabled, shape = shape, - color = backgroundColor, - contentColor = contentColor, - elevation = elevation.elevation(interactionSource).value, + color = colors.backgroundColor(enabled).value, + contentColor = contentColor.copy(alpha = 1f), + border = border, + elevation = elevation?.elevation(enabled, interactionSource)?.value ?: 0.dp, interactionSource = interactionSource, ) { CompositionLocalProvider(LocalContentAlpha provides contentColor.alpha) { - ProvideTextStyle(MaterialTheme.typography.button) { + ProvideTextStyle(value = MaterialTheme.typography.button) { Box( modifier = Modifier.defaultMinSize(minWidth = FabSize, minHeight = FabSize), - contentAlignment = Alignment.Center, + contentAlignment = Alignment.Center ) { content() } @@ -65,3 +68,18 @@ fun FloatingActionButton( } private val FabSize = 56.dp + +object FloatingActionButtonDefaults { + @Composable fun elevation() = ButtonDefaults.elevation( + defaultElevation = 6.dp, + pressedElevation = 12.dp, + disabledElevation = 6.dp, + hoveredElevation = 8.dp, + focusedElevation = 8.dp, + ) + + @Composable fun buttonColors() = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colors.secondary + ) +} + From 7720e3c1a04cbf32586054328b3f76099719e19b Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Wed, 15 Apr 2026 14:42:18 +0200 Subject: [PATCH 19/28] add SpeechBubbleNoArrow because there are so many common elements --- .../speech_bubble/SpeechBubbleNoArrow.kt | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubbleNoArrow.kt diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubbleNoArrow.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubbleNoArrow.kt new file mode 100644 index 0000000000..feb977b761 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubbleNoArrow.kt @@ -0,0 +1,44 @@ +package de.westnordost.streetcomplete.ui.common.speech_bubble + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.ui.theme.speechBubbleCornerRadius + +/** A spech bubble without an arrow, so basically mostly a surface with rounded corners. However, + * there are some common defaults, so it makes sense to put it into an own composable. */ +@Composable +fun SpeechBubbleNoArrow( + modifier: Modifier = Modifier, + cornerRadius: Dp = MaterialTheme.shapes.speechBubbleCornerRadius, + color: Color = MaterialTheme.colors.surface, + contentColor: Color = contentColorFor(color), + elevation: Dp = 0.dp, + border: BorderStroke? = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f)), + contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 12.dp), + content: @Composable () -> Unit, +) { + Surface( + modifier = modifier, + shape = RoundedCornerShape(cornerRadius), + color = color, + contentColor = contentColor, + border = border, + elevation = elevation, + ) { + Box(modifier = Modifier.padding(contentPadding)) { + content() + } + } +} From b1a19e831927fcea794e9bbef6f8545144a5477a Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Wed, 15 Apr 2026 14:42:43 +0200 Subject: [PATCH 20/28] use SpeechBubbleNoArrow --- .../ui/common/quest/NoteBubble.kt | 53 ------------------- .../ui/common/quest/QuestAnswerBubble.kt | 23 ++++---- .../ui/common/quest/QuestForm.kt | 37 +++++++++++-- 3 files changed, 43 insertions(+), 70 deletions(-) delete mode 100644 app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/NoteBubble.kt diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/NoteBubble.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/NoteBubble.kt deleted file mode 100644 index 91e509b8d5..0000000000 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/NoteBubble.kt +++ /dev/null @@ -1,53 +0,0 @@ -package de.westnordost.streetcomplete.ui.common.quest - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material.ContentAlpha -import androidx.compose.material.LocalContentAlpha -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import de.westnordost.streetcomplete.resources.* -import de.westnordost.streetcomplete.ui.theme.defaultTextLinkStyles -import de.westnordost.streetcomplete.ui.theme.speechBubbleCornerRadius -import de.westnordost.streetcomplete.ui.theme.titleSmall -import de.westnordost.streetcomplete.ui.util.annotateLinks -import org.jetbrains.compose.resources.stringResource - -/** Speech bubble (without arrow) that contains a note another user left for this object */ -@Composable -fun NoteBubble( - text: String, - modifier: Modifier = Modifier, - elevation: Dp = 0.dp, -) { - Surface( - modifier = modifier, - shape = RoundedCornerShape(MaterialTheme.shapes.speechBubbleCornerRadius), - elevation = elevation, - border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f)), - ) { - Column(Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) { - Text( - text = stringResource(Res.string.note_for_object), - style = MaterialTheme.typography.titleSmall - ) - CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { - SelectionContainer { - Text( - text = text.annotateLinks(MaterialTheme.typography.defaultTextLinkStyles()), - style = MaterialTheme.typography.body2, - ) - } - } - } - } -} diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerBubble.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerBubble.kt index a55bfd8a86..34d4524ceb 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerBubble.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerBubble.kt @@ -1,6 +1,5 @@ package de.westnordost.streetcomplete.ui.common.quest -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column @@ -8,21 +7,17 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.Divider import androidx.compose.material.MaterialTheme import androidx.compose.material.ProvideTextStyle -import androidx.compose.material.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubble -import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubbleArrowDirection +import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubbleNoArrow import de.westnordost.streetcomplete.ui.ktx.fadingVerticalScrollEdges -import de.westnordost.streetcomplete.ui.theme.speechBubbleCornerRadius /** Speech bubble for the quest answer, i.e. content and/or button bar answers */ @Composable @@ -35,11 +30,10 @@ fun QuestAnswerBubble( content: @Composable (BoxScope.() -> Unit)? = null ) { val scrollState = rememberScrollState() - Surface( + SpeechBubbleNoArrow( modifier = modifier, - shape = RoundedCornerShape(MaterialTheme.shapes.speechBubbleCornerRadius), elevation = elevation, - border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f)), + contentPadding = PaddingValues.Zero ) { ProvideTextStyle(MaterialTheme.typography.body1) { Column(Modifier @@ -48,11 +42,12 @@ fun QuestAnswerBubble( ) { if (content != null) { Box( - modifier = Modifier.fillMaxWidth().padding(contentPadding), - contentAlignment = Alignment.Center - ) { - content() - } + modifier = Modifier + .fillMaxWidth() + .padding(contentPadding), + contentAlignment = Alignment.Center, + content = content + ) } if (content != null && (answers.isNotEmpty() || otherAnswers.isNotEmpty())) { Divider() diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt index d6273af805..9b498cd2a4 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt @@ -39,6 +39,7 @@ import de.westnordost.streetcomplete.ui.common.bottom_sheet.BottomSheet import de.westnordost.streetcomplete.ui.common.bottom_sheet.BottomSheetState import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubble import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubbleArrowDirection +import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubbleNoArrow import de.westnordost.streetcomplete.ui.ktx.isLandscape import de.westnordost.streetcomplete.ui.theme.Dimensions import de.westnordost.streetcomplete.ui.theme.defaultTextLinkStyles @@ -66,9 +67,10 @@ data class Confirm( * header speech bubble, then an optional [note] by another mapper shown below as another speech * bubble, then finally the speech bubble containing the center-aligned [content] padded with a * [contentPadding] (if there is any content) and below a row of text buttons showing different - * [answers] (defined from start to end). At the very start of the text button row, there's a text - * button labeled "Uh…" that, when tapped, opens a dropdown menu containing [otherAnswers] - * (defined from start to bottom). */ + * [answers] (defined from start to end). + * + * At the very start of the text button row, there's a text button labeled "Uh…" that, when tapped, + * opens a dropdown menu containing [otherAnswers] (defined from start to bottom). */ @Composable fun QuestForm( questType: QuestType, @@ -150,6 +152,35 @@ fun QuestForm( } } +/** Speech bubble (without arrow) that contains a note another user left for this object */ +@Composable +private fun NoteBubble( + text: String, + modifier: Modifier = Modifier, + elevation: Dp = 0.dp, +) { + SpeechBubbleNoArrow( + modifier = modifier, + elevation = elevation + ) { + Column { + Text( + text = stringResource(Res.string.note_for_object), + style = MaterialTheme.typography.titleSmall + ) + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { + SelectionContainer { + Text( + text = text.annotateLinks(MaterialTheme.typography.defaultTextLinkStyles()), + style = MaterialTheme.typography.body2, + ) + } + } + } + } +} + + @Preview @Composable private fun QuestFormPreview() { From c2ea871819ee0312c6ecdbaaf350f1d448adc0da Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Wed, 15 Apr 2026 14:42:59 +0200 Subject: [PATCH 21/28] add OverlayForm --- .../ui/common/overlay/OverlayForm.kt | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/overlay/OverlayForm.kt diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/overlay/OverlayForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/overlay/OverlayForm.kt new file mode 100644 index 0000000000..3020a803a8 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/overlay/OverlayForm.kt @@ -0,0 +1,186 @@ +package de.westnordost.streetcomplete.ui.common.overlay + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ContentAlpha +import androidx.compose.material.Divider +import androidx.compose.material.DropdownMenu +import androidx.compose.material.IconButton +import androidx.compose.material.LocalContentAlpha +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.MaterialTheme +import androidx.compose.material.ProvideTextStyle +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalWindowInfo +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.datasource.LoremIpsum +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.resources.Res +import de.westnordost.streetcomplete.resources.quest_generic_otherAnswers2 +import de.westnordost.streetcomplete.ui.common.DropdownMenuItem +import de.westnordost.streetcomplete.ui.common.FloatingOkButton +import de.westnordost.streetcomplete.ui.common.MoreIcon +import de.westnordost.streetcomplete.ui.common.quest.Answer +import de.westnordost.streetcomplete.ui.common.quest.Answers +import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubbleNoArrow +import de.westnordost.streetcomplete.ui.theme.Dimensions +import de.westnordost.streetcomplete.ui.theme.speechBubbleCornerRadius +import de.westnordost.streetcomplete.ui.theme.titleMedium +import de.westnordost.streetcomplete.ui.theme.titleSmall +import org.jetbrains.compose.resources.stringResource + +/** A generic overlay form containing the center-aligned [content], padded with [contentPadding]. + * Above it, an optional bubble with a [label] (in which the element is usually named). + * + * Below the content, there's an empty bar that contains only a "more" icon button on the start + * that, when tapped, opens a dropdown menu containing [otherAnswers]. + * + * Floating in the lower end corner, an OK button for confirmation. [okIsVisible] should be true + * when the form is complete, while [okIsEnabled] should be true when any changes have been made. + * */ +@Composable +fun OverlayForm( + okIsVisible: Boolean, + okIsEnabled: Boolean, + onClickOk: () -> Unit, + modifier: Modifier = Modifier, + label: AnnotatedString? = null, + otherAnswers: Answers = Answers(), + contentPadding: PaddingValues = PaddingValues(horizontal = 24.dp, vertical = 12.dp), + content: @Composable BoxScope.() -> Unit +) { + val windowInfo = LocalWindowInfo.current + + val elevation = 4.dp + + Box(modifier = modifier.sizeIn(maxWidth = Dimensions.getMaxQuestFormWidth(windowInfo))) { + Column( + modifier = Modifier + .fillMaxWidth() + .safeDrawingPadding(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (label != null) { + SpeechBubbleNoArrow( + modifier = Modifier.padding(horizontal = 8.dp), + elevation = elevation, + ) { + CompositionLocalProvider( + LocalTextStyle provides MaterialTheme.typography.titleMedium, + LocalContentAlpha provides ContentAlpha.medium + ) { + Text(label) + } + } + } + + OverlayAnswerBubble( + modifier = Modifier.fillMaxWidth(), + elevation = elevation, + otherAnswers = otherAnswers.answers, + contentPadding = contentPadding, + content = content + ) + } + FloatingOkButton( + visible = okIsVisible, + enabled = okIsEnabled, + onClick = onClickOk, + modifier = Modifier + .align(Alignment.BottomEnd) + .safeDrawingPadding() + .padding(8.dp) + ) + } +} + +@Composable +private fun OverlayAnswerBubble( + modifier: Modifier = Modifier, + elevation: Dp = 0.dp, + otherAnswers: List = emptyList(), + contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 12.dp), + content: @Composable BoxScope.() -> Unit +) { + SpeechBubbleNoArrow( + modifier = modifier, + elevation = elevation, + contentPadding = PaddingValues.Zero + ) { + ProvideTextStyle(MaterialTheme.typography.body1) { + Column { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(contentPadding), + contentAlignment = Alignment.Center, + content = content + ) + Divider() + MoreButton(answers = otherAnswers) + } + } + } +} + +@Composable +private fun MoreButton( + answers: List, + modifier: Modifier = Modifier, +) { + var expanded by rememberSaveable { mutableStateOf(false) } + + Box(modifier) { + IconButton(onClick = { expanded = true }) { + MoreIcon() + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + for (answer in answers) { + DropdownMenuItem(onClick = { expanded = false; answer.action() }) { + Text(answer.text) + } + } + } + } +} + +@Preview +@Composable +private fun OverlayFormPreview() { + OverlayForm( + okIsVisible = true, + okIsEnabled = false, + onClickOk = {}, + label = AnnotatedString("some text"), + otherAnswers = Answers( + Answer("Can't say") {}, + Answer("Can say") {}, + ) + ) { + Text(LoremIpsum(50).values.joinToString(" ")) + } +} From 6abb7ebad7d3a113b6e473356510aec7370bebda Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Wed, 15 Apr 2026 15:02:54 +0200 Subject: [PATCH 22/28] fix Crosshair.kt --- .../streetcomplete/screens/main/controls/Crosshair.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/Crosshair.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/Crosshair.kt index 6ce28015ec..106412162c 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/Crosshair.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/Crosshair.kt @@ -20,7 +20,7 @@ import de.westnordost.streetcomplete.ui.theme.Dimensions /** A crosshair at the position at which a new POI should be created */ @Composable fun Crosshair(modifier: Modifier = Modifier) { - Box(modifier) { + Box(modifier.fillMaxSize()) { Icon( painter = painterResource(Res.drawable.crosshair), contentDescription = null, From 775d133c796f0c7303cab805b7f70d5c7a2cdb01 Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Wed, 15 Apr 2026 15:19:17 +0200 Subject: [PATCH 23/28] move speechBubbleCornerRadius to Dimensions.kt --- .../streetcomplete/ui/common/overlay/OverlayForm.kt | 1 - .../streetcomplete/ui/common/speech_bubble/SpeechBubble.kt | 4 ++-- .../ui/common/speech_bubble/SpeechBubbleNoArrow.kt | 4 ++-- .../de/westnordost/streetcomplete/ui/theme/Dimensions.kt | 4 ++++ .../kotlin/de/westnordost/streetcomplete/ui/theme/Shapes.kt | 2 -- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/overlay/OverlayForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/overlay/OverlayForm.kt index 3020a803a8..72b4bbea60 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/overlay/OverlayForm.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/overlay/OverlayForm.kt @@ -45,7 +45,6 @@ import de.westnordost.streetcomplete.ui.common.quest.Answer import de.westnordost.streetcomplete.ui.common.quest.Answers import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubbleNoArrow import de.westnordost.streetcomplete.ui.theme.Dimensions -import de.westnordost.streetcomplete.ui.theme.speechBubbleCornerRadius import de.westnordost.streetcomplete.ui.theme.titleMedium import de.westnordost.streetcomplete.ui.theme.titleSmall import org.jetbrains.compose.resources.stringResource diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubble.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubble.kt index 3c7ae64bc6..0b263736fa 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubble.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubble.kt @@ -13,14 +13,14 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import de.westnordost.streetcomplete.ui.theme.speechBubbleCornerRadius +import de.westnordost.streetcomplete.ui.theme.Dimensions /** Surface in the shape of a speech bubble with a border stroke and a default inner padding by * default. */ @Composable fun SpeechBubble( modifier: Modifier = Modifier, - cornerRadius: Dp = MaterialTheme.shapes.speechBubbleCornerRadius, + cornerRadius: Dp = Dimensions.speechBubbleCornerRadius, arrowSize: Dp = cornerRadius * 0.75f, arrowDirection: SpeechBubbleArrowDirection = SpeechBubbleArrowDirection.Bottom, arrowPlacementBias: Float = 0f, diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubbleNoArrow.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubbleNoArrow.kt index feb977b761..4fcb42446d 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubbleNoArrow.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubbleNoArrow.kt @@ -14,14 +14,14 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import de.westnordost.streetcomplete.ui.theme.speechBubbleCornerRadius +import de.westnordost.streetcomplete.ui.theme.Dimensions /** A spech bubble without an arrow, so basically mostly a surface with rounded corners. However, * there are some common defaults, so it makes sense to put it into an own composable. */ @Composable fun SpeechBubbleNoArrow( modifier: Modifier = Modifier, - cornerRadius: Dp = MaterialTheme.shapes.speechBubbleCornerRadius, + cornerRadius: Dp = Dimensions.speechBubbleCornerRadius, color: Color = MaterialTheme.colors.surface, contentColor: Color = contentColorFor(color), elevation: Dp = 0.dp, diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Dimensions.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Dimensions.kt index ffb66d6af9..031093e1eb 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Dimensions.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Dimensions.kt @@ -1,12 +1,16 @@ package de.westnordost.streetcomplete.ui.theme import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material.Shapes import androidx.compose.ui.platform.WindowInfo import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import de.westnordost.streetcomplete.ui.ktx.isLandscape object Dimensions { + + val speechBubbleCornerRadius: Dp get() = 16.dp + val QuestFormPeekHeight = 400.dp fun getMaxQuestFormWidth(windowInfo: WindowInfo): Dp = diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Shapes.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Shapes.kt index 8158e9b1b8..be416b9569 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Shapes.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Shapes.kt @@ -10,5 +10,3 @@ val Shapes = Shapes( medium = RoundedCornerShape(12.dp), large = RoundedCornerShape(12.dp) ) - -val Shapes.speechBubbleCornerRadius: Dp get() = 16.dp From 9f4aee4f4c0ce8905009128acd2b7935418992f2 Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Wed, 15 Apr 2026 16:18:59 +0200 Subject: [PATCH 24/28] put divider color into Color.kt --- .../streetcomplete/screens/main/teammode/TeamModeWizard.kt | 3 ++- .../de/westnordost/streetcomplete/quests/lanes/LanesForm.kt | 3 ++- .../streetcomplete/quests/note_comments/NoteCommentItem.kt | 3 ++- .../streetcomplete/screens/main/controls/MapButton.kt | 3 ++- .../streetcomplete/screens/main/controls/PointerPinButton.kt | 3 ++- .../streetcomplete/screens/main/controls/ZoomButtons.kt | 3 ++- .../de/westnordost/streetcomplete/screens/user/edits/Flag.kt | 5 +++-- .../westnordost/streetcomplete/ui/common/VerticalDivider.kt | 4 ++-- .../streetcomplete/ui/common/speech_bubble/SpeechBubble.kt | 3 ++- .../ui/common/speech_bubble/SpeechBubbleNoArrow.kt | 3 ++- .../ui/common/street_side_select/MiniCompass.kt | 3 ++- .../ui/common/street_side_select/StreetSideForm.kt | 3 ++- .../kotlin/de/westnordost/streetcomplete/ui/theme/Color.kt | 3 +++ 13 files changed, 28 insertions(+), 14 deletions(-) diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/teammode/TeamModeWizard.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/teammode/TeamModeWizard.kt index bc0e176363..884c06a404 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/teammode/TeamModeWizard.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/screens/main/teammode/TeamModeWizard.kt @@ -52,6 +52,7 @@ import kotlinx.coroutines.delay import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import androidx.compose.ui.tooling.preview.Preview +import de.westnordost.streetcomplete.ui.theme.divider /** Wizard which enables team mode */ @Composable @@ -200,7 +201,7 @@ private fun SplitQuestsIllustration( } val arrangement = Arrangement.spacedBy((-48 + 64 * padding.value).dp) - val dividerColor = MaterialTheme.colors.onSurface.copy(alpha = 0.12f) + val dividerColor = MaterialTheme.colors.divider val dividerWidth = 4.dp.toPx() Column( modifier = Modifier diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/lanes/LanesForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/lanes/LanesForm.kt index 67c72ead4e..f4246a1cf0 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/lanes/LanesForm.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/lanes/LanesForm.kt @@ -23,6 +23,7 @@ import de.westnordost.streetcomplete.resources.* import de.westnordost.streetcomplete.ui.common.dialogs.WheelPickerDialog import de.westnordost.streetcomplete.ui.common.last_picked.LastPickedChipsRow import de.westnordost.streetcomplete.ui.common.street_side_select.MiniCompass +import de.westnordost.streetcomplete.ui.theme.divider import org.jetbrains.compose.resources.stringResource /** Form to input how many lanes a road has */ @@ -80,7 +81,7 @@ fun LanesForm( modifier = Modifier .padding(horizontal = 8.dp) .align(Alignment.BottomStart), - chipBorder = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f)), + chipBorder = BorderStroke(1.dp, MaterialTheme.colors.divider), ) { laneCount -> LanesButtonContent( laneCount = laneCount, diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/note_comments/NoteCommentItem.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/note_comments/NoteCommentItem.kt index e1d49d04bd..19fc18b128 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/note_comments/NoteCommentItem.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/note_comments/NoteCommentItem.kt @@ -30,6 +30,7 @@ import de.westnordost.streetcomplete.data.osmnotes.NoteComment import de.westnordost.streetcomplete.resources.* import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubble import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubbleArrowDirection +import de.westnordost.streetcomplete.ui.theme.divider import de.westnordost.streetcomplete.ui.util.annotateLinks import de.westnordost.streetcomplete.ui.util.formatAnnotated import de.westnordost.streetcomplete.util.ktx.toLocalDateTime @@ -118,7 +119,7 @@ fun NoteCommentItem( Surface( elevation = 16.dp, shape = RoundedCornerShape(16.dp), - border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f)) + border = BorderStroke(1.dp, MaterialTheme.colors.divider) ) { Text( text = stringResource(actionTextResource) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/MapButton.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/MapButton.kt index 44338fc6d1..1e7a19038e 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/MapButton.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/MapButton.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.unit.dp import de.westnordost.streetcomplete.resources.* import org.jetbrains.compose.resources.painterResource import androidx.compose.ui.tooling.preview.Preview +import de.westnordost.streetcomplete.ui.theme.divider /** Small floating button on top of the map */ @OptIn(ExperimentalMaterialApi::class) @@ -39,7 +40,7 @@ fun MapButton( shape = CircleShape, color = colors.backgroundColor(enabled).value, contentColor = colors.contentColor(enabled).value, - border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f)), + border = BorderStroke(1.dp, MaterialTheme.colors.divider), elevation = 4.dp ) { Box(modifier = Modifier.padding(contentPadding), content = content) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/PointerPinButton.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/PointerPinButton.kt index b7a955cb3a..cf67551556 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/PointerPinButton.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/PointerPinButton.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.unit.dp import de.westnordost.streetcomplete.resources.* import de.westnordost.streetcomplete.ui.ktx.proportionalAbsoluteOffset import de.westnordost.streetcomplete.ui.ktx.proportionalPadding +import de.westnordost.streetcomplete.ui.theme.divider import org.jetbrains.compose.resources.painterResource import kotlin.math.PI import kotlin.math.cos @@ -69,7 +70,7 @@ fun PointerPinButton( shape = pointerPinShape, color = colors.backgroundColor(enabled).value, contentColor = colors.contentColor(enabled).value, - border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f)), + border = BorderStroke(1.dp, MaterialTheme.colors.divider), elevation = 4.dp ) { Box(Modifier diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/ZoomButtons.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/ZoomButtons.kt index 61c9fe0745..7a54f4b400 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/ZoomButtons.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/main/controls/ZoomButtons.kt @@ -20,6 +20,7 @@ import de.westnordost.streetcomplete.ui.common.ZoomInIcon import de.westnordost.streetcomplete.ui.common.ZoomOutIcon import androidx.compose.ui.tooling.preview.Preview import de.westnordost.streetcomplete.ui.ktx.pxToDp +import de.westnordost.streetcomplete.ui.theme.divider /** Combined control for zooming in and out */ @Composable @@ -39,7 +40,7 @@ fun ZoomButtons( shape = CircleShape, color = colors.backgroundColor(enabled).value, contentColor = colors.contentColor(enabled).value, - border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f)), + border = BorderStroke(1.dp, MaterialTheme.colors.divider), elevation = 4.dp ) { Column(Modifier diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/user/edits/Flag.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/user/edits/Flag.kt index e48c20b12e..9ac47f3cb9 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/user/edits/Flag.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/screens/user/edits/Flag.kt @@ -16,6 +16,7 @@ import de.westnordost.streetcomplete.data.flags.FlagAlignment import de.westnordost.streetcomplete.resources.* import de.westnordost.streetcomplete.ui.ktx.innerBorder import de.westnordost.streetcomplete.ui.ktx.pxToDp +import de.westnordost.streetcomplete.ui.theme.divider import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.painterResource @@ -27,7 +28,7 @@ fun Flag( modifier: Modifier = Modifier, ) { val resource = Res.getFlag(countryCode) ?: return - val color = MaterialTheme.colors.onSurface.copy(alpha = 0.12f) + val color = MaterialTheme.colors.divider Image( painter = painterResource(resource), contentDescription = countryCode, @@ -45,7 +46,7 @@ fun CircularFlag( ) { val resource = Res.getFlag(countryCode) ?: return val painter = painterResource(resource) - val color = MaterialTheme.colors.onSurface.copy(alpha = 0.12f) + val color = MaterialTheme.colors.divider Image( painter = painter, diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/VerticalDivider.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/VerticalDivider.kt index b384ee5252..4b38c720c4 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/VerticalDivider.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/VerticalDivider.kt @@ -10,12 +10,13 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.ui.theme.divider /** Same as a Divider, only vertical. (In Material3, this is already available.) */ @Composable fun VerticalDivider( modifier: Modifier = Modifier, - color: Color = MaterialTheme.colors.onSurface.copy(alpha = DividerAlpha), + color: Color = MaterialTheme.colors.divider, thickness: Dp = 1.dp ) { Canvas(modifier.fillMaxHeight().width(thickness)) { @@ -30,4 +31,3 @@ fun VerticalDivider( } } -private const val DividerAlpha = 0.12f diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubble.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubble.kt index 0b263736fa..d885d439da 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubble.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubble.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import de.westnordost.streetcomplete.ui.theme.Dimensions +import de.westnordost.streetcomplete.ui.theme.divider /** Surface in the shape of a speech bubble with a border stroke and a default inner padding by * default. */ @@ -26,7 +27,7 @@ fun SpeechBubble( arrowPlacementBias: Float = 0f, color: Color = MaterialTheme.colors.surface, contentColor: Color = contentColorFor(color), - border: BorderStroke? = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f)), + border: BorderStroke? = BorderStroke(1.dp, MaterialTheme.colors.divider), elevation: Dp = 0.dp, contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 12.dp), content: @Composable () -> Unit, diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubbleNoArrow.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubbleNoArrow.kt index 4fcb42446d..c0f8f62219 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubbleNoArrow.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubbleNoArrow.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import de.westnordost.streetcomplete.ui.theme.Dimensions +import de.westnordost.streetcomplete.ui.theme.divider /** A spech bubble without an arrow, so basically mostly a surface with rounded corners. However, * there are some common defaults, so it makes sense to put it into an own composable. */ @@ -25,7 +26,7 @@ fun SpeechBubbleNoArrow( color: Color = MaterialTheme.colors.surface, contentColor: Color = contentColorFor(color), elevation: Dp = 0.dp, - border: BorderStroke? = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f)), + border: BorderStroke? = BorderStroke(1.dp, MaterialTheme.colors.divider), contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 12.dp), content: @Composable () -> Unit, ) { diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/street_side_select/MiniCompass.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/street_side_select/MiniCompass.kt index 5d2a9c358f..d9aa93a641 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/street_side_select/MiniCompass.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/street_side_select/MiniCompass.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.dp import de.westnordost.streetcomplete.resources.* +import de.westnordost.streetcomplete.ui.theme.divider import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @@ -36,7 +37,7 @@ fun MiniCompass( painter = painterResource(Res.drawable.compass_needle_48), contentDescription = stringResource(Res.string.compass), modifier = Modifier - .border(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f), CircleShape) + .border(1.dp, MaterialTheme.colors.divider, CircleShape) .background(MaterialTheme.colors.surface, CircleShape) .padding(4.dp) .size(24.dp) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/street_side_select/StreetSideForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/street_side_select/StreetSideForm.kt index 3aaa28945e..c67896bc1a 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/street_side_select/StreetSideForm.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/street_side_select/StreetSideForm.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.unit.dp import de.westnordost.streetcomplete.osm.Sides import de.westnordost.streetcomplete.resources.* import de.westnordost.streetcomplete.ui.common.last_picked.LastPickedChipsRow +import de.westnordost.streetcomplete.ui.theme.divider import org.jetbrains.compose.resources.painterResource /** Form to input the something for the left and right side of a street */ @@ -78,7 +79,7 @@ import org.jetbrains.compose.resources.painterResource .padding(8.dp) .align(Alignment.BottomStart) .padding(lastPickedContentPadding), - chipBorder = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f)), + chipBorder = BorderStroke(1.dp, MaterialTheme.colors.divider), chipContentPadding = PaddingValues.Zero, ) { value -> StreetSideIllustration( diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Color.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Color.kt index f760c2864a..cd11ae5ac4 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Color.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/theme/Color.kt @@ -63,6 +63,9 @@ val DarkColors = darkColors( onSecondary = Color.White ) +val Colors.divider @ReadOnlyComposable @Composable get() = + onSurface.copy(alpha = 0.12f) + val Colors.surfaceContainer @ReadOnlyComposable @Composable get() = if (isLight) Color(0xffdddddd) else Color(0xff222222) From 2230076c0ac0bae496e4e4a3421691286c217c92 Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Wed, 15 Apr 2026 16:21:46 +0200 Subject: [PATCH 25/28] use SpeechBubbleNoArrow also for NoteCommentItem --- .../quests/note_comments/NoteCommentItem.kt | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/note_comments/NoteCommentItem.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/note_comments/NoteCommentItem.kt index 19fc18b128..4856107889 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/note_comments/NoteCommentItem.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/note_comments/NoteCommentItem.kt @@ -1,6 +1,5 @@ package de.westnordost.streetcomplete.quests.note_comments -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -8,11 +7,9 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.ContentAlpha import androidx.compose.material.Divider -import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -30,7 +27,7 @@ import de.westnordost.streetcomplete.data.osmnotes.NoteComment import de.westnordost.streetcomplete.resources.* import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubble import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubbleArrowDirection -import de.westnordost.streetcomplete.ui.theme.divider +import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubbleNoArrow import de.westnordost.streetcomplete.ui.util.annotateLinks import de.westnordost.streetcomplete.ui.util.formatAnnotated import de.westnordost.streetcomplete.util.ktx.toLocalDateTime @@ -116,11 +113,7 @@ fun NoteCommentItem( // the action (if anything else than a normal comment) is shown in a separate bubble, just // like for example in github ("comment and close") if (actionTextResource != null) { - Surface( - elevation = 16.dp, - shape = RoundedCornerShape(16.dp), - border = BorderStroke(1.dp, MaterialTheme.colors.divider) - ) { + SpeechBubbleNoArrow(elevation = elevation) { Text( text = stringResource(actionTextResource) .formatAnnotated(annotatedUserName, dateText), From fa2a4513532188d37dbf98fa06d5b8a2e3657f5f Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Wed, 15 Apr 2026 16:42:54 +0200 Subject: [PATCH 26/28] add some comments --- .../ui/common/overlay/OverlayForm.kt | 14 ++++---------- .../streetcomplete/ui/common/quest/QuestForm.kt | 4 ++-- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/overlay/OverlayForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/overlay/OverlayForm.kt index 72b4bbea60..d9909bed07 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/overlay/OverlayForm.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/overlay/OverlayForm.kt @@ -1,6 +1,5 @@ package de.westnordost.streetcomplete.ui.common.overlay -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope @@ -10,7 +9,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.sizeIn -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ContentAlpha import androidx.compose.material.Divider import androidx.compose.material.DropdownMenu @@ -19,9 +17,7 @@ import androidx.compose.material.LocalContentAlpha import androidx.compose.material.LocalTextStyle import androidx.compose.material.MaterialTheme import androidx.compose.material.ProvideTextStyle -import androidx.compose.material.Surface import androidx.compose.material.Text -import androidx.compose.material.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue @@ -36,8 +32,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.datasource.LoremIpsum import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import de.westnordost.streetcomplete.resources.Res -import de.westnordost.streetcomplete.resources.quest_generic_otherAnswers2 import de.westnordost.streetcomplete.ui.common.DropdownMenuItem import de.westnordost.streetcomplete.ui.common.FloatingOkButton import de.westnordost.streetcomplete.ui.common.MoreIcon @@ -46,8 +40,6 @@ import de.westnordost.streetcomplete.ui.common.quest.Answers import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubbleNoArrow import de.westnordost.streetcomplete.ui.theme.Dimensions import de.westnordost.streetcomplete.ui.theme.titleMedium -import de.westnordost.streetcomplete.ui.theme.titleSmall -import org.jetbrains.compose.resources.stringResource /** A generic overlay form containing the center-aligned [content], padded with [contentPadding]. * Above it, an optional bubble with a [label] (in which the element is usually named). @@ -94,7 +86,7 @@ fun OverlayForm( } } - OverlayAnswerBubble( + OverlayContentBubble( modifier = Modifier.fillMaxWidth(), elevation = elevation, otherAnswers = otherAnswers.answers, @@ -114,8 +106,9 @@ fun OverlayForm( } } +/** Speech bubble for the overlay form content answer, i.e. content and more-button */ @Composable -private fun OverlayAnswerBubble( +private fun OverlayContentBubble( modifier: Modifier = Modifier, elevation: Dp = 0.dp, otherAnswers: List = emptyList(), @@ -143,6 +136,7 @@ private fun OverlayAnswerBubble( } } +/** …-button that opens a dropdown with the provided [answers] */ @Composable private fun MoreButton( answers: List, diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt index 9b498cd2a4..f615ead8d9 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestForm.kt @@ -66,8 +66,8 @@ data class Confirm( /** A generic quest form, with a [title], [subtitle], [hintText] and [hintImages] in the * header speech bubble, then an optional [note] by another mapper shown below as another speech * bubble, then finally the speech bubble containing the center-aligned [content] padded with a - * [contentPadding] (if there is any content) and below a row of text buttons showing different - * [answers] (defined from start to end). + * [contentPadding] (if there is any content) and below *either* a row of text buttons showing + * different [answers] (defined from start to end) *or* an OK confirmation button. * * At the very start of the text button row, there's a text button labeled "Uh…" that, when tapped, * opens a dropdown menu containing [otherAnswers] (defined from start to bottom). */ From d6ea645cbe414653929d956263ef16df3deaf781 Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Wed, 15 Apr 2026 17:23:51 +0200 Subject: [PATCH 27/28] apply review suggestions --- .../streetcomplete/ui/common/FloatingActionButton.kt | 2 +- .../streetcomplete/ui/common/quest/QuestAnswerBubble.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/FloatingActionButton.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/FloatingActionButton.kt index 95ee46285b..3d021f5bb7 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/FloatingActionButton.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/FloatingActionButton.kt @@ -26,7 +26,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp /** Same as the normal FloatingActionButton, but the FloatingActionButton from Material2 doesn't - * have an `enabled` parameter … */ + * have an [enabled] parameter … */ @OptIn(ExperimentalMaterialApi::class) @Composable fun FloatingActionButton( diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerBubble.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerBubble.kt index 34d4524ceb..06b776e476 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerBubble.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/QuestAnswerBubble.kt @@ -19,7 +19,7 @@ import androidx.compose.ui.unit.dp import de.westnordost.streetcomplete.ui.common.speech_bubble.SpeechBubbleNoArrow import de.westnordost.streetcomplete.ui.ktx.fadingVerticalScrollEdges -/** Speech bubble for the quest answer, i.e. content and/or button bar answers */ +/** Speech bubble for the quest answer, i.e. [content] and/or button bar answers */ @Composable fun QuestAnswerBubble( modifier: Modifier = Modifier, From 2eff3b62671b3a89504634c6bc1136f6dfd41963 Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Wed, 15 Apr 2026 22:24:19 +0200 Subject: [PATCH 28/28] correct comment --- .../streetcomplete/ui/common/speech_bubble/SpeechBubble.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubble.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubble.kt index d885d439da..9f865436ff 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubble.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/speech_bubble/SpeechBubble.kt @@ -16,8 +16,7 @@ import androidx.compose.ui.unit.dp import de.westnordost.streetcomplete.ui.theme.Dimensions import de.westnordost.streetcomplete.ui.theme.divider -/** Surface in the shape of a speech bubble with a border stroke and a default inner padding by - * default. */ +/** Surface in the shape of a speech bubble with a default border stroke and inner padding. */ @Composable fun SpeechBubble( modifier: Modifier = Modifier,