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