express many ezgui widgets as a generic trait.

- prepares the API for anyone to implement widgets
- cleans up boilerplate code
- gets rid of hacks with Plot<T>
This commit is contained in:
Dustin Carlino 2020-03-22 14:06:02 -07:00
parent 2712ea8c74
commit 2b2b30a6bb
14 changed files with 383 additions and 354 deletions

View File

@ -15,6 +15,7 @@ profiler = ["cpuprofiler"]
abstutil = { path = "../abstutil" }
# backtrace = "0.3.40"
cpuprofiler = { version = "0.0.3", optional = true }
downcast-rs = "1.1.1"
geom = { path = "../geom" }
glium = { version = "0.26.0", optional = true }
glow = { version = "0.4.0", optional = true, default-features=false }

View File

@ -47,7 +47,7 @@ pub(crate) use crate::widgets::popup_menu::PopupMenu;
pub use crate::widgets::slider::{ItemSlider, Slider, WarpingItemSlider};
pub(crate) use crate::widgets::text_box::TextBox;
pub use crate::widgets::wizard::{Choice, Wizard, WrappedWizard};
pub(crate) use crate::widgets::WidgetImpl;
pub use crate::widgets::WidgetImpl;
pub enum InputResult<T: Clone> {
Canceled,

View File

@ -1,10 +1,10 @@
use crate::widgets::plot::Yvalue;
use crate::{
Btn, Button, Checkbox, Choice, Color, Drawable, Dropdown, EventCtx, Filler, GeomBatch, GfxCtx,
Histogram, HorizontalAlignment, JustDraw, MultiKey, Plot, PopupMenu, RewriteColor, ScreenDims,
ScreenPt, ScreenRectangle, Slider, Text, TextBox, VerticalAlignment, WidgetImpl,
};
use abstutil::Cloneable;
use geom::{Distance, Duration, Polygon};
use geom::{Distance, Polygon};
use std::collections::HashSet;
use stretch::geometry::{Rect, Size};
use stretch::node::{Node, Stretch};
@ -18,27 +18,17 @@ pub struct Widget {
style: LayoutStyle,
rect: ScreenRectangle,
bg: Option<Drawable>,
// TODO Only use this, not the other things
id: Option<String>,
}
enum WidgetType {
Draw(JustDraw),
Btn(Button),
Checkbox(Checkbox),
TextBox(TextBox),
Dropdown(Dropdown),
Slider(Slider),
Menu(PopupMenu<Box<dyn Cloneable>>),
Filler(Filler),
// TODO Sadness. Can't have some kind of wildcard generic here? I think this goes away when
// WidgetType becomes a trait.
DurationPlot(Plot<Duration>),
UsizePlot(Plot<usize>),
Histogram(Histogram),
Row(Vec<Widget>),
Column(Vec<Widget>),
Nothing,
Generic(Box<dyn WidgetImpl>),
}
struct LayoutStyle {
@ -242,7 +232,7 @@ impl Widget {
}
pub(crate) fn just_draw(j: JustDraw) -> Widget {
Widget::new(WidgetType::Draw(j))
Widget::new(WidgetType::Generic(Box::new(j)))
}
pub(crate) fn draw_text(ctx: &EventCtx, txt: Text) -> Widget {
@ -266,12 +256,12 @@ impl Widget {
Widget::new(WidgetType::Slider(slider))
}
pub fn menu(menu: PopupMenu<Box<dyn Cloneable>>) -> Widget {
Widget::new(WidgetType::Menu(menu))
pub fn menu<T: 'static + Clone>(menu: PopupMenu<T>) -> Widget {
Widget::new(WidgetType::Generic(Box::new(menu)))
}
pub fn filler(filler: Filler) -> Widget {
Widget::new(WidgetType::Filler(filler))
Widget::new(WidgetType::Generic(Box::new(filler)))
}
pub fn checkbox(
@ -290,21 +280,21 @@ impl Widget {
}
// TODO Not typesafe! Gotta pass a button.
pub fn custom_checkbox(enabled: bool, false_btn: Widget, true_btn: Widget) -> Widget {
Widget::new(WidgetType::Checkbox(Checkbox::new(
Widget::new(WidgetType::Generic(Box::new(Checkbox::new(
enabled,
false_btn.take_btn(),
true_btn.take_btn(),
)))
))))
}
pub fn text_entry(ctx: &EventCtx, prefilled: String, exclusive_focus: bool) -> Widget {
// TODO Hardcoded style, max chars
Widget::new(WidgetType::TextBox(TextBox::new(
Widget::new(WidgetType::Generic(Box::new(TextBox::new(
ctx,
50,
prefilled,
exclusive_focus,
)))
))))
}
pub fn dropdown<T: 'static + PartialEq>(
@ -323,16 +313,12 @@ impl Widget {
.outline(2.0, Color::WHITE)
}
pub(crate) fn duration_plot(plot: Plot<Duration>) -> Widget {
Widget::new(WidgetType::DurationPlot(plot))
}
pub(crate) fn usize_plot(plot: Plot<usize>) -> Widget {
Widget::new(WidgetType::UsizePlot(plot))
pub(crate) fn plot<T: 'static + Yvalue<T>>(plot: Plot<T>) -> Widget {
Widget::new(WidgetType::Generic(Box::new(plot)))
}
pub(crate) fn histogram(histogram: Histogram) -> Widget {
Widget::new(WidgetType::Histogram(histogram))
Widget::new(WidgetType::Generic(Box::new(histogram)))
}
pub fn row(widgets: Vec<Widget>) -> Widget {
@ -368,21 +354,12 @@ impl Widget {
impl Widget {
fn event(&mut self, ctx: &mut EventCtx, redo_layout: &mut bool) -> Option<Outcome> {
match self.widget {
WidgetType::Draw(_) => {}
WidgetType::Btn(ref mut btn) => {
btn.event(ctx);
if btn.clicked() {
return Some(Outcome::Clicked(btn.action.clone()));
}
}
WidgetType::Checkbox(ref mut checkbox) => {
if checkbox.event(ctx) {
*redo_layout = true;
}
}
WidgetType::TextBox(ref mut textbox) => {
textbox.event(ctx);
}
WidgetType::Dropdown(ref mut dropdown) => {
if dropdown.event(ctx, &self.rect) {
*redo_layout = true;
@ -391,13 +368,6 @@ impl Widget {
WidgetType::Slider(ref mut slider) => {
slider.event(ctx);
}
WidgetType::Menu(ref mut menu) => {
menu.event(ctx);
}
WidgetType::Filler(_)
| WidgetType::DurationPlot(_)
| WidgetType::UsizePlot(_)
| WidgetType::Histogram(_) => {}
WidgetType::Row(ref mut widgets) | WidgetType::Column(ref mut widgets) => {
for w in widgets {
if let Some(o) = w.event(ctx, redo_layout) {
@ -406,6 +376,11 @@ impl Widget {
}
}
WidgetType::Nothing => unreachable!(),
WidgetType::Generic(ref mut w) => {
if let Some(o) = w.event(ctx, &self.rect, redo_layout) {
return Some(o);
}
}
}
None
}
@ -416,10 +391,7 @@ impl Widget {
}
match self.widget {
WidgetType::Draw(ref j) => j.draw(g),
WidgetType::Btn(ref btn) => btn.draw(g),
WidgetType::Checkbox(ref checkbox) => checkbox.draw(g),
WidgetType::TextBox(ref textbox) => textbox.draw(g),
WidgetType::Dropdown(ref dropdown) => dropdown.draw(g),
WidgetType::Slider(ref slider) => {
if self.id != Some("horiz scrollbar".to_string())
@ -428,35 +400,22 @@ impl Widget {
slider.draw(g);
}
}
WidgetType::Menu(ref menu) => menu.draw(g),
WidgetType::Filler(_) => {}
WidgetType::DurationPlot(ref plot) => plot.draw(g),
WidgetType::UsizePlot(ref plot) => plot.draw(g),
WidgetType::Histogram(ref hgram) => hgram.draw(g),
WidgetType::Row(ref widgets) | WidgetType::Column(ref widgets) => {
for w in widgets {
w.draw(g);
}
}
WidgetType::Nothing => unreachable!(),
WidgetType::Generic(ref w) => w.draw(g),
}
}
// Populate a flattened list of Nodes, matching the traversal order
fn get_flexbox(&self, parent: Node, stretch: &mut Stretch, nodes: &mut Vec<Node>) {
// TODO Can I use | in the match and "cast" to Widget?
let widget: &dyn WidgetImpl = match self.widget {
WidgetType::Draw(ref widget) => widget,
WidgetType::Btn(ref widget) => widget,
WidgetType::Checkbox(ref widget) => widget,
WidgetType::TextBox(ref widget) => widget,
WidgetType::Dropdown(ref widget) => widget,
WidgetType::Slider(ref widget) => widget,
WidgetType::Menu(ref widget) => widget,
WidgetType::Filler(ref widget) => widget,
WidgetType::DurationPlot(ref widget) => widget,
WidgetType::UsizePlot(ref widget) => widget,
WidgetType::Histogram(ref widget) => widget,
let dims = match self.widget {
WidgetType::Btn(ref widget) => widget.get_dims(),
WidgetType::Dropdown(ref widget) => widget.get_dims(),
WidgetType::Slider(ref widget) => widget.get_dims(),
WidgetType::Row(ref widgets) => {
let mut style = Style {
flex_direction: FlexDirection::Row,
@ -486,8 +445,8 @@ impl Widget {
return;
}
WidgetType::Nothing => unreachable!(),
WidgetType::Generic(ref w) => w.get_dims(),
};
let dims = widget.get_dims();
let mut style = Style {
size: Size {
width: Dimension::Points(dims.width as f32),
@ -516,18 +475,13 @@ impl Widget {
let y: f64 = result.location.y.into();
let width: f64 = result.size.width.into();
let height: f64 = result.size.height.into();
let top_left = match self.widget {
WidgetType::Slider(_) => {
// Don't scroll the scrollbars
if self.id == Some("horiz scrollbar".to_string())
|| self.id == Some("vert scrollbar".to_string())
{
ScreenPt::new(x, y)
} else {
ScreenPt::new(x + dx - scroll_offset.0, y + dy - scroll_offset.1)
}
}
_ => ScreenPt::new(x + dx - scroll_offset.0, y + dy - scroll_offset.1),
// Don't scroll the scrollbars
let top_left = if self.id == Some("horiz scrollbar".to_string())
|| self.id == Some("vert scrollbar".to_string())
{
ScreenPt::new(x, y)
} else {
ScreenPt::new(x + dx - scroll_offset.0, y + dy - scroll_offset.1)
};
self.rect = ScreenRectangle::top_left(top_left, ScreenDims::new(width, height));
@ -550,39 +504,15 @@ impl Widget {
}
match self.widget {
WidgetType::Draw(ref mut widget) => {
widget.set_pos(top_left);
}
WidgetType::Btn(ref mut widget) => {
widget.set_pos(top_left);
}
WidgetType::Checkbox(ref mut widget) => {
widget.set_pos(top_left);
}
WidgetType::TextBox(ref mut widget) => {
widget.set_pos(top_left);
}
WidgetType::Dropdown(ref mut widget) => {
widget.set_pos(top_left);
}
WidgetType::Slider(ref mut widget) => {
widget.set_pos(top_left);
}
WidgetType::Menu(ref mut widget) => {
widget.set_pos(top_left);
}
WidgetType::Filler(ref mut widget) => {
widget.set_pos(top_left);
}
WidgetType::DurationPlot(ref mut widget) => {
widget.set_pos(top_left);
}
WidgetType::UsizePlot(ref mut widget) => {
widget.set_pos(top_left);
}
WidgetType::Histogram(ref mut widget) => {
widget.set_pos(top_left);
}
WidgetType::Row(ref mut widgets) => {
// layout() doesn't return absolute position; it's relative to the container.
for widget in widgets {
@ -611,21 +541,15 @@ impl Widget {
}
}
WidgetType::Nothing => unreachable!(),
WidgetType::Generic(ref mut w) => {
w.set_pos(top_left);
}
}
}
fn get_all_click_actions(&self, actions: &mut HashSet<String>) {
match self.widget {
WidgetType::Draw(_)
| WidgetType::Slider(_)
| WidgetType::Menu(_)
| WidgetType::Filler(_)
| WidgetType::Checkbox(_)
| WidgetType::TextBox(_)
| WidgetType::Dropdown(_)
| WidgetType::DurationPlot(_)
| WidgetType::UsizePlot(_) => {}
WidgetType::Histogram(_) => {}
WidgetType::Slider(_) | WidgetType::Dropdown(_) => {}
WidgetType::Btn(ref btn) => {
if actions.contains(&btn.action) {
panic!(
@ -641,6 +565,8 @@ impl Widget {
}
}
WidgetType::Nothing => unreachable!(),
// TODO Will need something
WidgetType::Generic(_) => {}
}
}
@ -950,23 +876,41 @@ impl Composite {
}
}
pub fn menu(&self, name: &str) -> &PopupMenu<Box<dyn Cloneable>> {
pub fn menu<T: 'static + Clone>(&self, name: &str) -> &PopupMenu<T> {
match self.find(name).widget {
WidgetType::Menu(ref menu) => menu,
WidgetType::Generic(ref w) => {
if let Some(menu) = w.downcast_ref::<PopupMenu<T>>() {
menu
} else {
panic!("{} isn't a menu", name);
}
}
_ => panic!("{} isn't a menu", name),
}
}
pub fn is_checked(&self, name: &str) -> bool {
match self.find(name).widget {
WidgetType::Checkbox(ref checkbox) => checkbox.enabled,
WidgetType::Generic(ref w) => {
if let Some(checkbox) = w.downcast_ref::<Checkbox>() {
checkbox.enabled
} else {
panic!("{} isn't a checkbox", name);
}
}
_ => panic!("{} isn't a checkbox", name),
}
}
pub fn text_box(&self, name: &str) -> String {
match self.find(name).widget {
WidgetType::TextBox(ref textbox) => textbox.get_entry(),
WidgetType::Generic(ref w) => {
if let Some(tb) = w.downcast_ref::<TextBox>() {
tb.get_line()
} else {
panic!("{} isn't a textbox", name);
}
}
_ => panic!("{} isn't a textbox", name),
}
}
@ -986,8 +930,15 @@ impl Composite {
}
pub fn filler_rect(&self, name: &str) -> ScreenRectangle {
match self.find(name).widget {
WidgetType::Filler(ref f) => ScreenRectangle::top_left(f.top_left, f.dims),
let widget = self.find(name);
match widget.widget {
WidgetType::Generic(ref w) => {
if let Some(_) = w.downcast_ref::<Filler>() {
widget.rect.clone()
} else {
panic!("{} isn't a filler", name);
}
}
_ => panic!("{} isn't a filler", name),
}
}

View File

@ -1,4 +1,4 @@
use crate::{Button, EventCtx, GfxCtx, ScreenDims, ScreenPt, WidgetImpl};
use crate::{Button, EventCtx, GfxCtx, Outcome, ScreenDims, ScreenPt, ScreenRectangle, WidgetImpl};
pub struct Checkbox {
pub(crate) enabled: bool,
@ -22,23 +22,6 @@ impl Checkbox {
}
}
}
// If true, widgets should be recomputed.
pub(crate) fn event(&mut self, ctx: &mut EventCtx) -> bool {
self.btn.event(ctx);
if self.btn.clicked() {
std::mem::swap(&mut self.btn, &mut self.other_btn);
self.btn.set_pos(self.other_btn.top_left);
self.enabled = !self.enabled;
true
} else {
false
}
}
pub(crate) fn draw(&self, g: &mut GfxCtx) {
self.btn.draw(g);
}
}
impl WidgetImpl for Checkbox {
@ -49,4 +32,25 @@ impl WidgetImpl for Checkbox {
fn set_pos(&mut self, top_left: ScreenPt) {
self.btn.set_pos(top_left);
}
fn event(
&mut self,
ctx: &mut EventCtx,
_rect: &ScreenRectangle,
redo_layout: &mut bool,
) -> Option<Outcome> {
self.btn.event(ctx);
if self.btn.clicked() {
std::mem::swap(&mut self.btn, &mut self.other_btn);
self.btn.set_pos(self.other_btn.top_left);
self.enabled = !self.enabled;
*redo_layout = true;
}
None
}
fn draw(&self, g: &mut GfxCtx) {
self.btn.draw(g);
}
}

View File

@ -52,7 +52,9 @@ impl Dropdown {
// If true, widgets should be recomputed.
pub fn event(&mut self, ctx: &mut EventCtx, our_rect: &ScreenRectangle) -> bool {
if let Some(ref mut m) = self.menu {
m.event(ctx);
// TODO wraaaaaaaaaaaawng
let mut redo_layout = false;
m.event(ctx, our_rect, &mut redo_layout);
match m.state {
InputResult::StillActive => {}
InputResult::Canceled => {

View File

@ -1,10 +1,10 @@
use crate::{ScreenDims, ScreenPt, WidgetImpl};
use crate::{EventCtx, GfxCtx, Outcome, ScreenDims, ScreenPt, ScreenRectangle, WidgetImpl};
// Doesn't do anything by itself, just used for widgetsing. Something else reaches in, asks for the
// ScreenRectangle to use.
pub struct Filler {
pub(crate) top_left: ScreenPt,
pub(crate) dims: ScreenDims,
top_left: ScreenPt,
dims: ScreenDims,
}
impl Filler {
@ -24,4 +24,14 @@ impl WidgetImpl for Filler {
fn set_pos(&mut self, top_left: ScreenPt) {
self.top_left = top_left;
}
fn event(
&mut self,
_ctx: &mut EventCtx,
_rect: &ScreenRectangle,
_redo_layout: &mut bool,
) -> Option<Outcome> {
None
}
fn draw(&self, _g: &mut GfxCtx) {}
}

View File

@ -1,6 +1,6 @@
use crate::{
Color, Drawable, EventCtx, GeomBatch, GfxCtx, Line, ScreenDims, ScreenPt, Text, TextExt,
Widget, WidgetImpl,
Color, Drawable, EventCtx, GeomBatch, GfxCtx, Line, Outcome, ScreenDims, ScreenPt,
ScreenRectangle, Text, TextExt, Widget, WidgetImpl,
};
use abstutil::prettyprint_usize;
use geom::{Distance, Duration, Polygon, Pt2D};
@ -99,8 +99,26 @@ impl Histogram {
x_axis.evenly_spaced(),
])])
}
}
pub(crate) fn draw(&self, g: &mut GfxCtx) {
impl WidgetImpl for Histogram {
fn get_dims(&self) -> ScreenDims {
self.dims
}
fn set_pos(&mut self, top_left: ScreenPt) {
self.top_left = top_left;
}
fn event(
&mut self,
_ctx: &mut EventCtx,
_rect: &ScreenRectangle,
_redo_layout: &mut bool,
) -> Option<Outcome> {
None
}
fn draw(&self, g: &mut GfxCtx) {
g.redraw_at(self.top_left, &self.draw);
if let Some(cursor) = g.canvas.get_cursor_in_screen_space() {
@ -115,16 +133,6 @@ impl Histogram {
}
}
impl WidgetImpl for Histogram {
fn get_dims(&self) -> ScreenDims {
self.dims
}
fn set_pos(&mut self, top_left: ScreenPt) {
self.top_left = top_left;
}
}
// min, max, bars
// TODO This has bugs. Perfect surface area to unit test.
fn bucketize(

View File

@ -12,14 +12,27 @@ pub mod slider;
pub mod text_box;
pub mod wizard;
use crate::{EventCtx, ScreenDims, ScreenPt};
use crate::{EventCtx, GfxCtx, Outcome, ScreenDims, ScreenPt, ScreenRectangle};
use ordered_float::NotNan;
pub trait WidgetImpl {
pub trait WidgetImpl: downcast_rs::Downcast {
fn get_dims(&self) -> ScreenDims;
fn set_pos(&mut self, top_left: ScreenPt);
// TODO Require everyone to implement it
fn event(
&mut self,
_ctx: &mut EventCtx,
_rect: &ScreenRectangle,
_redo_layout: &mut bool,
) -> Option<Outcome> {
None
}
fn draw(&self, _g: &mut GfxCtx) {}
}
downcast_rs::impl_downcast!(WidgetImpl);
#[derive(Clone, Copy)]
pub enum ContainerOrientation {
TopLeft,

View File

@ -1,6 +1,6 @@
use crate::{
svg, Drawable, EventCtx, GeomBatch, GfxCtx, RewriteColor, ScreenDims, ScreenPt, Text, Widget,
WidgetImpl,
svg, Drawable, EventCtx, GeomBatch, GfxCtx, Outcome, RewriteColor, ScreenDims, ScreenPt,
ScreenRectangle, Text, Widget, WidgetImpl,
};
// Just draw something. A widget just so widgetsing works.
@ -43,10 +43,6 @@ impl JustDraw {
pub fn text(ctx: &EventCtx, text: Text) -> Widget {
JustDraw::wrap(ctx, text.render_ctx(ctx))
}
pub(crate) fn draw(&self, g: &mut GfxCtx) {
g.redraw_at(self.top_left, &self.draw);
}
}
impl WidgetImpl for JustDraw {
@ -57,4 +53,17 @@ impl WidgetImpl for JustDraw {
fn set_pos(&mut self, top_left: ScreenPt) {
self.top_left = top_left;
}
fn event(
&mut self,
_ctx: &mut EventCtx,
_rect: &ScreenRectangle,
_redo_layout: &mut bool,
) -> Option<Outcome> {
None
}
fn draw(&self, g: &mut GfxCtx) {
g.redraw_at(self.top_left, &self.draw);
}
}

View File

@ -1,6 +1,6 @@
use crate::{
Color, Drawable, EventCtx, GeomBatch, GfxCtx, Line, ScreenDims, ScreenPt, ScreenRectangle,
Text, TextExt, Widget, WidgetImpl,
Color, Drawable, EventCtx, GeomBatch, GfxCtx, Line, Outcome, ScreenDims, ScreenPt,
ScreenRectangle, Text, TextExt, Widget, WidgetImpl,
};
use abstutil::prettyprint_usize;
use geom::{Angle, Bounds, Circle, Distance, Duration, FindClosest, PolyLine, Pt2D, Time};
@ -198,8 +198,56 @@ impl<T: 'static + Ord + PartialEq + Copy + core::fmt::Debug + Yvalue<T>> Plot<T>
(plot, legend, x_axis, y_axis)
}
}
pub(crate) fn draw(&self, g: &mut GfxCtx) {
// TODO Do we still need these two? :\
impl Plot<usize> {
pub fn new_usize(ctx: &EventCtx, series: Vec<Series<usize>>, opts: PlotOptions) -> Widget {
let (plot, legend, x_axis, y_axis) = Plot::new(ctx, series, 0, opts);
// Don't let the x-axis fill the parent container
Widget::row(vec![Widget::col(vec![
legend,
Widget::row(vec![y_axis.evenly_spaced(), Widget::plot(plot)]),
x_axis.evenly_spaced(),
])])
}
}
impl Plot<Duration> {
pub fn new_duration(
ctx: &EventCtx,
series: Vec<Series<Duration>>,
opts: PlotOptions,
) -> Widget {
let (plot, legend, x_axis, y_axis) = Plot::new(ctx, series, Duration::ZERO, opts);
// Don't let the x-axis fill the parent container
Widget::row(vec![Widget::col(vec![
legend,
Widget::row(vec![y_axis.evenly_spaced(), Widget::plot(plot)]),
x_axis.evenly_spaced(),
])])
}
}
impl<T: 'static + Yvalue<T>> WidgetImpl for Plot<T> {
fn get_dims(&self) -> ScreenDims {
self.dims
}
fn set_pos(&mut self, top_left: ScreenPt) {
self.top_left = top_left;
}
fn event(
&mut self,
_ctx: &mut EventCtx,
_rect: &ScreenRectangle,
_redo_layout: &mut bool,
) -> Option<Outcome> {
None
}
fn draw(&self, g: &mut GfxCtx) {
g.redraw_at(self.top_left, &self.draw);
if let Some(cursor) = g.canvas.get_cursor_in_screen_space() {
@ -233,44 +281,6 @@ impl<T: 'static + Ord + PartialEq + Copy + core::fmt::Debug + Yvalue<T>> Plot<T>
}
}
impl Plot<usize> {
pub fn new_usize(ctx: &EventCtx, series: Vec<Series<usize>>, opts: PlotOptions) -> Widget {
let (plot, legend, x_axis, y_axis) = Plot::new(ctx, series, 0, opts);
// Don't let the x-axis fill the parent container
Widget::row(vec![Widget::col(vec![
legend,
Widget::row(vec![y_axis.evenly_spaced(), Widget::usize_plot(plot)]),
x_axis.evenly_spaced(),
])])
}
}
impl Plot<Duration> {
pub fn new_duration(
ctx: &EventCtx,
series: Vec<Series<Duration>>,
opts: PlotOptions,
) -> Widget {
let (plot, legend, x_axis, y_axis) = Plot::new(ctx, series, Duration::ZERO, opts);
// Don't let the x-axis fill the parent container
Widget::row(vec![Widget::col(vec![
legend,
Widget::row(vec![y_axis.evenly_spaced(), Widget::duration_plot(plot)]),
x_axis.evenly_spaced(),
])])
}
}
impl<T> WidgetImpl for Plot<T> {
fn get_dims(&self) -> ScreenDims {
self.dims
}
fn set_pos(&mut self, top_left: ScreenPt) {
self.top_left = top_left;
}
}
pub trait Yvalue<T> {
// percent is [0.0, 1.0]
fn from_percent(&self, percent: f64) -> T;

View File

@ -1,5 +1,5 @@
use crate::{
hotkey, text, Choice, EventCtx, GfxCtx, InputResult, Key, Line, ScreenDims, ScreenPt,
hotkey, text, Choice, EventCtx, GfxCtx, InputResult, Key, Line, Outcome, ScreenDims, ScreenPt,
ScreenRectangle, Text, WidgetImpl,
};
use geom::Pt2D;
@ -29,118 +29,6 @@ impl<T: Clone> PopupMenu<T> {
m
}
pub fn event(&mut self, ctx: &mut EventCtx) {
match self.state {
InputResult::StillActive => {}
_ => unreachable!(),
}
// Handle the mouse
if ctx.redo_mouseover() {
if let Some(cursor) = ctx.canvas.get_cursor_in_screen_space() {
let mut top_left = self.top_left;
for idx in 0..self.choices.len() {
let rect = ScreenRectangle {
x1: top_left.x,
y1: top_left.y,
x2: top_left.x + self.dims.width,
y2: top_left.y + ctx.default_line_height(),
};
if rect.contains(cursor) {
self.current_idx = idx;
break;
}
top_left.y += ctx.default_line_height();
}
}
}
{
let choice = &self.choices[self.current_idx];
if ctx.normal_left_click() {
// Did we actually click the entry?
let mut top_left = self.top_left;
top_left.y += ctx.default_line_height() * (self.current_idx as f64);
let rect = ScreenRectangle {
x1: top_left.x,
y1: top_left.y,
x2: top_left.x + self.dims.width,
y2: top_left.y + ctx.default_line_height(),
};
if let Some(pt) = ctx.canvas.get_cursor_in_screen_space() {
if rect.contains(pt) && choice.active {
self.state = InputResult::Done(choice.label.clone(), choice.data.clone());
return;
}
// Unconsume the click, it was in screen space, but not on us.
ctx.input.unconsume_event();
} else {
// Clicked on the map? Cancel out
self.state = InputResult::Canceled;
return;
}
}
}
// Handle hotkeys
for choice in &self.choices {
if !choice.active {
continue;
}
if let Some(ref hotkey) = choice.hotkey {
if ctx.input.new_was_pressed(hotkey) {
self.state = InputResult::Done(choice.label.clone(), choice.data.clone());
return;
}
}
}
// Handle nav keys
if ctx.input.new_was_pressed(&hotkey(Key::Enter).unwrap()) {
let choice = &self.choices[self.current_idx];
if choice.active {
self.state = InputResult::Done(choice.label.clone(), choice.data.clone());
return;
} else {
return;
}
} else if ctx.input.new_was_pressed(&hotkey(Key::UpArrow).unwrap()) {
if self.current_idx > 0 {
self.current_idx -= 1;
}
} else if ctx.input.new_was_pressed(&hotkey(Key::DownArrow).unwrap()) {
if self.current_idx < self.choices.len() - 1 {
self.current_idx += 1;
}
}
}
pub fn draw(&self, g: &mut GfxCtx) {
let draw = g.upload(self.calculate_txt().render_g(g));
// In between tooltip and normal screenspace
g.fork(Pt2D::new(0.0, 0.0), self.top_left, 1.0, Some(0.1));
g.redraw(&draw);
g.unfork();
if let Some(ref info) = self.choices[self.current_idx].tooltip {
// Hold on, are we actually hovering on that entry right now?
let mut top_left = self.top_left;
top_left.y += g.default_line_height() * (self.current_idx as f64);
let rect = ScreenRectangle {
x1: top_left.x,
y1: top_left.y,
x2: top_left.x + self.dims.width,
y2: top_left.y + g.default_line_height(),
};
if let Some(pt) = g.canvas.get_cursor_in_screen_space() {
if rect.contains(pt) {
let mut txt = Text::new();
txt.add_wrapped(info.to_string(), 0.5 * g.canvas.window_width);
g.draw_mouse_tooltip(txt);
}
}
}
}
pub fn current_choice(&self) -> &T {
&self.choices[self.current_idx].data
}
@ -182,7 +70,7 @@ impl<T: Clone> PopupMenu<T> {
}
}
impl<T: Clone> WidgetImpl for PopupMenu<T> {
impl<T: 'static + Clone> WidgetImpl for PopupMenu<T> {
fn get_dims(&self) -> ScreenDims {
self.dims
}
@ -190,4 +78,123 @@ impl<T: Clone> WidgetImpl for PopupMenu<T> {
fn set_pos(&mut self, top_left: ScreenPt) {
self.top_left = top_left;
}
fn event(
&mut self,
ctx: &mut EventCtx,
_rect: &ScreenRectangle,
_redo_layout: &mut bool,
) -> Option<Outcome> {
match self.state {
InputResult::StillActive => {}
_ => unreachable!(),
}
// Handle the mouse
if ctx.redo_mouseover() {
if let Some(cursor) = ctx.canvas.get_cursor_in_screen_space() {
let mut top_left = self.top_left;
for idx in 0..self.choices.len() {
let rect = ScreenRectangle {
x1: top_left.x,
y1: top_left.y,
x2: top_left.x + self.dims.width,
y2: top_left.y + ctx.default_line_height(),
};
if rect.contains(cursor) {
self.current_idx = idx;
break;
}
top_left.y += ctx.default_line_height();
}
}
}
{
let choice = &self.choices[self.current_idx];
if ctx.normal_left_click() {
// Did we actually click the entry?
let mut top_left = self.top_left;
top_left.y += ctx.default_line_height() * (self.current_idx as f64);
let rect = ScreenRectangle {
x1: top_left.x,
y1: top_left.y,
x2: top_left.x + self.dims.width,
y2: top_left.y + ctx.default_line_height(),
};
if let Some(pt) = ctx.canvas.get_cursor_in_screen_space() {
if rect.contains(pt) && choice.active {
self.state = InputResult::Done(choice.label.clone(), choice.data.clone());
return None;
}
// Unconsume the click, it was in screen space, but not on us.
ctx.input.unconsume_event();
} else {
// Clicked on the map? Cancel out
self.state = InputResult::Canceled;
return None;
}
}
}
// Handle hotkeys
for choice in &self.choices {
if !choice.active {
continue;
}
if let Some(ref hotkey) = choice.hotkey {
if ctx.input.new_was_pressed(hotkey) {
self.state = InputResult::Done(choice.label.clone(), choice.data.clone());
return None;
}
}
}
// Handle nav keys
if ctx.input.new_was_pressed(&hotkey(Key::Enter).unwrap()) {
let choice = &self.choices[self.current_idx];
if choice.active {
self.state = InputResult::Done(choice.label.clone(), choice.data.clone());
return None;
} else {
return None;
}
} else if ctx.input.new_was_pressed(&hotkey(Key::UpArrow).unwrap()) {
if self.current_idx > 0 {
self.current_idx -= 1;
}
} else if ctx.input.new_was_pressed(&hotkey(Key::DownArrow).unwrap()) {
if self.current_idx < self.choices.len() - 1 {
self.current_idx += 1;
}
}
None
}
fn draw(&self, g: &mut GfxCtx) {
let draw = g.upload(self.calculate_txt().render_g(g));
// In between tooltip and normal screenspace
g.fork(Pt2D::new(0.0, 0.0), self.top_left, 1.0, Some(0.1));
g.redraw(&draw);
g.unfork();
if let Some(ref info) = self.choices[self.current_idx].tooltip {
// Hold on, are we actually hovering on that entry right now?
let mut top_left = self.top_left;
top_left.y += g.default_line_height() * (self.current_idx as f64);
let rect = ScreenRectangle {
x1: top_left.x,
y1: top_left.y,
x2: top_left.x + self.dims.width,
y2: top_left.y + g.default_line_height(),
};
if let Some(pt) = g.canvas.get_cursor_in_screen_space() {
if rect.contains(pt) {
let mut txt = Text::new();
txt.add_wrapped(info.to_string(), 0.5 * g.canvas.window_width);
g.draw_mouse_tooltip(txt);
}
}
}
}
}

View File

@ -1,6 +1,6 @@
use crate::{
text, Color, EventCtx, GeomBatch, GfxCtx, Key, Line, ScreenDims, ScreenPt, ScreenRectangle,
Text, WidgetImpl,
text, Color, EventCtx, GeomBatch, GfxCtx, Key, Line, Outcome, ScreenDims, ScreenPt,
ScreenRectangle, Text, WidgetImpl,
};
use geom::Polygon;
@ -34,7 +34,41 @@ impl TextBox {
}
}
pub fn event(&mut self, ctx: &mut EventCtx) {
fn calculate_text(&self) -> Text {
let mut txt = Text::from(Line(&self.line[0..self.cursor_x]));
if self.cursor_x < self.line.len() {
// TODO This "cursor" looks awful!
txt.append_all(vec![
Line("|").fg(text::SELECTED_COLOR),
Line(&self.line[self.cursor_x..=self.cursor_x]),
Line(&self.line[self.cursor_x + 1..]),
]);
} else {
txt.append(Line("|").fg(text::SELECTED_COLOR));
}
txt
}
pub fn get_line(&self) -> String {
self.line.clone()
}
}
impl WidgetImpl for TextBox {
fn get_dims(&self) -> ScreenDims {
self.dims
}
fn set_pos(&mut self, top_left: ScreenPt) {
self.top_left = top_left;
}
fn event(
&mut self,
ctx: &mut EventCtx,
_rect: &ScreenRectangle,
_redo_layout: &mut bool,
) -> Option<Outcome> {
if ctx.redo_mouseover() {
if let Some(pt) = ctx.canvas.get_cursor_in_screen_space() {
self.hovering = ScreenRectangle::top_left(self.top_left, self.dims).contains(pt);
@ -51,7 +85,7 @@ impl TextBox {
}
if !self.has_focus && !self.autofocus {
return;
return None;
}
if let Some(key) = ctx.input.any_key_pressed() {
match key {
@ -79,9 +113,11 @@ impl TextBox {
}
};
}
None
}
pub fn draw(&self, g: &mut GfxCtx) {
fn draw(&self, g: &mut GfxCtx) {
let mut batch = GeomBatch::from(vec![(
if self.has_focus || self.autofocus {
Color::ORANGE
@ -96,33 +132,4 @@ impl TextBox {
let draw = g.upload(batch);
g.redraw_at(self.top_left, &draw);
}
pub fn get_entry(&self) -> String {
self.line.clone()
}
fn calculate_text(&self) -> Text {
let mut txt = Text::from(Line(&self.line[0..self.cursor_x]));
if self.cursor_x < self.line.len() {
// TODO This "cursor" looks awful!
txt.append_all(vec![
Line("|").fg(text::SELECTED_COLOR),
Line(&self.line[self.cursor_x..=self.cursor_x]),
Line(&self.line[self.cursor_x + 1..]),
]);
} else {
txt.append(Line("|").fg(text::SELECTED_COLOR));
}
txt
}
}
impl WidgetImpl for TextBox {
fn get_dims(&self) -> ScreenDims {
self.dims
}
fn set_pos(&mut self, top_left: ScreenPt) {
self.top_left = top_left;
}
}

View File

@ -57,7 +57,7 @@ impl Wizard {
pub fn current_menu_choice<R: 'static + Cloneable>(&self) -> Option<&R> {
if let Some(ref comp) = self.menu_comp {
let item: &R = comp
.menu("menu")
.menu::<Box<dyn Cloneable>>("menu")
.current_choice()
.as_any()
.downcast_ref::<R>()?;
@ -295,7 +295,14 @@ impl<'a, 'b> WrappedWizard<'a, 'b> {
_ => {}
}
let (result, destroy) = match self.wizard.menu_comp.as_ref().unwrap().menu("menu").state {
let (result, destroy) = match self
.wizard
.menu_comp
.as_ref()
.unwrap()
.menu::<Box<dyn Cloneable>>("menu")
.state
{
InputResult::Canceled => {
self.wizard.alive = false;
(None, true)

View File

@ -15,7 +15,7 @@ aabb-quadtree = "0.1.0"
abstutil = { path = "../abstutil" }
built = { version = "0.4.0", optional = true, features=["chrono"] }
chrono = "0.4.10"
downcast-rs = "1.0.4"
downcast-rs = "1.1.1"
ezgui = { path = "../ezgui", default-features=false }
geom = { path = "../geom" }
instant = "0.1.2"