Open igordmn opened 3 years ago
Hopefully this is work in progress and being planned for release 1.0.0. Since there hasn't been any update on this tracker, I'd love to hear your thoughts on the following proposal.
From the application perspective, I'd love to see a Compose-friendly way to wrap Locale.getDefault()
and Locale.setDefault()
to enable the following:
ResourceBundle
created with that wrapped / observable Locale
This is something that I've been playing in Aurora in the last week or so, at different levels in the stack. The first is all application-level. Somewhere in your application
block you would have:
val currLocale = mutableStateOf(Locale.getDefault())
val resourceBundle = derivedStateOf {
ResourceBundle
.getBundle("org.pushingpixels.aurora.demo.Resources", currLocale.value)
}
The first one is a mutable wrapper around Locale.getDefault()
, and the second one is a derived ResourceBundle
. The next step is to wrap each Window
content block to set the layout direction:
CompositionLocalProvider(
LocalLayoutDirection provides
if (ComponentOrientation.getOrientation(currLocale.value).isLeftToRight)
LayoutDirection.Ltr else LayoutDirection.Rtl,
) {
// "real" content
}
Finally, you pass your ResourceBundle
into every composable that needs to display strings for the UI (or maybe you create a new derivedStateOf
instead of passing it around), and you pass locale: MutableState<Locale>
into every composable that needs to either read or write the locale value. Then, updating the app-level locale can look like this:
locale.value = Locale("en", "US")
Locale.setDefault(locale.value)
window.applyComponentOrientation(ComponentOrientation.getOrientation(locale.value))
The first line updates the observable mutable wrapper, the second updates the Swing-level locale, and the third optional one updates the embedded Swing content in this window, or all the other ones.
This is a bit cumbersome since it would require copy-pasting these blocks everywhere you need access to read and / or write the current Locale
value.
Going to post this comment and then do the second part.
The second part is a proposal to fold this common logic into Application.desktop.kt
.
First, add var applicationLocale: Locale
to the ApplicationScope
interface.
In awaitApplication
the implementation of that interface becomes
val currLocale = mutableStateOf(Locale.getDefault())
val applicationScope = object : ApplicationScope {
override var applicationLocale: Locale
get() = currLocale.value
set(value) {
Locale.setDefault(value)
currLocale.value = value
}
override fun exitApplication() {
isOpen = false
}
}
See how the setter also updates Locale.setDefault
so that it can be read by embedded Swing components if needed
Finally, in the composition.setContent
block, add the same definition of LocalLayoutDirection
in addition to LocalDensity
:
LocalLayoutDirection provides
if (ComponentOrientation.getOrientation(currLocale.value).isLeftToRight)
LayoutDirection.Ltr else LayoutDirection.Rtl
With this second proposal, applicationLocale
becomes available to read and write anywhere for ApplicationScope
blocks, and pass it around to other blocks with different scopes if necessary. It highlights that Locale
is not per-window, but rather a global language choice for the entire application. And it also exposes it as a mutable state, so that ResourceBundle
objects can be derived from it, with the entire window composable hierarchy getting updated for every single window when the current locale is updated
A side note - I can almost get there in Aurora, except for a couple of things.
GlobalSnapshotManager.ensureStarted()
is internal and not accessible outside of the androidx.compose.ui.platform
package. GlobalDensity
is in the similar bucket, but at least its implementation can be copied and expanded as:
LocalDensity provides
Density(
GraphicsEnvironment.getLocalGraphicsEnvironment()
.defaultScreenDevice
.defaultConfiguration.defaultTransform.scaleX.toFloat(),
fontScale = 1f
)
And androidx.compose.ui.configureSwingGlobalsForCompose
is marked as experimental - but it's public, which is nice.
Thanks for the detailed proposal!
Changing/reading the current locale would be a nice feature.
There are multiple things we need to consider:
currentLocale
looks similar to LocalLayoutDirection/LocalDensity. The question, why we don't make something like LocalLocale
too, which will be available in any Composable, and can be overridden for child Composable's.application
's variable. application
is designed in a way, that it shouldn't touch the outer scope (if that is possible). Also, Locale.setDefault
is java-only thing, and isn't needed for Compose, we can just pass Locale down, to the Composable tree.LocalLocale
a commonMain thing.A side note - I can almost get there in Aurora, except for a couple of things.
Are you trying to implement application
with extra features? Maybe wrapping it will help?
fun auroraApplication(content: @Composable AuroraApplicationScope.() -> Unit) {
application {
AuroraApplicationScope(this).content()
}
}
class AuroraApplicationScope(private original: ApplicationScope) : ApplicationScope by ApplicationScope {
...
}
P.S. If reading RTL probably will be in 1.0, designing/implementing currentLocale
/LocalLocale
most certainly will be only in post-1.0 versions
Thanks Igor. I have a first pass for this in Aurora at https://github.com/kirill-grouchnikov/aurora/commit/12adb644a27ee0531454d1110141418ad2909e54
First, there's the "extended" application scope:
interface AuroraLocaleScope {
var applicationLocale: Locale
}
class AuroraApplicationScope(private val original: ApplicationScope, private val currLocale: MutableState<Locale>) :
ApplicationScope, AuroraLocaleScope {
override var applicationLocale: Locale
get() = currLocale.value
set(value) {
Locale.setDefault(value)
currLocale.value = value
}
override fun exitApplication() {
original.exitApplication()
}
}
fun auroraApplication(content: @Composable AuroraApplicationScope.() -> Unit) {
application {
val currLocale = mutableStateOf(Locale.getDefault())
CompositionLocalProvider(
LocalLayoutDirection provides
if (ComponentOrientation.getOrientation(currLocale.value).isLeftToRight)
LayoutDirection.Ltr else LayoutDirection.Rtl
) {
AuroraApplicationScope(this, currLocale).content()
}
}
}
Then, if the app is using the auroraApplication
instead of application
, it can get access to applicationLocale
to create a ResourceBundle
as derivedStateOf
. It can also pass ::applicationLocale
property reference down to a composable that can do localeProperty.setter.call(Locale("iw", "IL"))
to switch the locale, and have the entire UI recomposed with strings from the updated resource bundle.
This is not the cleanest, I think. I'll go back to your points in the next day or so to continue this discussion.
Thanks for the hints to providing LocalLayoutDirection
in order to switch to the RTL direction in Compose. As a reply to @igordmn's
Besides layout, we need to be sure that any RTL text looks okay.
I may link to a little experiment I did at Briar Desktop. In general the direction switching works, however, a lot of things are broken, like padding and overlapping of elements that otherwise look fine in LTR mode. Also, most applications do the following with mixed Latin and Arabic texts: if a line starts with a Latin character, the text is aligned to the left and starts on the left. If a line starts with an Arabic character, it's the same just on the right.
Here's the full report on the experiment together with a lot of images:
https://code.briarproject.org/briar/briar-desktop/-/issues/293#note_63432
Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.
Compose 0.5.0-build235
Core Compose already supports right-to-left languages and changes layout accordingly.
All we need is to get
LayoutDirection
at the window/component level and pass it down to all internal classes (DesktopOwner, SkijaLayer, Window.setIcon, Tray).We can use this property:
get it from the component in ComposeLayer.setContent:
component.locale.layoutDirection
and pass it toDesktopOwner
Besides layout, we need to be sure that any RTL text looks okay.