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:
Dustin Carlino 2019-10-12 10:24:07 -07:00
parent f548ded8cc
commit a077276275
7 changed files with 199 additions and 86 deletions

View File

@ -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) {

View File

@ -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
} }

View File

@ -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
}
} }
} }
} }

View File

@ -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};

View 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
}
}

View File

@ -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> {

View File

@ -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![