in immediate mode GUI, you don't "replace" the content of UI: you just render different UI based on your app states.
when the button is clicked, you simply change your app states, then the next frame will be rendered differently. if you are not running the UI continously (a.k.a. polling), you should call ctx.request_repaint()
after you modify the app states.
for example, suppose the app has 3 pages: a welcome page, a login page, and a dashboard page, you can do it like this:
enum Wizard {
Welcome,
Login,
Dashboard,
}
impl Wizard {
fn is_first_page(&self) -> bool { matches!(self, Wizard::Welcome) }
fn is_last_page(&self) -> bool { matches!(self, Wizard::Dashboard) }
fn goto_next_page(&mut self) { ... }
fn goto_prev_page(&mut self) { ... }
}
struct MyApp {
wizard: Wizard,
}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &eframe::egui::Context, frame: &mut eframe::Frame) {
TopBottomPanel::top("navigation toolbar").show(ctx, |ui| {
if ui
.add_enabled(!self.wizard.is_first_page(), Button::new("prev"))
.clicked()
{
self.current_page.goto_prev_page();
ctx.request_repaint();
}
if ui
.add_enabled(!self.wizard.is_last_page(), Button::new("next"))
.clicked()
{
self.current_page.goto_next_page();
ctx.request_repaint();
}
});
CentralPanel::default().show(ctx, |ui| match self.wizard {
Wizard::Welcome => {
// render the welcome page
}
Wizard::Login => {
// render the login page
}
Wizard::Dashboard => {
// render the dashboard page
}
});
}
}
granted, each page may have states, so you need to define a type for each page. you can either store them as individual fields, or you can store them in an array or Vec
as trait objects, but you'll need to create a trait first:
struct WelcomePage { ... }
struct LoginPage { ... }
struct DashboardPage { ... }
// option 1: as individual fields:
struct MyApp {
current_page: Wizard,
welcome_page: WelcomePage,
login_page: LoginPage,
dashboard_page: DashboardPage,
}
// option 2: as array of trait objects
// note: in this case, you don't need `enum Wizard`,
// you can just use a page number
trait Page {
fn render(&mut self, ui: &mut egui::Ui);
}
struct MyApp {
current_page_number: usize,
pages: [Box<dyn Page>; 3],
}
immedate mode GUI rendering uses just regular code, if you want to be modular, you can refactor the rendering code into methods. it's generally good idea to use ui.push_id()
to group your components, to avoid thrashing the cache of the widget states.
for example, the above CentralPanel
ui code can be structured like this:
match self.wizard {
Wizard::Welcome => {
ui.push_id("welcome page", |ui| self.welcome_page.render(ui));
}
Wizard::Login => {
ui.push_id("login page", |ui| self.login_page.render(ui));
}
Wizard::Dashboard => {
ui.push_id("dashboard page", |ui| self.dashboard_page.render(ui));
}
}
each page can be put into a separate module, and has its own render()
method. this is typically how to compose "components" in immedate mode GUI: just call it's render code!.
// welcome.rs
pub struct WelcomePage {
greeting_of_the_day: String,
}
// can also use inherent impl, if not using trait objects
impl Page for WelcomePage {
fn render(&mut self, ui: &mut egui::Ui) {
ui.centered_and_justified(|ui| {
ui.label(&self.greeting_of_the_day);
});
}
}