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 showTranslators = MutableStateFlow(false)
val theme = appRepository.theme
// val uiScale = appRepository.uiScale
val uiScale = appRepository.uiScale
}
interface DownloadDialogManager {

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import com.abdownloadmanager.utils.compose.WithContentAlpha
import com.abdownloadmanager.utils.compose.WithContentColor
import ir.amirab.util.compose.IconSource
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.myTextSizes
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.takeOrElse
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.FrameWindowScope
import androidx.compose.ui.window.Window
@ -323,6 +325,7 @@ fun CustomWindow(
else Color.Black
}.toWindowColorType()
}
UiScaledContent {
CompositionLocalProvider(
LocalWindowController provides windowController,
LocalWindowState provides state,
@ -344,18 +347,6 @@ fun CustomWindow(
start = start,
end = end,
) {
// val defaultDensity = LocalDensity.current
// val uiScale = LocalUiScale.current
// val density = remember(uiScale) {
// if (uiScale == null) {
// defaultDensity
// } else {
// Density(uiScale)
// }
// }
// CompositionLocalProvider(
// LocalDensity provides density
// ) {
ResponsiveBox {
Box(Modifier.clearFocusOnTap()) {
PopUpContainer {
@ -363,11 +354,33 @@ fun CustomWindow(
}
}
}
// }
}
}
}
}
}
/**
* 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
private fun PreventMinimize() {

View File

@ -42,6 +42,8 @@ fun BaseOptionDialog(
}
}
// window.subtractInset()
UiScaledContent {
content()
}
}
}

View File

@ -12,12 +12,15 @@ import androidx.compose.foundation.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.*
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.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import androidx.compose.ui.window.rememberPopupPositionProviderAtPosition
import com.abdownloadmanager.desktop.ui.customwindow.UiScaledContent
import ir.amirab.util.compose.asStringSource
/*
@ -101,12 +104,14 @@ private val textSizes = TextSizes(
@Composable
fun ABDownloaderTheme(
myColors: MyColors,
// uiScale: Float? = null,
uiScale: Float? = null,
content: @Composable () -> Unit,
) {
val systemDensity = LocalDensity.current
CompositionLocalProvider(
LocalMyColors provides AnimatedColors(myColors, tween(500)),
// LocalUiScale provides uiScale,
LocalUiScale provides uiScale,
LocalSystemDensity provides systemDensity,
) {
CompositionLocalProvider(
LocalContextMenuRepresentation provides myContextMenuRepresentation(),
@ -120,10 +125,14 @@ fun ABDownloaderTheme(
fontSize = textSizes.base,
),
) {
// it is overridden by [Window] Composable,
// but I put this here. maybe I need this outside of window scope!
UiScaledContent {
content()
}
}
}
}
private class MyContextMenuRepresentation : ContextMenuRepresentation {
@Composable

View File

@ -4,9 +4,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.unit.Density
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> {
error("LocalTextSizes not provided")

View File

@ -2,3 +2,6 @@ plugins{
id(MyPlugins.kotlin)
id(MyPlugins.composeDesktop)
}
dependencies {
implementation(project(":desktop:shared"))
}

View File

@ -1,4 +1,5 @@
package ir.amirab.util.customwindow
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
@ -6,16 +7,17 @@ import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.unit.*
import androidx.compose.ui.window.FrameWindowScope
import ir.amirab.util.customwindow.util.CustomWindowDecorationAccessing
import ir.amirab.util.desktop.GlobalDensity
import java.awt.Rectangle
import java.awt.Shape
import java.awt.Window
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import kotlin.math.roundToInt
object HitSpots {
const val NO_HIT_SPOT = 0
@ -56,7 +58,9 @@ context (FrameWindowScope)
private fun Modifier.onPositionInRect(
onChange: (Rectangle) -> Unit,
) = 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 {
onChange(
it.positionInWindow().toDpRectangle(
@ -114,7 +118,9 @@ context (FrameWindowScope)
fun ProvideWindowSpotContainer(
content: @Composable () -> Unit,
) {
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
val windowSize = getCurrentWindowSize()
val containerSize = with(density) {
LocalWindowInfo.current.containerSize.let {
@ -142,10 +148,15 @@ fun ProvideWindowSpotContainer(
//
if (CustomWindowDecorationAccessing.isSupported) {
val startOffset = (windowSize - containerSize) / 2
val startWidthOffsetInDp = startOffset.width.value.toInt()
// val startHeightInDp=delta.height.value.toInt() //it seems no need here
val startWidthOffsetInDp = startOffset.width.value.roundToInt()
// val startHeightOffsetInDp = startOffset.width.value.roundToInt() //it seems no need here
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)
}

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

@ -34,7 +34,16 @@ inline fun <reified T> MapConfig.getDecoded(key:String):T?{
}
.getOrNull()
}
context(Json)
inline fun <reified T> MapConfig.getDecoded(key: ConfigKey.OfNotPrimitiveType<T>): T? {
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
tools=Tools
help=Help
system=System
all_finished=All Finished
all_unfinished=All Unfinished
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_theme=Theme
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_compact_top_bar=Compact Top Bar
settings_compact_top_bar_description=Merge top bar with title bar when the main window has enough width