mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-12-27 00:12:55 +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.
|
// 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_x: f64,
|
||||||
pub(crate) cursor_y: 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_width: f64,
|
||||||
pub window_height: f64,
|
pub window_height: f64,
|
||||||
@ -88,16 +88,6 @@ impl Canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_event(&mut self, input: &mut UserInput) {
|
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
|
// Can't start dragging or zooming on top of covered area
|
||||||
let mouse_on_map = self.get_cursor_in_map_space().is_some();
|
let mouse_on_map = self.get_cursor_in_map_space().is_some();
|
||||||
if input.left_mouse_button_pressed() && mouse_on_map {
|
if input.left_mouse_button_pressed() && mouse_on_map {
|
||||||
@ -113,12 +103,6 @@ impl Canvas {
|
|||||||
self.zoom_towards_mouse(delta);
|
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) {
|
pub(crate) fn start_drawing(&self) {
|
||||||
|
@ -81,6 +81,25 @@ impl UserInput {
|
|||||||
canvas.lctrl_held = false;
|
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.
|
// Create the context menu here, even if one already existed.
|
||||||
if input.right_mouse_button_pressed() {
|
if input.right_mouse_button_pressed() {
|
||||||
assert!(!input.event_consumed);
|
assert!(!input.event_consumed);
|
||||||
@ -253,9 +272,6 @@ impl UserInput {
|
|||||||
self.event == Event::RightMouseButtonDown
|
self.event == Event::RightMouseButtonDown
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn window_gained_cursor(&mut self) -> bool {
|
|
||||||
self.event == Event::WindowGainedCursor
|
|
||||||
}
|
|
||||||
pub fn window_lost_cursor(&self) -> bool {
|
pub fn window_lost_cursor(&self) -> bool {
|
||||||
self.event == Event::WindowLostCursor
|
self.event == Event::WindowLostCursor
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,6 @@ struct Choice<T: Clone> {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum Position {
|
pub enum Position {
|
||||||
ScreenCenter,
|
|
||||||
SomeCornerAt(ScreenPt),
|
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> {
|
pub fn active_choices(&self) -> Vec<&T> {
|
||||||
self.choices
|
self.choices
|
||||||
.iter()
|
.iter()
|
||||||
@ -409,25 +403,6 @@ impl<T: Clone> Menu<T> {
|
|||||||
.collect()
|
.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) {
|
fn recalculate_geom(&mut self, canvas: &Canvas) {
|
||||||
let mut txt = self.prompt.clone();
|
let mut txt = self.prompt.clone();
|
||||||
if !self.hidden {
|
if !self.hidden {
|
||||||
@ -463,12 +438,6 @@ impl Position {
|
|||||||
fn get_top_left(&self, canvas: &Canvas, menu_dims: ScreenDims) -> ScreenPt {
|
fn get_top_left(&self, canvas: &Canvas, menu_dims: ScreenDims) -> ScreenPt {
|
||||||
match self {
|
match self {
|
||||||
Position::SomeCornerAt(pt) => menu_dims.top_left_for_corner(*pt, canvas),
|
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 log_scroller;
|
||||||
mod menu;
|
mod menu;
|
||||||
mod modal_menu;
|
mod modal_menu;
|
||||||
|
mod popup_menu;
|
||||||
mod screenshot;
|
mod screenshot;
|
||||||
mod scroller;
|
mod scroller;
|
||||||
mod slider;
|
mod slider;
|
||||||
@ -14,6 +15,7 @@ pub use self::autocomplete::Autocomplete;
|
|||||||
pub use self::button::Button;
|
pub use self::button::Button;
|
||||||
pub use self::menu::{Menu, Position};
|
pub use self::menu::{Menu, Position};
|
||||||
pub use self::modal_menu::ModalMenu;
|
pub use self::modal_menu::ModalMenu;
|
||||||
|
pub(crate) use self::popup_menu::PopupMenu;
|
||||||
pub(crate) use self::screenshot::{screenshot_current, screenshot_everything};
|
pub(crate) use self::screenshot::{screenshot_current, screenshot_everything};
|
||||||
pub use self::scroller::Scroller;
|
pub use self::scroller::Scroller;
|
||||||
pub use self::slider::{ItemSlider, Slider, SliderWithTextBox, WarpingItemSlider};
|
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::log_scroller::LogScroller;
|
||||||
use crate::widgets::text_box::TextBox;
|
use crate::widgets::text_box::TextBox;
|
||||||
use crate::widgets::{Menu, Position};
|
use crate::widgets::PopupMenu;
|
||||||
use crate::{
|
use crate::{
|
||||||
layout, Canvas, EventCtx, GfxCtx, InputResult, Key, MultiKey, SliderWithTextBox, Text,
|
layout, Canvas, EventCtx, GfxCtx, InputResult, Key, MultiKey, SliderWithTextBox, Text,
|
||||||
UserInput,
|
UserInput,
|
||||||
@ -12,7 +12,7 @@ use std::collections::VecDeque;
|
|||||||
pub struct Wizard {
|
pub struct Wizard {
|
||||||
alive: bool,
|
alive: bool,
|
||||||
tb: Option<TextBox>,
|
tb: Option<TextBox>,
|
||||||
menu: Option<Menu<Box<dyn Cloneable>>>,
|
menu: Option<PopupMenu<Box<dyn Cloneable>>>,
|
||||||
log_scroller: Option<LogScroller>,
|
log_scroller: Option<LogScroller>,
|
||||||
slider: Option<SliderWithTextBox>,
|
slider: Option<SliderWithTextBox>,
|
||||||
|
|
||||||
@ -65,7 +65,7 @@ impl Wizard {
|
|||||||
// The caller can ask for any type at any time
|
// The caller can ask for any type at any time
|
||||||
pub fn current_menu_choice<R: 'static + Cloneable>(&self) -> Option<&R> {
|
pub fn current_menu_choice<R: 'static + Cloneable>(&self) -> Option<&R> {
|
||||||
if let Some(ref menu) = self.menu {
|
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);
|
return Some(item);
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@ -286,26 +286,19 @@ impl<'a, 'b> WrappedWizard<'a, 'b> {
|
|||||||
));
|
));
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut boxed_choices = Vec::new();
|
self.wizard.menu = Some(PopupMenu::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(
|
|
||||||
Text::prompt(query),
|
Text::prompt(query),
|
||||||
vec![boxed_choices],
|
choices
|
||||||
true,
|
.into_iter()
|
||||||
false,
|
.map(|c| Choice {
|
||||||
Position::ScreenCenter,
|
label: c.label,
|
||||||
self.ctx.canvas,
|
data: c.data.clone_box(),
|
||||||
);
|
hotkey: c.hotkey,
|
||||||
for label in inactive_labels {
|
active: c.active,
|
||||||
menu.mark_active(&label, false);
|
})
|
||||||
}
|
.collect(),
|
||||||
self.wizard.menu = Some(menu);
|
self.ctx,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
assert!(self.wizard.alive);
|
assert!(self.wizard.alive);
|
||||||
@ -315,14 +308,7 @@ impl<'a, 'b> WrappedWizard<'a, 'b> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ev = self.ctx.input.use_event_directly().unwrap();
|
match self.wizard.menu.as_mut().unwrap().event(self.ctx) {
|
||||||
match self
|
|
||||||
.wizard
|
|
||||||
.menu
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.event(ev, self.ctx.canvas)
|
|
||||||
{
|
|
||||||
InputResult::Canceled => {
|
InputResult::Canceled => {
|
||||||
self.wizard.menu = None;
|
self.wizard.menu = None;
|
||||||
self.wizard.alive = false;
|
self.wizard.alive = false;
|
||||||
@ -392,10 +378,10 @@ impl<'a, 'b> WrappedWizard<'a, 'b> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Choice<T: Clone> {
|
pub struct Choice<T: Clone> {
|
||||||
label: String,
|
pub(crate) label: String,
|
||||||
pub data: T,
|
pub data: T,
|
||||||
hotkey: Option<MultiKey>,
|
pub(crate) hotkey: Option<MultiKey>,
|
||||||
active: bool,
|
pub(crate) active: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Clone> Choice<T> {
|
impl<T: Clone> Choice<T> {
|
||||||
|
@ -138,7 +138,6 @@ fn splash_screen(
|
|||||||
EventLoopMode::InputOnly
|
EventLoopMode::InputOnly
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO No hotkey for quit because it's just the normal menu escape?
|
|
||||||
match wizard
|
match wizard
|
||||||
.choose("Welcome to A/B Street!", || {
|
.choose("Welcome to A/B Street!", || {
|
||||||
vec![
|
vec![
|
||||||
|
Loading…
Reference in New Issue
Block a user