persistent split buttons for time increments. still need few tweaks, but

largely there
This commit is contained in:
Dustin Carlino 2020-04-04 13:22:14 -07:00
parent edffcf3db7
commit 8b6485b233
9 changed files with 201 additions and 97 deletions

View File

@ -12,6 +12,7 @@
//! * [`Histogram`] - visualize a distribution
//! * [`JustDraw`] (argh private) - just draw text, `GeomBatch`es, SVGs
//! * [`Menu`] - select something from a menu, with keybindings
//! * [`PersistentSplit`] - a button with a dropdown to change its state
//! * [`Plot`] - visualize 2 variables with a line plot
//! * [`Slider`] - horizontal and vertical sliders
//! * [`Spinner`] - numeric input with up/down buttons
@ -62,6 +63,7 @@ pub use crate::widgets::filler::Filler;
pub use crate::widgets::histogram::Histogram;
pub(crate) use crate::widgets::just_draw::JustDraw;
pub(crate) use crate::widgets::menu::Menu;
pub use crate::widgets::persistent_split::PersistentSplit;
pub use crate::widgets::plot::{Plot, PlotOptions, Series};
pub use crate::widgets::slider::Slider;
pub use crate::widgets::spinner::Spinner;

View File

@ -1,8 +1,9 @@
use crate::widgets::containers::{Container, Nothing};
use crate::{
Autocomplete, Btn, Button, Checkbox, Choice, Color, Drawable, Dropdown, EventCtx, Filler,
GeomBatch, GfxCtx, HorizontalAlignment, JustDraw, Menu, MultiKey, RewriteColor, ScreenDims,
ScreenPt, ScreenRectangle, Slider, Spinner, TextBox, VerticalAlignment, WidgetImpl,
GeomBatch, GfxCtx, HorizontalAlignment, JustDraw, Menu, MultiKey, PersistentSplit,
RewriteColor, ScreenDims, ScreenPt, ScreenRectangle, Slider, Spinner, TextBox,
VerticalAlignment, WidgetImpl,
};
use geom::{Distance, Polygon};
use std::collections::HashSet;
@ -253,7 +254,13 @@ impl Widget {
default_value: T,
choices: Vec<Choice<T>>,
) -> Widget {
Widget::new(Box::new(Dropdown::new(ctx, label, default_value, choices)))
Widget::new(Box::new(Dropdown::new(
ctx,
label,
default_value,
choices,
false,
)))
.named(label)
.outline(2.0, Color::WHITE)
}
@ -709,6 +716,9 @@ impl Composite {
pub fn dropdown_value<T: 'static + PartialEq + Clone>(&self, name: &str) -> T {
self.find::<Dropdown<T>>(name).current_value()
}
pub fn persistent_split_value<T: 'static + PartialEq + Clone>(&self, name: &str) -> T {
self.find::<PersistentSplit<T>>(name).current_value()
}
pub fn autocomplete_done<T: 'static + Clone>(&self, name: &str) -> Option<Vec<T>> {
self.find::<Autocomplete<T>>(name).final_value()

View File

@ -12,7 +12,7 @@ pub struct Button {
draw_normal: Drawable,
draw_hovered: Drawable,
hotkey: Option<MultiKey>,
pub(crate) hotkey: Option<MultiKey>,
tooltip: Text,
// Screenspace, top-left always at the origin. Also, probably not a box. :P
hitbox: Polygon,

View File

@ -1,8 +1,8 @@
use crate::{
Btn, Button, Choice, Color, EventCtx, GfxCtx, InputResult, Menu, Outcome, ScreenDims, ScreenPt,
WidgetImpl,
Btn, Button, Choice, Color, EventCtx, GeomBatch, GfxCtx, InputResult, Menu, Outcome,
ScreenDims, ScreenPt, ScreenRectangle, WidgetImpl,
};
use geom::{Polygon, Pt2D};
use geom::{Distance, Polygon, Pt2D};
pub struct Dropdown<T: Clone> {
current_idx: usize,
@ -10,6 +10,7 @@ pub struct Dropdown<T: Clone> {
// TODO Why not T?
menu: Option<Menu<usize>>,
label: String,
blank_btn_label: bool,
choices: Vec<Choice<T>>,
}
@ -20,6 +21,8 @@ impl<T: 'static + PartialEq + Clone> Dropdown<T> {
label: &str,
default_value: T,
choices: Vec<Choice<T>>,
// TODO Ideally builder style
blank_btn_label: bool,
) -> Dropdown<T> {
let current_idx = choices
.iter()
@ -28,9 +31,10 @@ impl<T: 'static + PartialEq + Clone> Dropdown<T> {
Dropdown {
current_idx,
btn: make_btn(ctx, &choices[current_idx].label, label),
btn: make_btn(ctx, &choices[current_idx].label, label, blank_btn_label),
menu: None,
label: label.to_string(),
blank_btn_label,
choices,
}
}
@ -38,6 +42,9 @@ impl<T: 'static + PartialEq + Clone> Dropdown<T> {
pub fn current_value(&self) -> T {
self.choices[self.current_idx].data.clone()
}
pub(crate) fn current_value_label(&self) -> String {
self.choices[self.current_idx].label.clone()
}
}
impl<T: 'static + Clone> WidgetImpl for Dropdown<T> {
@ -61,9 +68,12 @@ impl<T: 'static + Clone> WidgetImpl for Dropdown<T> {
self.menu = None;
self.current_idx = idx;
let top_left = self.btn.top_left;
// TODO Recalculate widgets when this happens... outline around button should
// change
self.btn = make_btn(ctx, &self.choices[self.current_idx].label, &self.label);
self.btn = make_btn(
ctx,
&self.choices[self.current_idx].label,
&self.label,
self.blank_btn_label,
);
self.btn.set_pos(top_left);
*redo_layout = true;
}
@ -80,9 +90,16 @@ impl<T: 'static + Clone> WidgetImpl for Dropdown<T> {
.collect(),
)
.take_menu();
let y1_below = self.btn.top_left.y + self.btn.dims.height + 15.0;
menu.set_pos(ScreenPt::new(
self.btn.top_left.x,
self.btn.top_left.y + self.btn.dims.height + 15.0,
// top_left_for_corner doesn't quite work
if y1_below + menu.get_dims().height < ctx.canvas.window_height {
y1_below
} else {
self.btn.top_left.y - 15.0 - menu.get_dims().height
},
));
self.menu = Some(menu);
}
@ -94,21 +111,41 @@ impl<T: 'static + Clone> WidgetImpl for Dropdown<T> {
fn draw(&self, g: &mut GfxCtx) {
self.btn.draw(g);
if let Some(ref m) = self.menu {
// We need a background too!
g.fork(Pt2D::new(0.0, 0.0), m.top_left, 1.0, Some(0.1));
g.draw_polygon(
Color::grey(0.3),
&Polygon::rounded_rectangle(m.get_dims().width, m.get_dims().height, Some(5.0)),
// We need a background too! Add some padding and an outline.
// TODO Little embedded Composite could make more sense?
let pad = 5.0;
let width = m.get_dims().width + 2.0 * pad;
let height = m.get_dims().height + 2.0 * pad;
let rect = Polygon::rounded_rectangle(width, height, Some(5.0));
let draw_bg = g.upload(GeomBatch::from(vec![
(Color::grey(0.3), rect.clone()),
(Color::WHITE, rect.to_outline(Distance::meters(3.0))),
]));
g.fork(
Pt2D::new(0.0, 0.0),
ScreenPt::new(m.top_left.x - pad, m.top_left.y - pad),
1.0,
// Between SCREENSPACE_Z and TOOLTIP_Z
Some(0.1),
);
g.redraw(&draw_bg);
g.unfork();
m.draw(g);
// Dropdown menus often leak out of their Composite
g.canvas
.mark_covered_area(ScreenRectangle::top_left(m.top_left, m.get_dims()));
}
}
}
fn make_btn(ctx: &EventCtx, name: &str, label: &str) -> Button {
fn make_btn(ctx: &EventCtx, name: &str, label: &str, blank_btn_label: bool) -> Button {
(if blank_btn_label {
Btn::text_fg("")
} else {
Btn::text_fg(format!("{}", name))
})
.build(ctx, label, None)
.take_btn()
}

View File

@ -7,6 +7,7 @@ pub mod filler;
pub mod histogram;
pub mod just_draw;
pub mod menu;
pub mod persistent_split;
pub mod plot;
pub mod slider;
pub mod spinner;

View File

@ -0,0 +1,75 @@
use crate::{
Btn, Button, Choice, Dropdown, EventCtx, GfxCtx, MultiKey, Outcome, ScreenDims, ScreenPt,
Widget, WidgetImpl,
};
pub struct PersistentSplit<T: Clone + PartialEq> {
current_value: T,
btn: Button,
dropdown: Dropdown<T>,
}
impl<T: 'static + PartialEq + Clone> PersistentSplit<T> {
pub fn new(
ctx: &EventCtx,
label: &str,
default_value: T,
hotkey: Option<MultiKey>,
choices: Vec<Choice<T>>,
) -> Widget {
let dropdown = Dropdown::new(ctx, "change", default_value, choices, true);
let btn = Btn::plaintext(dropdown.current_value_label())
.build(ctx, label, hotkey)
.take_btn();
Widget::new(Box::new(PersistentSplit {
current_value: dropdown.current_value(),
btn,
dropdown,
}))
.named(label)
}
pub fn current_value(&self) -> T {
self.current_value.clone()
}
}
impl<T: 'static + Clone + PartialEq> WidgetImpl for PersistentSplit<T> {
fn get_dims(&self) -> ScreenDims {
let dims1 = self.btn.get_dims();
let dims2 = self.dropdown.get_dims();
ScreenDims::new(dims1.width + dims2.width, dims1.height.max(dims2.height))
}
fn set_pos(&mut self, top_left: ScreenPt) {
self.btn.set_pos(top_left);
self.dropdown
.set_pos(ScreenPt::new(top_left.x + self.btn.dims.width, top_left.y));
}
fn event(&mut self, ctx: &mut EventCtx, redo_layout: &mut bool) -> Option<Outcome> {
if let Some(o) = self.btn.event(ctx, redo_layout) {
return Some(o);
}
self.dropdown.event(ctx, redo_layout);
let new_value = self.dropdown.current_value();
if new_value != self.current_value {
self.current_value = new_value;
let hotkey = self.btn.hotkey.take();
let label = self.btn.action.clone();
self.btn = Btn::plaintext(self.dropdown.current_value_label())
.build(ctx, label, hotkey)
.take_btn();
*redo_layout = true;
}
None
}
fn draw(&self, g: &mut GfxCtx) {
self.btn.draw(g);
self.dropdown.draw(g);
}
}

View File

@ -3,6 +3,7 @@ use crate::game::{State, Transition};
use ezgui::{
hotkey, Btn, Choice, Composite, EventCtx, GfxCtx, Key, Line, Outcome, TextExt, Widget,
};
use geom::Duration;
// TODO SimOptions stuff too
#[derive(Clone)]
@ -10,6 +11,7 @@ pub struct Options {
pub traffic_signal_style: TrafficSignalStyle,
pub color_scheme: Option<String>,
pub dev: bool,
pub time_increment: Duration,
}
impl Options {
@ -18,6 +20,7 @@ impl Options {
traffic_signal_style: TrafficSignalStyle::GroupArrows,
color_scheme: None,
dev: false,
time_increment: Duration::minutes(10),
}
}
}

View File

@ -1011,19 +1011,19 @@ impl TutorialState {
)
.msg(
vec!["You can pause or resume time"],
arrow(speed.composite.inner.center_of("pause")),
arrow(speed.composite.center_of("pause")),
)
.msg(
vec!["Speed things up"],
arrow(speed.composite.inner.center_of("30x speed")),
arrow(speed.composite.center_of("30x speed")),
)
.msg(
vec!["Advance time by certain amounts"],
arrow(speed.composite.inner.center_of("step forwards 1 hour")),
arrow(speed.composite.center_of("step forwards")),
)
.msg(
vec!["And reset to the beginning of the day"],
arrow(speed.composite.inner.center_of("reset to midnight")),
arrow(speed.composite.center_of("reset to midnight")),
)
.msg(
vec!["Let's try these controls out. Run the simulation until 5pm or later."],
@ -1042,7 +1042,7 @@ impl TutorialState {
"You might've figured it out already,",
"But you'll be pausing/resuming time VERY frequently",
],
arrow(speed.composite.inner.center_of("pause")),
arrow(speed.composite.center_of("pause")),
)
.msg(
vec![
@ -1102,7 +1102,7 @@ impl TutorialState {
"You don't have to manually chase them; just click to follow.",
"(If you do lose track of them, just reset)",
],
arrow(speed.composite.inner.center_of("reset to midnight")),
arrow(speed.composite.center_of("reset to midnight")),
),
);

View File

@ -2,18 +2,17 @@ use crate::app::App;
use crate::common::{Overlays, Warping};
use crate::game::{msg, State, Transition};
use crate::helpers::ID;
use crate::managed::{WrappedComposite, WrappedOutcome};
use crate::sandbox::{GameplayMode, SandboxMode};
use ezgui::{
hotkey, Btn, Color, Composite, EventCtx, EventLoopMode, GeomBatch, GfxCtx, HorizontalAlignment,
Key, Line, Outcome, Plot, PlotOptions, RewriteColor, Series, Slider, Text, VerticalAlignment,
Widget,
hotkey, Btn, Choice, Color, Composite, EventCtx, EventLoopMode, GeomBatch, GfxCtx,
HorizontalAlignment, Key, Line, Outcome, PersistentSplit, Plot, PlotOptions, RewriteColor,
Series, Slider, Text, VerticalAlignment, Widget,
};
use geom::{Duration, Polygon, Time};
use instant::Instant;
pub struct SpeedControls {
pub composite: WrappedComposite,
pub composite: Composite,
paused: bool,
setting: SpeedSetting,
@ -33,12 +32,7 @@ enum SpeedSetting {
impl SpeedControls {
// TODO Could use custom_checkbox here, but not sure it'll make things that much simpler.
fn make_panel(
ctx: &mut EventCtx,
app: &App,
paused: bool,
setting: SpeedSetting,
) -> WrappedComposite {
fn make_panel(ctx: &mut EventCtx, app: &App, paused: bool, setting: SpeedSetting) -> Composite {
let mut row = Vec::new();
row.push(
if paused {
@ -87,26 +81,18 @@ impl SpeedControls {
);
row.push(
Widget::row(vec![
Btn::custom(
Text::from(Line("+1h").fg(Color::WHITE)).render_ctx(ctx),
Text::from(Line("+1h").fg(app.cs.hovering)).render_ctx(ctx),
{
let dims = Text::from(Line("+1h")).render_ctx(ctx).get_dims();
Polygon::rectangle(dims.width, dims.height)
},
PersistentSplit::new(
ctx,
"step forwards",
app.opts.time_increment,
hotkey(Key::M),
vec![
Choice::new("+1h", Duration::hours(1)),
Choice::new("+30m", Duration::minutes(30)),
Choice::new("+10m", Duration::minutes(10)),
Choice::new("+0.1s", Duration::seconds(0.1)),
],
)
.build(ctx, "step forwards 1 hour", hotkey(Key::N)),
Btn::custom(
Text::from(Line("+0.1s").fg(Color::WHITE)).render_ctx(ctx),
Text::from(Line("+0.1s").fg(app.cs.hovering)).render_ctx(ctx),
{
let dims = Text::from(Line("+0.1s")).render_ctx(ctx).get_dims();
Polygon::rectangle(dims.width, dims.height)
},
)
.build(ctx, "step forwards 0.1 seconds", hotkey(Key::M)),
])
.bg(app.cs.section_bg)
.margin_right(16),
);
@ -123,38 +109,12 @@ impl SpeedControls {
.bg(app.cs.section_bg),
);
WrappedComposite::new(
Composite::new(Widget::row(row).bg(app.cs.panel_bg).padding(16))
.aligned(
HorizontalAlignment::Center,
VerticalAlignment::BottomAboveOSD,
)
.build(ctx),
)
.cb(
"step forwards 0.1 seconds",
Box::new(|ctx, app| {
app.primary
.sim
.normal_step(&app.primary.map, Duration::seconds(0.1));
if let Some(ref mut s) = app.secondary {
s.sim.normal_step(&s.map, Duration::seconds(0.1));
}
app.recalculate_current_selection(ctx);
None
}),
)
.cb(
"step forwards 1 hour",
Box::new(|ctx, app| {
Some(Transition::Push(Box::new(TimeWarpScreen::new(
ctx,
app,
app.primary.sim.time() + Duration::hours(1),
false,
))))
}),
)
.build(ctx)
}
pub fn new(ctx: &mut EventCtx, app: &App) -> SpeedControls {
@ -172,11 +132,8 @@ impl SpeedControls {
app: &mut App,
maybe_mode: Option<&GameplayMode>,
) -> Option<Transition> {
match self.composite.event(ctx, app) {
Some(WrappedOutcome::Transition(t)) => {
return Some(t);
}
Some(WrappedOutcome::Clicked(x)) => match x.as_ref() {
match self.composite.event(ctx) {
Some(Outcome::Clicked(x)) => match x.as_ref() {
"real-time speed" => {
self.setting = SpeedSetting::Realtime;
self.composite = SpeedControls::make_panel(ctx, app, self.paused, self.setting);
@ -227,10 +184,29 @@ impl SpeedControls {
maybe_mode.cloned(),
))));
}
"step forwards" => {
let dt = self.composite.persistent_split_value("step forwards");
if dt == Duration::seconds(0.1) {
app.primary.sim.normal_step(&app.primary.map, dt);
if let Some(ref mut s) = app.secondary {
s.sim.normal_step(&s.map, dt);
}
app.recalculate_current_selection(ctx);
return None;
}
return Some(Transition::Push(Box::new(TimeWarpScreen::new(
ctx,
app,
app.primary.sim.time() + dt,
false,
))));
}
_ => unreachable!(),
},
None => {}
}
// Just kind of constantly scrape this
app.opts.time_increment = self.composite.persistent_split_value("step forwards");
if ctx.input.new_was_pressed(&hotkey(Key::LeftArrow).unwrap()) {
match self.setting {