wgui: basic i18n support, refactoring: use LayoutState, translation framework (LLM-based generator)
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
/target
|
||||
.gdb_history
|
||||
.vscode
|
||||
.cargo
|
||||
.cargo
|
||||
scripts/translator/node_modules
|
||||
5
Cargo.lock
generated
5
Cargo.lock
generated
@@ -4391,9 +4391,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.140"
|
||||
version = "1.0.141"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -5649,6 +5649,7 @@ dependencies = [
|
||||
"resvg",
|
||||
"roxmltree 0.20.0",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde_json",
|
||||
"slotmap",
|
||||
"smallvec",
|
||||
"taffy",
|
||||
|
||||
37
scripts/translator/bun.lock
Normal file
37
scripts/translator/bun.lock
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "llm_translator",
|
||||
"dependencies": {
|
||||
"ollama": "^0.5.14",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/node": "^22.13.13",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
|
||||
|
||||
"@types/node": ["@types/node@22.17.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"ollama": ["ollama@0.5.16", "", { "dependencies": { "whatwg-fetch": "^3.6.20" } }, "sha512-OEbxxOIUZtdZgOaTPAULo051F5y+Z1vosxEYOoABPnQKeW7i4O8tJNlxCB+xioyoorVqgjkdj+TA1f1Hy2ug/w=="],
|
||||
|
||||
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="],
|
||||
}
|
||||
}
|
||||
13
scripts/translator/description.txt
Normal file
13
scripts/translator/description.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
You are a translator for the VR application which translates strings from English to {TARGET_LANG} language.
|
||||
Info: This program is intended to be used as an utility to easily access your desktop display from within VR.
|
||||
|
||||
Glossary:
|
||||
wlx-overlay-s: The name of this software (also called WlxOverlay-S)
|
||||
WayVR: A Wayland compositor intended to be used in VR
|
||||
WayVR Dashboard: An application (and game) launcher which is displayed in front of the user
|
||||
OpenVR: API made by Valve
|
||||
OpenXR: API made by Khronos
|
||||
OSC: OpenSoundControl
|
||||
|
||||
You will be given the input in the code blocks which needs to be translated to {TARGET_LANG} language. Write only the result in codeblocks, do not explain. Keep any newlines and other important formatting-required identifiers as-is.
|
||||
|
||||
186
scripts/translator/main.ts
Normal file
186
scripts/translator/main.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { exit } from "process";
|
||||
import * as fsp from "fs/promises";
|
||||
import path from "path";
|
||||
import * as fs from "fs";
|
||||
import ollama from 'ollama'
|
||||
|
||||
const model_name = process.env["MODEL"] as string;
|
||||
const template_name = process.env["TEMPLATE"] as string;
|
||||
let lang_path = process.env["LANG_PATH"] as string;
|
||||
|
||||
if (model_name === undefined) {
|
||||
console.log("MODEL not set");
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
if (template_name === undefined) {
|
||||
console.log("TEMPLATE not set");
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
if (lang_path === undefined) {
|
||||
console.log("LANG_PATH not set. Try \"LANG_PATH=../../uidev/assets/lang/ ./run.sh\"");
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
lang_path = path.resolve(__dirname + "/" + lang_path);
|
||||
if (lang_path === undefined || !fs.existsSync(lang_path)) {
|
||||
console.log("Invalid or non-existent LANG_PATH");
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
const current_path = path.resolve(__dirname);
|
||||
const templates_path = path.resolve(__dirname + "/templates");
|
||||
|
||||
async function loop_object(obj: any, initial_str: string, callback: (key: string, value: string) => Promise<void>) {
|
||||
for (var key in obj) {
|
||||
let full_key = initial_str + key;
|
||||
if (typeof obj[key] === "object" && obj[key] !== null) {
|
||||
await loop_object(obj[key], full_key + ".", callback)
|
||||
} else if (obj.hasOwnProperty(key)) {
|
||||
await callback(full_key, obj[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function extract_backticks(str: string) {
|
||||
const regex = /`([^`]+)`/g;
|
||||
return str.match(regex)?.map(match => match.slice(1, -1).trim());
|
||||
}
|
||||
|
||||
function set_i18n_key(obj: any, key: string, value: string | undefined) {
|
||||
const parts = key.split('.');
|
||||
let cur_level = obj;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const part = parts[i]!;
|
||||
if (!cur_level[part]) {
|
||||
cur_level[part] = {};
|
||||
}
|
||||
cur_level = cur_level[part];
|
||||
}
|
||||
cur_level[parts[parts.length - 1]!] = value;
|
||||
}
|
||||
|
||||
function key_exists(obj: any, key: string) {
|
||||
const parts = key.split('.');
|
||||
let level = obj;
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i]!;
|
||||
if (!level || !level[part]) {
|
||||
return false;
|
||||
}
|
||||
level = level[part];
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
interface Example {
|
||||
key: string;
|
||||
en: string;
|
||||
translated: string;
|
||||
}
|
||||
|
||||
interface Template {
|
||||
full_name: string; // "Polish"
|
||||
examples: Example[]
|
||||
}
|
||||
|
||||
function gen_prompt(description: string, template: Template, key: string, english_translation: string) {
|
||||
let num = 1;
|
||||
for (const example of template.examples) {
|
||||
description += "\nExample " + num + ":\n\n";
|
||||
description += "Translate key `" + example.key + "` from English to " + template.full_name + ":\n\n";
|
||||
description += "```\n";
|
||||
description += example.en + "\n";
|
||||
description += "```\n\n";
|
||||
description += "Result:\n\n";
|
||||
description += "```\n";
|
||||
description += example.translated + "\n";
|
||||
description += "```\n";
|
||||
num += 1;
|
||||
}
|
||||
description += "\nEnd of examples.\n\n";
|
||||
description += "Translate key `" + key + "` from English to " + template.full_name + ":\n\n";
|
||||
description += "```\n";
|
||||
description += english_translation + "\n";
|
||||
description += "```\n";
|
||||
return description;
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const template = JSON.parse(await fsp.readFile(templates_path + "/" + template_name + ".json", "utf-8")) as Template;
|
||||
|
||||
let description_txt = await fsp.readFile(current_path + "/description.txt", "utf-8");
|
||||
description_txt = description_txt.replaceAll("{TARGET_LANG}", template.full_name);
|
||||
|
||||
const orig_english_json = JSON.parse(await fsp.readFile(lang_path + "/en.json") as any);
|
||||
|
||||
let orig_translated_json = {};
|
||||
try {
|
||||
orig_translated_json = JSON.parse((await fsp.readFile(lang_path + "/" + template_name + ".json")).toString());
|
||||
}
|
||||
catch (_e) { }
|
||||
|
||||
let llm_translated_json = {};
|
||||
const translated_json_path = lang_path + "/" + template_name + ".json";
|
||||
if (await fsp.exists(translated_json_path)) {
|
||||
llm_translated_json = JSON.parse((await fsp.readFile(translated_json_path)).toString());
|
||||
}
|
||||
|
||||
let human = 0;
|
||||
let llm = 0;
|
||||
|
||||
let total_count = 0;
|
||||
await loop_object(orig_english_json, "", async () => {
|
||||
total_count += 1;
|
||||
});
|
||||
|
||||
await loop_object(llm_translated_json, "", async (key, _) => {
|
||||
if (!key_exists(orig_english_json, key)) {
|
||||
console.log("Removing key", key);
|
||||
set_i18n_key(llm_translated_json, key, undefined);
|
||||
fsp.writeFile(translated_json_path, JSON.stringify(llm_translated_json, undefined, 2));
|
||||
}
|
||||
});
|
||||
|
||||
await loop_object(orig_english_json, "", async (key, english_translation) => {
|
||||
if (key_exists(orig_translated_json, key)) {
|
||||
human += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (key_exists(llm_translated_json, key)) {
|
||||
llm += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Translating", key, "...");
|
||||
llm++;
|
||||
|
||||
const prompt = gen_prompt(description_txt, template, key, english_translation);
|
||||
|
||||
const response = await ollama.chat({
|
||||
model: model_name,
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
})
|
||||
|
||||
const msg = extract_backticks(response.message.content);
|
||||
if (msg === undefined || msg[0] === undefined) {
|
||||
throw new Error("backticks failed. Raw content: " + response.message.content);
|
||||
}
|
||||
|
||||
console.log(" >>>", msg);
|
||||
|
||||
set_i18n_key(llm_translated_json, key, msg[0]);
|
||||
fsp.writeFile(translated_json_path, JSON.stringify(llm_translated_json, undefined, 2));
|
||||
});
|
||||
|
||||
console.log("\"" + template_name + "\" translation finished,", human, "were already human translated,", llm, "llm-translated (" + Math.round((llm / total_count) * 100.0) + "% machine-translated)");
|
||||
}
|
||||
|
||||
run().catch((e) => {
|
||||
console.log("Fatal error:", e);
|
||||
exit(-1);
|
||||
});
|
||||
108
scripts/translator/package-lock.json
generated
Normal file
108
scripts/translator/package-lock.json
generated
Normal file
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"name": "llm_translator",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "llm_translator",
|
||||
"dependencies": {
|
||||
"ollama": "^0.5.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/node": "^22.13.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/bun": {
|
||||
"version": "1.2.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.2.19.tgz",
|
||||
"integrity": "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bun-types": "1.2.19"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.0.tgz",
|
||||
"integrity": "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz",
|
||||
"integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/bun-types": {
|
||||
"version": "1.2.19",
|
||||
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.2.19.tgz",
|
||||
"integrity": "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/ollama": {
|
||||
"version": "0.5.16",
|
||||
"resolved": "https://registry.npmjs.org/ollama/-/ollama-0.5.16.tgz",
|
||||
"integrity": "sha512-OEbxxOIUZtdZgOaTPAULo051F5y+Z1vosxEYOoABPnQKeW7i4O8tJNlxCB+xioyoorVqgjkdj+TA1f1Hy2ug/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-fetch": "^3.6.20"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/whatwg-fetch": {
|
||||
"version": "3.6.20",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
|
||||
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
scripts/translator/package.json
Normal file
16
scripts/translator/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/node": "^22.13.13"
|
||||
},
|
||||
"name": "llm_translator",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"ollama": "^0.5.14"
|
||||
}
|
||||
}
|
||||
12
scripts/translator/run.sh
Executable file
12
scripts/translator/run.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
bun install
|
||||
|
||||
export MODEL="gemma3:12b"
|
||||
|
||||
TEMPLATE="pl" bun main.ts
|
||||
TEMPLATE="de" bun main.ts
|
||||
TEMPLATE="ja" bun main.ts
|
||||
TEMPLATE="es" bun main.ts
|
||||
20
scripts/translator/templates/de.json
Normal file
20
scripts/translator/templates/de.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"full_name": "German",
|
||||
"examples": [
|
||||
{
|
||||
"key": "BAR.OPACITY",
|
||||
"en": "Opacity",
|
||||
"translated": "Undurchsichtigkeit"
|
||||
},
|
||||
{
|
||||
"key": "PANEL.CLOSE",
|
||||
"en": "Close panel",
|
||||
"translated": "Schließen Sie das Panel"
|
||||
},
|
||||
{
|
||||
"key": "SETTINGS.LEFT_HANDED_MODE",
|
||||
"en": "Left-handed mode",
|
||||
"translated": "Linkshändige Modus"
|
||||
}
|
||||
]
|
||||
}
|
||||
20
scripts/translator/templates/es.json
Normal file
20
scripts/translator/templates/es.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"full_name": "Spanish",
|
||||
"examples": [
|
||||
{
|
||||
"key": "BAR.OPACITY",
|
||||
"en": "Opacity",
|
||||
"translated": "Opacidad"
|
||||
},
|
||||
{
|
||||
"key": "PANEL.CLOSE",
|
||||
"en": "Close panel",
|
||||
"translated": "Cerrar panel"
|
||||
},
|
||||
{
|
||||
"key": "SETTINGS.LEFT_HANDED_MODE",
|
||||
"en": "Left-handed mode",
|
||||
"translated": "Modo para zurdos"
|
||||
}
|
||||
]
|
||||
}
|
||||
20
scripts/translator/templates/ja.json
Normal file
20
scripts/translator/templates/ja.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"full_name": "Japanese",
|
||||
"examples": [
|
||||
{
|
||||
"key": "BAR.OPACITY",
|
||||
"en": "Opacity",
|
||||
"translated": "不透明度"
|
||||
},
|
||||
{
|
||||
"key": "PANEL.CLOSE",
|
||||
"en": "Close panel",
|
||||
"translated": "パネルを閉じる"
|
||||
},
|
||||
{
|
||||
"key": "SETTINGS.LEFT_HANDED_MODE",
|
||||
"en": "Left-handed mode",
|
||||
"translated": "左利きモード"
|
||||
}
|
||||
]
|
||||
}
|
||||
20
scripts/translator/templates/pl.json
Normal file
20
scripts/translator/templates/pl.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"full_name": "Polish",
|
||||
"examples": [
|
||||
{
|
||||
"key": "BAR.OPACITY",
|
||||
"en": "Opacity",
|
||||
"translated": "Przezroczystość"
|
||||
},
|
||||
{
|
||||
"key": "PANEL.CLOSE",
|
||||
"en": "Close panel",
|
||||
"translated": "Zamknij panel"
|
||||
},
|
||||
{
|
||||
"key": "SETTINGS.LEFT_HANDED_MODE",
|
||||
"en": "Left-handed mode",
|
||||
"translated": "Tryb leworęczny"
|
||||
}
|
||||
]
|
||||
}
|
||||
21
scripts/translator/tsconfig.json
Normal file
21
scripts/translator/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["esnext"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@
|
||||
width="1000" height="500" min_width="1000" min_height="500"
|
||||
gap="4" flex_direction="column"
|
||||
overflow_y="scroll">
|
||||
<label text="aaa" color="#FFFFFF" />
|
||||
<label text="Raw text" color="#FFFFFF" />
|
||||
<label translation="TESTBED.HELLO_WORLD" color="#FFFFFF" />
|
||||
|
||||
<button text="Red button" width="128" height="32" color="#FF0000" />
|
||||
<button text="Aqua button" width="128" height="32" color="#00FFFF" />
|
||||
|
||||
9
uidev/assets/lang/de.json
Normal file
9
uidev/assets/lang/de.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"BAR": {
|
||||
"OPACITY": "Undurchsichtigkeit",
|
||||
"ADDITIVE": "Additiv"
|
||||
},
|
||||
"TESTBED": {
|
||||
"HELLO_WORLD": "Hallo, Welt! Dies ist ein übersetzter I18n-Text."
|
||||
}
|
||||
}
|
||||
9
uidev/assets/lang/en.json
Normal file
9
uidev/assets/lang/en.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"BAR": {
|
||||
"OPACITY": "Opacity",
|
||||
"ADDITIVE": "Additive"
|
||||
},
|
||||
"TESTBED": {
|
||||
"HELLO_WORLD": "Hello, world! This is a translated I18n text."
|
||||
}
|
||||
}
|
||||
9
uidev/assets/lang/es.json
Normal file
9
uidev/assets/lang/es.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"BAR": {
|
||||
"OPACITY": "Opacidad",
|
||||
"ADDITIVE": "Aditivo"
|
||||
},
|
||||
"TESTBED": {
|
||||
"HELLO_WORLD": "¡Hola, mundo! Este es un texto traducido de I18n."
|
||||
}
|
||||
}
|
||||
9
uidev/assets/lang/ja.json
Normal file
9
uidev/assets/lang/ja.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"BAR": {
|
||||
"OPACITY": "不透明度",
|
||||
"ADDITIVE": "加法"
|
||||
},
|
||||
"TESTBED": {
|
||||
"HELLO_WORLD": "こんにちは、世界!これは翻訳されたI18nテキストです。"
|
||||
}
|
||||
}
|
||||
9
uidev/assets/lang/pl.json
Normal file
9
uidev/assets/lang/pl.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"BAR": {
|
||||
"OPACITY": "Przezroczystość",
|
||||
"ADDITIVE": "Addytywny"
|
||||
},
|
||||
"TESTBED": {
|
||||
"HELLO_WORLD": "Witaj, świecie! To przetłumaczony tekst I18n."
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::{assets, testbed::Testbed};
|
||||
use glam::Vec2;
|
||||
use wgui::{event::EventListenerCollection, layout::Layout, parser::ParserState};
|
||||
use wgui::{
|
||||
event::EventListenerCollection, globals::WguiGlobals, layout::Layout, parser::ParserState,
|
||||
};
|
||||
|
||||
pub struct TestbedAny {
|
||||
pub layout: Layout,
|
||||
@@ -15,8 +17,10 @@ impl TestbedAny {
|
||||
listeners: &mut EventListenerCollection<(), ()>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let path = format!("gui/{name}.xml");
|
||||
let (layout, state) =
|
||||
wgui::parser::new_layout_from_assets(Box::new(assets::Asset {}), listeners, &path)?;
|
||||
|
||||
let globals = WguiGlobals::new(Box::new(assets::Asset {}))?;
|
||||
|
||||
let (layout, state) = wgui::parser::new_layout_from_assets(globals, listeners, &path)?;
|
||||
Ok(Self { layout, state })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::{assets, testbed::Testbed};
|
||||
use glam::Vec2;
|
||||
use wgui::{event::EventListenerCollection, layout::Layout, parser::ParserState};
|
||||
use wgui::{
|
||||
event::EventListenerCollection, globals::WguiGlobals, layout::Layout, parser::ParserState,
|
||||
};
|
||||
|
||||
pub struct TestbedGeneric {
|
||||
pub layout: Layout,
|
||||
@@ -13,8 +15,9 @@ impl TestbedGeneric {
|
||||
pub fn new(listeners: &mut EventListenerCollection<(), ()>) -> anyhow::Result<Self> {
|
||||
const XML_PATH: &str = "gui/testbed.xml";
|
||||
|
||||
let (layout, state) =
|
||||
wgui::parser::new_layout_from_assets(Box::new(assets::Asset {}), listeners, XML_PATH)?;
|
||||
let globals = WguiGlobals::new(Box::new(assets::Asset {}))?;
|
||||
|
||||
let (layout, state) = wgui::parser::new_layout_from_assets(globals, listeners, XML_PATH)?;
|
||||
|
||||
Ok(Self { layout, state })
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ regex = "1.11.1"
|
||||
resvg = { version = "0.45.1", default-features = false }
|
||||
roxmltree = "0.20.0"
|
||||
rustc-hash = "2.1.1"
|
||||
serde_json = "1.0.141"
|
||||
slotmap = "1.0.7"
|
||||
smallvec = "1.15.0"
|
||||
taffy = "0.8.1"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use glam::{FloatExt, Vec2};
|
||||
|
||||
use crate::{
|
||||
event::{CallbackDataCommon, EventAlterables, EventRefs},
|
||||
layout::WidgetID,
|
||||
event::{CallbackDataCommon, EventAlterables},
|
||||
layout::{LayoutState, WidgetID},
|
||||
widget::{WidgetData, WidgetObj},
|
||||
};
|
||||
|
||||
@@ -89,13 +89,13 @@ impl Animation {
|
||||
}
|
||||
}
|
||||
|
||||
fn call(&self, refs: &EventRefs, alterables: &mut EventAlterables, pos: f32) {
|
||||
let Some(widget) = refs.widgets.get(self.target_widget).cloned() else {
|
||||
fn call(&self, state: &LayoutState, alterables: &mut EventAlterables, pos: f32) {
|
||||
let Some(widget) = state.widgets.get(self.target_widget).cloned() else {
|
||||
return; // failed
|
||||
};
|
||||
|
||||
let widget_node = *refs.nodes.get(self.target_widget).unwrap();
|
||||
let layout = refs.tree.layout(widget_node).unwrap(); // should always succeed
|
||||
let widget_node = *state.nodes.get(self.target_widget).unwrap();
|
||||
let layout = state.tree.layout(widget_node).unwrap(); // should always succeed
|
||||
|
||||
let mut widget = widget.lock();
|
||||
|
||||
@@ -109,7 +109,7 @@ impl Animation {
|
||||
pos,
|
||||
};
|
||||
|
||||
let common = &mut CallbackDataCommon { refs, alterables };
|
||||
let common = &mut CallbackDataCommon { state, alterables };
|
||||
|
||||
(self.callback)(common, data);
|
||||
}
|
||||
@@ -121,7 +121,7 @@ pub struct Animations {
|
||||
}
|
||||
|
||||
impl Animations {
|
||||
pub fn tick(&mut self, refs: &EventRefs, alterables: &mut EventAlterables) {
|
||||
pub fn tick(&mut self, state: &LayoutState, alterables: &mut EventAlterables) {
|
||||
for anim in &mut self.running_animations {
|
||||
let x = 1.0 - (anim.ticks_remaining as f32 / anim.ticks_duration as f32);
|
||||
let pos = if anim.ticks_remaining > 0 {
|
||||
@@ -133,7 +133,7 @@ impl Animations {
|
||||
|
||||
anim.pos_prev = anim.pos;
|
||||
anim.pos = pos;
|
||||
anim.call(refs, alterables, 1.0);
|
||||
anim.call(state, alterables, 1.0);
|
||||
|
||||
if anim.last_tick {
|
||||
alterables.needs_redraw = true;
|
||||
@@ -147,10 +147,10 @@ impl Animations {
|
||||
.retain(|anim| anim.ticks_remaining > 0);
|
||||
}
|
||||
|
||||
pub fn process(&mut self, refs: &EventRefs, alterables: &mut EventAlterables, alpha: f32) {
|
||||
pub fn process(&mut self, state: &LayoutState, alterables: &mut EventAlterables, alpha: f32) {
|
||||
for anim in &mut self.running_animations {
|
||||
let pos = anim.pos_prev.lerp(anim.pos, alpha);
|
||||
anim.call(refs, alterables, pos);
|
||||
anim.call(state, alterables, pos);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::{
|
||||
components::{Component, InitData},
|
||||
drawing::{self, Color},
|
||||
event::{CallbackDataCommon, EventListenerCollection, EventListenerKind, ListenerHandleVec},
|
||||
i18n::Translation,
|
||||
layout::{Layout, WidgetID},
|
||||
renderer_vk::text::{FontWeight, TextStyle},
|
||||
widget::{
|
||||
@@ -15,8 +16,8 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
pub struct Params<'a> {
|
||||
pub text: &'a str,
|
||||
pub struct Params {
|
||||
pub text: Translation,
|
||||
pub color: drawing::Color,
|
||||
pub border_color: drawing::Color,
|
||||
pub round: WLength,
|
||||
@@ -24,10 +25,10 @@ pub struct Params<'a> {
|
||||
pub text_style: TextStyle,
|
||||
}
|
||||
|
||||
impl Default for Params<'_> {
|
||||
impl Default for Params {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
text: "Text",
|
||||
text: Translation::from_raw_text(""),
|
||||
color: drawing::Color::new(1.0, 1.0, 1.0, 1.0),
|
||||
border_color: drawing::Color::new(0.0, 0.0, 0.0, 1.0),
|
||||
round: WLength::Units(4.0),
|
||||
@@ -55,12 +56,14 @@ impl Component for Button {
|
||||
}
|
||||
|
||||
impl Button {
|
||||
pub fn set_text<C>(&self, common: &mut CallbackDataCommon, text: &str) {
|
||||
pub fn set_text<C>(&self, common: &mut CallbackDataCommon, text: Translation) {
|
||||
let globals = common.state.globals.clone();
|
||||
|
||||
common
|
||||
.refs
|
||||
.state
|
||||
.widgets
|
||||
.call(self.data.text_id, |label: &mut TextLabel| {
|
||||
label.set_text(text);
|
||||
label.set_text(&mut globals.i18n(), text);
|
||||
});
|
||||
common.alterables.mark_redraw();
|
||||
common.alterables.mark_dirty(self.data.text_node);
|
||||
@@ -118,6 +121,8 @@ pub fn construct<U1, U2>(
|
||||
style.justify_content = Some(JustifyContent::Center);
|
||||
style.padding = length(1.0);
|
||||
|
||||
let globals = layout.state.globals.clone();
|
||||
|
||||
let (rect_id, _) = layout.add_child(
|
||||
parent,
|
||||
Rectangle::create(RectangleParams {
|
||||
@@ -137,18 +142,21 @@ pub fn construct<U1, U2>(
|
||||
|
||||
let (text_id, text_node) = layout.add_child(
|
||||
rect_id,
|
||||
TextLabel::create(TextParams {
|
||||
content: String::from(params.text),
|
||||
style: TextStyle {
|
||||
weight: Some(FontWeight::Bold),
|
||||
color: Some(if light_text {
|
||||
Color::new(1.0, 1.0, 1.0, 1.0)
|
||||
} else {
|
||||
Color::new(0.0, 0.0, 0.0, 1.0)
|
||||
}),
|
||||
..params.text_style
|
||||
TextLabel::create(
|
||||
&mut globals.i18n(),
|
||||
TextParams {
|
||||
content: params.text,
|
||||
style: TextStyle {
|
||||
weight: Some(FontWeight::Bold),
|
||||
color: Some(if light_text {
|
||||
Color::new(1.0, 1.0, 1.0, 1.0)
|
||||
} else {
|
||||
Color::new(0.0, 0.0, 0.0, 1.0)
|
||||
}),
|
||||
..params.text_style
|
||||
},
|
||||
},
|
||||
})?,
|
||||
)?,
|
||||
taffy::Style {
|
||||
..Default::default()
|
||||
},
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
use taffy::TaffyTree;
|
||||
|
||||
use crate::{
|
||||
any::AnyTrait,
|
||||
event::EventAlterables,
|
||||
layout::{WidgetID, WidgetMap},
|
||||
};
|
||||
use crate::{any::AnyTrait, event::EventAlterables, layout::LayoutState};
|
||||
|
||||
pub mod button;
|
||||
pub mod slider;
|
||||
|
||||
pub struct InitData<'a> {
|
||||
pub state: &'a LayoutState,
|
||||
pub alterables: &'a mut EventAlterables,
|
||||
pub widgets: &'a WidgetMap,
|
||||
pub tree: &'a TaffyTree<WidgetID>,
|
||||
}
|
||||
|
||||
pub trait Component: AnyTrait {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use glam::{Mat4, Vec2, Vec3};
|
||||
use taffy::{
|
||||
TaffyTree,
|
||||
prelude::{length, percent},
|
||||
};
|
||||
use taffy::prelude::{length, percent};
|
||||
|
||||
use crate::{
|
||||
animation::{Animation, AnimationEasing},
|
||||
@@ -14,7 +11,8 @@ use crate::{
|
||||
self, CallbackDataCommon, EventAlterables, EventListenerCollection, EventListenerKind,
|
||||
ListenerHandleVec,
|
||||
},
|
||||
layout::{Layout, WidgetID, WidgetMap},
|
||||
i18n::{I18n, Translation},
|
||||
layout::{Layout, LayoutState, WidgetID},
|
||||
renderer_vk::{
|
||||
text::{FontWeight, HorizontalAlign, TextStyle},
|
||||
util,
|
||||
@@ -76,13 +74,7 @@ impl Component for Slider {
|
||||
fn init(&self, init_data: &mut InitData) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
let value = state.values.value;
|
||||
state.set_value(
|
||||
&self.data,
|
||||
init_data.alterables,
|
||||
init_data.widgets,
|
||||
init_data.tree,
|
||||
value,
|
||||
);
|
||||
state.set_value(init_data.state, &self.data, init_data.alterables, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,44 +120,42 @@ impl SliderState {
|
||||
|
||||
let norm = map_mouse_x_to_normalized(
|
||||
mouse_pos.x - HANDLE_WIDTH / 2.0,
|
||||
get_width(data.slider_body_node, common.refs.tree) - HANDLE_WIDTH,
|
||||
get_width(data.slider_body_node, &common.state.tree) - HANDLE_WIDTH,
|
||||
);
|
||||
|
||||
let target_value = self.values.get_from_normalized(norm);
|
||||
let val = target_value;
|
||||
|
||||
self.set_value(
|
||||
data,
|
||||
common.alterables,
|
||||
common.refs.widgets,
|
||||
common.refs.tree,
|
||||
val,
|
||||
);
|
||||
self.set_value(common.state, data, common.alterables, val);
|
||||
}
|
||||
|
||||
fn update_text(&self, text: &mut TextLabel, value: f32) {
|
||||
fn update_text(&self, i18n: &mut I18n, text: &mut TextLabel, value: f32) {
|
||||
// round displayed value, should be sufficient for now
|
||||
text.set_text(&format!("{}", value.round()));
|
||||
text.set_text(
|
||||
i18n,
|
||||
Translation::from_raw_text(&format!("{}", value.round())),
|
||||
);
|
||||
}
|
||||
|
||||
fn set_value(
|
||||
&mut self,
|
||||
state: &LayoutState,
|
||||
data: &Data,
|
||||
alterables: &mut EventAlterables,
|
||||
widgets: &WidgetMap,
|
||||
tree: &TaffyTree<WidgetID>,
|
||||
value: f32,
|
||||
) {
|
||||
//common.call_on_widget(data.slider_handle_id, |_div: &mut Div| {});
|
||||
self.values.value = value;
|
||||
let mut style = tree.style(data.slider_handle_node).unwrap().clone();
|
||||
conf_handle_style(&self.values, data.slider_body_node, &mut style, tree);
|
||||
let mut style = state.tree.style(data.slider_handle_node).unwrap().clone();
|
||||
conf_handle_style(&self.values, data.slider_body_node, &mut style, &state.tree);
|
||||
alterables.mark_dirty(data.slider_handle_node);
|
||||
alterables.mark_redraw();
|
||||
alterables.set_style(data.slider_handle_node, style);
|
||||
widgets.call(data.slider_text_id, |label: &mut TextLabel| {
|
||||
self.update_text(label, value);
|
||||
});
|
||||
state
|
||||
.widgets
|
||||
.call(data.slider_text_id, |label: &mut TextLabel| {
|
||||
self.update_text(&mut state.globals.i18n(), label, value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,16 +388,22 @@ pub fn construct<U1, U2>(
|
||||
values: params.values,
|
||||
};
|
||||
|
||||
let globals = layout.state.globals.clone();
|
||||
let mut i18n = globals.i18n();
|
||||
|
||||
let (slider_text_id, _) = layout.add_child(
|
||||
slider_handle_id,
|
||||
TextLabel::create(TextParams {
|
||||
content: String::new(),
|
||||
style: TextStyle {
|
||||
weight: Some(FontWeight::Bold),
|
||||
align: Some(HorizontalAlign::Center),
|
||||
..Default::default()
|
||||
TextLabel::create(
|
||||
&mut i18n,
|
||||
TextParams {
|
||||
content: Translation::default(),
|
||||
style: TextStyle {
|
||||
weight: Some(FontWeight::Bold),
|
||||
align: Some(HorizontalAlign::Center),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
})?,
|
||||
)?,
|
||||
Default::default(),
|
||||
)?;
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ fn draw_widget(
|
||||
widget: &BoxWidget,
|
||||
parent_transform: &glam::Mat4,
|
||||
) {
|
||||
let Ok(l) = layout.tree.layout(node_id) else {
|
||||
let Ok(l) = layout.state.tree.layout(node_id) else {
|
||||
debug_assert!(false);
|
||||
return;
|
||||
};
|
||||
@@ -154,18 +154,18 @@ fn draw_children(
|
||||
parent_node_id: taffy::NodeId,
|
||||
model: &glam::Mat4,
|
||||
) {
|
||||
for node_id in layout.tree.child_ids(parent_node_id) {
|
||||
let Some(widget_id) = layout.tree.get_node_context(node_id).cloned() else {
|
||||
for node_id in layout.state.tree.child_ids(parent_node_id) {
|
||||
let Some(widget_id) = layout.state.tree.get_node_context(node_id).cloned() else {
|
||||
debug_assert!(false);
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(style) = layout.tree.style(node_id) else {
|
||||
let Ok(style) = layout.state.tree.style(node_id) else {
|
||||
debug_assert!(false);
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(widget) = layout.widget_map.get(widget_id) else {
|
||||
let Some(widget) = layout.state.widgets.get(widget_id) else {
|
||||
debug_assert!(false);
|
||||
continue;
|
||||
};
|
||||
@@ -181,11 +181,11 @@ pub fn draw(layout: &Layout) -> anyhow::Result<Vec<RenderPrimitive>> {
|
||||
let mut transform_stack = TransformStack::new();
|
||||
let model = glam::Mat4::IDENTITY;
|
||||
|
||||
let Some(root_widget) = layout.widget_map.get(layout.root_widget) else {
|
||||
let Some(root_widget) = layout.state.widgets.get(layout.root_widget) else {
|
||||
panic!();
|
||||
};
|
||||
|
||||
let Ok(style) = layout.tree.style(layout.root_node) else {
|
||||
let Ok(style) = layout.state.tree.style(layout.root_node) else {
|
||||
panic!();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use std::{
|
||||
cell::{RefCell, RefMut},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use glam::Vec2;
|
||||
use slotmap::SecondaryMap;
|
||||
|
||||
use crate::{
|
||||
animation::{self, Animation},
|
||||
layout::{WidgetID, WidgetMap, WidgetNodeMap},
|
||||
i18n::I18n,
|
||||
layout::{LayoutState, WidgetID},
|
||||
transform_stack::{Transform, TransformStack},
|
||||
widget::{WidgetData, WidgetObj},
|
||||
};
|
||||
@@ -87,12 +91,6 @@ impl Event {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EventRefs<'a> {
|
||||
pub widgets: &'a WidgetMap,
|
||||
pub nodes: &'a WidgetNodeMap,
|
||||
pub tree: &'a taffy::tree::TaffyTree<WidgetID>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EventAlterables {
|
||||
pub dirty_nodes: Vec<taffy::NodeId>,
|
||||
@@ -126,10 +124,16 @@ impl EventAlterables {
|
||||
}
|
||||
|
||||
pub struct CallbackDataCommon<'a> {
|
||||
pub refs: &'a EventRefs<'a>,
|
||||
pub state: &'a LayoutState,
|
||||
pub alterables: &'a mut EventAlterables,
|
||||
}
|
||||
|
||||
impl<'a> CallbackDataCommon<'a> {
|
||||
pub fn i18n(&self) -> RefMut<I18n> {
|
||||
self.state.globals.i18n()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CallbackData<'a> {
|
||||
pub obj: &'a mut dyn WidgetObj,
|
||||
pub widget_data: &'a mut WidgetData,
|
||||
|
||||
34
wgui/src/globals.rs
Normal file
34
wgui/src/globals.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use std::{
|
||||
cell::{RefCell, RefMut},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use crate::{assets::AssetProvider, i18n::I18n};
|
||||
|
||||
pub struct Globals {
|
||||
pub assets: Box<dyn AssetProvider>,
|
||||
pub i18n: I18n,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WguiGlobals(Rc<RefCell<Globals>>);
|
||||
|
||||
impl WguiGlobals {
|
||||
pub fn new(mut assets: Box<dyn AssetProvider>) -> anyhow::Result<Self> {
|
||||
let i18n = I18n::new(&mut assets)?;
|
||||
|
||||
Ok(Self(Rc::new(RefCell::new(Globals { assets, i18n }))))
|
||||
}
|
||||
|
||||
pub fn get(&self) -> RefMut<Globals> {
|
||||
self.0.borrow_mut()
|
||||
}
|
||||
|
||||
pub fn i18n(&self) -> RefMut<I18n> {
|
||||
RefMut::map(self.0.borrow_mut(), |x| &mut x.i18n)
|
||||
}
|
||||
|
||||
pub fn assets(&self) -> RefMut<Box<dyn AssetProvider>> {
|
||||
RefMut::map(self.0.borrow_mut(), |x| &mut x.assets)
|
||||
}
|
||||
}
|
||||
110
wgui/src/i18n.rs
Normal file
110
wgui/src/i18n.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::assets::AssetProvider;
|
||||
|
||||
// a string which optionally has translation key in it
|
||||
// it will hopefully support dynamic language changing soon
|
||||
// for now it's just a simple string container
|
||||
#[derive(Default)]
|
||||
pub struct Translation {
|
||||
text: Rc<str>,
|
||||
translated: bool, // if true, `text` is a translation key
|
||||
}
|
||||
|
||||
impl PartialEq for Translation {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
*self.text == *other.text && self.translated == other.translated
|
||||
}
|
||||
}
|
||||
|
||||
impl Translation {
|
||||
pub fn generate(&self, i18n: &mut I18n) -> Rc<str> {
|
||||
if self.translated {
|
||||
i18n.translate(&self.text)
|
||||
} else {
|
||||
self.text.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_raw_text(text: &str) -> Self {
|
||||
Self {
|
||||
text: Rc::from(text),
|
||||
translated: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_translation_key(translated: &str) -> Self {
|
||||
Self {
|
||||
text: Rc::from(translated),
|
||||
translated: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct I18n {
|
||||
json_root_translated: serde_json::Value, // any language
|
||||
|
||||
// TODO
|
||||
json_root_fallback: serde_json::Value, // english
|
||||
}
|
||||
|
||||
fn find_translation<'a>(translation: &str, mut val: &'a serde_json::Value) -> Option<&'a str> {
|
||||
for part in translation.split('.') {
|
||||
if let Some(sub) = val.get(part) {
|
||||
val = sub;
|
||||
}
|
||||
}
|
||||
|
||||
val.as_str()
|
||||
}
|
||||
|
||||
fn guess_lang() -> String {
|
||||
if let Ok(lang) = std::env::var("LANG") {
|
||||
if let Some((first, _)) = lang.split_once('_') {
|
||||
String::from(first)
|
||||
} else {
|
||||
lang
|
||||
}
|
||||
} else {
|
||||
log::warn!("LANG is not set, defaulting to \"en\".");
|
||||
String::from("en")
|
||||
}
|
||||
}
|
||||
|
||||
impl I18n {
|
||||
pub fn new(provider: &mut Box<dyn AssetProvider>) -> anyhow::Result<Self> {
|
||||
let mut lang = guess_lang();
|
||||
log::info!("Guessed system language: {lang}");
|
||||
|
||||
match lang.as_str() {
|
||||
"en" | "pl" | "it" | "ja" | "es" => {}
|
||||
_ => {
|
||||
log::warn!(
|
||||
"Unsupported language \"{}\", defaulting to \"en\".",
|
||||
lang.as_str()
|
||||
);
|
||||
|
||||
lang = String::from("en");
|
||||
}
|
||||
}
|
||||
|
||||
let data_english = provider.load_from_path(&format!("lang/{lang}.json"))?;
|
||||
let data_translated = provider.load_from_path(&format!("lang/{lang}.json"))?;
|
||||
|
||||
let json_root_fallback = serde_json::from_str(str::from_utf8(&data_english)?)?;
|
||||
let json_root_translated = serde_json::from_str(str::from_utf8(&data_translated)?)?;
|
||||
|
||||
Ok(Self {
|
||||
json_root_fallback,
|
||||
json_root_translated,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn translate(&mut self, translation_key: &str) -> Rc<str> {
|
||||
if let Some(translated) = find_translation(translation_key, &self.json_root_translated) {
|
||||
Rc::from(translated)
|
||||
} else {
|
||||
Rc::from(translation_key) // show translation key as a fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@ use std::{collections::VecDeque, rc::Rc, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
animation::Animations,
|
||||
assets::AssetProvider,
|
||||
components::{Component, InitData},
|
||||
event::{self, EventAlterables, EventListenerCollection, EventRefs},
|
||||
event::{self, EventAlterables, EventListenerCollection},
|
||||
globals::WguiGlobals,
|
||||
transform_stack::Transform,
|
||||
widget::{self, EventParams, WidgetObj, WidgetState, div::Div},
|
||||
};
|
||||
@@ -60,15 +60,18 @@ impl WidgetMap {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LayoutState {
|
||||
pub globals: WguiGlobals,
|
||||
pub widgets: WidgetMap,
|
||||
pub nodes: WidgetNodeMap,
|
||||
pub tree: taffy::tree::TaffyTree<WidgetID>,
|
||||
}
|
||||
|
||||
pub struct Layout {
|
||||
pub tree: TaffyTree<WidgetID>,
|
||||
pub state: LayoutState,
|
||||
|
||||
pub assets: Box<dyn AssetProvider>,
|
||||
pub components_to_init: VecDeque<Rc<dyn Component>>,
|
||||
|
||||
pub widget_map: WidgetMap,
|
||||
pub widget_node_map: WidgetNodeMap,
|
||||
|
||||
pub root_widget: WidgetID,
|
||||
pub root_node: taffy::NodeId,
|
||||
|
||||
@@ -83,21 +86,21 @@ pub struct Layout {
|
||||
|
||||
fn add_child_internal(
|
||||
tree: &mut taffy::TaffyTree<WidgetID>,
|
||||
widget_map: &mut WidgetMap,
|
||||
widget_node_map: &mut WidgetNodeMap,
|
||||
widgets: &mut WidgetMap,
|
||||
nodes: &mut WidgetNodeMap,
|
||||
parent_node: Option<taffy::NodeId>,
|
||||
widget: WidgetState,
|
||||
style: taffy::Style,
|
||||
) -> anyhow::Result<(WidgetID, taffy::NodeId)> {
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
let child_id = widget_map.insert(Arc::new(Mutex::new(widget)));
|
||||
let child_id = widgets.insert(Arc::new(Mutex::new(widget)));
|
||||
let child_node = tree.new_leaf_with_context(style, child_id)?;
|
||||
|
||||
if let Some(parent_node) = parent_node {
|
||||
tree.add_child(parent_node, child_node)?;
|
||||
}
|
||||
|
||||
widget_node_map.insert(child_id, child_node);
|
||||
nodes.insert(child_id, child_node);
|
||||
|
||||
Ok((child_id, child_node))
|
||||
}
|
||||
@@ -109,14 +112,14 @@ impl Layout {
|
||||
widget: WidgetState,
|
||||
style: taffy::Style,
|
||||
) -> anyhow::Result<(WidgetID, taffy::NodeId)> {
|
||||
let parent_node = *self.widget_node_map.get(parent_widget_id).unwrap();
|
||||
let parent_node = *self.state.nodes.get(parent_widget_id).unwrap();
|
||||
|
||||
self.needs_redraw = true;
|
||||
|
||||
add_child_internal(
|
||||
&mut self.tree,
|
||||
&mut self.widget_map,
|
||||
&mut self.widget_node_map,
|
||||
&mut self.state.tree,
|
||||
&mut self.state.widgets,
|
||||
&mut self.state.nodes,
|
||||
Some(parent_node),
|
||||
widget,
|
||||
style,
|
||||
@@ -128,9 +131,8 @@ impl Layout {
|
||||
|
||||
while let Some(c) = self.components_to_init.pop_front() {
|
||||
c.init(&mut InitData {
|
||||
widgets: &self.widget_map,
|
||||
state: &self.state,
|
||||
alterables: &mut alterables,
|
||||
tree: &self.tree,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -151,7 +153,7 @@ impl Layout {
|
||||
alterables: &mut EventAlterables,
|
||||
user_data: &mut (&mut U1, &mut U2),
|
||||
) -> anyhow::Result<()> {
|
||||
for child_id in self.tree.child_ids(parent_node_id) {
|
||||
for child_id in self.state.tree.child_ids(parent_node_id) {
|
||||
self.push_event_widget(listeners, child_id, event, alterables, user_data)?;
|
||||
}
|
||||
|
||||
@@ -166,14 +168,14 @@ impl Layout {
|
||||
alterables: &mut EventAlterables,
|
||||
user_data: &mut (&mut U1, &mut U2),
|
||||
) -> anyhow::Result<()> {
|
||||
let l = self.tree.layout(node_id)?;
|
||||
let Some(widget_id) = self.tree.get_node_context(node_id).cloned() else {
|
||||
let l = self.state.tree.layout(node_id)?;
|
||||
let Some(widget_id) = self.state.tree.get_node_context(node_id).cloned() else {
|
||||
anyhow::bail!("invalid widget ID");
|
||||
};
|
||||
|
||||
let style = self.tree.style(node_id)?;
|
||||
let style = self.state.tree.style(node_id)?;
|
||||
|
||||
let Some(widget) = self.widget_map.get(widget_id) else {
|
||||
let Some(widget) = self.state.widgets.get(widget_id) else {
|
||||
debug_assert!(false);
|
||||
anyhow::bail!("invalid widget");
|
||||
};
|
||||
@@ -191,11 +193,7 @@ impl Layout {
|
||||
let mut iter_children = true;
|
||||
|
||||
let mut params = EventParams {
|
||||
refs: &EventRefs {
|
||||
tree: &self.tree,
|
||||
widgets: &self.widget_map,
|
||||
nodes: &self.widget_node_map,
|
||||
},
|
||||
state: &self.state,
|
||||
layout: l,
|
||||
alterables,
|
||||
node_id,
|
||||
@@ -278,15 +276,18 @@ impl Layout {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn new(assets: Box<dyn AssetProvider>) -> anyhow::Result<Self> {
|
||||
let mut tree = TaffyTree::new();
|
||||
let mut widget_node_map = WidgetNodeMap::default();
|
||||
let mut widget_map = WidgetMap::new();
|
||||
pub fn new(globals: WguiGlobals) -> anyhow::Result<Self> {
|
||||
let mut state = LayoutState {
|
||||
tree: TaffyTree::new(),
|
||||
widgets: WidgetMap::new(),
|
||||
nodes: WidgetNodeMap::default(),
|
||||
globals,
|
||||
};
|
||||
|
||||
let (root_widget, root_node) = add_child_internal(
|
||||
&mut tree,
|
||||
&mut widget_map,
|
||||
&mut widget_node_map,
|
||||
&mut state.tree,
|
||||
&mut state.widgets,
|
||||
&mut state.nodes,
|
||||
None, // no parent
|
||||
Div::create()?,
|
||||
taffy::Style {
|
||||
@@ -296,17 +297,14 @@ impl Layout {
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
tree,
|
||||
state,
|
||||
prev_size: Vec2::default(),
|
||||
content_size: Vec2::default(),
|
||||
root_node,
|
||||
root_widget,
|
||||
widget_node_map,
|
||||
widget_map,
|
||||
needs_redraw: true,
|
||||
haptics_triggered: false,
|
||||
animations: Animations::default(),
|
||||
assets,
|
||||
components_to_init: VecDeque::new(),
|
||||
})
|
||||
}
|
||||
@@ -314,23 +312,17 @@ impl Layout {
|
||||
pub fn update(&mut self, size: Vec2, timestep_alpha: f32) -> anyhow::Result<()> {
|
||||
let mut alterables = EventAlterables::default();
|
||||
|
||||
let refs = EventRefs {
|
||||
tree: &self.tree,
|
||||
widgets: &self.widget_map,
|
||||
nodes: &self.widget_node_map,
|
||||
};
|
||||
|
||||
self
|
||||
.animations
|
||||
.process(&refs, &mut alterables, timestep_alpha);
|
||||
.process(&self.state, &mut alterables, timestep_alpha);
|
||||
|
||||
self.process_alterables(alterables)?;
|
||||
|
||||
if self.tree.dirty(self.root_node)? || self.prev_size != size {
|
||||
if self.state.tree.dirty(self.root_node)? || self.prev_size != size {
|
||||
self.needs_redraw = true;
|
||||
log::debug!("re-computing layout, size {}x{}", size.x, size.y);
|
||||
self.prev_size = size;
|
||||
self.tree.compute_layout_with_measure(
|
||||
self.state.tree.compute_layout_with_measure(
|
||||
self.root_node,
|
||||
taffy::Size {
|
||||
width: taffy::AvailableSpace::Definite(size.x),
|
||||
@@ -348,7 +340,7 @@ impl Layout {
|
||||
match node_context {
|
||||
None => taffy::Size::ZERO,
|
||||
Some(h) => {
|
||||
if let Some(w) = self.widget_map.get(*h) {
|
||||
if let Some(w) = self.state.widgets.get(*h) {
|
||||
w.lock().obj.measure(known_dimensions, available_space)
|
||||
} else {
|
||||
taffy::Size::ZERO
|
||||
@@ -357,7 +349,7 @@ impl Layout {
|
||||
}
|
||||
},
|
||||
)?;
|
||||
let root_size = self.tree.layout(self.root_node).unwrap().size;
|
||||
let root_size = self.state.tree.layout(self.root_node).unwrap().size;
|
||||
log::debug!(
|
||||
"content size {:.0}x{:.0} → {:.0}x{:.0}",
|
||||
self.content_size.x,
|
||||
@@ -373,13 +365,7 @@ impl Layout {
|
||||
pub fn tick(&mut self) -> anyhow::Result<()> {
|
||||
let mut alterables = EventAlterables::default();
|
||||
|
||||
let refs = EventRefs {
|
||||
tree: &self.tree,
|
||||
widgets: &self.widget_map,
|
||||
nodes: &self.widget_node_map,
|
||||
};
|
||||
|
||||
self.animations.tick(&refs, &mut alterables);
|
||||
self.animations.tick(&self.state, &mut alterables);
|
||||
self.process_alterables(alterables)?;
|
||||
self.process_pending_components()?;
|
||||
|
||||
@@ -388,7 +374,7 @@ impl Layout {
|
||||
|
||||
fn process_alterables(&mut self, alterables: EventAlterables) -> anyhow::Result<()> {
|
||||
for node in alterables.dirty_nodes {
|
||||
self.tree.mark_dirty(node)?;
|
||||
self.state.tree.mark_dirty(node)?;
|
||||
}
|
||||
|
||||
if alterables.needs_redraw {
|
||||
@@ -407,7 +393,7 @@ impl Layout {
|
||||
}
|
||||
|
||||
for request in alterables.style_set_requests {
|
||||
if let Err(e) = self.tree.set_style(request.0, request.1) {
|
||||
if let Err(e) = self.state.tree.set_style(request.0, request.1) {
|
||||
log::error!(
|
||||
"failed to set style for taffy widget ID {:?}: {:?}",
|
||||
request.0,
|
||||
|
||||
@@ -5,6 +5,8 @@ pub mod components;
|
||||
pub mod drawing;
|
||||
pub mod event;
|
||||
pub mod gfx;
|
||||
pub mod globals;
|
||||
pub mod i18n;
|
||||
pub mod layout;
|
||||
pub mod parser;
|
||||
pub mod renderer_vk;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
components::button,
|
||||
drawing::Color,
|
||||
i18n::Translation,
|
||||
layout::WidgetID,
|
||||
parser::{
|
||||
ParserContext, ParserFile, iter_attribs,
|
||||
@@ -18,8 +19,7 @@ pub fn parse_component_button<'a, U1, U2>(
|
||||
let mut color = Color::new(1.0, 1.0, 1.0, 1.0);
|
||||
let mut border_color: Option<Color> = None;
|
||||
let mut round = WLength::Units(4.0);
|
||||
|
||||
let mut text = String::default();
|
||||
let mut translation = Translation::default();
|
||||
|
||||
let attribs: Vec<_> = iter_attribs(file, ctx, &node, false).collect();
|
||||
let text_style = parse_text_style(&attribs);
|
||||
@@ -28,7 +28,10 @@ pub fn parse_component_button<'a, U1, U2>(
|
||||
for (key, value) in attribs {
|
||||
match key.as_ref() {
|
||||
"text" => {
|
||||
text = String::from(value.as_ref());
|
||||
translation = Translation::from_raw_text(&value);
|
||||
}
|
||||
"translation" => {
|
||||
translation = Translation::from_translation_key(&value);
|
||||
}
|
||||
"round" => {
|
||||
parse_round(&value, &mut round);
|
||||
@@ -59,7 +62,7 @@ pub fn parse_component_button<'a, U1, U2>(
|
||||
button::Params {
|
||||
color,
|
||||
border_color: border_color.unwrap(),
|
||||
text: &text,
|
||||
text: translation,
|
||||
style,
|
||||
text_style,
|
||||
round,
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::{
|
||||
components::Component,
|
||||
drawing::{self},
|
||||
event::EventListenerCollection,
|
||||
globals::WguiGlobals,
|
||||
layout::{Layout, WidgetID},
|
||||
parser::{
|
||||
component_button::parse_component_button, component_slider::parse_component_slider,
|
||||
@@ -631,11 +632,11 @@ pub fn parse_from_assets<U1, U2>(
|
||||
}
|
||||
|
||||
pub fn new_layout_from_assets<U1, U2>(
|
||||
assets: Box<dyn AssetProvider>,
|
||||
globals: WguiGlobals,
|
||||
listeners: &mut EventListenerCollection<U1, U2>,
|
||||
path: &str,
|
||||
) -> anyhow::Result<(Layout, ParserState)> {
|
||||
let mut layout = Layout::new(assets)?;
|
||||
let mut layout = Layout::new(globals)?;
|
||||
let widget = layout.root_widget;
|
||||
let state = parse_from_assets(&mut layout, listeners, widget, path)?;
|
||||
Ok((layout, state))
|
||||
@@ -650,7 +651,7 @@ fn get_doc_from_path<U1, U2>(
|
||||
ctx: &mut ParserContext<U1, U2>,
|
||||
path: &Path,
|
||||
) -> anyhow::Result<(ParserFile, roxmltree::NodeId)> {
|
||||
let xml = assets_path_to_xml(&mut ctx.layout.assets, path)?;
|
||||
let xml = assets_path_to_xml(&mut ctx.layout.state.globals.assets(), path)?;
|
||||
let document = Rc::new(XmlDocument::new(xml, |xml| {
|
||||
let opt = roxmltree::ParsingOptions {
|
||||
allow_dtd: true,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::{
|
||||
i18n::Translation,
|
||||
layout::WidgetID,
|
||||
parser::{
|
||||
ParserContext, ParserFile, iter_attribs, parse_children, parse_universal,
|
||||
@@ -22,15 +23,20 @@ pub fn parse_widget_label<'a, U1, U2>(
|
||||
for (key, value) in attribs {
|
||||
match &*key {
|
||||
"text" => {
|
||||
params.content = String::from(value.as_ref());
|
||||
params.content = Translation::from_raw_text(&value);
|
||||
}
|
||||
"translation" => params.content = Translation::from_translation_key(&value),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let (new_id, _) = ctx
|
||||
.layout
|
||||
.add_child(parent_id, TextLabel::create(params)?, style)?;
|
||||
let globals = ctx.layout.state.globals.clone();
|
||||
let mut i18n = globals.i18n();
|
||||
|
||||
let (new_id, _) =
|
||||
ctx
|
||||
.layout
|
||||
.add_child(parent_id, TextLabel::create(&mut i18n, params)?, style)?;
|
||||
|
||||
parse_universal(file, ctx, node, new_id)?;
|
||||
parse_children(file, ctx, node, new_id)?;
|
||||
|
||||
@@ -23,13 +23,14 @@ pub fn parse_widget_sprite<'a, U1, U2>(
|
||||
for (key, value) in attribs {
|
||||
match key.as_ref() {
|
||||
"src" => {
|
||||
glyph = match CustomGlyphContent::from_assets(&mut ctx.layout.assets, &value) {
|
||||
Ok(glyph) => Some(glyph),
|
||||
Err(e) => {
|
||||
log::warn!("failed to load {}: {}", value, e);
|
||||
None
|
||||
glyph =
|
||||
match CustomGlyphContent::from_assets(&mut ctx.layout.state.globals.assets(), &value) {
|
||||
Ok(glyph) => Some(glyph),
|
||||
Err(e) => {
|
||||
log::warn!("failed to load {value}: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"src_ext" => {
|
||||
if std::fs::exists(value.as_ref()).unwrap_or(false) {
|
||||
|
||||
@@ -7,9 +7,9 @@ use crate::{
|
||||
drawing,
|
||||
event::{
|
||||
self, CallbackData, CallbackDataCommon, CallbackMetadata, Event, EventAlterables,
|
||||
EventListenerKind, EventListenerVec, EventRefs, MouseWheelEvent,
|
||||
EventListenerKind, EventListenerVec, MouseWheelEvent,
|
||||
},
|
||||
layout::{Layout, WidgetID},
|
||||
layout::{Layout, LayoutState, WidgetID},
|
||||
transform_stack::TransformStack,
|
||||
};
|
||||
|
||||
@@ -124,7 +124,7 @@ pub trait WidgetObj: AnyTrait {
|
||||
pub struct EventParams<'a> {
|
||||
pub node_id: taffy::NodeId,
|
||||
pub style: &'a taffy::Style,
|
||||
pub refs: &'a EventRefs<'a>,
|
||||
pub state: &'a LayoutState,
|
||||
pub alterables: &'a mut EventAlterables,
|
||||
pub layout: &'a taffy::Layout,
|
||||
}
|
||||
@@ -197,7 +197,7 @@ macro_rules! call_event {
|
||||
};
|
||||
|
||||
let mut common = CallbackDataCommon {
|
||||
refs: $params.refs,
|
||||
state: $params.state,
|
||||
alterables: $params.alterables,
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use taffy::AvailableSpace;
|
||||
|
||||
use crate::{
|
||||
drawing::{self, Boundary},
|
||||
i18n::{I18n, Translation},
|
||||
renderer_vk::text::{FONT_SYSTEM, TextStyle},
|
||||
};
|
||||
|
||||
@@ -12,7 +13,7 @@ use super::{WidgetObj, WidgetState};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TextParams {
|
||||
pub content: String,
|
||||
pub content: Translation,
|
||||
pub style: TextStyle,
|
||||
}
|
||||
|
||||
@@ -23,7 +24,7 @@ pub struct TextLabel {
|
||||
}
|
||||
|
||||
impl TextLabel {
|
||||
pub fn create(params: TextParams) -> anyhow::Result<WidgetState> {
|
||||
pub fn create(i18n: &mut I18n, params: TextParams) -> anyhow::Result<WidgetState> {
|
||||
let metrics = Metrics::from(¶ms.style);
|
||||
let attrs = Attrs::from(¶ms.style);
|
||||
let wrap = Wrap::from(¶ms.style);
|
||||
@@ -35,7 +36,7 @@ impl TextLabel {
|
||||
buffer.set_wrap(wrap);
|
||||
|
||||
buffer.set_rich_text(
|
||||
[(params.content.as_str(), attrs)],
|
||||
[(params.content.generate(i18n).as_ref(), attrs)],
|
||||
&Attrs::new(),
|
||||
Shaping::Advanced,
|
||||
params.style.align.map(|a| a.into()),
|
||||
@@ -49,28 +50,24 @@ impl TextLabel {
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, text: &str) {
|
||||
if self.params.content.as_str() == text {
|
||||
pub fn set_text(&mut self, i18n: &mut I18n, translation: Translation) {
|
||||
if self.params.content == translation {
|
||||
return;
|
||||
}
|
||||
|
||||
self.params.content = String::from(text);
|
||||
self.params.content = translation;
|
||||
let attrs = Attrs::from(&self.params.style);
|
||||
let mut font_system = FONT_SYSTEM.lock();
|
||||
|
||||
let mut buffer = self.buffer.borrow_mut();
|
||||
buffer.set_rich_text(
|
||||
&mut font_system,
|
||||
[(self.params.content.as_str(), attrs)],
|
||||
[(self.params.content.generate(i18n).as_ref(), attrs)],
|
||||
&Attrs::new(),
|
||||
Shaping::Advanced,
|
||||
self.params.style.align.map(|a| a.into()),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn get_text(&self) -> &str {
|
||||
&self.params.content
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetObj for TextLabel {
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
<TopButton id="delete" src="bar/delete.svg" />
|
||||
</rectangle>
|
||||
<rectangle padding="8" gap="8" round="100%" color="~bg_color_active" justify_content="center" align_items="center">
|
||||
<label size="18" text="Opacity" color="~text_color" />
|
||||
<label size="18" translation="BAR.OPACITY" color="~text_color" />
|
||||
<slider width="150" height="24" min_value="0" max_value="100" value="100" />
|
||||
<label size="18" text="Additive:" color="~text_color" />
|
||||
<label size="18" translation="BAR.ADDITIVE" color="~text_color" />
|
||||
<sprite color="~device_color" width="20" height="20" src="bar/checkbox-checked.svg" />
|
||||
</rectangle>
|
||||
</div>
|
||||
|
||||
1
wlx-overlay-s/src/assets/lang/en.json
Normal file
1
wlx-overlay-s/src/assets/lang/en.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -44,11 +44,8 @@ impl<S> GuiPanel<S> {
|
||||
pub fn new_from_template(app: &mut AppState, path: &str, state: S) -> anyhow::Result<Self> {
|
||||
let mut listeners = EventListenerCollection::<AppState, S>::default();
|
||||
|
||||
let (layout, parser_state) = wgui::parser::new_layout_from_assets(
|
||||
Box::new(gui::asset::GuiAsset {}),
|
||||
&mut listeners,
|
||||
path,
|
||||
)?;
|
||||
let (layout, parser_state) =
|
||||
wgui::parser::new_layout_from_assets(app.wgui_globals.clone(), &mut listeners, path)?;
|
||||
|
||||
let context = WguiContext::new(&mut app.wgui_shared, 1.0)?;
|
||||
let mut timestep = Timestep::new();
|
||||
@@ -68,7 +65,7 @@ impl<S> GuiPanel<S> {
|
||||
}
|
||||
|
||||
pub fn new_blank(app: &mut AppState, state: S) -> anyhow::Result<Self> {
|
||||
let layout = Layout::new(Box::new(GuiAsset {}))?;
|
||||
let layout = Layout::new(app.wgui_globals.clone())?;
|
||||
let context = WguiContext::new(&mut app.wgui_shared, 1.0)?;
|
||||
let mut timestep = Timestep::new();
|
||||
timestep.set_tps(60.0);
|
||||
|
||||
@@ -16,7 +16,7 @@ use wgui::{
|
||||
|
||||
use crate::{
|
||||
backend::overlay::{OverlayData, OverlayState, Positioning},
|
||||
gui::{self, panel::GuiPanel},
|
||||
gui::panel::GuiPanel,
|
||||
state::AppState,
|
||||
subsystem::hid::{ALT, CTRL, META, SHIFT, SUPER, XkbKeymap},
|
||||
};
|
||||
@@ -76,7 +76,7 @@ where
|
||||
}
|
||||
|
||||
let (_, mut gui_state_key) = wgui::parser::new_layout_from_assets(
|
||||
Box::new(gui::asset::GuiAsset {}),
|
||||
app.wgui_globals.clone(),
|
||||
&mut panel.listeners,
|
||||
"gui/keyboard.xml",
|
||||
)?;
|
||||
@@ -169,7 +169,8 @@ where
|
||||
let key_state = {
|
||||
let rect = panel
|
||||
.layout
|
||||
.widget_map
|
||||
.state
|
||||
.widgets
|
||||
.get_as::<Rectangle>(*widget_id)
|
||||
.unwrap(); // want panic
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use glam::{Quat, vec3a};
|
||||
use idmap_derive::IntegerId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wgui::{
|
||||
i18n::Translation,
|
||||
parser::parse_color_hex,
|
||||
renderer_vk::text::{FontWeight, TextStyle},
|
||||
taffy::{
|
||||
@@ -168,6 +169,9 @@ fn new_toast(toast: Toast, app: &mut AppState) -> Option<(OverlayState, Box<dyn
|
||||
|
||||
let mut panel = GuiPanel::new_blank(app, ()).ok()?;
|
||||
|
||||
let globals = panel.layout.state.globals.clone();
|
||||
let mut i18n = globals.i18n();
|
||||
|
||||
let (rect, _) = panel
|
||||
.layout
|
||||
.add_child(
|
||||
@@ -192,13 +196,16 @@ fn new_toast(toast: Toast, app: &mut AppState) -> Option<(OverlayState, Box<dyn
|
||||
|
||||
let _ = panel.layout.add_child(
|
||||
rect,
|
||||
TextLabel::create(TextParams {
|
||||
content: title,
|
||||
style: TextStyle {
|
||||
color: parse_color_hex("#ffffff"),
|
||||
..Default::default()
|
||||
TextLabel::create(
|
||||
&mut i18n,
|
||||
TextParams {
|
||||
content: Translation::from_raw_text(&title),
|
||||
style: TextStyle {
|
||||
color: parse_color_hex("#ffffff"),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
.unwrap(),
|
||||
taffy::Style {
|
||||
size: taffy::Size {
|
||||
@@ -212,14 +219,17 @@ fn new_toast(toast: Toast, app: &mut AppState) -> Option<(OverlayState, Box<dyn
|
||||
|
||||
let _ = panel.layout.add_child(
|
||||
rect,
|
||||
TextLabel::create(TextParams {
|
||||
content: toast.body,
|
||||
style: TextStyle {
|
||||
weight: Some(FontWeight::Bold),
|
||||
color: parse_color_hex("#eeeeee"),
|
||||
..Default::default()
|
||||
TextLabel::create(
|
||||
&mut i18n,
|
||||
TextParams {
|
||||
content: Translation::from_raw_text(&toast.body),
|
||||
style: TextStyle {
|
||||
weight: Some(FontWeight::Bold),
|
||||
color: parse_color_hex("#eeeeee"),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
.unwrap(),
|
||||
taffy::Style {
|
||||
size: taffy::Size {
|
||||
|
||||
@@ -6,6 +6,7 @@ use glam::Vec3A;
|
||||
use regex::Regex;
|
||||
use wgui::{
|
||||
event::{self, EventListenerKind},
|
||||
i18n::Translation,
|
||||
widget::text::TextLabel,
|
||||
};
|
||||
|
||||
@@ -43,18 +44,20 @@ where
|
||||
|
||||
let mut label = panel
|
||||
.layout
|
||||
.widget_map
|
||||
.state
|
||||
.widgets
|
||||
.get_as::<TextLabel>(*widget_id)
|
||||
.unwrap();
|
||||
|
||||
let format = match role {
|
||||
"tz" => {
|
||||
let mut i18n = panel.layout.state.globals.i18n();
|
||||
if let Some(s) =
|
||||
tz_str.and_then(|tz| tz.split('/').next_back().map(|x| x.replace('_', " ")))
|
||||
{
|
||||
label.set_text(&s);
|
||||
label.set_text(&mut i18n, Translation::from_raw_text(&s));
|
||||
} else {
|
||||
label.set_text("Local");
|
||||
label.set_text(&mut i18n, Translation::from_raw_text("Local"));
|
||||
}
|
||||
|
||||
continue;
|
||||
@@ -69,7 +72,8 @@ where
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
label.set_text("ERR");
|
||||
let mut i18n = panel.layout.state.globals.i18n();
|
||||
label.set_text(&mut i18n, Translation::from_raw_text("ERR"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -87,8 +91,8 @@ where
|
||||
&mut panel.listener_handles,
|
||||
*widget_id,
|
||||
EventListenerKind::InternalStateChange,
|
||||
Box::new(move |_common, data, _, _| {
|
||||
clock_on_tick(&clock, data);
|
||||
Box::new(move |common, data, _, _| {
|
||||
clock_on_tick(&clock, common, data);
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -151,12 +155,16 @@ struct ClockState {
|
||||
format: Rc<str>,
|
||||
}
|
||||
|
||||
fn clock_on_tick(clock: &ClockState, data: &mut event::CallbackData) {
|
||||
fn clock_on_tick(
|
||||
clock: &ClockState,
|
||||
common: &event::CallbackDataCommon,
|
||||
data: &mut event::CallbackData,
|
||||
) {
|
||||
let date_time = clock.timezone.as_ref().map_or_else(
|
||||
|| format!("{}", Local::now().format(&clock.format)),
|
||||
|tz| format!("{}", Local::now().with_timezone(tz).format(&clock.format)),
|
||||
);
|
||||
|
||||
let label = data.obj.get_as_mut::<TextLabel>();
|
||||
label.set_text(&date_time);
|
||||
label.set_text(&mut common.i18n(), Translation::from_raw_text(&date_time));
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ use idmap::IdMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use std::sync::Arc;
|
||||
use wgui::{gfx::WGfx, renderer_vk::context::SharedContext as WSharedContext};
|
||||
use wgui::{
|
||||
gfx::WGfx, globals::WguiGlobals, renderer_vk::context::SharedContext as WSharedContext,
|
||||
};
|
||||
|
||||
#[cfg(feature = "wayvr")]
|
||||
use {
|
||||
@@ -20,6 +22,7 @@ use crate::{
|
||||
config::GeneralConfig,
|
||||
config_io,
|
||||
graphics::WGfxExtras,
|
||||
gui,
|
||||
overlays::toast::{DisplayMethod, ToastTopic},
|
||||
subsystem::{audio::AudioOutput, input::HidWrapper},
|
||||
};
|
||||
@@ -40,6 +43,8 @@ pub struct AppState {
|
||||
pub anchor: Affine3A,
|
||||
pub toast_sound: &'static [u8],
|
||||
|
||||
pub wgui_globals: WguiGlobals,
|
||||
|
||||
#[cfg(feature = "osc")]
|
||||
pub osc_sender: Option<OscSender>,
|
||||
|
||||
@@ -85,6 +90,7 @@ impl AppState {
|
||||
screens: smallvec![],
|
||||
anchor: Affine3A::IDENTITY,
|
||||
toast_sound: toast_sound_wav,
|
||||
wgui_globals: WguiGlobals::new(Box::new(gui::asset::GuiAsset {}))?,
|
||||
|
||||
#[cfg(feature = "osc")]
|
||||
osc_sender,
|
||||
|
||||
Reference in New Issue
Block a user