1use rand::SeedableRng;
2use rand_xorshift::XorShiftRng;
3
4use geom::{Angle, Duration, Polygon, Pt2D, Time, UnitFmt};
5use widgetry::{
6 lctrl, Choice, Color, ContentMode, DragDrop, Drawable, EventCtx, Fill, GeomBatch, GfxCtx,
7 HorizontalAlignment, Image, Key, Line, LinePlot, Outcome, Panel, PanelDims, PersistentSplit,
8 PlotOptions, ScreenDims, Series, Settings, SharedAppState, StackAxis, State, TabController,
9 Text, TextExt, Texture, Toggle, Transition, UpdateType, VerticalAlignment, Widget,
10};
11
12pub fn main() {
13 let settings = Settings::new("widgetry demo");
14 run(settings);
15}
16
17fn run(mut settings: Settings) {
18 abstutil::logger::setup();
19 settings = settings.read_svg(Box::new(abstio::slurp_bytes));
20 widgetry::run(settings, |ctx| {
26 (App {}, vec![Box::new(Demo::new(ctx))])
30 });
31}
32
33struct App {}
34
35impl SharedAppState for App {}
36
37struct Demo {
38 controls: Panel,
39 timeseries_panel: Option<(Duration, Panel)>,
40 scrollable_canvas: Drawable,
41 texture_demo: Drawable,
42 elapsed: Duration,
43 tabs: TabController,
44}
45
46impl Demo {
47 fn new(ctx: &mut EventCtx) -> Self {
48 let mut tabs = make_tabs(ctx);
49 Self {
50 controls: make_controls(ctx, &mut tabs),
51 timeseries_panel: None,
52 scrollable_canvas: setup_scrollable_canvas(ctx),
53 texture_demo: setup_texture_demo(ctx, Texture::SAND, Texture::CACTUS),
54 elapsed: Duration::ZERO,
55 tabs,
56 }
57 }
58 fn create_series<F>(&self, label: &str, color: Color, func: F) -> Series<Time, usize>
59 where
60 F: Fn(usize) -> usize,
61 {
62 Series {
63 label: label.to_string(),
64 color,
65 pts: (0..(self.elapsed.inner_seconds() as usize))
66 .map(|s: usize| (Time::START_OF_DAY + Duration::seconds(s as f64), func(s)))
67 .collect(),
68 }
69 }
70 fn make_timeseries_panel(&self, ctx: &mut EventCtx) -> Panel {
71 let mut col1 = vec![Line("Time").into_widget(ctx)];
73 let mut col = vec![Line("Linear").into_widget(ctx)];
74 let mut col3 = vec![Line("Quadratic").into_widget(ctx)];
75 for s in 0..(self.elapsed.inner_seconds() as usize) {
76 col1.push(
77 Line(format!("{}", Duration::seconds(s as f64)))
78 .secondary()
79 .into_widget(ctx),
80 );
81 col.push(Line(s.to_string()).secondary().into_widget(ctx));
82 col3.push(Line(s.pow(2).to_string()).secondary().into_widget(ctx));
83 }
84
85 let mut c = Panel::new_builder(Widget::col(vec![
86 Text::from_multiline(vec![
87 Line("Here's a bunch of text to force some scrolling.").small_heading(),
88 Line(
89 "Bug: scrolling by clicking and dragging doesn't work while the stopwatch is \
90 running.",
91 )
92 .fg(Color::RED),
93 ])
94 .into_widget(ctx),
95 Widget::row(vec![
96 Widget::col(col1).outline((2.0, Color::BLACK)).padding(5),
98 Widget::col(col).outline((5.0, Color::BLACK)).padding(5),
99 Widget::col(col3).outline((5.0, Color::BLUE)).padding(5),
100 ]),
101 LinePlot::new_widget(
102 ctx,
103 "timeseries",
104 vec![
105 self.create_series("Linear", Color::GREEN, |s| s),
106 self.create_series("Quadratic", Color::BLUE, |s| s.pow(2)),
107 ],
108 PlotOptions {
109 max_x: Some(Time::START_OF_DAY + self.elapsed),
112 ..Default::default()
113 },
114 UnitFmt::metric(),
115 ),
116 ]))
117 .dims_width(PanelDims::MaxPercent(0.3))
120 .dims_height(PanelDims::MaxPercent(0.4))
121 .aligned(HorizontalAlignment::Percent(0.6), VerticalAlignment::Center)
123 .build(ctx);
124
125 if let Some((_, ref old)) = self.timeseries_panel {
128 c.restore(ctx, old);
129 }
130 c
131 }
132
133 fn redraw_stopwatch(&mut self, ctx: &mut EventCtx) {
134 self.controls.replace(
136 ctx,
137 "stopwatch",
138 format!("Stopwatch: {}", self.elapsed).text_widget(ctx),
139 );
140 }
141}
142
143impl State<App> for Demo {
144 fn event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Transition<App> {
145 ctx.canvas_movement();
147
148 if let Outcome::Clicked(x) = self.controls.event(ctx) {
150 match x.as_ref() {
151 "reset the stopwatch" => {
154 self.elapsed = Duration::ZERO;
155 self.redraw_stopwatch(ctx);
156 }
157 "generate new faces" => {
158 self.scrollable_canvas = setup_scrollable_canvas(ctx);
159 }
160 "adjust timer" => {
161 let offset: Duration = self.controls.persistent_split_value("adjust timer");
162 self.elapsed += offset;
163 self.redraw_stopwatch(ctx);
164 }
165 "apply" => {
166 let (v_align, h_align) = self.controls.dropdown_value("alignment");
167 self.controls.align(v_align, h_align);
168 let (bg_texture, fg_texture) = self.controls.dropdown_value("texture");
169 self.texture_demo = setup_texture_demo(ctx, bg_texture, fg_texture);
170 }
171 action => {
172 if self.tabs.handle_action(ctx, action, &mut self.controls) {
173 } else if action.contains("btn_") {
175 log::info!("clicked button: {:?}", action);
176 } else {
177 unimplemented!("clicked: {:?}", x);
178 }
179 }
180 }
181 }
182
183 if let Some(dt) = ctx.input.nonblocking_is_update_event() {
188 ctx.input.use_update_event();
189 self.elapsed += dt;
190 self.redraw_stopwatch(ctx);
191 }
192
193 if self.controls.is_checked("Show timeseries") {
194 if self
196 .timeseries_panel
197 .as_ref()
198 .map(|(dt, _)| *dt != self.elapsed)
199 .unwrap_or(true)
200 {
201 self.timeseries_panel = Some((self.elapsed, self.make_timeseries_panel(ctx)));
202 }
203 } else {
204 self.timeseries_panel = None;
205 }
206
207 if let Some((_, ref mut p)) = self.timeseries_panel {
208 if let Outcome::Clicked(_) = p.event(ctx) {
209 unreachable!()
210 }
211 }
212
213 if !self.controls.is_checked("paused") {
216 ctx.request_update(UpdateType::Game);
217 }
218
219 Transition::Keep
220 }
221
222 fn draw(&self, g: &mut GfxCtx, _: &App) {
223 g.clear(Color::BLACK);
224
225 if self.controls.is_checked("Draw scrollable canvas") {
226 g.redraw(&self.scrollable_canvas);
227 }
228
229 self.controls.draw(g);
230
231 if let Some((_, ref p)) = self.timeseries_panel {
232 p.draw(g);
233 }
234
235 g.redraw(&self.texture_demo);
236 }
237}
238
239fn setup_texture_demo(ctx: &mut EventCtx, bg_texture: Texture, fg_texture: Texture) -> Drawable {
240 let mut batch = GeomBatch::new();
241
242 let mut rect = Polygon::rectangle(100.0, 100.0);
243 rect = rect.translate(200.0, 900.0);
244 batch.push(Texture::NOOP, rect);
247
248 let triangle = geom::Triangle {
249 pt1: Pt2D::new(0.0, 100.0),
250 pt2: Pt2D::new(50.0, 0.0),
251 pt3: Pt2D::new(100.0, 100.0),
252 };
253 let mut triangle_poly = Polygon::from_triangle(&triangle);
254 triangle_poly = triangle_poly.translate(400.0, 900.0);
255 batch.push(bg_texture, triangle_poly);
256
257 let circle = geom::Circle::new(Pt2D::new(50.0, 50.0), geom::Distance::meters(50.0));
258 let mut circle_poly = circle.to_polygon();
259 circle_poly = circle_poly.translate(600.0, 900.0);
260 batch.push(
261 Fill::ColoredTexture(Color::RED, bg_texture),
262 circle_poly.clone(),
263 );
264 batch.push(fg_texture, circle_poly);
265
266 batch.upload(ctx)
267}
268
269fn setup_scrollable_canvas(ctx: &mut EventCtx) -> Drawable {
272 let mut batch = GeomBatch::new();
273 batch.push(
274 Color::hex("#4E30A6"),
275 Polygon::rounded_rectangle(5000.0, 5000.0, 25.0),
276 );
277 batch
279 .append(GeomBatch::load_svg(ctx, "system/assets/pregame/logo.svg").translate(300.0, 300.0));
280 batch.append(
282 Text::from(Line("Awesome vector text thanks to usvg and lyon").fg(Color::hex("#DF8C3D")))
283 .render_autocropped(ctx)
284 .scale(2.0)
285 .centered_on(Pt2D::new(600.0, 500.0))
286 .rotate(Angle::degrees(-30.0)),
287 );
288
289 let mut rng = if cfg!(target_arch = "wasm32") {
290 XorShiftRng::seed_from_u64(0)
291 } else {
292 XorShiftRng::from_entropy()
293 };
294 for i in 0..10 {
295 let mut svg_data = Vec::new();
296 svg_face::generate_face(&mut svg_data, &mut rng).unwrap();
297 let face = GeomBatch::load_svg_bytes_uncached(&svg_data).autocrop();
298 let dims = face.get_dims();
299 batch.append(
300 face.scale((200.0 / dims.width).min(200.0 / dims.height))
301 .translate(250.0 * (i as f64), 0.0),
302 );
303 }
304
305 ctx.canvas.map_dims = (5000.0, 5000.0);
307 batch.upload(ctx)
308}
309
310fn make_tabs(ctx: &mut EventCtx) -> TabController {
311 let style = ctx.style();
312
313 let mut tabs = TabController::new("demo_tabs");
314
315 let gallery_bar_item = style.btn_tab.text("Component Gallery");
316 let gallery_content = Widget::col(vec![
317 Text::from(Line("Text").big_heading_styled().size(18)).into_widget(ctx),
318 Text::from_all(vec![
319 Line("You can "),
320 Line("change fonts ").big_heading_plain(),
321 Line("on the same ").small().fg(Color::BLUE),
322 Line("line!").small_heading(),
323 ])
324 .bg(Color::GREEN)
325 .into_widget(ctx),
326 Text::from(Line("Buttons").big_heading_styled().size(18)).into_widget(ctx),
328 Widget::row(vec![
329 style
330 .btn_solid_primary
331 .text("Primary")
332 .build_widget(ctx, "btn_solid_primary_text"),
333 Widget::row(vec![
334 style
335 .btn_solid_primary
336 .icon("system/assets/tools/map.svg")
337 .build_widget(ctx, "btn_solid_primary_icon"),
338 style
339 .btn_plain_primary
340 .icon("system/assets/tools/map.svg")
341 .build_widget(ctx, "btn_plain_primary_icon"),
342 ]),
343 style
344 .btn_solid_primary
345 .icon_text("system/assets/tools/location.svg", "Primary")
346 .build_widget(ctx, "btn_solid_primary_icon_text"),
347 ]),
348 Widget::row(vec![
349 style
350 .btn_outline
351 .text("Secondary")
352 .build_widget(ctx, "btn_outline_text"),
353 Widget::row(vec![
354 style
355 .btn_outline
356 .icon("system/assets/tools/map.svg")
357 .build_widget(ctx, "btn_outline_icon"),
358 style
359 .btn_plain
360 .icon("system/assets/tools/map.svg")
361 .build_widget(ctx, "btn_plain_icon"),
362 ]),
363 style
364 .btn_outline
365 .icon_text("system/assets/tools/home.svg", "Secondary")
366 .build_widget(ctx, "btn_outline.icon_text"),
367 ]),
368 Widget::row(vec![style
369 .btn_popup_icon_text("system/assets/tools/map.svg", "Popup")
370 .build_widget(ctx, "btn_popup_icon_text")]),
371 Text::from_multiline(vec![
372 Line("Images").big_heading_styled().size(18),
373 Line(
374 "Images can be colored, scaled, and stretched. They can have a background and \
375 padding.",
376 ),
377 ])
378 .into_widget(ctx),
379 Widget::row(vec![
380 Image::from_path("system/assets/tools/home.svg").into_widget(ctx),
381 Image::from_path("system/assets/tools/home.svg")
382 .color(Color::ORANGE)
383 .bg_color(Color::BLACK)
384 .dims(50.0)
385 .into_widget(ctx),
386 Image::from_path("system/assets/tools/home.svg")
387 .color(Color::RED)
388 .bg_color(Color::BLACK)
389 .padding(20)
390 .dims(ScreenDims::new(50.0, 100.0))
391 .content_mode(ContentMode::ScaleAspectFit)
392 .tooltip(
393 "With ScaleAspectFit content grows, without distorting its aspect ratio, \
394 until it reaches its padding bounds.",
395 )
396 .into_widget(ctx),
397 Image::from_path("system/assets/tools/home.svg")
398 .color(Color::GREEN)
399 .bg_color(Color::PURPLE)
400 .padding(20)
401 .dims(ScreenDims::new(50.0, 100.0))
402 .content_mode(ContentMode::ScaleToFill)
403 .tooltip("With ScaleToFill content can stretches to fill its size (less padding)")
404 .into_widget(ctx),
405 Image::from_path("system/assets/tools/home.svg")
406 .color(Color::BLUE)
407 .bg_color(Color::YELLOW)
408 .padding(20)
409 .dims(ScreenDims::new(50.0, 100.0))
410 .content_mode(ContentMode::ScaleAspectFill)
411 .tooltip("With ScaleAspectFill content can exceed its visible bounds")
412 .into_widget(ctx),
413 ]),
414 Text::from(Line("Spinner").big_heading_styled().size(18)).into_widget(ctx),
415 widgetry::Spinner::widget(ctx, "spinner", (0, 11), 1, 1),
416 Text::from(Line("Drag & Drop Cards").big_heading_styled().size(18)).into_widget(ctx),
417 build_drag_drop(ctx, 5).into_widget(ctx),
418 ]);
419 tabs.push_tab(gallery_bar_item, gallery_content);
420
421 let qa_bar_item = style.btn_tab.text("Conformance Checks");
422 let qa_content = Widget::col(vec![
423 Text::from(
424 Line("Controls should be same height")
425 .big_heading_styled()
426 .size(18),
427 )
428 .into_widget(ctx),
429 {
430 let row_height = 10;
431 let mut id = 0;
432 let mut next_id = || {
433 id += 1;
434 format!("btn_height_check_{}", id)
435 };
436 Widget::row(vec![
437 Widget::col(
438 (0..row_height)
439 .map(|_| {
440 style
441 .btn_outline
442 .icon("system/assets/tools/layers.svg")
443 .build_widget(ctx, &next_id())
444 })
445 .collect::<Vec<_>>(),
446 ),
447 Widget::col(
448 (0..row_height)
449 .map(|_| style.btn_outline.text("text").build_widget(ctx, &next_id()))
450 .collect::<Vec<_>>(),
451 ),
452 Widget::col(
453 (0..row_height)
454 .map(|_| {
455 style
456 .btn_outline
457 .icon_text("system/assets/tools/layers.svg", "icon+text")
458 .build_widget(ctx, &next_id())
459 })
460 .collect::<Vec<_>>(),
461 ),
462 Widget::col(
463 (0..row_height)
464 .map(|_| {
465 style
466 .btn_popup_icon_text("system/assets/tools/layers.svg", "icon+text")
467 .build_widget(ctx, &next_id())
468 })
469 .collect::<Vec<_>>(),
470 ),
471 Widget::col(
472 (0..row_height)
473 .map(|_| {
474 style
475 .btn_outline
476 .popup("popup")
477 .build_widget(ctx, &next_id())
478 })
479 .collect::<Vec<_>>(),
480 ),
481 Widget::col(
482 (0..row_height)
483 .map(|i| {
484 widgetry::Spinner::widget(ctx, format!("spinner {}", i), (0, 11), 1, 1)
485 })
486 .collect::<Vec<_>>(),
487 ),
488 Widget::col(
489 (0..row_height)
490 .map(|_| widgetry::Toggle::checkbox(ctx, "checkbox", None, true))
491 .collect::<Vec<_>>(),
492 ),
493 ])
494 },
495 ]);
496
497 tabs.push_tab(qa_bar_item, qa_content);
498
499 tabs
500}
501
502fn make_controls(ctx: &mut EventCtx, tabs: &mut TabController) -> Panel {
503 Panel::new_builder(Widget::col(vec![
504 Text::from(Line("widgetry demo").big_heading_styled()).into_widget(ctx),
505 Widget::col(vec![
506 Text::from(
507 "Click and drag the background to pan, use touchpad or scroll wheel to zoom",
508 )
509 .into_widget(ctx),
510 Widget::row(vec![
511 ctx.style()
512 .btn_outline
513 .text("New faces")
514 .hotkey(Key::F)
515 .build_widget(ctx, "generate new faces"),
516 Toggle::switch(ctx, "Draw scrollable canvas", None, true),
517 Toggle::switch(ctx, "Show timeseries", lctrl(Key::T), false),
518 ]),
519 "Stopwatch: ..."
520 .text_widget(ctx)
521 .named("stopwatch")
522 .margin_above(30),
523 Widget::row(vec![
524 Toggle::new_widget(
525 false,
526 ctx.style()
527 .btn_outline
528 .text("Pause")
529 .hotkey(Key::Space)
530 .build(ctx, "pause the stopwatch"),
531 ctx.style()
532 .btn_outline
533 .text("Resume")
534 .hotkey(Key::Space)
535 .build(ctx, "resume the stopwatch"),
536 )
537 .named("paused"),
538 PersistentSplit::widget(
539 ctx,
540 "adjust timer",
541 Duration::seconds(20.0),
542 None,
543 vec![
544 Choice::new("+20s", Duration::seconds(20.0)),
545 Choice::new("-10s", Duration::seconds(-10.0)),
546 ],
547 ),
548 ctx.style()
549 .btn_outline
550 .text("Reset Timer")
551 .build_widget(ctx, "reset the stopwatch"),
552 ])
553 .evenly_spaced(),
554 Widget::row(vec![
555 Widget::dropdown(
556 ctx,
557 "alignment",
558 (HorizontalAlignment::Center, VerticalAlignment::Top),
559 vec![
560 Choice::new("Top", (HorizontalAlignment::Center, VerticalAlignment::Top)),
561 Choice::new(
562 "Left",
563 (HorizontalAlignment::Left, VerticalAlignment::Center),
564 ),
565 Choice::new(
566 "Bottom",
567 (HorizontalAlignment::Center, VerticalAlignment::Bottom),
568 ),
569 Choice::new(
570 "Right",
571 (HorizontalAlignment::Right, VerticalAlignment::Center),
572 ),
573 Choice::new(
574 "Center",
575 (HorizontalAlignment::Center, VerticalAlignment::Center),
576 ),
577 ],
578 ),
579 Widget::dropdown(
580 ctx,
581 "texture",
582 (Texture::SAND, Texture::CACTUS),
583 vec![
584 Choice::new("Cold", (Texture::SNOW, Texture::SNOW_PERSON)),
585 Choice::new("Hot", (Texture::SAND, Texture::CACTUS)),
586 ],
587 ),
588 ctx.style()
589 .btn_solid_primary
590 .text("Apply")
591 .build_widget(ctx, "apply"),
592 ])
593 .margin_above(30),
594 ])
595 .section(ctx),
596 tabs.build_widget(ctx),
597 ])) .aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
599 .build(ctx)
600}
601
602fn build_drag_drop(ctx: &EventCtx, num_cards: usize) -> DragDrop<usize> {
603 fn build_card(ctx: &EventCtx, num: usize) -> (ScreenDims, GeomBatch, GeomBatch, GeomBatch) {
604 let dims = ScreenDims::new(100.0 + (num % 2) as f64 * 50.0, 150.0);
605
606 let mut default_batch = GeomBatch::new();
607 default_batch.push(Color::ORANGE, Polygon::rectangle(dims.width, dims.height));
608 default_batch.append(Text::from(format!("Card {}", num)).render(ctx));
609
610 let mut hovering_batch = GeomBatch::new();
611 hovering_batch.push(Color::RED, Polygon::rectangle(dims.width, dims.height));
612 hovering_batch.append(Text::from(format!("Card {}", num)).render(ctx));
613
614 let mut selected_batch = GeomBatch::new();
615 selected_batch.push(Color::BLUE, Polygon::rectangle(dims.width, dims.height));
616 selected_batch.append(Text::from(format!("Card {}", num)).render(ctx));
617
618 (dims, default_batch, hovering_batch, selected_batch)
619 }
620
621 let mut drag_drop = DragDrop::new(ctx, "drag and drop cards", StackAxis::Horizontal);
622 for i in 0..num_cards {
623 let (dims, default_batch, hovering_batch, selected_batch) = build_card(ctx, i);
624 drag_drop.push_card(i, dims, default_batch, hovering_batch, selected_batch);
625 }
626 drag_drop
627}
628
629#[cfg(target_arch = "wasm32")]
632use wasm_bindgen::prelude::*;
633
634#[cfg(target_arch = "wasm32")]
635#[wasm_bindgen(js_name = "run")]
636pub fn run_wasm(root_dom_id: String, assets_base_url: String, assets_are_gzipped: bool) {
637 let settings = Settings::new("widgetry demo")
638 .root_dom_element_id(root_dom_id)
639 .assets_base_url(assets_base_url)
640 .assets_are_gzipped(assets_are_gzipped);
641 run(settings);
642}