make to_server() blocking, post fixes & cleanup, use server address

Former-commit-id: 7debc790e158a996ed58271d3307214e42b850bd
This commit is contained in:
Imbris 2019-04-04 09:49:31 -04:00 committed by Pfauenauge90
parent c10564d436
commit fb83c836a1
8 changed files with 50 additions and 548 deletions

4
.gitignore vendored
View File

@ -20,7 +20,5 @@
**/server_conf.toml **/server_conf.toml
**/keybinds.toml **/keybinds.toml
assets/voxygen assets/voxygen
UI3.rar
assets.rar
*.rar *.rar
assets/voxygen

@ -1 +1 @@
Subproject commit e3083ec8e8e634af8c9daed00ea82435da195979 Subproject commit 6b64a65356c1fb4c77ad0295908f4006f75a5448

View File

@ -1,74 +0,0 @@
#[derive(Debug)]
pub enum PostError {
InvalidMessage,
InternalError,
Disconnected,
}
#[derive(Debug)]
pub enum PostErrorInternal {
Io(std::io::Error),
Serde(bincode::Error),
ChannelRecv(std::sync::mpsc::TryRecvError),
ChannelSend, // Empty because I couldn't figure out how to handle generic type in mpsc::TrySendError properly
MsgSizeLimitExceeded,
MioError,
}
impl<'a, T: Into<&'a PostErrorInternal>> From<T> for PostError {
fn from(err: T) -> Self {
match err.into() {
// TODO: Are I/O errors always disconnect errors?
PostErrorInternal::Io(_) => PostError::Disconnected,
PostErrorInternal::Serde(_) => PostError::InvalidMessage,
PostErrorInternal::MsgSizeLimitExceeded => PostError::InvalidMessage,
PostErrorInternal::MioError => PostError::InternalError,
PostErrorInternal::ChannelRecv(_) => PostError::InternalError,
PostErrorInternal::ChannelSend => PostError::InternalError,
}
}
}
impl From<PostErrorInternal> for PostError {
fn from(err: PostErrorInternal) -> Self {
(&err).into()
}
}
impl From<std::io::Error> for PostErrorInternal {
fn from(err: std::io::Error) -> Self {
PostErrorInternal::Io(err)
}
}
impl From<bincode::Error> for PostErrorInternal {
fn from(err: bincode::Error) -> Self {
PostErrorInternal::Serde(err)
}
}
impl From<std::sync::mpsc::TryRecvError> for PostErrorInternal {
fn from(err: std::sync::mpsc::TryRecvError) -> Self {
PostErrorInternal::ChannelRecv(err)
}
}
impl From<std::io::Error> for PostError {
fn from(err: std::io::Error) -> Self {
(&PostErrorInternal::from(err)).into()
}
}
impl From<bincode::Error> for PostError {
fn from(err: bincode::Error) -> Self {
(&PostErrorInternal::from(err)).into()
}
}
impl From<std::sync::mpsc::TryRecvError> for PostError {
fn from(err: std::sync::mpsc::TryRecvError) -> Self {
(&PostErrorInternal::from(err)).into()
}
}

View File

@ -1,270 +0,0 @@
// Standard
use std::{
collections::VecDeque,
convert::TryFrom,
io::{
ErrorKind,
Read,
},
net::SocketAddr,
thread,
time::Duration,
sync::mpsc::TryRecvError,
};
// External
use bincode;
use mio::{net::TcpStream, Events, Poll, PollOpt, Ready, Token};
use mio_extras::channel::{channel, Receiver, Sender};
// Crate
use super::{
data::ControlMsg,
error::{
PostError,
PostErrorInternal,
},
PostRecv,
PostSend,
};
// Constants
const CTRL_TOKEN: Token = Token(0); // Token for thread control messages
const DATA_TOKEN: Token = Token(1); // Token for thread data exchange
const CONN_TOKEN: Token = Token(2); // Token for TcpStream for the PostBox child thread
const MESSAGE_SIZE_CAP: u64 = 1 << 20; // Maximum accepted length of a packet
/// A high-level wrapper of [`TcpStream`](mio::net::TcpStream).
/// [`PostBox`] takes care of serializing sent packets and deserializing received packets in the background, providing a simple API for sending and receiving objects over network.
pub struct PostBox<S, R>
where
S: PostSend,
R: PostRecv,
{
handle: Option<thread::JoinHandle<()>>,
ctrl: Sender<ControlMsg>,
recv: Receiver<Result<R, PostErrorInternal>>,
send: Sender<S>,
poll: Poll,
err: Option<PostErrorInternal>,
}
impl<S, R> PostBox<S, R>
where
S: PostSend,
R: PostRecv,
{
/// Creates a new [`PostBox`] connected to specified address, meant to be used by the client
pub fn to_server<A: Into<SocketAddr>>(addr: A) -> Result<PostBox<S, R>, PostError> {
let connection = TcpStream::connect(&addr.into())?;
Self::from_tcpstream(connection)
}
/// Creates a new [`PostBox`] from an existing connection, meant to be used by [`PostOffice`](super::PostOffice) on the server
pub fn from_tcpstream(connection: TcpStream) -> Result<PostBox<S, R>, PostError> {
let (ctrl_tx, ctrl_rx) = channel(); // Control messages
let (send_tx, send_rx) = channel(); // main thread -[data to be serialized and sent]> worker thread
let (recv_tx, recv_rx) = channel(); // main thread <[received and deserialized data]- worker thread
let thread_poll = Poll::new().unwrap();
let postbox_poll = Poll::new().unwrap();
thread_poll
.register(&connection, CONN_TOKEN, Ready::readable(), PollOpt::edge())
.unwrap();
thread_poll
.register(&ctrl_rx, CTRL_TOKEN, Ready::readable(), PollOpt::edge())
.unwrap();
thread_poll
.register(&send_rx, DATA_TOKEN, Ready::readable(), PollOpt::edge())
.unwrap();
postbox_poll
.register(&recv_rx, DATA_TOKEN, Ready::readable(), PollOpt::edge())
.unwrap();
let handle = thread::Builder::new()
.name("postbox_worker".into())
.spawn(move || postbox_thread(connection, ctrl_rx, send_rx, recv_tx, thread_poll))?;
Ok(PostBox {
handle: Some(handle),
ctrl: ctrl_tx,
recv: recv_rx,
send: send_tx,
poll: postbox_poll,
err: None,
})
}
/// Return an `Option<PostError>` indicating the current status of the `PostBox`.
pub fn status(&self) -> Option<PostError> {
self.err.as_ref().map(|err| err.into())
}
/// Non-blocking sender method
pub fn send(&mut self, data: S) -> Result<(), PostError> {
match &mut self.err {
err @ None => if let Err(_) = self.send.send(data) {
*err = Some(PostErrorInternal::MioError);
Err(err.as_ref().unwrap().into())
} else {
Ok(())
},
err => Err(err.as_ref().unwrap().into()),
}
}
/// Non-blocking receiver method returning an iterator over already received and deserialized objects
/// # Errors
/// If the other side disconnects PostBox won't realize that until you try to send something
pub fn new_messages(&mut self) -> impl ExactSizeIterator<Item = R> {
let mut events = Events::with_capacity(4096);
let mut items = VecDeque::new();
// If an error occured, or previously occured, just give up
if let Some(_) = self.err {
return items.into_iter();
} else if let Err(err) = self.poll.poll(&mut events, Some(Duration::new(0, 0))) {
self.err = Some(err.into());
return items.into_iter();
}
for event in events {
match event.token() {
DATA_TOKEN => loop {
match self.recv.try_recv() {
Ok(Ok(item)) => items.push_back(item),
Err(TryRecvError::Empty) => break,
Err(err) => self.err = Some(err.into()),
Ok(Err(err)) => self.err = Some(err.into()),
}
},
_ => (),
}
}
items.into_iter()
}
}
fn postbox_thread<S, R>(
mut connection: TcpStream,
ctrl_rx: Receiver<ControlMsg>,
send_rx: Receiver<S>,
recv_tx: Sender<Result<R, PostErrorInternal>>,
poll: Poll,
) where
S: PostSend,
R: PostRecv,
{
// Receiving related variables
let mut events = Events::with_capacity(64);
let mut recv_buff = Vec::new();
let mut recv_nextlen: u64 = 0;
loop {
let mut disconnected = false;
poll.poll(&mut events, Some(Duration::from_millis(20)))
.expect("Failed to execute poll(), most likely fault of the OS");
println!("FINISHED POLL!");
for event in events.iter() {
println!("EVENT!");
match event.token() {
CTRL_TOKEN => match ctrl_rx.try_recv().unwrap() {
ControlMsg::Shutdown => return,
},
CONN_TOKEN => match connection.read_to_end(&mut recv_buff) {
Ok(_) => {}
// Returned when all the data has been read
Err(ref e) if e.kind() == ErrorKind::WouldBlock => {}
Err(e) => recv_tx.send(Err(e.into())).unwrap(),
},
DATA_TOKEN => {
let msg = send_rx.try_recv().unwrap();
println!("Send: {:?}", msg);
let mut packet = bincode::serialize(&msg).unwrap();
packet.splice(0..0, (packet.len() as u64).to_be_bytes().iter().cloned());
match connection.write_bufs(&[packet.as_slice().into()]) {
Ok(_) => { println!("Sent!"); }
Err(e) => {
println!("Send error!");
recv_tx.send(Err(e.into())).unwrap();
}
};
}
_ => {}
}
}
loop {
if recv_nextlen == 0 && recv_buff.len() >= 8 {
println!("Read nextlen");
recv_nextlen = u64::from_be_bytes(
<[u8; 8]>::try_from(recv_buff.drain(0..8).collect::<Vec<u8>>().as_slice()).unwrap(),
);
if recv_nextlen > MESSAGE_SIZE_CAP {
recv_tx.send(Err(PostErrorInternal::MsgSizeLimitExceeded)).unwrap();
connection.shutdown(std::net::Shutdown::Both).unwrap();
recv_buff.drain(..);
recv_nextlen = 0;
break;
}
}
if recv_buff.len() as u64 >= recv_nextlen && recv_nextlen != 0 {
match bincode::deserialize(recv_buff
.drain(
0..usize::try_from(recv_nextlen)
.expect("Message size was larger than usize (insane message size and 32 bit OS)"),
)
.collect::<Vec<u8>>()
.as_slice()) {
Ok(msg) => {
println!("Recv: {:?}", msg);
recv_tx
.send(Ok(msg))
.unwrap();
recv_nextlen = 0;
}
Err(e) => {
println!("Recv error: {:?}", e);
recv_tx.send(Err(e.into())).unwrap();
recv_nextlen = 0;
}
}
} else {
break;
}
}
match connection.take_error().unwrap() {
Some(e) => {
if e.kind() == ErrorKind::BrokenPipe {
disconnected = true;
}
recv_tx.send(Err(e.into())).unwrap();
}
None => {}
}
if disconnected == true {
break;
}
}
// Loop after disconnected
loop {
poll.poll(&mut events, None)
.expect("Failed to execute poll(), most likely fault of the OS");
for event in events.iter() {
match event.token() {
CTRL_TOKEN => match ctrl_rx.try_recv().unwrap() {
ControlMsg::Shutdown => return,
},
_ => {}
}
}
}
}
impl<S, R> Drop for PostBox<S, R>
where
S: PostSend,
R: PostRecv,
{
fn drop(&mut self) {
self.ctrl.send(ControlMsg::Shutdown).unwrap_or(());
self.handle.take().map(|handle| handle.join());
}
}

View File

@ -1,150 +0,0 @@
// Standard
use core::time::Duration;
use std::{
collections::VecDeque,
net::SocketAddr,
thread,
sync::mpsc::TryRecvError,
};
// External
use mio::{net::TcpListener, Events, Poll, PollOpt, Ready, Token};
use mio_extras::channel::{channel, Receiver, Sender};
// Crate
use super::{
data::ControlMsg,
error::{
PostError,
PostErrorInternal,
},
postbox::PostBox,
PostRecv,
PostSend,
};
// Constants
const CTRL_TOKEN: Token = Token(0); // Token for thread control messages
const DATA_TOKEN: Token = Token(1); // Token for thread data exchange
const CONN_TOKEN: Token = Token(2); // Token for TcpStream for the PostBox child thread
/// A high-level wrapper of [`TcpListener`](mio::net::TcpListener).
/// [`PostOffice`] listens for incoming connections in the background and wraps them into [`PostBox`]es, providing a simple non-blocking API for receiving them.
pub struct PostOffice<S, R>
where
S: PostSend,
R: PostRecv,
{
handle: Option<thread::JoinHandle<()>>,
ctrl: Sender<ControlMsg>,
recv: Receiver<Result<PostBox<S, R>, PostErrorInternal>>,
poll: Poll,
err: Option<PostErrorInternal>,
}
impl<S, R> PostOffice<S, R>
where
S: PostSend,
R: PostRecv,
{
/// Creates a new [`PostOffice`] listening on specified address
pub fn new<A: Into<SocketAddr>>(addr: A) -> Result<Self, PostError> {
let listener = TcpListener::bind(&addr.into())?;
let (ctrl_tx, ctrl_rx) = channel();
let (recv_tx, recv_rx) = channel();
let thread_poll = Poll::new()?;
let postbox_poll = Poll::new()?;
thread_poll.register(&listener, CONN_TOKEN, Ready::readable(), PollOpt::edge())?;
thread_poll.register(&ctrl_rx, CTRL_TOKEN, Ready::readable(), PollOpt::edge())?;
postbox_poll.register(&recv_rx, DATA_TOKEN, Ready::readable(), PollOpt::edge())?;
let handle = thread::Builder::new()
.name("postoffice_worker".into())
.spawn(move || postoffice_thread(listener, ctrl_rx, recv_tx, thread_poll))?;
Ok(PostOffice {
handle: Some(handle),
ctrl: ctrl_tx,
recv: recv_rx,
poll: postbox_poll,
err: None,
})
}
/// Return an `Option<PostError>` indicating the current status of the `PostOffice`.
pub fn status(&self) -> Option<PostError> {
self.err.as_ref().map(|err| err.into())
}
/// Non-blocking method returning an iterator over new connections wrapped in [`PostBox`]es
pub fn new_connections(
&mut self,
) -> impl ExactSizeIterator<Item = PostBox<S, R>> {
let mut events = Events::with_capacity(256);
let mut conns = VecDeque::new();
// If an error occured, or previously occured, just give up
if let Some(_) = self.err {
return conns.into_iter();
} else if let Err(err) = self.poll.poll(&mut events, Some(Duration::new(0, 0))) {
self.err = Some(err.into());
return conns.into_iter();
}
for event in events {
match event.token() {
DATA_TOKEN => loop {
match self.recv.try_recv() {
Ok(Ok(conn)) => conns.push_back(conn),
Err(TryRecvError::Empty) => break,
Err(err) => self.err = Some(err.into()),
Ok(Err(err)) => self.err = Some(err.into()),
}
},
_ => (),
}
}
conns.into_iter()
}
}
fn postoffice_thread<S, R>(
listener: TcpListener,
ctrl_rx: Receiver<ControlMsg>,
recv_tx: Sender<Result<PostBox<S, R>, PostErrorInternal>>,
poll: Poll,
) where
S: PostSend,
R: PostRecv,
{
let mut events = Events::with_capacity(256);
loop {
poll.poll(&mut events, None).expect("Failed to execute recv_poll.poll() in PostOffce receiver thread, most likely fault of the OS.");
for event in events.iter() {
match event.token() {
CTRL_TOKEN => match ctrl_rx.try_recv().unwrap() {
ControlMsg::Shutdown => return,
},
CONN_TOKEN => {
let (conn, _addr) = listener.accept().unwrap();
recv_tx.send(PostBox::from_tcpstream(conn)
// TODO: Is it okay to count a failure to create a postbox here as an 'internal error'?
.map_err(|_| PostErrorInternal::MioError)).unwrap();
}
_ => (),
}
}
}
}
impl<S, R> Drop for PostOffice<S, R>
where
S: PostSend,
R: PostRecv,
{
fn drop(&mut self) {
self.ctrl.send(ControlMsg::Shutdown).unwrap_or(()); // If this fails the thread is dead already
self.handle.take().map(|handle| handle.join());
}
}

View File

@ -7,11 +7,11 @@ use crate::{
}; };
use common::assets; use common::assets;
use conrod_core::{ use conrod_core::{
color, Color, color,
image::Id as ImgId, image::Id as ImgId,
text::font::Id as FontId, text::font::Id as FontId,
widget::{Button, Image, Rectangle, Scrollbar, Text}, widget::{Button, Image, Rectangle, Scrollbar, Text},
widget_ids, Colorable, Labelable, Positionable, Sizeable, Widget, widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, Widget,
}; };
widget_ids! { widget_ids! {
@ -246,13 +246,13 @@ pub(self) struct Imgs {
impl Imgs { impl Imgs {
fn new(ui: &mut Ui, renderer: &mut Renderer) -> Imgs { fn new(ui: &mut Ui, renderer: &mut Renderer) -> Imgs {
let mut load = |filename| { let mut load = |filename| {
let fullpath: String = [ let fullpath: String = ["/voxygen/", filename].concat();
"/voxygen/",
filename,
].concat();
let image = image::load_from_memory( let image = image::load_from_memory(
assets::load(fullpath.as_str()).expect("Error loading file").as_slice() assets::load(fullpath.as_str())
).unwrap(); .expect("Error loading file")
.as_slice(),
)
.unwrap();
ui.new_image(renderer, &image).unwrap() ui.new_image(renderer, &image).unwrap()
}; };
Imgs { Imgs {
@ -422,7 +422,7 @@ pub struct Hud {
//#[inline] //#[inline]
//pub fn rgba_bytes(r: u8, g: u8, b: u8, a: f32) -> Color { //pub fn rgba_bytes(r: u8, g: u8, b: u8, a: f32) -> Color {
//Color::Rgba(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a) //Color::Rgba(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a)
//} //}
impl Hud { impl Hud {
@ -479,12 +479,11 @@ impl Hud {
let mut events = Vec::new(); let mut events = Vec::new();
let ref mut ui_widgets = self.ui.set_widgets(); let ref mut ui_widgets = self.ui.set_widgets();
const TEXT_COLOR: Color = Color::Rgba(0.86, 0.86, 0.86, 0.8); const TEXT_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0);
const HP_COLOR: Color = Color::Rgba(0.33, 0.63, 0.0, 1.0); const HP_COLOR: Color = Color::Rgba(0.33, 0.63, 0.0, 1.0);
const MANA_COLOR: Color = Color::Rgba(0.42, 0.41, 0.66, 1.0); const MANA_COLOR: Color = Color::Rgba(0.42, 0.41, 0.66, 1.0);
const XP_COLOR: Color = Color::Rgba(0.59, 0.41, 0.67, 1.0); const XP_COLOR: Color = Color::Rgba(0.59, 0.41, 0.67, 1.0);
if self.show_ui { if self.show_ui {
// Add Bag-Space Button // Add Bag-Space Button
if self.inventorytest_button { if self.inventorytest_button {
@ -753,7 +752,6 @@ impl Hud {
.top_right_with_margins_on(self.ids.health_bar, 5.0, 0.0) .top_right_with_margins_on(self.ids.health_bar, 5.0, 0.0)
.set(self.ids.health_bar_color, ui_widgets); .set(self.ids.health_bar_color, ui_widgets);
// Mana Bar // Mana Bar
Image::new(self.imgs.mana_bar) Image::new(self.imgs.mana_bar)
.w_h(1120.0 / 6.0, 96.0 / 6.0) .w_h(1120.0 / 6.0, 96.0 / 6.0)
@ -766,7 +764,6 @@ impl Hud {
.top_left_with_margins_on(self.ids.mana_bar, 5.0, 0.0) .top_left_with_margins_on(self.ids.mana_bar, 5.0, 0.0)
.set(self.ids.mana_bar_color, ui_widgets); .set(self.ids.mana_bar_color, ui_widgets);
// Buffs/Debuffs // Buffs/Debuffs
// Buffs // Buffs

View File

@ -225,13 +225,13 @@ struct Imgs {
impl Imgs { impl Imgs {
fn new(ui: &mut Ui, renderer: &mut Renderer) -> Imgs { fn new(ui: &mut Ui, renderer: &mut Renderer) -> Imgs {
let mut load = |filename| { let mut load = |filename| {
let fullpath: String = [ let fullpath: String = ["/voxygen/", filename].concat();
"/voxygen/",
filename,
].concat();
let image = image::load_from_memory( let image = image::load_from_memory(
assets::load(fullpath.as_str()).expect("Error loading file").as_slice() assets::load(fullpath.as_str())
).unwrap(); .expect("Error loading file")
.as_slice(),
)
.unwrap();
ui.new_image(renderer, &image).unwrap() ui.new_image(renderer, &image).unwrap()
}; };
Imgs { Imgs {
@ -345,7 +345,7 @@ pub enum Event {
Play, Play,
} }
const TEXT_COLOR: Color = Color::Rgba(0.86, 0.86, 0.86, 0.8); const TEXT_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0);
pub struct CharSelectionUi { pub struct CharSelectionUi {
ui: Ui, ui: Ui,
@ -495,7 +495,6 @@ impl CharSelectionUi {
.color(TEXT_COLOR) .color(TEXT_COLOR)
.set(self.ids.char_level, ui_widgets); .set(self.ids.char_level, ui_widgets);
// Selected Character // Selected Character
if no == 1 { if no == 1 {
Image::new(self.imgs.test_char_l_big) Image::new(self.imgs.test_char_l_big)
@ -872,11 +871,11 @@ impl CharSelectionUi {
Their greatest strengths are their adaptability and intelligence, which makes them allrounders in many fields."; Their greatest strengths are their adaptability and intelligence, which makes them allrounders in many fields.";
const ORC_DESC: &str = const ORC_DESC: &str =
"They are considered brutal, rude and combative. \n\ "They are considered brutal, rude and combative. \n\
But once you gained their trust they will be loyal friends \n\ But once you gained their trust they will be loyal friends \n\
that follow a strict code of honor in all of their actions. \n\ that follow a strict code of honor in all of their actions. \n\
\n\ \n\
Their warriors are masters of melee combat, but their true power \ Their warriors are masters of melee combat, but their true power \
comes from the magical rituals of their powerful shamans."; comes from the magical rituals of their powerful shamans.";
const DWARF_DESC: &str = const DWARF_DESC: &str =
"Smoking chimneys, the sound of countless hammers and hoes. \ "Smoking chimneys, the sound of countless hammers and hoes. \
Infinite tunnel systems to track down even the last chunk of metal in the ground. \n\ Infinite tunnel systems to track down even the last chunk of metal in the ground. \n\

View File

@ -10,8 +10,7 @@ use conrod_core::{
position::Dimension, position::Dimension,
text::font::Id as FontId, text::font::Id as FontId,
widget::{text_box::Event as TextBoxEvent, Button, Image, Rectangle, Text, TextBox}, widget::{text_box::Event as TextBoxEvent, Button, Image, Rectangle, Text, TextBox},
widget_ids, Borderable, Color, widget_ids, Borderable, Color, Colorable, Labelable, Positionable, Sizeable, Widget,
Colorable, Labelable, Positionable, Sizeable, Widget,
}; };
widget_ids! { widget_ids! {
@ -58,13 +57,13 @@ impl Imgs {
fn new(ui: &mut Ui, renderer: &mut Renderer) -> Imgs { fn new(ui: &mut Ui, renderer: &mut Renderer) -> Imgs {
// TODO: update paths // TODO: update paths
let mut load = |filename| { let mut load = |filename| {
let fullpath: String = [ let fullpath: String = ["/voxygen/", filename].concat();
"/voxygen/",
filename,
].concat();
let image = image::load_from_memory( let image = image::load_from_memory(
assets::load(fullpath.as_str()).expect("Error loading file").as_slice() assets::load(fullpath.as_str())
).unwrap(); .expect("Error loading file")
.as_slice(),
)
.unwrap();
ui.new_image(renderer, &image).unwrap() ui.new_image(renderer, &image).unwrap()
}; };
Imgs { Imgs {
@ -170,7 +169,7 @@ impl MainMenuUi {
}); });
}; };
} }
const TEXT_COLOR: Color = Color::Rgba(0.94, 0.94, 0.94, 0.8); const TEXT_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0);
// Username // Username
// TODO: get a lower resolution and cleaner input_bg.png // TODO: get a lower resolution and cleaner input_bg.png
Image::new(self.imgs.input_bg) Image::new(self.imgs.input_bg)
@ -193,7 +192,9 @@ impl MainMenuUi {
// Note: TextBox limits the input string length to what fits in it // Note: TextBox limits the input string length to what fits in it
self.username = username.to_string(); self.username = username.to_string();
} }
TextBoxEvent::Enter => { login!(); } TextBoxEvent::Enter => {
login!();
}
} }
} }
// Login error // Login error
@ -211,8 +212,7 @@ impl MainMenuUi {
.parent(ui_widgets.window) .parent(ui_widgets.window)
.up_from(self.ids.username_bg, 35.0) .up_from(self.ids.username_bg, 35.0)
.set(self.ids.login_error_bg, ui_widgets); .set(self.ids.login_error_bg, ui_widgets);
text text.middle_of(self.ids.login_error_bg)
.middle_of(self.ids.login_error_bg)
.set(self.ids.login_error, ui_widgets); .set(self.ids.login_error, ui_widgets);
} }
// Server address // Server address
@ -235,7 +235,9 @@ impl MainMenuUi {
TextBoxEvent::Update(server_address) => { TextBoxEvent::Update(server_address) => {
self.server_address = server_address.to_string(); self.server_address = server_address.to_string();
} }
TextBoxEvent::Enter => { login!(); } TextBoxEvent::Enter => {
login!();
}
} }
} }
// Login button // Login button