diff --git a/game/src/game.rs b/game/src/game.rs index a162766066..41e060fe1d 100644 --- a/game/src/game.rs +++ b/game/src/game.rs @@ -1,4 +1,4 @@ -use crate::pregame::{MainMenu, TitleScreen}; +use crate::pregame::{main_menu, TitleScreen}; use crate::render::DrawOptions; use crate::sandbox::{GameplayMode, SandboxMode}; use crate::ui::{Flags, ShowEverything, UI}; @@ -20,7 +20,7 @@ impl Game { vec![Box::new(TitleScreen::new(ctx, &ui))] } else { vec![ - Box::new(MainMenu::new(ctx)), + main_menu(ctx, &ui), Box::new(SandboxMode::new(ctx, &mut ui, GameplayMode::Freeform)), ] }; diff --git a/game/src/main.rs b/game/src/main.rs index 06603e26a4..638ee48cd7 100644 --- a/game/src/main.rs +++ b/game/src/main.rs @@ -5,6 +5,7 @@ mod debug; mod edit; mod game; mod helpers; +mod managed; mod mission; mod pregame; mod render; diff --git a/game/src/managed.rs b/game/src/managed.rs new file mode 100644 index 0000000000..06b538fdc2 --- /dev/null +++ b/game/src/managed.rs @@ -0,0 +1,91 @@ +use crate::game::{State, Transition}; +use crate::ui::UI; +use ezgui::{layout, Color, EventCtx, GfxCtx, Line, MultiText, ScreenPt, Text, TextButton}; + +type Callback = Box Option>; + +pub struct ManagedGUIStateBuilder { + multi_txt: MultiText, + buttons: Vec<(String, Callback)>, +} + +impl ManagedGUIStateBuilder { + pub fn draw_text(&mut self, txt: Text, pt: ScreenPt) { + self.multi_txt.add(txt, pt); + } + + pub fn text_button(&mut self, label: &str, onclick: Callback) { + self.buttons.push((label.to_string(), onclick)); + } + + pub fn build(self, ctx: &EventCtx) -> Box { + Box::new(ManagedGUIState { + multi_txt: self.multi_txt, + // TODO Default style. Lots of variations. + buttons: self + .buttons + .into_iter() + .map(|(label, onclick)| { + ( + TextButton::new( + Text::from(Line(label).fg(Color::BLACK)), + Color::WHITE, + Color::ORANGE, + ctx, + ), + onclick, + ) + }) + .collect(), + }) + } +} + +pub struct ManagedGUIState { + multi_txt: MultiText, + buttons: Vec<(TextButton, Callback)>, +} + +impl ManagedGUIState { + pub fn builder() -> ManagedGUIStateBuilder { + ManagedGUIStateBuilder { + multi_txt: MultiText::new(), + buttons: Vec::new(), + } + } +} + +impl State for ManagedGUIState { + fn event(&mut self, ctx: &mut EventCtx, ui: &mut UI) -> Transition { + layout::stack_vertically( + layout::ContainerOrientation::Centered, + ctx, + self.buttons + .iter_mut() + .map(|(btn, _)| btn as &mut dyn layout::Widget) + .collect(), + ); + for (btn, onclick) in self.buttons.iter_mut() { + btn.event(ctx); + if btn.clicked() { + if let Some(t) = (onclick)(ctx, ui) { + return t; + } + } + } + Transition::Keep + } + + fn draw_default_ui(&self) -> bool { + false + } + + fn draw(&self, g: &mut GfxCtx, ui: &UI) { + // Happens to be a nice background color too ;) + g.clear(ui.cs.get("grass")); + self.multi_txt.draw(g); + for (btn, _) in &self.buttons { + btn.draw(g); + } + } +} diff --git a/game/src/pregame.rs b/game/src/pregame.rs index 133acde6b0..9534c11ccf 100644 --- a/game/src/pregame.rs +++ b/game/src/pregame.rs @@ -1,15 +1,13 @@ use crate::abtest::setup::PickABTest; use crate::challenges::challenges_picker; use crate::game::{State, Transition}; +use crate::managed::ManagedGUIState; use crate::mission::MissionEditMode; use crate::sandbox::{GameplayMode, SandboxMode}; use crate::tutorial::TutorialMode; use crate::ui::UI; use abstutil::elapsed_seconds; -use ezgui::{ - layout, Color, EventCtx, EventLoopMode, GfxCtx, HorizontalAlignment, Line, MultiText, ScreenPt, - Text, TextButton, VerticalAlignment, -}; +use ezgui::{layout, Color, EventCtx, EventLoopMode, GfxCtx, Line, ScreenPt, Text, TextButton}; use geom::{Duration, Line, Pt2D, Speed}; use map_model::Map; use rand::Rng; @@ -46,7 +44,7 @@ impl State for TitleScreen { // TODO or any keypress self.play_btn.event(ctx); if self.play_btn.clicked() { - return Transition::Replace(Box::new(MainMenu::new(ctx))); + return Transition::Replace(main_menu(ctx, ui)); } self.screensaver.update(&mut self.rng, ctx, &ui.primary.map); @@ -59,185 +57,81 @@ impl State for TitleScreen { } } -pub struct MainMenu { - txts: MultiText, - // TODO Icon with text? Nah, just an image button on a rounded rectangle - tutorial_btn: TextButton, - sandbox_btn: TextButton, - challenges_btn: TextButton, - dev_tools_btn: TextButton, - abtest_btn: TextButton, - about_btn: TextButton, +pub fn main_menu(ctx: &EventCtx, ui: &UI) -> Box { + let mut state = ManagedGUIState::builder(); + + state.draw_text( + Text::from(Line("A/B STREET").size(50)).no_bg(), + ScreenPt::new(200.0, 100.0), + ); + state.draw_text( + Text::from(Line("Created by Dustin Carlino")).no_bg(), + ScreenPt::new(250.0, 300.0), + ); + state.draw_text( + Text::from(Line("Choose your game")).no_bg(), + ScreenPt::new(250.0, 500.0), + ); + + state.text_button( + "TUTORIAL", + Box::new(|ctx, _| Some(Transition::Push(Box::new(TutorialMode::new(ctx))))), + ); + state.text_button( + "SANDBOX", + Box::new(|ctx, ui| { + Some(Transition::Push(Box::new(SandboxMode::new( + ctx, + ui, + GameplayMode::Freeform, + )))) + }), + ); + state.text_button( + "CHALLENGES", + Box::new(|_, _| Some(Transition::Push(challenges_picker()))), + ); + if ui.primary.current_flags.dev { + state.text_button( + "INTERNAL DEV TOOLS", + Box::new(|ctx, _| Some(Transition::Push(Box::new(MissionEditMode::new(ctx))))), + ); + state.text_button( + "INTERNAL A/B TEST MODE", + Box::new(|_, _| Some(Transition::Push(PickABTest::new()))), + ); + } + state.text_button( + "About A/B Street", + Box::new(|ctx, _| Some(Transition::Push(about(ctx)))), + ); + state.text_button( + "QUIT", + Box::new(|_, _| { + // TODO before_quit? + std::process::exit(0); + }), + ); + state.build(ctx) } -impl MainMenu { - pub fn new(ctx: &EventCtx) -> MainMenu { - let mut txts = MultiText::new(); - // TODO Lots wrong here. - txts.add( - Text::from(Line("A/B STREET").size(50)).no_bg(), - ScreenPt::new(200.0, 100.0), - ); - txts.add( - Text::from(Line("Created by Dustin Carlino")).no_bg(), - ScreenPt::new(250.0, 300.0), - ); - txts.add( - Text::from(Line("Choose your game")).no_bg(), - ScreenPt::new(250.0, 500.0), - ); +fn about(ctx: &EventCtx) -> Box { + let mut state = ManagedGUIState::builder(); - MainMenu { - txts, - tutorial_btn: TextButton::new( - Text::from(Line("TUTORIAL").fg(Color::BLACK)), - Color::WHITE, - Color::ORANGE, - ctx, - ), - sandbox_btn: TextButton::new( - Text::from(Line("SANDBOX").fg(Color::BLACK)), - Color::WHITE, - Color::ORANGE, - ctx, - ), - challenges_btn: TextButton::new( - Text::from(Line("CHALLENGES").fg(Color::BLACK)), - Color::WHITE, - Color::ORANGE, - ctx, - ), - // TODO hide unless ui.primary.current_flags.dev - dev_tools_btn: TextButton::new( - Text::from(Line("INTERNAL DEV TOOLS").fg(Color::BLACK)), - Color::WHITE, - Color::ORANGE, - ctx, - ), - abtest_btn: TextButton::new( - Text::from(Line("INTERNAL A/B TEST MODE").fg(Color::BLACK)), - Color::WHITE, - Color::ORANGE, - ctx, - ), - // TODO No background at all... - about_btn: TextButton::new( - Text::from(Line("About A/B Street").fg(Color::WHITE)), - Color::BLACK, - Color::ORANGE, - ctx, - ), - } - } -} + let mut txt = Text::new().no_bg(); + txt.add(Line("A/B STREET").size(50)); + txt.add(Line("Created by Dustin Carlino")); + txt.add(Line("")); + txt.add(Line("Contact: dabreegster@gmail.com")); + txt.add(Line("Project: http://github.com/dabreegster/abstreet")); + txt.add(Line("Map data from OpenStreetMap and King County GIS")); + // TODO Lots more credits + // TODO centered + state.draw_text(txt, ScreenPt::new(100.0, 100.0)); -impl State for MainMenu { - fn event(&mut self, ctx: &mut EventCtx, ui: &mut UI) -> Transition { - // TODO How do we want to express layouting? - layout::stack_vertically( - layout::ContainerOrientation::Centered, - ctx, - vec![ - &mut self.tutorial_btn, - &mut self.sandbox_btn, - &mut self.challenges_btn, - &mut self.dev_tools_btn, - &mut self.abtest_btn, - &mut self.about_btn, - ], - ); + state.text_button("BACK", Box::new(|_, _| Some(Transition::Pop))); - self.tutorial_btn.event(ctx); - self.sandbox_btn.event(ctx); - self.challenges_btn.event(ctx); - self.dev_tools_btn.event(ctx); - self.abtest_btn.event(ctx); - self.about_btn.event(ctx); - - if self.tutorial_btn.clicked() { - return Transition::Push(Box::new(TutorialMode::new(ctx))); - } - if self.sandbox_btn.clicked() { - return Transition::Push(Box::new(SandboxMode::new(ctx, ui, GameplayMode::Freeform))); - } - if self.challenges_btn.clicked() { - return Transition::Push(challenges_picker()); - } - if self.dev_tools_btn.clicked() { - return Transition::Push(Box::new(MissionEditMode::new(ctx))); - } - if self.abtest_btn.clicked() { - return Transition::Push(PickABTest::new()); - } - if self.about_btn.clicked() { - return Transition::Push(Box::new(About::new(ctx))); - } - - // TODO Hotkeys? How to communicate? - - Transition::Keep - } - - fn draw_default_ui(&self) -> bool { - false - } - - fn draw(&self, g: &mut GfxCtx, ui: &UI) { - g.clear(ui.cs.get("grass")); - self.txts.draw(g); - self.tutorial_btn.draw(g); - self.sandbox_btn.draw(g); - self.challenges_btn.draw(g); - self.dev_tools_btn.draw(g); - self.abtest_btn.draw(g); - self.about_btn.draw(g); - } -} - -struct About { - txt: Text, - back_btn: TextButton, -} - -impl About { - fn new(ctx: &EventCtx) -> About { - let mut txt = Text::new().no_bg(); - txt.add(Line("A/B STREET").size(50)); - txt.add(Line("Created by Dustin Carlino")); - txt.add(Line("")); - txt.add(Line("Contact: dabreegster@gmail.com")); - txt.add(Line("Project: http://github.com/dabreegster/abstreet")); - txt.add(Line("Map data from OpenStreetMap and King County GIS")); - // TODO Lots more credits - About { - txt, - // TODO Arrow and no background - back_btn: TextButton::new(Text::from(Line("BACK")), Color::BLACK, Color::ORANGE, ctx), - } - } -} - -impl State for About { - fn event(&mut self, ctx: &mut EventCtx, _: &mut UI) -> Transition { - self.back_btn.event(ctx); - if self.back_btn.clicked() { - return Transition::Pop; - } - Transition::Keep - } - - fn draw_default_ui(&self) -> bool { - false - } - - fn draw(&self, g: &mut GfxCtx, ui: &UI) { - g.clear(ui.cs.get("grass")); - g.draw_blocking_text( - &self.txt, - (HorizontalAlignment::Center, VerticalAlignment::Center), - ); - self.back_btn.draw(g); - } + state.build(ctx) } const SPEED: Speed = Speed::const_meters_per_second(20.0);