add ui scale feature

This commit is contained in:
AmirHossein Abdolmotallebi 2024-12-03 17:53:02 +03:30
parent 9cdb1f3844
commit 6b704cc9ea
14 changed files with 246 additions and 83 deletions

View File

@ -852,7 +852,7 @@ class AppComponent(
val showOpenSourceLibraries = MutableStateFlow(false) val showOpenSourceLibraries = MutableStateFlow(false)
val showTranslators = MutableStateFlow(false) val showTranslators = MutableStateFlow(false)
val theme = appRepository.theme val theme = appRepository.theme
// val uiScale = appRepository.uiScale val uiScale = appRepository.uiScale
} }
interface DownloadDialogManager { interface DownloadDialogManager {

View File

@ -199,17 +199,17 @@ fun proxyConfig(proxyManager: ProxyManager, scope: CoroutineScope): ProxyConfigu
) )
} }
/* fun uiScaleConfig(appSettings: AppSettingsStorage): EnumConfigurable<Float?> {
fun uiScaleConfig(appSettings: AppSettings): EnumConfigurable<Float?> {
return EnumConfigurable( return EnumConfigurable(
title = "Ui Scale", title = Res.string.settings_ui_scale.asStringSource(),
description = "Scale Ui Elements", description = Res.string.settings_ui_scale_description.asStringSource(),
backedBy = appSettings.uiScale, backedBy = appSettings.uiScale,
possibleValues = listOf( possibleValues = listOf(
null, null,
0.5f, 0.8f,
0.75f, 0.9f,
1f, 1f,
1.1f,
1.25f, 1.25f,
1.5f, 1.5f,
1.75f, 1.75f,
@ -218,14 +218,13 @@ fun uiScaleConfig(appSettings: AppSettings): EnumConfigurable<Float?> {
renderMode = EnumConfigurable.RenderMode.Spinner, renderMode = EnumConfigurable.RenderMode.Spinner,
describe = { describe = {
if (it == null) { if (it == null) {
"System" Res.string.system.asStringSource()
} else { } else {
"$it x" "$it x".asStringSource()
} }
} }
) )
} }
*/
fun themeConfig( fun themeConfig(
themeManager: ThemeManager, themeManager: ThemeManager,
@ -377,7 +376,7 @@ class SettingsComponent(
Appearance -> listOf( Appearance -> listOf(
themeConfig(themeManager, scope), themeConfig(themeManager, scope),
languageConfig(languageManager, scope), languageConfig(languageManager, scope),
// uiScaleConfig(appSettings), uiScaleConfig(appSettings),
autoStartConfig(appSettings), autoStartConfig(appSettings),
mergeTopBarWithTitleBarConfig(appSettings), mergeTopBarWithTitleBarConfig(appSettings),
playSoundNotification(appSettings), playSoundNotification(appSettings),

View File

@ -23,7 +23,7 @@ class AppRepository : KoinComponent {
private val proxyManager: ProxyManager by inject() private val proxyManager: ProxyManager by inject()
val theme = appSettings.theme val theme = appSettings.theme
// val uiScale = appSettings.uiScale val uiScale = appSettings.uiScale
private val downloadSystem : DownloadSystem by inject() private val downloadSystem : DownloadSystem by inject()
private val downloadSettings: DownloadSettings by inject() private val downloadSettings: DownloadSettings by inject()
private val downloadManager: DownloadManager = downloadSystem.downloadManager private val downloadManager: DownloadManager = downloadSystem.downloadManager

View File

@ -4,6 +4,7 @@ import com.abdownloadmanager.desktop.utils.*
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import arrow.optics.Lens import arrow.optics.Lens
import arrow.optics.optics import arrow.optics.optics
import com.abdownloadmanager.desktop.App
import ir.amirab.util.compose.localizationmanager.LanguageStorage import ir.amirab.util.compose.localizationmanager.LanguageStorage
import ir.amirab.util.config.* import ir.amirab.util.config.*
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -15,6 +16,7 @@ import java.io.File
data class AppSettingsModel( data class AppSettingsModel(
val theme: String = "dark", val theme: String = "dark",
val language: String = "en", val language: String = "en",
val uiScale: Float? = null,
val mergeTopBarWithTitleBar: Boolean = false, val mergeTopBarWithTitleBar: Boolean = false,
val threadCount: Int = 8, val threadCount: Int = 8,
val dynamicPartCreation: Boolean = true, val dynamicPartCreation: Boolean = true,
@ -40,6 +42,7 @@ data class AppSettingsModel(
object Keys { object Keys {
val theme = stringKeyOf("theme") val theme = stringKeyOf("theme")
val language = stringKeyOf("language") val language = stringKeyOf("language")
val uiScale = floatKeyOf("uiScale")
val mergeTopBarWithTitleBar = booleanKeyOf("mergeTopBarWithTitleBar") val mergeTopBarWithTitleBar = booleanKeyOf("mergeTopBarWithTitleBar")
val threadCount = intKeyOf("threadCount") val threadCount = intKeyOf("threadCount")
val dynamicPartCreation = booleanKeyOf("dynamicPartCreation") val dynamicPartCreation = booleanKeyOf("dynamicPartCreation")
@ -57,12 +60,12 @@ data class AppSettingsModel(
} }
override fun get(source: MapConfig): AppSettingsModel { override fun get(source: MapConfig): AppSettingsModel {
val default by lazy { AppSettingsModel.default } val default by lazy { AppSettingsModel.default }
return AppSettingsModel( return AppSettingsModel(
theme = source.get(Keys.theme) ?: default.theme, theme = source.get(Keys.theme) ?: default.theme,
language = source.get(Keys.language) ?: default.language, language = source.get(Keys.language) ?: default.language,
uiScale = source.get(Keys.uiScale) ?: default.uiScale,
mergeTopBarWithTitleBar = source.get(Keys.mergeTopBarWithTitleBar) ?: default.mergeTopBarWithTitleBar, mergeTopBarWithTitleBar = source.get(Keys.mergeTopBarWithTitleBar) ?: default.mergeTopBarWithTitleBar,
threadCount = source.get(Keys.threadCount) ?: default.threadCount, threadCount = source.get(Keys.threadCount) ?: default.threadCount,
dynamicPartCreation = source.get(Keys.dynamicPartCreation) ?: default.dynamicPartCreation, dynamicPartCreation = source.get(Keys.dynamicPartCreation) ?: default.dynamicPartCreation,
@ -88,6 +91,7 @@ data class AppSettingsModel(
return source.apply { return source.apply {
put(Keys.theme, focus.theme) put(Keys.theme, focus.theme)
put(Keys.language, focus.language) put(Keys.language, focus.language)
putNullable(Keys.uiScale, focus.uiScale)
put(Keys.mergeTopBarWithTitleBar, focus.mergeTopBarWithTitleBar) put(Keys.mergeTopBarWithTitleBar, focus.mergeTopBarWithTitleBar)
put(Keys.threadCount, focus.threadCount) put(Keys.threadCount, focus.threadCount)
put(Keys.dynamicPartCreation, focus.dynamicPartCreation) put(Keys.dynamicPartCreation, focus.dynamicPartCreation)
@ -107,6 +111,16 @@ data class AppSettingsModel(
} }
} }
private val uiScaleLens: Lens<AppSettingsModel, Float?>
get() = Lens(
get = {
it.uiScale
},
set = { s, f ->
s.copy(uiScale = f)
}
)
class AppSettingsStorage( class AppSettingsStorage(
settings: DataStore<MapConfig>, settings: DataStore<MapConfig>,
) : ) :
@ -114,6 +128,7 @@ class AppSettingsStorage(
LanguageStorage { LanguageStorage {
var theme = from(AppSettingsModel.theme) var theme = from(AppSettingsModel.theme)
override val selectedLanguage = from(AppSettingsModel.language) override val selectedLanguage = from(AppSettingsModel.language)
var uiScale = from(uiScaleLens)
var mergeTopBarWithTitleBar = from(AppSettingsModel.mergeTopBarWithTitleBar) var mergeTopBarWithTitleBar = from(AppSettingsModel.mergeTopBarWithTitleBar)
val threadCount = from(AppSettingsModel.threadCount) val threadCount = from(AppSettingsModel.threadCount)
val dynamicPartCreation = from(AppSettingsModel.dynamicPartCreation) val dynamicPartCreation = from(AppSettingsModel.dynamicPartCreation)

View File

@ -60,7 +60,7 @@ object Ui : KoinComponent {
ProvideNotificationManager { ProvideNotificationManager {
ABDownloaderTheme( ABDownloaderTheme(
myColors = theme, myColors = theme,
// uiScale = appComponent.uiScale.collectAsState().value uiScale = appComponent.uiScale.collectAsState().value
) { ) {
ProvideGlobalExceptionHandler(globalAppExceptionHandler) { ProvideGlobalExceptionHandler(globalAppExceptionHandler) {
val trayState = rememberTrayState() val trayState = rememberTrayState()

View File

@ -5,7 +5,7 @@ import com.abdownloadmanager.utils.compose.WithContentAlpha
import com.abdownloadmanager.utils.compose.WithContentColor import com.abdownloadmanager.utils.compose.WithContentColor
import ir.amirab.util.compose.IconSource import ir.amirab.util.compose.IconSource
import com.abdownloadmanager.desktop.ui.icon.MyIcons import com.abdownloadmanager.desktop.ui.icon.MyIcons
//import com.abdownloadmanager.desktop.ui.theme.LocalUiScale import com.abdownloadmanager.desktop.ui.theme.LocalUiScale
import com.abdownloadmanager.desktop.ui.theme.myColors import com.abdownloadmanager.desktop.ui.theme.myColors
import com.abdownloadmanager.desktop.ui.theme.myTextSizes import com.abdownloadmanager.desktop.ui.theme.myTextSizes
import com.abdownloadmanager.desktop.utils.* import com.abdownloadmanager.desktop.utils.*
@ -25,9 +25,11 @@ import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.FrameWindowScope import androidx.compose.ui.window.FrameWindowScope
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
@ -323,52 +325,63 @@ fun CustomWindow(
else Color.Black else Color.Black
}.toWindowColorType() }.toWindowColorType()
} }
CompositionLocalProvider( UiScaledContent {
LocalWindowController provides windowController, CompositionLocalProvider(
LocalWindowState provides state, LocalWindowController provides windowController,
LocalWindow provides window, LocalWindowState provides state,
) { LocalWindow provides window,
if (preventMinimize) {
PreventMinimize()
}
// a window frame which totally rendered with compose
CustomWindowFrame(
onRequestMinimize = onRequestMinimize,
onRequestClose = onCloseRequest,
onRequestToggleMaximize = onRequestToggleMaximize,
title = title,
titlePosition = titlePosition,
windowIcon = icon,
background = background,
onBackground = myColors.onBackground,
start = start,
end = end,
) { ) {
// val defaultDensity = LocalDensity.current if (preventMinimize) {
// val uiScale = LocalUiScale.current PreventMinimize()
// val density = remember(uiScale) { }
// if (uiScale == null) { // a window frame which totally rendered with compose
// defaultDensity CustomWindowFrame(
// } else { onRequestMinimize = onRequestMinimize,
// Density(uiScale) onRequestClose = onCloseRequest,
// } onRequestToggleMaximize = onRequestToggleMaximize,
// } title = title,
// CompositionLocalProvider( titlePosition = titlePosition,
// LocalDensity provides density windowIcon = icon,
// ) { background = background,
ResponsiveBox { onBackground = myColors.onBackground,
Box(Modifier.clearFocusOnTap()) { start = start,
PopUpContainer { end = end,
content() ) {
ResponsiveBox {
Box(Modifier.clearFocusOnTap()) {
PopUpContainer {
content()
}
} }
} }
} }
// }
} }
} }
} }
} }
/**
* put this in every window because [Window] composable override [LocalDensity]
*/
@Composable
fun UiScaledContent(
defaultDensity: Density = LocalDensity.current,
uiScale: Float? = LocalUiScale.current,
content: @Composable () -> Unit,
) {
val density = remember(uiScale) {
if (uiScale == null) {
defaultDensity
} else {
Density(uiScale)
}
}
CompositionLocalProvider(
LocalDensity provides density,
content,
)
}
@Composable @Composable
private fun PreventMinimize() { private fun PreventMinimize() {
val state = LocalWindowState.current val state = LocalWindowState.current

View File

@ -10,7 +10,7 @@ import java.awt.event.WindowFocusListener
fun BaseOptionDialog( fun BaseOptionDialog(
onCloseRequest: () -> Unit, onCloseRequest: () -> Unit,
state: DialogState = rememberDialogState(), state: DialogState = rememberDialogState(),
resizeable:Boolean=true, resizeable: Boolean = true,
content: @Composable WindowScope.() -> Unit, content: @Composable WindowScope.() -> Unit,
) { ) {
DialogWindow( DialogWindow(
@ -21,7 +21,7 @@ fun BaseOptionDialog(
resizable = resizeable, resizable = resizeable,
onCloseRequest = onCloseRequest, onCloseRequest = onCloseRequest,
) { ) {
val focusListener=remember { val focusListener = remember {
object : WindowFocusListener { object : WindowFocusListener {
override fun windowGainedFocus(e: WindowEvent?) { override fun windowGainedFocus(e: WindowEvent?) {
//do nothing //do nothing
@ -32,16 +32,18 @@ fun BaseOptionDialog(
} }
} }
} }
DisposableEffect(window){ DisposableEffect(window) {
window.addWindowFocusListener(focusListener); window.addWindowFocusListener(focusListener);
window.isAlwaysOnTop=true window.isAlwaysOnTop = true
//we need this to allow click outside //we need this to allow click outside
window.modalityType=java.awt.Dialog.ModalityType.MODELESS window.modalityType = java.awt.Dialog.ModalityType.MODELESS
onDispose{ onDispose {
window.removeWindowFocusListener(focusListener) window.removeWindowFocusListener(focusListener)
} }
} }
// window.subtractInset() // window.subtractInset()
content() UiScaledContent {
content()
}
} }
} }

View File

@ -12,12 +12,15 @@ import androidx.compose.foundation.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Popup import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties import androidx.compose.ui.window.PopupProperties
import androidx.compose.ui.window.rememberPopupPositionProviderAtPosition import androidx.compose.ui.window.rememberPopupPositionProviderAtPosition
import com.abdownloadmanager.desktop.ui.customwindow.UiScaledContent
import ir.amirab.util.compose.asStringSource import ir.amirab.util.compose.asStringSource
/* /*
@ -101,12 +104,14 @@ private val textSizes = TextSizes(
@Composable @Composable
fun ABDownloaderTheme( fun ABDownloaderTheme(
myColors: MyColors, myColors: MyColors,
// uiScale: Float? = null, uiScale: Float? = null,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val systemDensity = LocalDensity.current
CompositionLocalProvider( CompositionLocalProvider(
LocalMyColors provides AnimatedColors(myColors, tween(500)), LocalMyColors provides AnimatedColors(myColors, tween(500)),
// LocalUiScale provides uiScale, LocalUiScale provides uiScale,
LocalSystemDensity provides systemDensity,
) { ) {
CompositionLocalProvider( CompositionLocalProvider(
LocalContextMenuRepresentation provides myContextMenuRepresentation(), LocalContextMenuRepresentation provides myContextMenuRepresentation(),
@ -120,7 +125,11 @@ fun ABDownloaderTheme(
fontSize = textSizes.base, fontSize = textSizes.base,
), ),
) { ) {
content() // it is overridden by [Window] Composable,
// but I put this here. maybe I need this outside of window scope!
UiScaledContent {
content()
}
} }
} }
} }
@ -172,7 +181,7 @@ private fun myDefaultScrollBarStyle(): ScrollbarStyle {
thickness = 12.dp, thickness = 12.dp,
shape = RoundedCornerShape(4.dp), shape = RoundedCornerShape(4.dp),
hoverDurationMillis = 300, hoverDurationMillis = 300,
unhoverColor = myColors.onBackground/10, unhoverColor = myColors.onBackground / 10,
hoverColor = myColors.onBackground/30 hoverColor = myColors.onBackground / 30
) )
} }

View File

@ -4,9 +4,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
//val LocalUiScale = staticCompositionLocalOf { null as Float? } val LocalSystemDensity = staticCompositionLocalOf<Density?> { null }
val LocalUiScale = staticCompositionLocalOf<Float?> { null }
val LocalTextSizes = compositionLocalOf<TextSizes> { val LocalTextSizes = compositionLocalOf<TextSizes> {
error("LocalTextSizes not provided") error("LocalTextSizes not provided")

View File

@ -1,4 +1,7 @@
plugins{ plugins{
id(MyPlugins.kotlin) id(MyPlugins.kotlin)
id(MyPlugins.composeDesktop) id(MyPlugins.composeDesktop)
}
dependencies {
implementation(project(":desktop:shared"))
} }

View File

@ -1,4 +1,5 @@
package ir.amirab.util.customwindow package ir.amirab.util.customwindow
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -6,16 +7,17 @@ import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import androidx.compose.ui.window.FrameWindowScope import androidx.compose.ui.window.FrameWindowScope
import ir.amirab.util.customwindow.util.CustomWindowDecorationAccessing import ir.amirab.util.customwindow.util.CustomWindowDecorationAccessing
import ir.amirab.util.desktop.GlobalDensity
import java.awt.Rectangle import java.awt.Rectangle
import java.awt.Shape import java.awt.Shape
import java.awt.Window import java.awt.Window
import java.awt.event.ComponentAdapter import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent import java.awt.event.ComponentEvent
import kotlin.math.roundToInt
object HitSpots { object HitSpots {
const val NO_HIT_SPOT = 0 const val NO_HIT_SPOT = 0
@ -56,7 +58,9 @@ context (FrameWindowScope)
private fun Modifier.onPositionInRect( private fun Modifier.onPositionInRect(
onChange: (Rectangle) -> Unit, onChange: (Rectangle) -> Unit,
) = composed { ) = composed {
val density = LocalDensity.current // we use Global for sake of awt here.
// because we want to calculate height and pass it to awt
val density = GlobalDensity
onGloballyPositioned { onGloballyPositioned {
onChange( onChange(
it.positionInWindow().toDpRectangle( it.positionInWindow().toDpRectangle(
@ -114,8 +118,10 @@ context (FrameWindowScope)
fun ProvideWindowSpotContainer( fun ProvideWindowSpotContainer(
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val density = LocalDensity.current // we use Global for sake of awt here.
val windowSize =getCurrentWindowSize() // because we want to calculate height and pass it to awt
val density = GlobalDensity
val windowSize = getCurrentWindowSize()
val containerSize = with(density) { val containerSize = with(density) {
LocalWindowInfo.current.containerSize.let { LocalWindowInfo.current.containerSize.let {
DpSize(it.width.toDp(), it.height.toDp()) DpSize(it.width.toDp(), it.height.toDp())
@ -142,10 +148,15 @@ fun ProvideWindowSpotContainer(
// //
if (CustomWindowDecorationAccessing.isSupported) { if (CustomWindowDecorationAccessing.isSupported) {
val startOffset = (windowSize - containerSize) / 2 val startOffset = (windowSize - containerSize) / 2
val startWidthOffsetInDp = startOffset.width.value.toInt() val startWidthOffsetInDp = startOffset.width.value.roundToInt()
// val startHeightInDp=delta.height.value.toInt() //it seems no need here // val startHeightOffsetInDp = startOffset.width.value.roundToInt() //it seems no need here
val spots: Map<Shape, Int> = spotsWithInfo.values.associate { (rect, spot) -> val spots: Map<Shape, Int> = spotsWithInfo.values.associate { (rect, spot) ->
Rectangle(rect.x + startWidthOffsetInDp, rect.y, rect.width, rect.height) to spot Rectangle(
rect.x + startWidthOffsetInDp,
rect.y /*+ startHeightOffsetInDp*/,
rect.width,
rect.height
) to spot
} }
placeHitSpots(window, spots, toolbarHeight) placeHitSpots(window, spots, toolbarHeight)
} }

View File

@ -0,0 +1,97 @@
package ir.amirab.util.desktop.screen
import androidx.compose.ui.unit.*
//import androidx.compose.ui.window.WindowPlacement
//import androidx.compose.ui.window.WindowPosition
//import androidx.compose.ui.window.WindowState
import ir.amirab.util.desktop.GlobalDensity
import java.awt.GraphicsEnvironment
fun getGlobalScale(): Float {
val graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment()
val defaultScreenDevice = graphicsEnvironment.defaultScreenDevice
val defaultTransform = defaultScreenDevice.defaultConfiguration.defaultTransform
return defaultTransform.scaleX.toFloat() // Assuming uniform scaling
}
fun Int.applyUiScale(
userUiScale: Float?,
systemUiScale: Float = GlobalDensity.density,
): Int {
if (userUiScale == null) return this
return (this * userUiScale / systemUiScale).toInt()
}
fun Int.unApplyUiScale(
userUiScale: Float?,
systemUiScale: Float = GlobalDensity.density,
): Int {
if (userUiScale == null) return this
return (this * systemUiScale / userUiScale).toInt()
}
fun DpSize.applyUiScale(
userUiScale: Float?,
systemUiScale: Float = GlobalDensity.density,
): DpSize {
if (userUiScale == null) return this
if (this == DpSize.Unspecified) return this
return DpSize(
width = width.let {
if (isSpecified) it.value.toInt().applyUiScale(userUiScale, systemUiScale).dp
else it
},
height = height.let {
if (isSpecified) it.value.toInt().applyUiScale(userUiScale, systemUiScale).dp
else it
},
)
}
fun DpSize.unApplyUiScale(
userUiScale: Float?,
systemUiScale: Float = GlobalDensity.density,
): DpSize {
if (userUiScale == null) return this
if (this == DpSize.Unspecified) return this
return DpSize(
width = width.let {
if (isSpecified) it.value.toInt().unApplyUiScale(userUiScale, systemUiScale).dp
else it
},
height = height.let {
if (isSpecified) it.value.toInt().applyUiScale(userUiScale, systemUiScale).dp
else it
},
)
}
/*
class WindowStateUiScaleAware(
private val delegate: WindowState,
private val uiScale: Float?,
) : WindowState {
override var isMinimized: Boolean
get() = delegate.isMinimized
set(value) {
delegate.isMinimized = value
}
override var placement: WindowPlacement
get() = delegate.placement
set(value) {
delegate.placement = value
}
override var position: WindowPosition
get() = delegate.position
set(value) {
delegate.position = value
}
override var size: DpSize
get() = run {
val s = delegate.size
s.applyUiScale(uiScale)
}
set(value) {
delegate.size = value.unApplyUiScale(uiScale)
}
}*/

View File

@ -5,27 +5,27 @@ import kotlinx.serialization.serializer
context(Json) context(Json)
inline fun <reified T:Any> MapConfig.putEncoded(key:String, value:T){ inline fun <reified T : Any> MapConfig.putEncoded(key: String, value: T) {
putString(key,encodeToString(serializer<T>(),value)) putString(key, encodeToString(serializer<T>(), value))
} }
context(Json) context(Json)
inline fun <reified T> MapConfig.putEncodedNullable(key: ConfigKey.OfNotPrimitiveType<T>, value:T?){ inline fun <reified T> MapConfig.putEncodedNullable(key: ConfigKey.OfNotPrimitiveType<T>, value: T?) {
if (value != null){ if (value != null) {
putString(key.keyName,encodeToString(serializer<T>(),value)) putString(key.keyName, encodeToString(serializer<T>(), value))
}else{ } else {
removeKey(key) removeKey(key)
} }
} }
context(Json) context(Json)
inline fun <reified T:Any>MapConfig.putEncoded(key: ConfigKey.OfNotPrimitiveType<T>, value: T){ inline fun <reified T : Any> MapConfig.putEncoded(key: ConfigKey.OfNotPrimitiveType<T>, value: T) {
putEncoded<T>(key.keyName,value) putEncoded<T>(key.keyName, value)
} }
context(Json) context(Json)
inline fun <reified T> MapConfig.getDecoded(key:String):T?{ inline fun <reified T> MapConfig.getDecoded(key: String): T? {
val str=getString(key)?:return null val str = getString(key) ?: return null
return runCatching<T> { return runCatching<T> {
decodeFromString(str) decodeFromString(str)
} }
@ -34,7 +34,16 @@ inline fun <reified T> MapConfig.getDecoded(key:String):T?{
} }
.getOrNull() .getOrNull()
} }
context(Json) context(Json)
inline fun <reified T> MapConfig.getDecoded(key: ConfigKey.OfNotPrimitiveType<T>):T?{ inline fun <reified T> MapConfig.getDecoded(key: ConfigKey.OfNotPrimitiveType<T>): T? {
return getDecoded(key.keyName) return getDecoded(key.keyName)
}
inline fun <reified T : Any> MapConfig.putNullable(key: ConfigKey.OfPrimitiveType<T>, value: T?) {
if (value == null) {
removeKey(key)
} else {
put(key, value)
}
} }

View File

@ -56,6 +56,7 @@ file=File
tasks=Tasks tasks=Tasks
tools=Tools tools=Tools
help=Help help=Help
system=System
all_finished=All Finished all_finished=All Finished
all_unfinished=All Unfinished all_unfinished=All Unfinished
entire_list=Entire List entire_list=Entire List
@ -194,6 +195,8 @@ settings_use_proxy_describe_system_proxy=System Proxy will be used
settings_use_proxy_describe_manual_proxy="{{value}}" will be used settings_use_proxy_describe_manual_proxy="{{value}}" will be used
settings_theme=Theme settings_theme=Theme
settings_theme_description=Select a theme for the App settings_theme_description=Select a theme for the App
settings_ui_scale=UI Scale
settings_ui_scale_description=Adjust the scale of the user interface to make elements larger or smaller
settings_language=Language settings_language=Language
settings_compact_top_bar=Compact Top Bar settings_compact_top_bar=Compact Top Bar
settings_compact_top_bar_description=Merge top bar with title bar when the main window has enough width settings_compact_top_bar_description=Merge top bar with title bar when the main window has enough width