mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-12-29 17:34:58 +03:00
moving a/b test edit plugin to a/b test mode as the initial state. putting secondary sim state into the mode directly
This commit is contained in:
parent
8cc86f623a
commit
58b3d6d201
@ -1,4 +1,7 @@
|
|||||||
|
mod setup;
|
||||||
|
|
||||||
use crate::game::{GameState, Mode};
|
use crate::game::{GameState, Mode};
|
||||||
|
use crate::state::PerMapUI;
|
||||||
use crate::ui::ShowEverything;
|
use crate::ui::ShowEverything;
|
||||||
use abstutil::elapsed_seconds;
|
use abstutil::elapsed_seconds;
|
||||||
use ezgui::{Color, EventCtx, EventLoopMode, GfxCtx, Text, Wizard};
|
use ezgui::{Color, EventCtx, EventLoopMode, GfxCtx, Text, Wizard};
|
||||||
@ -10,11 +13,14 @@ use std::time::Instant;
|
|||||||
const ADJUST_SPEED: f64 = 0.1;
|
const ADJUST_SPEED: f64 = 0.1;
|
||||||
|
|
||||||
pub struct ABTestMode {
|
pub struct ABTestMode {
|
||||||
desired_speed: f64, // sim seconds per real second
|
pub desired_speed: f64, // sim seconds per real second
|
||||||
state: State,
|
pub state: State,
|
||||||
|
// TODO Urgh, hack. Need to be able to take() it to switch states sometimes.
|
||||||
|
pub secondary: Option<PerMapUI>,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum State {
|
pub enum State {
|
||||||
|
Setup(setup::ABTestSetup),
|
||||||
Paused,
|
Paused,
|
||||||
Running {
|
Running {
|
||||||
last_step: Instant,
|
last_step: Instant,
|
||||||
@ -27,13 +33,19 @@ impl ABTestMode {
|
|||||||
pub fn new() -> ABTestMode {
|
pub fn new() -> ABTestMode {
|
||||||
ABTestMode {
|
ABTestMode {
|
||||||
desired_speed: 1.0,
|
desired_speed: 1.0,
|
||||||
state: State::Paused,
|
state: State::Setup(setup::ABTestSetup::Pick(Wizard::new())),
|
||||||
|
secondary: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn event(state: &mut GameState, ctx: &mut EventCtx) -> EventLoopMode {
|
pub fn event(state: &mut GameState, ctx: &mut EventCtx) -> EventLoopMode {
|
||||||
match state.mode {
|
match state.mode {
|
||||||
Mode::ABTest(ref mut mode) => {
|
Mode::ABTest(ref mut mode) => {
|
||||||
|
if let State::Setup(_) = mode.state {
|
||||||
|
setup::ABTestSetup::event(state, ctx);
|
||||||
|
return EventLoopMode::InputOnly;
|
||||||
|
}
|
||||||
|
|
||||||
ctx.canvas.handle_event(ctx.input);
|
ctx.canvas.handle_event(ctx.input);
|
||||||
state.ui.state.primary.current_selection = state.ui.handle_mouseover(
|
state.ui.state.primary.current_selection = state.ui.handle_mouseover(
|
||||||
ctx,
|
ctx,
|
||||||
@ -97,6 +109,10 @@ impl ABTestMode {
|
|||||||
};
|
};
|
||||||
} else if ctx.input.modal_action("run one step of sim") {
|
} else if ctx.input.modal_action("run one step of sim") {
|
||||||
state.ui.state.primary.sim.step(&state.ui.state.primary.map);
|
state.ui.state.primary.sim.step(&state.ui.state.primary.map);
|
||||||
|
{
|
||||||
|
let s = mode.secondary.as_mut().unwrap();
|
||||||
|
s.sim.step(&s.map);
|
||||||
|
}
|
||||||
//*ctx.recalculate_current_selection = true;
|
//*ctx.recalculate_current_selection = true;
|
||||||
}
|
}
|
||||||
EventLoopMode::InputOnly
|
EventLoopMode::InputOnly
|
||||||
@ -118,6 +134,10 @@ impl ABTestMode {
|
|||||||
if dt_s >= sim::TIMESTEP.inner_seconds() / mode.desired_speed {
|
if dt_s >= sim::TIMESTEP.inner_seconds() / mode.desired_speed {
|
||||||
ctx.input.use_update_event();
|
ctx.input.use_update_event();
|
||||||
state.ui.state.primary.sim.step(&state.ui.state.primary.map);
|
state.ui.state.primary.sim.step(&state.ui.state.primary.map);
|
||||||
|
{
|
||||||
|
let s = mode.secondary.as_mut().unwrap();
|
||||||
|
s.sim.step(&s.map);
|
||||||
|
}
|
||||||
//*ctx.recalculate_current_selection = true;
|
//*ctx.recalculate_current_selection = true;
|
||||||
*last_step = Instant::now();
|
*last_step = Instant::now();
|
||||||
|
|
||||||
@ -131,6 +151,7 @@ impl ABTestMode {
|
|||||||
}
|
}
|
||||||
EventLoopMode::Animation
|
EventLoopMode::Animation
|
||||||
}
|
}
|
||||||
|
State::Setup(_) => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
@ -138,17 +159,20 @@ impl ABTestMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw(state: &GameState, g: &mut GfxCtx) {
|
pub fn draw(state: &GameState, g: &mut GfxCtx) {
|
||||||
|
state.ui.new_draw(
|
||||||
|
g,
|
||||||
|
None,
|
||||||
|
HashMap::new(),
|
||||||
|
&state.ui.state.primary.sim,
|
||||||
|
&ShowEverything::new(),
|
||||||
|
);
|
||||||
|
|
||||||
match state.mode {
|
match state.mode {
|
||||||
Mode::ABTest(ref mode) => match mode.state {
|
Mode::ABTest(ref mode) => match mode.state {
|
||||||
_ => {
|
State::Setup(ref setup) => {
|
||||||
state.ui.new_draw(
|
setup.draw(g);
|
||||||
g,
|
|
||||||
None,
|
|
||||||
HashMap::new(),
|
|
||||||
&state.ui.state.primary.sim,
|
|
||||||
&ShowEverything::new(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
},
|
},
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
|
132
editor/src/abtest/setup.rs
Normal file
132
editor/src/abtest/setup.rs
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
use crate::abtest::{ABTestMode, State};
|
||||||
|
use crate::game::{GameState, Mode};
|
||||||
|
use crate::plugins::{choose_edits, choose_scenario, load_ab_test};
|
||||||
|
use crate::state::{Flags, PerMapUI, UIState};
|
||||||
|
use ezgui::{EventCtx, GfxCtx, LogScroller, Wizard, WrappedWizard};
|
||||||
|
use map_model::Map;
|
||||||
|
use sim::{ABTest, SimFlags};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub enum ABTestSetup {
|
||||||
|
Pick(Wizard),
|
||||||
|
Manage(ABTest, LogScroller),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ABTestSetup {
|
||||||
|
pub fn event(state: &mut GameState, ctx: &mut EventCtx) {
|
||||||
|
match state.mode {
|
||||||
|
Mode::ABTest(ref mut mode) => match mode.state {
|
||||||
|
State::Setup(ref mut setup) => match setup {
|
||||||
|
ABTestSetup::Pick(ref mut wizard) => {
|
||||||
|
if let Some(ab_test) = pick_ab_test(
|
||||||
|
&state.ui.state.primary.map,
|
||||||
|
wizard.wrap(ctx.input, ctx.canvas),
|
||||||
|
) {
|
||||||
|
let scroller =
|
||||||
|
LogScroller::new(ab_test.test_name.clone(), ab_test.describe());
|
||||||
|
*setup = ABTestSetup::Manage(ab_test, scroller);
|
||||||
|
} else if wizard.aborted() {
|
||||||
|
state.mode = Mode::SplashScreen(Wizard::new(), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ABTestSetup::Manage(test, ref mut scroller) => {
|
||||||
|
ctx.input.set_mode_with_prompt(
|
||||||
|
"A/B Test Editor",
|
||||||
|
format!("A/B Test Editor for {}", test.test_name),
|
||||||
|
&ctx.canvas,
|
||||||
|
);
|
||||||
|
if scroller.event(ctx.input) {
|
||||||
|
state.mode = Mode::SplashScreen(Wizard::new(), None);
|
||||||
|
} else if ctx.input.modal_action("run A/B test") {
|
||||||
|
state.mode = launch_test(test, &mut state.ui.state, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(&self, g: &mut GfxCtx) {
|
||||||
|
match self {
|
||||||
|
ABTestSetup::Pick(wizard) => {
|
||||||
|
wizard.draw(g);
|
||||||
|
}
|
||||||
|
ABTestSetup::Manage(_, scroller) => {
|
||||||
|
scroller.draw(g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pick_ab_test(map: &Map, mut wizard: WrappedWizard) -> Option<ABTest> {
|
||||||
|
let load_existing = "Load existing A/B test";
|
||||||
|
let create_new = "Create new A/B test";
|
||||||
|
if wizard.choose_string("What A/B test to manage?", vec![load_existing, create_new])?
|
||||||
|
== load_existing
|
||||||
|
{
|
||||||
|
load_ab_test(map, &mut wizard, "Load which A/B test?")
|
||||||
|
} else {
|
||||||
|
let test_name = wizard.input_string("Name the A/B test")?;
|
||||||
|
let ab_test = ABTest {
|
||||||
|
test_name,
|
||||||
|
map_name: map.get_name().to_string(),
|
||||||
|
scenario_name: choose_scenario(map, &mut wizard, "What scenario to run?")?,
|
||||||
|
edits1_name: choose_edits(map, &mut wizard, "For the 1st run, what map edits to use?")?,
|
||||||
|
edits2_name: choose_edits(map, &mut wizard, "For the 2nd run, what map edits to use?")?,
|
||||||
|
};
|
||||||
|
ab_test.save();
|
||||||
|
Some(ab_test)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn launch_test(test: &ABTest, state: &mut UIState, ctx: &mut EventCtx) -> Mode {
|
||||||
|
println!("Launching A/B test {}...", test.test_name);
|
||||||
|
let load = PathBuf::from(format!(
|
||||||
|
"../data/scenarios/{}/{}.json",
|
||||||
|
test.map_name, test.scenario_name
|
||||||
|
));
|
||||||
|
let current_flags = &state.primary.current_flags;
|
||||||
|
let rng_seed = if current_flags.sim_flags.rng_seed.is_some() {
|
||||||
|
current_flags.sim_flags.rng_seed
|
||||||
|
} else {
|
||||||
|
Some(42)
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO Cheaper to load the edits for the map and then instantiate the scenario for the
|
||||||
|
// primary.
|
||||||
|
let (primary, _) = PerMapUI::new(
|
||||||
|
Flags {
|
||||||
|
sim_flags: SimFlags {
|
||||||
|
load: load.clone(),
|
||||||
|
rng_seed,
|
||||||
|
run_name: format!("{} with {}", test.test_name, test.edits1_name),
|
||||||
|
edits_name: test.edits1_name.clone(),
|
||||||
|
},
|
||||||
|
..current_flags.clone()
|
||||||
|
},
|
||||||
|
&state.cs,
|
||||||
|
ctx.prerender,
|
||||||
|
);
|
||||||
|
let (secondary, _) = PerMapUI::new(
|
||||||
|
Flags {
|
||||||
|
sim_flags: SimFlags {
|
||||||
|
load,
|
||||||
|
rng_seed,
|
||||||
|
run_name: format!("{} with {}", test.test_name, test.edits2_name),
|
||||||
|
edits_name: test.edits2_name.clone(),
|
||||||
|
},
|
||||||
|
..current_flags.clone()
|
||||||
|
},
|
||||||
|
&state.cs,
|
||||||
|
ctx.prerender,
|
||||||
|
);
|
||||||
|
|
||||||
|
state.primary = primary;
|
||||||
|
Mode::ABTest(ABTestMode {
|
||||||
|
desired_speed: 1.0,
|
||||||
|
state: State::Paused,
|
||||||
|
secondary: Some(secondary),
|
||||||
|
})
|
||||||
|
}
|
@ -1,141 +0,0 @@
|
|||||||
use crate::colors::ColorScheme;
|
|
||||||
use crate::objects::DrawCtx;
|
|
||||||
use crate::plugins::{choose_edits, choose_scenario, load_ab_test, BlockingPlugin, PluginCtx};
|
|
||||||
use crate::state::{Flags, PerMapUI, PluginsPerMap};
|
|
||||||
use ezgui::{GfxCtx, LogScroller, Prerender, Wizard, WrappedWizard};
|
|
||||||
use map_model::Map;
|
|
||||||
use sim::{ABTest, SimFlags};
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
pub enum ABTestManager {
|
|
||||||
PickABTest(Wizard),
|
|
||||||
ManageABTest(ABTest, LogScroller),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ABTestManager {
|
|
||||||
pub fn new(ctx: &mut PluginCtx) -> Option<ABTestManager> {
|
|
||||||
if ctx.primary.current_selection.is_none() && ctx.input.action_chosen("manage A/B tests") {
|
|
||||||
return Some(ABTestManager::PickABTest(Wizard::new()));
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BlockingPlugin for ABTestManager {
|
|
||||||
fn blocking_event_with_plugins(
|
|
||||||
&mut self,
|
|
||||||
ctx: &mut PluginCtx,
|
|
||||||
primary_plugins: &mut PluginsPerMap,
|
|
||||||
) -> bool {
|
|
||||||
match self {
|
|
||||||
ABTestManager::PickABTest(ref mut wizard) => {
|
|
||||||
if let Some(ab_test) =
|
|
||||||
pick_ab_test(&ctx.primary.map, wizard.wrap(ctx.input, ctx.canvas))
|
|
||||||
{
|
|
||||||
let scroller = LogScroller::new(ab_test.test_name.clone(), ab_test.describe());
|
|
||||||
*self = ABTestManager::ManageABTest(ab_test, scroller);
|
|
||||||
} else if wizard.aborted() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ABTestManager::ManageABTest(test, ref mut scroller) => {
|
|
||||||
ctx.input.set_mode_with_prompt(
|
|
||||||
"A/B Test Editor",
|
|
||||||
format!("A/B Test Editor for {}", test.test_name),
|
|
||||||
&ctx.canvas,
|
|
||||||
);
|
|
||||||
if ctx.input.modal_action("run A/B test") {
|
|
||||||
let ((new_primary, new_primary_plugins), new_secondary) =
|
|
||||||
launch_test(test, &ctx.primary.current_flags, &ctx.cs, &ctx.prerender);
|
|
||||||
*ctx.primary = new_primary;
|
|
||||||
*primary_plugins = new_primary_plugins;
|
|
||||||
*ctx.secondary = Some(new_secondary);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if scroller.event(ctx.input) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw(&self, g: &mut GfxCtx, _ctx: &DrawCtx) {
|
|
||||||
match self {
|
|
||||||
ABTestManager::PickABTest(wizard) => {
|
|
||||||
wizard.draw(g);
|
|
||||||
}
|
|
||||||
ABTestManager::ManageABTest(_, scroller) => {
|
|
||||||
scroller.draw(g);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pick_ab_test(map: &Map, mut wizard: WrappedWizard) -> Option<ABTest> {
|
|
||||||
let load_existing = "Load existing A/B test";
|
|
||||||
let create_new = "Create new A/B test";
|
|
||||||
if wizard.choose_string("What A/B test to manage?", vec![load_existing, create_new])?
|
|
||||||
== load_existing
|
|
||||||
{
|
|
||||||
load_ab_test(map, &mut wizard, "Load which A/B test?")
|
|
||||||
} else {
|
|
||||||
let test_name = wizard.input_string("Name the A/B test")?;
|
|
||||||
let ab_test = ABTest {
|
|
||||||
test_name,
|
|
||||||
map_name: map.get_name().to_string(),
|
|
||||||
scenario_name: choose_scenario(map, &mut wizard, "What scenario to run?")?,
|
|
||||||
edits1_name: choose_edits(map, &mut wizard, "For the 1st run, what map edits to use?")?,
|
|
||||||
edits2_name: choose_edits(map, &mut wizard, "For the 2nd run, what map edits to use?")?,
|
|
||||||
};
|
|
||||||
ab_test.save();
|
|
||||||
Some(ab_test)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn launch_test(
|
|
||||||
test: &ABTest,
|
|
||||||
current_flags: &Flags,
|
|
||||||
cs: &ColorScheme,
|
|
||||||
prerender: &Prerender,
|
|
||||||
) -> ((PerMapUI, PluginsPerMap), (PerMapUI, PluginsPerMap)) {
|
|
||||||
println!("Launching A/B test {}...", test.test_name);
|
|
||||||
let load = PathBuf::from(format!(
|
|
||||||
"../data/scenarios/{}/{}.json",
|
|
||||||
test.map_name, test.scenario_name
|
|
||||||
));
|
|
||||||
let rng_seed = if current_flags.sim_flags.rng_seed.is_some() {
|
|
||||||
current_flags.sim_flags.rng_seed
|
|
||||||
} else {
|
|
||||||
Some(42)
|
|
||||||
};
|
|
||||||
|
|
||||||
let primary = PerMapUI::new(
|
|
||||||
Flags {
|
|
||||||
sim_flags: SimFlags {
|
|
||||||
load: load.clone(),
|
|
||||||
rng_seed,
|
|
||||||
run_name: format!("{} with {}", test.test_name, test.edits1_name),
|
|
||||||
edits_name: test.edits1_name.clone(),
|
|
||||||
},
|
|
||||||
..current_flags.clone()
|
|
||||||
},
|
|
||||||
cs,
|
|
||||||
prerender,
|
|
||||||
);
|
|
||||||
let secondary = PerMapUI::new(
|
|
||||||
Flags {
|
|
||||||
sim_flags: SimFlags {
|
|
||||||
load,
|
|
||||||
rng_seed,
|
|
||||||
run_name: format!("{} with {}", test.test_name, test.edits2_name),
|
|
||||||
edits_name: test.edits2_name.clone(),
|
|
||||||
},
|
|
||||||
..current_flags.clone()
|
|
||||||
},
|
|
||||||
cs,
|
|
||||||
prerender,
|
|
||||||
);
|
|
||||||
// That's all! The scenario will be instantiated.
|
|
||||||
(primary, secondary)
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
pub mod a_b_tests;
|
|
@ -1,4 +1,3 @@
|
|||||||
pub mod edit;
|
|
||||||
pub mod sim;
|
pub mod sim;
|
||||||
pub mod view;
|
pub mod view;
|
||||||
|
|
||||||
|
@ -278,8 +278,6 @@ impl SandboxMode {
|
|||||||
*last_step = Instant::now();
|
*last_step = Instant::now();
|
||||||
|
|
||||||
if benchmark.has_real_time_passed(Duration::seconds(1.0)) {
|
if benchmark.has_real_time_passed(Duration::seconds(1.0)) {
|
||||||
// I think the benchmark should naturally account for the delay of
|
|
||||||
// the secondary sim.
|
|
||||||
*speed =
|
*speed =
|
||||||
state.ui.state.primary.sim.measure_speed(benchmark, false);
|
state.ui.state.primary.sim.measure_speed(benchmark, false);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::colors::ColorScheme;
|
use crate::colors::ColorScheme;
|
||||||
use crate::objects::{DrawCtx, RenderingHints, ID};
|
use crate::objects::{DrawCtx, RenderingHints, ID};
|
||||||
use crate::plugins;
|
use crate::plugins;
|
||||||
use crate::plugins::{edit, view, AmbientPlugin, BlockingPlugin, NonblockingPlugin, PluginCtx};
|
use crate::plugins::{view, AmbientPlugin, BlockingPlugin, NonblockingPlugin, PluginCtx};
|
||||||
use crate::render::DrawMap;
|
use crate::render::DrawMap;
|
||||||
use abstutil::MeasureMemory;
|
use abstutil::MeasureMemory;
|
||||||
use ezgui::EventCtx;
|
use ezgui::EventCtx;
|
||||||
@ -120,10 +120,6 @@ impl UIState {
|
|||||||
|
|
||||||
if let Some(p) = view::warp::WarpState::new(&mut ctx) {
|
if let Some(p) = view::warp::WarpState::new(&mut ctx) {
|
||||||
self.exclusive_blocking_plugin = Some(Box::new(p));
|
self.exclusive_blocking_plugin = Some(Box::new(p));
|
||||||
} else if ctx.secondary.is_none() {
|
|
||||||
if let Some(p) = edit::a_b_tests::ABTestManager::new(&mut ctx) {
|
|
||||||
self.exclusive_blocking_plugin = Some(Box::new(p));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +31,6 @@ impl GUI for UI {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
folders.extend(vec![
|
folders.extend(vec![
|
||||||
Folder::new("Edit", vec![(Some(Key::B), "manage A/B tests")]),
|
|
||||||
Folder::new("Simulation", vec![(Some(Key::D), "diff all A/B trips")]),
|
Folder::new("Simulation", vec![(Some(Key::D), "diff all A/B trips")]),
|
||||||
Folder::new("View", vec![(Some(Key::J), "warp to an object")]),
|
Folder::new("View", vec![(Some(Key::J), "warp to an object")]),
|
||||||
]);
|
]);
|
||||||
@ -40,7 +39,6 @@ impl GUI for UI {
|
|||||||
|
|
||||||
fn modal_menus(&self) -> Vec<ModalMenu> {
|
fn modal_menus(&self) -> Vec<ModalMenu> {
|
||||||
vec![
|
vec![
|
||||||
ModalMenu::new("A/B Test Editor", vec![(Key::R, "run A/B test")]),
|
|
||||||
ModalMenu::new("A/B Trip Explorer", vec![(Key::Enter, "quit")]),
|
ModalMenu::new("A/B Trip Explorer", vec![(Key::Enter, "quit")]),
|
||||||
ModalMenu::new("A/B All Trips Explorer", vec![(Key::Enter, "quit")]),
|
ModalMenu::new("A/B All Trips Explorer", vec![(Key::Enter, "quit")]),
|
||||||
// The new exciting things!
|
// The new exciting things!
|
||||||
@ -173,6 +171,10 @@ impl GUI for UI {
|
|||||||
(Key::V, "visualize"),
|
(Key::V, "visualize"),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
ModalMenu::new(
|
||||||
|
"A/B Test Editor",
|
||||||
|
vec![(Key::Escape, "quit"), (Key::R, "run A/B test")],
|
||||||
|
),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user