← All Workshops

MudEngine Part 4: Single-Player Dioxus GUI

Step 7 / 10

The App component

The App component ties everything together. It holds three pieces of reactive state:

  1. world — a Signal<World> containing the game state
  2. input — a Signal<String> bound to the text input box
  3. feedback — a Signal<String> for error messages and help text

The component renders the grid, the current room description (calling world.read().look() which triggers reactivity), and an input box. When the player presses Enter, the handler parses the command, mutates the world, and updates feedback.

mud-engine/src/main.rs
#[component]
fn App() -> Element {
    let mut world = use_signal(|| World::new());
    let mut input = use_signal(|| String::new());
    let mut feedback = use_signal(|| String::new());

    rsx! {
        div {
            id: "main",
            h1 { "🧙 MudEngine" }
            p { style: "color: #666; margin-top: -8px;",
               "Part 4 — Dioxus GUI" }

            WorldGrid { world }

            div {
                class: "description",
                "{world.read().look()}"
            }

            if !feedback.read().is_empty() {
                p { style: "color: #ff6b6b; margin: 8px 0;",
                   "{feedback}" }
            }

            input {
                class: "command-input",
                value: "{input}",
                placeholder: "Type a command and press Enter...",
                oninput: move |e| input.set(e.value()),
                onkeydown: move |e| {
                    if e.key() == Key::Enter {
                        let cmd = input.read().trim().to_lowercase();
                        if cmd.is_empty() { return; }
                        input.write().clear();
                        let parts: Vec<&str> =
                            cmd.splitn(2, ' ').collect();
                        match parts[0] {
                            "look" | "l" => feedback.write().clear(),
                            "north" | "n" | "south" | "s"
                            | "east" | "e" | "west" | "w" => {
                                let dir = match parts[0] {
                                    "n" => "north",
                                    "s" => "south",
                                    "e" => "east",
                                    "w" => "west",
                                    d => d,
                                };
                                if world.write().go(dir) {
                                    feedback.write().clear();
                                } else {
                                    feedback.set(
                                        format!("You cannot go {} from here.", dir),
                                    );
                                }
                            }
                            "help" | "?" => {
                                feedback.set(
                                    "Commands: look/l | north/n south/s east/e west/w | help/? | quit/exit"
                                        .into(),
                                );
                            }
                            "quit" | "exit" => {
                                feedback.set(
                                    "Farewell, adventurer!".into(),
                                );
                            }
                            _ => {
                                feedback.set(
                                    "Unknown command. Type 'help' or '?' for a list."
                                        .into(),
                                );
                            }
                        }
                    }
                },
            }
        }
    }
}
💡 How reactivity works here
  • world is a Signal<World>. Reading it in the render function (via world.read().look()) subscribes the component to changes. When the onkeydown handler calls world.write().go(dir), Dioxus schedules a re-render of App and WorldGrid (because the signal changed).

  • input is a Signal<String> that keeps the input box value in sync via value: "{input}" and oninput: move |e| input.set(e.value()). When Enter is pressed, we read the value, clear the signal, and process the command.

  • feedback is for transient messages (errors, help text, quit message). It is shown only when non-empty. Since it is also a signal, setting it triggers a re-render.

The WorldGrid subscribes independently via its ReadSignal<World> prop and only re-renders when the world actually changes (thanks to PartialEq).

Step 7 / 10