diff --git a/ezgui/src/canvas.rs b/ezgui/src/canvas.rs index 6b2654f7f7..ac115d934c 100644 --- a/ezgui/src/canvas.rs +++ b/ezgui/src/canvas.rs @@ -19,9 +19,9 @@ pub struct Canvas { // TODO We probably shouldn't even track screen-space cursor when we don't have the cursor. pub(crate) cursor_x: f64, pub(crate) cursor_y: f64, - window_has_cursor: bool, + pub(crate) window_has_cursor: bool, - left_mouse_drag_from: Option, + pub(crate) left_mouse_drag_from: Option, pub window_width: f64, pub window_height: f64, @@ -88,16 +88,6 @@ impl Canvas { } pub fn handle_event(&mut self, input: &mut UserInput) { - if let Some(pt) = input.get_moved_mouse() { - self.cursor_x = pt.x; - self.cursor_y = pt.y; - - if let Some(click) = self.left_mouse_drag_from { - self.cam_x += click.x - pt.x; - self.cam_y += click.y - pt.y; - self.left_mouse_drag_from = Some(pt); - } - } // Can't start dragging or zooming on top of covered area let mouse_on_map = self.get_cursor_in_map_space().is_some(); if input.left_mouse_button_pressed() && mouse_on_map { @@ -113,12 +103,6 @@ impl Canvas { self.zoom_towards_mouse(delta); } } - if input.window_gained_cursor() { - self.window_has_cursor = true; - } - if input.window_lost_cursor() { - self.window_has_cursor = false; - } } pub(crate) fn start_drawing(&self) { diff --git a/ezgui/src/input.rs b/ezgui/src/input.rs index 064c6b0dfa..e8f2abb23c 100644 --- a/ezgui/src/input.rs +++ b/ezgui/src/input.rs @@ -81,6 +81,25 @@ impl UserInput { canvas.lctrl_held = false; } + if let Some(pt) = input.get_moved_mouse() { + canvas.cursor_x = pt.x; + canvas.cursor_y = pt.y; + + // OK to update this here; the drag has to be initiated from canvas.handle_event, which + // the caller must invoke. + if let Some(click) = canvas.left_mouse_drag_from { + canvas.cam_x += click.x - pt.x; + canvas.cam_y += click.y - pt.y; + canvas.left_mouse_drag_from = Some(pt); + } + } + if input.event == Event::WindowGainedCursor { + canvas.window_has_cursor = true; + } + if input.window_lost_cursor() { + canvas.window_has_cursor = false; + } + // Create the context menu here, even if one already existed. if input.right_mouse_button_pressed() { assert!(!input.event_consumed); @@ -253,9 +272,6 @@ impl UserInput { self.event == Event::RightMouseButtonDown } - pub(crate) fn window_gained_cursor(&mut self) -> bool { - self.event == Event::WindowGainedCursor - } pub fn window_lost_cursor(&self) -> bool { self.event == Event::WindowLostCursor } diff --git a/ezgui/src/widgets/menu.rs b/ezgui/src/widgets/menu.rs index a62c4e107e..a5f435001a 100644 --- a/ezgui/src/widgets/menu.rs +++ b/ezgui/src/widgets/menu.rs @@ -39,7 +39,6 @@ struct Choice { #[derive(Clone)] pub enum Position { - ScreenCenter, SomeCornerAt(ScreenPt), } @@ -391,11 +390,6 @@ impl Menu { } } - pub fn current_choice(&self) -> Option<&T> { - let idx = self.current_idx?; - Some(&self.choices[idx].data) - } - pub fn active_choices(&self) -> Vec<&T> { self.choices .iter() @@ -409,25 +403,6 @@ impl Menu { .collect() } - pub fn mark_active(&mut self, label: &str, is_active: bool) { - for choice in self.choices.iter_mut() { - if choice.label == label { - if choice.active == is_active { - panic!( - "Menu choice for {} already had active={}", - choice.label, is_active - ); - } - choice.active = is_active; - return; - } - } - panic!( - "Menu with prompt {:?} has no choice {} to mark active", - self.prompt, label - ); - } - fn recalculate_geom(&mut self, canvas: &Canvas) { let mut txt = self.prompt.clone(); if !self.hidden { @@ -463,12 +438,6 @@ impl Position { fn get_top_left(&self, canvas: &Canvas, menu_dims: ScreenDims) -> ScreenPt { match self { Position::SomeCornerAt(pt) => menu_dims.top_left_for_corner(*pt, canvas), - Position::ScreenCenter => { - let mut pt = canvas.center_to_screen_pt(); - pt.x -= menu_dims.width / 2.0; - pt.y -= menu_dims.height / 2.0; - pt - } } } } diff --git a/ezgui/src/widgets/mod.rs b/ezgui/src/widgets/mod.rs index 25498987a1..f28a38b4ee 100644 --- a/ezgui/src/widgets/mod.rs +++ b/ezgui/src/widgets/mod.rs @@ -3,6 +3,7 @@ mod button; mod log_scroller; mod menu; mod modal_menu; +mod popup_menu; mod screenshot; mod scroller; mod slider; @@ -14,6 +15,7 @@ pub use self::autocomplete::Autocomplete; pub use self::button::Button; pub use self::menu::{Menu, Position}; pub use self::modal_menu::ModalMenu; +pub(crate) use self::popup_menu::PopupMenu; pub(crate) use self::screenshot::{screenshot_current, screenshot_everything}; pub use self::scroller::Scroller; pub use self::slider::{ItemSlider, Slider, SliderWithTextBox, WarpingItemSlider}; diff --git a/ezgui/src/widgets/popup_menu.rs b/ezgui/src/widgets/popup_menu.rs new file mode 100644 index 0000000000..4ed788c8b7 --- /dev/null +++ b/ezgui/src/widgets/popup_menu.rs @@ -0,0 +1,157 @@ +use crate::layout::Widget; +use crate::{ + hotkey, layout, text, Choice, EventCtx, GfxCtx, InputResult, Key, Line, ScreenDims, ScreenPt, + ScreenRectangle, Text, +}; + +// Separate from ModalMenu. There are some similarities, but I'm not sure it's worth making both +// complex. + +pub struct PopupMenu { + prompt: Text, + choices: Vec>, + current_idx: usize, + standalone_layout: Option, + + top_left: ScreenPt, + dims: ScreenDims, +} + +impl PopupMenu { + pub fn new(prompt: Text, choices: Vec>, ctx: &EventCtx) -> PopupMenu { + let mut m = PopupMenu { + prompt, + choices, + current_idx: 0, + standalone_layout: Some(layout::ContainerOrientation::Centered), + + top_left: ScreenPt::new(0.0, 0.0), + dims: ScreenDims::new(0.0, 0.0), + }; + m.recalculate_dims(ctx); + m + } + + pub fn event(&mut self, ctx: &mut EventCtx) -> InputResult { + if let Some(o) = self.standalone_layout { + layout::stack_vertically(o, ctx.canvas, vec![self]); + self.recalculate_dims(ctx); + } + + // Handle the mouse + if ctx.redo_mouseover() { + let cursor = ctx.canvas.get_cursor_in_screen_space(); + let mut top_left = self.top_left; + top_left.y += ctx.canvas.text_dims(&self.prompt).1; + for idx in 0..self.choices.len() { + let rect = ScreenRectangle { + x1: top_left.x, + y1: top_left.y, + x2: top_left.x + self.dims.width, + y2: top_left.y + ctx.canvas.line_height, + }; + if rect.contains(cursor) { + self.current_idx = idx; + break; + } + top_left.y += ctx.canvas.line_height; + } + } + { + let choice = &self.choices[self.current_idx]; + if ctx.input.left_mouse_button_pressed() && choice.active { + return InputResult::Done(choice.label.clone(), choice.data.clone()); + } + } + + // Handle hotkeys + for choice in &self.choices { + if !choice.active { + continue; + } + if let Some(hotkey) = choice.hotkey { + if ctx.input.new_was_pressed(hotkey) { + return InputResult::Done(choice.label.clone(), choice.data.clone()); + } + } + } + + // Handle nav keys + if ctx.input.new_was_pressed(hotkey(Key::Enter).unwrap()) { + let choice = &self.choices[self.current_idx]; + if choice.active { + return InputResult::Done(choice.label.clone(), choice.data.clone()); + } else { + return InputResult::StillActive; + } + } else if ctx.input.new_was_pressed(hotkey(Key::UpArrow).unwrap()) { + if self.current_idx > 0 { + self.current_idx -= 1; + } + } else if ctx.input.new_was_pressed(hotkey(Key::DownArrow).unwrap()) { + if self.current_idx < self.choices.len() - 1 { + self.current_idx += 1; + } + } else if ctx.input.new_was_pressed(hotkey(Key::Escape).unwrap()) { + return InputResult::Canceled; + } + + InputResult::StillActive + } + + pub fn draw(&self, g: &mut GfxCtx) { + g.draw_text_at_screenspace_topleft(&self.calculate_txt(), self.top_left); + } + + pub fn current_choice(&self) -> &T { + &self.choices[self.current_idx].data + } + + fn recalculate_dims(&mut self, ctx: &EventCtx) { + let (w, h) = ctx.canvas.text_dims(&self.calculate_txt()); + self.dims = ScreenDims::new(w, h); + } + + fn calculate_txt(&self) -> Text { + let mut txt = self.prompt.clone(); + + for (idx, choice) in self.choices.iter().enumerate() { + if choice.active { + if let Some(key) = choice.hotkey { + txt.add_appended(vec![ + Line(key.describe()).fg(text::HOTKEY_COLOR), + Line(format!(" - {}", choice.label)), + ]); + } else { + txt.add(Line(&choice.label)); + } + } else { + if let Some(key) = choice.hotkey { + txt.add( + Line(format!("{} - {}", key.describe(), choice.label)) + .fg(text::INACTIVE_CHOICE_COLOR), + ); + } else { + txt.add(Line(&choice.label).fg(text::INACTIVE_CHOICE_COLOR)); + } + } + + // TODO BG color should be on the TextSpan, so this isn't so terrible? + if idx == self.current_idx { + txt.highlight_last_line(text::SELECTED_COLOR); + } + } + txt + } +} + +impl Widget for PopupMenu { + fn get_dims(&self) -> ScreenDims { + self.dims + } + + fn set_pos(&mut self, top_left: ScreenPt, _total_width: f64) { + self.top_left = top_left; + // TODO Stretch to fill total width if it's smaller than us? Or that's impossible + } +} diff --git a/ezgui/src/widgets/wizard.rs b/ezgui/src/widgets/wizard.rs index 187df2d990..948e8bfd41 100644 --- a/ezgui/src/widgets/wizard.rs +++ b/ezgui/src/widgets/wizard.rs @@ -1,6 +1,6 @@ use crate::widgets::log_scroller::LogScroller; use crate::widgets::text_box::TextBox; -use crate::widgets::{Menu, Position}; +use crate::widgets::PopupMenu; use crate::{ layout, Canvas, EventCtx, GfxCtx, InputResult, Key, MultiKey, SliderWithTextBox, Text, UserInput, @@ -12,7 +12,7 @@ use std::collections::VecDeque; pub struct Wizard { alive: bool, tb: Option, - menu: Option>>, + menu: Option>>, log_scroller: Option, slider: Option, @@ -65,7 +65,7 @@ impl Wizard { // The caller can ask for any type at any time pub fn current_menu_choice(&self) -> Option<&R> { if let Some(ref menu) = self.menu { - let item: &R = menu.current_choice()?.as_any().downcast_ref::()?; + let item: &R = menu.current_choice().as_any().downcast_ref::()?; return Some(item); } None @@ -286,26 +286,19 @@ impl<'a, 'b> WrappedWizard<'a, 'b> { )); return None; } - let mut boxed_choices = Vec::new(); - let mut inactive_labels = Vec::new(); - for choice in choices { - if !choice.active { - inactive_labels.push(choice.label.clone()); - } - boxed_choices.push((choice.hotkey, choice.label, choice.data.clone_box())); - } - let mut menu = Menu::new( + self.wizard.menu = Some(PopupMenu::new( Text::prompt(query), - vec![boxed_choices], - true, - false, - Position::ScreenCenter, - self.ctx.canvas, - ); - for label in inactive_labels { - menu.mark_active(&label, false); - } - self.wizard.menu = Some(menu); + choices + .into_iter() + .map(|c| Choice { + label: c.label, + data: c.data.clone_box(), + hotkey: c.hotkey, + active: c.active, + }) + .collect(), + self.ctx, + )); } assert!(self.wizard.alive); @@ -315,14 +308,7 @@ impl<'a, 'b> WrappedWizard<'a, 'b> { return None; } - let ev = self.ctx.input.use_event_directly().unwrap(); - match self - .wizard - .menu - .as_mut() - .unwrap() - .event(ev, self.ctx.canvas) - { + match self.wizard.menu.as_mut().unwrap().event(self.ctx) { InputResult::Canceled => { self.wizard.menu = None; self.wizard.alive = false; @@ -392,10 +378,10 @@ impl<'a, 'b> WrappedWizard<'a, 'b> { } pub struct Choice { - label: String, + pub(crate) label: String, pub data: T, - hotkey: Option, - active: bool, + pub(crate) hotkey: Option, + pub(crate) active: bool, } impl Choice { diff --git a/game/src/splash_screen.rs b/game/src/splash_screen.rs index 3aaea07409..70b8d80753 100644 --- a/game/src/splash_screen.rs +++ b/game/src/splash_screen.rs @@ -138,7 +138,6 @@ fn splash_screen( EventLoopMode::InputOnly }; - // TODO No hotkey for quit because it's just the normal menu escape? match wizard .choose("Welcome to A/B Street!", || { vec![