~whereswaldon/gio-x

0252dbbe449f43310beb5039d5a8880ff705ea0c — Inkeliz 2 months ago 6db7626
add: pref package

Signed-off-by: Inkeliz <inkeliz@inkeliz.com>
M README.md => README.md +1 -0
@@ 14,6 14,7 @@ This table describes the current status of each package in `gioui.org/x`:
| haptic      | Haptic feedback for mobile devices     | no                 | yes                    | unstable      |
| notify      | Background notifications               | no                 | yes                    | unstable      |
| outlay      | Extra layouts                          | yes                | no                     | unstable      |
| pref        | Query user/device preferences          | no                 | yes                    | unstable      |
| profiling   | Gio render performance recording tools | uncertain          | no                     | unstable      |
| scroll      | Scrollbar widget for Gio               | yes                | no                     | unstable      |


A pref/README.md => pref/README.md +65 -0
@@ 0,0 1,65 @@
# pref [![Go Reference](https://pkg.go.dev/badge/gioui.org/x/pref.svg)](https://pkg.go.dev/gioui.org/x/pref)

-------------

Get the user preferences for your Gio app.

## What can it be used for?

The [Theme](https://pkg.go.dev/gioui.org/x/pref/theme) package provides `IsDarkMode`, which can be used to
change your palette in order to honor the user's preferences.

    // Check the preference:
    isDark, _ := theme.IsDarkMode() 
    
    // Change the Palette based on the preference:
	var palette material.Palette
	if isDark {
		palette.Bg = color.NRGBA{A: 255}                         // Black Background
		palette.Fg = color.NRGBA{R: 255, G: 255, B: 255, A: 255} // White Foreground
	} else {
		palette.Bg = color.NRGBA{R: 255, G: 255, B: 255, A: 255} // White Background 
		palette.Fg = color.NRGBA{A: 255}                         // Black Foreground
	}

The [Locale](https://pkg.go.dev/gioui.org/x/pref/locale) makes possible to match the user language preference, that
is important for multi-language apps. So, let your app speak the user's native language.

	// Your dictionary (in that case using x/text/catalog):
	cat := catalog.NewBuilder()
	cat.SetString(language.English, "Hello World", "Hello World")
	cat.SetString(language.Portuguese, "Hello World", "Olá Mundo")
	cat.SetString(language.Spanish, "Hello World", "Hola Mundo")
	cat.SetString(language.Czech, "Hello World", "Ahoj světe")
	cat.SetString(language.French, "Hello World", "Bonjour le monde")

	// Get the user preferences:
	userLanguage, _ := locale.Language()

	// Get the best match based on the preferred language:
	userLanguage, _, confidence := cat.Matcher().Match(userLanguage)
	if confidence <= language.Low {
		userLanguage = language.English // Switch to the default language, due to low confidence.
	}

	// Creates the printer with the user language:
	printer := message.NewPrinter(userLanguage, message.Catalog(cat))

	// Display the text based on the language:
	widget.Label{}.Layout(gtx,
		yourTheme.Shaper,
		text.Font{},
		unit.Dp(12),
		printer.Sprintf("Hello World"),
	)

## Status

Most of the features is supported across Android 6+, JS and Windows 10. It will return ErrAvailableAPI for any other
platform that isn't supported.

| Package      | OS | 
| ----------- | ----------- | 
| [Locale](https://pkg.go.dev/gioui.org/x/pref/locale)     |  Android 6+  <br> JS <br> Linux <br> Windows Vista+ | 
| [Theme](https://pkg.go.dev/gioui.org/x/pref/theme)   |  Android 4+ <br> JS <br> Windows 10+ | 
| [Battery](https://pkg.go.dev/gioui.org/x/pref/battery)   |  Android 6+ <br> JS (Chrome) <br> Windows Vista+| 

A pref/battery/battery.go => pref/battery/battery.go +36 -0
@@ 0,0 1,36 @@
// Package battery provides functions to get the current status of the batteries.
//
// That is useful to change app behavior based on the battery level. You can reduce animations
// or requests when the battery Level is low or if the user IsSaving battery.
package battery

import (
	"errors"
)

var (
	// ErrNotAvailableAPI indicates that the current device/OS doesn't support such function.
	ErrNotAvailableAPI = errors.New("pref: not available api")

	// ErrNoSystemBattery indicates that the current device doesn't use batteries.
	//
	// Some APIs (like Android and JS) don't provide a mechanism to determine whether the machine uses batteries or not.
	// In such a case ErrNoSystemBattery will never be returned.
	ErrNoSystemBattery = errors.New("pref: device isn't battery-powered")
)

// Level returns the battery level as percent level, between 0~100.
func Level() (uint8, error) {
	return batteryLevel()
}

// IsSaving returns "true" if the end-user enables the battery saver on the device.
func IsSaving() (bool, error) {
	return isSavingBattery()
}

// IsCharging returns "true" if the device is charging.
// If the device doesn't rely on batteries it will be always true.
func IsCharging() (bool, error) {
	return isCharging()
}

A pref/battery/battery_android.go => pref/battery/battery_android.go +38 -0
@@ 0,0 1,38 @@
package battery

import (
	"gioui.org/app"
	"gioui.org/x/pref/internal/xjni"
	"git.wow.st/gmp/jni"
)

//go:generate javac -source 8 -target 8 -bootclasspath $ANDROID_HOME/platforms/android-29/android.jar -d $TEMP/pref_battery/classes battery_android.java
//go:generate jar cf battery_android.jar -C $TEMP/pref_battery/classes .

var (
	_Lib = "org/gioui/x/pref/battery/battery_android"
)

func batteryLevel() (uint8, error) {
	i, err := xjni.DoInt(_Lib, "batteryLevel", "(Landroid/content/Context;)I", jni.Value(app.AppContext()))
	if err != nil || i < 0 {
		return 100, ErrNotAvailableAPI
	}
	return uint8(i), nil
}

func isSavingBattery() (bool, error) {
	i, err := xjni.DoInt(_Lib, "isSaving", "(Landroid/content/Context;)I", jni.Value(app.AppContext()))
	if err != nil || i < 0 {
		return false, ErrNotAvailableAPI
	}
	return i >= 1, err
}

func isCharging() (bool, error) {
	i, err := xjni.DoInt(_Lib, "isCharging", "(Landroid/content/Context;)I", jni.Value(app.AppContext()))
	if err != nil || i < 0 {
		return false, ErrNotAvailableAPI
	}
	return i >= 1, err
}

A pref/battery/battery_android.jar => pref/battery/battery_android.jar +0 -0
A pref/battery/battery_android.java => pref/battery/battery_android.java +54 -0
@@ 0,0 1,54 @@
package org.gioui.x.pref.battery;

import android.os.Build;
import android.content.Context;
import android.os.BatteryManager;
import android.os.PowerManager;
import android.util.Log;

public class battery_android {
    public static int batteryLevel(Context context) {
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP) {
            return -1;
        }

        BatteryManager bm = (BatteryManager) context.getSystemService(Context.BATTERY_SERVICE);
        if (bm == null) {
            return -1;
        }

        return bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
    }

    public static int isCharging(Context context) {
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP) {
            return -1;
        }

        BatteryManager bm = (BatteryManager) context.getSystemService(Context.BATTERY_SERVICE);
        if (bm == null) {
            return -1;
        }

        if (bm.isCharging()) {
            return 1;
        }
        return 0;
    }

    public static int isSaving(Context context) {
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP) {
            return -1;
        }

        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
        if (pm == null) {
            return -1;
        }

        if (pm.isPowerSaveMode()) {
            return 1;
        }
        return 0;
    }
}
\ No newline at end of file

A pref/battery/battery_js.go => pref/battery/battery_js.go +83 -0
@@ 0,0 1,83 @@
package battery

import (
	"errors"
	"math"
	"syscall/js"
)

var (
	_GetBattery = js.Global().Get("navigator").Get("getBattery")
)

func batteryLevel() (uint8, error) {
	value, err := do("level")
	if err != nil || !value.Truthy() {
		return 100, err
	}

	b := uint8(math.Ceil(value.Float() * 100))
	switch {
	case b > 100:
		return 100, nil
	case b < 0:
		return 0, nil
	default:
		return b, nil
	}
}

func isSavingBattery() (bool, error) {
	return false, ErrNotAvailableAPI
}

func isCharging() (bool, error) {
	value, err := do("charging")
	if err != nil || !value.Truthy() {
		return false, err
	}

	return value.Bool(), nil
}

func do(name string) (js.Value, error) {
	if !_GetBattery.Truthy() {
		return js.Value{}, ErrNotAvailableAPI
	}

	var (
		success, failure js.Func

		value = make(chan js.Value, 1)
		err   = make(chan error, 1)
	)

	success = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		success.Release()
		failure.Release()

		value <- args[0].Get(name)

		return nil
	})

	failure = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		success.Release()
		failure.Release()

		err <- errors.New("failure getting battery")

		return nil
	})

	go func() {
		js.Global().Get("navigator").Call("getBattery").Call("then", success, failure)
	}()

	select {
	case value := <-value:
		return value, nil
	case <-err:
		return js.Value{}, ErrNotAvailableAPI
	}
}

A pref/battery/battery_unsupported.go => pref/battery/battery_unsupported.go +15 -0
@@ 0,0 1,15 @@
//+build !js,!windows,!android

package battery

func batteryLevel() (uint8, error) {
	return 100, ErrNotAvailableAPI
}

func isSavingBattery() (bool, error) {
	return false, ErrNotAvailableAPI
}

func isCharging() (bool, error) {
	return false, ErrNotAvailableAPI
}

A pref/battery/battery_windows.go => pref/battery/battery_windows.go +80 -0
@@ 0,0 1,80 @@
package battery

import (
	"golang.org/x/sys/windows"
	"unsafe"
)

var (
	_Kernel32 = windows.NewLazySystemDLL("kernel32")

	// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getsystempowerstatus
	_GetSystemPowerStatus = _Kernel32.NewProc("GetSystemPowerStatus")

	_BatteryFlagCharging        byte = 8
	_BatteryFlagNoSystemBattery byte = 128
	_BatteryFlagUnknownStatus   byte = 255

	_BatteryLifePercentUnknown byte = 255
)

func batteryLevel() (uint8, error) {
	resp, err := powerStatus()
	if err != nil {
		return 100, ErrNotAvailableAPI
	}

	if resp.BatteryLifePercent == _BatteryLifePercentUnknown || resp.BatteryFlag&_BatteryFlagNoSystemBattery > 0 {
		return 100, ErrNoSystemBattery
	}

	return resp.BatteryLifePercent, nil
}

// isSavingBattery returns "true" if battery saver is enabled on Windows 10.
// For Windows Vista, Windows 7, Windows 8 it wil always "false" without any error. Because and it's indistinguishable
// from a truly-false, see: https://docs.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-system_power_status
func isSavingBattery() (bool, error) {
	resp, err := powerStatus()
	if err != nil {
		return false, ErrNotAvailableAPI
	}

	if resp.BatteryFlag&_BatteryFlagNoSystemBattery > 0 {
		return false, ErrNoSystemBattery
	}

	return resp.SystemStatusFlag == 1, nil
}

func isCharging() (bool, error) {
	resp, err := powerStatus()
	if err != nil || resp.BatteryFlag == _BatteryFlagUnknownStatus {
		return false, ErrNotAvailableAPI
	}

	if resp.BatteryFlag&_BatteryFlagNoSystemBattery > 0 {
		return true, ErrNoSystemBattery
	}

	return resp.BatteryFlag&_BatteryFlagCharging > 0, nil
}

// _SystemPowerStatus follows https://docs.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-system_power_status
type _SystemPowerStatus struct {
	ACLineStatus        byte
	BatteryFlag         byte
	BatteryLifePercent  byte
	SystemStatusFlag    byte
	BatteryLifeTime     int32
	BatteryFullLifeTime int32
}

func powerStatus() (resp _SystemPowerStatus, err error) {
	r, _, err := _GetSystemPowerStatus.Call(uintptr(unsafe.Pointer(&resp)))
	if r == 0 {
		return resp, err
	}

	return resp, nil
}

A pref/go.mod => pref/go.mod +10 -0
@@ 0,0 1,10 @@
module gioui.org/x/pref

go 1.16

require (
	gioui.org v0.0.0-20210410094005-495c69018772
	git.wow.st/gmp/jni v0.0.0-20200827154156-014cd5c7c4c0
	golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57
	golang.org/x/text v0.3.6
)

A pref/go.sum => pref/go.sum +31 -0
@@ 0,0 1,31 @@
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
gioui.org v0.0.0-20210410094005-495c69018772 h1:QpGMsubuP4JVlZj5VvvFfRikbLDN0QJj71oX9ABeaqM=
gioui.org v0.0.0-20210410094005-495c69018772/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
git.wow.st/gmp/jni v0.0.0-20200827154156-014cd5c7c4c0 h1:Ynp3h+TC8k1clvf45D28VFQlmy0bPx8M/MG5bB24Vj8=
git.wow.st/gmp/jni v0.0.0-20200827154156-014cd5c7c4c0/go.mod h1:+axXBRUTIDlCeE73IKeD/os7LoEnTKdkp8/gQOFjqyo=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

A pref/internal/xjni/jni_android.go => pref/internal/xjni/jni_android.go +52 -0
@@ 0,0 1,52 @@
// That package is used to reduce code duplication, when using JNI.
package xjni

import (
	"gioui.org/app"
	"git.wow.st/gmp/jni"
)

// DoInt invokes a static int method in the JVM and returns its results. lib is the path to the
// java class with the method, function is the name of the method, signature is the JNI
// string description of the method signature, and args allows providing parameters to
// the method.
func DoInt(lib string, function string, signature string, args ...jni.Value) (i int, err error) {
	err = jni.Do(jni.JVMFor(app.JavaVM()), func(env jni.Env) error {
		class, err := jni.LoadClass(env, jni.ClassLoaderFor(env, jni.Object(app.AppContext())), lib)
		if err != nil {
			return err
		}

		i, err = jni.CallStaticIntMethod(env, class, jni.GetStaticMethodID(env, class, function, signature), args...)
		if err != nil {
			return err
		}

		return nil
	})

	return i, err
}

// DoString invokes a static String method in the JVM and returns its results. lib is the path to the
// java class with the method, function is the name of the method, signature is the JNI
// string description of the method signature, and args allows providing parameters to
// the method.
func DoString(lib string, function string, signature string, args ...jni.Value) (s string, err error) {
	err = jni.Do(jni.JVMFor(app.JavaVM()), func(env jni.Env) error {
		class, err := jni.LoadClass(env, jni.ClassLoaderFor(env, jni.Object(app.AppContext())), lib)
		if err != nil {
			return err
		}

		o, err := jni.CallStaticObjectMethod(env, class, jni.GetStaticMethodID(env, class, function, signature), args...)
		if err != nil {
			return err
		}

		s = jni.GoString(env, jni.String(o))
		return nil
	})

	return s, err
}

A pref/locale/locale.go => pref/locale/locale.go +30 -0
@@ 0,0 1,30 @@
// Package locale can be used to get the end-user preferred language, useful for multi-language apps.
package locale

import (
	"errors"
	"golang.org/x/text/language"
)

var (
	// ErrNotAvailableAPI indicates that the current device/OS doesn't support such function.
	ErrNotAvailableAPI = errors.New("pref: not available api")

	// ErrUnknownLanguage indicates that the current language is not supported by x/text/language.
	ErrUnknownLanguage = errors.New("pref: unknown language")
)

// Language is the preferred language of the end-user or the language of the operating system.
func Language() (language.Tag, error) {
	l := getLanguage()
	if l == "" {
		return language.Tag{}, ErrNotAvailableAPI
	}

	tag, err := language.Parse(l)
	if err != nil {
		return language.Tag{}, ErrUnknownLanguage
	}

	return tag, nil
}

A pref/locale/locale_android.go => pref/locale/locale_android.go +21 -0
@@ 0,0 1,21 @@
package locale

import (
	"gioui.org/x/pref/internal/xjni"
)

//go:generate javac -source 8 -target 8 -bootclasspath $ANDROID_HOME/platforms/android-29/android.jar -d $TEMP/x_locale/classes locale_android.java
//go:generate jar cf locale_android.jar -C $TEMP/x_locale/classes .

var (
	_Lib = "org/gioui/x/pref/locale/locale_android"
)

func getLanguage() string {
	lang, err := xjni.DoString(_Lib, "getLanguage", "()Ljava/lang/String;")
	if err != nil {
		return ""
	}

	return lang
}

A pref/locale/locale_android.jar => pref/locale/locale_android.jar +0 -0
A pref/locale/locale_android.java => pref/locale/locale_android.java +19 -0
@@ 0,0 1,19 @@
package org.gioui.x.pref.locale;

import android.content.res.Resources;
import android.os.Build;
import java.util.Locale;

public class locale_android {
	public static String getLanguage() {
	    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP) {
	        return "";
	    }

        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
           return Resources.getSystem().getConfiguration().locale.toLanguageTag();
        }

        return Resources.getSystem().getConfiguration().getLocales().get(0).toLanguageTag();
	}
}
\ No newline at end of file

A pref/locale/locale_js.go => pref/locale/locale_js.go +17 -0
@@ 0,0 1,17 @@
package locale

import (
	"syscall/js"
)

var (
	_Navigator = js.Global().Get("navigator")
)

func getLanguage() string {
	if !_Navigator.Truthy() {
		return ""
	}

	return _Navigator.Get("language").String()
}

A pref/locale/locale_linux.go => pref/locale/locale_linux.go +23 -0
@@ 0,0 1,23 @@
//+build !android

package locale

import (
	"os"
	"strings"
)

func getLanguage() string {
	lang := os.Getenv("LANG")
	if lang == "" {
		return ""
	}

	// Strip the ".UTF-8" (or equivalent) from the language.
	langs := strings.Split(lang, ".")
	if len(langs) < 1 {
		return ""
	}

	return langs[0]
}

A pref/locale/locale_unsupported.go => pref/locale/locale_unsupported.go +7 -0
@@ 0,0 1,7 @@
//+build !js,!windows,!android,!linux

package locale

func getLanguage() string {
	return ""
}

A pref/locale/locale_windows.go => pref/locale/locale_windows.go +31 -0
@@ 0,0 1,31 @@
package locale

import (
	"golang.org/x/sys/windows"
	"unsafe"
)

var (
	_Kernel32 = windows.NewLazySystemDLL("kernel32")

	// https://docs.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getuserdefaultlocalename
	_DefaultUserLang = _Kernel32.NewProc("GetUserDefaultLocaleName")

	// https://docs.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getsystemdefaultlocalename
	_DefaultSystemLang = _Kernel32.NewProc("GetSystemDefaultLocaleName")

	// _LocaleNameMaxSize is the "LOCALE_NAME_MAX_LENGTH".
	// https://docs.microsoft.com/en-us/windows/win32/intl/locale-name-constants
	_LocaleNameMaxSize = 85
)

func getLanguage() string {
	lang := make([]uint16, _LocaleNameMaxSize)

	r, _, _ := _DefaultUserLang.Call(uintptr(unsafe.Pointer(&lang[0])), uintptr(_LocaleNameMaxSize))
	if r == 0 {
		_DefaultSystemLang.Call(uintptr(unsafe.Pointer(&lang[0])), uintptr(_LocaleNameMaxSize))
	}

	return windows.UTF16ToString(lang)
}

A pref/theme/theme.go => pref/theme/theme.go +21 -0
@@ 0,0 1,21 @@
// Package theme provides functions to retrieve user preferences related to theme and accessibility.
package theme

import (
	"errors"
)

var (
	// ErrNotAvailableAPI indicates that the current device doesn't support such function.
	ErrNotAvailableAPI = errors.New("pref: not available api")
)

// IsDarkMode returns "true" if the end-user prefers dark-mode theme.
func IsDarkMode() (bool, error) {
	return isDark()
}

// IsReducedMotion returns "true" if the end-user prefers reduced-motion/disabled-animations.
func IsReducedMotion() (bool, error) {
	return isReducedMotion()
}

A pref/theme/theme_android.go => pref/theme/theme_android.go +28 -0
@@ 0,0 1,28 @@
package theme

//go:generate javac -source 8 -target 8 -bootclasspath $ANDROID_HOME/platforms/android-29/android.jar -d $TEMP/pref_theme/classes theme_android.java
//go:generate jar cf theme_android.jar -C $TEMP/pref_theme/classes .

import (
	"gioui.org/x/pref/internal/xjni"
)

var (
	_Lib = "org/gioui/x/pref/theme/theme_android"
)

func isDark() (bool, error) {
	i, err := xjni.DoInt(_Lib, "isDark", "()I")
	if err != nil || i < 0 {
		return false, ErrNotAvailableAPI
	}
	return i >= 1, nil
}

func isReducedMotion() (bool, error) {
	i, err := xjni.DoInt(_Lib, "isReducedMotion", "()I")
	if err != nil || i < 0 {
		return false, ErrNotAvailableAPI
	}
	return i >= 1, nil
}

A pref/theme/theme_android.jar => pref/theme/theme_android.jar +0 -0
A pref/theme/theme_android.java => pref/theme/theme_android.java +33 -0
@@ 0,0 1,33 @@
package org.gioui.x.pref.theme;

import android.content.res.Resources;
import android.content.res.Configuration;
import android.provider.Settings.Global;
import android.os.Build;

public class theme_android {
	public static int isDark() {
        Resources res = Resources.getSystem();
        if (res == null) {
            return -1;
        }

        Configuration config = res.getConfiguration();
        if (config == null) {
            return -1;
        }

        if ((config.uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES) {
           return 1;
        }

        return 0;
	}

	public static int isReducedMotion() {
	    if (Global.TRANSITION_ANIMATION_SCALE == "0") {
	        return 1;
	    }
	    return 0;
	}
}
\ No newline at end of file

A pref/theme/theme_js.go => pref/theme/theme_js.go +25 -0
@@ 0,0 1,25 @@
package theme

import (
	"syscall/js"
)

var (
	_MatchMedia = js.Global().Get("matchMedia")
)

func isDark() (bool, error) {
	return do("(prefers-color-scheme: dark)")
}

func isReducedMotion() (bool, error) {
	return do("(prefers-reduced-motion: reduce)")
}

func do(name string) (bool, error) {
	if !_MatchMedia.Truthy() {
		return false, ErrNotAvailableAPI
	}

	return _MatchMedia.Invoke(name).Get("matches").Bool(), nil
}

A pref/theme/theme_unsupported.go => pref/theme/theme_unsupported.go +11 -0
@@ 0,0 1,11 @@
//+build !js,!windows,!android

package theme

func isDark() (bool, error) {
	return false, ErrNotAvailableAPI
}

func isReducedMotion() (bool, error) {
	return false, ErrNotAvailableAPI
}

A pref/theme/theme_windows.go => pref/theme/theme_windows.go +46 -0
@@ 0,0 1,46 @@
package theme

import (
	"golang.org/x/sys/windows"
	"golang.org/x/sys/windows/registry"
	"unsafe"
)

var (
	_RegistryPersonalize = `SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize`

	_User32 = windows.NewLazySystemDLL("user32")

	// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-systemparametersinfow
	_SystemParameters = _User32.NewProc("SystemParametersInfoW")
)

const (
	// _GetClientAreaAnimation is the_SPI_GETCLIENTAREAANIMATION.
	_GetClientAreaAnimation = 0x1042
)

func isDark() (bool, error) {
	k, err := registry.OpenKey(registry.CURRENT_USER, _RegistryPersonalize, registry.QUERY_VALUE)
	if err != nil {
		return false, ErrNotAvailableAPI
	}
	defer k.Close()

	v, _, err := k.GetIntegerValue("AppsUseLightTheme")
	if err != nil {
		return false, ErrNotAvailableAPI
	}

	return v == 0, nil
}

func isReducedMotion() (bool, error) {
	disabled := true
	r, _, _ := _SystemParameters.Call(uintptr(_GetClientAreaAnimation), 0, uintptr(unsafe.Pointer(&disabled)), 0)
	if r == 0 {
		return false, ErrNotAvailableAPI
	}

	return !disabled, nil
}