mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-12-25 23:43:25 +03:00
render separators between groups of menu items. very messy
implementation, but many other ideas fell through, and this works.
This commit is contained in:
parent
f7b5cf9a9f
commit
8d1581241f
@ -39,10 +39,10 @@ impl ContextMenu {
|
||||
} else {
|
||||
ContextMenu::Displaying(Menu::new(
|
||||
Text::new(),
|
||||
actions
|
||||
vec![actions
|
||||
.into_iter()
|
||||
.map(|(key, action)| (hotkey(key), action, key))
|
||||
.collect(),
|
||||
.collect()],
|
||||
false,
|
||||
false,
|
||||
Position::SomeCornerAt(origin),
|
||||
|
@ -21,13 +21,4 @@ impl ScreenRectangle {
|
||||
pub fn contains(&self, pt: ScreenPt) -> bool {
|
||||
pt.x >= self.x1 && pt.x <= self.x2 && pt.y >= self.y1 && pt.y <= self.y2
|
||||
}
|
||||
|
||||
pub fn translate(&self, dx: f64, dy: f64) -> ScreenRectangle {
|
||||
ScreenRectangle {
|
||||
x1: self.x1 + dx,
|
||||
y1: self.y1 + dy,
|
||||
x2: self.x2 + dx,
|
||||
y2: self.y2 + dy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ use nom::{alt, char, do_parse, many1, named, separated_pair, take_till1, take_un
|
||||
use textwrap;
|
||||
|
||||
const FG_COLOR: Color = Color::WHITE;
|
||||
const BG_COLOR: Color = Color::grey(0.2);
|
||||
pub const BG_COLOR: Color = Color::grey(0.2);
|
||||
pub const PROMPT_COLOR: Color = Color::BLUE;
|
||||
pub const SELECTED_COLOR: Color = Color::RED;
|
||||
pub const HOTKEY_COLOR: Color = Color::GREEN;
|
||||
@ -42,6 +42,7 @@ pub struct Text {
|
||||
// The bg_color will cover the entire block, but some lines can have extra highlighting.
|
||||
lines: Vec<(Option<Color>, Vec<TextSpan>)>,
|
||||
bg_color: Option<Color>,
|
||||
pub(crate) override_width: Option<f64>,
|
||||
}
|
||||
|
||||
impl Text {
|
||||
@ -49,6 +50,7 @@ impl Text {
|
||||
Text {
|
||||
lines: Vec::new(),
|
||||
bg_color: Some(BG_COLOR),
|
||||
override_width: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,6 +64,7 @@ impl Text {
|
||||
Text {
|
||||
lines: Vec::new(),
|
||||
bg_color,
|
||||
override_width: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,7 +176,10 @@ impl Text {
|
||||
max_width = max_width.max(width);
|
||||
height += canvas.line_height(max_size);
|
||||
}
|
||||
(f64::from(max_width), height)
|
||||
(
|
||||
self.override_width.unwrap_or_else(|| f64::from(max_width)),
|
||||
height,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,27 +1,33 @@
|
||||
use crate::screen_geom::ScreenRectangle;
|
||||
use crate::{
|
||||
hotkey, lctrl, text, Canvas, Event, GfxCtx, InputResult, Key, MultiKey, ScreenPt, Text,
|
||||
hotkey, lctrl, text, Canvas, Color, Event, GfxCtx, InputResult, Key, MultiKey, ScreenPt, Text,
|
||||
};
|
||||
use geom::{Distance, Polygon, Pt2D};
|
||||
use std::collections::HashSet;
|
||||
|
||||
// Stores some associated data with each choice
|
||||
pub struct Menu<T: Clone> {
|
||||
prompt: Text,
|
||||
// The bool is whether this choice is active or not
|
||||
choices: Vec<(Option<MultiKey>, String, bool, T)>,
|
||||
choices: Vec<Choice<T>>,
|
||||
current_idx: Option<usize>,
|
||||
mouse_in_bounds: bool,
|
||||
keys_enabled: bool,
|
||||
hideable: bool,
|
||||
hidden: bool,
|
||||
pos: Position,
|
||||
geom: Geometry,
|
||||
top_left: ScreenPt,
|
||||
total_width: f64,
|
||||
// dy1 values of the separator half-rows
|
||||
separators: Vec<f64>,
|
||||
}
|
||||
|
||||
struct Geometry {
|
||||
row_height: f64,
|
||||
top_left: ScreenPt,
|
||||
first_choice_row: ScreenRectangle,
|
||||
struct Choice<T: Clone> {
|
||||
hotkey: Option<MultiKey>,
|
||||
label: String,
|
||||
active: bool,
|
||||
data: T,
|
||||
// How far is the top of this row below the prompt's bottom?
|
||||
dy1: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -31,105 +37,66 @@ pub enum Position {
|
||||
TopRightOfScreen,
|
||||
}
|
||||
|
||||
impl Position {
|
||||
fn geometry<T>(
|
||||
&self,
|
||||
canvas: &Canvas,
|
||||
prompt: Text,
|
||||
choices: &Vec<(Option<MultiKey>, String, bool, T)>,
|
||||
) -> Geometry {
|
||||
// This is actually a constant, effectively...
|
||||
let row_height = canvas.line_height(text::FONT_SIZE);
|
||||
|
||||
let mut txt = prompt;
|
||||
let (_, prompt_height) = canvas.text_dims(&txt);
|
||||
for (hotkey, choice, _, _) in choices {
|
||||
if let Some(key) = hotkey {
|
||||
txt.add_line(format!("{} - {}", key.describe(), choice));
|
||||
} else {
|
||||
txt.add_line(choice.to_string());
|
||||
}
|
||||
}
|
||||
let (total_width, total_height) = canvas.text_dims(&txt);
|
||||
|
||||
let top_left = match self {
|
||||
Position::SomeCornerAt(pt) => {
|
||||
// TODO Ideally also avoid covered canvas areas (modal menu)
|
||||
if pt.x + total_width < canvas.window_width {
|
||||
// pt.x is the left corner
|
||||
if pt.y + total_height < canvas.window_height {
|
||||
// pt.y is the top corner
|
||||
*pt
|
||||
} else {
|
||||
// pt.y is the bottom corner
|
||||
ScreenPt::new(pt.x, pt.y - total_height)
|
||||
}
|
||||
} else {
|
||||
// pt.x is the right corner
|
||||
if pt.y + total_height < canvas.window_height {
|
||||
// pt.y is the top corner
|
||||
ScreenPt::new(pt.x - total_width, pt.y)
|
||||
} else {
|
||||
// pt.y is the bottom corner
|
||||
ScreenPt::new(pt.x - total_width, pt.y - total_height)
|
||||
}
|
||||
}
|
||||
}
|
||||
Position::ScreenCenter => {
|
||||
let mut pt = canvas.center_to_screen_pt();
|
||||
pt.x -= total_width / 2.0;
|
||||
pt.y -= total_height / 2.0;
|
||||
pt
|
||||
}
|
||||
Position::TopRightOfScreen => ScreenPt::new(canvas.window_width - total_width, 0.0),
|
||||
};
|
||||
Geometry {
|
||||
row_height,
|
||||
top_left,
|
||||
first_choice_row: ScreenRectangle {
|
||||
x1: top_left.x,
|
||||
y1: top_left.y + prompt_height,
|
||||
x2: top_left.x + total_width,
|
||||
y2: top_left.y + prompt_height + row_height,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Menu<T> {
|
||||
pub fn new(
|
||||
prompt: Text,
|
||||
raw_choices: Vec<(Option<MultiKey>, String, T)>,
|
||||
mut prompt: Text,
|
||||
raw_choice_groups: Vec<Vec<(Option<MultiKey>, String, T)>>,
|
||||
keys_enabled: bool,
|
||||
hideable: bool,
|
||||
pos: Position,
|
||||
canvas: &Canvas,
|
||||
) -> Menu<T> {
|
||||
if raw_choices.is_empty() {
|
||||
let row_height = row_height(canvas);
|
||||
|
||||
let mut used_keys = HashSet::new();
|
||||
let mut used_labels = HashSet::new();
|
||||
let mut choices = Vec::new();
|
||||
let mut txt = prompt.clone();
|
||||
let prompt_lines = prompt.num_lines();
|
||||
let mut separator_offset = 0.0;
|
||||
let mut separators = Vec::new();
|
||||
for group in raw_choice_groups {
|
||||
for (maybe_key, label, data) in group {
|
||||
if let Some(key) = maybe_key {
|
||||
if used_keys.contains(&key) {
|
||||
panic!("Menu for {:?} uses {} twice", prompt, key.describe());
|
||||
}
|
||||
used_keys.insert(key);
|
||||
}
|
||||
|
||||
if used_labels.contains(&label) {
|
||||
panic!("Menu for {:?} has two entries for {}", prompt, label);
|
||||
}
|
||||
used_labels.insert(label.clone());
|
||||
|
||||
let dy1 = ((txt.num_lines() - prompt_lines) as f64) * row_height + separator_offset;
|
||||
if let Some(key) = maybe_key {
|
||||
txt.add_line(format!("{} - {}", key.describe(), label));
|
||||
} else {
|
||||
txt.add_line(label.clone());
|
||||
}
|
||||
|
||||
choices.push(Choice {
|
||||
hotkey: maybe_key,
|
||||
label,
|
||||
active: true,
|
||||
data,
|
||||
dy1,
|
||||
});
|
||||
}
|
||||
separator_offset += row_height / 2.0;
|
||||
separators.push(choices.last().unwrap().dy1 + row_height);
|
||||
}
|
||||
// The last one would be at the very bottom of the menu
|
||||
separators.pop();
|
||||
|
||||
if choices.is_empty() {
|
||||
panic!("Can't create a menu without choices for {:?}", prompt);
|
||||
}
|
||||
let mut used_keys = HashSet::new();
|
||||
let mut used_actions = HashSet::new();
|
||||
for (maybe_key, name, _) in &raw_choices {
|
||||
if let Some(key) = maybe_key {
|
||||
if used_keys.contains(&key) {
|
||||
panic!("Menu for {:?} uses {} twice", prompt, key.describe());
|
||||
}
|
||||
used_keys.insert(key);
|
||||
}
|
||||
|
||||
if used_actions.contains(name) {
|
||||
panic!("Menu for {:?} has two entries for {}", prompt, name);
|
||||
}
|
||||
used_actions.insert(name.to_string());
|
||||
}
|
||||
|
||||
// All choices start active.
|
||||
let choices = raw_choices
|
||||
.into_iter()
|
||||
.map(|(key, choice, data)| (key, choice, true, data))
|
||||
.collect();
|
||||
let geom = pos.geometry(canvas, prompt.clone(), &choices);
|
||||
let (total_width, total_height) = canvas.text_dims(&txt);
|
||||
let top_left = pos.get_top_left(canvas, total_width, total_height);
|
||||
prompt.override_width = Some(total_width);
|
||||
|
||||
Menu {
|
||||
prompt,
|
||||
@ -141,7 +108,9 @@ impl<T: Clone> Menu<T> {
|
||||
pos,
|
||||
hideable,
|
||||
hidden: false,
|
||||
geom,
|
||||
top_left,
|
||||
total_width,
|
||||
separators,
|
||||
}
|
||||
}
|
||||
|
||||
@ -150,9 +119,9 @@ impl<T: Clone> Menu<T> {
|
||||
// Handle the mouse
|
||||
if ev == Event::LeftMouseButtonDown {
|
||||
if let Some(i) = self.current_idx {
|
||||
let (_, choice, active, data) = self.choices[i].clone();
|
||||
if active && self.mouse_in_bounds {
|
||||
return InputResult::Done(choice, data);
|
||||
let choice = &self.choices[i];
|
||||
if choice.active && self.mouse_in_bounds {
|
||||
return InputResult::Done(choice.label.clone(), choice.data.clone());
|
||||
} else {
|
||||
return InputResult::StillActive;
|
||||
}
|
||||
@ -163,15 +132,22 @@ impl<T: Clone> Menu<T> {
|
||||
return InputResult::Canceled;
|
||||
} else if let Event::MouseMovedTo(pt) = ev {
|
||||
if !canvas.is_dragging() {
|
||||
for i in 0..self.choices.len() {
|
||||
if self.choices[i].2
|
||||
&& self
|
||||
.geom
|
||||
.first_choice_row
|
||||
.translate(0.0, (i as f64) * self.geom.row_height)
|
||||
.contains(pt)
|
||||
let row_height = row_height(canvas);
|
||||
for (idx, choice) in self.choices.iter().enumerate() {
|
||||
let y1 = self.top_left.y
|
||||
+ choice.dy1
|
||||
+ (self.prompt.num_lines() as f64) * row_height;
|
||||
|
||||
if choice.active
|
||||
&& (ScreenRectangle {
|
||||
x1: self.top_left.x,
|
||||
y1,
|
||||
x2: self.top_left.x + self.total_width,
|
||||
y2: y1 + row_height,
|
||||
}
|
||||
.contains(pt))
|
||||
{
|
||||
self.current_idx = Some(i);
|
||||
self.current_idx = Some(idx);
|
||||
self.mouse_in_bounds = true;
|
||||
return InputResult::StillActive;
|
||||
}
|
||||
@ -188,9 +164,9 @@ impl<T: Clone> Menu<T> {
|
||||
if self.keys_enabled {
|
||||
let idx = self.current_idx.unwrap();
|
||||
if ev == Event::KeyPress(Key::Enter) {
|
||||
let (_, name, active, data) = self.choices[idx].clone();
|
||||
if active {
|
||||
return InputResult::Done(name, data);
|
||||
let choice = &self.choices[idx];
|
||||
if choice.active {
|
||||
return InputResult::Done(choice.label.clone(), choice.data.clone());
|
||||
} else {
|
||||
return InputResult::StillActive;
|
||||
}
|
||||
@ -225,9 +201,9 @@ impl<T: Clone> Menu<T> {
|
||||
} else {
|
||||
hotkey(key)
|
||||
};
|
||||
for (maybe_key, choice, active, data) in &self.choices {
|
||||
if *active && pressed == *maybe_key {
|
||||
return InputResult::Done(choice.to_string(), data.clone());
|
||||
for choice in &self.choices {
|
||||
if choice.active && pressed == choice.hotkey {
|
||||
return InputResult::Done(choice.label.clone(), choice.data.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -245,71 +221,116 @@ impl<T: Clone> Menu<T> {
|
||||
}
|
||||
|
||||
pub fn draw(&self, g: &mut GfxCtx) {
|
||||
let mut txt = self.prompt.clone();
|
||||
if !self.hidden {
|
||||
for (idx, (hotkey, choice, active, _)) in self.choices.iter().enumerate() {
|
||||
let bg = if Some(idx) == self.current_idx {
|
||||
Some(text::SELECTED_COLOR)
|
||||
g.draw_text_at_screenspace_topleft(&self.prompt, self.top_left);
|
||||
if self.hidden {
|
||||
return;
|
||||
}
|
||||
|
||||
let row_height = row_height(g.canvas);
|
||||
let base_y = self.top_left.y + (self.prompt.num_lines() as f64) * row_height;
|
||||
|
||||
let choices_total_height = self.choices.last().unwrap().dy1 + row_height;
|
||||
g.fork_screenspace();
|
||||
g.draw_polygon(
|
||||
text::BG_COLOR,
|
||||
&Polygon::rectangle_topleft(
|
||||
Pt2D::new(self.top_left.x, base_y),
|
||||
Distance::meters(self.total_width),
|
||||
Distance::meters(choices_total_height),
|
||||
),
|
||||
);
|
||||
g.canvas.mark_covered_area(ScreenRectangle {
|
||||
x1: self.top_left.x,
|
||||
y1: base_y,
|
||||
x2: self.top_left.x + self.total_width,
|
||||
y2: base_y + choices_total_height,
|
||||
});
|
||||
|
||||
for dy1 in &self.separators {
|
||||
g.draw_polygon(
|
||||
Color::grey(0.4),
|
||||
&Polygon::rectangle_topleft(
|
||||
Pt2D::new(self.top_left.x, base_y + *dy1 + (row_height / 4.0)),
|
||||
Distance::meters(self.total_width),
|
||||
Distance::meters(row_height / 4.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
g.unfork();
|
||||
|
||||
for (idx, choice) in self.choices.iter().enumerate() {
|
||||
let mut txt = Text::with_bg_color(if Some(idx) == self.current_idx {
|
||||
Some(text::SELECTED_COLOR)
|
||||
} else {
|
||||
None
|
||||
});
|
||||
txt.override_width = Some(self.total_width);
|
||||
if choice.active {
|
||||
if let Some(key) = choice.hotkey {
|
||||
txt.add_styled_line(key.describe(), Some(text::HOTKEY_COLOR), None, None);
|
||||
txt.append(format!(" - {}", choice.label), None);
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if *active {
|
||||
if let Some(key) = hotkey {
|
||||
txt.add_styled_line(key.describe(), Some(text::HOTKEY_COLOR), bg, None);
|
||||
txt.append(format!(" - {}", choice), None);
|
||||
} else {
|
||||
txt.add_styled_line(choice.to_string(), None, bg, None);
|
||||
}
|
||||
txt.add_line(choice.label.clone());
|
||||
}
|
||||
} else {
|
||||
if let Some(key) = choice.hotkey {
|
||||
txt.add_styled_line(
|
||||
format!("{} - {}", key.describe(), choice.label),
|
||||
Some(text::INACTIVE_CHOICE_COLOR),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
if let Some(key) = hotkey {
|
||||
txt.add_styled_line(
|
||||
format!("{} - {}", key.describe(), choice),
|
||||
Some(text::INACTIVE_CHOICE_COLOR),
|
||||
bg,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
txt.add_styled_line(
|
||||
choice.to_string(),
|
||||
Some(text::INACTIVE_CHOICE_COLOR),
|
||||
bg,
|
||||
None,
|
||||
);
|
||||
}
|
||||
txt.add_styled_line(
|
||||
choice.label.clone(),
|
||||
Some(text::INACTIVE_CHOICE_COLOR),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Is drawing each row individually slower?
|
||||
g.draw_text_at_screenspace_topleft(
|
||||
&txt,
|
||||
ScreenPt::new(self.top_left.x, base_y + choice.dy1),
|
||||
);
|
||||
}
|
||||
g.draw_text_at_screenspace_topleft(&txt, self.geom.top_left);
|
||||
}
|
||||
|
||||
pub fn current_choice(&self) -> Option<&T> {
|
||||
let idx = self.current_idx?;
|
||||
Some(&self.choices[idx].3)
|
||||
Some(&self.choices[idx].data)
|
||||
}
|
||||
|
||||
pub fn active_choices(&self) -> Vec<&T> {
|
||||
self.choices
|
||||
.iter()
|
||||
.filter_map(|(_, _, active, data)| if *active { Some(data) } else { None })
|
||||
.filter_map(|choice| {
|
||||
if choice.active {
|
||||
Some(&choice.data)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// If there's no matching choice, be silent. The two callers don't care.
|
||||
pub fn mark_active(&mut self, choice: &str) {
|
||||
for (_, action, ref mut active, _) in self.choices.iter_mut() {
|
||||
if choice == action {
|
||||
if *active {
|
||||
panic!("Menu choice for {} was already active", choice);
|
||||
pub fn mark_active(&mut self, label: &str) {
|
||||
for choice in self.choices.iter_mut() {
|
||||
if choice.label == label {
|
||||
if choice.active {
|
||||
panic!("Menu choice for {} was already active", choice.label);
|
||||
}
|
||||
*active = true;
|
||||
choice.active = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_all_inactive(&mut self) {
|
||||
for (_, _, ref mut active, _) in self.choices.iter_mut() {
|
||||
*active = false;
|
||||
for choice in self.choices.iter_mut() {
|
||||
choice.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -326,14 +347,59 @@ impl<T: Clone> Menu<T> {
|
||||
}
|
||||
|
||||
fn recalculate_geom(&mut self, canvas: &Canvas) {
|
||||
if self.hidden {
|
||||
self.geom = self
|
||||
.pos
|
||||
.geometry::<()>(canvas, self.prompt.clone(), &Vec::new());
|
||||
} else {
|
||||
self.geom = self
|
||||
.pos
|
||||
.geometry(canvas, self.prompt.clone(), &self.choices);
|
||||
let mut txt = self.prompt.clone();
|
||||
if !self.hidden {
|
||||
for choice in &self.choices {
|
||||
if let Some(key) = choice.hotkey {
|
||||
txt.add_line(format!("{} - {}", key.describe(), choice.label));
|
||||
} else {
|
||||
txt.add_line(choice.label.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
let (total_width, total_height) = canvas.text_dims(&txt);
|
||||
self.top_left = self.pos.get_top_left(canvas, total_width, total_height);
|
||||
self.total_width = total_width;
|
||||
self.prompt.override_width = Some(total_width);
|
||||
}
|
||||
}
|
||||
|
||||
impl Position {
|
||||
fn get_top_left(&self, canvas: &Canvas, total_width: f64, total_height: f64) -> ScreenPt {
|
||||
match self {
|
||||
Position::SomeCornerAt(pt) => {
|
||||
// TODO Ideally also avoid covered canvas areas (modal menu)
|
||||
if pt.x + total_width < canvas.window_width {
|
||||
// pt.x is the left corner
|
||||
if pt.y + total_height < canvas.window_height {
|
||||
// pt.y is the top corner
|
||||
*pt
|
||||
} else {
|
||||
// pt.y is the bottom corner
|
||||
ScreenPt::new(pt.x, pt.y - total_height)
|
||||
}
|
||||
} else {
|
||||
// pt.x is the right corner
|
||||
if pt.y + total_height < canvas.window_height {
|
||||
// pt.y is the top corner
|
||||
ScreenPt::new(pt.x - total_width, pt.y)
|
||||
} else {
|
||||
// pt.y is the bottom corner
|
||||
ScreenPt::new(pt.x - total_width, pt.y - total_height)
|
||||
}
|
||||
}
|
||||
}
|
||||
Position::ScreenCenter => {
|
||||
let mut pt = canvas.center_to_screen_pt();
|
||||
pt.x -= total_width / 2.0;
|
||||
pt.y -= total_height / 2.0;
|
||||
pt
|
||||
}
|
||||
Position::TopRightOfScreen => ScreenPt::new(canvas.window_width - total_width, 0.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn row_height(canvas: &Canvas) -> f64 {
|
||||
canvas.line_height(text::FONT_SIZE)
|
||||
}
|
||||
|
@ -16,8 +16,12 @@ impl ModalMenu {
|
||||
Text::prompt(prompt_line),
|
||||
choice_groups
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|(multikey, action)| (multikey, action.to_string(), ()))
|
||||
.map(|group| {
|
||||
group
|
||||
.into_iter()
|
||||
.map(|(multikey, action)| (multikey, action.to_string(), ()))
|
||||
.collect()
|
||||
})
|
||||
.collect(),
|
||||
false,
|
||||
true,
|
||||
|
@ -218,7 +218,7 @@ impl<'a> WrappedWizard<'a> {
|
||||
.collect();
|
||||
self.wizard.menu = Some(Menu::new(
|
||||
Text::prompt(query),
|
||||
boxed_choices,
|
||||
vec![boxed_choices],
|
||||
true,
|
||||
false,
|
||||
Position::ScreenCenter,
|
||||
|
Loading…
Reference in New Issue
Block a user