2020-03-06 23:21:36 +03:00
|
|
|
// You have to run from the ezgui crate (abstreet/ezgui), due to relative paths to fonts and
|
|
|
|
// images.
|
|
|
|
//
|
2020-03-06 19:27:00 +03:00
|
|
|
// To run:
|
2020-03-21 04:05:50 +03:00
|
|
|
// > cargo run --example demo
|
2020-03-06 19:27:00 +03:00
|
|
|
//
|
|
|
|
// Try the web version, but there's no text rendering yet:
|
2020-03-21 04:05:50 +03:00
|
|
|
// > cargo web start --target wasm32-unknown-unknown --no-default-features \
|
|
|
|
// --features wasm-backend --example demo
|
2020-03-06 19:27:00 +03:00
|
|
|
|
2020-03-04 03:24:27 +03:00
|
|
|
use ezgui::{
|
2020-04-06 21:12:23 +03:00
|
|
|
hotkey, lctrl, Btn, Checkbox, Color, Composite, Drawable, EventCtx, EventLoopMode, GeomBatch,
|
|
|
|
GfxCtx, HorizontalAlignment, Key, Line, Outcome, Plot, PlotOptions, Series, Text, TextExt,
|
2020-03-21 09:41:07 +03:00
|
|
|
VerticalAlignment, Widget, GUI,
|
2020-03-04 03:24:27 +03:00
|
|
|
};
|
2020-03-06 21:31:08 +03:00
|
|
|
use geom::{Angle, Duration, Polygon, Pt2D, Time};
|
2020-03-04 03:24:27 +03:00
|
|
|
|
2020-03-06 23:21:36 +03:00
|
|
|
fn main() {
|
|
|
|
// Control flow surrendered here. App implements State, which has an event handler and a draw
|
|
|
|
// callback.
|
|
|
|
ezgui::run(
|
|
|
|
ezgui::Settings::new("ezgui demo", "../data/system/fonts"),
|
|
|
|
|ctx| App::new(ctx),
|
|
|
|
);
|
|
|
|
}
|
2020-03-04 03:24:27 +03:00
|
|
|
|
2020-03-06 23:21:36 +03:00
|
|
|
struct App {
|
|
|
|
controls: Composite,
|
|
|
|
timeseries_panel: Option<(Duration, Composite)>,
|
|
|
|
scrollable_canvas: Drawable,
|
2020-03-04 03:24:27 +03:00
|
|
|
elapsed: Duration,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl App {
|
|
|
|
fn new(ctx: &mut EventCtx) -> App {
|
|
|
|
App {
|
2020-03-06 23:21:36 +03:00
|
|
|
controls: make_controls(ctx),
|
|
|
|
timeseries_panel: None,
|
|
|
|
scrollable_canvas: setup_scrollable_canvas(ctx),
|
2020-03-04 03:24:27 +03:00
|
|
|
elapsed: Duration::ZERO,
|
|
|
|
}
|
|
|
|
}
|
2020-03-06 21:31:08 +03:00
|
|
|
|
2020-03-06 23:21:36 +03:00
|
|
|
fn make_timeseries_panel(&self, ctx: &mut EventCtx) -> Composite {
|
|
|
|
// Make a table with 3 columns.
|
2020-03-27 01:42:37 +03:00
|
|
|
let mut col1 = vec![Line("Time").draw(ctx)];
|
|
|
|
let mut col2 = vec![Line("Linear").draw(ctx)];
|
|
|
|
let mut col3 = vec![Line("Quadratic").draw(ctx)];
|
2020-03-06 21:31:08 +03:00
|
|
|
for s in 0..(self.elapsed.inner_seconds() as usize) {
|
2020-03-27 01:42:37 +03:00
|
|
|
col1.push(
|
|
|
|
Line(Duration::seconds(s as f64).to_string())
|
|
|
|
.secondary()
|
|
|
|
.draw(ctx),
|
|
|
|
);
|
|
|
|
col2.push(Line(s.to_string()).secondary().draw(ctx));
|
|
|
|
col3.push(Line(s.pow(2).to_string()).secondary().draw(ctx));
|
2020-03-06 21:31:08 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
let mut c = Composite::new(
|
2020-03-21 09:41:07 +03:00
|
|
|
Widget::col(vec![
|
|
|
|
Widget::row(vec![{
|
2020-03-06 21:31:08 +03:00
|
|
|
let mut txt = Text::from(
|
2020-03-27 01:42:37 +03:00
|
|
|
Line("Here's a bunch of text to force some scrolling.").small_heading(),
|
2020-03-06 21:31:08 +03:00
|
|
|
);
|
|
|
|
txt.add(
|
|
|
|
Line(
|
|
|
|
"Bug: scrolling by clicking and dragging doesn't work while the \
|
|
|
|
stopwatch is running.",
|
|
|
|
)
|
|
|
|
.fg(Color::RED),
|
|
|
|
);
|
2020-03-14 09:06:52 +03:00
|
|
|
txt.draw(ctx)
|
|
|
|
}]),
|
2020-03-21 09:41:07 +03:00
|
|
|
Widget::row(vec![
|
2020-03-06 23:21:36 +03:00
|
|
|
// Examples of styling widgets
|
2020-03-21 09:41:07 +03:00
|
|
|
Widget::col(col1).outline(3.0, Color::BLACK).margin(5),
|
|
|
|
Widget::col(col2).outline(3.0, Color::BLACK).margin(5),
|
|
|
|
Widget::col(col3).outline(3.0, Color::BLACK).margin(5),
|
2020-03-06 21:31:08 +03:00
|
|
|
]),
|
2020-04-07 00:05:59 +03:00
|
|
|
Plot::new(
|
2020-03-06 21:31:08 +03:00
|
|
|
ctx,
|
2020-04-07 00:05:59 +03:00
|
|
|
"timeseries",
|
2020-03-06 21:31:08 +03:00
|
|
|
vec![
|
|
|
|
Series {
|
|
|
|
label: "Linear".to_string(),
|
|
|
|
color: Color::GREEN,
|
2020-03-06 23:21:36 +03:00
|
|
|
// These points are (x axis = Time, y axis = usize)
|
2020-03-06 21:31:08 +03:00
|
|
|
pts: (0..(self.elapsed.inner_seconds() as usize))
|
|
|
|
.map(|s| (Time::START_OF_DAY + Duration::seconds(s as f64), s))
|
|
|
|
.collect(),
|
|
|
|
},
|
|
|
|
Series {
|
|
|
|
label: "Quadratic".to_string(),
|
|
|
|
color: Color::BLUE,
|
|
|
|
pts: (0..(self.elapsed.inner_seconds() as usize))
|
|
|
|
.map(|s| {
|
|
|
|
(Time::START_OF_DAY + Duration::seconds(s as f64), s.pow(2))
|
|
|
|
})
|
|
|
|
.collect(),
|
|
|
|
},
|
|
|
|
],
|
|
|
|
PlotOptions {
|
2020-03-06 23:21:36 +03:00
|
|
|
// Without this, the plot doesn't stretch to cover times in between whole
|
|
|
|
// seconds.
|
2020-03-06 21:31:08 +03:00
|
|
|
max_x: Some(Time::START_OF_DAY + self.elapsed),
|
|
|
|
},
|
|
|
|
),
|
|
|
|
])
|
|
|
|
.bg(Color::grey(0.4)),
|
|
|
|
)
|
2020-03-06 23:21:36 +03:00
|
|
|
// Don't let the panel exceed this percentage of the window. Scrollbars appear
|
|
|
|
// automatically if needed.
|
2020-03-06 21:31:08 +03:00
|
|
|
.max_size_percent(30, 40)
|
2020-03-06 23:21:36 +03:00
|
|
|
// We take up 30% width, and we want to leave 10% window width as buffer.
|
2020-03-06 21:31:08 +03:00
|
|
|
.aligned(HorizontalAlignment::Percent(0.6), VerticalAlignment::Center)
|
|
|
|
.build(ctx);
|
|
|
|
|
2020-04-07 00:05:59 +03:00
|
|
|
// Since we're creating an entirely new panel when the time changes, we need to preserve
|
|
|
|
// some internal state, like scroll and whether plot checkboxes were enabled.
|
2020-03-06 23:21:36 +03:00
|
|
|
if let Some((_, ref old)) = self.timeseries_panel {
|
2020-04-07 00:05:59 +03:00
|
|
|
c.restore(ctx, old);
|
2020-03-06 21:31:08 +03:00
|
|
|
}
|
|
|
|
c
|
|
|
|
}
|
2020-03-04 03:24:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
impl GUI for App {
|
|
|
|
fn event(&mut self, ctx: &mut EventCtx) -> EventLoopMode {
|
2020-03-06 23:21:36 +03:00
|
|
|
// Allow panning and zooming to work.
|
2020-03-04 03:24:27 +03:00
|
|
|
ctx.canvas_movement();
|
|
|
|
|
2020-03-06 23:21:36 +03:00
|
|
|
// This dispatches event handling to all of the widgets inside.
|
|
|
|
match self.controls.event(ctx) {
|
2020-03-04 03:24:27 +03:00
|
|
|
Some(Outcome::Clicked(x)) => match x.as_ref() {
|
2020-03-06 23:21:36 +03:00
|
|
|
// These outcomes should probably be a custom enum per Composite, to be more
|
|
|
|
// typesafe.
|
2020-03-04 03:24:27 +03:00
|
|
|
"reset the stopwatch" => {
|
|
|
|
self.elapsed = Duration::ZERO;
|
2020-03-06 23:21:36 +03:00
|
|
|
// We can replace any named widget with another one. Layout gets recalculated.
|
|
|
|
self.controls.replace(
|
2020-03-04 03:24:27 +03:00
|
|
|
ctx,
|
|
|
|
"stopwatch",
|
2020-03-14 09:06:52 +03:00
|
|
|
format!("Stopwatch: {}", self.elapsed)
|
|
|
|
.draw_text(ctx)
|
|
|
|
.named("stopwatch"),
|
2020-03-04 03:24:27 +03:00
|
|
|
);
|
|
|
|
}
|
|
|
|
_ => unreachable!(),
|
|
|
|
},
|
|
|
|
None => {}
|
|
|
|
}
|
|
|
|
|
2020-03-06 23:21:36 +03:00
|
|
|
// An update event means that no keyboard/mouse input happened, but time has passed.
|
|
|
|
// (Ignore the "nonblocking"; this API is funky right now. Only one caller "consumes" an
|
|
|
|
// event, so that multiple things don't all respond to one keypress, but that's set up
|
|
|
|
// oddly for update events.)
|
2020-03-04 03:24:27 +03:00
|
|
|
if let Some(dt) = ctx.input.nonblocking_is_update_event() {
|
|
|
|
ctx.input.use_update_event();
|
|
|
|
self.elapsed += dt;
|
2020-03-06 23:21:36 +03:00
|
|
|
self.controls.replace(
|
2020-03-04 03:24:27 +03:00
|
|
|
ctx,
|
|
|
|
"stopwatch",
|
2020-03-14 09:06:52 +03:00
|
|
|
format!("Stopwatch: {}", self.elapsed)
|
|
|
|
.draw_text(ctx)
|
|
|
|
.named("stopwatch"),
|
2020-03-04 03:24:27 +03:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-03-06 23:21:36 +03:00
|
|
|
if self.controls.is_checked("Show timeseries") {
|
|
|
|
// Update the panel when time changes.
|
2020-03-06 21:31:08 +03:00
|
|
|
if self
|
2020-03-06 23:21:36 +03:00
|
|
|
.timeseries_panel
|
2020-03-06 21:31:08 +03:00
|
|
|
.as_ref()
|
|
|
|
.map(|(dt, _)| *dt != self.elapsed)
|
|
|
|
.unwrap_or(true)
|
|
|
|
{
|
2020-03-06 23:21:36 +03:00
|
|
|
self.timeseries_panel = Some((self.elapsed, self.make_timeseries_panel(ctx)));
|
2020-03-06 21:31:08 +03:00
|
|
|
}
|
|
|
|
} else {
|
2020-03-06 23:21:36 +03:00
|
|
|
self.timeseries_panel = None;
|
2020-03-06 21:31:08 +03:00
|
|
|
}
|
|
|
|
|
2020-03-06 23:21:36 +03:00
|
|
|
if let Some((_, ref mut p)) = self.timeseries_panel {
|
2020-03-06 21:31:08 +03:00
|
|
|
match p.event(ctx) {
|
|
|
|
// No buttons in there
|
|
|
|
Some(Outcome::Clicked(_)) => unreachable!(),
|
|
|
|
None => {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-06 23:21:36 +03:00
|
|
|
// If we're paused, only call event() again when there's some kind of input. If not, also
|
|
|
|
// sprinkle in periodic update events as time passes.
|
|
|
|
if self.controls.is_checked("paused") {
|
2020-03-04 03:24:27 +03:00
|
|
|
EventLoopMode::InputOnly
|
|
|
|
} else {
|
|
|
|
EventLoopMode::Animation
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn draw(&self, g: &mut GfxCtx) {
|
|
|
|
g.clear(Color::BLACK);
|
|
|
|
|
2020-03-06 23:21:36 +03:00
|
|
|
if self.controls.is_checked("Draw scrollable canvas") {
|
|
|
|
g.redraw(&self.scrollable_canvas);
|
2020-03-04 03:24:27 +03:00
|
|
|
}
|
|
|
|
|
2020-03-06 23:21:36 +03:00
|
|
|
self.controls.draw(g);
|
2020-03-06 21:31:08 +03:00
|
|
|
|
2020-03-06 23:21:36 +03:00
|
|
|
if let Some((_, ref p)) = self.timeseries_panel {
|
2020-03-06 21:31:08 +03:00
|
|
|
p.draw(g);
|
|
|
|
}
|
2020-03-04 03:24:27 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-06 23:21:36 +03:00
|
|
|
// This prepares a bunch of geometry (colored polygons) and uploads it to the GPU once. Then it can
|
|
|
|
// be redrawn cheaply later.
|
|
|
|
fn setup_scrollable_canvas(ctx: &mut EventCtx) -> Drawable {
|
|
|
|
let mut batch = GeomBatch::new();
|
|
|
|
batch.push(
|
|
|
|
Color::hex("#4E30A6"),
|
2020-04-03 23:22:05 +03:00
|
|
|
Polygon::rounded_rectangle(5000.0, 5000.0, Some(25.0)),
|
2020-03-06 23:21:36 +03:00
|
|
|
);
|
|
|
|
// SVG support using lyon and usvg.
|
|
|
|
batch.add_svg(
|
|
|
|
&ctx.prerender,
|
|
|
|
"../data/system/assets/pregame/logo.svg",
|
|
|
|
// Translate
|
|
|
|
Pt2D::new(300.0, 300.0),
|
|
|
|
// Scale
|
|
|
|
1.0,
|
|
|
|
// Rotate
|
|
|
|
Angle::ZERO,
|
|
|
|
);
|
|
|
|
// Text rendering also goes through lyon and usvg.
|
|
|
|
batch.add_transformed(
|
|
|
|
Text::from(Line("Awesome vector text thanks to usvg and lyon").fg(Color::hex("#DF8C3D")))
|
|
|
|
.render_to_batch(&ctx.prerender),
|
|
|
|
// Translate
|
|
|
|
Pt2D::new(600.0, 500.0),
|
|
|
|
// Scale
|
|
|
|
2.0,
|
|
|
|
// Rotate
|
|
|
|
Angle::new_degs(30.0),
|
2020-03-04 03:24:27 +03:00
|
|
|
);
|
2020-03-06 23:21:36 +03:00
|
|
|
// This is a bit of a hack; it's needed so that zooming in/out has reasonable limits.
|
|
|
|
ctx.canvas.map_dims = (5000.0, 5000.0);
|
|
|
|
batch.upload(ctx)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn make_controls(ctx: &mut EventCtx) -> Composite {
|
|
|
|
Composite::new(
|
2020-03-21 09:41:07 +03:00
|
|
|
Widget::col(vec![
|
2020-03-14 09:06:52 +03:00
|
|
|
{
|
2020-03-27 01:42:37 +03:00
|
|
|
let mut txt = Text::from(Line("ezgui demo").small_heading());
|
2020-03-06 23:21:36 +03:00
|
|
|
txt.add(Line(
|
|
|
|
"Click and drag to pan, use touchpad or scroll wheel to zoom",
|
|
|
|
));
|
2020-03-14 09:06:52 +03:00
|
|
|
txt.draw(ctx)
|
|
|
|
},
|
2020-03-21 09:41:07 +03:00
|
|
|
Widget::row(vec![
|
2020-03-06 23:21:36 +03:00
|
|
|
// This just cycles between two arbitrary buttons
|
2020-04-06 21:12:23 +03:00
|
|
|
Checkbox::new(
|
2020-03-06 23:21:36 +03:00
|
|
|
false,
|
2020-03-18 21:45:20 +03:00
|
|
|
Btn::text_bg1("Pause").build(ctx, "pause the stopwatch", hotkey(Key::Space)),
|
|
|
|
Btn::text_bg1("Resume").build(ctx, "resume the stopwatch", hotkey(Key::Space)),
|
2020-03-06 23:21:36 +03:00
|
|
|
)
|
|
|
|
.named("paused")
|
|
|
|
.margin(5),
|
2020-03-18 21:45:20 +03:00
|
|
|
Btn::text_fg("Reset")
|
|
|
|
.build(ctx, "reset the stopwatch", None)
|
|
|
|
.margin(5),
|
2020-04-07 00:05:59 +03:00
|
|
|
Checkbox::text(ctx, "Draw scrollable canvas", None, true).margin(5),
|
|
|
|
Checkbox::text(ctx, "Show timeseries", lctrl(Key::T), false).margin(5),
|
2020-03-06 23:21:36 +03:00
|
|
|
])
|
|
|
|
.evenly_spaced(),
|
2020-03-14 09:06:52 +03:00
|
|
|
"Stopwatch: ...".draw_text(ctx).named("stopwatch"),
|
2020-03-06 23:21:36 +03:00
|
|
|
])
|
|
|
|
.bg(Color::grey(0.4)),
|
|
|
|
)
|
|
|
|
.aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
|
|
|
|
.build(ctx)
|
2020-03-04 03:24:27 +03:00
|
|
|
}
|