diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/Main.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/Main.kt index 7435413515bbe7ad25a7e9b8ef055126abb0d895..f0eb6e3fe2b496e2b72d9e6705be0a517ed228b9 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/Main.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/Main.kt @@ -115,6 +115,7 @@ private class Main : CliktCommand( DaggerBriarDesktopApp.builder().desktopCoreModule( DesktopCoreModule(dataDir, socksPort, controlPort) ).build() + // We need to load the eager singletons directly after making the // dependency graphs BrambleCoreEagerSingletons.Helper.injectEagerSingletons(app) diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt index bd5e773b4ff4f47395c732417e232dc34b5ec0e6..632d99d22115b9544d6cfdfde2bfc722a8347282 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt @@ -56,6 +56,7 @@ import androidx.compose.material.icons.filled.SouthWest import androidx.compose.material.icons.filled.Warning import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment @@ -67,6 +68,7 @@ import androidx.compose.ui.input.pointer.PointerIconDefaults import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription @@ -104,8 +106,8 @@ import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nF import org.briarproject.briar.desktop.utils.PreviewUtils import org.briarproject.briar.desktop.utils.PreviewUtils.preview +import org.briarproject.briar.desktop.utils.UiUtils.DensityDimension import org.briarproject.briar.desktop.viewmodel.viewModel -import java.awt.Dimension @Suppress("HardCodedStringLiteral") const val link = "briar://ady23gvb2r76afe5zhxh5kvnh4b22zrcnxibn63tfknrdcwrw7zrs" @@ -196,6 +198,7 @@ fun AddContactDialog( if (!visible) { return } + val density = LocalDensity.current Dialog( title = i18n("contact.add.title_dialog"), onCloseRequest = onClose, @@ -204,67 +207,70 @@ fun AddContactDialog( size = DpSize(width = 560.dp, height = 520.dp), ), ) { - window.minimumSize = Dimension(360, 512) - val clipboardManager = LocalClipboardManager.current - val scaffoldState = rememberScaffoldState() - val coroutineScope = rememberCoroutineScope() - val aliasFocusRequester = remember { FocusRequester() } - Surface { - Scaffold( - modifier = Modifier.padding(horizontal = 24.dp).padding(top = 24.dp, bottom = 12.dp), - topBar = { - Box(Modifier.fillMaxWidth()) { - Text( - i18n("contact.add.remote.title"), - style = MaterialTheme.typography.h6, - modifier = Modifier.padding(bottom = 12.dp) - ) - } - }, - scaffoldState = scaffoldState, - content = { - Column(Modifier.fillMaxSize()) { - if (error != null) { - AddContactErrorDialog(error, onErrorDialogDismissed) + CompositionLocalProvider(LocalDensity provides density) { + window.minimumSize = DensityDimension(360, 512) + window.preferredSize = DensityDimension(520, 512) + val clipboardManager = LocalClipboardManager.current + val scaffoldState = rememberScaffoldState() + val coroutineScope = rememberCoroutineScope() + val aliasFocusRequester = remember { FocusRequester() } + Surface { + Scaffold( + modifier = Modifier.padding(horizontal = 24.dp).padding(top = 24.dp, bottom = 12.dp), + topBar = { + Box(Modifier.fillMaxWidth()) { + Text( + i18n("contact.add.remote.title"), + style = MaterialTheme.typography.h6, + modifier = Modifier.padding(bottom = 12.dp) + ) } - OwnLink( - handshakeLink, - clipboardManager, - coroutineScope, - scaffoldState, - ) - ContactLink( - remoteHandshakeLink, - setRemoteHandshakeLink, - clipboardManager, - coroutineScope, - scaffoldState, - aliasFocusRequester, - ) - Alias( - alias, - setAddContactAlias, - aliasFocusRequester, - onSubmitAddContactDialog, - ) - } - }, - bottomBar = { - Box(Modifier.fillMaxWidth()) { - Row(Modifier.align(Alignment.CenterEnd)) { - TextButton( - onClose, - colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colors.error) - ) { - Text(i18n("cancel")) + }, + scaffoldState = scaffoldState, + content = { + Column(Modifier.fillMaxSize()) { + if (error != null) { + AddContactErrorDialog(error, onErrorDialogDismissed) } - Button(onSubmitAddContactDialog, modifier = Modifier.padding(start = 8.dp)) { - Text(i18n("add")) + OwnLink( + handshakeLink, + clipboardManager, + coroutineScope, + scaffoldState, + ) + ContactLink( + remoteHandshakeLink, + setRemoteHandshakeLink, + clipboardManager, + coroutineScope, + scaffoldState, + aliasFocusRequester, + ) + Alias( + alias, + setAddContactAlias, + aliasFocusRequester, + onSubmitAddContactDialog, + ) + } + }, + bottomBar = { + Box(Modifier.fillMaxWidth()) { + Row(Modifier.align(Alignment.CenterEnd)) { + TextButton( + onClose, + colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colors.error) + ) { + Text(i18n("cancel")) + } + Button(onSubmitAddContactDialog, modifier = Modifier.padding(start = 8.dp)) { + Text(i18n("add")) + } } } - } - }, - ) + }, + ) + } } } } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingDetails.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingDetails.kt index 95321ecf0a76ee4e7abf8fa968ca4fb4831824d6..ff075b6b623f66560eb5c32849ba81497b69a5ca 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingDetails.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingDetails.kt @@ -29,18 +29,25 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedButton import androidx.compose.material.OutlinedExposedDropDownMenu +import androidx.compose.material.Slider import androidx.compose.material.Switch import androidx.compose.material.Text import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.FormatSize import androidx.compose.material.icons.filled.Warning import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp @@ -90,6 +97,33 @@ fun SettingDetails(viewModel: SettingsViewModel) { ) } + // TODO: add description + DetailItem( + label = i18n("settings.display.ui_scale.title"), + description = "" + ) { + val uiScale = remember { mutableStateOf(viewModel.selectedUiScale.value) } + + Row( + horizontalArrangement = Arrangement.spacedBy(2.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.width(200.dp) + ) { + Icon(Icons.Default.FormatSize, null, Modifier.scale(0.7f)) + Slider( + value = uiScale.value ?: LocalDensity.current.density, + onValueChange = { uiScale.value = it }, + onValueChangeFinished = { viewModel.selectUiScale(uiScale.value!!) }, + valueRange = 1f..3f, + steps = 3, + // todo: without setting the width explicitly, + // the slider takes up the whole remaining space + modifier = Modifier.width(150.dp) + ) + Icon(Icons.Default.FormatSize, null) + } + } + DetailItem( label = i18n("settings.security.title"), description = i18n("access.settings.click_to_change_password") diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt index 54882686a48455e4a43eef64eb81c870f971dde1..397c971630ba66b8a394548ba8b8c04548932cc9 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt @@ -61,6 +61,9 @@ constructor( private val _selectedLanguage = mutableStateOf(unencryptedSettings.language) val selectedLanguage = _selectedLanguage.asState() + private val _selectedUiScale = mutableStateOf(unencryptedSettings.uiScale) + val selectedUiScale = _selectedUiScale.asState() + private val _changePasswordDialogVisible = mutableStateOf(false) val changePasswordDialogVisible = _changePasswordDialogVisible.asState() @@ -92,6 +95,11 @@ constructor( briarExecutors.onIoThread { unencryptedSettings.language = language } } + fun selectUiScale(uiScale: Float) { + _selectedUiScale.value = uiScale + briarExecutors.onIoThread { unencryptedSettings.uiScale = uiScale } + } + fun showChangePasswordDialog() { _changePasswordDialogVisible.value = true } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/UnencryptedSettings.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/UnencryptedSettings.kt index 30065f167e41ea7dcb17aa97e79f52719f7b30b4..7acbc6ac8482f156cc8d6c7f7945bde876a35673 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/UnencryptedSettings.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/UnencryptedSettings.kt @@ -25,6 +25,7 @@ import java.util.Locale interface UnencryptedSettingsReadOnly { val theme: UnencryptedSettings.Theme val language: UnencryptedSettings.Language + val uiScale: Float? val invalidateScreen: SingleStateEvent<Unit> } @@ -57,6 +58,7 @@ interface UnencryptedSettings : UnencryptedSettingsReadOnly { override var theme: Theme override var language: Language + override var uiScale: Float? override val invalidateScreen: SingleStateEvent<Unit> } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/UnencryptedSettingsImpl.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/UnencryptedSettingsImpl.kt index 9467e89b78fa41807d6164360acd46e5130de56d..2ea6dfb0aa147ca5797a2322588a75e16ccf2352 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/UnencryptedSettingsImpl.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/UnencryptedSettingsImpl.kt @@ -33,6 +33,7 @@ import kotlin.reflect.KProperty const val PREF_THEME = "theme" // NON-NLS const val PREF_LANG = "language" // NON-NLS +const val PREF_UI_SCALE = "uiScale" // NON-NLS class UnencryptedSettingsImpl @Inject internal constructor() : UnencryptedSettings { @@ -40,14 +41,26 @@ class UnencryptedSettingsImpl @Inject internal constructor() : UnencryptedSettin private val LOG = KotlinLogging.logger {} } - // used for unencrypted settings, namely theme and language + // used for unencrypted settings, namely theme, language and UI scale factor private val prefs = Preferences.userNodeForPackage(this::class.java) override val invalidateScreen = SingleStateEvent<Unit>() - override var theme by EnumEntry(PREF_THEME, AUTO, Theme::class.java) + override var theme by EnumEntry(PREF_THEME, AUTO, Theme::class.java, invalidateScreenOnChange = true) - override var language by EnumEntry(PREF_LANG, DEFAULT, Language::class.java, ::updateLocale) + override var language by EnumEntry( + PREF_LANG, + DEFAULT, + Language::class.java, + onChange = ::updateLocale, + invalidateScreenOnChange = true + ) + + override var uiScale by FloatEntry( + PREF_UI_SCALE, + null, + invalidateScreenOnChange = true + ) init { updateLocale(language) @@ -57,22 +70,20 @@ class UnencryptedSettingsImpl @Inject internal constructor() : UnencryptedSettin InternationalizationUtils.locale = language.locale } - private class EnumEntry<T : Enum<*>>( + private open class Entry<T : Any>( private val key: String, private val default: T, - private val enumClass: Class<T>, - private val onChange: (value: T) -> Unit = {} + private val deserialize: (string: String) -> T?, + private val serialize: (value: T) -> String = { it.toString() }, + private val onChange: (value: T) -> Unit = {}, + private val invalidateScreenOnChange: Boolean = false, ) { private lateinit var current: T operator fun getValue(thisRef: UnencryptedSettingsImpl, property: KProperty<*>): T { if (!::current.isInitialized) { - val value = thisRef.prefs.get(key, default.name) - current = enumClass.enumConstants.find { it.name == value } - ?: run { - LOG.e { "Unexpected enum value for ${enumClass.simpleName}: $value" } - default - } + current = deserialize(thisRef.prefs.get(key, serialize(default))) + ?: throw IllegalArgumentException() } return current } @@ -82,10 +93,75 @@ class UnencryptedSettingsImpl @Inject internal constructor() : UnencryptedSettin if (current == value) return current = value - thisRef.prefs.put(key, value.name) + thisRef.prefs.put(key, serialize(value)) thisRef.prefs.flush() // write preferences to disk onChange(value) - thisRef.invalidateScreen.emit(Unit) + if (invalidateScreenOnChange) thisRef.invalidateScreen.emit(Unit) } } + + private open class NullableEntry<T : Any>( + private val key: String, + private val default: T?, + private val deserialize: (string: String?) -> T?, + private val serialize: (value: T?) -> String? = { it.toString() }, + private val onChange: (value: T?) -> Unit = {}, + private val invalidateScreenOnChange: Boolean = false, + ) { + private var read = false + private var current: T? = null + + operator fun getValue(thisRef: UnencryptedSettingsImpl, property: KProperty<*>): T? { + if (!read) { + read = true + current = deserialize(thisRef.prefs.get(key, serialize(default))) + } + return current + } + + @IoExecutor + operator fun setValue(thisRef: UnencryptedSettingsImpl, property: KProperty<*>, value: T?) { + if (current == value) return + + current = value + if (current == default || serialize(value) == null) { + thisRef.prefs.remove(key) + } else { + thisRef.prefs.put(key, serialize(value)) + } + thisRef.prefs.flush() // write preferences to disk + onChange(value) + if (invalidateScreenOnChange) thisRef.invalidateScreen.emit(Unit) + } + } + + private class EnumEntry<T : Enum<*>>( + key: String, + default: T, + private val enumClass: Class<T>, + serialize: (value: T) -> String = { it.toString() }, + deserialize: (string: String) -> T? = { string -> + enumClass.enumConstants.find { + serialize(it) == string + } ?: run { + LOG.e { "Unexpected enum value for ${enumClass.simpleName}: $string" } + default + } + }, + onChange: (value: T) -> Unit = {}, + invalidateScreenOnChange: Boolean = false, + ) : Entry<T>( + key, default, deserialize, serialize, onChange, invalidateScreenOnChange + ) + + private class FloatEntry( + key: String, + default: Float?, + deserialize: (string: String?) -> Float? = { it?.toFloatOrNull() }, + serialize: (value: Float?) -> String? = { it?.toString() }, + onChange: (value: Float?) -> Unit = {}, + invalidateScreenOnChange: Boolean = false, + ) : NullableEntry<Float>( + key, default, deserialize, serialize, onChange, invalidateScreenOnChange + ) } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/theme/Theme.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/theme/Theme.kt index a6d544da2191c7c81b4a1c6f96b844746ad3deed..e5fcb4f58203d5f0b43976e8df522b824b451b36 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/theme/Theme.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/theme/Theme.kt @@ -33,9 +33,11 @@ import androidx.compose.material.lightColors import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.platform.Font +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.sp val Colors.divider: Color get() = if (isLight) Gray300 else Gray800 @@ -53,12 +55,12 @@ val Colors.textSecondary: Color get() = if (isLight) TextSecondaryMaterialLight val Colors.privateMessageDate: Color get() = Gray200 val Colors.buttonTextNegative: Color get() = Red500 val Colors.buttonTextPositive: Color get() = Blue400 -val Colors.warningBackground get() = Red500 -val Colors.warningForeground get() = Color.White -val Colors.sendButton get() = if (isLight) Lime700 else Lime500 -val Colors.passwordStrengthWeak get() = Red500 -val Colors.passwordStrengthMiddle get() = if (isLight) Orange700 else Orange500 -val Colors.passwordStrengthStrong get() = if (isLight) Lime700 else Lime500 +val Colors.warningBackground: Color get() = Red500 +val Colors.warningForeground: Color get() = Color.White +val Colors.sendButton: Color get() = if (isLight) Lime700 else Lime500 +val Colors.passwordStrengthWeak: Color get() = Red500 +val Colors.passwordStrengthMiddle: Color get() = if (isLight) Orange700 else Orange500 +val Colors.passwordStrengthStrong: Color get() = if (isLight) Lime700 else Lime500 val DarkColors = darkColors( primary = Blue500, @@ -119,6 +121,7 @@ val briarTypography = Typography( @Composable fun BriarTheme( isDarkTheme: Boolean = isSystemInDarkTheme(), + density: Float? = null, colors: Colors? = null, content: @Composable () -> Unit, ) = MaterialTheme( @@ -138,6 +141,7 @@ fun BriarTheme( CompositionLocalProvider( LocalTextSelectionColors provides customTextSelectionColors, LocalContextMenuRepresentation provides contextMenuRepresentation, + LocalDensity provides if (density != null) Density(density) else LocalDensity.current, ) { Surface { content() diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt index c9fe9daf2a696da2a7f9469f427a3cc414658813..897ee60ff4de569ae81ebbfe7cc4bbbae3abc969 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt @@ -37,6 +37,7 @@ import androidx.compose.ui.graphics.toAwtImage import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Density import androidx.compose.ui.window.FrameWindowScope import androidx.compose.ui.window.Window import org.briarproject.bramble.api.event.EventBus @@ -62,8 +63,9 @@ import org.briarproject.briar.desktop.ui.Screen.EXPIRED import org.briarproject.briar.desktop.ui.Screen.MAIN import org.briarproject.briar.desktop.ui.Screen.STARTUP import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n +import org.briarproject.briar.desktop.utils.UiUtils.DensityDimension +import org.briarproject.briar.desktop.utils.UiUtils.GlobalDensity import org.briarproject.briar.desktop.viewmodel.ViewModelProvider -import java.awt.Dimension import java.awt.event.WindowEvent import java.awt.event.WindowFocusListener import javax.annotation.concurrent.Immutable @@ -205,7 +207,13 @@ constructor( } } - window.minimumSize = Dimension(800, 600) + CompositionLocalProvider( + LocalDensity provides Density(configuration.uiScale ?: GlobalDensity), + ) { + window.minimumSize = DensityDimension(800, 600) + window.preferredSize = DensityDimension(800, 600) + } + CompositionLocalProvider( LocalWindowScope provides this, LocalWindowFocusState provides focusState, @@ -221,7 +229,7 @@ constructor( val isDarkTheme = configuration.theme == DARK || (configuration.theme == AUTO && isSystemInDarkTheme()) - BriarTheme(isDarkTheme) { + BriarTheme(isDarkTheme, configuration.uiScale) { Column(Modifier.fillMaxSize()) { ExpirationBanner { screenState = EXPIRED; stop() } when (screenState) { diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt index a0ccb0b221a51a854a2498f063881ce033159b23..2c7ba9bda8f875deeb078b84dc6b7a446db00c73 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt @@ -47,6 +47,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import androidx.compose.ui.window.singleWindowApplication import org.briarproject.bramble.api.UniqueId @@ -55,7 +57,9 @@ import org.briarproject.briar.desktop.theme.BriarTheme import org.briarproject.briar.desktop.ui.LocalWindowFocusState import org.briarproject.briar.desktop.ui.LocalWindowScope import org.briarproject.briar.desktop.ui.WindowFocusState +import org.briarproject.briar.desktop.utils.UiUtils.DensityDimension import org.briarproject.briar.desktop.viewmodel.SingleStateEvent +import java.util.prefs.Preferences import kotlin.random.Random object PreviewUtils { @@ -208,6 +212,9 @@ object PreviewUtils { ) { val scope = PreviewScope() + val prefs = Preferences.userNodeForPackage(PreviewUtils::class.java) + val settingsDensity: Float? = prefs.get("previewsUiScale", null)?.toFloat() + singleWindowApplication(title = "Interactive Preview") { val focusState = remember { WindowFocusState() } CompositionLocalProvider( @@ -215,22 +222,27 @@ object PreviewUtils { LocalWindowFocusState provides focusState ) { Column { - Column(Modifier.padding(10.dp)) { - scope.addBooleanParameter("darkTheme", true) - scope.addDropDownParameter( - "language", - DropDownValues(0, UnencryptedSettings.Language.values().toList().map { it.name }) - ) - parameters.forEach { (name, initial) -> - when (initial) { - is String -> scope.addStringParameter(name, initial) - is Boolean -> scope.addBooleanParameter(name, initial) - is Int -> scope.addIntParameter(name, initial) - is Long -> scope.addLongParameter(name, initial) - is Float -> scope.addFloatParameter(name, initial) - is FloatSlider -> scope.addFloatSliderParameter(name, initial) - is DropDownValues -> scope.addDropDownParameter(name, initial) - else -> throw IllegalArgumentException("Type ${initial::class.simpleName} is not supported for previewing.") + val density = settingsDensity ?: LocalDensity.current.density + CompositionLocalProvider(LocalDensity provides Density(density)) { + window.preferredSize = DensityDimension(800, 600) + Column(Modifier.padding(10.dp)) { + scope.addBooleanParameter("darkTheme", true) + scope.addDropDownParameter( + "language", + DropDownValues(0, UnencryptedSettings.Language.values().toList().map { it.name }) + ) + scope.addFloatParameter("density", density) + parameters.forEach { (name, initial) -> + when (initial) { + is String -> scope.addStringParameter(name, initial) + is Boolean -> scope.addBooleanParameter(name, initial) + is Int -> scope.addIntParameter(name, initial) + is Long -> scope.addLongParameter(name, initial) + is Float -> scope.addFloatParameter(name, initial) + is FloatSlider -> scope.addFloatSliderParameter(name, initial) + is DropDownValues -> scope.addDropDownParameter(name, initial) + else -> throw IllegalArgumentException("Type ${initial::class.simpleName} is not supported for previewing.") + } } } } @@ -245,7 +257,10 @@ object PreviewUtils { invalidate.react { return@Column } - BriarTheme(isDarkTheme = scope.getBooleanParameter("darkTheme")) { + BriarTheme( + isDarkTheme = scope.getBooleanParameter("darkTheme"), + density = scope.getFloatParameter("density"), + ) { Box(Modifier.fillMaxSize(1f)) { Column(Modifier.padding(10.dp)) { content(scope) diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/UiUtils.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/UiUtils.kt index 806f90fa17d326abb9ed8be5bbb3ec015ed8e830..800f9fc5ee5e87877182a4367c39d4bdcc8cbd3b 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/UiUtils.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/UiUtils.kt @@ -18,7 +18,43 @@ package org.briarproject.briar.desktop.utils +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp +import java.awt.Dimension +import java.awt.GraphicsConfiguration +import java.awt.GraphicsEnvironment + object UiUtils { fun getContactDisplayName(name: String, alias: String?) = if (alias == null) name else "$alias ($name)" + + // See androidx.compose.ui.window.LayoutConfiguration + internal val GlobalDensity + get() = GraphicsEnvironment.getLocalGraphicsEnvironment() + .defaultScreenDevice + .defaultConfiguration + .density + + // See androidx.compose.ui.window.LayoutConfiguration + private val GraphicsConfiguration.density: Float + get() = defaultTransform.scaleX.toFloat() + + /** + * Compute an AWT Dimension for the given width and height in dp units, taking + * into account the LocalDensity as well as the global density as detected by the + * local graphics environment. + * + * On macOS hidpi devices, the global density is usually something like 2 while on Linux + * it is usually 1 independent of the actual density. The global density is taken into + * account by AWT itself, so we need to remove that factor from the equation, otherwise + * it will be accounted for twice resulting in windows that are bigger than expected. + */ + @Composable + fun DensityDimension(width: Int, height: Int): Dimension { + with(Density(LocalDensity.current.density / GlobalDensity)) { + return Dimension(width.dp.roundToPx(), height.dp.roundToPx()) + } + } } diff --git a/briar-desktop/src/main/resources/strings/BriarDesktop.properties b/briar-desktop/src/main/resources/strings/BriarDesktop.properties index ece143408d036ccb853e52faecb25a32f8b5d8ae..3543d39d7db4abfa2fa89ba58b72cb4c0fb9c232 100644 --- a/briar-desktop/src/main/resources/strings/BriarDesktop.properties +++ b/briar-desktop/src/main/resources/strings/BriarDesktop.properties @@ -323,6 +323,7 @@ settings.display.theme.dark=Dark settings.display.theme.light=Light settings.display.language.title=Language settings.display.language.auto=System default +settings.display.ui_scale.title=UI Scale # Settings Connections settings.connections.title=Connections diff --git a/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/SetPreviewUtilsDensity.kt b/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/SetPreviewUtilsDensity.kt new file mode 100644 index 0000000000000000000000000000000000000000..d335a4ccf4cbeff2e29d05856aba22f4eeee53d6 --- /dev/null +++ b/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/SetPreviewUtilsDensity.kt @@ -0,0 +1,35 @@ +/* + * Briar Desktop + * Copyright (C) 2021-2022 The Briar Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package org.briarproject.briar.desktop + +import org.briarproject.briar.desktop.utils.PreviewUtils +import java.util.prefs.Preferences + +/** + * This executable stores a custom density used for UI previews created using [PreviewUtils] into the user settings. + * On hidpi Linux devices it makes sense to set this to some value once that makes the previews appear big enough from + * then on. + * We're using a different dedicated preference node in order to keep this independent of the settings used in Briar + * itself. + */ +fun main() { + val prefs = Preferences.userNodeForPackage(PreviewUtils::class.java) + prefs.put("previewsUiScale", "2.0") + prefs.flush() +}