a-b-street/abstreet · error

Two buttons in one Panel both use action

Error message

Two buttons in one Panel both use action {}

What it means

Panel builds a map from click action strings to handlers; if two enabled Buttons in the same Panel share the same action string, event routing would be ambiguous, so the library panics. Action strings must be unique within a Panel.

Solutions

  1. Rename one button's action to a unique string (e.g. prefix with the section name)
  2. If both buttons should do the same thing, keep one and reuse the same widget instead of duplicating
  3. Only one of the buttons may be enabled at a time — the check skips disabled buttons
  4. Generate action strings programmatically to guarantee uniqueness

Example fix

// before
btn1 = Button::text("Save", "save");
btn2 = Button::text("Save", "save");
// after
btn2 = Button::text("Save", "save_final");
Defensive patterns

Strategy: validation

Validate before calling

let mut seen = std::collections::HashSet::new();
for action in [btn1.action.clone(), btn2.action.clone()] {
    assert!(seen.insert(action), "duplicate action {}", action);
}

Prevention

When it happens

Trigger: Creating two Button widgets with the same `.action(...)` string (e.g. reusing "back" or a constant) and both being enabled when the Panel lays out and calls get_all_click_actions.

Common situations: Copy-pasting button construction without renaming the action; a loop that adds several buttons but reuses one action constant; two containers each adding a similarly-named button.

Understand the failure class

Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.

Related errors


AI-assisted analysis of a-b-street/abstreet@0964f29315 (2026-09-13). Data as JSON: /api/errors/75cb957ceaa9e127. Report an issue: GitHub.

Appendix: source

Thrown at widgetry/src/widgets/mod.rs:717

                    nodes,
                    x + dx,
                    y + dy,
                    scroll_offset,
                    ctx,
                    recompute_layout,
                    defer_draw,
                );
            }
        } else {
            self.widget.set_pos(top_left);
        }
    }

    fn get_all_click_actions(&self, actions: &mut HashSet<String>) {
        if let Some(btn) = self.widget.downcast_ref::<Button>() {
            if btn.is_enabled() {
                if actions.contains(&btn.action) {
                    panic!("Two buttons in one Panel both use action {}", btn.action);
                }
                actions.insert(btn.action.clone());
            }
        } else if let Some(container) = self.widget.downcast_ref::<Container>() {
            for w in &container.members {
                w.get_all_click_actions(actions);
            }
        }
    }

    fn currently_hovering(&self) -> Option<&String> {
        if let Some(btn) = self.widget.downcast_ref::<Button>() {
            if btn.hovering {
                return Some(&btn.action);
            }
        } else if let Some(checkbox) = self.widget.downcast_ref::<Toggle>() {
            if checkbox.btn.hovering {
                return Some(&checkbox.btn.action);

View on GitHub (pinned to 0964f29315)