Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ class GameMenuControllerLayoutFocusTest {
}

@Test
fun fiveEnglishSegmentLabelsFitOneRowAtCompactWidth() {
fun fiveEnglishSegmentLabelsFitTwoRowsAtCompactWidth() {
composeTestRule.setContent {
val density = LocalDensity.current
CompositionLocalProvider(
Expand All @@ -170,11 +170,22 @@ class GameMenuControllerLayoutFocusTest {
GameMenu.SegmentOption(label, index == 0, Runnable {})
},
onSegmentClick = {},
modifier = Modifier.width(164.dp).height(36.dp)
columnCount = 3,
modifier = Modifier.width(164.dp).height(73.dp)
)
}
}

val bounds = (0 until 5).map { index ->
composeTestRule.onNodeWithTag("inlineSegmentLabel$index", useUnmergedTree = true)
.fetchSemanticsNode()
.boundsInRoot
}
assertEquals(bounds[0].top, bounds[1].top, 0.5f)
assertEquals(bounds[1].top, bounds[2].top, 0.5f)
assertTrue(bounds[3].top > bounds[0].top)
assertEquals(bounds[3].top, bounds[4].top, 0.5f)

repeat(5) { index ->
val layoutResults = mutableListOf<TextLayoutResult>()
val action = composeTestRule.onNodeWithTag(
Expand Down
10 changes: 8 additions & 2 deletions app/src/main/java/com/limelight/gamemenu/GameMenu.kt
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,10 @@ class GameMenu(
val checked: Boolean,
val toggleAction: Runnable? = null
) : InlineControl
data class Segmented(val segments: List<SegmentOption>) : InlineControl
data class Segmented(
val segments: List<SegmentOption>,
val smallScreenColumnCount: Int? = null
) : InlineControl
}

data class SegmentOption(
Expand Down Expand Up @@ -2308,7 +2311,10 @@ class GameMenu(
isShowIcon = true,
isKeepDialog = true,
showChevron = true,
inlineControl = InlineControl.Segmented(buildTouchModeSegments(compactLabels = true))
inlineControl = InlineControl.Segmented(
segments = buildTouchModeSegments(compactLabels = true),
smallScreenColumnCount = 3
)
))

normalOptions.add(MenuOption(
Expand Down
163 changes: 112 additions & 51 deletions app/src/main/java/com/limelight/gamemenu/GameMenuOptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
Expand All @@ -56,6 +57,11 @@ import androidx.compose.ui.unit.sp
import com.limelight.R
import com.limelight.ui.theme.AppShapes

private val SEGMENT_ROW_HEIGHT = 36.dp
private val SEGMENT_ROW_GAP = 1.dp
private val SEGMENT_COLUMN_GAP = 1.dp
internal const val LARGE_SCREEN_MIN_SMALLEST_WIDTH_DP = 600


@Composable
internal fun MenuOptionColumn(
Expand Down Expand Up @@ -99,6 +105,17 @@ private fun MenuOptionRow(
val hapticFeedback = LocalGameMenuHapticFeedback.current
val shape = GameMenuCardShape
val inlineControl = option.inlineControl
val smallestScreenWidthDp = LocalConfiguration.current.smallestScreenWidthDp
val segmentedColumnCount = (inlineControl as? GameMenu.InlineControl.Segmented)?.let {
responsiveSegmentColumnCount(
itemCount = it.segments.size,
smallestScreenWidthDp = smallestScreenWidthDp,
smallScreenColumnCount = it.smallScreenColumnCount
)
}
val segmentRows = segmentedColumnCount?.let { columns ->
segmentedRowCount(inlineControl.segments.size, columns)
} ?: 1
val showChevronAfterTitle = option.showChevron && inlineControl != null
val hasDedicatedToggleAction = inlineControl is GameMenu.InlineControl.Toggle &&
inlineControl.toggleAction != null
Expand Down Expand Up @@ -244,9 +261,13 @@ private fun MenuOptionRow(
InlineSegmentedControl(
segments = inlineControl.segments,
onSegmentClick = onSegmentClick,
columnCount = segmentedColumnCount ?: inlineControl.segments.size,
modifier = Modifier
.weight(1f)
.height(36.dp)
.height(
SEGMENT_ROW_HEIGHT * segmentRows +
SEGMENT_ROW_GAP * (segmentRows - 1)
)
)
}
null -> if (option.showChevron) MenuChevron()
Expand Down Expand Up @@ -332,6 +353,7 @@ internal fun InlineToggle(
internal fun InlineSegmentedControl(
segments: List<GameMenu.SegmentOption>,
onSegmentClick: (GameMenu.SegmentOption) -> Unit,
columnCount: Int = segments.size.coerceAtLeast(1),
modifier: Modifier = Modifier
) {
val hapticFeedback = LocalGameMenuHapticFeedback.current
Expand All @@ -350,66 +372,105 @@ internal fun InlineSegmentedControl(
null
}
}
Row(
val boundedColumnCount = columnCount.coerceIn(1, segments.size.coerceAtLeast(1))
Column(
modifier = modifier
.padding(horizontal = 1.dp)
.selectableGroup(),
horizontalArrangement = Arrangement.spacedBy(1.dp),
verticalAlignment = Alignment.CenterVertically
verticalArrangement = Arrangement.spacedBy(SEGMENT_ROW_GAP)
) {
segments.forEachIndexed { index, segment ->
val targets = segmentedFocusTargets(segments.size, index, segments.size)
val segmentShape = AppShapes.small
val background = if (segment.selected) {
accent.copy(alpha = 0.12f)
} else {
Color.Transparent
}
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.focusRequester(focusRequesters[index])
.segmentedFocusNavigation(targets, focusRequesters)
.clip(segmentShape)
.background(background)
.gamepadFocusOutline(segmentShape)
.selectable(
selected = segment.selected,
role = Role.RadioButton,
onClick = {
hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
onSegmentClick(segment)
}
)
.padding(horizontal = labelHorizontalPadding),
contentAlignment = Alignment.Center
segments.chunked(boundedColumnCount).forEachIndexed { rowIndex, rowSegments ->
Row(
modifier = Modifier.weight(1f),
horizontalArrangement = Arrangement.spacedBy(SEGMENT_COLUMN_GAP),
verticalAlignment = Alignment.CenterVertically
) {
BasicText(
text = compactSegmentLabel(segment.label),
style = TextStyle(
color = if (segment.selected) {
accent
} else {
colorResource(R.color.game_menu_text_secondary)
},
fontSize = 10.sp,
fontWeight = if (segment.selected) {
FontWeight.Medium
} else {
FontWeight.Normal
}
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
autoSize = labelAutoSize,
modifier = Modifier.testTag("inlineSegmentLabel$index")
)
rowSegments.forEachIndexed { columnIndex, segment ->
val index = rowIndex * boundedColumnCount + columnIndex
val targets = segmentedFocusTargets(
segments.size,
index,
boundedColumnCount
)
val segmentShape = AppShapes.small
val background = if (segment.selected) {
accent.copy(alpha = 0.12f)
} else {
Color.Transparent
}
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.focusRequester(focusRequesters[index])
.segmentedFocusNavigation(targets, focusRequesters)
.clip(segmentShape)
.background(background)
.gamepadFocusOutline(segmentShape)
.selectable(
selected = segment.selected,
role = Role.RadioButton,
onClick = {
hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
onSegmentClick(segment)
}
)
.padding(horizontal = labelHorizontalPadding),
contentAlignment = Alignment.Center
) {
BasicText(
text = compactSegmentLabel(segment.label),
style = TextStyle(
color = if (segment.selected) {
accent
} else {
colorResource(R.color.game_menu_text_secondary)
},
fontSize = 10.sp,
fontWeight = if (segment.selected) {
FontWeight.Medium
} else {
FontWeight.Normal
}
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
autoSize = labelAutoSize,
modifier = Modifier.testTag("inlineSegmentLabel$index")
)
}
}
repeat(boundedColumnCount - rowSegments.size) {
Spacer(Modifier.weight(1f))
}
}
}
}
}

internal fun responsiveSegmentColumnCount(
itemCount: Int,
smallestScreenWidthDp: Int,
smallScreenColumnCount: Int?
): Int {
require(itemCount > 0)
val requestedSmallColumns = smallScreenColumnCount?.coerceAtLeast(1)
return if (
requestedSmallColumns != null &&
smallestScreenWidthDp < LARGE_SCREEN_MIN_SMALLEST_WIDTH_DP
) {
minOf(itemCount, requestedSmallColumns)
} else {
itemCount
}
}

internal fun segmentedRowCount(itemCount: Int, columnCount: Int): Int {
require(itemCount > 0)
require(columnCount > 0)
return (itemCount + columnCount - 1) / columnCount
}

internal data class SegmentedFocusTargets(
val left: Int?,
val right: Int?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ class StreamSettings : AppCompatActivity() {
"category_screen_position" -> R.drawable.phc_video_camera
"category_display_behavior" -> R.drawable.phc_perf_resolution
"category_audio_settings" -> R.drawable.phc_audio
"category_microphone_settings" -> R.drawable.ic_mic_gm
"category_gamepad_settings" -> R.drawable.phc_gamepad
"category_input_settings" -> R.drawable.phc_keyboard
"category_onscreen_controls" -> R.drawable.phc_game_controller
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/values-zh-rCN/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@
<string name="title_checkbox_resume_stream">自动恢复串流</string>
<string name="summary_checkbox_resume_stream">当从后台切回Moonlight时,自动恢复串流会话。</string>
<string name="category_host_settings">主机</string>
<string name="title_checkbox_enable_sops">让游戏设置匹配串流规格</string>
<string name="title_checkbox_enable_sops">自动优化主机设置</string>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<string name="summary_checkbox_enable_sops">允许兼容的主机软件根据请求的串流分辨率和质量调整受支持游戏;更改发生在主机上。</string>
<string name="title_checkbox_host_audio">同时在主机播放声音</string>
<string name="summary_checkbox_host_audio">在主机和本设备上同时输出串流音频。</string>
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/values-zh-rTW/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@
<string name="title_checkbox_small_icon_mode">使用小封面</string>
<string name="summary_checkbox_small_icon_mode">使用小圖示以在螢幕上顯示更多應用程式</string>
<string name="category_host_settings">主機</string>
<string name="title_checkbox_enable_sops">讓遊戲設定符合串流規格</string>
<string name="title_checkbox_enable_sops">最佳化遊戲設定</string>
<string name="summary_checkbox_enable_sops">允許相容的主機軟體依照所請求的串流解析度與品質調整支援的遊戲;變更發生在主機上。</string>
<string name="title_checkbox_host_audio">在電腦上播放音訊</string>
<string name="summary_checkbox_host_audio">在電腦和本裝置同時輸出音訊</string>
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,7 @@
<string name="summary_checkbox_resume_stream">Automatically resume the stream when returning to Moonlight from the background</string>

<string name="category_host_settings">Host</string>
<string name="title_checkbox_enable_sops">Match game settings to the stream</string>
<string name="title_checkbox_enable_sops">Optimize game settings</string>
<string name="summary_checkbox_enable_sops">Allow compatible host software to adjust supported games for the requested stream resolution and quality. Changes are made on the host.</string>
<string name="title_checkbox_host_audio">Play audio on PC</string>
<string name="summary_checkbox_host_audio">Play audio from the computer and this device</string>
Expand Down
Loading
Loading