mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-12-26 07:52:05 +03:00
rewriting the wizard menu from scratch too. also making sure to update
master GUI state (where's the cursor?) up-front. that's independent from letting the canvas pan, a client decision
This commit is contained in:
parent
f548ded8cc
commit
a077276275
@ -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<ScreenPt>,
|
||||
pub(crate) left_mouse_drag_from: Option<ScreenPt>,
|
||||
|
||||
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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -39,7 +39,6 @@ struct Choice<T: Clone> {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Position {
|
||||
ScreenCenter,
|
||||
SomeCornerAt(ScreenPt),
|
||||
}
|
||||
|
||||
@ -391,11 +390,6 @@ impl<T: Clone> Menu<T> {
|
||||
}
|
||||
}
|
||||
|
||||
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<T: Clone> Menu<T> {
|
||||
.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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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};
|
||||
|
157
ezgui/src/widgets/popup_menu.rs
Normal file
157
ezgui/src/widgets/popup_menu.rs
Normal file
@ -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<T: Clone> {
|
||||
prompt: Text,
|
||||
choices: Vec<Choice<T>>,
|
||||
current_idx: usize,
|
||||
standalone_layout: Option<layout::ContainerOrientation>,
|
||||
|
||||
top_left: ScreenPt,
|
||||
dims: ScreenDims,
|
||||
}
|
||||
|
||||
impl<T: Clone> PopupMenu<T> {
|
||||
pub fn new(prompt: Text, choices: Vec<Choice<T>>, ctx: &EventCtx) -> PopupMenu<T> {
|
||||
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<T> {
|
||||
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<T: Clone> Widget for PopupMenu<T> {
|
||||
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
|
||||
}
|
||||
}
|
@ -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<TextBox>,
|
||||
menu: Option<Menu<Box<dyn Cloneable>>>,
|
||||
menu: Option<PopupMenu<Box<dyn Cloneable>>>,
|
||||
log_scroller: Option<LogScroller>,
|
||||
slider: Option<SliderWithTextBox>,
|
||||
|
||||
@ -65,7 +65,7 @@ impl Wizard {
|
||||
// The caller can ask for any type at any time
|
||||
pub fn current_menu_choice<R: 'static + Cloneable>(&self) -> Option<&R> {
|
||||
if let Some(ref menu) = self.menu {
|
||||
let item: &R = menu.current_choice()?.as_any().downcast_ref::<R>()?;
|
||||
let item: &R = menu.current_choice().as_any().downcast_ref::<R>()?;
|
||||
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<T: Clone> {
|
||||
label: String,
|
||||
pub(crate) label: String,
|
||||
pub data: T,
|
||||
hotkey: Option<MultiKey>,
|
||||
active: bool,
|
||||
pub(crate) hotkey: Option<MultiKey>,
|
||||
pub(crate) active: bool,
|
||||
}
|
||||
|
||||
impl<T: Clone> Choice<T> {
|
||||
|
@ -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![
|
||||
|
Loading…
Reference in New Issue
Block a user