1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
use std::time::Duration;
use eframe::egui;
use egui_async::{Bind, EguiAsyncPlugin, StateWithData};
fn main() -> eframe::Result {
let native_options = eframe::NativeOptions::default();
eframe::run_native(
"egui-async Login Example",
native_options,
Box::new(|_cc| Ok(Box::new(LoginApp::default()))),
)
}
struct LoginApp {
username: String,
password: String,
// We bind to <String, String> -> <Success Message, Error Message>
login: Bind<String, String>,
}
#[allow(clippy::derivable_impls)]
impl Default for LoginApp {
fn default() -> Self {
Self {
username: String::new(),
password: String::new(),
// We use default() (retain = false) because if the user closes the window
// or we change views, we probably don't need to keep the login state floating around.
login: Bind::default(),
}
}
}
// A mock async function to simulate a server request
async fn perform_login(username: String, password: String) -> Result<String, String> {
// Simulate network latency (2 seconds)
#[cfg(not(target_family = "wasm"))]
tokio::time::sleep(Duration::from_secs(2)).await;
// Simple validation logic
if username.trim().is_empty() {
return Err("Username cannot be empty.".to_owned());
}
if password == "secret" {
Ok(format!("User '{username}'"))
} else {
Err("Invalid password. (Hint: try 'secret')".to_owned())
}
}
impl eframe::App for LoginApp {
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// 1. Register the plugin (Required!)
ctx.plugin_or_default::<EguiAsyncPlugin>();
}
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show_inside(ui, |ui| {
ui.vertical(|ui| {
ui.heading("Login Portal");
ui.add_space(20.0);
// 2. Explicit State Control Pattern
// We match exhaustively on the state to determine exactly what the UI looks like.
match self.login.state() {
StateWithData::Idle => {
// State: Idle -> Show Input Form
ui.label("Please enter your credentials:");
ui.add_space(10.0);
// Use a Grid to align the labels and text boxes nicely
egui::Grid::new("login_form")
.num_columns(2)
.spacing([10.0, 10.0])
.show(ui, |ui| {
ui.label("Username:");
ui.text_edit_singleline(&mut self.username);
ui.end_row();
ui.label("Password:");
ui.add(
egui::TextEdit::singleline(&mut self.password).password(true),
);
ui.end_row();
});
ui.add_space(20.0);
// TRIGGER: User clicks button -> Transitions to Pending
if ui.button("Log In").clicked() {
let fut = perform_login(self.username.clone(), self.password.clone());
self.login.request(fut);
}
}
StateWithData::Pending => {
// State: Pending -> Show Loading Indicator
// We disable inputs or just hide them. Here we show a spinner.
ui.spinner();
ui.label("Authenticating...");
ui.add_space(10.0);
// Option: Allow cancelling the request
if ui.button("Cancel").clicked() {
// On native, this physically aborts the tokio task if configured
self.login.clear();
}
}
StateWithData::Finished(success_msg) => {
// State: Finished (Ok) -> Show Success Screen
ui.label(
egui::RichText::new("Login Successful!")
.color(egui::Color32::GREEN)
.size(20.0),
);
ui.label(success_msg);
ui.add_space(20.0);
// TRIGGER: Reset to Idle to allow logging in again
if ui.button("Log Out").clicked() {
self.username.clear();
self.password.clear();
self.login.clear();
}
}
StateWithData::Failed(err_msg) => {
// State: Failed (Err) -> Show Error and Retry
ui.label(
egui::RichText::new("Login Failed")
.color(egui::Color32::RED)
.strong(),
);
ui.label(err_msg);
ui.add_space(20.0);
// TRIGGER: Reset to Idle to try again (keeps previous username/pass typed in)
if ui.button("Try Again").clicked() {
self.login.clear();
}
}
}
});
});
}
}