make popup menus use scrolling

This commit is contained in:
Dustin Carlino 2020-01-02 11:35:41 -06:00
parent c9b8438bd5
commit 3bd4a7220d
6 changed files with 142 additions and 70 deletions

View File

@ -1,14 +1,18 @@
use crate::layout::Widget;
use crate::widgets::PopupMenu;
use crate::{
Button, Color, DrawBoth, EventCtx, Filler, GeomBatch, GfxCtx, Histogram, HorizontalAlignment,
JustDraw, Plot, ScreenDims, ScreenPt, ScreenRectangle, Slider, Text, VerticalAlignment,
};
use abstutil::Cloneable;
use geom::{Distance, Duration, Polygon};
use std::collections::{HashMap, HashSet};
use stretch::geometry::{Rect, Size};
use stretch::node::{Node, Stretch};
use stretch::style::{AlignItems, Dimension, FlexDirection, FlexWrap, JustifyContent, Style};
type Menu = PopupMenu<Box<dyn Cloneable>>;
pub struct ManagedWidget {
widget: WidgetType,
style: LayoutStyle,
@ -20,6 +24,7 @@ enum WidgetType {
Draw(JustDraw),
Btn(Button),
Slider(String),
Menu(String),
Filler(String),
// TODO Sadness. Can't have some kind of wildcard generic here?
DurationPlot(Plot<Duration>),
@ -166,6 +171,10 @@ impl ManagedWidget {
ManagedWidget::new(WidgetType::Slider(label.to_string()))
}
pub fn menu(label: &str) -> ManagedWidget {
ManagedWidget::new(WidgetType::Menu(label.to_string()))
}
pub fn filler(label: &str) -> ManagedWidget {
ManagedWidget::new(WidgetType::Filler(label.to_string()))
}
@ -197,6 +206,7 @@ impl ManagedWidget {
&mut self,
ctx: &mut EventCtx,
sliders: &mut HashMap<String, Slider>,
menus: &mut HashMap<String, Menu>,
) -> Option<Outcome> {
match self.widget {
WidgetType::Draw(_) => {}
@ -209,13 +219,16 @@ impl ManagedWidget {
WidgetType::Slider(ref name) => {
sliders.get_mut(name).unwrap().event(ctx);
}
WidgetType::Menu(ref name) => {
menus.get_mut(name).unwrap().event(ctx);
}
WidgetType::Filler(_)
| WidgetType::DurationPlot(_)
| WidgetType::UsizePlot(_)
| WidgetType::Histogram(_) => {}
WidgetType::Row(ref mut widgets) | WidgetType::Column(ref mut widgets) => {
for w in widgets {
if let Some(o) = w.event(ctx, sliders) {
if let Some(o) = w.event(ctx, sliders, menus) {
return Some(o);
}
}
@ -224,7 +237,12 @@ impl ManagedWidget {
None
}
fn draw(&self, g: &mut GfxCtx, sliders: &HashMap<String, Slider>) {
fn draw(
&self,
g: &mut GfxCtx,
sliders: &HashMap<String, Slider>,
menus: &HashMap<String, Menu>,
) {
if let Some(ref bg) = self.bg {
bg.redraw(ScreenPt::new(self.rect.x1, self.rect.y1), g);
}
@ -233,13 +251,14 @@ impl ManagedWidget {
WidgetType::Draw(ref j) => j.draw(g),
WidgetType::Btn(ref btn) => btn.draw(g),
WidgetType::Slider(ref name) => sliders[name].draw(g),
WidgetType::Menu(ref name) => menus[name].draw(g),
WidgetType::Filler(_) => {}
WidgetType::DurationPlot(ref plot) => plot.draw(g),
WidgetType::UsizePlot(ref plot) => plot.draw(g),
WidgetType::Histogram(ref hgram) => hgram.draw(g),
WidgetType::Row(ref widgets) | WidgetType::Column(ref widgets) => {
for w in widgets {
w.draw(g, sliders);
w.draw(g, sliders, menus);
}
}
}
@ -250,6 +269,7 @@ impl ManagedWidget {
&self,
parent: Node,
sliders: &HashMap<String, Slider>,
menus: &HashMap<String, Menu>,
fillers: &HashMap<String, Filler>,
stretch: &mut Stretch,
nodes: &mut Vec<Node>,
@ -259,6 +279,7 @@ impl ManagedWidget {
WidgetType::Draw(ref widget) => widget,
WidgetType::Btn(ref widget) => widget,
WidgetType::Slider(ref name) => &sliders[name],
WidgetType::Menu(ref name) => &menus[name],
WidgetType::Filler(ref name) => &fillers[name],
WidgetType::DurationPlot(ref widget) => widget,
WidgetType::UsizePlot(ref widget) => widget,
@ -272,7 +293,7 @@ impl ManagedWidget {
let row = stretch.new_node(style, Vec::new()).unwrap();
nodes.push(row);
for widget in widgets {
widget.get_flexbox(row, sliders, fillers, stretch, nodes);
widget.get_flexbox(row, sliders, menus, fillers, stretch, nodes);
}
stretch.add_child(parent, row).unwrap();
return;
@ -286,7 +307,7 @@ impl ManagedWidget {
let col = stretch.new_node(style, Vec::new()).unwrap();
nodes.push(col);
for widget in widgets {
widget.get_flexbox(col, sliders, fillers, stretch, nodes);
widget.get_flexbox(col, sliders, menus, fillers, stretch, nodes);
}
stretch.add_child(parent, col).unwrap();
return;
@ -309,6 +330,7 @@ impl ManagedWidget {
fn apply_flexbox(
&mut self,
sliders: &mut HashMap<String, Slider>,
menus: &mut HashMap<String, Menu>,
fillers: &mut HashMap<String, Filler>,
stretch: &Stretch,
nodes: &mut Vec<Node>,
@ -358,6 +380,9 @@ impl ManagedWidget {
WidgetType::Slider(ref name) => {
sliders.get_mut(name).unwrap().set_pos(top_left);
}
WidgetType::Menu(ref name) => {
menus.get_mut(name).unwrap().set_pos(top_left);
}
WidgetType::Filler(ref name) => {
fillers.get_mut(name).unwrap().set_pos(top_left);
}
@ -375,6 +400,7 @@ impl ManagedWidget {
for widget in widgets {
widget.apply_flexbox(
sliders,
menus,
fillers,
stretch,
nodes,
@ -389,6 +415,7 @@ impl ManagedWidget {
for widget in widgets {
widget.apply_flexbox(
sliders,
menus,
fillers,
stretch,
nodes,
@ -406,6 +433,7 @@ impl ManagedWidget {
match self.widget {
WidgetType::Draw(_)
| WidgetType::Slider(_)
| WidgetType::Menu(_)
| WidgetType::Filler(_)
| WidgetType::DurationPlot(_)
| WidgetType::UsizePlot(_) => {}
@ -433,6 +461,7 @@ pub struct Composite {
pos: CompositePosition,
sliders: HashMap<String, Slider>,
menus: HashMap<String, Menu>,
fillers: HashMap<String, Filler>,
// TODO This doesn't clip. There's no way to express that the scrollable thing should occupy a
@ -453,6 +482,8 @@ enum CompositePosition {
const SCROLL_SPEED: f64 = 5.0;
// TODO These APIs aren't composable. Need a builer pattern or ideally, to scrape all the special
// objects from the tree.
impl Composite {
fn new(top_level: ManagedWidget, pos: CompositePosition) -> Composite {
Composite {
@ -460,6 +491,7 @@ impl Composite {
pos,
sliders: HashMap::new(),
menus: HashMap::new(),
fillers: HashMap::new(),
scrollable: false,
@ -516,7 +548,11 @@ impl Composite {
c
}
pub fn scrollable(ctx: &EventCtx, top_level: ManagedWidget) -> Composite {
pub fn scrollable(
ctx: &EventCtx,
top_level: ManagedWidget,
menus: Vec<(&str, Menu)>,
) -> Composite {
let mut c = Composite::new(
top_level,
CompositePosition::Aligned(HorizontalAlignment::Left, VerticalAlignment::Top),
@ -528,6 +564,9 @@ impl Composite {
Slider::vertical(ctx, ctx.canvas.window_height - 100.0),
);
c.top_level = ManagedWidget::row(vec![c.top_level, ManagedWidget::slider("scrollbar")]);
for (name, menu) in menus {
c.menus.insert(name.to_string(), menu);
}
c.recompute_layout(ctx);
c
}
@ -556,8 +595,14 @@ impl Composite {
.unwrap();
let mut nodes = vec![];
self.top_level
.get_flexbox(root, &self.sliders, &self.fillers, &mut stretch, &mut nodes);
self.top_level.get_flexbox(
root,
&self.sliders,
&self.menus,
&self.fillers,
&mut stretch,
&mut nodes,
);
nodes.reverse();
stretch.compute_layout(root, Size::undefined()).unwrap();
@ -576,6 +621,7 @@ impl Composite {
let offset = self.scroll_y_offset(ctx);
self.top_level.apply_flexbox(
&mut self.sliders,
&mut self.menus,
&mut self.fillers,
&stretch,
&mut nodes,
@ -628,7 +674,9 @@ impl Composite {
}
let before = self.scroll_y_offset(ctx);
let result = self.top_level.event(ctx, &mut self.sliders);
let result = self
.top_level
.event(ctx, &mut self.sliders, &mut self.menus);
if self.scroll_y_offset(ctx) != before {
self.recompute_layout(ctx);
}
@ -637,7 +685,7 @@ impl Composite {
pub fn draw(&self, g: &mut GfxCtx) {
g.canvas.mark_covered_area(self.top_level.rect.clone());
self.top_level.draw(g, &self.sliders);
self.top_level.draw(g, &self.sliders, &self.menus);
}
pub fn get_all_click_actions(&self) -> HashSet<String> {
@ -666,6 +714,10 @@ impl Composite {
self.sliders.remove(name).unwrap()
}
pub fn menu(&self, name: &str) -> &Menu {
&self.menus[name]
}
pub fn filler_rect(&self, name: &str) -> ScreenRectangle {
let f = &self.fillers[name];
ScreenRectangle::top_left(f.top_left, f.dims)

View File

@ -1,6 +1,6 @@
use crate::layout::Widget;
use crate::{
hotkey, layout, text, Choice, EventCtx, GfxCtx, InputResult, Key, Line, ScreenDims, ScreenPt,
hotkey, text, Choice, EventCtx, GfxCtx, InputResult, Key, Line, ScreenDims, ScreenPt,
ScreenRectangle, Text,
};
@ -8,29 +8,22 @@ use crate::{
// complex.
pub struct PopupMenu<T: Clone> {
prompt: Text,
choices: Vec<Choice<T>>,
current_idx: usize,
standalone_layout: Option<layout::ContainerOrientation>,
click_to_cancel: bool,
pub(crate) state: InputResult<T>,
top_left: ScreenPt,
dims: ScreenDims,
}
impl<T: Clone> PopupMenu<T> {
pub fn new(
prompt: Text,
choices: Vec<Choice<T>>,
ctx: &EventCtx,
click_to_cancel: bool,
) -> PopupMenu<T> {
pub fn new(choices: Vec<Choice<T>>, ctx: &EventCtx) -> PopupMenu<T> {
let mut m = PopupMenu {
prompt,
choices,
current_idx: 0,
standalone_layout: Some(layout::ContainerOrientation::Centered),
click_to_cancel,
state: InputResult::StillActive,
top_left: ScreenPt::new(0.0, 0.0),
dims: ScreenDims::new(0.0, 0.0),
@ -39,17 +32,16 @@ impl<T: Clone> PopupMenu<T> {
m
}
pub fn event(&mut self, ctx: &mut EventCtx) -> InputResult<T> {
if let Some(o) = self.standalone_layout {
layout::stack_vertically(o, ctx, vec![self]);
self.recalculate_dims(ctx);
pub fn event(&mut self, ctx: &mut EventCtx) {
match self.state {
InputResult::StillActive => {}
_ => unreachable!(),
}
// 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.text_dims(&self.prompt).height;
for idx in 0..self.choices.len() {
let rect = ScreenRectangle {
x1: top_left.x,
@ -69,7 +61,6 @@ impl<T: Clone> PopupMenu<T> {
if ctx.normal_left_click() {
// Did we actually click the entry?
let mut top_left = self.top_left;
top_left.y += ctx.text_dims(&self.prompt).height;
top_left.y += ctx.default_line_height() * (self.current_idx as f64);
let rect = ScreenRectangle {
x1: top_left.x,
@ -79,10 +70,9 @@ impl<T: Clone> PopupMenu<T> {
};
if rect.contains(ctx.canvas.get_cursor_in_screen_space()) {
if choice.active {
return InputResult::Done(choice.label.clone(), choice.data.clone());
self.state = InputResult::Done(choice.label.clone(), choice.data.clone());
return;
}
} else if self.click_to_cancel {
return InputResult::Canceled;
}
}
}
@ -94,7 +84,8 @@ impl<T: Clone> PopupMenu<T> {
}
if let Some(hotkey) = choice.hotkey {
if ctx.input.new_was_pressed(hotkey) {
return InputResult::Done(choice.label.clone(), choice.data.clone());
self.state = InputResult::Done(choice.label.clone(), choice.data.clone());
return;
}
}
}
@ -103,9 +94,10 @@ impl<T: Clone> PopupMenu<T> {
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());
self.state = InputResult::Done(choice.label.clone(), choice.data.clone());
return;
} else {
return InputResult::StillActive;
return;
}
} else if ctx.input.new_was_pressed(hotkey(Key::UpArrow).unwrap()) {
if self.current_idx > 0 {
@ -116,10 +108,9 @@ impl<T: Clone> PopupMenu<T> {
self.current_idx += 1;
}
} else if ctx.input.new_was_pressed(hotkey(Key::Escape).unwrap()) {
return InputResult::Canceled;
self.state = InputResult::Canceled;
return;
}
InputResult::StillActive
}
pub fn draw(&self, g: &mut GfxCtx) {
@ -135,7 +126,7 @@ impl<T: Clone> PopupMenu<T> {
}
fn calculate_txt(&self) -> Text {
let mut txt = self.prompt.clone();
let mut txt = Text::new();
for (idx, choice) in self.choices.iter().enumerate() {
if choice.active {

View File

@ -1,7 +1,10 @@
use crate::widgets::log_scroller::LogScroller;
use crate::widgets::text_box::TextBox;
use crate::widgets::PopupMenu;
use crate::{layout, EventCtx, GfxCtx, InputResult, Key, MultiKey, SliderWithTextBox, Text};
use crate::{
layout, Color, Composite, EventCtx, GfxCtx, InputResult, Key, ManagedWidget, MultiKey,
SliderWithTextBox, Text,
};
use abstutil::Cloneable;
use geom::Time;
use std::collections::VecDeque;
@ -9,7 +12,7 @@ use std::collections::VecDeque;
pub struct Wizard {
alive: bool,
tb: Option<TextBox>,
menu: Option<PopupMenu<Box<dyn Cloneable>>>,
menu_comp: Option<Composite>,
log_scroller: Option<LogScroller>,
slider: Option<SliderWithTextBox>,
@ -22,7 +25,7 @@ impl Wizard {
Wizard {
alive: true,
tb: None,
menu: None,
menu_comp: None,
log_scroller: None,
slider: None,
confirmed_state: Vec::new(),
@ -30,8 +33,8 @@ impl Wizard {
}
pub fn draw(&self, g: &mut GfxCtx) {
if let Some(ref menu) = self.menu {
menu.draw(g);
if let Some(ref comp) = self.menu_comp {
comp.draw(g);
}
if let Some(ref tb) = self.tb {
tb.draw(g);
@ -61,8 +64,12 @@ 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>()?;
if let Some(ref comp) = self.menu_comp {
let item: &R = comp
.menu("menu")
.current_choice()
.as_any()
.downcast_ref::<R>()?;
return Some(item);
}
None
@ -242,7 +249,7 @@ impl<'a, 'b> WrappedWizard<'a, 'b> {
}
// If the menu was empty, wait for the user to acknowledge the text-box before aborting the
// wizard.
// wizard
if self.wizard.log_scroller.is_some() {
if self
.wizard
@ -257,7 +264,7 @@ impl<'a, 'b> WrappedWizard<'a, 'b> {
return None;
}
if self.wizard.menu.is_none() {
if self.wizard.menu_comp.is_none() {
let choices: Vec<Choice<R>> = choices_generator();
if choices.is_empty() {
self.wizard.log_scroller = Some(LogScroller::new(
@ -266,19 +273,28 @@ impl<'a, 'b> WrappedWizard<'a, 'b> {
));
return None;
}
self.wizard.menu = Some(PopupMenu::new(
Text::prompt(query),
choices
.into_iter()
.map(|c| Choice {
label: c.label,
data: c.data.clone_box(),
hotkey: c.hotkey,
active: c.active,
})
.collect(),
self.wizard.menu_comp = Some(Composite::scrollable(
self.ctx,
false,
ManagedWidget::col(vec![
ManagedWidget::draw_text(self.ctx, Text::prompt(query)),
ManagedWidget::menu("menu"),
])
.bg(Color::grey(0.4)),
vec![(
"menu",
PopupMenu::new(
choices
.into_iter()
.map(|c| Choice {
label: c.label,
data: c.data.clone_box(),
hotkey: c.hotkey,
active: c.active,
})
.collect(),
self.ctx,
),
)],
));
}
@ -289,22 +305,26 @@ impl<'a, 'b> WrappedWizard<'a, 'b> {
return None;
}
match self.wizard.menu.as_mut().unwrap().event(self.ctx) {
self.wizard.menu_comp.as_mut().unwrap().event(self.ctx);
let (result, destroy) = match self.wizard.menu_comp.as_ref().unwrap().menu("menu").state {
InputResult::Canceled => {
self.wizard.menu = None;
self.wizard.alive = false;
None
(None, true)
}
InputResult::StillActive => None,
InputResult::Done(choice, item) => {
self.wizard.menu = None;
InputResult::StillActive => (None, false),
InputResult::Done(ref choice, ref item) => {
self.wizard
.confirmed_state
.push(Box::new((choice.to_string(), item.clone())));
let downcasted_item: &R = item.as_any().downcast_ref::<R>().unwrap();
Some((choice, downcasted_item.clone()))
(Some((choice.to_string(), downcasted_item.clone())), true)
}
};
if destroy {
self.wizard.menu_comp = None;
}
result
}
pub fn choose_string<S: Into<String>, F: Fn() -> Vec<S>>(
@ -363,7 +383,7 @@ impl<'a, 'b> WrappedWizard<'a, 'b> {
// If the control flow through a wizard block needs to change, might need to call this.
pub fn reset(&mut self) {
assert!(self.wizard.tb.is_none());
assert!(self.wizard.menu.is_none());
assert!(self.wizard.menu_comp.is_none());
assert!(self.wizard.log_scroller.is_none());
assert!(self.wizard.slider.is_none());
self.wizard.confirmed_state.clear();

View File

@ -78,7 +78,11 @@ impl InfoPanel {
}
InfoPanel {
composite: Composite::scrollable(ctx, ManagedWidget::col(col).bg(Color::grey(0.3))),
composite: Composite::scrollable(
ctx,
ManagedWidget::col(col).bg(Color::grey(0.3)),
Vec::new(),
),
}
}
}

View File

@ -293,5 +293,9 @@ fn make_diagram(i: IntersectionID, selected: usize, ui: &UI, ctx: &EventCtx) ->
);
}
Composite::scrollable(ctx, ManagedWidget::col(col).bg(Color::hex("#545454")))
Composite::scrollable(
ctx,
ManagedWidget::col(col).bg(Color::hex("#545454")),
Vec::new(),
)
}

View File

@ -63,6 +63,7 @@ pub fn make(ctx: &EventCtx, ui: &UI, tab: Tab) -> Box<dyn State> {
.padding(10),
content,
]),
Vec::new(),
))
.cb("BACK", Box::new(|_, _| Some(Transition::Pop)));
for (t, label) in tab_data {