1 // To run:
2 // > cargo run --example demo
3 //
4 // Try the web version, but there's no text rendering yet:
5 // > cargo web start --target wasm32-unknown-unknown --no-default-features \
6 // --features wasm-backend --example demo
7 
8 use geom::{Angle, Duration, Percent, Polygon, Pt2D, Time};
9 use rand::SeedableRng;
10 use rand_xorshift::XorShiftRng;
11 use std::collections::HashSet;
12 use widgetry::{
13     hotkey, lctrl, Btn, Checkbox, Color, Drawable, EventCtx, GeomBatch, GfxCtx,
14     HorizontalAlignment, Key, Line, LinePlot, Outcome, Panel, PlotOptions, Series, Text, TextExt,
15     UpdateType, VerticalAlignment, Widget, GUI,
16 };
17 
main()18 fn main() {
19     // Control flow surrendered here. App implements State, which has an event handler and a draw
20     // callback.
21     widgetry::run(widgetry::Settings::new("widgetry demo"), |ctx| {
22         App::new(ctx)
23     });
24 }
25 
26 struct App {
27     controls: Panel,
28     timeseries_panel: Option<(Duration, Panel)>,
29     scrollable_canvas: Drawable,
30     elapsed: Duration,
31 }
32 
33 impl App {
new(ctx: &mut EventCtx) -> App34     fn new(ctx: &mut EventCtx) -> App {
35         App {
36             controls: make_controls(ctx),
37             timeseries_panel: None,
38             scrollable_canvas: setup_scrollable_canvas(ctx),
39             elapsed: Duration::ZERO,
40         }
41     }
42 
make_timeseries_panel(&self, ctx: &mut EventCtx) -> Panel43     fn make_timeseries_panel(&self, ctx: &mut EventCtx) -> Panel {
44         // Make a table with 3 columns.
45         let mut col1 = vec![Line("Time").draw(ctx)];
46         let mut col = vec![Line("Linear").draw(ctx)];
47         let mut col3 = vec![Line("Quadratic").draw(ctx)];
48         for s in 0..(self.elapsed.inner_seconds() as usize) {
49             col1.push(
50                 Line(Duration::seconds(s as f64).to_string())
51                     .secondary()
52                     .draw(ctx),
53             );
54             col.push(Line(s.to_string()).secondary().draw(ctx));
55             col3.push(Line(s.pow(2).to_string()).secondary().draw(ctx));
56         }
57 
58         let mut c = Panel::new(Widget::col(vec![
59             Text::from_multiline(vec![
60                 Line("Here's a bunch of text to force some scrolling.").small_heading(),
61                 Line(
62                     "Bug: scrolling by clicking and dragging doesn't work while the stopwatch is \
63                      running.",
64                 )
65                 .fg(Color::RED),
66             ])
67             .draw(ctx),
68             Widget::row(vec![
69                 // Examples of styling widgets
70                 Widget::col(col1).outline(3.0, Color::BLACK).padding(5),
71                 Widget::col(col).outline(3.0, Color::BLACK).padding(5),
72                 Widget::col(col3).outline(3.0, Color::BLACK).padding(5),
73             ]),
74             LinePlot::new(
75                 ctx,
76                 vec![
77                     Series {
78                         label: "Linear".to_string(),
79                         color: Color::GREEN,
80                         // These points are (x axis = Time, y axis = usize)
81                         pts: (0..(self.elapsed.inner_seconds() as usize))
82                             .map(|s| (Time::START_OF_DAY + Duration::seconds(s as f64), s))
83                             .collect(),
84                     },
85                     Series {
86                         label: "Quadratic".to_string(),
87                         color: Color::BLUE,
88                         pts: (0..(self.elapsed.inner_seconds() as usize))
89                             .map(|s| (Time::START_OF_DAY + Duration::seconds(s as f64), s.pow(2)))
90                             .collect(),
91                     },
92                 ],
93                 PlotOptions {
94                     filterable: false,
95                     // Without this, the plot doesn't stretch to cover times in between whole
96                     // seconds.
97                     max_x: Some(Time::START_OF_DAY + self.elapsed),
98                     max_y: None,
99                     disabled: HashSet::new(),
100                 },
101             ),
102         ]))
103         // Don't let the panel exceed this percentage of the window. Scrollbars appear
104         // automatically if needed.
105         .max_size(Percent::int(30), Percent::int(40))
106         // We take up 30% width, and we want to leave 10% window width as buffer.
107         .aligned(HorizontalAlignment::Percent(0.6), VerticalAlignment::Center)
108         .build(ctx);
109 
110         // Since we're creating an entirely new panel when the time changes, we need to preserve
111         // some internal state, like scroll and whether plot checkboxes were enabled.
112         if let Some((_, ref old)) = self.timeseries_panel {
113             c.restore(ctx, old);
114         }
115         c
116     }
117 }
118 
119 impl GUI for App {
event(&mut self, ctx: &mut EventCtx)120     fn event(&mut self, ctx: &mut EventCtx) {
121         // Allow panning and zooming to work.
122         ctx.canvas_movement();
123 
124         // This dispatches event handling to all of the widgets inside.
125         match self.controls.event(ctx) {
126             Outcome::Clicked(x) => match x.as_ref() {
127                 // These outcomes should probably be a custom enum per Panel, to be more
128                 // typesafe.
129                 "reset the stopwatch" => {
130                     self.elapsed = Duration::ZERO;
131                     // We can replace any named widget with another one. Layout gets recalculated.
132                     self.controls.replace(
133                         ctx,
134                         "stopwatch",
135                         format!("Stopwatch: {}", self.elapsed)
136                             .draw_text(ctx)
137                             .named("stopwatch"),
138                     );
139                 }
140                 "generate new faces" => {
141                     self.scrollable_canvas = setup_scrollable_canvas(ctx);
142                 }
143                 _ => unreachable!(),
144             },
145             _ => {}
146         }
147 
148         // An update event means that no keyboard/mouse input happened, but time has passed.
149         // (Ignore the "nonblocking"; this API is funky right now. Only one caller "consumes" an
150         // event, so that multiple things don't all respond to one keypress, but that's set up
151         // oddly for update events.)
152         if let Some(dt) = ctx.input.nonblocking_is_update_event() {
153             ctx.input.use_update_event();
154             self.elapsed += dt;
155             self.controls.replace(
156                 ctx,
157                 "stopwatch",
158                 format!("Stopwatch: {}", self.elapsed)
159                     .draw_text(ctx)
160                     .named("stopwatch"),
161             );
162         }
163 
164         if self.controls.is_checked("Show timeseries") {
165             // Update the panel when time changes.
166             if self
167                 .timeseries_panel
168                 .as_ref()
169                 .map(|(dt, _)| *dt != self.elapsed)
170                 .unwrap_or(true)
171             {
172                 self.timeseries_panel = Some((self.elapsed, self.make_timeseries_panel(ctx)));
173             }
174         } else {
175             self.timeseries_panel = None;
176         }
177 
178         if let Some((_, ref mut p)) = self.timeseries_panel {
179             match p.event(ctx) {
180                 // No buttons in there
181                 Outcome::Clicked(_) => unreachable!(),
182                 _ => {}
183             }
184         }
185 
186         // If we're paused, only call event() again when there's some kind of input. If not, also
187         // sprinkle in periodic update events as time passes.
188         if !self.controls.is_checked("paused") {
189             ctx.request_update(UpdateType::Game);
190         }
191     }
192 
draw(&self, g: &mut GfxCtx)193     fn draw(&self, g: &mut GfxCtx) {
194         g.clear(Color::BLACK);
195 
196         if self.controls.is_checked("Draw scrollable canvas") {
197             g.redraw(&self.scrollable_canvas);
198         }
199 
200         self.controls.draw(g);
201 
202         if let Some((_, ref p)) = self.timeseries_panel {
203             p.draw(g);
204         }
205     }
206 }
207 
208 // This prepares a bunch of geometry (colored polygons) and uploads it to the GPU once. Then it can
209 // be redrawn cheaply later.
setup_scrollable_canvas(ctx: &mut EventCtx) -> Drawable210 fn setup_scrollable_canvas(ctx: &mut EventCtx) -> Drawable {
211     let mut batch = GeomBatch::new();
212     batch.push(
213         Color::hex("#4E30A6"),
214         Polygon::rounded_rectangle(5000.0, 5000.0, Some(25.0)),
215     );
216     // SVG support using lyon and usvg. Map-space means don't scale for high DPI monitors.
217     batch.append(
218         GeomBatch::load_svg(&ctx.prerender, "system/assets/pregame/logo.svg")
219             .translate(300.0, 300.0),
220     );
221     // Text rendering also goes through lyon and usvg.
222     batch.append(
223         Text::from(Line("Awesome vector text thanks to usvg and lyon").fg(Color::hex("#DF8C3D")))
224             .render_to_batch(&ctx.prerender)
225             .scale(2.0)
226             .centered_on(Pt2D::new(600.0, 500.0))
227             .rotate(Angle::new_degs(-30.0)),
228     );
229 
230     let mut rng = if cfg!(target_arch = "wasm32") {
231         XorShiftRng::from_seed([0; 16])
232     } else {
233         XorShiftRng::from_entropy()
234     };
235     for i in 0..10 {
236         let mut svg_data = Vec::new();
237         svg_face::generate_face(&mut svg_data, &mut rng).unwrap();
238         let face = GeomBatch::from_svg_contents(svg_data).autocrop();
239         let dims = face.get_dims();
240         batch.append(
241             face.scale((200.0 / dims.width).min(200.0 / dims.height))
242                 .translate(250.0 * (i as f64), 0.0),
243         );
244     }
245 
246     // This is a bit of a hack; it's needed so that zooming in/out has reasonable limits.
247     ctx.canvas.map_dims = (5000.0, 5000.0);
248     batch.upload(ctx)
249 }
250 
make_controls(ctx: &mut EventCtx) -> Panel251 fn make_controls(ctx: &mut EventCtx) -> Panel {
252     Panel::new(Widget::col(vec![
253         Text::from_multiline(vec![
254             Line("widgetry demo").small_heading(),
255             Line("Click and drag to pan, use touchpad or scroll wheel to zoom"),
256         ])
257         .draw(ctx),
258         Widget::row(vec![
259             // This just cycles between two arbitrary buttons
260             Checkbox::new(
261                 false,
262                 Btn::text_bg1("Pause").build(ctx, "pause the stopwatch", hotkey(Key::Space)),
263                 Btn::text_bg1("Resume").build(ctx, "resume the stopwatch", hotkey(Key::Space)),
264             )
265             .named("paused"),
266             Btn::text_fg("Reset timer").build(ctx, "reset the stopwatch", None),
267             Btn::text_fg("New faces").build(ctx, "generate new faces", hotkey(Key::F)),
268             Checkbox::switch(ctx, "Draw scrollable canvas", None, true),
269             Checkbox::switch(ctx, "Show timeseries", lctrl(Key::T), false),
270         ])
271         .evenly_spaced(),
272         "Stopwatch: ...".draw_text(ctx).named("stopwatch"),
273     ]))
274     .aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
275     .build(ctx)
276 }
277