hold a ref to AndroidApp (#928)

* hold a ref to AndroidApp

* more

* more

* more

* more

* more
This commit is contained in:
Logan McNaughton
2026-05-20 16:16:17 +02:00
committed by GitHub
parent 7eb090a622
commit 01ea4c422a
13 changed files with 432 additions and 353 deletions
Generated
-1
View File
@@ -2249,7 +2249,6 @@ dependencies = [
"futures",
"jni 0.22.4",
"mimalloc",
"ndk-context",
"open",
"postcard",
"rand 0.10.1",
-1
View File
@@ -47,7 +47,6 @@ open = { version = "5.3", optional = true }
[target.'cfg(target_os = "android")'.dependencies]
slint = { version = "1.16", default-features = false, features = ["compat-1-2", "std", "backend-android-activity-06", "accessibility"], optional = true }
jni = "0.22"
ndk-context = "0.1"
[build-dependencies]
winresource = "0.1"
+2 -7
View File
@@ -311,13 +311,7 @@ pub async fn run() -> std::io::Result<()> {
async fn android_main(app: slint::android::AndroidApp) {
slint::android::init(app.clone()).unwrap();
if let Err(_) = android::DIRS.set(ui::Dirs {
config_dir: app.internal_data_path().unwrap().join("config"),
data_dir: app.external_data_path().unwrap().join("data"),
cache_dir: app.internal_data_path().unwrap().join("cache"),
}) {
println!("Android dirs already set");
}
*android::ANDROID_APP.lock().unwrap() = Some(app);
let dirs = ui::get_dirs();
@@ -327,4 +321,5 @@ async fn android_main(app: slint::android::AndroidApp) {
std::fs::create_dir_all(dirs.data_dir.join("states")).unwrap();
ui::gui::app_window(true);
*android::ANDROID_APP.lock().unwrap() = None;
}
+20 -23
View File
@@ -128,32 +128,29 @@ pub fn sdl_close() {
}
}
#[cfg(not(target_os = "android"))]
pub fn get_dirs() -> Dirs {
let exe_path = std::env::current_exe().unwrap();
let portable_dir = exe_path.parent();
let portable = portable_dir.unwrap().join("portable.txt").exists();
if portable {
Dirs {
config_dir: portable_dir.unwrap().join("portable_data").join("config"),
data_dir: portable_dir.unwrap().join("portable_data").join("data"),
cache_dir: portable_dir.unwrap().join("portable_data").join("cache"),
}
} else {
Dirs {
config_dir: dirs::config_dir().unwrap().join("gopher64"),
data_dir: dirs::data_dir().unwrap().join("gopher64"),
cache_dir: dirs::cache_dir().unwrap().join("gopher64"),
#[cfg(not(target_os = "android"))]
{
let exe_path = std::env::current_exe().unwrap();
let portable_dir = exe_path.parent();
let portable = portable_dir.unwrap().join("portable.txt").exists();
if portable {
Dirs {
config_dir: portable_dir.unwrap().join("portable_data").join("config"),
data_dir: portable_dir.unwrap().join("portable_data").join("data"),
cache_dir: portable_dir.unwrap().join("portable_data").join("cache"),
}
} else {
Dirs {
config_dir: dirs::config_dir().unwrap().join("gopher64"),
data_dir: dirs::data_dir().unwrap().join("gopher64"),
cache_dir: dirs::cache_dir().unwrap().join("gopher64"),
}
}
}
}
#[cfg(target_os = "android")]
pub fn get_dirs() -> Dirs {
if let Some(android_dirs) = android::DIRS.get() {
android_dirs.clone()
} else {
panic!("Android app not initialized");
#[cfg(target_os = "android")]
{
android::get_dirs()
}
}
+50 -25
View File
@@ -3,10 +3,12 @@ use jni::refs::Global;
use jni::{Env, JavaVM, bind_java_type};
use crate::ui;
pub static DIRS: std::sync::OnceLock<ui::Dirs> = std::sync::OnceLock::new();
pub static ANDROID_APP: std::sync::Mutex<Option<slint::android::AndroidApp>> =
std::sync::Mutex::new(None);
bind_java_type! {
AndroidContext => "android.content.Context",
AndroidActivity => "android.app.Activity",
type_map = {
AndroidIntent => "android.content.Intent",
},
@@ -72,9 +74,14 @@ pub struct ControllerInfo {
/// Lists connected gamepads and joysticks using the Android framework.
pub fn list_controllers() -> Vec<ControllerInfo> {
let ctx = ndk_context::android_context();
let vm = unsafe { JavaVM::from_raw(ctx.vm().cast()) };
let vm = if let Ok(app) = ANDROID_APP.lock()
&& let Some(app) = app.as_ref()
{
unsafe { JavaVM::from_raw(app.vm_as_ptr().cast()) }
} else {
eprintln!("Android app not initialized");
return Vec::new();
};
match vm.attach_current_thread(list_controllers_on_jvm) {
Ok(controllers) => controllers,
@@ -150,35 +157,39 @@ fn list_controllers_on_jvm(env: &mut Env<'_>) -> jni::errors::Result<Vec<Control
/// Opens a URI in the user's default app via [`Intent::ACTION_VIEW`](https://developer.android.com/reference/android/content/Intent#ACTION_VIEW).
pub fn open_uri(path: &str) {
let ctx = ndk_context::android_context();
let path = path.to_string();
let vm = unsafe { JavaVM::from_raw(ctx.vm().cast()) };
if let Err(err) = vm.attach_current_thread(|env| open_uri_on_jvm(env, ctx.context(), &path)) {
let vm = if let Ok(app) = ANDROID_APP.lock()
&& let Some(app) = app.as_ref()
{
unsafe { JavaVM::from_raw(app.vm_as_ptr().cast()) }
} else {
eprintln!("Android app not initialized");
return;
};
if let Err(err) = vm.attach_current_thread(|env| open_uri_on_jvm(env, &path)) {
eprintln!("JNI error while opening URI: {err:?}");
}
}
fn open_uri_on_jvm(
env: &mut Env<'_>,
context: *mut std::ffi::c_void,
path: &str,
) -> jni::errors::Result<()> {
let context_ptr = context.cast();
let context = unsafe { env.as_cast_raw::<Global<AndroidContext>>(&context_ptr)? };
fn open_uri_on_jvm(env: &mut Env<'_>, path: &str) -> jni::errors::Result<()> {
if let Ok(app) = ANDROID_APP.lock()
&& let Some(app) = app.as_ref()
{
let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
let activity = unsafe { env.as_cast_raw::<Global<AndroidActivity>>(&raw_activity_global)? };
let uri_string = JString::from_str(env, path.to_string())?;
let uri = AndroidUri::parse(env, &uri_string)?;
let uri_string = JString::from_str(env, path.to_string())?;
let uri = AndroidUri::parse(env, &uri_string)?;
let action_view = AndroidIntent::ACTION_VIEW(env)?;
let flag = AndroidIntent::FLAG_ACTIVITY_NEW_TASK(env)?;
let intent = AndroidIntent::new(env, &action_view)?
.set_data(env, &uri)?
.add_flags(env, flag)?;
let action_view = AndroidIntent::ACTION_VIEW(env)?;
let intent = AndroidIntent::new(env, &action_view)?.set_data(env, &uri)?;
context.as_ref().start_activity(env, &intent)?;
Ok(())
activity.as_ref().start_activity(env, &intent)?;
Ok(())
} else {
Err(jni::errors::Error::UninitializedJavaVM)
}
}
pub async fn select_rom(_rom_dir: slint::SharedString) -> Option<std::path::PathBuf> {
@@ -192,3 +203,17 @@ pub async fn select_gb_rom(_player: i32) -> Option<std::path::PathBuf> {
pub async fn select_gb_ram(_player: i32) -> Option<std::path::PathBuf> {
None
}
pub fn get_dirs() -> ui::Dirs {
if let Ok(app) = ANDROID_APP.lock()
&& let Some(app) = app.as_ref()
{
ui::Dirs {
config_dir: app.internal_data_path().unwrap().join("config"),
data_dir: app.external_data_path().unwrap().join("data"),
cache_dir: app.internal_data_path().unwrap().join("cache"),
}
} else {
panic!("Android app not initialized");
}
}
+28 -28
View File
@@ -105,34 +105,34 @@ export component AppWindow inherits Window {
portrait: root.is-portrait;
title: "Gopher64";
model: [
{
icon: @image-url("../../../data/ui/icons/local_game.svg"),
label: @tr("Menu" => "Local Game"),
},
{
icon: @image-url("../../../data/ui/icons/controller.svg"),
label: @tr("Menu" => "Controller Config"),
},
{
icon: @image-url("../../../data/ui/icons/netplay.svg"),
label: @tr("Menu" => "Netplay"),
},
{
icon: @image-url("../../../data/ui/icons/cheats.svg"),
label: @tr("Menu" => "Cheats"),
},
{
icon: @image-url("../../../data/ui/icons/achievements.svg"),
label: @tr("Menu" => "RetroAchievements"),
},
{
icon: @image-url("../../../data/ui/icons/settings.svg"),
label: @tr("Menu" => "Settings"),
},
{
icon: @image-url("../../../data/ui/icons/about.svg"),
label: @tr("Menu" => "About"),
},
{
icon: @image-url("../../../data/ui/icons/local_game.svg"),
label: @tr("Menu" => "Local Game"),
},
{
icon: @image-url("../../../data/ui/icons/controller.svg"),
label: @tr("Menu" => "Controller Config"),
},
{
icon: @image-url("../../../data/ui/icons/netplay.svg"),
label: @tr("Menu" => "Netplay"),
},
{
icon: @image-url("../../../data/ui/icons/cheats.svg"),
label: @tr("Menu" => "Cheats"),
},
{
icon: @image-url("../../../data/ui/icons/achievements.svg"),
label: @tr("Menu" => "RetroAchievements"),
},
{
icon: @image-url("../../../data/ui/icons/settings.svg"),
label: @tr("Menu" => "Settings"),
},
{
icon: @image-url("../../../data/ui/icons/about.svg"),
label: @tr("Menu" => "About"),
},
];
x: root.safe-left;
y: root.is-portrait ? parent.height - root.bar-thickness - root.safe-bottom : root.safe-top;
+4
View File
@@ -13,6 +13,10 @@ export component InputProfileDialog inherits Window {
in-out property profile_name <=> InputProfileData.profile_name;
in-out property deadzone <=> InputProfileData.deadzone;
VerticalBox {
padding-left: root.safe-area-insets.left;
padding-top: root.safe-area-insets.top;
padding-right: root.safe-area-insets.right;
padding-bottom: root.safe-area-insets.bottom;
alignment: start;
HorizontalBox {
+112 -91
View File
@@ -1,8 +1,10 @@
import { LineEdit, HorizontalBox, VerticalBox, Button, ComboBox } from "std-widgets.slint";
import { LineEdit, HorizontalBox, VerticalBox, Button, ComboBox, ScrollView } from "std-widgets.slint";
import { NetplayData } from "netplay_page.slint";
export component NetplayCreate inherits Window {
title: @tr("Netplay: Create Session");
preferred-width: 425px;
preferred-height: 425px;
in-out property server_names <=> NetplayData.server_names;
in-out property server_urls <=> NetplayData.server_urls;
in-out property custom_server_url <=> NetplayData.custom_server_url;
@@ -18,108 +20,121 @@ export component NetplayCreate inherits Window {
callback create_session(string, string, string, string, string, string, string);
VerticalBox {
HorizontalBox {
vertical-stretch: 1;
padding-left: root.safe-area-insets.left;
padding-top: root.safe-area-insets.top;
padding-right: root.safe-area-insets.right;
padding-bottom: root.safe-area-insets.bottom;
ScrollView {
mouse-drag-pan-enabled: true;
vertical-stretch: 1;
VerticalBox {
Text {
text: @tr("Session Name:");
}
alignment: start;
HorizontalBox {
VerticalBox {
Text {
text: @tr("Session Name:");
}
LineEdit {
placeholder-text: @tr("Enter session name");
edited(text) => {
NetplayData.session_name = text;
LineEdit {
placeholder-text: @tr("Enter session name");
edited(text) => {
NetplayData.session_name = text;
}
}
}
}
}
VerticalBox {
Text {
text: @tr("Password (Optional):");
}
VerticalBox {
Text {
text: @tr("Password (Optional):");
}
LineEdit {
placeholder-text: @tr("Enter session password");
edited(text) => {
NetplayData.password = text;
}
}
}
}
VerticalBox {
Text {
text: @tr("Player Name:");
}
LineEdit {
placeholder-text: @tr("Enter player name");
edited(text) => {
NetplayData.player_name = text;
}
}
}
VerticalBox {
Text {
text: @tr("Server:");
}
HorizontalBox {
server := ComboBox {
model: NetplayData.server_names;
selected => {
if (self.current-index >= 0) {
if (self.current-value == "Custom") {
get_custom_url();
LineEdit {
placeholder-text: @tr("Enter session password");
edited(text) => {
NetplayData.password = text;
}
}
}
}
}
}
HorizontalBox {
alignment: center;
if NetplayData.game_name == "": Button {
text: @tr("Select ROM");
clicked => {
select_rom(NetplayData.rom_dir);
VerticalBox {
Text {
text: @tr("Player Name:");
}
LineEdit {
placeholder-text: @tr("Enter player name");
edited(text) => {
NetplayData.player_name = text;
}
}
}
}
if NetplayData.game_name != "": Button {
text: NetplayData.game_name;
clicked => {
select_rom(NetplayData.rom_dir);
VerticalBox {
Text {
text: @tr("Server:");
}
HorizontalBox {
server := ComboBox {
model: NetplayData.server_names;
selected => {
if (self.current-index >= 0) {
if (self.current-value == "Custom") {
get_custom_url();
}
}
}
}
}
}
}
}
Rectangle { }
if NetplayData.game_cheats != "":Text {
text: @tr("This game has cheats enabled. Cheats will be synchronized with other players.");
}
Rectangle { }
Button {
property <string> server_url;
text: @tr("Create Session");
enabled: NetplayData.session_name != "" && NetplayData.player_name != "" && NetplayData.game_name != "" && !NetplayData.pending_session;
clicked => {
NetplayData.pending_session = true;
if (server.current-value == "Custom") {
server_url = "ws://" + NetplayData.custom_server_url;
} else {
server_url = NetplayData.server_urls[server.current-index];
HorizontalBox {
alignment: center;
if NetplayData.game_name == "": Button {
text: @tr("Select ROM");
clicked => {
select_rom(NetplayData.rom_dir);
}
}
if NetplayData.game_name != "": Button {
text: NetplayData.game_name;
clicked => {
select_rom(NetplayData.rom_dir);
}
}
}
create_session(
server_url,
NetplayData.session_name,
NetplayData.player_name,
NetplayData.game_name,
NetplayData.game_hash,
NetplayData.game_cheats,
NetplayData.password);
Rectangle { }
if NetplayData.game_cheats != "":Text {
text: @tr("This game has cheats enabled. Cheats will be synchronized with other players.");
}
Rectangle { }
Button {
property <string> server_url;
text: @tr("Create Session");
enabled: NetplayData.session_name != "" && NetplayData.player_name != "" && NetplayData.game_name != "" && !NetplayData.pending_session;
clicked => {
NetplayData.pending_session = true;
if (server.current-value == "Custom") {
server_url = "ws://" + NetplayData.custom_server_url;
} else {
server_url = NetplayData.server_urls[server.current-index];
}
create_session(
server_url,
NetplayData.session_name,
NetplayData.player_name,
NetplayData.game_name,
NetplayData.game_hash,
NetplayData.game_cheats,
NetplayData.password);
}
}
Rectangle { }
}
}
}
@@ -127,7 +142,13 @@ export component NetplayCreate inherits Window {
export component DispatcherDialog inherits Dialog {
title: @tr("Creating Server");
Text {
text: @tr("Creating server, please wait...This may take about 30 seconds.");
VerticalBox {
padding-left: root.safe-area-insets.left;
padding-top: root.safe-area-insets.top;
padding-right: root.safe-area-insets.right;
padding-bottom: root.safe-area-insets.bottom;
Text {
text: @tr("Creating server, please wait...This may take about 30 seconds.");
}
}
}
+108 -95
View File
@@ -1,4 +1,4 @@
import { VerticalBox, LineEdit, StandardTableView, HorizontalBox, ComboBox, Button } from "std-widgets.slint";
import { VerticalBox, LineEdit, StandardTableView, HorizontalBox, ComboBox, Button, ScrollView } from "std-widgets.slint";
import { NetplayData } from "netplay_page.slint";
export component NetplayJoin inherits Window {
@@ -22,116 +22,129 @@ export component NetplayJoin inherits Window {
callback select_rom(string);
callback join_session(string, string, string, string, int);
preferred-width: 700px;
preferred-height: 450px;
VerticalBox {
HorizontalBox {
vertical-stretch: 1;
padding-left: root.safe-area-insets.left;
padding-top: root.safe-area-insets.top;
padding-right: root.safe-area-insets.right;
padding-bottom: root.safe-area-insets.bottom;
ScrollView {
mouse-drag-pan-enabled: true;
vertical-stretch: 1;
VerticalBox {
Text {
text: @tr("Player Name (required):");
}
alignment: start;
HorizontalBox {
VerticalBox {
Text {
text: @tr("Player Name (required):");
}
LineEdit {
placeholder-text: @tr("Enter player name");
edited(text) => {
NetplayData.player_name = text;
LineEdit {
placeholder-text: @tr("Enter player name");
edited(text) => {
NetplayData.player_name = text;
}
}
}
VerticalBox {
Text {
text: @tr("Custom Server URL (optional):");
}
LineEdit {
placeholder-text: "example.com:45000";
text: NetplayData.custom_server_url;
edited(text) => {
NetplayData.custom_server_url = text;
}
}
}
}
}
VerticalBox {
Text {
text: @tr("Custom Server URL (optional):");
if NetplayData.pending_refresh: Text {
text: @tr("Refreshing session list, please wait...");
horizontal-alignment: center;
vertical-alignment: center;
}
LineEdit {
placeholder-text: "example.com:45000";
text: NetplayData.custom_server_url;
edited(text) => {
NetplayData.custom_server_url = text;
if !NetplayData.pending_refresh && NetplayData.sessions.length > 0: Text {
text: @tr("Click on a session below to select it");
horizontal-alignment: center;
vertical-alignment: center;
}
if !NetplayData.pending_refresh && NetplayData.sessions.length == 0: Text {
text: @tr("No sessions available. Please refresh to check again.");
horizontal-alignment: center;
vertical-alignment: center;
}
StandardTableView {
columns: [
{ title: @tr("Server") },
{ title: @tr("Session Name") },
{ title: @tr("Game Name") },
{ title: @tr("Password") },
{ title: @tr("Cheats") }
];
rows: NetplayData.sessions;
current-row: NetplayData.current_session;
current-row-changed(current-row) => {
NetplayData.current_session = current-row;
}
}
}
}
if NetplayData.pending_refresh: Text {
text: @tr("Refreshing session list, please wait...");
horizontal-alignment: center;
vertical-alignment: center;
}
if !NetplayData.pending_refresh && NetplayData.sessions.length > 0: Text {
text: @tr("Click on a session below to select it");
horizontal-alignment: center;
vertical-alignment: center;
}
if !NetplayData.pending_refresh && NetplayData.sessions.length == 0: Text {
text: @tr("No sessions available. Please refresh to check again.");
horizontal-alignment: center;
vertical-alignment: center;
}
StandardTableView {
columns: [
{ title: @tr("Server") },
{ title: @tr("Session Name") },
{ title: @tr("Game Name") },
{ title: @tr("Password") },
{ title: @tr("Cheats") }
];
rows: NetplayData.sessions;
current-row: NetplayData.current_session;
current-row-changed(current-row) => {
NetplayData.current_session = current-row;
}
}
HorizontalBox {
Rectangle { }
HorizontalBox {
Rectangle { }
Button {
enabled: !NetplayData.pending_refresh;
text: @tr("Refresh Session List");
clicked => {
NetplayData.pending_refresh = true;
NetplayData.current_session = -1;
NetplayData.sessions = [];
NetplayData.room_urls = [];
NetplayData.room_ports = [];
refresh_session();
Button {
enabled: !NetplayData.pending_refresh;
text: @tr("Refresh Session List");
clicked => {
NetplayData.pending_refresh = true;
NetplayData.current_session = -1;
NetplayData.sessions = [];
NetplayData.room_urls = [];
NetplayData.room_ports = [];
refresh_session();
}
}
}
}
}
HorizontalBox {
LineEdit {
placeholder-text: @tr("Password (if required)");
edited(text) => {
NetplayData.password = text;
}
}
HorizontalBox {
LineEdit {
placeholder-text: @tr("Password (if required)");
edited(text) => {
NetplayData.password = text;
}
}
if NetplayData.game_name == "": Button {
text: @tr("Select ROM");
clicked => {
select_rom(NetplayData.rom_dir);
}
}
if NetplayData.game_name != "": Button {
text: NetplayData.game_name;
clicked => {
select_rom(NetplayData.rom_dir);
}
}
if NetplayData.game_name == "": Button {
text: @tr("Select ROM");
clicked => {
select_rom(NetplayData.rom_dir);
}
}
if NetplayData.game_name != "": Button {
text: NetplayData.game_name;
clicked => {
select_rom(NetplayData.rom_dir);
}
}
Button {
text: @tr("Join Session");
enabled: NetplayData.player_name != "" && NetplayData.current_session >= 0 && NetplayData.game_name != "" && !NetplayData.pending_session;
clicked => {
NetplayData.pending_session = true;
join_session(
NetplayData.player_name,
NetplayData.game_hash,
NetplayData.password,
NetplayData.room_urls[NetplayData.current_session],
NetplayData.room_ports[NetplayData.current_session]);
Button {
text: @tr("Join Session");
enabled: NetplayData.player_name != "" && NetplayData.current_session >= 0 && NetplayData.game_name != "" && !NetplayData.pending_session;
clicked => {
NetplayData.pending_session = true;
join_session(
NetplayData.player_name,
NetplayData.game_hash,
NetplayData.password,
NetplayData.room_urls[NetplayData.current_session],
NetplayData.room_ports[NetplayData.current_session]);
}
}
}
}
}
+13 -3
View File
@@ -82,12 +82,22 @@ export component CustomNetplayServer inherits Dialog {
title: @tr("Custom Netplay Server");
preferred-width: 400px;
callback ok_clicked(string);
server_url := LineEdit {
text: NetplayData.custom_server_url;
placeholder-text: "example.com:45000";
VerticalBox {
alignment: start;
vertical-stretch: 1;
padding-left: root.safe-area-insets.left;
padding-top: root.safe-area-insets.top;
padding-right: root.safe-area-insets.right;
padding-bottom: root.safe-area-insets.bottom;
server_url := LineEdit {
vertical-stretch: 0;
text: NetplayData.custom_server_url;
placeholder-text: "example.com:45000";
}
}
StandardButton {
vertical-stretch: 0;
kind: ok;
clicked => {
NetplayData.custom_server_url = server_url.text;
+80 -64
View File
@@ -1,4 +1,4 @@
import { VerticalBox, HorizontalBox, TextEdit, LineEdit, Button, GridBox } from "std-widgets.slint";
import { VerticalBox, HorizontalBox, TextEdit, LineEdit, Button, GridBox, ScrollView } from "std-widgets.slint";
import { NetplayData } from "netplay_page.slint";
export global NetplayWaitData {
@@ -12,8 +12,8 @@ export global NetplayWaitData {
export component NetplayWait inherits Window {
title: @tr("Netplay: Waiting Room");
preferred-width: 400px;
preferred-height: 400px;
preferred-width: 500px;
preferred-height: 450px;
in-out property players <=> NetplayWaitData.players;
in-out property session_name <=> NetplayData.session_name;
in-out property game_name <=> NetplayData.game_name;
@@ -27,83 +27,99 @@ export component NetplayWait inherits Window {
callback begin_game;
VerticalBox {
Text {
text: @tr("Session Name:") + " " + NetplayData.session_name;
font-size: 15px;
horizontal-alignment: center;
}
Text {
text: @tr("Game Name:") + " " + NetplayData.game_name;
font-size: 15px;
horizontal-alignment: center;
}
Text {
text: @tr("Ping:") + " " + NetplayWaitData.ping;
horizontal-alignment: center;
}
GridBox {
Row {
vertical-stretch: 1;
padding-left: root.safe-area-insets.left;
padding-top: root.safe-area-insets.top;
padding-right: root.safe-area-insets.right;
padding-bottom: root.safe-area-insets.bottom;
ScrollView {
mouse-drag-pan-enabled: true;
vertical-stretch: 1;
VerticalBox {
alignment: start;
Text {
text: @tr("Player 1:") + " " + players[0];
text: @tr("Session Name:") + " " + NetplayData.session_name;
font-size: 15px;
horizontal-alignment: center;
}
Text {
text: @tr("Player 2:") + " " + players[1];
}
}
Row {
Text {
text: @tr("Player 3:") + " " + players[2];
text: @tr("Game Name:") + " " + NetplayData.game_name;
font-size: 15px;
horizontal-alignment: center;
}
Text {
text: @tr("Player 4:") + " " + players[3];
text: @tr("Ping:") + " " + NetplayWaitData.ping;
horizontal-alignment: center;
}
}
}
TextEdit {
read-only: true;
text: NetplayWaitData.chat_text;
}
GridBox {
Row {
Text {
text: @tr("Player 1:") + " " + players[0];
}
HorizontalBox {
chat_message := LineEdit {
placeholder-text: @tr("Enter chat message");
key-released(event) => {
if (event.text == Key.Return && chat_message.text != "") {
send_chat_message(chat_message.text);
chat_message.text = "";
Text {
text: @tr("Player 2:") + " " + players[1];
}
}
Row {
Text {
text: @tr("Player 3:") + " " + players[2];
}
Text {
text: @tr("Player 4:") + " " + players[3];
}
}
accept
}
}
Button {
text: @tr("Send Message");
enabled: chat_message.text != "";
clicked => {
send_chat_message(chat_message.text);
chat_message.text = "";
TextEdit {
read-only: true;
min-height: 120px;
text: NetplayWaitData.chat_text;
}
}
}
Text {
text: NetplayWaitData.motd;
wrap: word-wrap;
}
HorizontalBox {
chat_message := LineEdit {
placeholder-text: @tr("Enter chat message");
key-released(event) => {
if (event.text == Key.Return && chat_message.text != "") {
send_chat_message(chat_message.text);
chat_message.text = "";
}
accept
}
}
Button {
text: @tr("Start Game");
enabled: NetplayWaitData.can_start;
clicked => {
NetplayWaitData.can_start = false;
begin_game();
Button {
text: @tr("Send Message");
enabled: chat_message.text != "";
clicked => {
send_chat_message(chat_message.text);
chat_message.text = "";
}
}
}
Text {
text: NetplayWaitData.motd;
wrap: word-wrap;
}
Button {
text: @tr("Start Game");
enabled: NetplayWaitData.can_start;
clicked => {
NetplayWaitData.can_start = false;
begin_game();
}
}
Rectangle {
vertical-stretch: 0;
}
}
}
}
+1 -1
View File
@@ -108,7 +108,7 @@ export component RetroAchievements inherits Page {
edited(text) => {
RAData.ra_username = text;
}
}
password_widget := LineEdit {
enabled: RAData.enabled;
+14 -14
View File
@@ -46,20 +46,20 @@ component SideBarButton inherits Rectangle {
}
states [
selected when root.selected: {
highlight.opacity: 1;
highlight.background: Palette.accent-background;
icon-display.colorize: Palette.accent-foreground;
}
pressed when touch.pressed && !root.selected: {
highlight.opacity: 0.55;
}
hover when touch.has-hover && !root.selected: {
highlight.opacity: 0.4;
}
focused when root.has-focus && !root.selected: {
highlight.opacity: 0.4;
}
selected when root.selected: {
highlight.opacity: 1;
highlight.background: Palette.accent-background;
icon-display.colorize: Palette.accent-foreground;
}
pressed when touch.pressed && !root.selected: {
highlight.opacity: 0.55;
}
hover when touch.has-hover && !root.selected: {
highlight.opacity: 0.4;
}
focused when root.has-focus && !root.selected: {
highlight.opacity: 0.4;
}
]
}